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:
parent
00cf8f445d
commit
d9638a4ea4
19 changed files with 711 additions and 101 deletions
|
|
@ -56,10 +56,17 @@ var _ghost_wanderer_target_day: int = -1
|
|||
## to manually un-pause every threat modal dismissal.
|
||||
var _speed_before_auto_pause: int = -1
|
||||
|
||||
## Timed buff registry for seasonal / resource events.
|
||||
## Each entry: { kind: StringName, multiplier: float, expires_day: int }
|
||||
## Callers query via get_buff_multiplier(kind). Phase 17 events populate this
|
||||
## via _add_buff(); the registry is serialised through save_dict()/apply_dict().
|
||||
var _buffs: Array = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Clock.phase_changed.connect(_on_phase_changed)
|
||||
EventBus.pawn_died.connect(_on_pawn_died)
|
||||
EventBus.sim_tick.connect(_on_sim_tick_buffs)
|
||||
|
||||
|
||||
# ── public API ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -86,6 +93,35 @@ func roll_today() -> void:
|
|||
_do_daily_roll()
|
||||
|
||||
|
||||
## Add a timed multiplier buff for `kind` (e.g. &"harvest", &"chop", &"mine",
|
||||
## &"crop_growth", &"sleep_decay", &"threat_weight"). Lasts `duration_days`
|
||||
## in-game days from the current day. Multiple buffs of the same kind stack
|
||||
## multiplicatively (callers must not assume additive stacking).
|
||||
func add_buff(kind: StringName, multiplier: float, duration_days: int) -> void:
|
||||
var expires: int = Clock.day_index_from_start() + duration_days
|
||||
_buffs.append({"kind": kind, "multiplier": multiplier, "expires_day": expires})
|
||||
Audit.log("storyteller", "buff added: kind=%s ×%.2f expires_day=%d" % [kind, multiplier, expires])
|
||||
|
||||
|
||||
## Returns the combined multiplier for all active buffs of `kind`. Returns 1.0
|
||||
## when no buff is active. Expired buffs are pruned on each sim tick so stale
|
||||
## entries are not present during normal gameplay; callers don't need to guard.
|
||||
func get_buff_multiplier(kind: StringName) -> float:
|
||||
var result: float = 1.0
|
||||
for b in _buffs:
|
||||
if b["kind"] == kind:
|
||||
result *= b["multiplier"]
|
||||
return result
|
||||
|
||||
|
||||
## True if any active buff of `kind` exists. Convenience for simple gate checks.
|
||||
func has_buff(kind: StringName) -> bool:
|
||||
for b in _buffs:
|
||||
if b["kind"] == kind:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## Called by UI (banner/modal) after the player makes a choice or dismisses.
|
||||
func resolve_current(choice_index: int = 0) -> void:
|
||||
if _current_event == null:
|
||||
|
|
@ -114,6 +150,14 @@ func save_dict() -> Dictionary:
|
|||
var cat_fired_str: Dictionary = {}
|
||||
for k: StringName in _category_last_fired:
|
||||
cat_fired_str[String(k)] = _category_last_fired[k]
|
||||
# Serialise buffs: StringName kind → String for JSON.
|
||||
var buffs_ser: Array = []
|
||||
for b in _buffs:
|
||||
buffs_ser.append({
|
||||
"kind": String(b["kind"]),
|
||||
"multiplier": b["multiplier"],
|
||||
"expires_day": b["expires_day"],
|
||||
})
|
||||
return {
|
||||
"tension": tension,
|
||||
"ghost_state": ghost_state,
|
||||
|
|
@ -122,6 +166,7 @@ func save_dict() -> Dictionary:
|
|||
"category_last_fired": cat_fired_str,
|
||||
"ghost_wanderer_target_day": _ghost_wanderer_target_day,
|
||||
"speed_before_auto_pause": _speed_before_auto_pause,
|
||||
"buffs": buffs_ser,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -138,6 +183,14 @@ func apply_dict(d: Dictionary) -> void:
|
|||
_category_last_fired.clear()
|
||||
for k: String in d.get("category_last_fired", {}):
|
||||
_category_last_fired[StringName(k)] = int(d["category_last_fired"][k])
|
||||
# Restore buff registry.
|
||||
_buffs.clear()
|
||||
for b in d.get("buffs", []):
|
||||
_buffs.append({
|
||||
"kind": StringName(b.get("kind", "")),
|
||||
"multiplier": float(b.get("multiplier", 1.0)),
|
||||
"expires_day": int(b.get("expires_day", 0)),
|
||||
})
|
||||
|
||||
|
||||
# ── phase listener — triggers the daily 6 AM roll at dawn ────────────────────
|
||||
|
|
@ -217,9 +270,14 @@ func _compute_weight(def: EventDef) -> float:
|
|||
EventDef.Category.THREAT:
|
||||
# Low tension → boost threats (exciting); high tension → suppress (breathing room).
|
||||
w *= lerp(2.0, 0.3, tension / 100.0)
|
||||
# Winter's Edge buff: raise threat weight for the season duration.
|
||||
w *= get_buff_multiplier(&"threat_weight")
|
||||
EventDef.Category.RESOURCE:
|
||||
# High tension → more positive events to balance.
|
||||
w *= lerp(0.5, 1.5, tension / 100.0)
|
||||
EventDef.Category.WANDERER:
|
||||
# Winter's Edge buff can suppress wanderers (multiplier < 1.0).
|
||||
w *= get_buff_multiplier(&"wanderer_weight")
|
||||
_:
|
||||
pass # Other categories: weight unmodified.
|
||||
|
||||
|
|
@ -322,6 +380,29 @@ func _try_fire_ghost_wanderer() -> void:
|
|||
|
||||
# ── utilities ─────────────────────────────────────────────────────────────────
|
||||
|
||||
## Prune expired buffs once per in-game day (dawn phase, after _do_daily_roll).
|
||||
## Called from _on_phase_changed; also called from _on_sim_tick_buffs for the
|
||||
## very first tick so buffs from save-load are healthy immediately.
|
||||
func _prune_expired_buffs() -> void:
|
||||
var today: int = Clock.day_index_from_start()
|
||||
var before: int = _buffs.size()
|
||||
_buffs = _buffs.filter(func(b: Dictionary) -> bool: return b["expires_day"] > today)
|
||||
var pruned: int = before - _buffs.size()
|
||||
if pruned > 0:
|
||||
Audit.log("storyteller", "%d buff(s) expired" % pruned)
|
||||
|
||||
|
||||
## Sim-tick listener: prune expired buffs and handle seasonal threat bias.
|
||||
## Only does real work once per in-game day to stay cheap.
|
||||
var _last_buff_prune_day: int = -1
|
||||
func _on_sim_tick_buffs(_tick: int) -> void:
|
||||
var today: int = Clock.day_index_from_start()
|
||||
if today == _last_buff_prune_day:
|
||||
return
|
||||
_last_buff_prune_day = today
|
||||
_prune_expired_buffs()
|
||||
|
||||
|
||||
## Convert a Category enum value to the snake-case StringName used in
|
||||
## CATEGORY_COOLDOWN_DAYS and _category_last_fired.
|
||||
func _category_to_str(cat: EventDef.Category) -> StringName:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue