Choose when you eat between fights
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8afcba79fc
commit
bc99a1e0c5
5 changed files with 43 additions and 5 deletions
2
app.py
2
app.py
|
|
@ -454,6 +454,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||||
if path == "/api/prayers":
|
if path == "/api/prayers":
|
||||||
ids = body.get("ids")
|
ids = body.get("ids")
|
||||||
return game.set_prayers(player, ids) if isinstance(ids, list) else "Bad request"
|
return game.set_prayers(player, ids) if isinstance(ids, list) else "Bad request"
|
||||||
|
if path == "/api/eat":
|
||||||
|
return game.set_eat_below(player, body.get("below"))
|
||||||
if path == "/api/food":
|
if path == "/api/food":
|
||||||
food = body.get("item")
|
food = body.get("item")
|
||||||
if food is not None and "heal" not in game.ITEMS.get(str(food), {}):
|
if food is not None and "heal" not in game.ITEMS.get(str(food), {}):
|
||||||
|
|
|
||||||
22
game.py
22
game.py
|
|
@ -26,6 +26,8 @@ MAX_LOG = 200
|
||||||
REGEN_PER_HOUR = 0.25 # share of max HP recovered per game hour
|
REGEN_PER_HOUR = 0.25 # share of max HP recovered per game hour
|
||||||
REST_RATE = 0.01 # share of max HP back per game second resting between fights...
|
REST_RATE = 0.01 # share of max HP back per game second resting between fights...
|
||||||
BITE = 2.0 # ...and each bite of food takes this long, so food rests you faster
|
BITE = 2.0 # ...and each bite of food takes this long, so food rests you faster
|
||||||
|
EAT_BELOW = 0.5 # unless you choose otherwise, you only eat while under this share of your HP...
|
||||||
|
EAT_CHOICES = (0, 0.25, 0.5, 0.75, 1.0) # ...and these are the choices (never, up to always)
|
||||||
KNOCKED_OUT_HP = 0.3 # share of max HP you come round with after being knocked out
|
KNOCKED_OUT_HP = 0.3 # share of max HP you come round with after being knocked out
|
||||||
KNOCKOUTS_TO_STOP = 3 # in a row, and you stop fighting
|
KNOCKOUTS_TO_STOP = 3 # in a row, and you stop fighting
|
||||||
|
|
||||||
|
|
@ -2018,7 +2020,7 @@ def new_player(uid: str, name: str, cls: str, now: float) -> dict:
|
||||||
"hp": 44, "hp_at": now,
|
"hp": 44, "hp_at": now,
|
||||||
"action": None, "stopped": None,
|
"action": None, "stopped": None,
|
||||||
"last_active": now, "overflow": "city",
|
"last_active": now, "overflow": "city",
|
||||||
"perks": {"bank_slot": 0}, "manual": [], "patterns": [],
|
"perks": {"bank_slot": 0}, "manual": [], "patterns": [], "eat_below": EAT_BELOW,
|
||||||
"rented": 0, "boosts": [], "bought": None, "deeds": {}, "owned": [], "look": dict(DEFAULT_LOOK),
|
"rented": 0, "boosts": [], "bought": None, "deeds": {}, "owned": [], "look": dict(DEFAULT_LOOK),
|
||||||
"garrison": False,
|
"garrison": False,
|
||||||
"donated": 0,
|
"donated": 0,
|
||||||
|
|
@ -2211,14 +2213,16 @@ def _average_hit(max_hit: float, crit: float = CRIT_CHANCE) -> float:
|
||||||
|
|
||||||
def rest_plan(world: dict | None, player: dict, hp: float | None = None) -> tuple[float, dict]:
|
def rest_plan(world: dict | None, player: dict, hp: float | None = None) -> tuple[float, dict]:
|
||||||
"""Resting back to full before a fight: how many game seconds, and the food eaten on the way.
|
"""Resting back to full before a fight: how many game seconds, and the food eaten on the way.
|
||||||
A bite is only taken when it's quicker than resting, so better food makes for shorter rests."""
|
You eat while you're under the share of your HP you chose, and only when a bite is quicker than
|
||||||
|
resting, so better food makes for shorter rests. The rest of the way you rest."""
|
||||||
top = max_hp(player)
|
top = max_hp(player)
|
||||||
missing = top - (player["hp"] if hp is None else hp)
|
missing = top - (player["hp"] if hp is None else hp)
|
||||||
rate = top * REST_RATE * regen_boost(world, player) / ware(player, "rest", "combat")
|
rate = top * REST_RATE * regen_boost(world, player) / ware(player, "rest", "combat")
|
||||||
|
hungry = top * (1 - player.get("eat_below", EAT_BELOW)) # eat while more than this is missing
|
||||||
seconds, eaten = 0.0, {}
|
seconds, eaten = 0.0, {}
|
||||||
for food in edible(player):
|
for food in edible(player):
|
||||||
heal, left = ITEMS[food]["heal"], player["bank"].get(food, 0)
|
heal, left = ITEMS[food]["heal"], player["bank"].get(food, 0)
|
||||||
while missing > 0 and left and min(heal, missing) / rate > BITE:
|
while missing > hungry and missing > 0 and left and min(heal, missing) / rate > BITE:
|
||||||
eaten[food] = eaten.get(food, 0) + 1
|
eaten[food] = eaten.get(food, 0) + 1
|
||||||
left -= 1
|
left -= 1
|
||||||
missing -= heal
|
missing -= heal
|
||||||
|
|
@ -2398,6 +2402,14 @@ def _input(world: dict, player: dict, now: float, kind: str, *args) -> str | Non
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def set_eat_below(player: dict, share) -> str | None:
|
||||||
|
"""Below what share of your HP you eat between fights (0: never, 1: whenever it's quicker)."""
|
||||||
|
if share not in EAT_CHOICES:
|
||||||
|
return "Pick one of the choices"
|
||||||
|
player["eat_below"] = share
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def learn_pattern(player: dict, item: str) -> str | None:
|
def learn_pattern(player: dict, item: str) -> str | None:
|
||||||
"""Read a pattern: from then on you can reforge its masterwork. The pattern's used up."""
|
"""Read a pattern: from then on you can reforge its masterwork. The pattern's used up."""
|
||||||
spec = ITEMS.get(item, {})
|
spec = ITEMS.get(item, {})
|
||||||
|
|
@ -5964,6 +5976,8 @@ PATCH_NOTES = [
|
||||||
("new", "One unique per dungeon from its last boss (8% each), worn as a ring, amulet or trinket, that changes an "
|
("new", "One unique per dungeon from its last boss (8% each), worn as a ring, amulet or trinket, that changes an "
|
||||||
"ability: One-Eye's fang, Ogre's knuckle, Barrow king's pendant, Warlord's whistle, Forgeheart, Rimeglass "
|
"ability: One-Eye's fang, Ogre's knuckle, Barrow king's pendant, Warlord's whistle, Forgeheart, Rimeglass "
|
||||||
"lens, Wyrmscale quiver. Abilities it changes get a ✦."),
|
"lens, Wyrmscale quiver. Abilities it changes get a ✦."),
|
||||||
|
("new", "Choose when you eat between fights: below 25 / 50 / 75% of your HP, always, or never (the food slot, "
|
||||||
|
"under \"Eat below\"). Default 50%: above it you just rest, so a scratch doesn't cost food."),
|
||||||
]},
|
]},
|
||||||
{"date": "2026-10-24", "title": "A dungeon in every area", "notes": [
|
{"date": "2026-10-24", "title": "A dungeon in every area", "notes": [
|
||||||
("new", "Six more dungeons for 2–4, seven pulls each with two mini-bosses and a final boss: Goblin Warren "
|
("new", "Six more dungeons for 2–4, seven pulls each with two mini-bosses and a final boss: Goblin Warren "
|
||||||
|
|
@ -6081,7 +6095,7 @@ def content() -> dict:
|
||||||
"prayers": [{**prayer, "text": describe_prayer(prayer)} for prayer in PRAYERS], "max_prayers": MAX_PRAYERS,
|
"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,
|
"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, "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,
|
"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,
|
"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,
|
"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,
|
||||||
"styles": STYLES, "damage_types": DAMAGE_TYPES, "weights": WEIGHTS, "vulnerable": VULNERABLE,
|
"styles": STYLES, "damage_types": DAMAGE_TYPES, "weights": WEIGHTS, "vulnerable": VULNERABLE,
|
||||||
|
|
|
||||||
|
|
@ -2180,8 +2180,12 @@ function foodPanel(p) {
|
||||||
const tiles = [...p.edible].sort((a, b) => item(a).heal - item(b).heal).map((f) => `<button class="doll-slot swap ${f === id ? "on" : ""}" data-food="${f}"
|
const tiles = [...p.edible].sort((a, b) => item(a).heal - item(b).heal).map((f) => `<button class="doll-slot swap ${f === id ? "on" : ""}" data-food="${f}"
|
||||||
title="${esc(`${item(f).name}: heals ${item(f).heal}`)}">${item(f).icon}<span class="swap-qty">${fmt(p.bank[f])}</span></button>`);
|
title="${esc(`${item(f).name}: heals ${item(f).heal}`)}">${item(f).icon}<span class="swap-qty">${fmt(p.bank[f])}</span></button>`);
|
||||||
const cheap = `<button class="doll-slot swap ${id ? "" : "on"}" data-food="cheapest" title="Cheapest first">${ANY_FOOD}</button>`;
|
const cheap = `<button class="doll-slot swap ${id ? "" : "on"}" data-food="cheapest" title="Cheapest first">${ANY_FOOD}</button>`;
|
||||||
|
const below = p.eat_below ?? C.eat_below;
|
||||||
|
const label = (x) => x === 0 ? "Never" : x === 1 ? "Always" : `${Math.round(x * 100)}%`;
|
||||||
|
const when = `<div class="swap-head muted small" title="Between fights you eat while under this much of your HP, then rest the rest of the way">Eat below</div>
|
||||||
|
<div class="eat-below">${C.eat_choices.map((x) => `<button class="btn sm ${x === below ? "on" : ""}" data-eat="${x}">${label(x)}</button>`).join("")}</div>`;
|
||||||
return head + (p.edible.length ? `<div class="swap-head muted small">Eat first</div><div class="swap-grid">${cheap}${tiles.join("")}</div>`
|
return head + (p.edible.length ? `<div class="swap-head muted small">Eat first</div><div class="swap-grid">${cheap}${tiles.join("")}</div>`
|
||||||
: `<div class="small warn">No food in your bank.</div>`);
|
: `<div class="small warn">No food in your bank.</div>`) + when;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The equipment panel, the same on the character sheet and the combat page (which leaves out tools).
|
// The equipment panel, the same on the character sheet and the combat page (which leaves out tools).
|
||||||
|
|
@ -2301,6 +2305,7 @@ document.addEventListener("click", (e) => {
|
||||||
if (d.mode) return act("/api/manual", { id: d.mode, on: !S.me.manual.includes(d.mode) });
|
if (d.mode) return act("/api/manual", { id: d.mode, on: !S.me.manual.includes(d.mode) });
|
||||||
if ("pull" in d) return act("/api/pull", {});
|
if ("pull" in d) return act("/api/pull", {});
|
||||||
if (d.learn) return act("/api/learn", { item: d.learn });
|
if (d.learn) return act("/api/learn", { item: d.learn });
|
||||||
|
if (d.eat !== undefined) return act("/api/eat", { below: Number(d.eat) });
|
||||||
if (d.aim) return d.aim === S.uid ? undefined : act("/api/target", { id: d.aim });
|
if (d.aim) return d.aim === S.uid ? undefined : act("/api/target", { id: d.aim });
|
||||||
if (d.start) return act("/api/action", { id: d.start });
|
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: true });
|
||||||
|
|
|
||||||
|
|
@ -587,6 +587,8 @@ button.sheet-skill:hover { background: var(--panel-2); }
|
||||||
.ab-lv .gi { width: 10px; height: 10px; }
|
.ab-lv .gi { width: 10px; height: 10px; }
|
||||||
.ab.boosted .ab-press::after { content: "✦"; position: absolute; top: 1px; right: 3px; font-size: 10px; color: var(--gold); }
|
.ab.boosted .ab-press::after { content: "✦"; position: absolute; top: 1px; right: 3px; font-size: 10px; color: var(--gold); }
|
||||||
.unique-text { color: var(--gold); margin-top: 4px; }
|
.unique-text { color: var(--gold); margin-top: 4px; }
|
||||||
|
.eat-below { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||||
|
.eat-below .btn.on { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold); }
|
||||||
.ab.locked { opacity: .35; filter: grayscale(1); }
|
.ab.locked { opacity: .35; filter: grayscale(1); }
|
||||||
.ab.fired .ab-press { box-shadow: 0 0 0 2px var(--gold); }
|
.ab.fired .ab-press { box-shadow: 0 0 0 2px var(--gold); }
|
||||||
.ab.spent { opacity: .35; }
|
.ab.spent { opacity: .35; }
|
||||||
|
|
|
||||||
|
|
@ -587,6 +587,7 @@ class CombatTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_food_shortens_the_rest_between_fights(self):
|
def test_food_shortens_the_rest_between_fights(self):
|
||||||
world, p = self.fighter(level=30)
|
world, p = self.fighter(level=30)
|
||||||
|
p["eat_below"] = 1.0 # eat whenever a bite is quicker
|
||||||
top = game.max_hp(p)
|
top = game.max_hp(p)
|
||||||
p["bank"] = {}
|
p["bank"] = {}
|
||||||
resting, eaten = game.rest_plan(world, p, top / 2)
|
resting, eaten = game.rest_plan(world, p, top / 2)
|
||||||
|
|
@ -605,6 +606,20 @@ class CombatTest(unittest.TestCase):
|
||||||
p["bank"] = {"bread": 100}
|
p["bank"] = {"bread": 100}
|
||||||
self.assertEqual(game.rest_plan(world, p, 100)[1], {}) # a bite of bread is no faster than resting
|
self.assertEqual(game.rest_plan(world, p, 100)[1], {}) # a bite of bread is no faster than resting
|
||||||
|
|
||||||
|
def test_you_choose_how_hurt_you_eat_at(self):
|
||||||
|
world, p = self.fighter(level=30)
|
||||||
|
top = game.max_hp(p)
|
||||||
|
p["bank"] = {"cooked_salmon": 100}
|
||||||
|
self.assertEqual(p["eat_below"], 0.5)
|
||||||
|
self.assertEqual(game.rest_plan(world, p, top * 0.95)[1], {}) # a scratch: just rest
|
||||||
|
_seconds, eaten = game.rest_plan(world, p, top * 0.2)
|
||||||
|
self.assertEqual(eaten, {"cooked_salmon": 2}) # back over half, then rest
|
||||||
|
self.assertIsNone(game.set_eat_below(p, 0))
|
||||||
|
self.assertEqual(game.rest_plan(world, p, top * 0.2)[1], {}) # never
|
||||||
|
self.assertIsNone(game.set_eat_below(p, 1.0))
|
||||||
|
self.assertEqual(game.rest_plan(world, p, top * 0.2)[1], {"cooked_salmon": 5}) # all the way
|
||||||
|
self.assertEqual(game.set_eat_below(p, 0.3), "Pick one of the choices")
|
||||||
|
|
||||||
def test_food_is_eaten_resting_not_fighting(self):
|
def test_food_is_eaten_resting_not_fighting(self):
|
||||||
world, p = self.fighter(level=15, weapon="iron_sword", helmet="iron_helmet", body="iron_body", legs="iron_legs")
|
world, p = self.fighter(level=15, weapon="iron_sword", helmet="iron_helmet", body="iron_body", legs="iron_legs")
|
||||||
p["bank"] = {"cooked_trout": 100}
|
p["bank"] = {"cooked_trout": 100}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue