class_name Toil extends RefCounted ## A single atomic step within a Job — walk, wait, idle, etc. ## ## Save/load contract: every value in `data` MUST be JSON-safe. ## Vector2i is NOT JSON-safe in Godot 4 — tile coordinates are stored as ## "to_x"/"to_y" integer keys, never as Vector2i. get_walk_destination() ## reconstructs Vector2i on demand. ## ## Round-trip invariant: ## var t2 := Toil.from_dict(t.to_dict()) ## assert(t2.kind == t.kind and t2.done == t.done and t2.data == t.data) const KIND_WALK: StringName = &"walk" const KIND_WAIT: StringName = &"wait" const KIND_IDLE: StringName = &"idle" const KIND_INTERACT: StringName = &"interact" # Timed action on a target entity (Tree, Rock, …) const KIND_PICKUP: StringName = &"pickup" # Transfer Item at pawn.tile into pawn.carried_item const KIND_DEPOSIT: StringName = &"deposit" # Place pawn.carried_item at pawn.tile const KIND_BUILD: StringName = &"build" # Timed construction on a Wall / Floor / Door entity const KIND_CRAFT: StringName = &"craft" # Timed crafting at a Workbench driven by a Bill const KIND_EAT: StringName = &"eat" # Consume pawn.carried_item and restore hunger const KIND_SLEEP: StringName = &"sleep" # Sleep in a Bed (or on the floor) until pawn.sleep is full var kind: StringName = KIND_IDLE ## Toil-specific params — all values must be int, float, bool, String, Dict, or Array. var data: Dictionary = {} ## Set by JobRunner when this toil is complete. var done: bool = false # ── factories ──────────────────────────────────────────────────────────────── ## Walk to the given tile. Stores coords as separate ints for JSON safety. static func walk_to(tile: Vector2i) -> Toil: var t := Toil.new() t.kind = KIND_WALK t.data = { "to_x": tile.x, "to_y": tile.y, "started": false, } return t ## Pause for `n` sim ticks. static func wait_ticks(n: int) -> Toil: var t := Toil.new() t.kind = KIND_WAIT t.data = {"ticks_remaining": n} return t ## Stand idle — never completes on its own; JobRunner must cancel or replace. static func idle() -> Toil: var t := Toil.new() t.kind = KIND_IDLE t.data = {} return t ## Timed action on a scene-node target (Tree, Rock, …). ## `target_node_path` is the NodePath of the entity; stored as String for JSON safety. ## `tick_method` is the method to call each sim tick (e.g. "on_chop_tick"). ## JobRunner resolves the node at first-tick and calls tick_method every sim tick ## until the target is no longer choppable/mineable (method source sets done via ## is_choppable() / is_mineable() returning false). static func interact(target_node_path: NodePath, tick_method: StringName) -> Toil: var t := Toil.new() t.kind = KIND_INTERACT t.data = { "target": String(target_node_path), "tick_method": String(tick_method), "started": false, } return t ## Pick up an Item at pawn.tile into pawn.carried_item. Single-tick action. ## data is empty — the item is located at pawn.tile at execution time. static func pickup() -> Toil: var t := Toil.new() t.kind = KIND_PICKUP t.data = {} return t ## Construction action on a scene-node target (Wall, Floor, Door, …). ## `target_node_path` is the NodePath of the entity; stored as String for JSON safety. ## JobRunner resolves the node at first-tick and calls on_build_tick() every sim tick ## until is_buildable() returns false (construction complete or site cancelled/removed). static func build_at(target_node_path: NodePath) -> Toil: var t := Toil.new() t.kind = KIND_BUILD t.data = { "target": String(target_node_path), "started": false, } return t ## Place pawn.carried_item at pawn.tile. Single-tick action. ## data is empty — the item comes from pawn.carried_item at execution time. static func deposit() -> Toil: var t := Toil.new() t.kind = KIND_DEPOSIT t.data = {} return t ## Consume pawn.carried_item and restore hunger. Single-tick action. ## data is empty — the item comes from pawn.carried_item at execution time. static func eat() -> Toil: var t := Toil.new() t.kind = KIND_EAT t.data = {} return t ## Sleep in the given Bed until pawn.sleep is full. ## `bed_path` is the NodePath of the Bed entity, stored as String for JSON safety. ## An empty string means "sleep on the floor" — no bed is claimed. ## ## data keys: ## "bed" — String(bed_path); empty means floor sleep. ## "started" — bool; false on first tick, true after claim resolved. ## "ticks_slept" — int; incremented each tick for audit logging + emergency wake. static func sleep_in_bed(bed_path: NodePath) -> Toil: var t := Toil.new() t.kind = KIND_SLEEP t.data = { "bed": String(bed_path), "started": false, "ticks_slept": 0, } return t ## Timed crafting action at a Workbench. ## `workbench_path` is the NodePath of the Workbench entity (stored as String for JSON safety). ## `bill_index` is the index into workbench.bills that this toil should run. ## JobRunner resolves the workbench and bill on the first tick, validates pawn position and ## carried ingredient, then advances workbench.current_work_progress each tick until ## bill.recipe.work_ticks is reached — at which point the ingredient is consumed, an output ## Item is spawned with a quality roll, and bill.record_completion() is called. static func craft_at(workbench_path: NodePath, bill_index: int) -> Toil: var t := Toil.new() t.kind = KIND_CRAFT t.data = { "workbench": String(workbench_path), "bill_index": bill_index, "started": false, } return t # ── save / load ────────────────────────────────────────────────────────────── func to_dict() -> Dictionary: return { "kind": str(kind), "data": data.duplicate(true), "done": done, } static func from_dict(d: Dictionary) -> Toil: var t := Toil.new() t.kind = StringName(d.get("kind", str(KIND_IDLE))) t.data = (d.get("data", {}) as Dictionary).duplicate(true) t.done = d.get("done", false) return t # ── convenience ────────────────────────────────────────────────────────────── ## Rebuild Vector2i from the JSON-safe int fields. Only valid for KIND_WALK. func get_walk_destination() -> Vector2i: return Vector2i(data.get("to_x", 0), data.get("to_y", 0))