Each entity completion handler (wall/floor/door/bed/torch/workbench/crate /tree/rock/big_rock/grave_slot) now calls World.clear_designation_at(tile) so the orange/blue/etc. highlight overlay disappears with the job. BigRock iterates its footprint to clear all four tiles. World.designation_ctl is set during the scene boot wire-up; the helper no-ops when the controller is absent (e.g. headless tests).
254 lines
9.6 KiB
GDScript
254 lines
9.6 KiB
GDScript
class_name Torch extends Node2D
|
||
## Torch furniture entity — buildable wall-hung or floor-standing light source.
|
||
##
|
||
## Rendered as a small stick + wrapped head + flame using _draw() in the same
|
||
## 3/4-perspective convention as Wall / Bed / Workbench. Ghost state (40% alpha)
|
||
## while construction is in progress; solid + lit once _completed.
|
||
##
|
||
## Light model (docs/architecture.md "LightingSystem"):
|
||
## Emits a PointLight2D with a procedural radial-gradient texture (soft
|
||
## falloff). The Godot visual light is additive with CanvasModulate for
|
||
## night-time darkness. The sim-side light radius (LIGHT_RADIUS tiles,
|
||
## Manhattan distance) is registered with World.light_sources so that
|
||
## is_tile_lit() can answer "in darkness" thought queries cheaply.
|
||
##
|
||
## Light-source duck-type interface (shared with Hearth / Workbench):
|
||
## is_on() → bool
|
||
## get_light_tile() → Vector2i
|
||
## get_light_radius() → int
|
||
##
|
||
## Build model (same BuildJob interface as Wall / Bed / Workbench):
|
||
## BUILD_TICKS ticks via the standard BuildJob toil; PointLight2D enabled
|
||
## only after _complete().
|
||
##
|
||
## Save/load: to_dict / from_dict capture tile, label_text, build_progress,
|
||
## _completed, _is_on. Phase 16 wires these into the full save layer.
|
||
##
|
||
## World registration: World.register_light_source / World.unregister_light_source
|
||
## called from _ready / _exit_tree.
|
||
|
||
const TILE_SIZE_PX: int = 16
|
||
|
||
## Sim ticks to build a torch (30 ticks ≈ 1.5 sim seconds at 1×).
|
||
const BUILD_TICKS: int = 30
|
||
|
||
## Sim-side light radius in tiles (Manhattan). Max 8 per architecture.md.
|
||
const LIGHT_RADIUS: int = 6
|
||
|
||
## Pixel size of the procedural radial gradient used for the PointLight2D.
|
||
## Larger values give a smoother falloff; 64 is sufficient for a 6-tile radius.
|
||
const LIGHT_TEXTURE_SIZE: int = 64
|
||
|
||
# ── exports ───────────────────────────────────────────────────────────────────
|
||
|
||
## Tile position of this torch in world-tile coordinates.
|
||
@export var tile: Vector2i = Vector2i.ZERO
|
||
|
||
## Player-visible label. Drives Audit logs and job descriptions.
|
||
@export var label_text: String = "Torch"
|
||
|
||
# ── state ─────────────────────────────────────────────────────────────────────
|
||
|
||
## Ticks of construction work applied so far. 0..BUILD_TICKS.
|
||
var build_progress: int = 0
|
||
|
||
## True once build_progress >= BUILD_TICKS.
|
||
var _completed: bool = false
|
||
|
||
## Whether the torch is emitting light. Always true for Phase 11;
|
||
## Phase 17 may add fuel consumption or a player on/off toggle.
|
||
var _is_on: bool = true
|
||
|
||
## PointLight2D child node for Godot's visual lighting pipeline.
|
||
## Built in _ready(); enabled only once _complete() fires.
|
||
var _light: PointLight2D = null
|
||
|
||
|
||
# ── lifecycle ─────────────────────────────────────────────────────────────────
|
||
|
||
func _ready() -> void:
|
||
# Bottom-anchor so Y-sort occludes pawns correctly (same pivot as Wall/Bed).
|
||
position = Vector2(
|
||
tile.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0,
|
||
tile.y * TILE_SIZE_PX + TILE_SIZE_PX
|
||
)
|
||
World.register_light_source(self)
|
||
World.register_build_site(self)
|
||
_light = _build_point_light_2d()
|
||
add_child(_light)
|
||
_light.enabled = false # dark until built
|
||
queue_redraw()
|
||
|
||
|
||
func _exit_tree() -> void:
|
||
World.unregister_light_source(self)
|
||
World.unregister_build_site(self)
|
||
|
||
|
||
## One-shot initialiser. Call after add_child() so _ready() has already fired.
|
||
func setup(p_tile: Vector2i) -> void:
|
||
tile = p_tile
|
||
position = Vector2(
|
||
tile.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0,
|
||
tile.y * TILE_SIZE_PX + TILE_SIZE_PX
|
||
)
|
||
queue_redraw()
|
||
|
||
|
||
# ── BuildJob interface (matches Wall / Bed / Workbench shape) ─────────────────
|
||
|
||
## True while the torch still needs construction work.
|
||
func is_buildable() -> bool:
|
||
return not _completed
|
||
|
||
|
||
## Human-readable label for job descriptions and Audit logs.
|
||
func label() -> String:
|
||
return label_text
|
||
|
||
|
||
## Called by the BUILD toil in JobRunner once per sim tick while the pawn works.
|
||
## Advances build_progress and completes the torch at BUILD_TICKS.
|
||
func on_build_tick() -> void:
|
||
if _completed:
|
||
return
|
||
build_progress += 1
|
||
queue_redraw()
|
||
if build_progress >= BUILD_TICKS:
|
||
_complete()
|
||
|
||
|
||
## True once the torch has been fully built.
|
||
func is_completed() -> bool:
|
||
return _completed
|
||
|
||
|
||
## Torches are walkable — pawns step around them visually but the tile remains
|
||
## passable for pathfinding purposes.
|
||
func blocks_pathing_when_complete() -> bool:
|
||
return false
|
||
|
||
|
||
# ── light-source duck-type interface ──────────────────────────────────────────
|
||
## Used by World.is_tile_lit() and the "in darkness" thought.
|
||
## Hearth / Workbench exposes the same three functions.
|
||
|
||
## True when the torch is built and switched on.
|
||
func is_on() -> bool:
|
||
return _completed and _is_on
|
||
|
||
|
||
## The tile this light source occupies (for distance calculations).
|
||
func get_light_tile() -> Vector2i:
|
||
return tile
|
||
|
||
|
||
## The sim-side Manhattan-distance radius of this light source.
|
||
func get_light_radius() -> int:
|
||
return LIGHT_RADIUS
|
||
|
||
|
||
## Toggle the torch on/off. Phase 17 may call this from a fuel-depletion system
|
||
## or player action; safe to call any time including before _complete().
|
||
func set_on(value: bool) -> void:
|
||
_is_on = value
|
||
if _light != null:
|
||
_light.enabled = _completed and _is_on
|
||
Audit.log("light", "torch at %s set_on → %s" % [tile, value])
|
||
|
||
|
||
# ── save / load ───────────────────────────────────────────────────────────────
|
||
|
||
## Serialise all persistent state for World save (wired in Phase 16).
|
||
func to_dict() -> Dictionary:
|
||
return {
|
||
"class_id": &"torch",
|
||
"tile_x": tile.x,
|
||
"tile_y": tile.y,
|
||
"label_text": label_text,
|
||
"build_progress": build_progress,
|
||
"completed": _completed,
|
||
"is_on": _is_on,
|
||
}
|
||
|
||
|
||
## Restore from a dict produced by to_dict().
|
||
func from_dict(d: Dictionary) -> void:
|
||
tile = Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0)))
|
||
label_text = str(d.get("label_text", "Torch"))
|
||
build_progress = int(d.get("build_progress", 0))
|
||
_completed = bool(d.get("completed", false))
|
||
_is_on = bool(d.get("is_on", true))
|
||
# _light is rebuilt in _ready() — no need to restore the node.
|
||
setup(tile)
|
||
|
||
|
||
# ── render ─────────────────────────────────────────────────────────────────────
|
||
|
||
func _draw() -> void:
|
||
# 3/4-perspective torch — fits within the tile (16×16 local box).
|
||
# Origin (0,0) = tile bottom-centre. Tile spans local Y: -16 to 0.
|
||
# Ghost (not yet built) draws at 0.4 alpha.
|
||
var alpha: float = 1.0 if _completed else 0.4
|
||
|
||
# Stick — narrow vertical brown rect, centred horizontally.
|
||
draw_rect(Rect2(Vector2(-1.0, -9.0), Vector2(2.0, 9.0)), Color(0.35, 0.22, 0.10, alpha))
|
||
|
||
# Wrapped head — slightly wider, darker, at the top of the stick.
|
||
draw_rect(Rect2(Vector2(-2.0, -13.0), Vector2(4.0, 4.0)), Color(0.20, 0.14, 0.06, alpha))
|
||
|
||
# Flame — only when built and on; two overlapping circles for depth.
|
||
if _completed and _is_on:
|
||
draw_circle(Vector2(0.0, -15.0), 2.5, Color(1.0, 0.65, 0.10, alpha))
|
||
draw_circle(Vector2(0.0, -16.0), 1.5, Color(1.0, 0.90, 0.40, alpha))
|
||
|
||
|
||
# ── internal helpers ──────────────────────────────────────────────────────────
|
||
|
||
## Construct and return the PointLight2D that provides Godot-side visual
|
||
## lighting. The light is positioned at the flame, not the tile base.
|
||
func _build_point_light_2d() -> PointLight2D:
|
||
var p := PointLight2D.new()
|
||
p.texture = _build_radial_light_texture(LIGHT_TEXTURE_SIZE)
|
||
# Scale the texture so its radius in world-pixels matches LIGHT_RADIUS tiles.
|
||
p.texture_scale = float(LIGHT_RADIUS) * float(TILE_SIZE_PX) / float(LIGHT_TEXTURE_SIZE) * 2.0
|
||
p.color = Color(1.0, 0.85, 0.55, 1.0) # warm fire tint
|
||
p.energy = 1.2
|
||
# Offset upward so the light originates from the flame position.
|
||
p.position = Vector2(0.0, -15.0)
|
||
return p
|
||
|
||
|
||
## Build a soft radial gradient Image and return it as an ImageTexture.
|
||
## White centre, fades to transparent at the edge via smoothstep falloff.
|
||
## Called once per torch in _ready(); result is ~4 KB of VRAM at 64×64 RGBA8.
|
||
static func _build_radial_light_texture(size: int) -> Texture2D:
|
||
var img := Image.create(size, size, false, Image.FORMAT_RGBA8)
|
||
var cx: float = float(size) / 2.0
|
||
var cy: float = float(size) / 2.0
|
||
var max_r: float = float(size) / 2.0
|
||
for x in size:
|
||
for y in size:
|
||
var dx: float = float(x) - cx
|
||
var dy: float = float(y) - cy
|
||
var d: float = sqrt(dx * dx + dy * dy)
|
||
var t: float = clampf(1.0 - d / max_r, 0.0, 1.0)
|
||
# Smoothstep so the edge is soft rather than a hard circle cutoff.
|
||
var a: float = t * t * (3.0 - 2.0 * t)
|
||
img.set_pixel(x, y, Color(1.0, 1.0, 1.0, a))
|
||
return ImageTexture.create_from_image(img)
|
||
|
||
|
||
## Called when build_progress reaches BUILD_TICKS.
|
||
func _complete() -> void:
|
||
_completed = true
|
||
if _light != null:
|
||
_light.enabled = _is_on
|
||
queue_redraw()
|
||
World.clear_designation_at(tile)
|
||
Audit.log("torch", "built at %s" % tile)
|
||
# Phase 13 — notify BeautySystem so nearby tile beauty scores update.
|
||
var bs = World.get("beauty_system")
|
||
if bs != null:
|
||
bs.register_furniture(self)
|
||
bs.recompute_around(tile)
|