Add new test file for phase 19 scenery with unique identifier
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
class_name StageBackgroundCardPreview
|
||||
extends Control
|
||||
## Reuses the Stage's Sky and wall-finish drawing rules in gallery cards.
|
||||
|
||||
const SKY_VIEW: Script = preload("res://scripts/stage/scenery/stage_sky_view.gd")
|
||||
const SURFACE_RENDERER: Script = preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
|
||||
var _background_kind: String = ""
|
||||
var _preview_id: StringName = &""
|
||||
var _sky_view: StageSkyView
|
||||
|
||||
|
||||
func configure(entry: Dictionary) -> void:
|
||||
_background_kind = String(entry.get("background_kind", ""))
|
||||
_preview_id = StringName(String(entry.get("id", ""))) if _background_kind == "sky" else StringName(String(entry.get("finish_id", "")))
|
||||
clip_contents = true
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
if _background_kind == "sky":
|
||||
_sky_view = SKY_VIEW.new() as StageSkyView
|
||||
_sky_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_sky_view.set_sky(_preview_id)
|
||||
add_child(_sky_view)
|
||||
var frame: Panel = Panel.new()
|
||||
frame.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var frame_style: StyleBoxFlat = StyleBoxFlat.new()
|
||||
frame_style.bg_color = Color.TRANSPARENT
|
||||
frame_style.draw_center = false
|
||||
frame_style.set_border_width_all(2)
|
||||
frame_style.border_color = Color("#D7DEE7")
|
||||
frame_style.set_corner_radius_all(4)
|
||||
frame.add_theme_stylebox_override("panel", frame_style)
|
||||
add_child(frame)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if _background_kind == "wall" and SURFACE_RENDERER.is_wall_finish(_preview_id):
|
||||
var wall_tint: Color = Color("#7FA36C") if _preview_id == &"finish_wall_paint" else Color.WHITE
|
||||
SURFACE_RENDERER.draw_wall(self, Rect2(Vector2.ZERO, size), _preview_id, wall_tint, Vector2.ZERO)
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_RESIZED:
|
||||
queue_redraw()
|
||||
@@ -0,0 +1 @@
|
||||
uid://csk4xjt1unoh
|
||||
@@ -0,0 +1,134 @@
|
||||
class_name StageSceneryArtwork
|
||||
extends Node2D
|
||||
## Visual-only live-reference Scenery placement. Never participates in physics.
|
||||
|
||||
const PROP_FILE := preload("res://scripts/editors/prop/prop_file.gd")
|
||||
const PROP_VISUALS := preload("res://scripts/props/prop_visuals.gd")
|
||||
const DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd")
|
||||
const CONTENT_PATHS := preload("res://scripts/shared/content_paths.gd")
|
||||
|
||||
var asset_path: String = ""
|
||||
var display_name: String = "Scenery"
|
||||
var depth: StringName = &"near"
|
||||
var last_known_local_bounds: Rect2 = Rect2(Vector2(-32.0, -32.0), Vector2(64.0, 64.0))
|
||||
var has_known_bounds: bool = false
|
||||
var missing: bool = false
|
||||
var _shapes: Array[Dictionary] = []
|
||||
var _visuals: Node2D
|
||||
|
||||
|
||||
func load_visual(path: String) -> bool:
|
||||
var normalized_path: String = CONTENT_PATHS.canonicalize(path)
|
||||
if not FileAccess.file_exists(normalized_path):
|
||||
_set_missing()
|
||||
return false
|
||||
var raw: Dictionary = PROP_FILE.load(normalized_path)
|
||||
var parsed: Dictionary = PROP_FILE.from_file_dict(raw)
|
||||
var parsed_shapes: Array[Dictionary] = parsed.get("shapes", []) as Array[Dictionary]
|
||||
if parsed_shapes.is_empty():
|
||||
_set_missing()
|
||||
return false
|
||||
if _visuals != null and is_instance_valid(_visuals):
|
||||
_visuals.queue_free()
|
||||
_visuals = Node2D.new()
|
||||
_visuals.name = "Visuals"
|
||||
add_child(_visuals)
|
||||
asset_path = normalized_path
|
||||
_shapes = parsed_shapes
|
||||
var bounds: Rect2 = PROP_VISUALS.build(_visuals, _shapes)
|
||||
var stroke: float = _max_stroke_width(_shapes) * 0.5
|
||||
last_known_local_bounds = bounds.grow(stroke) if bounds.size.x > 0.0 or bounds.size.y > 0.0 else Rect2(Vector2(-32.0, -32.0), Vector2(64.0, 64.0))
|
||||
has_known_bounds = true
|
||||
missing = false
|
||||
set_process(false)
|
||||
_apply_depth()
|
||||
queue_redraw()
|
||||
return true
|
||||
|
||||
|
||||
func set_depth(value: StringName) -> void:
|
||||
depth = value if value in [&"far", &"near", &"floor"] else &"near"
|
||||
_apply_depth()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if missing:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_visual_world_bounds() -> Rect2:
|
||||
return global_transform * last_known_local_bounds
|
||||
|
||||
|
||||
func get_saved_footprint() -> Rect2:
|
||||
return last_known_local_bounds
|
||||
|
||||
|
||||
func configure_missing(path: String, name: String, saved_bounds: Rect2, saved_depth: StringName) -> void:
|
||||
asset_path = CONTENT_PATHS.canonicalize(path)
|
||||
display_name = name.strip_edges() if not name.strip_edges().is_empty() else asset_path.get_file().get_basename()
|
||||
if saved_bounds.size.x > 0.0 and saved_bounds.size.y > 0.0:
|
||||
last_known_local_bounds = saved_bounds
|
||||
has_known_bounds = true
|
||||
else:
|
||||
last_known_local_bounds = Rect2(Vector2(-32.0, -32.0), Vector2(64.0, 64.0))
|
||||
has_known_bounds = false
|
||||
depth = saved_depth
|
||||
load_visual(asset_path)
|
||||
|
||||
|
||||
func _set_missing() -> void:
|
||||
missing = true
|
||||
set_process(true)
|
||||
if _visuals != null and is_instance_valid(_visuals):
|
||||
_visuals.queue_free()
|
||||
_visuals = null
|
||||
_apply_depth()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _apply_depth() -> void:
|
||||
z_index = DRAW_ORDER.scenery_z(depth)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if not missing:
|
||||
return
|
||||
var rect: Rect2 = last_known_local_bounds
|
||||
draw_rect(rect, Color("#87909D30"), true)
|
||||
_draw_dashed_line(rect.position, Vector2(rect.end.x, rect.position.y))
|
||||
_draw_dashed_line(Vector2(rect.end.x, rect.position.y), rect.end)
|
||||
_draw_dashed_line(rect.end, Vector2(rect.position.x, rect.end.y))
|
||||
_draw_dashed_line(Vector2(rect.position.x, rect.end.y), rect.position)
|
||||
var camera: Camera2D = get_viewport().get_camera_2d() if is_inside_tree() else null
|
||||
var zoom: float = maxf(0.05, camera.zoom.x) if camera != null else 1.0
|
||||
draw_set_transform(rect.position + Vector2(0.0, -18.0), 0.0, Vector2.ONE / zoom)
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(188.0, 32.0)), Color("#242B37E8"), true)
|
||||
draw_circle(Vector2(16.0, 16.0), 10.0, Color("#FFD166"))
|
||||
var font: Font = ThemeDB.fallback_font
|
||||
if font != null:
|
||||
draw_string(font, Vector2(13.0, 22.0), "?", HORIZONTAL_ALIGNMENT_LEFT, -1.0, 16, Color("#242B37"))
|
||||
draw_string(font, Vector2(32.0, 22.0), "Artwork not found", HORIZONTAL_ALIGNMENT_LEFT, -1.0, 18, Color("#F5F7FA"))
|
||||
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
|
||||
|
||||
|
||||
func _draw_dashed_line(start: Vector2, finish: Vector2) -> void:
|
||||
var length: float = start.distance_to(finish)
|
||||
if length <= 0.0:
|
||||
return
|
||||
var direction: Vector2 = (finish - start) / length
|
||||
var offset: float = 0.0
|
||||
var segment_index: int = 0
|
||||
while offset < length:
|
||||
var segment_end: float = minf(length, offset + 9.0)
|
||||
var color: Color = Color("#E2E6EC") if segment_index % 2 == 0 else Color("#687281")
|
||||
draw_line(start + direction * offset, start + direction * segment_end, color, 2.0)
|
||||
offset += 15.0
|
||||
segment_index += 1
|
||||
|
||||
|
||||
func _max_stroke_width(shapes: Array[Dictionary]) -> float:
|
||||
var width: float = 0.0
|
||||
for shape: Dictionary in shapes:
|
||||
width = maxf(width, float(shape.get("outline_width", 0.0)))
|
||||
return width
|
||||
@@ -0,0 +1 @@
|
||||
uid://ysop8p4ekoc4
|
||||
@@ -0,0 +1,323 @@
|
||||
class_name StageSkyView
|
||||
extends Control
|
||||
## Adaptive screen-space Sky behind the world.
|
||||
|
||||
const SKY_IDS: Array[StringName] = [&"sky_clear_day", &"sky_cloudy_day", &"sky_sunset", &"sky_starry_night", &"sky_neutral"]
|
||||
const GRADIENT_TEXTURE_HEIGHT: int = 2048
|
||||
const CLOUD_VIEWBOX: Vector2 = Vector2(210.0, 110.0)
|
||||
const NIGHT_STAR_COLOR: Color = Color("#FFF4CE")
|
||||
|
||||
var _sky_id: StringName = &"sky_neutral"
|
||||
var _stage_theme: SandboxTheme
|
||||
var _gradient_texture: GradientTexture2D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||
_update_gradient_texture()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func set_sky(id: StringName) -> void:
|
||||
var next_sky_id: StringName = id if SKY_IDS.has(id) else &"sky_neutral"
|
||||
if _sky_id == next_sky_id:
|
||||
return
|
||||
_sky_id = next_sky_id
|
||||
_update_gradient_texture()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_sky() -> StringName:
|
||||
return _sky_id
|
||||
|
||||
|
||||
func apply_theme(theme: SandboxTheme) -> void:
|
||||
_stage_theme = theme
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_RESIZED:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if size.x <= 0.0 or size.y <= 0.0:
|
||||
return
|
||||
if _gradient_texture == null:
|
||||
_update_gradient_texture()
|
||||
if _gradient_texture != null:
|
||||
draw_texture_rect(_gradient_texture, Rect2(Vector2.ZERO, size), false)
|
||||
if _sky_id == &"sky_starry_night":
|
||||
_draw_stars()
|
||||
_draw_moon()
|
||||
_draw_clouds()
|
||||
else:
|
||||
if _sky_id != &"sky_cloudy_day":
|
||||
_draw_sun()
|
||||
_draw_clouds()
|
||||
|
||||
|
||||
func _update_gradient_texture() -> void:
|
||||
var gradient: Gradient = Gradient.new()
|
||||
var offsets: PackedFloat32Array = PackedFloat32Array()
|
||||
var colors: PackedColorArray = PackedColorArray()
|
||||
match _sky_id:
|
||||
&"sky_clear_day":
|
||||
offsets = PackedFloat32Array([0.0, 0.48, 1.0])
|
||||
colors = PackedColorArray([Color("#54ADEA"), Color("#95D4F4"), Color("#E0F5FF")])
|
||||
&"sky_cloudy_day":
|
||||
offsets = PackedFloat32Array([0.0, 0.52, 1.0])
|
||||
colors = PackedColorArray([Color("#687D98"), Color("#A2B7C6"), Color("#D9E2E4")])
|
||||
&"sky_sunset":
|
||||
offsets = PackedFloat32Array([0.0, 0.38, 0.71, 1.0])
|
||||
colors = PackedColorArray([Color("#644B86"), Color("#A96B99"), Color("#E89491"), Color("#FFC57F")])
|
||||
&"sky_starry_night":
|
||||
offsets = PackedFloat32Array([0.0, 0.55, 1.0])
|
||||
colors = PackedColorArray([Color("#111A3C"), Color("#253963"), Color("#526E95")])
|
||||
_:
|
||||
offsets = PackedFloat32Array([0.0, 0.5, 1.0])
|
||||
colors = PackedColorArray([Color("#323944"), Color("#6B737F"), Color("#AEB6C1")])
|
||||
gradient.offsets = offsets
|
||||
gradient.colors = colors
|
||||
_gradient_texture = GradientTexture2D.new()
|
||||
_gradient_texture.gradient = gradient
|
||||
_gradient_texture.width = 2
|
||||
_gradient_texture.height = GRADIENT_TEXTURE_HEIGHT
|
||||
_gradient_texture.fill_from = Vector2.ZERO
|
||||
_gradient_texture.fill_to = Vector2(0.0, 1.0)
|
||||
|
||||
|
||||
func _draw_sun() -> void:
|
||||
var center: Vector2 = Vector2(size.x * 0.78125, size.y * (0.241 if _sky_id == &"sky_sunset" else 0.242))
|
||||
var radius: float = size.y * (0.0979 if _sky_id == &"sky_sunset" else 0.0792)
|
||||
var halo_color: Color = Color("#FFE1A2") if _sky_id == &"sky_sunset" else Color("#FFF1B3")
|
||||
var sun_color: Color = Color("#FFE296") if _sky_id == &"sky_sunset" else Color("#FFE990")
|
||||
draw_circle(center, radius * 1.5, Color(halo_color.r, halo_color.g, halo_color.b, 0.14))
|
||||
draw_circle(center, radius, sun_color)
|
||||
draw_circle(center + Vector2(-radius * 0.19, -radius * 0.2), radius * 0.54, Color(1.0, 0.97, 0.82, 0.32))
|
||||
|
||||
|
||||
func _draw_stars() -> void:
|
||||
for index: int in 48:
|
||||
var x: float = fposmod(float(index * 137 + 39), size.x)
|
||||
var y: float = fposmod(float(index * 83 + 21), size.y * 0.62)
|
||||
draw_circle(Vector2(x, y), 1.3 if index % 4 != 0 else 2.0, NIGHT_STAR_COLOR)
|
||||
for star_index: int in 3:
|
||||
var center: Vector2 = Vector2(size.x * [0.3675, 0.9425, 0.22125][star_index], size.y * [0.085, 0.229, 0.348][star_index])
|
||||
var radius: float = maxf(1.3, size.y / 480.0 * 7.0)
|
||||
draw_line(center - Vector2(0.0, radius), center + Vector2(0.0, radius), Color("#FFEABB"), maxf(1.0, radius * 0.22), true)
|
||||
draw_line(center - Vector2(radius, 0.0), center + Vector2(radius, 0.0), Color("#FFEABB"), maxf(1.0, radius * 0.22), true)
|
||||
|
||||
|
||||
func _draw_clouds() -> void:
|
||||
var origins: Array[Vector4] = []
|
||||
var opacities: Array[float] = []
|
||||
var outer_color: Color
|
||||
var underside_color: Color
|
||||
var accent_color: Color
|
||||
match _sky_id:
|
||||
&"sky_clear_day":
|
||||
origins = [Vector4(42.0, 66.0, 205.0, 108.0), Vector4(282.0, 167.0, 244.0, 127.0), Vector4(612.0, 288.0, 187.0, 98.0)]
|
||||
opacities = [0.93, 0.88, 0.8]
|
||||
outer_color = Color("#FFFFFF")
|
||||
underside_color = Color("#CBE6FF", 0.72)
|
||||
accent_color = Color("#D9EEFF", 0.86)
|
||||
&"sky_cloudy_day":
|
||||
origins = [Vector4(-16.0, 47.0, 246.0, 129.0), Vector4(188.0, 145.0, 242.0, 127.0), Vector4(508.0, 64.0, 281.0, 147.0), Vector4(74.0, 302.0, 190.0, 100.0), Vector4(535.0, 324.0, 231.0, 121.0)]
|
||||
opacities = [1.0, 0.93, 0.95, 0.7, 0.69]
|
||||
outer_color = Color("#F8FCFF")
|
||||
underside_color = Color("#BED7EF", 0.9)
|
||||
accent_color = Color("#D0E5F7", 1.0)
|
||||
&"sky_sunset":
|
||||
origins = [Vector4(24.0, 82.0, 235.0, 123.0), Vector4(294.0, 244.0, 258.0, 135.0), Vector4(585.0, 310.0, 218.0, 114.0)]
|
||||
opacities = [0.85, 0.9, 0.81]
|
||||
outer_color = Color("#FFE3D1")
|
||||
underside_color = Color("#E9A8BA", 0.8)
|
||||
accent_color = Color("#FFF2D7", 0.8)
|
||||
_:
|
||||
origins = [Vector4(-4.0, 331.0, 231.0, 121.0), Vector4(314.0, 380.0, 188.0, 98.0)]
|
||||
opacities = [0.28, 0.23]
|
||||
outer_color = Color("#A9BCE1")
|
||||
underside_color = Color("#7898C5", 0.8)
|
||||
accent_color = Color("#D7E3F7", 0.45)
|
||||
var viewport_scale: Vector2 = Vector2(size.x / 800.0, size.y / 480.0)
|
||||
for cloud_index: int in origins.size():
|
||||
var placement: Vector4 = origins[cloud_index]
|
||||
var cloud_rect: Rect2 = Rect2(
|
||||
Vector2(placement.x, placement.y) * viewport_scale,
|
||||
Vector2(placement.z, placement.w) * viewport_scale
|
||||
)
|
||||
_draw_cloud(cloud_rect, outer_color, underside_color, accent_color, opacities[cloud_index])
|
||||
|
||||
|
||||
func _draw_cloud(rect: Rect2, outer_color: Color, underside_color: Color, accent_color: Color, opacity: float) -> void:
|
||||
var outline: PackedVector2Array = _map_cloud_points(_build_cloud_outline(), rect)
|
||||
var cloud_color: Color = _with_opacity(outer_color, opacity)
|
||||
draw_colored_polygon(outline, cloud_color)
|
||||
draw_polyline(outline, cloud_color, maxf(1.0, rect.size.y / CLOUD_VIEWBOX.y * 1.5), true)
|
||||
var underside: PackedVector2Array = _map_cloud_points(_build_cloud_underside(), rect)
|
||||
draw_colored_polygon(underside, _with_opacity(underside_color, opacity))
|
||||
for accent_curve: PackedVector2Array in _build_cloud_accents():
|
||||
var accent_points: PackedVector2Array = _map_cloud_points(_sample_curve_chain([accent_curve], 12), rect)
|
||||
draw_polyline(accent_points, _with_opacity(accent_color, opacity), maxf(1.0, rect.size.y / CLOUD_VIEWBOX.y * 3.0), true)
|
||||
|
||||
|
||||
func _draw_moon() -> void:
|
||||
var moon_curves: Array[PackedVector2Array] = _build_moon_curves()
|
||||
var moon_local: PackedVector2Array = _sample_curve_chain(moon_curves, 16)
|
||||
var outer_arc: PackedVector2Array = _sample_curve_chain(_build_moon_outer_arc_curves(), 16)
|
||||
var moon_scale: float = size.y / 480.0 * 0.63
|
||||
var moon_origin: Vector2 = Vector2(size.x * 0.7625, size.y * 0.09583)
|
||||
var moon_size: Vector2 = Vector2(150.0, 240.0) * moon_scale
|
||||
var moon_rect: Rect2 = Rect2(moon_origin, moon_size)
|
||||
var moon_outline: PackedVector2Array = _map_moon_points(moon_local, moon_rect)
|
||||
draw_colored_polygon(moon_outline, Color("#17213A"))
|
||||
draw_colored_polygon(moon_outline, Color("#FFEA31"))
|
||||
var warm_rim: PackedVector2Array = _build_moon_warm_rim(outer_arc, 7.5)
|
||||
draw_colored_polygon(_map_moon_points(warm_rim, moon_rect), Color("#F4A629"))
|
||||
var highlight: PackedVector2Array = _sample_curve_chain(_build_moon_highlight(), 18)
|
||||
draw_polyline(_map_moon_points(highlight, moon_rect), Color(1.0, 0.98, 0.63, 0.62), maxf(1.0, moon_scale * 8.0), true)
|
||||
draw_polyline(moon_outline, Color("#17213A"), maxf(1.0, moon_scale * 5.5), true)
|
||||
|
||||
|
||||
func _build_cloud_outline() -> PackedVector2Array:
|
||||
return _sample_curve_chain(_build_cloud_outer_curves(), 10)
|
||||
|
||||
|
||||
func _build_cloud_underside() -> PackedVector2Array:
|
||||
return _sample_curve_chain(_build_cloud_underside_curves(), 10)
|
||||
|
||||
|
||||
func _build_cloud_outer_curves() -> Array[PackedVector2Array]:
|
||||
var curves: Array[PackedVector2Array] = []
|
||||
curves.append(PackedVector2Array([Vector2(18, 74), Vector2(7, 71), Vector2(2, 62), Vector2(5, 52)]))
|
||||
curves.append(PackedVector2Array([Vector2(5, 52), Vector2(8, 42), Vector2(19, 36), Vector2(32, 39)]))
|
||||
curves.append(PackedVector2Array([Vector2(32, 39), Vector2(29, 25), Vector2(40, 13), Vector2(54, 13)]))
|
||||
curves.append(PackedVector2Array([Vector2(54, 13), Vector2(64, 13), Vector2(72, 18), Vector2(77, 25)]))
|
||||
curves.append(PackedVector2Array([Vector2(77, 25), Vector2(84, 9), Vector2(101, 2), Vector2(116, 7)]))
|
||||
curves.append(PackedVector2Array([Vector2(116, 7), Vector2(131, 11), Vector2(141, 24), Vector2(142, 38)]))
|
||||
curves.append(PackedVector2Array([Vector2(142, 38), Vector2(153, 30), Vector2(167, 32), Vector2(176, 40)]))
|
||||
curves.append(PackedVector2Array([Vector2(176, 40), Vector2(182, 45), Vector2(185, 51), Vector2(185, 57)]))
|
||||
curves.append(PackedVector2Array([Vector2(185, 57), Vector2(197, 58), Vector2(205, 67), Vector2(204, 78)]))
|
||||
curves.append(PackedVector2Array([Vector2(204, 78), Vector2(203, 89), Vector2(193, 97), Vector2(180, 97)]))
|
||||
curves.append(PackedVector2Array([Vector2(180, 97), Vector2(171, 97), Vector2(164, 94), Vector2(159, 90)]))
|
||||
curves.append(PackedVector2Array([Vector2(159, 90), Vector2(148, 100), Vector2(131, 101), Vector2(119, 93)]))
|
||||
curves.append(PackedVector2Array([Vector2(119, 93), Vector2(107, 102), Vector2(88, 103), Vector2(75, 94)]))
|
||||
curves.append(PackedVector2Array([Vector2(75, 94), Vector2(63, 101), Vector2(47, 98), Vector2(39, 89)]))
|
||||
curves.append(PackedVector2Array([Vector2(39, 89), Vector2(30, 89), Vector2(23, 83), Vector2(18, 74)]))
|
||||
return curves
|
||||
|
||||
|
||||
func _build_cloud_underside_curves() -> Array[PackedVector2Array]:
|
||||
var curves: Array[PackedVector2Array] = []
|
||||
curves.append(PackedVector2Array([Vector2(9, 69), Vector2(20, 79), Vector2(31, 79), Vector2(42, 75)]))
|
||||
curves.append(PackedVector2Array([Vector2(42, 75), Vector2(57, 89), Vector2(71, 86), Vector2(82, 81)]))
|
||||
curves.append(PackedVector2Array([Vector2(82, 81), Vector2(96, 92), Vector2(111, 89), Vector2(122, 82)]))
|
||||
curves.append(PackedVector2Array([Vector2(122, 82), Vector2(139, 93), Vector2(152, 89), Vector2(163, 80)]))
|
||||
curves.append(PackedVector2Array([Vector2(163, 80), Vector2(179, 89), Vector2(195, 83), Vector2(202, 74)]))
|
||||
curves.append(PackedVector2Array([Vector2(202, 74), Vector2(205, 87), Vector2(195, 97), Vector2(180, 97)]))
|
||||
curves.append(PackedVector2Array([Vector2(180, 97), Vector2(171, 97), Vector2(164, 94), Vector2(159, 90)]))
|
||||
curves.append(PackedVector2Array([Vector2(159, 90), Vector2(148, 100), Vector2(131, 101), Vector2(119, 93)]))
|
||||
curves.append(PackedVector2Array([Vector2(119, 93), Vector2(107, 102), Vector2(88, 103), Vector2(75, 94)]))
|
||||
curves.append(PackedVector2Array([Vector2(75, 94), Vector2(63, 101), Vector2(47, 98), Vector2(39, 89)]))
|
||||
curves.append(PackedVector2Array([Vector2(39, 89), Vector2(25, 91), Vector2(13, 82), Vector2(9, 69)]))
|
||||
return curves
|
||||
|
||||
|
||||
func _build_cloud_accents() -> Array[PackedVector2Array]:
|
||||
var curves: Array[PackedVector2Array] = []
|
||||
curves.append(PackedVector2Array([Vector2(42, 43), Vector2(49, 54), Vector2(58, 59), Vector2(69, 58)]))
|
||||
curves.append(PackedVector2Array([Vector2(85, 33), Vector2(96, 21), Vector2(114, 21), Vector2(126, 32)]))
|
||||
curves.append(PackedVector2Array([Vector2(142, 58), Vector2(151, 48), Vector2(164, 47), Vector2(173, 53)]))
|
||||
return curves
|
||||
|
||||
|
||||
func _build_moon_curves() -> Array[PackedVector2Array]:
|
||||
var curves: Array[PackedVector2Array] = []
|
||||
curves.append(PackedVector2Array([Vector2(15, 13), Vector2(69, 20), Vector2(113, 54), Vector2(129, 104)]))
|
||||
curves.append(PackedVector2Array([Vector2(129, 104), Vector2(149, 166), Vector2(119, 224), Vector2(61, 235)]))
|
||||
curves.append(PackedVector2Array([Vector2(61, 235), Vector2(36, 240), Vector2(13, 231), Vector2(2, 216)]))
|
||||
curves.append(PackedVector2Array([Vector2(2, 216), Vector2(40, 218), Vector2(60, 193), Vector2(68, 158)]))
|
||||
curves.append(PackedVector2Array([Vector2(68, 158), Vector2(85, 103), Vector2(63, 47), Vector2(15, 13)]))
|
||||
return curves
|
||||
|
||||
|
||||
func _build_moon_outer_arc_curves() -> Array[PackedVector2Array]:
|
||||
var curves: Array[PackedVector2Array] = []
|
||||
curves.append(PackedVector2Array([Vector2(15, 13), Vector2(69, 20), Vector2(113, 54), Vector2(129, 104)]))
|
||||
curves.append(PackedVector2Array([Vector2(129, 104), Vector2(149, 166), Vector2(119, 224), Vector2(61, 235)]))
|
||||
curves.append(PackedVector2Array([Vector2(61, 235), Vector2(36, 240), Vector2(13, 231), Vector2(2, 216)]))
|
||||
return curves
|
||||
|
||||
|
||||
func _build_moon_highlight() -> Array[PackedVector2Array]:
|
||||
var curves: Array[PackedVector2Array] = []
|
||||
curves.append(PackedVector2Array([Vector2(29, 37), Vector2(63, 58), Vector2(85, 98), Vector2(87, 128)]))
|
||||
return curves
|
||||
|
||||
|
||||
func _sample_curve_chain(curves: Array[PackedVector2Array], steps_per_curve: int) -> PackedVector2Array:
|
||||
var sampled: PackedVector2Array = PackedVector2Array()
|
||||
if steps_per_curve <= 0:
|
||||
return sampled
|
||||
for curve_index: int in curves.size():
|
||||
var curve: PackedVector2Array = curves[curve_index]
|
||||
if curve.size() != 4:
|
||||
continue
|
||||
for step_index: int in range(steps_per_curve + 1):
|
||||
if curve_index > 0 and step_index == 0:
|
||||
continue
|
||||
var t: float = float(step_index) / float(steps_per_curve)
|
||||
sampled.append(_cubic_point(curve, t))
|
||||
return sampled
|
||||
|
||||
|
||||
func _cubic_point(curve: PackedVector2Array, t: float) -> Vector2:
|
||||
var inverse_t: float = 1.0 - t
|
||||
return (
|
||||
curve[0] * inverse_t * inverse_t * inverse_t
|
||||
+ curve[1] * 3.0 * inverse_t * inverse_t * t
|
||||
+ curve[2] * 3.0 * inverse_t * t * t
|
||||
+ curve[3] * t * t * t
|
||||
)
|
||||
|
||||
|
||||
func _map_cloud_points(points: PackedVector2Array, rect: Rect2) -> PackedVector2Array:
|
||||
var mapped: PackedVector2Array = PackedVector2Array()
|
||||
for point: Vector2 in points:
|
||||
mapped.append(rect.position + Vector2(point.x / CLOUD_VIEWBOX.x * rect.size.x, point.y / CLOUD_VIEWBOX.y * rect.size.y))
|
||||
return mapped
|
||||
|
||||
|
||||
func _map_moon_points(points: PackedVector2Array, rect: Rect2) -> PackedVector2Array:
|
||||
var mapped: PackedVector2Array = PackedVector2Array()
|
||||
for point: Vector2 in points:
|
||||
mapped.append(rect.position + point * (rect.size.y / 240.0))
|
||||
return mapped
|
||||
|
||||
|
||||
func _build_moon_warm_rim(outer_arc: PackedVector2Array, width: float) -> PackedVector2Array:
|
||||
var rim: PackedVector2Array = PackedVector2Array()
|
||||
if outer_arc.size() < 3:
|
||||
return rim
|
||||
for point: Vector2 in outer_arc:
|
||||
rim.append(point)
|
||||
for point_index: int in range(outer_arc.size() - 1, -1, -1):
|
||||
var tangent: Vector2
|
||||
if point_index == 0:
|
||||
tangent = outer_arc[1] - outer_arc[0]
|
||||
elif point_index == outer_arc.size() - 1:
|
||||
tangent = outer_arc[point_index] - outer_arc[point_index - 1]
|
||||
else:
|
||||
tangent = outer_arc[point_index + 1] - outer_arc[point_index - 1]
|
||||
var inward: Vector2 = Vector2(-tangent.y, tangent.x).normalized()
|
||||
var progress: float = float(point_index) / float(outer_arc.size() - 1)
|
||||
var local_width: float = width * sin(PI * progress)
|
||||
rim.append(outer_arc[point_index] + inward * local_width)
|
||||
return rim
|
||||
|
||||
|
||||
func _with_opacity(color: Color, opacity: float) -> Color:
|
||||
return Color(color.r, color.g, color.b, color.a * opacity)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dxoqflho7kyik
|
||||
@@ -0,0 +1,116 @@
|
||||
class_name StageStarterCardPreview
|
||||
extends SubViewportContainer
|
||||
## Lightweight live preview of a saved starter Stage scene.
|
||||
|
||||
const STAGE_SCENE_IO := preload("res://scripts/stage/stage_scene_io.gd")
|
||||
const STAGE_SCENE_ADAPTER := preload("res://scripts/editors/stage/stage_scene_adapter.gd")
|
||||
const STAGE_SPAWNER := preload("res://scripts/stage/stage_spawner.gd")
|
||||
const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd")
|
||||
const STAGE_SKY_VIEW := preload("res://scripts/stage/scenery/stage_sky_view.gd")
|
||||
const STAGE_WALL_PANEL := preload("res://scripts/stage/scenery/stage_wall_panel.gd")
|
||||
const PROP_BLOCK := preload("res://scripts/props/prop_block.gd")
|
||||
|
||||
const PREVIEW_SIZE := Vector2i(256, 256)
|
||||
const PREVIEW_MARGIN := 18.0
|
||||
const MIN_PREVIEW_ZOOM := 0.08
|
||||
const MAX_PREVIEW_ZOOM := 0.42
|
||||
|
||||
var _viewport: SubViewport
|
||||
var _sky_view: StageSkyView
|
||||
var _world: Node2D
|
||||
var _loaded_nodes: Array[Node2D] = []
|
||||
var _scene_valid: bool = false
|
||||
|
||||
|
||||
func configure(entry: Dictionary) -> void:
|
||||
name = "StarterScenePreview"
|
||||
custom_minimum_size = Vector2(128.0, 128.0)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
stretch = true
|
||||
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||
|
||||
var result: Dictionary = STAGE_SCENE_IO.read_file(String(entry.get("path", "")))
|
||||
if not bool(result.get("ok", false)):
|
||||
return
|
||||
var data: Dictionary = result.get("data", {}) as Dictionary
|
||||
_scene_valid = true
|
||||
_build_preview_viewport(data)
|
||||
|
||||
|
||||
func has_preview_scene() -> bool:
|
||||
return _scene_valid and _viewport != null and _sky_view != null and _world != null
|
||||
|
||||
|
||||
func get_loaded_object_count() -> int:
|
||||
return _loaded_nodes.size()
|
||||
|
||||
|
||||
func _build_preview_viewport(data: Dictionary) -> void:
|
||||
_viewport = SubViewport.new()
|
||||
_viewport.name = "PreviewViewport"
|
||||
_viewport.size = PREVIEW_SIZE
|
||||
_viewport.world_2d = World2D.new()
|
||||
_viewport.transparent_bg = false
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
|
||||
add_child(_viewport)
|
||||
|
||||
var sky_layer := CanvasLayer.new()
|
||||
sky_layer.name = "PreviewSky"
|
||||
sky_layer.layer = -1
|
||||
_viewport.add_child(sky_layer)
|
||||
_sky_view = STAGE_SKY_VIEW.new() as StageSkyView
|
||||
_sky_view.name = "SkyView"
|
||||
_sky_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_sky_view.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_sky_view.set_sky(StringName(String(data.get("sky_id", "sky_neutral"))))
|
||||
sky_layer.add_child(_sky_view)
|
||||
|
||||
_world = Node2D.new()
|
||||
_world.name = "PreviewWorld"
|
||||
_viewport.add_child(_world)
|
||||
var spawner: StageSpawner = STAGE_SPAWNER.new(_world) as StageSpawner
|
||||
var adapter: StageSceneAdapter = STAGE_SCENE_ADAPTER.new() as StageSceneAdapter
|
||||
var applied: Dictionary = adapter.apply(data, _world, spawner)
|
||||
for raw_node: Variant in applied.get("loaded_nodes", []):
|
||||
var stage_node: Node2D = raw_node as Node2D
|
||||
if stage_node == null:
|
||||
continue
|
||||
_loaded_nodes.append(stage_node)
|
||||
if stage_node is PropBlock:
|
||||
var prop: PropBlock = stage_node as PropBlock
|
||||
prop.freeze = true
|
||||
prop.gravity_scale = 0.0
|
||||
prop.collision_layer = 0
|
||||
prop.collision_mask = 0
|
||||
call_deferred("_fit_scene_to_preview")
|
||||
|
||||
|
||||
func _fit_scene_to_preview() -> void:
|
||||
if _world == null or not is_instance_valid(_world):
|
||||
return
|
||||
var content_bounds := Rect2()
|
||||
var has_bounds: bool = false
|
||||
for node: Node2D in _loaded_nodes:
|
||||
if node == null or not is_instance_valid(node):
|
||||
continue
|
||||
var bounds: Rect2
|
||||
if node.get_script() == STAGE_WALL_PANEL:
|
||||
bounds = node.global_transform * (node as StageWallPanel).get_local_bounds()
|
||||
else:
|
||||
bounds = STAGE_SELECTION.get_world_aabb(node)
|
||||
bounds = bounds.abs()
|
||||
if bounds.size.x <= 0.0 or bounds.size.y <= 0.0:
|
||||
continue
|
||||
content_bounds = bounds if not has_bounds else content_bounds.merge(bounds)
|
||||
has_bounds = true
|
||||
if not has_bounds:
|
||||
_world.scale = Vector2.ONE * 0.2
|
||||
_world.position = Vector2(PREVIEW_SIZE) * 0.5
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
return
|
||||
var usable_size: Vector2 = Vector2(PREVIEW_SIZE) - Vector2.ONE * PREVIEW_MARGIN * 2.0
|
||||
var fit_zoom: float = minf(usable_size.x / content_bounds.size.x, usable_size.y / content_bounds.size.y)
|
||||
fit_zoom = clampf(fit_zoom, MIN_PREVIEW_ZOOM, MAX_PREVIEW_ZOOM)
|
||||
_world.scale = Vector2.ONE * fit_zoom
|
||||
_world.position = Vector2(PREVIEW_SIZE) * 0.5 - content_bounds.get_center() * fit_zoom
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
@@ -0,0 +1 @@
|
||||
uid://lw57y3sl5wfk
|
||||
@@ -0,0 +1,267 @@
|
||||
class_name StageSurfaceRenderer
|
||||
extends RefCounted
|
||||
## World-aligned, bounded vector motifs for finite walls and Ground.
|
||||
|
||||
const WALL_FINISHES: Array[StringName] = [&"finish_wall_paint", &"finish_wall_brick", &"finish_wall_wood"]
|
||||
const GROUND_FINISHES: Array[StringName] = [&"finish_ground_paint", &"finish_ground_brick", &"finish_ground_wood", &"finish_ground_grass", &"finish_ground_dirt", &"finish_ground_sidewalk", &"finish_ground_asphalt"]
|
||||
const MAX_CELLS: int = 1200
|
||||
const BRICK_WIDTH: float = 82.0
|
||||
const BRICK_HEIGHT: float = 34.0
|
||||
const MAX_VISIBLE_BRICK_CELLS: int = 12000
|
||||
|
||||
|
||||
static func is_wall_finish(id: StringName) -> bool:
|
||||
return WALL_FINISHES.has(id)
|
||||
|
||||
|
||||
static func is_ground_finish(id: StringName) -> bool:
|
||||
return GROUND_FINISHES.has(id) or id == &"finish_ground_legacy"
|
||||
|
||||
|
||||
static func draw_wall(
|
||||
canvas: CanvasItem,
|
||||
rect: Rect2,
|
||||
finish: StringName,
|
||||
tint: Color,
|
||||
world_origin: Vector2,
|
||||
drawing_scale: Vector2 = Vector2.ONE
|
||||
) -> void:
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, true)
|
||||
|
||||
|
||||
static func draw_ground(
|
||||
canvas: CanvasItem,
|
||||
rect: Rect2,
|
||||
finish: StringName,
|
||||
tint: Color,
|
||||
world_origin: Vector2,
|
||||
drawing_scale: Vector2 = Vector2.ONE
|
||||
) -> void:
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false)
|
||||
|
||||
|
||||
static func _draw_surface(
|
||||
canvas: CanvasItem,
|
||||
rect: Rect2,
|
||||
finish: StringName,
|
||||
tint: Color,
|
||||
world_origin: Vector2,
|
||||
drawing_scale: Vector2,
|
||||
wall: bool
|
||||
) -> void:
|
||||
var normalized := rect.abs()
|
||||
if normalized.size.x <= 0.0 or normalized.size.y <= 0.0:
|
||||
return
|
||||
var motif_scale: Vector2 = _normalized_scale(drawing_scale)
|
||||
var base: Color = tint
|
||||
if finish == &"finish_ground_legacy":
|
||||
base = Color("#468B42")
|
||||
elif finish.ends_with("brick"):
|
||||
base = Color("#B95F3D") if wall else Color("#994D35")
|
||||
elif finish.ends_with("wood"):
|
||||
base = Color("#9A673C") if wall else Color("#B88951")
|
||||
elif finish == &"finish_ground_grass":
|
||||
base = Color("#72A84C")
|
||||
elif finish == &"finish_ground_dirt":
|
||||
base = Color("#8E633E")
|
||||
elif finish == &"finish_ground_sidewalk":
|
||||
base = Color("#B7B9B2")
|
||||
elif finish == &"finish_ground_asphalt":
|
||||
base = Color("#505A62")
|
||||
canvas.draw_rect(normalized, base, true)
|
||||
if finish.ends_with("brick"):
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas)
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0:
|
||||
var detail_scale: float = brick_detail_scale_for_view(view_rect.size if not view_rect.size.is_zero_approx() else normalized.size, motif_scale)
|
||||
_draw_bricks(canvas, pattern_rect, world_origin, base, motif_scale, detail_scale)
|
||||
elif finish.ends_with("wood"):
|
||||
_draw_wood(canvas, normalized, world_origin, base, motif_scale)
|
||||
elif finish == &"finish_ground_grass":
|
||||
_draw_grass(canvas, normalized, world_origin, motif_scale)
|
||||
elif finish == &"finish_ground_dirt":
|
||||
_draw_dirt(canvas, normalized, world_origin, motif_scale)
|
||||
elif finish == &"finish_ground_sidewalk":
|
||||
_draw_sidewalk(canvas, normalized, world_origin, motif_scale)
|
||||
elif finish == &"finish_ground_asphalt":
|
||||
_draw_asphalt(canvas, normalized, world_origin, motif_scale)
|
||||
elif finish != &"finish_wall_paint" and finish != &"finish_ground_paint" and finish != &"finish_ground_legacy":
|
||||
push_warning("StageSurfaceRenderer: unknown finish '%s'; drawing paint." % String(finish))
|
||||
|
||||
|
||||
static func brick_detail_scale_for_view(view_size: Vector2, motif_scale: Vector2 = Vector2.ONE) -> float:
|
||||
var scale: float = 1.0
|
||||
var safe_size: Vector2 = Vector2(maxf(1.0, view_size.x), maxf(1.0, view_size.y))
|
||||
var safe_motif_scale: Vector2 = _normalized_scale(motif_scale)
|
||||
for _iteration: int in 8:
|
||||
var columns: int = int(ceil(safe_size.x * safe_motif_scale.x / (BRICK_WIDTH * scale))) + 2
|
||||
var rows: int = int(ceil(safe_size.y * safe_motif_scale.y / (BRICK_HEIGHT * scale))) + 2
|
||||
var cell_count: int = rows * columns
|
||||
if cell_count <= MAX_VISIBLE_BRICK_CELLS:
|
||||
return scale
|
||||
scale *= sqrt(float(cell_count) / float(MAX_VISIBLE_BRICK_CELLS)) * 1.01
|
||||
return scale
|
||||
|
||||
|
||||
static func _draw_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, base: Color, motif_scale: Vector2, detail_scale: float = 1.0) -> void:
|
||||
var brick_world_w: float = BRICK_WIDTH * detail_scale
|
||||
var brick_world_h: float = BRICK_HEIGHT * detail_scale
|
||||
var brick_w: float = brick_world_w / motif_scale.x
|
||||
var brick_h: float = brick_world_h / motif_scale.y
|
||||
var mortar := base.lightened(0.46)
|
||||
var x_start: float = (floorf((rect.position.x * motif_scale.x + origin.x) / brick_world_w) * brick_world_w - origin.x) / motif_scale.x
|
||||
var y_start: float = (floorf((rect.position.y * motif_scale.y + origin.y) / brick_world_h) * brick_world_h - origin.y) / motif_scale.y
|
||||
var rows: int = int(ceil(rect.size.y / brick_h)) + 2
|
||||
var columns: int = int(ceil(rect.size.x / brick_w)) + 2
|
||||
for row: int in rows:
|
||||
var y: float = y_start + float(row) * brick_h
|
||||
var offset: float = brick_w * 0.5 if posmod(row + int(floori((rect.position.y * motif_scale.y + origin.y) / brick_world_h)), 2) == 1 else 0.0
|
||||
_draw_clipped_line(canvas, rect, Vector2(rect.position.x, y), Vector2(rect.end.x, y), mortar, 3.0 / motif_scale.y)
|
||||
for column: int in columns:
|
||||
var x: float = x_start + float(column) * brick_w + offset
|
||||
if x < rect.position.x or x > rect.end.x:
|
||||
continue
|
||||
var next_row_y: float = y + brick_h
|
||||
_draw_clipped_line(canvas, rect, Vector2(x, y), Vector2(x, next_row_y), mortar, 3.0 / motif_scale.x)
|
||||
|
||||
|
||||
static func _visible_local_rect(canvas: CanvasItem) -> Rect2:
|
||||
if canvas == null or not canvas.is_inside_tree():
|
||||
return Rect2()
|
||||
var viewport: Viewport = canvas.get_viewport()
|
||||
if viewport == null:
|
||||
return Rect2()
|
||||
var local_from_viewport: Transform2D = canvas.get_global_transform_with_canvas().affine_inverse()
|
||||
var viewport_rect: Rect2 = viewport.get_visible_rect()
|
||||
var corners: Array[Vector2] = [
|
||||
viewport_rect.position,
|
||||
Vector2(viewport_rect.end.x, viewport_rect.position.y),
|
||||
viewport_rect.end,
|
||||
Vector2(viewport_rect.position.x, viewport_rect.end.y),
|
||||
]
|
||||
var local_rect: Rect2 = Rect2(local_from_viewport * corners[0], Vector2.ZERO)
|
||||
for corner_index: int in range(1, corners.size()):
|
||||
local_rect = local_rect.expand(local_from_viewport * corners[corner_index])
|
||||
return local_rect
|
||||
|
||||
|
||||
static func _draw_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, base: Color, motif_scale: Vector2) -> void:
|
||||
const BOARD: float = 64.0
|
||||
var line_color := base.darkened(0.28)
|
||||
var board_h: float = BOARD / motif_scale.y
|
||||
var start_y: float = (floorf((rect.position.y * motif_scale.y + origin.y) / BOARD) * BOARD - origin.y) / motif_scale.y - board_h
|
||||
var count: int = mini(int(ceil(rect.size.y / board_h)) + 3, MAX_CELLS)
|
||||
for index: int in count:
|
||||
var y: float = start_y + float(index) * board_h
|
||||
_draw_clipped_line(canvas, rect, Vector2(rect.position.x, y), Vector2(rect.end.x, y), line_color, 2.0 / motif_scale.y)
|
||||
var seed_x: float = fposmod(origin.x * 0.37 + float(index * 41), 90.0) / motif_scale.x
|
||||
_draw_clipped_line(
|
||||
canvas,
|
||||
rect,
|
||||
Vector2(rect.position.x + seed_x, y + board_h * 0.5),
|
||||
Vector2(rect.position.x + seed_x + 28.0 / motif_scale.x, y + board_h * 0.5),
|
||||
line_color,
|
||||
1.0 / motif_scale.y
|
||||
)
|
||||
|
||||
|
||||
static func _draw_clipped_line(canvas: CanvasItem, rect: Rect2, from: Vector2, to: Vector2, color: Color, width: float) -> void:
|
||||
var clipped: PackedVector2Array = _clip_line_to_rect(rect, from, to, width)
|
||||
if clipped.size() == 2:
|
||||
canvas.draw_line(clipped[0], clipped[1], color, width)
|
||||
|
||||
|
||||
static func _clip_line_to_rect(rect: Rect2, from: Vector2, to: Vector2, width: float) -> PackedVector2Array:
|
||||
var inset: float = maxf(0.0, width) * 0.5
|
||||
var safe_rect: Rect2 = Rect2(rect.position + Vector2.ONE * inset, rect.size - Vector2.ONE * inset * 2.0)
|
||||
if safe_rect.size.x <= 0.0 or safe_rect.size.y <= 0.0:
|
||||
return PackedVector2Array()
|
||||
var delta: Vector2 = to - from
|
||||
var p_values: Array[float] = [-delta.x, delta.x, -delta.y, delta.y]
|
||||
var q_values: Array[float] = [
|
||||
from.x - safe_rect.position.x,
|
||||
safe_rect.end.x - from.x,
|
||||
from.y - safe_rect.position.y,
|
||||
safe_rect.end.y - from.y,
|
||||
]
|
||||
var t_min: float = 0.0
|
||||
var t_max: float = 1.0
|
||||
for edge_index: int in p_values.size():
|
||||
var p_value: float = p_values[edge_index]
|
||||
var q_value: float = q_values[edge_index]
|
||||
if is_zero_approx(p_value):
|
||||
if q_value < 0.0:
|
||||
return PackedVector2Array()
|
||||
continue
|
||||
var ratio: float = q_value / p_value
|
||||
if p_value < 0.0:
|
||||
if ratio > t_max:
|
||||
return PackedVector2Array()
|
||||
t_min = maxf(t_min, ratio)
|
||||
else:
|
||||
if ratio < t_min:
|
||||
return PackedVector2Array()
|
||||
t_max = minf(t_max, ratio)
|
||||
if t_min > t_max:
|
||||
return PackedVector2Array()
|
||||
return PackedVector2Array([from + delta * t_min, from + delta * t_max])
|
||||
|
||||
|
||||
static func _draw_grass(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void:
|
||||
var pitch: float = 18.0 / motif_scale.x
|
||||
var phase: float = fposmod(origin.x, 18.0) / motif_scale.x
|
||||
var count: int = mini(int(ceil(rect.size.x / pitch)) + 2, MAX_CELLS)
|
||||
var baseline: float = rect.position.y
|
||||
for index: int in count:
|
||||
var x: float = rect.position.x + float(index) * pitch - phase
|
||||
var seed: int = absi(int(floori((x * motif_scale.x + origin.x) * 0.23)))
|
||||
var height: float = 5.0 + float(seed % 8)
|
||||
var green := Color("#3D7E39") if seed % 3 == 0 else Color("#4D913F")
|
||||
canvas.draw_line(Vector2(x, baseline + 4.0 / motif_scale.y), Vector2(x - 3.0 / motif_scale.x, baseline - height / motif_scale.y), green, 2.0 / motif_scale.x)
|
||||
canvas.draw_line(Vector2(x + 4.0 / motif_scale.x, baseline + 4.0 / motif_scale.y), Vector2(x + 7.0 / motif_scale.x, baseline - height * 0.72 / motif_scale.y), green, 1.5 / motif_scale.x)
|
||||
if seed % 5 == 0:
|
||||
_draw_world_circle(canvas, Vector2(x + 2.0 / motif_scale.x, baseline - (height + 2.0) / motif_scale.y), 2.0, Color("#C6D96B"), motif_scale)
|
||||
|
||||
|
||||
static func _draw_dirt(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void:
|
||||
var pitch: float = 26.0 / motif_scale.x
|
||||
var phase: float = fposmod(origin.x, 26.0) / motif_scale.x
|
||||
for index: int in mini(int(ceil(rect.size.x / pitch)) + 1, MAX_CELLS):
|
||||
var x: float = rect.position.x + float(index) * pitch - phase
|
||||
var y: float = rect.position.y + fposmod(float(index * 17) + origin.y, 18.0) / motif_scale.y
|
||||
_draw_world_circle(canvas, Vector2(x, y), 2.0, Color("#67492F"), motif_scale)
|
||||
|
||||
|
||||
static func _draw_sidewalk(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void:
|
||||
const SIZE: float = 72.0
|
||||
var local_size: float = SIZE / motif_scale.x
|
||||
var color := Color("#92968F")
|
||||
var x: float = rect.position.x - fposmod(origin.x, SIZE) / motif_scale.x
|
||||
var count: int = mini(int(ceil(rect.size.x / local_size)) + 2, MAX_CELLS)
|
||||
for index: int in count:
|
||||
var px: float = x + float(index) * local_size
|
||||
canvas.draw_line(Vector2(px, rect.position.y), Vector2(px, rect.end.y), color, 2.0 / motif_scale.x)
|
||||
canvas.draw_line(Vector2(rect.position.x, rect.position.y), Vector2(rect.end.x, rect.position.y), color, 2.0 / motif_scale.y)
|
||||
|
||||
|
||||
static func _draw_asphalt(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void:
|
||||
var local_pitch: float = 80.0 / motif_scale.x
|
||||
var phase: float = fposmod(origin.x, 80.0) / motif_scale.x
|
||||
var count: int = mini(int(ceil(rect.size.x / local_pitch)) + 2, MAX_CELLS)
|
||||
var world_height: float = maxf(rect.size.y * motif_scale.y, 1.0)
|
||||
for index: int in count:
|
||||
var x: float = rect.position.x + float(index) * local_pitch - phase
|
||||
var y: float = rect.position.y + fposmod(float(index * 31) + origin.y, world_height) / motif_scale.y
|
||||
_draw_world_circle(canvas, Vector2(x, y), 1.5, Color("#727A7D"), motif_scale)
|
||||
|
||||
|
||||
static func _draw_world_circle(canvas: CanvasItem, center: Vector2, radius: float, color: Color, motif_scale: Vector2) -> void:
|
||||
var points := PackedVector2Array()
|
||||
for point_index: int in 12:
|
||||
var angle: float = TAU * float(point_index) / 12.0
|
||||
points.append(center + Vector2(cos(angle) * radius / motif_scale.x, sin(angle) * radius / motif_scale.y))
|
||||
canvas.draw_colored_polygon(points, color)
|
||||
|
||||
|
||||
static func _normalized_scale(scale: Vector2) -> Vector2:
|
||||
return Vector2(maxf(absf(scale.x), 0.001), maxf(absf(scale.y), 0.001))
|
||||
@@ -0,0 +1 @@
|
||||
uid://cefj7bf1p3bq2
|
||||
@@ -0,0 +1,36 @@
|
||||
class_name StageWallPanel
|
||||
extends Node2D
|
||||
## Finite decorative wall panel with world-scale finish motifs.
|
||||
|
||||
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd")
|
||||
var finish_id: StringName = &"finish_wall_paint"
|
||||
var tint: Color = Color("#5A9D70")
|
||||
var panel_size: Vector2 = Vector2(320.0, 400.0)
|
||||
## Saved source-wall anchor offset, retained to preserve erased-fragment metadata.
|
||||
## Motif drawing uses the fragment's current world origin so its brick courses stay aligned.
|
||||
var motif_origin_offset: Vector2 = Vector2.ZERO
|
||||
|
||||
|
||||
func configure(new_size: Vector2, finish: StringName, color: Color) -> void:
|
||||
panel_size = Vector2(maxf(1.0, new_size.x), maxf(1.0, new_size.y))
|
||||
finish_id = finish if SURFACE_RENDERER.is_wall_finish(finish) else &"finish_wall_paint"
|
||||
tint = color
|
||||
z_index = DRAW_ORDER.WALL
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_local_bounds() -> Rect2:
|
||||
return Rect2(-panel_size * 0.5, panel_size)
|
||||
|
||||
|
||||
func get_effective_motif_origin_world() -> Vector2:
|
||||
return global_position + motif_origin_offset
|
||||
|
||||
|
||||
func get_surface_draw_origin_world() -> Vector2:
|
||||
return global_position
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
SURFACE_RENDERER.draw_wall(self, get_local_bounds(), finish_id, tint, get_surface_draw_origin_world())
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1ttvnqumh2hd
|
||||
@@ -0,0 +1,21 @@
|
||||
class_name StageDrawOrder
|
||||
extends RefCounted
|
||||
## Semantic draw bands shared by scenery, terrain, Props, and characters.
|
||||
|
||||
const WALL: int = -40
|
||||
const FAR: int = -30
|
||||
const NEAR: int = -20
|
||||
const TERRAIN: int = -10
|
||||
const FLOOR: int = -5
|
||||
|
||||
|
||||
static func scenery_z(depth: StringName) -> int:
|
||||
match depth:
|
||||
&"far": return FAR
|
||||
&"floor": return FLOOR
|
||||
_: return NEAR
|
||||
|
||||
|
||||
static func apply_terrain(node: TerrainBlock) -> void:
|
||||
if node != null:
|
||||
node.z_index = TERRAIN
|
||||
@@ -0,0 +1 @@
|
||||
uid://dsyoepjah36yk
|
||||
@@ -11,6 +11,8 @@ extends Node2D
|
||||
|
||||
const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd")
|
||||
const PropBlockScript := preload("res://scripts/props/prop_block.gd")
|
||||
const StageWallPanelScript := preload("res://scripts/stage/scenery/stage_wall_panel.gd")
|
||||
const StageSceneryArtworkScript := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Enums
|
||||
@@ -42,6 +44,7 @@ const ROTATE_HANDLE_MARGIN_PX := 48.0
|
||||
const ROTATE_RING_TOLERANCE_PX := 12.0
|
||||
const SCALE_CROSS_SIZE_PX := 5.0
|
||||
const SCALE_HIT_RADIUS_PX := 12.0
|
||||
const SCENERY_SCALE_HIT_HALF_SIZE_PX := 22.0
|
||||
const MIN_SCALE: float = 0.05
|
||||
const MAX_SCALE: float = 20.0
|
||||
|
||||
@@ -55,6 +58,7 @@ var camera: Camera2D = null
|
||||
var snap_size: float = 0.0
|
||||
|
||||
var _enabled: bool = true
|
||||
var _transform_handles_visible: bool = true
|
||||
var _targets: Array[Node2D] = []
|
||||
var _hovered: Node2D = null
|
||||
var _dragging: Handle = Handle.NONE
|
||||
@@ -91,6 +95,11 @@ func set_enabled(enabled: bool) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func set_transform_handles_visible(visible: bool) -> void:
|
||||
_transform_handles_visible = visible
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func set_snap_size(size: float) -> void:
|
||||
snap_size = maxf(size, 0.0)
|
||||
|
||||
@@ -102,12 +111,18 @@ func set_box_rect(rect: Rect2) -> void:
|
||||
|
||||
## Returns ROTATE if the cursor is on the rotate ring (single selection only).
|
||||
func hit_test(world_pos: Vector2) -> Handle:
|
||||
if not _enabled or _targets.size() != 1:
|
||||
if not _enabled or not _transform_handles_visible or _targets.size() != 1:
|
||||
return Handle.NONE
|
||||
var target := _targets[0]
|
||||
if target == null or not is_instance_valid(target):
|
||||
return Handle.NONE
|
||||
var rect := STAGE_SELECTION.get_world_aabb(target)
|
||||
var rect: Rect2 = _target_bounds(target)
|
||||
if _is_scenery_target(target):
|
||||
for corner: Vector2 in _corners(rect):
|
||||
var offset: Vector2 = (world_pos - corner) * _zoom()
|
||||
if absf(offset.x) <= SCENERY_SCALE_HIT_HALF_SIZE_PX and absf(offset.y) <= SCENERY_SCALE_HIT_HALF_SIZE_PX:
|
||||
return Handle.SCALE
|
||||
return Handle.NONE
|
||||
if target is PropBlockScript:
|
||||
for corner: Vector2 in _corners(rect):
|
||||
if world_pos.distance_to(corner) <= SCALE_HIT_RADIUS_PX / _zoom():
|
||||
@@ -118,12 +133,29 @@ func hit_test(world_pos: Vector2) -> Handle:
|
||||
return Handle.NONE
|
||||
|
||||
|
||||
func get_scenery_resize_corner(world_pos: Vector2) -> Vector2:
|
||||
if not _enabled or not _transform_handles_visible or _targets.size() != 1 or not _is_scenery_target(_targets[0]):
|
||||
return Vector2.INF
|
||||
var bounds: Rect2 = _target_bounds(_targets[0])
|
||||
var closest: Vector2 = Vector2.INF
|
||||
var closest_distance: float = INF
|
||||
for corner: Vector2 in _corners(bounds):
|
||||
var offset: Vector2 = (world_pos - corner) * _zoom()
|
||||
if absf(offset.x) > SCENERY_SCALE_HIT_HALF_SIZE_PX or absf(offset.y) > SCENERY_SCALE_HIT_HALF_SIZE_PX:
|
||||
continue
|
||||
var distance: float = offset.length_squared()
|
||||
if distance < closest_distance:
|
||||
closest = corner
|
||||
closest_distance = distance
|
||||
return closest
|
||||
|
||||
|
||||
## Begins a rotate drag (used when the ring is clicked).
|
||||
func begin_drag(world_pos: Vector2) -> void:
|
||||
_dragging = hit_test(world_pos)
|
||||
_drag_last = world_pos
|
||||
if _dragging == Handle.ROTATE and _targets.size() == 1 and is_instance_valid(_targets[0]):
|
||||
_rotate_center = STAGE_SELECTION.get_world_aabb(_targets[0]).get_center()
|
||||
_rotate_center = _target_bounds(_targets[0]).get_center()
|
||||
_rotate_start_rotation = _targets[0].global_rotation
|
||||
_rotate_start_angle = (world_pos - _rotate_center).angle()
|
||||
transform_started.emit(_targets.duplicate())
|
||||
@@ -220,16 +252,21 @@ func _draw() -> void:
|
||||
return
|
||||
var zoom := _zoom()
|
||||
if _hovered != null and is_instance_valid(_hovered) and not _hovered.is_queued_for_deletion() and not _targets.has(_hovered):
|
||||
draw_rect(STAGE_SELECTION.get_world_aabb(_hovered), HOVER_COLOR, false, HOVER_WIDTH / zoom)
|
||||
draw_rect(_target_bounds(_hovered), HOVER_COLOR, false, HOVER_WIDTH / zoom)
|
||||
# A selection outline for every selected object.
|
||||
for node: Node2D in _targets:
|
||||
if node != null and is_instance_valid(node):
|
||||
draw_rect(STAGE_SELECTION.get_world_aabb(node), SELECTION_COLOR, false, SELECTION_WIDTH / zoom)
|
||||
draw_rect(_target_bounds(node), SELECTION_COLOR, false, SELECTION_WIDTH / zoom)
|
||||
# Rotate ring only for a single selection.
|
||||
if _targets.size() == 1 and is_instance_valid(_targets[0]):
|
||||
var rect := STAGE_SELECTION.get_world_aabb(_targets[0])
|
||||
draw_arc(rect.get_center(), _rotate_ring_radius(rect), 0.0, TAU, 64, ROTATE_COLOR, ROTATE_WIDTH / zoom, true)
|
||||
if _targets[0] is PropBlockScript:
|
||||
if _transform_handles_visible and _targets.size() == 1 and is_instance_valid(_targets[0]):
|
||||
var rect: Rect2 = _target_bounds(_targets[0])
|
||||
if not _is_scenery_target(_targets[0]):
|
||||
draw_arc(rect.get_center(), _rotate_ring_radius(rect), 0.0, TAU, 64, ROTATE_COLOR, ROTATE_WIDTH / zoom, true)
|
||||
if _is_scenery_target(_targets[0]):
|
||||
for corner: Vector2 in _corners(rect):
|
||||
draw_circle(corner, 11.0 / zoom, Color("#111821E8"))
|
||||
draw_circle(corner, 7.0 / zoom, Color("#F5F7FA"))
|
||||
elif _targets[0] is PropBlockScript:
|
||||
var half: float = SCALE_CROSS_SIZE_PX / zoom
|
||||
for corner: Vector2 in _corners(rect):
|
||||
draw_line(corner + Vector2(-half, 0.0), corner + Vector2(half, 0.0), Color(0.0, 0.0, 0.0, 0.85), 3.5 / zoom)
|
||||
@@ -261,6 +298,18 @@ func _snap(v: Vector2) -> Vector2:
|
||||
func _corners(rect: Rect2) -> Array[Vector2]:
|
||||
return [rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)]
|
||||
|
||||
|
||||
func _is_scenery_target(node: Node2D) -> bool:
|
||||
return node != null and (node.get_script() == StageWallPanelScript or node.get_script() == StageSceneryArtworkScript)
|
||||
|
||||
|
||||
func _target_bounds(node: Node2D) -> Rect2:
|
||||
if node != null and node.get_script() == StageWallPanelScript:
|
||||
return node.global_transform * (node as StageWallPanel).get_local_bounds()
|
||||
if node != null and node.get_script() == StageSceneryArtworkScript:
|
||||
return (node as StageSceneryArtwork).get_visual_world_bounds()
|
||||
return STAGE_SELECTION.get_world_aabb(node)
|
||||
|
||||
func _nearest_corner(rect: Rect2, world_pos: Vector2) -> Vector2:
|
||||
var nearest: Vector2 = rect.position
|
||||
var distance: float = INF
|
||||
|
||||
@@ -3,13 +3,17 @@ extends RefCounted
|
||||
## JSON boundary for Sandbox Stage documents. This class deliberately has no
|
||||
## scene-tree dependencies, which makes rejected documents non-destructive.
|
||||
|
||||
const FILE_VERSION: String = "1.4"
|
||||
const FILE_VERSION: String = "1.5"
|
||||
const CONTENT_PATHS := preload("res://scripts/shared/content_paths.gd")
|
||||
const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd")
|
||||
const TRIGGER_REGISTRY := preload("res://scripts/stage/trigger_registry.gd")
|
||||
const SPEECH_PAYLOAD := preload("res://scripts/speech/speech_payload.gd")
|
||||
const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd")
|
||||
const PROP_USE_SPOT_DATA := preload("res://scripts/props/prop_use_spot_data.gd")
|
||||
const WALL_FINISHES: Array[String] = ["finish_wall_paint", "finish_wall_brick", "finish_wall_wood"]
|
||||
const GROUND_FINISHES: Array[String] = ["finish_ground_paint", "finish_ground_brick", "finish_ground_wood", "finish_ground_grass", "finish_ground_dirt", "finish_ground_sidewalk", "finish_ground_asphalt", "finish_ground_legacy"]
|
||||
const SKY_IDS: Array[String] = ["sky_clear_day", "sky_cloudy_day", "sky_sunset", "sky_starry_night", "sky_neutral"]
|
||||
const SCENERY_DEPTHS: Array[String] = ["far", "near", "floor"]
|
||||
|
||||
static func vector_to_data(value: Vector2) -> Dictionary:
|
||||
return {"x": value.x, "y": value.y}
|
||||
@@ -26,6 +30,14 @@ static func data_to_vector(value: Variant, fallback: Vector2) -> Vector2:
|
||||
var data: Dictionary = value
|
||||
return Vector2(float(data["x"]), float(data["y"]))
|
||||
|
||||
static func color_from_data(value: Variant, fallback: Color) -> Color:
|
||||
if not _valid_color(value):
|
||||
return fallback
|
||||
var html_color: String = String(value).strip_edges()
|
||||
if not html_color.begins_with("#"):
|
||||
html_color = "#" + html_color
|
||||
return Color.from_string(html_color, fallback)
|
||||
|
||||
static func encode_queue(actions: Array[Dictionary]) -> Dictionary:
|
||||
var out: Array[Dictionary] = []
|
||||
var warnings: Array[String] = []
|
||||
@@ -171,9 +183,10 @@ static func _migrate_to_current(data: Dictionary) -> Dictionary:
|
||||
prop_entry["wobble_on_impact"] = false
|
||||
_assign_legacy_depths(current.get("objects", []))
|
||||
return current
|
||||
if version not in ["1.0", "1.1", "1.2", "1.3"]:
|
||||
if version not in ["1.0", "1.1", "1.2", "1.3", "1.4"]:
|
||||
return {}
|
||||
var migrated: Dictionary = data.duplicate(true)
|
||||
migrated["sky_id"] = "sky_neutral"
|
||||
for raw: Variant in migrated.get("objects", []):
|
||||
if not raw is Dictionary:
|
||||
continue
|
||||
@@ -187,6 +200,8 @@ static func _migrate_to_current(data: Dictionary) -> Dictionary:
|
||||
object["next_use_spot_number"] = object.get("next_use_spot_number", 1)
|
||||
if not object.has("wobble_on_impact"):
|
||||
object["wobble_on_impact"] = false
|
||||
if String(object.get("type", "")) == "terrain" and String(object.get("asset_id", "")) == "ground":
|
||||
object["finish_id"] = "finish_ground_legacy"
|
||||
migrated["version"] = FILE_VERSION
|
||||
_assign_legacy_depths(migrated.get("objects", []))
|
||||
return migrated
|
||||
@@ -217,6 +232,9 @@ static func _assign_legacy_depths(raw_objects: Variant) -> void:
|
||||
roles["depth"] = PROP_STAGE_ROLES.DEPTH_FRONT if characters_before > characters_after else PROP_STAGE_ROLES.DEPTH_BEHIND
|
||||
|
||||
static func _validate_root(data: Dictionary) -> Dictionary:
|
||||
var sky_id: Variant = data.get("sky_id", "sky_neutral")
|
||||
if not sky_id is String or not SKY_IDS.has(String(sky_id)):
|
||||
return {"ok": false, "error": "Scene Sky is invalid."}
|
||||
var seen: Dictionary = {}
|
||||
var seen_stage_ids: Dictionary = {}
|
||||
for raw: Variant in data["objects"]:
|
||||
@@ -249,8 +267,62 @@ static func _validate_root(data: Dictionary) -> Dictionary:
|
||||
for key: String in ["holds_other_props", "blocks_stickmen", "can_pick_up", "released_blocks_stickmen", "released_bumps_other_props", "legacy_draw_order"]:
|
||||
if roles.has(key) and not roles[key] is bool:
|
||||
return {"ok": false, "error": "Prop role '%s' must be true or false." % key}
|
||||
elif String(entry.get("type", "")) == "wall_panel":
|
||||
if not is_vector_data(entry.get("size")):
|
||||
return {"ok": false, "error": "Wall panel size is invalid."}
|
||||
var wall_size: Vector2 = data_to_vector(entry.get("size"), Vector2.ZERO)
|
||||
if wall_size.x <= 0.0 or wall_size.y <= 0.0 or not WALL_FINISHES.has(String(entry.get("finish_id", ""))):
|
||||
return {"ok": false, "error": "Wall panel size or finish is invalid."}
|
||||
if not _valid_color(entry.get("color", "#5A9D70")):
|
||||
return {"ok": false, "error": "Wall panel color is invalid."}
|
||||
if entry.has("motif_origin_offset") and not is_vector_data(entry.get("motif_origin_offset")):
|
||||
return {"ok": false, "error": "Wall panel motif origin is invalid."}
|
||||
elif String(entry.get("type", "")) == "scenery_artwork":
|
||||
var asset_id: Variant = entry.get("asset_id", "")
|
||||
if not asset_id is String or String(asset_id).strip_edges().is_empty():
|
||||
return {"ok": false, "error": "Scenery artwork reference is invalid."}
|
||||
entry["asset_id"] = CONTENT_PATHS.canonicalize(String(asset_id))
|
||||
if not entry.get("display_name", "Scenery") is String or not SCENERY_DEPTHS.has(String(entry.get("depth", "near"))):
|
||||
return {"ok": false, "error": "Scenery artwork label or depth is invalid."}
|
||||
var art_scale: Vector2 = data_to_vector(entry.get("scale"), Vector2.ZERO)
|
||||
if not is_vector_data(entry.get("scale")) or art_scale.x <= 0.0 or art_scale.y <= 0.0:
|
||||
return {"ok": false, "error": "Scenery artwork scale is invalid."}
|
||||
if not _finite_number(entry.get("rotation", 0.0)):
|
||||
return {"ok": false, "error": "Scenery artwork rotation is invalid."}
|
||||
var bounds_value: Variant = entry.get("last_known_local_bounds", null)
|
||||
if bounds_value != null and not _valid_rect_data(bounds_value):
|
||||
entry.erase("last_known_local_bounds")
|
||||
elif String(entry.get("type", "")) == "terrain" and String(entry.get("asset_id", "")) == "ground":
|
||||
var finish: String = String(entry.get("finish_id", "finish_ground_legacy"))
|
||||
if not GROUND_FINISHES.has(finish):
|
||||
return {"ok": false, "error": "Ground finish is invalid."}
|
||||
return {"ok": true, "error": ""}
|
||||
|
||||
|
||||
static func _valid_color(value: Variant) -> bool:
|
||||
if not value is String:
|
||||
return false
|
||||
var raw: String = String(value).strip_edges()
|
||||
if raw.begins_with("#"):
|
||||
raw = raw.substr(1)
|
||||
if raw.length() not in [6, 8]:
|
||||
return false
|
||||
const HEX_DIGITS: String = "0123456789abcdefABCDEF"
|
||||
for index: int in range(raw.length()):
|
||||
if not HEX_DIGITS.contains(raw.substr(index, 1)):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _valid_rect_data(value: Variant) -> bool:
|
||||
if not value is Dictionary:
|
||||
return false
|
||||
var rect_data: Dictionary = value
|
||||
if not is_vector_data(rect_data.get("position")) or not is_vector_data(rect_data.get("size")):
|
||||
return false
|
||||
var size: Vector2 = data_to_vector(rect_data.get("size"), Vector2.ZERO)
|
||||
return size.x > 0.0 and size.y > 0.0
|
||||
|
||||
static func _recover_backup(path: String) -> void:
|
||||
var bak := path + ".bak"
|
||||
if not FileAccess.file_exists(path) and FileAccess.file_exists(bak): DirAccess.rename_absolute(bak, path)
|
||||
|
||||
@@ -19,6 +19,10 @@ signal selection_changed(nodes: Array[Node2D])
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const RAGDOLL_CONTAINER_NAME := "RagdollBodyContainer"
|
||||
const STAGE_WALL_PANEL := preload("res://scripts/stage/scenery/stage_wall_panel.gd")
|
||||
const STAGE_SCENERY_ARTWORK := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd")
|
||||
|
||||
enum SelectionScope { ACTION, SCENERY }
|
||||
|
||||
## Rig-local bounds for stickman hit-testing (mirrors the rig's standing bounds).
|
||||
const RIG_LOCAL_RECT := Rect2(Vector2(-120.0, -1000.0), Vector2(240.0, 1000.0))
|
||||
@@ -32,6 +36,7 @@ var _camera: Camera2D
|
||||
var _selected: Array[Node2D] = []
|
||||
var _hovered: Node2D = null
|
||||
var _primary: Node2D = null
|
||||
var _scope: SelectionScope = SelectionScope.ACTION
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
@@ -53,6 +58,12 @@ func get_primary() -> Node2D:
|
||||
return _primary
|
||||
|
||||
|
||||
func set_scope(scope: SelectionScope) -> void:
|
||||
_scope = scope
|
||||
clear_selection()
|
||||
clear_hover()
|
||||
|
||||
|
||||
func clear_selection() -> void:
|
||||
_selected.clear()
|
||||
_primary = null
|
||||
@@ -110,7 +121,7 @@ func hit_test_all(world_pos: Vector2) -> Array[Node2D]:
|
||||
continue
|
||||
if get_world_aabb(node).has_point(world_pos):
|
||||
hits.append(node)
|
||||
var ragdoll_hit: Node2D = _ragdoll_rig_at(world_pos)
|
||||
var ragdoll_hit: Node2D = _ragdoll_rig_at(world_pos) if _scope == SelectionScope.ACTION else null
|
||||
if ragdoll_hit != null and not hits.has(ragdoll_hit):
|
||||
hits.append(ragdoll_hit)
|
||||
hits.sort_custom(func(a: Node2D, b: Node2D) -> bool:
|
||||
@@ -219,6 +230,11 @@ func _is_selectable(node: Node) -> bool:
|
||||
return false
|
||||
if node.is_in_group("stickman_ragdoll_container") or bool(node.get_meta("stickman_ragdoll_container", false)):
|
||||
return false
|
||||
var is_scenery: bool = node.get_script() == STAGE_WALL_PANEL or node.get_script() == STAGE_SCENERY_ARTWORK
|
||||
if _scope == SelectionScope.ACTION and is_scenery:
|
||||
return false
|
||||
if _scope == SelectionScope.SCENERY and not is_scenery:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ const PROP_LIBRARY := preload("res://scripts/props/prop_library.gd")
|
||||
const STICKMAN_FACTORY := preload("res://scripts/characters/stickman/stickman_factory.gd")
|
||||
const CONTENT_PATHS := preload("res://scripts/shared/content_paths.gd")
|
||||
const TRIGGER_AREA := preload("res://scripts/stage/trigger_area.gd")
|
||||
const STAGE_DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -67,13 +68,14 @@ func get_spawnable_ids() -> Array[String]:
|
||||
|
||||
## UI-safe registry view. Callers receive no templates or mutable registry state.
|
||||
func get_palette_entries() -> Array[Dictionary]:
|
||||
var icons: Dictionary = {"ground":"🧱", "ramp":"📐", "step":"🪜", "prop":"📦", "stickman":"🧍", "area":"🎯"}
|
||||
var categories: Dictionary = {"terrain":"Terrain", "prop":"Props", "stickman":"Cast", "area":"Events"}
|
||||
var icons: Dictionary = {"ground":"🧱", "ramp":"📐", "step":"🪜", "prop":"📦", "stickman":"🧍", "area":"🎯", "scenery":"🌄"}
|
||||
var categories: Dictionary = {"terrain":"Terrain", "prop":"Props", "stickman":"Cast", "area":"Events", "scenery":"Backgrounds"}
|
||||
var result: Array[Dictionary] = []
|
||||
for entry: Dictionary in _registry:
|
||||
var id: String = String(entry.get("id", ""))
|
||||
var kind: String = String(entry.get("kind", ""))
|
||||
result.append({"placement_id":id, "kind":kind, "label":String(entry.get("label", id)), "icon":String(icons.get(id, "")), "category":String(categories.get(kind, "")), "opens_selector":kind == "prop" or kind == "stickman"})
|
||||
result.append({"placement_id":"scenery", "kind":"scenery", "label":"Scenery", "icon":"🌄", "category":"Backgrounds", "opens_selector":true})
|
||||
return result
|
||||
|
||||
|
||||
@@ -328,6 +330,10 @@ func _spawn_terrain(entry: Dictionary, world_position: Vector2) -> TerrainBlock:
|
||||
)
|
||||
block.position = world_position
|
||||
block.spawn_id = String(entry.get("id", ""))
|
||||
if block.spawn_id == "ground":
|
||||
block.finish_id = &"finish_ground_legacy"
|
||||
block.set_finish(&"finish_ground_legacy")
|
||||
STAGE_DRAW_ORDER.apply_terrain(block)
|
||||
return block
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user