Crafting queue

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-26 22:51:56 +00:00
parent 912f94f775
commit 40e5274c4e
5 changed files with 207 additions and 17 deletions

108
game.py
View file

@ -1524,7 +1524,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
# --- world ---------------------------------------------------------------
WORLD_VERSION = 31
WORLD_VERSION = 32
def new_world(now: float, speed: float = 1.0) -> dict:
@ -1770,6 +1770,11 @@ def migrate(world: dict, now: float | None = None) -> dict:
held = [*player["bank"], *player["equip"].values()]
player.setdefault("found", [item for item in UNIQUES if item in held])
world["version"] = 31
if world["version"] < 32:
# Crafting queue: makes lined up after the current one.
for player in world["players"].values():
player.setdefault("queue", [])
world["version"] = 32
return world
@ -2024,7 +2029,7 @@ def new_player(uid: str, name: str, cls: str, now: float) -> dict:
"food": None, # preferred food in fights; None eats the cheapest first
"pp": 0, "prayers": [], "potions": {},
"hp": 44, "hp_at": now,
"action": None, "stopped": None,
"action": None, "stopped": None, "queue": [],
"last_active": now, "overflow": "city",
"perks": {"bank_slot": 0}, "manual": [], "patterns": [], "found": [], "eat_below": EAT_BELOW,
"rented": 0, "boosts": [], "bought": None, "deeds": {}, "owned": [], "look": dict(DEFAULT_LOOK),
@ -3030,7 +3035,7 @@ def gain_xp(world: dict, player: dict, skill: str, xp: float, now: float, summar
def empty_summary() -> dict:
return {"done": 0, "items": {}, "overflow": {}, "xp": {}, "levels": [], "eaten": {}, "used": {}, "damage": 0,
"gold": 0, "wins": 0, "losses": 0, "lucky": 0, "stopped": None, "resumed": None,
"gold": 0, "wins": 0, "losses": 0, "lucky": 0, "stopped": None, "resumed": None, "skipped": [],
"sold": {}, "sold_gold": 0, "pp_spent": 0, "pp_gained": 0, "prayer_out": False, "second_wind": 0,
"kept": 0, "potions_drunk": {}, "potion_out": [], "labour": 0}
@ -3075,6 +3080,9 @@ def process_player(world: dict, player: dict, now: float, rng: random.Random | N
burn_fires(world, current["start"] + duration)
reason = can_start(world, player, action)
if reason:
if next_queued(world, player, current["start"], summary, skipped=current):
current = player["action"]
continue
stop(player, reason)
summary["stopped"] = reason
break
@ -3090,13 +3098,15 @@ def process_player(world: dict, player: dict, now: float, rng: random.Random | N
current["turn"] = 1 - current.get("turn", 0)
if player["action"] is current and "left" in current:
current["left"] -= 1
if current["left"] <= 0 and finish_once(player, current["start"]):
if current["left"] <= 0 and finish_once(world, player, current["start"], summary=summary):
current = player["action"]
summary["resumed"] = current["id"]
if player["action"] is current and "enemy" not in action and not current.get("plot2"):
# Out of materials: stop now, not once the next bar has filled.
reason = can_start(world, player, growing(player))
if reason:
if next_queued(world, player, current["start"], summary, skipped=current):
current = player["action"]
continue
stop(player, reason)
summary["stopped"] = reason
break
@ -3108,10 +3118,77 @@ def process_player(world: dict, player: dict, now: float, rng: random.Random | N
def stop(player: dict, reason: str | None = None) -> None:
player["action"] = None
player["stopped"] = reason
player["queue"] = []
def finish_once(player: dict, at: float, reason: str | None = None) -> bool:
"""A one-off is done: back to what you were doing before (True), or stop."""
def queueable(action: dict) -> bool:
"""Crafting: turns materials into something. Only these can be queued."""
return bool(action["inputs"] and action["outputs"]) and not (
action.get("enemy") or action.get("special") or action.get("together"))
def queue_reason(world: dict, player: dict, action: dict) -> str | None:
"""Why it can't go in the queue. Materials are checked when its turn comes."""
if not queueable(action):
return "Only crafting can be queued"
if skill_level(player, action["skill"]) < action["level"]:
return f"Needs level {action['level']}"
if action.get("pattern") and action["pattern"] not in player.get("patterns", []):
return "Learn its pattern first"
for key, level in action.get("city", {}).items():
if building_level(world, key) < level:
return f"Needs {BUILDINGS[key]['name']} level {level}"
return None
def queue_add(world: dict, player: dict, action_id: str, qty: int, now: float) -> str | None:
"""Line a make up after the current one. With no make running, it just starts."""
action = ACTION_BY_ID.get(action_id)
if not action:
return "No such action"
reason = queue_reason(world, player, action)
if reason:
return reason
if not (player["action"] and "left" in player["action"]):
return start_action(world, player, action_id, now, once=qty)
if len(player["queue"]) >= QUEUE_MAX:
return f"Queue full ({QUEUE_MAX})"
player["queue"].append({"id": action_id, "n": qty})
return None
def queue_remove(player: dict, index: int) -> str | None:
if not 0 <= index < len(player["queue"]):
return "Not in the queue"
player["queue"].pop(index)
return None
def next_queued(world: dict, player: dict, at: float, summary: dict | None, skipped: dict | None = None) -> bool:
"""Start the next make in the queue (True), skipping any you can't make right now.
`skipped` is a make that ran out of materials before its count was done."""
current = player["action"]
if skipped is not None and (current is not skipped or "left" not in skipped or not player["queue"]):
return False
gone = summary.setdefault("skipped", []) if summary is not None else []
if skipped is not None:
gone.append(skipped["id"])
while player["queue"]:
entry = player["queue"].pop(0)
action = ACTION_BY_ID.get(entry["id"])
if action and not can_start(world, player, action):
player["action"] = {**{k: v for k, v in current.items() if k in ("resume", "resume_plot2")},
"id": entry["id"], "start": at, "left": entry["n"]}
return True
gone.append(entry["id"])
return False
def finish_once(world: dict, player: dict, at: float, reason: str | None = None, summary: dict | None = None) -> bool:
"""A one-off is done: on to the next in the queue, back to what you were doing before (True), or stop.
Going back is noted in `summary` as resumed."""
if next_queued(world, player, at, summary):
return True
current = player["action"]
back = current.get("resume")
if not back:
@ -3120,6 +3197,8 @@ def finish_once(player: dict, at: float, reason: str | None = None) -> bool:
player["action"] = {"id": back, "start": at}
if current.get("resume_plot2"):
player["action"].update(plot2=current["resume_plot2"], turn=0)
if summary is not None:
summary["resumed"] = back
return True
@ -3349,8 +3428,7 @@ def process_party(world: dict, pid: str, now: float, rng: random.Random) -> None
if current["left"] <= 0:
if world["parties"].get(current.get("party")) is party:
leave_party(world, player, pid, end)
if finish_once(player, end):
summary["resumed"] = player["action"]["id"]
finish_once(world, player, end, summary=summary)
if action.get("dungeon") and world["parties"].get(pid) is party:
_advance_run(world, party, action, fight, members, end, rng)
@ -3398,9 +3476,7 @@ def _advance_run(world: dict, party: dict, action: dict, fight: dict, members: l
del world["parties"][party["id"]]
for player in members:
summary = player.setdefault("away", empty_summary())
if finish_once(player, now, reason):
summary["resumed"] = player["action"]["id"]
else:
if not finish_once(world, player, now, reason, summary):
summary["stopped"] = reason
@ -3802,6 +3878,7 @@ def working_order(world: dict, player: dict, action: dict) -> dict | None:
MAKE_MAX = 1000 # most you can ask for in one "Make X"
QUEUE_MAX = 5 # makes lined up after the current one
def start_action(world: dict, player: dict, action_id: str, now: float, once: int = 0) -> str | None:
@ -3822,7 +3899,9 @@ def start_action(world: dict, player: dict, action_id: str, now: float, once: in
plot2 = current.get("resume_plot2") or current.get("plot2")
if plot2:
entry["resume_plot2"] = plot2
elif current and current.get("plot2") and current["plot2"] != action_id and action["skill"] == "farming":
else:
player["queue"] = [] # something new: the lined-up makes go
if not once and current and current.get("plot2") and current["plot2"] != action_id and action["skill"] == "farming":
# Replanting the first plot leaves the second one growing.
entry.update(plot2=current["plot2"], turn=0)
if current and current.get("party") in world["parties"]:
@ -6090,6 +6169,7 @@ PATCH_NOTES = [
("new", "Fittings: ✓ Fitted on ones the town has (✓ Paused while damaged)."),
("fix", "Council threat card names the dragon or raid band, not always \"Raiders\"."),
("new", "Donate, Give, Sell: type an amount (starts at 1). 1/10/100 buttons gone, All stays."),
("new", "Added crafting queue"),
]},
{"date": "2026-10-25", "title": "Titles", "notes": [
("new", "🏆 Achievements (under You): 127. Patterns, uniques, titles, emblems, looks, home. Tracks only, awards nothing."),
@ -6203,7 +6283,7 @@ def content() -> dict:
"city_uses": {item: city_uses(item) for item in ITEMS},
"prayers": [{**prayer, "text": describe_prayer(prayer)} for prayer in PRAYERS], "max_prayers": MAX_PRAYERS,
"potion_charges": POTION_CHARGES, "order_recipes": ORDER_RECIPES, "order_raw": ORDER_RAW,
"order_limit": ORDER_LIMIT, "order_max": ORDER_MAX, "make_max": MAKE_MAX, "input_share": INPUT_SHARE, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
"order_limit": ORDER_LIMIT, "order_max": ORDER_MAX, "make_max": MAKE_MAX, "queue_max": QUEUE_MAX, "input_share": INPUT_SHARE, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
"equip_slots": EQUIP_SLOTS, "class_speed": CLASS_SPEED, "rest_rate": REST_RATE, "bite": BITE, "eat_below": EAT_BELOW, "eat_choices": EAT_CHOICES,
"abilities": combat.ABILITIES, "enemy_abilities": combat.ENEMY_ABILITIES, "unlock": combat.UNLOCK, "auto_cd": combat.AUTO_CD, "watch": combat.WATCH, "gcd": combat.GCD,
"weapon_kits": combat.WEAPON_KITS, "group_size": GROUP_SIZE, "dungeon_size": DUNGEON_SIZE, "dungeon_wipes": DUNGEON_WIPES, "trade_ability": combat.TRADE_ABILITY, "potion_below": combat.POTION_BELOW,