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
|
||||
Reference in New Issue
Block a user