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
@@ -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())