class_name Bill extends RefCounted ## A bill is one entry in a workbench's recipe queue. ## ## Three modes per design.md "Bill modes (full Rimworld fidelity)": ## FOREVER — keep crafting until the player pauses or removes the bill. ## COUNT — craft exactly `target_count` times then auto-pause. ## UNTIL_N — keep a world-wide stockpile count of `recipe.output_type` at ## >= `target_count`; auto-pauses when the threshold is met, ## resumes when items are consumed below it. ## ## Save/load contract: ## var b2 := Bill.from_dict(b.to_dict()) ## assert(b2.mode == b.mode and b2.completed_count == b.completed_count) enum Mode { FOREVER, ## Craft until paused by player. COUNT, ## Craft exactly target_count times. UNTIL_N, ## Maintain >= target_count of output_type in stockpiles. } ## The recipe this bill executes. Must not be null when the bill is active. var recipe: Recipe = null ## Which termination mode applies. var mode: Mode = Mode.FOREVER ## COUNT: stop after this many completions. UNTIL_N: target world count. var target_count: int = 0 ## Number of successful craft outputs produced (used to gate COUNT mode). var completed_count: int = 0 ## True when the bill is manually paused by the player, or auto-paused by the ## engine (COUNT exhausted, UNTIL_N threshold met). CraftingProvider skips ## paused bills. var paused: bool = false # ── queries ─────────────────────────────────────────────────────────────────── ## Returns true when CraftingProvider should pick up this bill. ## All three mode checks also short-circuit on paused. func is_active() -> bool: if paused: return false match mode: Mode.FOREVER: return true Mode.COUNT: return completed_count < target_count Mode.UNTIL_N: return _world_output_count() < target_count return false # ── state mutation ──────────────────────────────────────────────────────────── ## Called by JobRunner._tick_craft after each successful craft output. ## Increments completed_count and auto-pauses COUNT bills that are exhausted. func record_completion() -> void: completed_count += 1 if mode == Mode.COUNT and completed_count >= target_count: paused = true # ── save / load ─────────────────────────────────────────────────────────────── func to_dict() -> Dictionary: return { "recipe": recipe.to_dict() if recipe != null else null, "mode": mode, "target_count": target_count, "completed_count": completed_count, "paused": paused, } static func from_dict(d: Dictionary) -> Bill: var b := Bill.new() b.mode = int(d.get("mode", Mode.FOREVER)) as Mode b.target_count = int(d.get("target_count", 0)) b.completed_count = int(d.get("completed_count", 0)) b.paused = bool(d.get("paused", false)) var recipe_dict: Variant = d.get("recipe") b.recipe = Recipe.from_dict(recipe_dict) if recipe_dict is Dictionary else null return b # ── helpers ─────────────────────────────────────────────────────────────────── ## Counts world items of recipe.output_type that are not currently being ## carried — used by UNTIL_N mode. Queries World.items directly (no cache). func _world_output_count() -> int: if recipe == null: return 0 var total: int = 0 for item in World.items: if item.item_type == recipe.output_type and not item.being_carried: total += item.stack_size return total