Add new test file for phase 19 scenery with unique identifier

This commit is contained in:
2026-10-17 12:53:38 -04:00
parent 36032ebb1e
commit a0510e6405
74 changed files with 40948 additions and 149 deletions
@@ -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
+323
View File
@@ -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
+36
View File
@@ -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