rimlike/scenes/entities/rock.gd
megaproxy a4163ba222 Chop/mine designation gate + reachability gates on Doctor & Eat
Player reported pawns ignoring chop designations. Root cause:
ChopProvider/MineProvider iterated World.trees/World.rocks
unconditionally — paint set a null sentinel and never touched the entity,
so designation was cosmetic only. Pawns auto-chopped nearest unfelled tree.

* Added chop_designated: bool to Tree, mine_designated: bool to Rock and
  BigRock (footprint-aware: paint on any of the 4 footprint cells flags
  the boulder). Save/load round-trips the flag.

* world.gd._on_designation_added 'chop'/'mine' cases now find the entity
  at the painted tile and flip the flag. _on_designation_cleared inverts.

* Boot seed auto-designates SAMPLE_TREES / SAMPLE_ROCKS / SAMPLE_BIG_ROCKS
  so the cabin demo still produces wood + stone end-to-end without
  requiring the player to paint first.

Also from the same audit (researcher mapped all 11 WorkProviders):

* DoctorProvider + EatProvider now pre-check reachability with
  pathfinder.find_path before issuing a job, mirroring HaulingProvider's
  pattern. Previously they handed out doomed walks that JobRunner had to
  cancel, busy-spinning at 20 Hz.

Verified end-to-end via MCP runtime: undesignated tree/rock returns null
from provider; paint flips the flag and provider returns a chop/mine job;
un-paint clears the flag; BigRock footprint paint works on any of the 4
cells.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:53:50 +01:00

171 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])
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
)