Crafting queue
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
912f94f775
commit
40e5274c4e
5 changed files with 207 additions and 17 deletions
10
app.py
10
app.py
|
|
@ -146,6 +146,8 @@ def player_view(world: dict, player: dict, now: float) -> dict:
|
|||
/ game.speed(world)
|
||||
if "enemy" in action else game.action_time(world, player, action) for action in game.ACTIONS}
|
||||
view["blocked"] = {action["id"]: game.can_start(world, player, action) for action in game.ACTIONS}
|
||||
view["queue_why"] = {action["id"]: game.queue_reason(world, player, action) for action in game.ACTIONS
|
||||
if game.queueable(action)}
|
||||
view["drill_per_action"] = game.drill_per_action(player)
|
||||
view["recon_gain"] = {action["id"]: game.recon_gain(world, player, action) for action in game.ACTIONS
|
||||
if action.get("special") == "recon"}
|
||||
|
|
@ -363,6 +365,14 @@ class Handler(BaseHTTPRequestHandler):
|
|||
if not isinstance(once, int) or not 0 <= once <= game.MAKE_MAX:
|
||||
return f"1 to {game.MAKE_MAX:,}"
|
||||
return game.start_action(world, player, str(body.get("id")), now, once=int(once))
|
||||
if path == "/api/queue":
|
||||
qty = body.get("qty")
|
||||
if not isinstance(qty, int) or not 1 <= qty <= game.MAKE_MAX:
|
||||
return f"1 to {game.MAKE_MAX:,}"
|
||||
return game.queue_add(world, player, str(body.get("id")), qty, now)
|
||||
if path == "/api/queue/remove":
|
||||
index = body.get("index")
|
||||
return game.queue_remove(player, index) if isinstance(index, int) else "Not in the queue"
|
||||
if path == "/api/join":
|
||||
return game.join_fight(world, player, str(body.get("id")), now)
|
||||
if path == "/api/press":
|
||||
|
|
|
|||
108
game.py
108
game.py
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ function showSummary(sum) {
|
|||
if (sold.length) toast(`📦 Bank full: sold ${sold.map(([id, q]) => `${fmt(q)} ${item(id).name.toLowerCase()}`).join(", ")} for ${fmt(sum.sold_gold)} gold`);
|
||||
for (const [skill, level] of sum.levels) toast(`🎉 ${skillOf(skill).name} level ${level}!`, "level");
|
||||
if (sum.stopped) toast(`Stopped: ${sum.stopped}`, "bad");
|
||||
if (sum.skipped?.length) toast(`⏭️ Skipped: ${skippedNames(sum)}`, "bad");
|
||||
for (const id of sum.potion_out || []) toast(`🧪 Your ${item(id).name.toLowerCase()} ran out`, "bad");
|
||||
for (const id of Object.keys(sum.potions_drunk || {})) toast(`🧪 Drank another ${item(id).name.toLowerCase()}`);
|
||||
if (sum.prayer_out) toast("🙏 Out of prayer points", "bad");
|
||||
|
|
@ -196,6 +197,11 @@ function showSummary(sum) {
|
|||
if (sum.resumed) toast(`Back to ${C.actions.find((x) => x.id === sum.resumed).name.toLowerCase()}`);
|
||||
}
|
||||
|
||||
// Queued makes passed over for want of materials.
|
||||
function skippedNames(sum) {
|
||||
return sum.skipped.map((id) => C.actions.find((x) => x.id === id).name).join(", ");
|
||||
}
|
||||
|
||||
function awayModal(sum) {
|
||||
const items = Object.entries(sum.items).map(([id, q]) => `<li>${item(id).icon} ${fmt(q)} ${esc(item(id).name)}</li>`).join("");
|
||||
const xp = Object.entries(sum.xp).map(([id, q]) => `<li>${skillOf(id).icon} ${fmt(q)} ${esc(skillOf(id).name)} XP</li>`).join("");
|
||||
|
|
@ -214,6 +220,7 @@ function awayModal(sum) {
|
|||
${levels ? `<ul>${levels}</ul>` : ""}
|
||||
${sold ? `<p class="muted small">Bank full, sold for ${fmt(sum.sold_gold)} gold:</p><ul>${sold}</ul>` : ""}
|
||||
${spilled ? `<p class="muted small">Bank full, sent to the stockpile:</p><ul>${spilled}</ul>` : ""}
|
||||
${sum.skipped?.length ? `<p class="tag bad">⏭️ Skipped: ${esc(skippedNames(sum))}</p>` : ""}
|
||||
${sum.stopped ? `<p class="tag bad">Stopped: ${esc(sum.stopped)}</p>` : ""}
|
||||
<p><button class="btn primary" data-close>Back to work</button></p>`);
|
||||
}
|
||||
|
|
@ -761,7 +768,7 @@ function topbar() {
|
|||
let chips = "";
|
||||
if (S.me?.action) {
|
||||
const action = C.actions.find((a) => a.id === (S.me.action.growing || S.me.action.id));
|
||||
chips += `<button class="chip act" data-nav="skill:${action.skill}">${skillOf(action.skill).icon} <span class="label">${esc(action.name)}</span><span class="bar mini ${S.me.action.fight ? "foe" : ""}"><span data-progress></span></span></button>`;
|
||||
chips += `<button class="chip act" data-nav="skill:${action.skill}">${skillOf(action.skill).icon} <span class="label">${esc(action.name)}</span>${S.me.queue.length ? `<span class="small muted" title="${esc(queueTip())}">+${S.me.queue.length}</span>` : ""}<span class="bar mini ${S.me.action.fight ? "foe" : ""}"><span data-progress></span></span></button>`;
|
||||
} else if (S.me) {
|
||||
chips += `<span class="chip muted">Idle${S.me.stopped ? ` · ${esc(S.me.stopped)}` : ""}</span>`;
|
||||
}
|
||||
|
|
@ -777,6 +784,20 @@ function topbar() {
|
|||
return `<button class="menu-btn" data-menu aria-label="Menu">☰</button><h1>${esc(title)}</h1>${chips}${S.me ? trackedBar() : ""}`;
|
||||
}
|
||||
|
||||
function queueTip() {
|
||||
return `Queued: ${S.me.queue.map((q) => `${C.actions.find((x) => x.id === q.id).name} ×${fmt(q.n)}`).join(" → ")}`;
|
||||
}
|
||||
|
||||
// Makes lined up after this one: tap ✕ to drop one.
|
||||
function queueRow() {
|
||||
const q = S.me.queue;
|
||||
if (!q.length) return "";
|
||||
return `<div class="queue small">⏳ ${q.map((e, i) => {
|
||||
const a = C.actions.find((x) => x.id === e.id);
|
||||
return `<span class="item-chip" title="${esc(a.name)}">${a.icon} ×${fmt(e.n)}<button class="x" data-unqueue="${i}" title="Remove">✕</button></span>`;
|
||||
}).join(`<span class="arrow">→</span>`)}</div>`;
|
||||
}
|
||||
|
||||
// The recipe you're after, under the top bar on every page: have / need for each input.
|
||||
function trackedBar() {
|
||||
const me = S.me, a = me.tracked && C.actions.find((x) => x.id === me.tracked);
|
||||
|
|
@ -1656,6 +1677,9 @@ function actionCard(a) {
|
|||
&& me.action && C.actions.find((x) => x.id === me.action.id)?.skill === "farming" && !me.action.left;
|
||||
const why = me.blocked[a.id];
|
||||
const locked = isLocked(a);
|
||||
// Queue it behind the make that's running: materials only have to be there when its turn comes.
|
||||
const canQueue = me.action?.left && !active && me.queue_why[a.id] === null;
|
||||
const queued = me.queue.map((q, i) => q.id === a.id ? i + 1 : 0).filter(Boolean);
|
||||
const out = Object.keys(a.outputs)[0];
|
||||
const icon = a.icon;
|
||||
const time = me.times[a.id];
|
||||
|
|
@ -1717,13 +1741,15 @@ function actionCard(a) {
|
|||
${me.action?.plot2 && (active || second) ? `<div class="small muted">Plot ${active ? 1 : 2}${growingNow ? " · growing now" : " · next"}</div>` : ""}
|
||||
${growingNow && !a.enemy ? `<div class="bar thick"><span data-progress></span></div>` : ""}
|
||||
${active && a.enemy ? `<div><span class="tag gold">⚔️ Fighting${me.action.with?.length ? ` with ${me.action.with.map((uid) => esc(playerName(uid))).join(", ")}` : ""}</span></div>` : ""}
|
||||
${active && me.action.left ? `<div class="small muted">${me.action.left === 1 ? "Just one" : `${fmt(me.action.left)} more`}${me.action.resume ? `, then back to ${esc(C.actions.find((x) => x.id === me.action.resume).name.toLowerCase())}` : ""}</div>` : ""}
|
||||
${active && me.action.left ? `<div class="small muted">${me.action.left === 1 ? "Just one" : `${fmt(me.action.left)} more`}${me.queue.length ? `, then ${me.queue.length} queued` : ""}${me.action.resume ? `, then back to ${esc(C.actions.find((x) => x.id === me.action.resume).name.toLowerCase())}` : ""}</div>${queueRow()}` : ""}
|
||||
${queued.length ? `<div><span class="tag" title="${esc(queueTip())}">⏳ Queued #${queued.join(", #")}</span></div>` : ""}
|
||||
<div class="foot">
|
||||
<span class="why">${!active && !second && why ? esc(why) : ""}</span>
|
||||
${second ? `<button class="btn" data-plot="">Clear plot 2</button>`
|
||||
: active ? `<button class="btn danger" data-stop>Stop</button>`
|
||||
: `<span class="btns">${!a.enemy && Object.keys(a.outputs).length
|
||||
? `<form data-form="make" data-id="${a.id}" class="make" title="Make this many, then resume"><button class="btn" ${why ? "disabled" : ""}>Make</button><input type="number" min="1" max="${C.make_max}" value="1" data-keep="make-${a.id}" ${why ? "disabled" : ""}></form>`
|
||||
? `<form data-form="make" data-id="${a.id}" class="make" title="Make this many, then resume"><button class="btn" ${why ? "disabled" : ""}>Make</button><input type="number" min="1" max="${C.make_max}" value="1" data-keep="make-${a.id}" ${why && !canQueue ? "disabled" : ""}>${canQueue
|
||||
? `<button class="btn q" data-q ${me.queue.length >= C.queue_max ? `disabled title="Queue full (${C.queue_max})"` : `title="Queue: make this many after the current make"`}>+Q</button>` : ""}</form>`
|
||||
: `<button class="btn" data-once="${a.id}" ${why ? "disabled" : ""} title="Once, then resume">${a.enemy ? "Fight once" : "Once"}</button>`}
|
||||
${canPlot ? `<button class="btn" data-plot="${a.id}" ${why ? "disabled" : ""} title="Grow in plot 2">Plot 2</button>` : ""}
|
||||
<button class="btn primary" data-start="${a.id}" ${why ? "disabled" : ""}>Start</button></span>`}
|
||||
|
|
@ -2814,6 +2840,7 @@ document.addEventListener("click", (e) => {
|
|||
if (d.start) return act("/api/action", { id: d.start });
|
||||
if (d.once) return act("/api/action", { id: d.once, once: 1 });
|
||||
if ("stop" in d) return act("/api/stop", {});
|
||||
if (d.unqueue) return act("/api/queue/remove", { index: Number(d.unqueue) });
|
||||
if (d.join) { tagging = true; return act("/api/join", { id: d.join }); }
|
||||
if ("orderNew" in d) {
|
||||
draft = draft ? null : { kind: "craft", item: "", qty: 1, provide: [], reward: 0 };
|
||||
|
|
@ -2979,6 +3006,7 @@ document.addEventListener("submit", async (e) => {
|
|||
const n = Math.floor(Number(value));
|
||||
if (!(n >= 1 && n <= C.make_max)) return toast(`1 to ${fmt(C.make_max)}`, "bad");
|
||||
field.value = "1";
|
||||
if (e.submitter?.hasAttribute("data-q")) return act("/api/queue", { id: form.dataset.id, qty: n });
|
||||
return act("/api/action", { id: form.dataset.id, once: n });
|
||||
}
|
||||
if (form.dataset.form === "qty") {
|
||||
|
|
|
|||
|
|
@ -711,6 +711,8 @@ button.sheet-skill:hover { background: var(--panel-2); }
|
|||
.make .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; }
|
||||
.make input { width: 60px; padding-inline: 8px; appearance: textfield; border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -1px; }
|
||||
.make.sm input { width: 70px; padding-block: 3px; font-size: 13px; }
|
||||
.make .btn.q { border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -1px; }
|
||||
.make:has(.q) input { border-top-right-radius: 0; border-bottom-right-radius: 0; }
|
||||
.make input::-webkit-inner-spin-button, .make input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
|
||||
.reward-input { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
||||
.reward-input input { width: 110px; }
|
||||
|
|
@ -848,3 +850,5 @@ button.swatch { width: 36px; height: 36px; }
|
|||
.ach.held { border: 2px dashed var(--gold); }
|
||||
.ach.on { border-color: var(--text); }
|
||||
.ach-line { margin: 10px 0 0; }
|
||||
.queue { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 4px; }
|
||||
.queue .x { background: none; border: 0; color: var(--muted); cursor: pointer; padding: 0 0 0 4px; font-size: 11px; }
|
||||
|
|
|
|||
|
|
@ -84,6 +84,74 @@ class ActionTest(unittest.TestCase):
|
|||
self.assertGreater(p["bank"]["stone"], 100)
|
||||
self.assertEqual(p["bank"]["stone"], 100 + summary["lucky"])
|
||||
|
||||
def _smith(self, bars):
|
||||
world = world_with(("fred", "commoner"))
|
||||
p = world["players"]["fred"]
|
||||
p["xp"]["smithing"] = game.XP_TABLE[10]
|
||||
p["bank"] = {"copper_bar": bars}
|
||||
game.start_action(world, p, "mine_stone", T0)
|
||||
return world, p
|
||||
|
||||
def test_queue_runs_in_order_then_back_to_work(self):
|
||||
world, p = self._smith(6)
|
||||
self.assertIsNone(game.start_action(world, p, "smith_copper_helmet", T0, once=1))
|
||||
self.assertIsNone(game.queue_add(world, p, "smith_copper_legs", 1, T0))
|
||||
self.assertEqual(p["queue"], [{"id": "smith_copper_legs", "n": 1}])
|
||||
summary = game.process_player(world, p, T0 + 500, random.Random(1))
|
||||
self.assertEqual((p["bank"].get("copper_helmet"), p["bank"].get("copper_legs")), (1, 1))
|
||||
self.assertEqual(p["action"]["id"], "mine_stone")
|
||||
self.assertEqual(summary["resumed"], "mine_stone")
|
||||
self.assertEqual(p["queue"], [])
|
||||
|
||||
def test_queue_skips_what_you_cant_make(self):
|
||||
world, p = self._smith(4)
|
||||
game.start_action(world, p, "smith_copper_helmet", T0, once=1)
|
||||
game.queue_add(world, p, "smith_copper_legs", 1, T0) # 4 bars, only 2 left by then
|
||||
game.queue_add(world, p, "smith_copper_sword", 1, T0)
|
||||
summary = game.process_player(world, p, T0 + 500, random.Random(1))
|
||||
self.assertEqual((p["bank"].get("copper_helmet"), p["bank"].get("copper_sword")), (1, 1))
|
||||
self.assertNotIn("copper_legs", p["bank"])
|
||||
self.assertEqual(summary["skipped"], ["smith_copper_legs"])
|
||||
self.assertEqual(summary["resumed"], "mine_stone") # not the queued makes on the way
|
||||
self.assertEqual(p["action"]["id"], "mine_stone")
|
||||
|
||||
def test_running_out_mid_make_moves_on_to_the_next(self):
|
||||
world, p = self._smith(5)
|
||||
game.start_action(world, p, "smith_copper_helmet", T0, once=3) # bars for 2
|
||||
game.queue_add(world, p, "smith_copper_rivets", 1, T0)
|
||||
summary = game.process_player(world, p, T0 + 500, random.Random(1))
|
||||
self.assertEqual((p["bank"].get("copper_helmet"), p["bank"].get("copper_rivets")), (2, 5))
|
||||
self.assertEqual(summary["skipped"], ["smith_copper_helmet"])
|
||||
self.assertEqual(p["action"]["id"], "mine_stone")
|
||||
|
||||
def test_out_of_materials_with_nothing_queued_still_stops(self):
|
||||
world, p = self._smith(4)
|
||||
game.start_action(world, p, "smith_copper_helmet", T0, once=3)
|
||||
summary = game.process_player(world, p, T0 + 500, random.Random(1))
|
||||
self.assertIsNone(p["action"])
|
||||
self.assertTrue(summary["stopped"].startswith("Out of"))
|
||||
|
||||
def test_queue_rules(self):
|
||||
world, p = self._smith(4)
|
||||
self.assertEqual(game.queue_add(world, p, "cut_logs", 1, T0), "Only crafting can be queued")
|
||||
self.assertIsNone(game.queue_add(world, p, "smith_copper_helmet", 1, T0))
|
||||
self.assertEqual(p["action"]["id"], "smith_copper_helmet") # nothing being made: it just starts
|
||||
self.assertEqual(p["action"]["resume"], "mine_stone")
|
||||
p["bank"] = {}
|
||||
for _ in range(game.QUEUE_MAX):
|
||||
self.assertIsNone(game.queue_add(world, p, "smith_copper_helmet", 1, T0)) # bars aren't checked yet
|
||||
self.assertEqual(game.queue_add(world, p, "smith_copper_helmet", 1, T0), f"Queue full ({game.QUEUE_MAX})")
|
||||
high = next(a["id"] for a in game.ACTIONS if game.queueable(a) and a["skill"] == "smithing" and a["level"] > 10)
|
||||
self.assertTrue(game.queue_add(world, p, high, 1, T0).startswith("Needs level"))
|
||||
self.assertIsNone(game.queue_remove(p, 0))
|
||||
self.assertEqual(len(p["queue"]), game.QUEUE_MAX - 1)
|
||||
game.start_action(world, p, "mine_stone", T0)
|
||||
self.assertEqual(p["queue"], [])
|
||||
game.start_action(world, p, "smith_copper_helmet", T0, once=1)
|
||||
game.queue_add(world, p, "smith_copper_legs", 1, T0)
|
||||
game.drop_out(world, p, T0)
|
||||
self.assertEqual(p["queue"], [])
|
||||
|
||||
def test_offline_cap(self):
|
||||
world = world_with(("fred", "commoner"))
|
||||
p = world["players"]["fred"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue