fix six critical bugs from audit sprint

save/load round-trip: workbench bills, crop static-method, bed owner,
wolf target now all survive reload via Bill.from_dict reconstruction,
_spawn_crop using setup(), and a new _post_load_resolve_references pass.

PlantProvider: sow path added; consumes 1 grain on a TILLED crop tile.

CraftingProvider: ingredient2 supported via new KIND_DEPOSIT_AT_WB toil
and Workbench.deposited_inputs buffer. Cremation pyre now actually
consumes wood.

HaulingProvider: per-item haul_retry_count + haul_rejected after 3
orphan passes; new EventBus.stockpile_layout_changed resets rejects on
any player stockpile edit.

Storyteller: 14 stubbed event effects implemented. New buff registry
(add_buff/get_buff_multiplier/has_buff, day-prune, save/load) drives
seasonal/resource events. New request_pawn_spawn signal + WANDERER
table for arrivals. New SICK status + 3 mood thoughts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-16 18:06:55 +01:00
parent 00cf8f445d
commit d9638a4ea4
19 changed files with 711 additions and 101 deletions

View file

@ -125,6 +125,12 @@ var current_work_progress: int = 0
## enabled only after _complete() fires.
var _light: PointLight2D = null
## Per-recipe input buffer for multi-ingredient crafts (two-trip strategy).
## Keyed by StringName item_type → int count already deposited.
## Populated by _tick_deposit_at_wb; consumed and cleared when the craft
## completes or is interrupted. Not stocked by single-ingredient recipes.
var deposited_inputs: Dictionary = {}
# ── lifecycle ─────────────────────────────────────────────────────────────────
@ -261,6 +267,30 @@ func on_craft_complete() -> void:
func on_craft_interrupted() -> void:
current_bill = null
current_work_progress = 0
deposited_inputs.clear()
## Called by _tick_deposit_at_wb to stash ingredient1 into the per-recipe buffer.
## Accumulates count; the same item type may be deposited in multiple trips if
## ingredient1_count > 1 (Phase 14: always 1 for corpse, but flexible for future).
func add_deposited_input(item_type: StringName, count: int) -> void:
deposited_inputs[item_type] = deposited_inputs.get(item_type, 0) + count
## Returns true if the deposited_inputs buffer holds at least `count` of `item_type`.
func has_deposited_input(item_type: StringName, count: int) -> bool:
return deposited_inputs.get(item_type, 0) >= count
## Consume (remove) exactly `count` of `item_type` from deposited_inputs.
## Clamps to zero; does nothing if not present.
func consume_deposited_input(item_type: StringName, count: int) -> void:
var held: int = deposited_inputs.get(item_type, 0)
var remaining: int = held - count
if remaining <= 0:
deposited_inputs.erase(item_type)
else:
deposited_inputs[item_type] = remaining
# ── Phase 11: light-source duck-type interface ────────────────────────────────
@ -293,6 +323,11 @@ func to_dict() -> Dictionary:
var bills_data: Array = []
for b in bills:
bills_data.append(b.to_dict())
# Serialise deposited_inputs: StringName keys → int values.
# Store as Array of [String, int] pairs for JSON safety.
var deposited_array: Array = []
for k in deposited_inputs:
deposited_array.append([String(k), deposited_inputs[k]])
return {
"class_id": &"workbench",
"tile_x": tile.x,
@ -303,13 +338,15 @@ func to_dict() -> Dictionary:
"completed": _completed,
"current_work_progress": current_work_progress,
"bills": bills_data,
"deposited_inputs": deposited_array,
}
## Restore from a dict produced by to_dict().
## Bill objects are reconstructed by the caller using Bill.from_dict() once the
## Bill class is registered. current_bill is left null — JobRunner reconnects
## from its own saved state on the next sim tick.
## Bills are reconstructed here using Bill.from_dict(). current_bill is left
## null — JobRunner reconnects from its own saved state on the next sim tick.
const _BILL_SCRIPT: Script = preload("res://scenes/ai/bill.gd")
func from_dict(d: Dictionary) -> void:
tile = Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0)))
label_text = str(d.get("label_text", "Workbench"))
@ -317,8 +354,18 @@ func from_dict(d: Dictionary) -> void:
build_progress = int(d.get("build_progress", 0))
_completed = bool(d.get("completed", false))
current_work_progress = int(d.get("current_work_progress", 0))
# Bills are re-populated by World.load_workbenches() after Bill class loads.
# Raw dicts are kept in the dict; the caller handles reconstruction.
# Reconstruct bills from the saved array. Clear first so this is idempotent.
bills.clear()
for bill_dict in d.get("bills", []):
if bill_dict is Dictionary:
var b: Bill = _BILL_SCRIPT.from_dict(bill_dict)
if b != null:
bills.append(b)
# Restore deposited_inputs from the [String, int] pair array.
deposited_inputs.clear()
for pair in d.get("deposited_inputs", []):
if pair is Array and pair.size() == 2:
deposited_inputs[StringName(str(pair[0]))] = int(pair[1])
setup(tile)