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:
parent
922f269a6c
commit
a4163ba222
9 changed files with 80 additions and 10 deletions
|
|
@ -34,6 +34,10 @@ func find_best_for(pawn) -> Job:
|
|||
for tree in World.trees:
|
||||
if not tree.is_choppable():
|
||||
continue
|
||||
# Gate on player designation — pawns don't auto-chop undesignated trees.
|
||||
# Boot seed in world.gd auto-designates SAMPLE_TREES so the demo still runs.
|
||||
if not tree.chop_designated:
|
||||
continue
|
||||
if Job.is_target_taken_by_other(tree, pawn):
|
||||
continue
|
||||
var d: int = abs(tree.tile.x - pawn.tile.x) + abs(tree.tile.y - pawn.tile.y)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ func find_best_for(pawn) -> Job:
|
|||
continue
|
||||
if Job.is_target_taken_by_other(p, pawn):
|
||||
continue
|
||||
# Reachability — same pattern as HaulingProvider. Skip patients we
|
||||
# can't path to instead of issuing a doomed walk that JobRunner cancels.
|
||||
if pawn.tile != p.tile and World.pathfinder.find_path(pawn.tile, p.tile).is_empty():
|
||||
continue
|
||||
var d: int = abs(p.tile.x - pawn.tile.x) + abs(p.tile.y - pawn.tile.y)
|
||||
if d < best_dist:
|
||||
best_dist = d
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ func find_best_for(pawn) -> Job:
|
|||
var fp: int = _food_priority(it.item_type)
|
||||
if fp < 0:
|
||||
continue
|
||||
# Reachability — same pattern as HaulingProvider. Skip food we can't
|
||||
# path to instead of returning a doomed walk job.
|
||||
if pawn.tile != it.tile and World.pathfinder.find_path(pawn.tile, it.tile).is_empty():
|
||||
continue
|
||||
var d: int = abs(it.tile.x - pawn.tile.x) + abs(it.tile.y - pawn.tile.y)
|
||||
# Higher food-priority tier beats distance; within same tier nearest wins.
|
||||
if fp > best_priority or (fp == best_priority and d < best_dist):
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ func find_best_for(pawn) -> Job:
|
|||
for rock in World.rocks:
|
||||
if not rock.is_mineable():
|
||||
continue
|
||||
# Gate on player designation — pawns don't auto-mine undesignated rocks.
|
||||
# Boot seed in world.gd auto-designates SAMPLE_ROCKS so the demo still runs.
|
||||
if not rock.mine_designated:
|
||||
continue
|
||||
if Job.is_target_taken_by_other(rock, pawn):
|
||||
continue
|
||||
var d: int = abs(rock.tile.x - pawn.tile.x) + abs(rock.tile.y - pawn.tile.y)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ 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 ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -165,6 +168,7 @@ func to_dict() -> Dictionary:
|
|||
"origin_x": origin_tile.x,
|
||||
"origin_y": origin_tile.y,
|
||||
"mine_progress": mine_progress,
|
||||
"mine_designated": mine_designated,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -173,6 +177,7 @@ static func from_dict(d: Dictionary) -> Dictionary:
|
|||
"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)),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ const STONE_DROPS_ON_MINE: int = 1
|
|||
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")
|
||||
|
|
@ -130,6 +133,7 @@ func to_dict() -> Dictionary:
|
|||
"tile_x": tile.x,
|
||||
"tile_y": tile.y,
|
||||
"mine_progress": mine_progress,
|
||||
"mine_designated": mine_designated,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -138,6 +142,7 @@ static func from_dict(d: Dictionary) -> Dictionary:
|
|||
"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)),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ const STACK_SIZE_PER_DROP: int = 1
|
|||
var tile: Vector2i = Vector2i.ZERO
|
||||
## 0..CHOP_TICKS. Advanced by on_chop_tick(); tree is felled when equal to CHOP_TICKS.
|
||||
var chop_progress: int = 0
|
||||
## True once a player has painted a chop designation on this tree. ChopProvider
|
||||
## ignores undesignated trees (Rimworld parity — pawns don't auto-chop).
|
||||
var chop_designated: bool = false
|
||||
|
||||
# Preloaded scene for spawned wood items.
|
||||
const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn")
|
||||
|
|
@ -125,6 +128,7 @@ func to_dict() -> Dictionary:
|
|||
"tile_x": tile.x,
|
||||
"tile_y": tile.y,
|
||||
"chop_progress": chop_progress,
|
||||
"chop_designated": chop_designated,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -133,6 +137,7 @@ static func from_dict(d: Dictionary) -> Dictionary:
|
|||
"tile_x": int(d.get("tile_x", 0)),
|
||||
"tile_y": int(d.get("tile_y", 0)),
|
||||
"chop_progress": int(d.get("chop_progress", 0)),
|
||||
"chop_designated": bool(d.get("chop_designated", false)),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue