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>
This commit is contained in:
megaproxy 2026-05-15 14:53:50 +01:00
parent 922f269a6c
commit a4163ba222
9 changed files with 80 additions and 10 deletions

View file

@ -409,18 +409,24 @@ func _spawn_sample_harvestables() -> void:
# Untyped vars — Godot's class-name cache for class_name'd classes is
# scan-time and intermittently lags behind file changes. Duck typing is
# safer here and the calls below are all spec'd on the entity types.
# Boot seed auto-designates so the production-chain demo runs end-to-end
# without requiring a player to paint chop/mine first. Real player-painted
# trees / rocks still gate on chop_designated / mine_designated (Rimworld parity).
for t_tile in SAMPLE_TREES:
var tree = TREE_SCENE.instantiate()
add_child(tree)
tree.setup(t_tile)
tree.chop_designated = true
for r_tile in SAMPLE_ROCKS:
var rock = ROCK_SCENE.instantiate()
add_child(rock)
rock.setup(r_tile)
rock.mine_designated = true
for br_origin in SAMPLE_BIG_ROCKS:
var big = BIG_ROCK_SCENE.instantiate()
add_child(big)
big.setup(br_origin)
big.mine_designated = true
Audit.log("world", "spawned %d trees + %d rocks + %d big rocks" % [
SAMPLE_TREES.size(), SAMPLE_ROCKS.size(), SAMPLE_BIG_ROCKS.size()
])
@ -674,16 +680,30 @@ func _on_designation_added(cell: Vector2i, tool: StringName) -> void:
add_child(gs)
gs.setup(cell)
entity = gs
# Phase 17 — chop / mine: designation ghost only; providers auto-scan
# World.trees / World.rocks and will service the nearest entity.
# _build_sites_by_tile tracks the ghost so cancel works.
&"chop", &"mine":
# No entity to spawn — the ghost tile on the designation layer IS the
# marker. Register a sentinel (null body) so _build_sites_by_tile can
# clear the ghost on cancel without double-spawning.
# Chop / mine: flag the Tree / Rock entity at this tile so ChopProvider /
# MineProvider treat it as work. Designation ghost on the TileMap is the
# visual cue; _build_sites_by_tile holds a sentinel so cancel can clear.
&"chop":
for t in World.trees:
if t.tile == cell:
t.chop_designated = true
break
_build_sites_by_tile[cell] = null
Audit.log("world", "designation ghost '%s' at %s (no entity)" % [tool, cell])
return # skip the entity/site registration below
Audit.log("world", "chop designation at %s" % cell)
return
&"mine":
for r in World.rocks:
# BigRock occupies a 2×2 footprint; flag if cell is any of them.
if r.has_method("footprint_tiles"):
if cell in r.footprint_tiles():
r.mine_designated = true
break
elif r.tile == cell:
r.mine_designated = true
break
_build_sites_by_tile[cell] = null
Audit.log("world", "mine designation at %s" % cell)
return
# Phase 17 — crate.
&"build_crate":
entity = CRATE_SCENE.instantiate()
@ -743,8 +763,21 @@ func _on_designation_cleared(cell: Vector2i) -> void:
return
var entity = _build_sites_by_tile[cell]
_build_sites_by_tile.erase(cell)
# Phase 17 — chop/mine designations store null as their sentinel; nothing to free.
# Phase 17 — chop/mine designations store null as their sentinel; nothing to
# free, but clear the corresponding flag on any Tree / Rock at that tile.
if entity == null:
for t in World.trees:
if t.tile == cell:
t.chop_designated = false
break
for r in World.rocks:
if r.has_method("footprint_tiles"):
if cell in r.footprint_tiles():
r.mine_designated = false
break
elif r.tile == cell:
r.mine_designated = false
break
return
if not is_instance_valid(entity):
return