Alerts: wire room_too_large, no_stockpile_accepts, bill_blocked
Three alert signals had no UI subscribers — gameplay failures vanished silently. Now all three feed AlertsLog via translator handlers that forward to the generic alert_added sink. - EventBus: new no_stockpile_accepts(item_type, tile) and bill_blocked(recipe_label, reason, focus_tile) signals. - HaulingProvider: per-item-type 30s cooldown; emits when find_best_for scan finishes with viable items but no destinations. - CraftingProvider: per-(workbench, reason) 60s cooldown; emits at the skill_too_low and missing_ingredient continue sites. no_workbench reason declared for future use but not emitted (the iteration shape has no natural site for it). - AlertsLog: connect + disconnect for all three signals using the same has_signal-guarded pattern; translator handlers convert to localized alert_added(severity, text, focus_tile). - AlertsLog catch-up: room_too_large emits during World init, before this CanvasLayer mounts. _catch_up_room_too_large() in _ready scans World.rooms for rooms > ROOM_AUTOROOF_CAP and replays them, so the pre-built cabin's 24-tile-too-large warning lands in the log on every boot. Hauling/bill signals fire at runtime so they need no catch-up. Verified runtime: cabin warning shows up in AlertsLog with severity 'warn' and focus_tile (45, 24) — the cabin top-left. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
335ccf52b2
commit
708080a022
4 changed files with 115 additions and 0 deletions
|
|
@ -12,12 +12,22 @@ class_name CraftingProvider extends WorkProvider
|
|||
## search is global (no per-bench radius restriction — Phase 17 polish item per
|
||||
## docs/architecture.md "Ingredient acquisition radius").
|
||||
##
|
||||
## When a bill cannot proceed it emits EventBus.bill_blocked once per
|
||||
## (workbench × reason) per BILL_BLOCKED_COOLDOWN_TICKS (60 in-game seconds).
|
||||
##
|
||||
## Workbench and Pawn are intentionally duck-typed (no class_name reference) to match
|
||||
## WorkProvider convention and avoid init-order issues. Only Item.Quality and
|
||||
## QualityCalc are referenced by class_name (in job_runner.gd, not here).
|
||||
##
|
||||
## See docs/architecture.md "CraftingProvider" and docs/design.md "Bills".
|
||||
|
||||
## Rate-limit for bill_blocked alerts: one emit per (workbench × reason) per
|
||||
## 60 in-game seconds (20 Hz × 60 s = 1200 ticks).
|
||||
const BILL_BLOCKED_COOLDOWN_TICKS: int = 1200
|
||||
|
||||
## Per-(workbench_id|reason) cooldown map: String → tick at which next emit is allowed.
|
||||
var _bill_blocked_cooldown: Dictionary = {}
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
category = &"crafting"
|
||||
|
|
@ -53,11 +63,13 @@ func find_best_for(pawn) -> Job:
|
|||
|
||||
# Skill threshold check — pawn must meet the bill's minimum.
|
||||
if pawn.get_skill(b.recipe.required_skill) < b.recipe.skill_threshold:
|
||||
_emit_bill_blocked(b.recipe.label, &"skill_too_low", wb)
|
||||
continue
|
||||
|
||||
# Confirm a qualifying ingredient exists on the floor.
|
||||
var src = _find_ingredient_item(b.recipe.ingredient_type)
|
||||
if src == null:
|
||||
_emit_bill_blocked(b.recipe.label, &"missing_ingredient", wb)
|
||||
continue
|
||||
|
||||
# Score: total Manhattan travel distance pawn → ingredient → workbench.
|
||||
|
|
@ -103,3 +115,14 @@ func _find_ingredient_item(item_type: StringName):
|
|||
## Manhattan distance between two Vector2i tile coordinates.
|
||||
func _manhattan(a: Vector2i, b: Vector2i) -> int:
|
||||
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||
|
||||
|
||||
## Emits EventBus.bill_blocked for the given workbench and reason, rate-limited
|
||||
## to once per (workbench × reason) per BILL_BLOCKED_COOLDOWN_TICKS.
|
||||
func _emit_bill_blocked(recipe_label: String, reason: StringName, wb) -> void:
|
||||
var key: String = "%s|%s" % [wb.get_instance_id(), reason]
|
||||
if _bill_blocked_cooldown.get(key, 0) > Sim.tick:
|
||||
return
|
||||
_bill_blocked_cooldown[key] = Sim.tick + BILL_BLOCKED_COOLDOWN_TICKS
|
||||
var focus: Vector2i = wb.get("tile") if wb.get("tile") != null else Vector2i(-1, -1)
|
||||
EventBus.bill_blocked.emit(recipe_label, reason, focus)
|
||||
|
|
|
|||
|
|
@ -11,11 +11,22 @@ class_name HaulingProvider extends WorkProvider
|
|||
## higher-priority destination has space — enabling the "items flow upward"
|
||||
## priority cascade described in design.md.
|
||||
##
|
||||
## When an item needs hauling but no stockpile accepts it, the provider emits
|
||||
## EventBus.no_stockpile_accepts once per item_type per ALERT_COOLDOWN_TICKS
|
||||
## ticks (30 in-game seconds at 20 Hz).
|
||||
##
|
||||
## Pawn is intentionally duck-typed (no class_name reference) to match the
|
||||
## WorkProvider convention and avoid init-order issues.
|
||||
##
|
||||
## See docs/architecture.md "HaulingProvider".
|
||||
|
||||
## Rate-limit for no_stockpile_accepts alerts: one emit per item_type per
|
||||
## 30 in-game seconds (20 Hz × 30 s = 600 ticks).
|
||||
const ALERT_COOLDOWN_TICKS: int = 600
|
||||
|
||||
## Per-item-type cooldown map: StringName → tick at which the next emit is allowed.
|
||||
var _alert_cooldown: Dictionary = {}
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
category = &"haul"
|
||||
|
|
@ -41,6 +52,11 @@ func find_best_for(pawn) -> Job:
|
|||
var best_dist: int = 999999
|
||||
var best_is_corpse: bool = false
|
||||
|
||||
# Tracks the first item_type that needed hauling but had no valid destination.
|
||||
# Used to emit no_stockpile_accepts once per cooldown window.
|
||||
var first_orphan_type: StringName = &""
|
||||
var first_orphan_tile: Vector2i = Vector2i(-1, -1)
|
||||
|
||||
# ── regular items ─────────────────────────────────────────────────────────
|
||||
for item in World.items_needing_haul.keys():
|
||||
# Skip items another pawn is already carrying.
|
||||
|
|
@ -50,6 +66,10 @@ func find_best_for(pawn) -> Job:
|
|||
# Find the best destination for this item type + priority.
|
||||
var dest = _find_best_destination_for(item)
|
||||
if dest == null:
|
||||
# Item needs hauling but no stockpile accepts it — record first occurrence.
|
||||
if first_orphan_type == &"":
|
||||
first_orphan_type = item.item_type
|
||||
first_orphan_tile = item.tile
|
||||
continue
|
||||
|
||||
var drop: Vector2i = dest.find_drop_position(item)
|
||||
|
|
@ -72,6 +92,12 @@ func find_best_for(pawn) -> Job:
|
|||
best_drop_cell = drop
|
||||
best_is_corpse = false
|
||||
|
||||
# Emit alert for the first orphaned item_type, rate-limited per type.
|
||||
if first_orphan_type != &"":
|
||||
if _alert_cooldown.get(first_orphan_type, 0) <= Sim.tick:
|
||||
EventBus.no_stockpile_accepts.emit(first_orphan_type, first_orphan_tile)
|
||||
_alert_cooldown[first_orphan_type] = Sim.tick + ALERT_COOLDOWN_TICKS
|
||||
|
||||
# ── Phase 14: corpses ─────────────────────────────────────────────────────
|
||||
# Corpses route to GraveSlot StorageDestinations exactly like items, but
|
||||
# use PICKUP_CORPSE / DEPOSIT_CORPSE toils (since Corpse is not an Item).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue