Make X: count for one-off crafts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2996d7a9d7
commit
f735831a1e
5 changed files with 37 additions and 8 deletions
5
app.py
5
app.py
|
|
@ -358,7 +358,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||
if not isinstance(qty, int):
|
||||
return "Bad quantity"
|
||||
if path == "/api/action":
|
||||
return game.start_action(world, player, str(body.get("id")), now, once=bool(body.get("once")))
|
||||
once = body.get("once") or 0
|
||||
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/join":
|
||||
return game.join_fight(world, player, str(body.get("id")), now)
|
||||
if path == "/api/press":
|
||||
|
|
|
|||
12
game.py
12
game.py
|
|
@ -3801,8 +3801,11 @@ def working_order(world: dict, player: dict, action: dict) -> dict | None:
|
|||
return order_by_id(world, current["order"])
|
||||
|
||||
|
||||
def start_action(world: dict, player: dict, action_id: str, now: float, once: bool = False) -> str | None:
|
||||
"""Start an action. `once` does it a single time, then goes back to what you were doing."""
|
||||
MAKE_MAX = 1000 # most you can ask for in one "Make X"
|
||||
|
||||
|
||||
def start_action(world: dict, player: dict, action_id: str, now: float, once: int = 0) -> str | None:
|
||||
"""Start an action. `once` does it that many times, then goes back to what you were doing."""
|
||||
action = ACTION_BY_ID.get(action_id)
|
||||
if not action:
|
||||
return "No such action"
|
||||
|
|
@ -3813,7 +3816,7 @@ def start_action(world: dict, player: dict, action_id: str, now: float, once: bo
|
|||
current = player["action"]
|
||||
if once:
|
||||
back = current and (current.get("resume") or current["id"])
|
||||
entry["left"] = 1
|
||||
entry["left"] = int(once)
|
||||
if back and back != action_id:
|
||||
entry["resume"] = back
|
||||
plot2 = current.get("resume_plot2") or current.get("plot2")
|
||||
|
|
@ -6081,6 +6084,7 @@ ORDER_RECIPES, ORDER_RAW = _order_sources()
|
|||
# line here (new / balance / fix), and a new release a new entry on top.
|
||||
PATCH_NOTES = [
|
||||
{"date": "2026-10-26", "title": "Fittings", "notes": [
|
||||
("new", "Make X: \"Make one\" now takes a count (1 by default, up to 1,000), then you go back to what you were doing."),
|
||||
("new", "Construction › Fittings: ✓ Fitted on the ones the town already has (✓ Paused while the building is damaged)."),
|
||||
]},
|
||||
{"date": "2026-10-25", "title": "Titles", "notes": [
|
||||
|
|
@ -6236,7 +6240,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, "input_share": INPUT_SHARE, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
|
||||
"order_limit": ORDER_LIMIT, "order_max": ORDER_MAX, "make_max": MAKE_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,
|
||||
|
|
|
|||
|
|
@ -1712,12 +1712,14 @@ 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">Just one${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.action.resume ? `, then back to ${esc(C.actions.find((x) => x.id === me.action.resume).name.toLowerCase())}` : ""}</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"><button class="btn" data-once="${a.id}" ${why ? "disabled" : ""} title="Once, then resume">${a.enemy ? "Fight once" : Object.keys(a.outputs).length ? "Make one" : "Once"}</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>`
|
||||
: `<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>`}
|
||||
</div>
|
||||
|
|
@ -2798,7 +2800,7 @@ document.addEventListener("click", (e) => {
|
|||
return d.aim === S.uid ? undefined : act("/api/target", { id: d.aim });
|
||||
}
|
||||
if (d.start) return act("/api/action", { id: d.start });
|
||||
if (d.once) return act("/api/action", { id: d.once, once: true });
|
||||
if (d.once) return act("/api/action", { id: d.once, once: 1 });
|
||||
if ("stop" in d) return act("/api/stop", {});
|
||||
if (d.join) { tagging = true; return act("/api/join", { id: d.join }); }
|
||||
if ("orderNew" in d) {
|
||||
|
|
@ -2961,6 +2963,12 @@ document.addEventListener("submit", async (e) => {
|
|||
field.value = "";
|
||||
return act("/api/monument", { id: form.dataset.id, qty: Number(value) });
|
||||
}
|
||||
if (form.dataset.form === "make") {
|
||||
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";
|
||||
return act("/api/action", { id: form.dataset.id, once: n });
|
||||
}
|
||||
if (form.dataset.form === "gold") {
|
||||
field.value = "";
|
||||
return act("/api/donate", { item: "gold", qty: Number(value) });
|
||||
|
|
|
|||
|
|
@ -706,6 +706,9 @@ button.sheet-skill:hover { background: var(--panel-2); }
|
|||
.ro-pick > .doll-slot .gi { width: 34px; height: 34px; } .ro-pick .small .gi { width: 14px; height: 14px; }
|
||||
.stepper { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
|
||||
.stepper input { width: 90px; }
|
||||
.make { display: flex; }
|
||||
.make .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; }
|
||||
.make input { width: 48px; padding-inline: 6px; border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -1px; }
|
||||
.reward-input { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
||||
.reward-input input { width: 110px; }
|
||||
@media (max-width: 900px) { .compose-grid { grid-template-columns: 1fr; gap: 14px; } }
|
||||
|
|
|
|||
|
|
@ -55,6 +55,17 @@ class ActionTest(unittest.TestCase):
|
|||
self.assertEqual(p["action"]["id"], "mine_stone")
|
||||
self.assertEqual(p["bank"]["stone"], 5 + 2) # mining carried on from when the pickaxe was done
|
||||
|
||||
def test_make_x_then_back_to_work(self):
|
||||
world = world_with(("fred", "commoner"))
|
||||
p = world["players"]["fred"]
|
||||
game.start_action(world, p, "mine_stone", T0)
|
||||
self.assertIsNone(game.start_action(world, p, "cut_logs", T0, once=3))
|
||||
self.assertEqual(p["action"]["left"], 3)
|
||||
summary = game.process_player(world, p, T0 + 500, random.Random(1))
|
||||
self.assertEqual(p["bank"]["logs"], 3)
|
||||
self.assertEqual(summary["resumed"], "mine_stone")
|
||||
self.assertEqual(p["action"]["id"], "mine_stone")
|
||||
|
||||
def test_make_one_while_idle_just_stops(self):
|
||||
world = world_with(("fred", "commoner"))
|
||||
p = world["players"]["fred"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue