rimlike/scenes/entities/big_rock.gd
megaproxy f67c12c51f Clear designation tile-highlight when jobs complete
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).
2026-05-15 19:31:55 +01:00

250 lines
9.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## BigRock — 2×2 boulder formation. Same mining contract as Rock but occupies
## four tiles, drops four stone, and blocks pathfinding on its footprint so
## pawns route around it.
##
## Duck-typed to Rock's public interface (tile, is_mineable, on_mine_tick,
## approach_tile_for) so MineProvider can scan World.rocks without checking
## the entity's concrete type.
##
## Position semantics (different from single Rock):
## • origin_tile is the TOP-LEFT of the 2×2 footprint (player-friendly anchor).
## • The Sprite2D is 32×32 and is centred at the intersection of the four
## tiles, so its visual centre sits at (origin_tile + (1, 1)) * 16 px.
## • For MineProvider distance, `tile` aliases to origin_tile (cheap, stable).
## • For the pawn's walk destination, approach_tile_for(pawn_tile) returns
## the closest walkable perimeter neighbour, so the pawn stands BESIDE the
## boulder instead of trying to path onto a blocked tile.
class_name BigRock extends Node2D
const TILE_SIZE_PX: int = 16
## Sim ticks to mine a big rock. Roughly 4× a small rock (120 ticks) since the
## footprint represents four tiles' worth of stone.
const MINE_TICKS: int = 480
## Stone Items dropped on a successful mine (one per footprint tile).
const STONE_DROPS_ON_MINE: int = 4
## Footprint dimensions. Locked at 2×2 for the first BigRock pass; if larger
## boulders ever ship, generalise here and the perimeter math in approach_tile_for.
const FOOTPRINT_W: int = 2
const FOOTPRINT_H: int = 2
# Preloaded scene for spawned stone items.
const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn")
## ElvGames Grasslands tileset — 2×2 cluster sprites starting at these
## top-left coords. Visually confirmed against /tmp/rocks_labeled_grid.png
## in the 2026-05-12 visual pass.
const _ROCK_TEX: Texture2D = preload("res://art/tiles/FG_Grasslands_Spring.png")
const _BIG_ROCK_ATLAS_COORDS: Array[Vector2i] = [
Vector2i(22, 3), # brown 2×2 boulder
Vector2i(30, 3), # gray 2×2 boulder
]
# ── state ─────────────────────────────────────────────────────────────────────
## Top-left tile of the 2×2 footprint.
var origin_tile: Vector2i = Vector2i.ZERO
## 0..MINE_TICKS. Advanced by on_mine_tick(); rock is mined when equal to MINE_TICKS.
var mine_progress: int = 0
## True once a player has painted a mine designation on any footprint tile.
## MineProvider ignores undesignated boulders (Rimworld parity).
var mine_designated: bool = false
# ── lifecycle ─────────────────────────────────────────────────────────────────
## All init work happens inside setup() (not _ready) because callers
## always do `add_child(node); node.setup(origin)` — _ready fires inside
## add_child with origin_tile still at its zero default, so anything that
## reads origin_tile from _ready would stamp the pathfinder / position at
## the wrong tile. setup() is the single authoritative entry point.
func _ready() -> void:
pass
func _exit_tree() -> void:
World.unregister_rock(self)
_set_footprint_walkable(true)
# ── public API ────────────────────────────────────────────────────────────────
## One-shot initialiser. Builds the sprite, positions the node, marks the four
## footprint tiles unwalkable, and registers with World.rocks. Call after
## add_child().
func setup(p_origin: Vector2i) -> void:
origin_tile = p_origin
mine_progress = 0
position = Vector2(
(origin_tile.x + 1) * TILE_SIZE_PX,
(origin_tile.y + 1) * TILE_SIZE_PX,
)
_build_sprite()
_set_footprint_walkable(false)
World.register_rock(self)
queue_redraw()
Audit.log("big_rock", "spawned 2×2 at %s" % origin_tile)
## Alias used by MineProvider's distance calc (`rock.tile`). Returns the
## top-left so the manhattan distance is stable; refining to the centre would
## require fractional tiles.
var tile: Vector2i:
get:
return origin_tile
## True when the boulder hasn't been fully mined yet.
func is_mineable() -> bool:
return mine_progress < MINE_TICKS
## Returns the perimeter tile closest to `pawn_tile` that the pawn can stand
## on while mining. Mirrors Rock.approach_tile_for so MineProvider can ask the
## entity for its walk destination without knowing which kind of rock it is.
## Falls back to the origin tile when no perimeter is walkable — the pathfinder
## will then return an empty path and the job runner cancels cleanly.
func approach_tile_for(pawn_tile: Vector2i) -> Vector2i:
var perimeter := _perimeter_tiles()
var best: Vector2i = origin_tile
var best_d: int = 0x7fffffff
var found_any: bool = false
for t in perimeter:
if World.pathfinder == null:
continue
if not World.pathfinder.is_walkable(t):
continue
var d: int = abs(t.x - pawn_tile.x) + abs(t.y - pawn_tile.y)
if d < best_d:
best_d = d
best = t
found_any = true
if not found_any:
return origin_tile
return best
## Called by the INTERACT toil in JobRunner once per sim tick while the pawn
## works this boulder. Advances mine_progress and triggers mined() when done.
func on_mine_tick() -> void:
if not is_mineable():
return
mine_progress += 1
queue_redraw()
if mine_progress >= MINE_TICKS:
mined()
## Drop four stone Items (one per footprint tile) and free this node. Called
## by on_mine_tick() automatically; can also be called for scripted removal.
func mined() -> void:
for ft in footprint_tiles():
var item: Item = ITEM_SCENE.instantiate()
get_parent().add_child(item)
item.setup(Item.TYPE_STONE, 1, ft)
Audit.log("big_rock", "mined 2×2 at %s; %d stone drops" % [origin_tile, STONE_DROPS_ON_MINE])
if Audio != null:
Audio.play_sfx(&"mine_tick")
# BigRocks span 2×2 — clear the designation stamp from every footprint tile.
for ft in footprint_tiles():
World.clear_designation_at(ft)
queue_free()
## The four tiles this boulder occupies (origin + the three south/east neighbours).
func footprint_tiles() -> Array[Vector2i]:
return [
origin_tile,
origin_tile + Vector2i(1, 0),
origin_tile + Vector2i(0, 1),
origin_tile + Vector2i(1, 1),
]
# ── save / load ───────────────────────────────────────────────────────────────
func to_dict() -> Dictionary:
return {
"class_id": &"big_rock",
"origin_x": origin_tile.x,
"origin_y": origin_tile.y,
"mine_progress": mine_progress,
"mine_designated": mine_designated,
}
static func from_dict(d: Dictionary) -> Dictionary:
return {
"origin_x": int(d.get("origin_x", 0)),
"origin_y": int(d.get("origin_y", 0)),
"mine_progress": int(d.get("mine_progress", 0)),
"mine_designated": bool(d.get("mine_designated", false)),
}
# ── render ────────────────────────────────────────────────────────────────────
func _draw() -> void:
# Sprite child draws the boulder body. _draw renders only the mine-progress
# crack overlay so the player sees mining damage.
if mine_progress > 0:
var ratio := float(mine_progress) / float(MINE_TICKS)
var crack_len := ratio * 12.0
draw_line(
Vector2(-4.0, -2.0),
Vector2(-4.0 + crack_len, 4.0),
Color(0.15, 0.12, 0.10, 0.85),
1.5
)
# ── internal ──────────────────────────────────────────────────────────────────
## Build a single 32×32 Sprite2D from a 2×2 region of FG_Grasslands_Spring.
## Variant chosen deterministically from origin_tile so the same boulder renders
## the same colour across boots and load/save.
func _build_sprite() -> void:
var sprite := Sprite2D.new()
sprite.name = "Sprite"
sprite.texture = _ROCK_TEX
sprite.region_enabled = true
var coord_idx: int = (origin_tile.x * 31 + origin_tile.y * 17) % _BIG_ROCK_ATLAS_COORDS.size()
var coord: Vector2i = _BIG_ROCK_ATLAS_COORDS[coord_idx]
sprite.region_rect = Rect2(
coord.x * TILE_SIZE_PX,
coord.y * TILE_SIZE_PX,
TILE_SIZE_PX * FOOTPRINT_W,
TILE_SIZE_PX * FOOTPRINT_H,
)
sprite.centered = true
sprite.offset = Vector2.ZERO
add_child(sprite)
## Mark or unmark the four footprint tiles in the pathfinder. Called from
## _ready / _exit_tree so the boulder appears as an obstacle while it exists.
func _set_footprint_walkable(walkable: bool) -> void:
if World.pathfinder == null:
return
for ft in footprint_tiles():
World.pathfinder.set_cell_walkable(ft, walkable)
## The eight perimeter tiles around the 2×2 footprint (cardinal + diagonals
## of the bounding rect). Used by approach_tile_for to find a stand-and-mine
## tile near the pawn.
func _perimeter_tiles() -> Array[Vector2i]:
var out: Array[Vector2i] = []
# Top + bottom rows.
for dx in range(-1, FOOTPRINT_W + 1):
out.append(origin_tile + Vector2i(dx, -1))
out.append(origin_tile + Vector2i(dx, FOOTPRINT_H))
# Left + right columns (skip the corners — already in the top/bottom rows).
for dy in range(0, FOOTPRINT_H):
out.append(origin_tile + Vector2i(-1, dy))
out.append(origin_tile + Vector2i(FOOTPRINT_W, dy))
return out