Add Stage Surface Color Dialog and Background Ground Functionality

- Implemented StageSurfaceColorDialog for custom color selection with live preview.
- Created StageBackgroundGround class for non-walkable scenery behind the set.
- Introduced StageRectSurface for shared rectangular geometry and tint preview.
- Added StageSurfacePreview for rendering surface previews.
- Developed comprehensive tests for surface color selection, rendering, and serialization.
This commit is contained in:
2026-10-17 21:00:03 -04:00
parent a0510e6405
commit 26dd9c64e1
39 changed files with 3118 additions and 142 deletions
+2 -1
View File
@@ -13,7 +13,7 @@ const DEFAULT_CONFIG: Dictionary = {
"effects": {"impact_wobble": {"duration": 0.48, "maximum_angle_degrees": 7.0, "sideways_offset": 4.0}},
"grid": {"snap_size": 15.0},
"chrome": {"style_defaults": {}, "top_bar": {"height": 64}, "status_bar": {"height": 38}, "mode_switcher": {"segment_min_width": 124}, "mode_frame": {"border_width": 3}, "mode_badge": {"corner_radius": 12, "padding": 10}, "timestamp_badge": {"corner_radius": 10, "padding": 8, "bg_color": "#171B24F2"}, "tooltip": {"bg_color": "#101010E6", "border_width": 1}, "dialogs": {"queue_size":[680,520], "rule_size":[720,560], "action_size":[560,390], "confirm_size":[620,300], "font_size":22, "message_font_size":26}},
"popups": {"style_defaults": {"bg_color": "#202633FA", "border_color": "#FFFFFF33", "corner_radius": 12, "padding": 12}, "action_menu": {"font_size": 24}, "dialog": {}, "asset_selector": {"backdrop_alpha": 0.55, "cell_size":[190,220], "thumbnail_size":[156,156]}},
"popups": {"style_defaults": {"bg_color": "#202633FA", "border_color": "#FFFFFF33", "corner_radius": 12, "padding": 12}, "action_menu": {"font_size": 24}, "dialog": {"surface_color_size":[560,640], "surface_color_preview_size":[420,140], "surface_color_picker_min_size":[320,340], "action_min_target_size":48}, "asset_selector": {"backdrop_alpha": 0.55, "cell_size":[190,220], "thumbnail_size":[156,156], "surface_preview_size":[420,132], "look_cue_size":[80,42], "setup_gap":10}},
"palette": {"style_defaults": {}, "dock_width": 116, "card_size": [96, 94], "icon_size": 30, "drag_threshold": 8, "selected_border_width": 3},
"stage_panels": {
"shell": {"preferred_width": 380, "edge_inset": 12, "padding": 12, "header_gap": 8, "section_gap": 12, "corner_radius": 10, "surface_color": "#252B38FA", "border_color": "#FFFFFF33"},
@@ -24,6 +24,7 @@ const DEFAULT_CONFIG: Dictionary = {
"prop": {"stickman_cue_color": "#F5F7FA", "prop_cue_color": "#B7824D", "cue_outline_color": "#171B24"},
"spots": {"row_gap": 8, "pin_visual_size": 30, "pin_hit_size": 44, "pin_outline_width": 2, "focus_ring_width": 3, "name_pill_padding": 10, "pin_surface_color": "#F4F7FB", "pin_outline_color": "#171B24", "completion_duration": 0.6}
},
"stage_surface_colors": {"section_gap": 8, "swatch_gap": 8, "swatch_size": 48, "swatch_border": "#FFFFFFB8", "swatch_focus_border": "#FFFFFF", "swatch_corner_radius": 8, "swatch_border_width": 2, "swatch_check_size": 24, "selected_border_width": 4, "focus_border_width": 3},
"overlays": {"style_defaults": {"line_width": 2, "dash_length": 8, "dash_gap": 5}, "terrain_guide": {"color": "#22C6FF"}, "completed_path_alpha": 0.20, "completed_path_color": "#959EAD", "rule_connector_color": "#AAB2C0", "rule_action_color": "#7E57C2", "valid": {"color": "#4DE66680"}, "same_type": {"color": "#FFFFFF26"}, "invalid": {"color": "#FF4D4D80"}, "set_down": {"unknown_color": "#C5CAD4", "reach_color": "#FFB300", "ghost_alpha": 0.36}, "use_preview": {"valid_color": "#42D887", "invalid_color": "#F06464", "uncertain_color": "#FFB300", "neutral_color": "#C5CAD4", "completion_duration": 0.6}},
"widgets": {"swatches": ["#FFFFFF", "#111111", "#22C6FF", "#FFB300", "#33DD77", "#FF4D4D"], "size_presets": [{"id":"small", "label":"Small", "value":20}, {"id":"medium", "label":"Medium", "value":28}, {"id":"huge", "label":"HUGE", "value":40}], "emoji": ["😀", "😂", "😍", "😮", "😢", "😡", "👍", "❤️"]},
"speech": {"toolbar": {"surface":"#1C2431F7", "border":"#9EADBF", "selection_halo":"#FFD84D", "gap":10, "max_width":480, "min_hit_height":48}, "ui": {"panel_width": 440, "button_min_height": 44, "heading_size": 24}, "layout": {"max_width": 420, "stack_gap": 12, "screen_margin": 18, "bubble_padding": 18}, "animation": {"letter_interval": 0.045, "word_interval": 0.20, "pop_duration": 0.24}, "styles": {"talk": {"fill":"#FFFDF6", "outline":"#202020", "tail":"talk"}, "shout": {"fill":"#FFD642", "outline":"#D92F26", "tail":"shout"}, "think": {"fill":"#E9F5FF", "outline":"#37698B", "tail":"think"}, "whisper": {"fill":"#F2F2F2", "outline":"#6D7885", "tail":"whisper"}, "sfx": {"fill":"#FFDD39", "outline":"#D92F26", "tail":"none"}}, "text_colors": [{"name":"Ink","color":"#202020"},{"name":"Midnight","color":"#111827"},{"name":"Slate","color":"#475569"},{"name":"Snow","color":"#FFFFFF"},{"name":"Cloud","color":"#CBD5E1"},{"name":"Sky","color":"#2196F3"},{"name":"Ocean","color":"#0077B6"},{"name":"Teal","color":"#00897B"},{"name":"Leaf","color":"#2E7D32"},{"name":"Lime","color":"#8BC34A"},{"name":"Sunshine","color":"#FFC107"},{"name":"Orange","color":"#F57C00"},{"name":"Sunset red","color":"#E53935"},{"name":"Rose","color":"#D81B60"},{"name":"Violet","color":"#7E57C2"},{"name":"Brown","color":"#795548"}], "preview_ghost":"#FFFFFF99", "connector":"#FFFFFFB0"}
@@ -37,9 +37,37 @@ func configure(entry: Dictionary) -> void:
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)
# The preview rect is already small and clipped, so draw its full pattern directly.
SURFACE_RENDERER.draw_wall(
self,
Rect2(Vector2.ZERO, size),
_preview_id,
wall_tint,
Vector2.ZERO,
Vector2.ONE,
false
)
elif _background_kind == "wall_setup":
var finishes: Array[StringName] = [&"finish_wall_paint", &"finish_wall_brick", &"finish_wall_wood"]
_draw_finish_samples(finishes, true)
elif _background_kind == "background_ground_setup":
var finishes: Array[StringName] = [&"finish_background_grass", &"finish_background_concrete", &"finish_background_carpet"]
_draw_finish_samples(finishes, false)
func _notification(what: int) -> void:
if what == NOTIFICATION_RESIZED:
queue_redraw()
func _draw_finish_samples(finishes: Array[StringName], wall: bool) -> void:
var gap: float = 2.0
var sample_width: float = (size.x - gap * float(finishes.size() - 1)) / float(finishes.size())
for index: int in finishes.size():
var sample_rect := Rect2(Vector2(float(index) * (sample_width + gap), 0.0), Vector2(sample_width, size.y))
var finish: StringName = finishes[index]
var tint: Color = SURFACE_RENDERER.default_tint(finish)
if wall:
SURFACE_RENDERER.draw_wall(self, sample_rect, finish, tint, Vector2.ZERO, Vector2.ONE, false)
else:
SURFACE_RENDERER.draw_background(self, sample_rect, finish, tint, Vector2.ZERO, Vector2.ONE, false)
@@ -0,0 +1,15 @@
class_name StageBackgroundGround
extends "res://scripts/stage/scenery/stage_rect_surface.gd"
## Non-walkable rectangular scenery surface behind the set.
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
const DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd")
func configure(new_size: Vector2, finish: StringName, color: Color) -> void:
super.configure(new_size, finish if SURFACE_RENDERER.is_background_finish(finish) else &"finish_background_grass", color)
z_index = DRAW_ORDER.BACKGROUND_GROUND
func _draw() -> void:
SURFACE_RENDERER.draw_background(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world())
@@ -0,0 +1 @@
uid://bl3tfgsieeq5x
@@ -0,0 +1,39 @@
class_name StageRectSurface
extends Node2D
## Shared rectangular geometry and transient tint preview for inert Stage scenery.
var panel_size: Vector2 = Vector2(320.0, 400.0)
var finish_id: StringName = &""
var tint: Color = Color.WHITE
var _preview_tint: Color = Color.WHITE
var _has_preview_tint: bool = false
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
tint = color
queue_redraw()
func get_local_bounds() -> Rect2:
return Rect2(-panel_size * 0.5, panel_size)
func set_preview_tint(color: Color) -> void:
_preview_tint = color
_has_preview_tint = true
queue_redraw()
func clear_preview_tint() -> void:
_has_preview_tint = false
queue_redraw()
func get_render_tint() -> Color:
return _preview_tint if _has_preview_tint else tint
func get_surface_draw_origin_world() -> Vector2:
return global_position
@@ -0,0 +1 @@
uid://sk8pkbk41ef0
@@ -0,0 +1,28 @@
class_name StageSurfacePreview
extends Control
## Small render-only preview that uses the Stage surface motif renderer.
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
var surface_kind: StringName = &"wall"
var finish_id: StringName = &"finish_wall_paint"
var tint: Color = Color("#5A9D70")
func configure(kind: StringName, finish: StringName, color: Color) -> void:
surface_kind = kind
finish_id = finish
tint = color
mouse_filter = Control.MOUSE_FILTER_IGNORE
queue_redraw()
func _draw() -> void:
var rect: Rect2 = Rect2(Vector2(3.0, 3.0), size - Vector2(6.0, 6.0))
if rect.size.x <= 0.0 or rect.size.y <= 0.0:
return
if surface_kind == &"background_ground":
SURFACE_RENDERER.draw_background(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false)
else:
SURFACE_RENDERER.draw_wall(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false)
draw_rect(rect, Color("#F5F7FA"), false, 2.0)
@@ -0,0 +1 @@
uid://kyveykcipe5c
+589 -15
View File
@@ -3,17 +3,37 @@ 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 BACKGROUND_FINISHES: Array[StringName] = [&"finish_background_grass", &"finish_background_concrete", &"finish_background_carpet"]
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
const WALL_BRICK_WIDTH: float = 100.0
const WALL_BRICK_HEIGHT: float = 38.0
const MAX_WALL_BRICK_CELLS: int = 1200
const MAX_BACKGROUND_CELLS: int = 12000
static func is_wall_finish(id: StringName) -> bool:
return WALL_FINISHES.has(id)
static func is_background_finish(id: StringName) -> bool:
return BACKGROUND_FINISHES.has(id)
static func default_tint(finish: StringName) -> Color:
match finish:
&"finish_wall_paint": return Color("#5A9D70")
&"finish_wall_brick": return Color("#B95F3D")
&"finish_wall_wood": return Color("#C87137")
&"finish_background_grass": return Color("#72A84C")
&"finish_background_concrete": return Color("#A6AAA3")
&"finish_background_carpet": return Color("#8B537B")
_: return Color("#5A9D70")
static func is_ground_finish(id: StringName) -> bool:
return GROUND_FINISHES.has(id) or id == &"finish_ground_legacy"
@@ -24,9 +44,10 @@ static func draw_wall(
finish: StringName,
tint: Color,
world_origin: Vector2,
drawing_scale: Vector2 = Vector2.ONE
drawing_scale: Vector2 = Vector2.ONE,
cull_to_viewport: bool = true
) -> void:
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, true)
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, true, false, cull_to_viewport)
static func draw_ground(
@@ -37,7 +58,19 @@ static func draw_ground(
world_origin: Vector2,
drawing_scale: Vector2 = Vector2.ONE
) -> void:
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false)
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false, false, true)
static func draw_background(
canvas: CanvasItem,
rect: Rect2,
finish: StringName,
tint: Color,
world_origin: Vector2,
drawing_scale: Vector2 = Vector2.ONE,
cull_to_viewport: bool = true
) -> void:
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false, true, cull_to_viewport)
static func _draw_surface(
@@ -47,7 +80,9 @@ static func _draw_surface(
tint: Color,
world_origin: Vector2,
drawing_scale: Vector2,
wall: bool
wall: bool,
background: bool,
cull_to_viewport: bool
) -> void:
var normalized := rect.abs()
if normalized.size.x <= 0.0 or normalized.size.y <= 0.0:
@@ -57,11 +92,15 @@ static func _draw_surface(
if finish == &"finish_ground_legacy":
base = Color("#468B42")
elif finish.ends_with("brick"):
base = Color("#B95F3D") if wall else Color("#994D35")
base = Color("#D2CBAF") if wall else Color("#994D35")
elif finish.ends_with("wood"):
base = Color("#9A673C") if wall else Color("#B88951")
elif finish == &"finish_ground_grass":
elif finish == &"finish_ground_grass" or finish == &"finish_background_grass":
base = Color("#72A84C")
elif finish == &"finish_background_concrete":
base = Color("#A6AAA3")
elif finish == &"finish_background_carpet":
base = tint
elif finish == &"finish_ground_dirt":
base = Color("#8E633E")
elif finish == &"finish_ground_sidewalk":
@@ -70,13 +109,39 @@ static func _draw_surface(
base = Color("#505A62")
canvas.draw_rect(normalized, base, true)
if finish.ends_with("brick"):
var view_rect: Rect2 = _visible_local_rect(canvas)
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
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)
var detail_size: Vector2 = view_rect.size if not view_rect.size.is_zero_approx() else normalized.size
var detail_scale: float = brick_detail_scale_for_view(detail_size, motif_scale, wall)
if wall:
_draw_wall_bricks(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint)
else:
_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)
if wall:
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
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 = wood_detail_scale_for_view(pattern_rect.size, motif_scale)
_draw_wall_wood(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint)
else:
_draw_wood(canvas, normalized, world_origin, base, motif_scale)
elif finish == &"finish_background_grass":
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
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:
_draw_background_grass(canvas, pattern_rect, world_origin, motif_scale)
elif finish == &"finish_background_concrete":
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
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:
_draw_background_concrete(canvas, pattern_rect, world_origin, motif_scale)
elif finish == &"finish_background_carpet":
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
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:
_draw_background_carpet(canvas, pattern_rect, world_origin, motif_scale, tint)
elif finish == &"finish_ground_grass":
_draw_grass(canvas, normalized, world_origin, motif_scale)
elif finish == &"finish_ground_dirt":
@@ -89,20 +154,529 @@ static func _draw_surface(
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:
static func brick_detail_scale_for_view(view_size: Vector2, motif_scale: Vector2 = Vector2.ONE, wall: bool = false) -> 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)
var brick_width: float = WALL_BRICK_WIDTH if wall else BRICK_WIDTH
var brick_height: float = WALL_BRICK_HEIGHT if wall else BRICK_HEIGHT
var max_cells: int = MAX_WALL_BRICK_CELLS if wall else MAX_VISIBLE_BRICK_CELLS
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_cells:
return scale
scale *= sqrt(float(cell_count) / float(max_cells)) * 1.01
return scale
static func wood_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 columns: int = int(ceil(safe_size.x * safe_motif_scale.x / (320.0 * scale))) + 2
var rows: int = int(ceil(safe_size.y * safe_motif_scale.y / (64.0 * scale))) + 2
var cell_count: int = rows * columns
if cell_count <= MAX_VISIBLE_BRICK_CELLS:
if cell_count <= MAX_CELLS:
return scale
scale *= sqrt(float(cell_count) / float(MAX_VISIBLE_BRICK_CELLS)) * 1.01
scale *= sqrt(float(cell_count) / float(MAX_CELLS)) * 1.01
return scale
static func _draw_wall_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color) -> void:
const BOARD_HEIGHT: float = 64.0
const PLANK_WIDTH: float = 320.0
var safe_scale: Vector2 = _normalized_scale(motif_scale)
var board_world_height: float = BOARD_HEIGHT * detail_scale
var plank_world_width: float = PLANK_WIDTH * detail_scale
var world_left: float = rect.position.x * safe_scale.x + origin.x
var world_right: float = rect.end.x * safe_scale.x + origin.x
var world_top: float = rect.position.y * safe_scale.y + origin.y
var world_bottom: float = rect.end.y * safe_scale.y + origin.y
var first_row: int = floori(world_top / board_world_height)
var last_row: int = floori((world_bottom - 0.001) / board_world_height)
var row_count: int = mini(last_row - first_row + 1, MAX_CELLS)
var natural: Color = default_tint(&"finish_wall_wood")
var seam_color: Color = _material_color(Color("#784421"), tint, natural)
var grain_color: Color = _material_color(Color("#A05A2C"), tint, natural)
var highlight_color: Color = _material_color(Color("#E39A59"), tint, natural)
for row_offset: int in row_count:
var row: int = first_row + row_offset
var board_world_top: float = float(row) * board_world_height
var board_local_top: float = (board_world_top - origin.y) / safe_scale.y
var stagger: float = plank_world_width * 0.5 if posmod(row, 2) == 1 else 0.0
var first_column: int = floori((world_left - stagger) / plank_world_width)
var last_column: int = floori((world_right - stagger - 0.001) / plank_world_width)
var column_count: int = mini(last_column - first_column + 1, MAX_CELLS)
for column_offset: int in column_count:
var column: int = first_column + column_offset
var plank_world_left: float = float(column) * plank_world_width + stagger
var plank_rect := Rect2(
Vector2((plank_world_left - origin.x) / safe_scale.x, board_local_top),
Vector2(plank_world_width / safe_scale.x, board_world_height / safe_scale.y)
)
var visible_plank: Rect2 = rect.intersection(plank_rect)
if visible_plank.size.x <= 0.0 or visible_plank.size.y <= 0.0:
continue
var tone_index: int = posmod(row + column, 3)
var plank_color: Color = _material_color(Color("#C87137"), tint, natural)
if tone_index == 1:
plank_color = plank_color.darkened(0.045)
elif tone_index == 2:
plank_color = plank_color.lightened(0.035)
canvas.draw_rect(visible_plank, plank_color, true)
var plank_local_left: float = (plank_world_left - origin.x) / safe_scale.x
_draw_clipped_line(
canvas, rect,
Vector2(plank_local_left, board_local_top),
Vector2(plank_local_left, board_local_top + board_world_height / safe_scale.y),
_color_with_alpha(seam_color, 0.88), 2.0 / safe_scale.x
)
_draw_wall_wood_grain(
canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height,
origin, safe_scale, row, column, grain_color, highlight_color, detail_scale, tint, natural
)
_draw_wall_wood_knot(
canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height,
origin, safe_scale, row, column, detail_scale, tint, natural
)
_draw_clipped_line(
canvas, rect,
Vector2(rect.position.x, board_local_top), Vector2(rect.end.x, board_local_top),
seam_color, 2.4 / safe_scale.y
)
_draw_clipped_line(
canvas, rect,
Vector2(rect.position.x, board_local_top + 2.4 / safe_scale.y),
Vector2(rect.end.x, board_local_top + 2.4 / safe_scale.y),
_color_with_alpha(highlight_color, 0.62), 1.0 / safe_scale.y
)
static func _draw_wall_wood_grain(
canvas: CanvasItem,
clip_rect: Rect2,
plank_world_left: float,
board_world_top: float,
plank_world_width: float,
board_world_height: float,
origin: Vector2,
motif_scale: Vector2,
row: int,
column: int,
grain_color: Color,
highlight_color: Color,
detail_scale: float,
tint: Color,
natural: Color
) -> void:
const TRACK_COUNT: int = 5
const SAMPLES: int = 32
var seed: int = row * 73856093 + column * 19349663
for track: int in TRACK_COUNT:
var track_seed: int = seed + track * 83492791
var lane: float = 0.16 + _wood_hash(track_seed) * 0.68
var amplitude: float = (0.55 + _wood_hash(track_seed + 11) * 2.4) * detail_scale
var cycles: float = 1.0 + _wood_hash(track_seed + 23) * 2.6
var phase: float = _wood_hash(track_seed + 37) * TAU
var points := PackedVector2Array()
for sample: int in SAMPLES + 1:
var t: float = float(sample) / float(SAMPLES)
var wave: float = sin(t * TAU * cycles + phase) * amplitude
wave += sin(t * TAU * (cycles * 0.43 + 0.4) + phase * 1.7) * amplitude * 0.32
var world_x: float = plank_world_left + t * plank_world_width
var world_y: float = board_world_top + lane * board_world_height + wave
points.append(Vector2((world_x - origin.x) / motif_scale.x, (world_y - origin.y) / motif_scale.y))
_draw_clipped_polyline(canvas, clip_rect, points, _color_with_alpha(grain_color, 0.19), 5.0 * detail_scale / motif_scale.y)
_draw_clipped_polyline(canvas, clip_rect, points, _color_with_alpha(grain_color, 0.58), 1.15 * detail_scale / motif_scale.y)
var highlight_points := PackedVector2Array()
for point: Vector2 in points:
highlight_points.append(point + Vector2(0.0, -1.0 * detail_scale / motif_scale.y))
_draw_clipped_polyline(canvas, clip_rect, highlight_points, _color_with_alpha(highlight_color, 0.32), 0.9 * detail_scale / motif_scale.y)
static func _draw_wall_wood_knot(
canvas: CanvasItem,
clip_rect: Rect2,
plank_world_left: float,
board_world_top: float,
plank_world_width: float,
board_world_height: float,
origin: Vector2,
motif_scale: Vector2,
row: int,
column: int,
detail_scale: float,
tint: Color,
natural: Color
) -> void:
var seed: int = row * 92821 + column * 68917 + 31
if _wood_hash(seed) > 0.24:
return
var center_world := Vector2(
plank_world_left + plank_world_width * (0.27 + _wood_hash(seed + 1) * 0.46),
board_world_top + board_world_height * (0.34 + _wood_hash(seed + 2) * 0.34)
)
var radius_x: float = (12.0 + _wood_hash(seed + 3) * 12.0) * detail_scale
var radius_y: float = (3.0 + _wood_hash(seed + 4) * 2.8) * detail_scale
var center := Vector2((center_world.x - origin.x) / motif_scale.x, (center_world.y - origin.y) / motif_scale.y)
for ring: int in 3:
var ring_scale: float = 1.0 - float(ring) * 0.27
var points := PackedVector2Array()
for point_index: int in 33:
var angle: float = TAU * float(point_index) / 32.0
var wobble: float = 1.0 + 0.07 * sin(angle * 3.0 + float(seed % 17))
var world_offset := Vector2(
cos(angle) * radius_x * ring_scale * wobble,
sin(angle) * radius_y * ring_scale * (1.0 + 0.08 * cos(angle * 2.0))
)
points.append(center + Vector2(world_offset.x / motif_scale.x, world_offset.y / motif_scale.y))
var ring_source: Color = Color("#784421") if ring != 1 else Color("#E39A59")
var ring_color: Color = _color_with_alpha(_material_color(ring_source, tint, natural), 0.56 if ring != 1 else 0.52)
var ring_width: float = (1.25 if ring != 1 else 0.85) * detail_scale / motif_scale.y
_draw_clipped_polyline(canvas, clip_rect, points, ring_color, ring_width)
static func _draw_clipped_polyline(canvas: CanvasItem, rect: Rect2, points: PackedVector2Array, color: Color, width: float) -> void:
if points.size() < 2:
return
var inset: float = maxf(0.0, width) * 0.5
var safe_rect := Rect2(rect.position + Vector2.ONE * inset, rect.size - Vector2.ONE * inset * 2.0)
var fully_inside: bool = safe_rect.size.x > 0.0 and safe_rect.size.y > 0.0
if fully_inside:
for point: Vector2 in points:
if not safe_rect.has_point(point):
fully_inside = false
break
if fully_inside:
canvas.draw_polyline(points, color, width, true)
return
for index: int in range(points.size() - 1):
_draw_clipped_line(canvas, rect, points[index], points[index + 1], color, width)
static func _wood_hash(seed: int) -> float:
var value: float = sin(float(seed) * 12.9898 + 78.233) * 43758.5453
return value - floorf(value)
static func _color_with_alpha(color: Color, alpha: float) -> Color:
return Color(color.r, color.g, color.b, alpha)
static func _material_color(source: Color, selected: Color, natural: Color) -> Color:
var natural_value: float = maxf(0.08, natural.v)
var source_value_ratio: float = source.v / natural_value
var value: float = clampf(selected.v * source_value_ratio, 0.035, 1.0)
var natural_saturation: float = maxf(0.08, natural.s)
var source_saturation_ratio: float = source.s / natural_saturation
var saturation: float = clampf(selected.s * source_saturation_ratio, 0.0, 1.0)
return Color.from_hsv(selected.h, saturation, value, source.a)
static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void:
const CELL_WIDTH: float = 44.0
const CELL_HEIGHT: float = 36.0
var world_left: float = rect.position.x * motif_scale.x + origin.x
var world_right: float = rect.end.x * motif_scale.x + origin.x
var world_top: float = rect.position.y * motif_scale.y + origin.y
var world_bottom: float = rect.end.y * motif_scale.y + origin.y
var first_column: int = floori(world_left / CELL_WIDTH) - 1
var last_column: int = floori(world_right / CELL_WIDTH) + 1
var first_row: int = floori(world_top / CELL_HEIGHT) - 1
var last_row: int = floori(world_bottom / CELL_HEIGHT) + 1
var remaining: int = MAX_BACKGROUND_CELLS
for row: int in range(first_row, last_row + 1):
for column: int in range(first_column, last_column + 1):
if remaining <= 0:
return
remaining -= 1
var seed: int = row * 73856093 + column * 19349663 + 197
var center_world := Vector2((float(column) + 0.5) * CELL_WIDTH, (float(row) + 0.5) * CELL_HEIGHT)
center_world += Vector2((_wood_hash(seed) - 0.5) * 34.0, (_wood_hash(seed + 1) - 0.5) * 26.0)
var center := Vector2((center_world.x - origin.x) / motif_scale.x, (center_world.y - origin.y) / motif_scale.y)
if not rect.grow(8.0).has_point(center):
continue
var tuft_color: Color = Color("#3A7837").lightened(_wood_hash(seed + 2) * 0.24)
var highlight: Color = Color("#C3DA74").darkened(_wood_hash(seed + 3) * 0.24)
var radius: float = 1.8 + _wood_hash(seed + 4) * 1.6
canvas.draw_circle(center, radius, _color_with_alpha(tuft_color, 0.56))
for blade: int in 4:
var blade_seed: int = seed + blade * 47 + 11
var x_offset: float = (_wood_hash(blade_seed) - 0.5) * 9.0 / motif_scale.x
var y_offset: float = (_wood_hash(blade_seed + 1) - 0.5) * 7.0 / motif_scale.y
var blade_height: float = (4.5 + _wood_hash(blade_seed + 2) * 4.0) / motif_scale.y
var start := center + Vector2(x_offset, y_offset)
var tip := start + Vector2((_wood_hash(blade_seed + 3) - 0.5) * 4.0 / motif_scale.x, -blade_height)
_draw_clipped_line(canvas, rect, start, tip, _color_with_alpha(highlight, 0.78), 1.25 / motif_scale.y)
static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void:
const SLAB_WIDTH: float = 280.0
const SLAB_HEIGHT: float = 190.0
var world_left: float = rect.position.x * motif_scale.x + origin.x
var world_right: float = rect.end.x * motif_scale.x + origin.x
var world_top: float = rect.position.y * motif_scale.y + origin.y
var world_bottom: float = rect.end.y * motif_scale.y + origin.y
var first_column: int = floori(world_left / SLAB_WIDTH)
var last_column: int = floori(world_right / SLAB_WIDTH)
var first_row: int = floori(world_top / SLAB_HEIGHT)
var last_row: int = floori(world_bottom / SLAB_HEIGHT)
var column_gap: float = 1.4 / motif_scale.x
var row_gap: float = 1.4 / motif_scale.y
var remaining: int = MAX_BACKGROUND_CELLS
for column: int in range(first_column, mini(last_column + 1, first_column + MAX_BACKGROUND_CELLS)):
var x: float = float(column) * SLAB_WIDTH
var local_x: float = (x - origin.x) / motif_scale.x
_draw_clipped_line(canvas, rect, Vector2(local_x, rect.position.y), Vector2(local_x, rect.end.y), Color("#68757A", 0.32), column_gap)
for row: int in range(first_row, mini(last_row + 1, first_row + MAX_BACKGROUND_CELLS)):
if remaining <= 0:
return
remaining -= 1
var seed: int = column * 92821 + row * 68917 + 51
var world_point := Vector2(x + 16.0 + _wood_hash(seed) * (SLAB_WIDTH - 32.0), float(row) * SLAB_HEIGHT + 14.0 + _wood_hash(seed + 1) * (SLAB_HEIGHT - 28.0))
var point := Vector2((world_point.x - origin.x) / motif_scale.x, (world_point.y - origin.y) / motif_scale.y)
if rect.grow(3.0).has_point(point):
var speckle: Color = Color("#E2E1D8") if _wood_hash(seed + 2) > 0.48 else Color("#677173")
canvas.draw_circle(point, (0.65 + _wood_hash(seed + 3) * 0.8) / motif_scale.y, _color_with_alpha(speckle, 0.34))
for row: int in range(first_row, mini(last_row + 1, first_row + MAX_BACKGROUND_CELLS)):
var y: float = float(row) * SLAB_HEIGHT
var local_y: float = (y - origin.y) / motif_scale.y
_draw_clipped_line(canvas, rect, Vector2(rect.position.x, local_y), Vector2(rect.end.x, local_y), Color("#68757A", 0.32), row_gap)
static func _draw_background_carpet(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, tint: Color) -> void:
const CELL_WIDTH: float = 34.0
const CELL_HEIGHT: float = 26.0
var world_left: float = rect.position.x * motif_scale.x + origin.x
var world_right: float = rect.end.x * motif_scale.x + origin.x
var world_top: float = rect.position.y * motif_scale.y + origin.y
var world_bottom: float = rect.end.y * motif_scale.y + origin.y
var first_column: int = floori(world_left / CELL_WIDTH)
var last_column: int = floori(world_right / CELL_WIDTH)
var first_row: int = floori(world_top / CELL_HEIGHT)
var last_row: int = floori(world_bottom / CELL_HEIGHT)
var remaining: int = MAX_BACKGROUND_CELLS
var natural: Color = default_tint(&"finish_background_carpet")
for row: int in range(first_row, last_row + 1):
for column: int in range(first_column, last_column + 1):
if remaining <= 0:
return
remaining -= 1
var seed: int = row * 92821 + column * 68917 + 71
var cell_left: float = float(column) * CELL_WIDTH
var cell_top: float = float(row) * CELL_HEIGHT
for fiber: int in 2:
var fiber_seed: int = seed + fiber * 53
var start_world := Vector2(cell_left + 3.0 + _wood_hash(fiber_seed) * (CELL_WIDTH - 6.0), cell_top + 3.0 + _wood_hash(fiber_seed + 1) * (CELL_HEIGHT - 6.0))
var end_world := start_world + Vector2(2.0 + _wood_hash(fiber_seed + 2) * 4.0, (_wood_hash(fiber_seed + 3) - 0.5) * 2.2)
var start := Vector2((start_world.x - origin.x) / motif_scale.x, (start_world.y - origin.y) / motif_scale.y)
var finish := Vector2((end_world.x - origin.x) / motif_scale.x, (end_world.y - origin.y) / motif_scale.y)
var source: Color = Color("#E7B8D2") if _wood_hash(fiber_seed + 4) > 0.52 else Color("#3E3044")
var fiber_color: Color = _material_color(source, tint, natural)
_draw_clipped_line(canvas, rect, start, finish, _color_with_alpha(fiber_color, 0.42), 0.85 / motif_scale.y)
static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color) -> void:
var brick_world_w: float = WALL_BRICK_WIDTH * detail_scale
var brick_world_h: float = WALL_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 world_left: float = rect.position.x * motif_scale.x + origin.x
var world_right: float = rect.end.x * motif_scale.x + origin.x
var world_top: float = rect.position.y * motif_scale.y + origin.y
var world_bottom: float = rect.end.y * motif_scale.y + origin.y
var first_row: int = floori(world_top / brick_world_h)
var last_row: int = floori((world_bottom - 0.001) / brick_world_h)
var mortar_gap := Vector2(3.5 * detail_scale / motif_scale.x, 3.5 * detail_scale / motif_scale.y)
var source_palette: Array[Color] = [
Color("#B65E3E"), Color("#C16B47"), Color("#A95337"),
Color("#BD704B"), Color("#AD583A"), Color("#C47A53"),
]
var palette: Array[Color] = []
for source_color: Color in source_palette:
palette.append(_material_color(source_color, tint, default_tint(&"finish_wall_brick")))
var row_count: int = mini(last_row - first_row + 1, MAX_WALL_BRICK_CELLS)
for row_offset: int in row_count:
var row: int = first_row + row_offset
var row_world_top: float = float(row) * brick_world_h
var row_local_top: float = (row_world_top - origin.y) / motif_scale.y
var stagger_world: float = brick_world_w * 0.5 if posmod(row, 2) == 1 else 0.0
var first_column: int = floori((world_left - stagger_world) / brick_world_w) - 1
var column_count: int = int(ceil(rect.size.x * motif_scale.x / brick_world_w)) + 2
for column_offset: int in column_count:
var column: int = first_column + column_offset
var seed: int = row * 92821 + column * 68917 + 31
var brick_world_left: float = float(column) * brick_world_w + stagger_world
var brick_local_left: float = (brick_world_left - origin.x) / motif_scale.x
var width_variation: float = 0.92 + _wood_hash(seed + 1) * 0.14
var height_variation: float = 0.92 + _wood_hash(seed + 2) * 0.12
var face_size := Vector2(brick_w * width_variation, brick_h * height_variation) - mortar_gap * 2.0
var center_jitter := Vector2(
(_wood_hash(seed + 3) - 0.5) * 2.0 * mortar_gap.x * 0.3,
(_wood_hash(seed + 4) - 0.5) * 2.0 * mortar_gap.y * 0.3
)
var face_rect := Rect2(
Vector2(brick_local_left, row_local_top) + mortar_gap + center_jitter,
face_size
)
if face_rect.end.x < rect.position.x or face_rect.position.x > rect.end.x:
continue
if face_rect.end.y < rect.position.y or face_rect.position.y > rect.end.y:
continue
var roughness := Vector2(1.7 * detail_scale / motif_scale.x, 1.2 * detail_scale / motif_scale.y)
var outline: PackedVector2Array = _rough_brick_outline(face_rect, seed, roughness)
var shadow_offset := Vector2(1.3 * detail_scale / motif_scale.x, 1.8 * detail_scale / motif_scale.y)
var shadow_points: PackedVector2Array = _clip_polygon_to_rect(_offset_polygon(outline, shadow_offset), rect)
if shadow_points.size() >= 3:
canvas.draw_colored_polygon(shadow_points, _color_with_alpha(_material_color(Color("#72503E"), tint, default_tint(&"finish_wall_brick")), 0.40))
var face_points: PackedVector2Array = _clip_polygon_to_rect(outline, rect)
if face_points.size() < 3:
continue
var color_index: int = int(_wood_hash(seed + 5) * float(palette.size()))
var face_color: Color = palette[color_index]
canvas.draw_colored_polygon(face_points, face_color)
var closed_outline := face_points.duplicate()
closed_outline.append(face_points[0])
_draw_clipped_polyline(
canvas, rect, closed_outline, _color_with_alpha(_material_color(Color("#784631"), tint, default_tint(&"finish_wall_brick")), 0.30), 1.2 * detail_scale / motif_scale.y
)
var upper_edge := PackedVector2Array()
for point_index: int in mini(5, outline.size()):
upper_edge.append(outline[point_index])
_draw_clipped_polyline(
canvas, rect, upper_edge, _color_with_alpha(_material_color(Color("#F0B27C"), tint, default_tint(&"finish_wall_brick")), 0.58), 1.15 * detail_scale / motif_scale.y
)
var facet := PackedVector2Array([
Vector2(face_rect.position.x + face_rect.size.x * 0.06, face_rect.position.y + face_rect.size.y * 0.20),
Vector2(face_rect.position.x + face_rect.size.x * 0.40, face_rect.position.y + face_rect.size.y * 0.12),
Vector2(face_rect.position.x + face_rect.size.x * 0.68, face_rect.position.y + face_rect.size.y * 0.22),
Vector2(face_rect.position.x + face_rect.size.x * 0.34, face_rect.position.y + face_rect.size.y * 0.40),
])
var facet_points: PackedVector2Array = _clip_polygon_to_rect(facet, rect)
if facet_points.size() >= 3:
canvas.draw_colored_polygon(facet_points, _color_with_alpha(_material_color(Color("#E49A68"), tint, default_tint(&"finish_wall_brick")), 0.20))
_draw_wall_brick_texture(canvas, rect, face_rect, seed, detail_scale, motif_scale, tint)
static func _rough_brick_outline(rect: Rect2, seed: int, roughness: Vector2) -> PackedVector2Array:
var perimeter: Array[Vector2] = [
Vector2(0.02, 0.05), Vector2(0.22, 0.00), Vector2(0.45, 0.04), Vector2(0.68, 0.00),
Vector2(0.96, 0.03), Vector2(1.00, 0.22), Vector2(0.97, 0.48), Vector2(1.00, 0.73),
Vector2(0.97, 0.95), Vector2(0.72, 1.00), Vector2(0.48, 0.96), Vector2(0.22, 1.00),
Vector2(0.03, 0.96), Vector2(0.00, 0.73), Vector2(0.04, 0.48), Vector2(0.00, 0.22),
]
var points := PackedVector2Array()
for point_index: int in perimeter.size():
var normalized_point: Vector2 = perimeter[point_index]
var jitter := Vector2(
(_wood_hash(seed + point_index * 2 + 11) - 0.5) * 2.0 * roughness.x,
(_wood_hash(seed + point_index * 2 + 12) - 0.5) * 2.0 * roughness.y
)
points.append(rect.position + normalized_point * rect.size + jitter)
return points
static func _draw_wall_brick_texture(
canvas: CanvasItem,
clip_rect: Rect2,
face_rect: Rect2,
seed: int,
detail_scale: float,
motif_scale: Vector2,
tint: Color
) -> void:
var mark_count: int = 4 + int(_wood_hash(seed + 201) * 4.0)
var natural: Color = default_tint(&"finish_wall_brick")
var dark_ochre: Color = _material_color(Color("#74412D"), tint, natural)
var warm_clay: Color = _material_color(Color("#E09A68"), tint, natural)
for mark_index: int in mark_count:
var mark_seed: int = seed + mark_index * 37 + 211
var start_x: float = face_rect.position.x + face_rect.size.x * (0.09 + _wood_hash(mark_seed) * 0.72)
var center_y: float = face_rect.position.y + face_rect.size.y * (0.20 + _wood_hash(mark_seed + 1) * 0.63)
var length: float = face_rect.size.x * (0.08 + _wood_hash(mark_seed + 2) * 0.22)
var wave: float = (0.7 + _wood_hash(mark_seed + 3) * 1.8) * detail_scale / motif_scale.y
var points := PackedVector2Array([
Vector2(start_x, center_y),
Vector2(start_x + length * 0.48, center_y + wave),
Vector2(start_x + length, center_y - wave * 0.35),
])
var light_mark: bool = _wood_hash(mark_seed + 4) > 0.72
var mark_color: Color = warm_clay if light_mark else dark_ochre
var alpha: float = 0.20 + _wood_hash(mark_seed + 5) * 0.25
var width: float = (0.8 + _wood_hash(mark_seed + 6) * 1.6) * detail_scale / motif_scale.y
_draw_clipped_polyline(canvas, clip_rect, points, _color_with_alpha(mark_color, alpha), width)
var pore_count: int = 3 + int(_wood_hash(seed + 401) * 4.0)
for pore_index: int in pore_count:
var pore_seed: int = seed + pore_index * 53 + 419
var pore := Vector2(
face_rect.position.x + face_rect.size.x * (0.10 + _wood_hash(pore_seed) * 0.80),
face_rect.position.y + face_rect.size.y * (0.15 + _wood_hash(pore_seed + 1) * 0.70)
)
var radius: float = (0.55 + _wood_hash(pore_seed + 2) * 0.8) * detail_scale / motif_scale.y
var pore_color: Color = warm_clay if _wood_hash(pore_seed + 3) > 0.62 else dark_ochre
canvas.draw_circle(pore, radius, _color_with_alpha(pore_color, 0.38))
static func _offset_polygon(points: PackedVector2Array, offset: Vector2) -> PackedVector2Array:
var result := PackedVector2Array()
for point: Vector2 in points:
result.append(point + offset)
return result
static func _clip_polygon_to_rect(points: PackedVector2Array, rect: Rect2) -> PackedVector2Array:
var clipped := points
for edge: int in 4:
clipped = _clip_polygon_to_edge(clipped, rect, edge)
if clipped.is_empty():
return clipped
return clipped
static func _clip_polygon_to_edge(points: PackedVector2Array, rect: Rect2, edge: int) -> PackedVector2Array:
var result := PackedVector2Array()
if points.is_empty():
return result
var previous: Vector2 = points[points.size() - 1]
var previous_inside: bool = _point_inside_rect_edge(previous, rect, edge)
for current: Vector2 in points:
var current_inside: bool = _point_inside_rect_edge(current, rect, edge)
if current_inside:
if not previous_inside:
result.append(_rect_edge_intersection(previous, current, rect, edge))
result.append(current)
elif previous_inside:
result.append(_rect_edge_intersection(previous, current, rect, edge))
previous = current
previous_inside = current_inside
return result
static func _point_inside_rect_edge(point: Vector2, rect: Rect2, edge: int) -> bool:
match edge:
0:
return point.x >= rect.position.x
1:
return point.x <= rect.end.x
2:
return point.y >= rect.position.y
_:
return point.y <= rect.end.y
static func _rect_edge_intersection(from: Vector2, to: Vector2, rect: Rect2, edge: int) -> Vector2:
if edge <= 1:
var boundary_x: float = rect.position.x if edge == 0 else rect.end.x
var ratio_x: float = (boundary_x - from.x) / (to.x - from.x)
return Vector2(boundary_x, lerpf(from.y, to.y, ratio_x))
var boundary_y: float = rect.position.y if edge == 2 else rect.end.y
var ratio_y: float = (boundary_y - from.y) / (to.y - from.y)
return Vector2(lerpf(from.x, to.x, ratio_y), boundary_y)
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
+3 -13
View File
@@ -1,27 +1,17 @@
class_name StageWallPanel
extends Node2D
extends "res://scripts/stage/scenery/stage_rect_surface.gd"
## 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
super.configure(new_size, finish if SURFACE_RENDERER.is_wall_finish(finish) else &"finish_wall_paint", 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:
@@ -33,4 +23,4 @@ func get_surface_draw_origin_world() -> Vector2:
func _draw() -> void:
SURFACE_RENDERER.draw_wall(self, get_local_bounds(), finish_id, tint, get_surface_draw_origin_world())
SURFACE_RENDERER.draw_wall(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world())
+1
View File
@@ -3,6 +3,7 @@ extends RefCounted
## Semantic draw bands shared by scenery, terrain, Props, and characters.
const WALL: int = -40
const BACKGROUND_GROUND: int = -50
const FAR: int = -30
const NEAR: int = -20
const TERRAIN: int = -10
+4 -3
View File
@@ -12,6 +12,7 @@ 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 StageBackgroundGroundScript := preload("res://scripts/stage/scenery/stage_background_ground.gd")
const StageSceneryArtworkScript := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd")
# ---------------------------------------------------------------------------
@@ -300,12 +301,12 @@ func _corners(rect: Rect2) -> Array[Vector2]:
func _is_scenery_target(node: Node2D) -> bool:
return node != null and (node.get_script() == StageWallPanelScript or node.get_script() == StageSceneryArtworkScript)
return node != null and (node.get_script() == StageWallPanelScript or node.get_script() == StageBackgroundGroundScript 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 is StageRectSurface:
return node.global_transform * (node as StageRectSurface).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)
+34 -4
View File
@@ -3,7 +3,8 @@ 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.5"
const FILE_VERSION: String = "1.6"
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
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")
@@ -11,6 +12,7 @@ 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 BACKGROUND_FINISHES: Array[String] = ["finish_background_grass", "finish_background_concrete", "finish_background_carpet"]
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"]
@@ -183,10 +185,11 @@ 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", "1.4"]:
if version not in ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5"]:
return {}
var migrated: Dictionary = data.duplicate(true)
migrated["sky_id"] = "sky_neutral"
if version != "1.5":
migrated["sky_id"] = "sky_neutral"
for raw: Variant in migrated.get("objects", []):
if not raw is Dictionary:
continue
@@ -200,13 +203,32 @@ 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":
if version != "1.5" and String(object.get("type", "")) == "terrain" and String(object.get("asset_id", "")) == "ground":
object["finish_id"] = "finish_ground_legacy"
if String(object.get("type", "")) == "wall_panel":
_migrate_legacy_wall_tint(object)
migrated["version"] = FILE_VERSION
_assign_legacy_depths(migrated.get("objects", []))
return migrated
static func _migrate_legacy_wall_tint(entry: Dictionary) -> void:
var finish: StringName = StringName(String(entry.get("finish_id", "finish_wall_paint")))
if finish not in [&"finish_wall_brick", &"finish_wall_wood"]:
if not entry.has("color"):
entry["color"] = "#5A9D70"
return
if not entry.has("color") or _is_legacy_green_tint(entry.get("color")):
entry["color"] = "#" + SURFACE_RENDERER.default_tint(finish).to_html(true)
static func _is_legacy_green_tint(value: Variant) -> bool:
if not value is String:
return false
var raw: String = String(value).strip_edges().trim_prefix("#").to_upper()
return raw == "5A9D70" or raw == "5A9D70FF"
static func _assign_legacy_depths(raw_objects: Variant) -> void:
if not raw_objects is Array:
return
@@ -277,6 +299,14 @@ static func _validate_root(data: Dictionary) -> Dictionary:
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", "")) == "background_ground":
if not is_vector_data(entry.get("size")):
return {"ok": false, "error": "Background ground size is invalid."}
var background_size: Vector2 = data_to_vector(entry.get("size"), Vector2.ZERO)
if background_size.x <= 0.0 or background_size.y <= 0.0 or not BACKGROUND_FINISHES.has(String(entry.get("finish_id", ""))):
return {"ok": false, "error": "Background ground size or finish is invalid."}
if not _valid_color(entry.get("color", "")):
return {"ok": false, "error": "Background ground color 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():
+4 -1
View File
@@ -20,6 +20,7 @@ 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_BACKGROUND_GROUND := preload("res://scripts/stage/scenery/stage_background_ground.gd")
const STAGE_SCENERY_ARTWORK := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd")
enum SelectionScope { ACTION, SCENERY }
@@ -172,6 +173,8 @@ func box_select(rect: Rect2, additive: bool) -> void:
static func get_world_aabb(node: Node2D) -> Rect2:
if node == null or not is_instance_valid(node):
return Rect2()
if node is StageRectSurface:
return node.global_transform * (node as StageRectSurface).get_local_bounds()
# Duck-typed TriggerArea: expose its centered local rect through the transform.
if node.has_method("get_area_rect"):
return node.global_transform * (node.call("get_area_rect") as Rect2)
@@ -230,7 +233,7 @@ 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
var is_scenery: bool = node.get_script() == STAGE_WALL_PANEL or node.get_script() == STAGE_BACKGROUND_GROUND or node.get_script() == STAGE_SCENERY_ARTWORK
if _scope == SelectionScope.ACTION and is_scenery:
return false
if _scope == SelectionScope.SCENERY and not is_scenery: