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) _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) ## 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 { "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() 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)