rimlike/scenes/entities/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

174 lines
6.8 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.

## Rock entity — mineable by a pawn with a Mine job. Drops a stone Item node
## when mined out.
##
## Mirrors Tree's chopping model; stone is harder so MINE_TICKS is longer.
## A MineProvider (Opus, Phase 4) creates a Job whose INTERACT toil calls
## on_mine_tick() once per sim tick via JobRunner.
##
## World registration (World.register_rock / World.unregister_rock) is called
## here but the methods land in World during Opus integration.
class_name Rock extends Node2D
const TILE_SIZE_PX: int = 16
## Sim ticks to mine a rock at 1× speed (120 ticks = 6 sim seconds at 20 Hz).
## Stone is harder than wood — MINE_TICKS > Tree.CHOP_TICKS.
const MINE_TICKS: int = 120
## Stone Items dropped on a successful mine.
const STONE_DROPS_ON_MINE: int = 1
# ── state ─────────────────────────────────────────────────────────────────────
var 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 this rock. MineProvider
## ignores undesignated rocks (Rimworld parity — pawns don't auto-mine).
var mine_designated: bool = false
# Preloaded scene for spawned stone items.
const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn")
## Rock sprite atlas — re-uses the Grasslands tileset (already imported for the
## decoration overlay). These coords pick standalone single-tile boulders from
## the rock band (x=16..29, y=3 and y=5) — chosen because each has a clean
## green margin on all four sides, so they read as separate small rocks rather
## than chunks of a tiled cluster. The earlier (24,7)/(28,7)/(12,13) coords
## were autotile interior pieces; replaced 2026-05-12 after the user flagged
## the obvious tile-cluster artifacts on small rocks.
##
## Multi-tile cluster formations live at (22..23, 3..4) brown and (30..31, 3..4)
## gray — those are reserved for the upcoming BigRock entity (2×2 boulder).
const _ROCK_TEX: Texture2D = preload("res://art/tiles/FG_Grasslands_Spring.png")
const _ROCK_VARIANT_COORDS: Array[Vector2i] = [
Vector2i(16, 3), # brown round, medium
Vector2i(20, 3), # brown peaked, smaller
Vector2i(16, 5), # brown squat, low
Vector2i(24, 3), # gray round, medium
Vector2i(28, 3), # gray peaked, smaller
Vector2i(24, 5), # gray squat, low
]
# ── lifecycle ─────────────────────────────────────────────────────────────────
func _ready() -> void:
position = _tile_to_world(tile)
_build_sprite()
World.register_rock(self)
## Adds a Sprite2D child with one of the rock variants. Variant chosen
## deterministically from the tile coord so the same tile renders the same
## rock 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: Vector2i = _ROCK_VARIANT_COORDS[(tile.x * 31 + tile.y * 17) % _ROCK_VARIANT_COORDS.size()]
sprite.region_rect = Rect2(coord.x * TILE_SIZE_PX, coord.y * TILE_SIZE_PX, TILE_SIZE_PX, TILE_SIZE_PX)
sprite.centered = true
sprite.offset = Vector2.ZERO # 16×16 tile, sits centered on the tile
add_child(sprite)
func _exit_tree() -> void:
World.unregister_rock(self)
# ── public API ────────────────────────────────────────────────────────────────
## One-shot initialiser. Call after add_child() so _ready() already fired.
func setup(start_tile: Vector2i) -> void:
tile = start_tile
mine_progress = 0
position = _tile_to_world(tile)
queue_redraw()
Audit.log("rock", "spawned at %s" % tile)
## True when the rock hasn't been fully mined yet.
func is_mineable() -> bool:
return mine_progress < MINE_TICKS
## Walk destination for a pawn approaching this rock. Small rocks are walkable
## (Phase 4 simplification), so the pawn stands on the rock tile while mining.
## BigRock overrides this to return a perimeter neighbour because its footprint
## is blocked. MineProvider always asks the entity for this rather than reading
## `.tile` directly, so single Rock and BigRock plug into the same walk toil.
func approach_tile_for(_pawn_tile: Vector2i) -> Vector2i:
return tile
## Called by the INTERACT toil in JobRunner once per sim tick while the pawn
## works this rock. Advances mine_progress and triggers mined() when complete.
func on_mine_tick() -> void:
if not is_mineable():
return
mine_progress += 1
queue_redraw()
if mine_progress >= MINE_TICKS:
mined()
## Drop stone Item(s) and free this node. Called automatically by on_mine_tick()
## but also accessible for scripted removal (debug, storyteller events).
func mined() -> void:
# Single drop lands on the rock's own tile.
var item: Item = ITEM_SCENE.instantiate()
get_parent().add_child(item)
item.setup(Item.TYPE_STONE, 1, tile)
Audit.log("rock", "mined at %s; %d stone drop" % [tile, STONE_DROPS_ON_MINE])
if Audio != null:
Audio.play_sfx(&"mine_tick")
World.clear_designation_at(tile)
queue_free()
# ── save / load ───────────────────────────────────────────────────────────────
func to_dict() -> Dictionary:
return {
"class_id": &"rock",
"tile_x": tile.x,
"tile_y": tile.y,
"mine_progress": mine_progress,
"mine_designated": mine_designated,
}
static func from_dict(d: Dictionary) -> Dictionary:
return {
"tile_x": int(d.get("tile_x", 0)),
"tile_y": int(d.get("tile_y", 0)),
"mine_progress": int(d.get("mine_progress", 0)),
"mine_designated": bool(d.get("mine_designated", false)),
}
# ── render ────────────────────────────────────────────────────────────────────
func _draw() -> void:
# Rock body comes from the Sprite2D child (see _build_sprite).
# This _draw renders only the mine-progress crack overlaid on the sprite.
if mine_progress > 0:
var ratio := float(mine_progress) / float(MINE_TICKS)
var crack_len := ratio * 5.0
draw_line(
Vector2(-1.0, -2.0),
Vector2(-1.0 + crack_len, 1.0),
Color(0.15, 0.12, 0.10, 0.85),
1.5
)
# ── helpers ───────────────────────────────────────────────────────────────────
func _tile_to_world(t: Vector2i) -> Vector2:
return Vector2(
t.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0,
t.y * TILE_SIZE_PX + TILE_SIZE_PX / 2.0
)