From a7ff97815f8745fb17585e81577ccf35bc182bd3 Mon Sep 17 00:00:00 2001 From: rope Date: Tue, 27 Oct 2026 08:39:34 +0000 Subject: [PATCH] Prayer: bury on the spot, retuned costs Co-Authored-By: Claude Opus 5.5 --- README.md | 12 ++++---- app.py | 3 ++ game.py | 72 ++++++++++++++++++++++++++++++++++------------ static/app.js | 19 +++++++++--- tests/test_game.py | 32 +++++++++++++++++---- 5 files changed, 106 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 19530f1..0b7490b 100644 --- a/README.md +++ b/README.md @@ -145,11 +145,13 @@ Beta. The core game is in place; changes from here are mostly balance and tuning them a day, so it slows as the houses fill, plus slow family growth); it promotes the settlement (hamlet, village at 40, town at 90, city at 150), which raises building level caps and gives everyone bank slots. -- **Prayer:** enemies drop bones; burying them gives prayer points and - Prayer XP. Up to two prayers (eight unlock from level 1 to 60) boost each - fight while their points last, and praying trains Prayer too. A Temple adds - points per bone and lets players offer bones as devotion, which wards the - city in the next raid. +- **Prayer:** enemies drop bones, which are buried on the spot for prayer + points and half the XP of burying by hand (a setting; off sends them to + the bank, to bury by hand for full XP). Up to two prayers (eight unlock from level 1 to 60) boost each + fight while their points last, and praying trains Prayer. Early in each + bone tier bones cover two prayers; later a Prayer potion makes up the gap. + A Temple adds 25% points from bones a level and lets players offer bones as + devotion, which wards the city in the next raid. - **Herblore:** farmers grow six herbs (guam to snapdragon), one per tier. Every potion is a ladder of up to six tiers (I–VI): the tier's herb plus the matching rung of the skill's own resources (copper ore → runite ore, diff --git a/app.py b/app.py index dad5bbf..4b90e66 100644 --- a/app.py +++ b/app.py @@ -499,6 +499,9 @@ class Handler(BaseHTTPRequestHandler): if path == "/api/track": return game.toggle_track(player, str(body.get("id"))) if path == "/api/settings": + if isinstance(body.get("bury"), bool): + player["bury"] = body["bury"] + return None if body.get("overflow") not in ("city", "stop"): return "Bad setting" player["overflow"] = body["overflow"] diff --git a/game.py b/game.py index f2676cb..205bb61 100644 --- a/game.py +++ b/game.py @@ -1291,8 +1291,8 @@ ACTIONS = [ _action("pack_fish", "cooking", "Pack fish rations", 1, 20, 6, {"cooked_shrimp": 2}, {"provisions": 1}, group="Rations"), _action("drill_militia", "combat", "Drill the militia", 1, 60, 12, special="drill", group="Frontier"), _action("bury_bones", "prayer", "Bury bones", 1, 10, 4, {"bones": 1}, special="bury", pp=2, group="Bury"), - _action("bury_big_bones", "prayer", "Bury big bones", 15, 10, 15, {"big_bones": 1}, special="bury", pp=6, group="Bury"), - _action("bury_dragon_bones", "prayer", "Bury dragon bones", 40, 12, 72, {"dragon_bones": 1}, special="bury", pp=20, group="Bury"), + _action("bury_big_bones", "prayer", "Bury big bones", 15, 10, 15, {"big_bones": 1}, special="bury", pp=3, group="Bury"), + _action("bury_dragon_bones", "prayer", "Bury dragon bones", 40, 12, 72, {"dragon_bones": 1}, special="bury", pp=5, group="Bury"), _action("offer_bones", "prayer", "Offer bones at the temple", 1, 12, 7, {"bones": 1}, special="offer", devotion=2, city={"temple": 1}, group="Offer at the temple"), _action("offer_big_bones", "prayer", "Offer big bones", 15, 12, 25, {"big_bones": 1}, special="offer", @@ -1452,7 +1452,7 @@ BUILDINGS = { "base": {"stone": 700, "bricks": 200, "cut_stone": 60, "provisions": 60}, "advanced": {"beam": 15, "rope": 40, "iron_nails": 150}, "late": {"willow_logs": 80, "oak_plank": 35, "hide_glue": 40, "steel_hinges": 20}, - "effect": "Per level: +1 prayer point per bone, +25% devotion", + "effect": "Per level: +25% prayer points from bones, +25% devotion", }, "granary": { "name": "Granary", "icon": "🌾", "max": 3, "gold": 150, @@ -1524,7 +1524,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows # --- world --------------------------------------------------------------- -WORLD_VERSION = 32 +WORLD_VERSION = 33 def new_world(now: float, speed: float = 1.0) -> dict: @@ -1775,6 +1775,11 @@ def migrate(world: dict, now: float | None = None) -> dict: for player in world["players"].values(): player.setdefault("queue", []) world["version"] = 32 + if world["version"] < 33: + # Bury on the spot: fight bones go straight to prayer points. + for player in world["players"].values(): + player.setdefault("bury", True) + world["version"] = 33 return world @@ -2030,7 +2035,7 @@ def new_player(uid: str, name: str, cls: str, now: float) -> dict: "pp": 0, "prayers": [], "potions": {}, "hp": 44, "hp_at": now, "action": None, "stopped": None, "queue": [], - "last_active": now, "overflow": "city", + "last_active": now, "overflow": "city", "bury": True, "perks": {"bank_slot": 0}, "manual": [], "patterns": [], "found": [], "eat_below": EAT_BELOW, "rented": 0, "boosts": [], "bought": None, "deeds": {}, "owned": [], "look": dict(DEFAULT_LOOK), "garrison": False, @@ -2499,18 +2504,18 @@ def pack_xp(fight: dict, killed_only: bool = False) -> float: MEND = 0.0004 # Rapid Heal: share of max HP mended per second of fighting PRAYERS = [ - {"id": "thick_skin", "name": "Thick Skin", "icon": "🪨", "level": 1, "cost": 1, "reduce": 0.10}, - {"id": "burst_of_strength", "name": "Burst of Strength", "icon": "💪", "level": 5, "cost": 1, "power": 5}, - {"id": "rapid_heal", "name": "Rapid Heal", "icon": "💗", "level": 10, "cost": 1, "mend": MEND}, - {"id": "steel_skin", "name": "Steel Skin", "icon": "🛡️", "level": 20, "cost": 2, "reduce": 0.20}, - {"id": "superhuman_strength", "name": "Superhuman Strength", "icon": "🦾", "level": 30, "cost": 2, "power": 12}, - {"id": "second_wind", "name": "Second Wind", "icon": "🌬️", "level": 40, "cost": 1, "wind": True}, - {"id": "protection", "name": "Protection", "icon": "✨", "level": 50, "cost": 3, "reduce": 0.35}, - {"id": "piety", "name": "Piety", "icon": "😇", "level": 60, "cost": 4, "power": 20, "reduce": 0.20}, + {"id": "thick_skin", "name": "Thick Skin", "icon": "🪨", "level": 1, "cost": 2, "reduce": 0.10}, + {"id": "burst_of_strength", "name": "Burst of Strength", "icon": "💪", "level": 5, "cost": 2, "power": 5}, + {"id": "rapid_heal", "name": "Rapid Heal", "icon": "💗", "level": 10, "cost": 3, "mend": MEND}, + {"id": "steel_skin", "name": "Steel Skin", "icon": "🛡️", "level": 20, "cost": 4, "reduce": 0.20}, + {"id": "superhuman_strength", "name": "Superhuman Strength", "icon": "🦾", "level": 30, "cost": 6, "power": 12}, + {"id": "second_wind", "name": "Second Wind", "icon": "🌬️", "level": 40, "cost": 3, "wind": True}, + {"id": "protection", "name": "Protection", "icon": "✨", "level": 50, "cost": 9, "reduce": 0.35}, + {"id": "piety", "name": "Piety", "icon": "😇", "level": 60, "cost": 12, "power": 20, "reduce": 0.20}, ] PRAYER_BY_ID = {prayer["id"]: prayer for prayer in PRAYERS} MAX_PRAYERS = 2 -PRAYER_XP_PER_POINT = 5 +PRAYER_XP_PER_POINT = 3 NO_BLESSING = {"cost": 0, "power": 0, "taken": 1.0, "mend": 0, "wind": False} @@ -2554,8 +2559,34 @@ def set_prayers(player: dict, ids: list) -> str | None: return None -def bone_points(world: dict, action: dict) -> int: - return action["pp"] + building_level(world, "temple") + fitted(world, "carved_altar") +def bone_points(world: dict, action: dict) -> float: + return action["pp"] * (1 + 0.25 * (building_level(world, "temple") + fitted(world, "carved_altar"))) + + +BURY_LEVEL = {bone: ACTION_BY_ID[f"bury_{bone}"]["level"] for bone in ("bones", "big_bones", "dragon_bones")} +SPOT_BURY_XP = 0.5 # burying on the spot gives half the XP of burying by hand + + +def bury_on_the_spot(world: dict, player: dict, drops: dict, now: float, summary: dict) -> dict: + """A won fight's bones go straight into prayer points and half the XP of burying by hand. Bones + above the player's Prayer level, or everything with the setting off, go to the bank.""" + if not player.get("bury", True): + return drops + kept = dict(drops) + for bone, level in BURY_LEVEL.items(): + count = kept.get(bone, 0) + if not count or skill_level(player, "prayer") < level: + continue + del kept[bone] + action = ACTION_BY_ID[f"bury_{bone}"] + for _ in range(count): + points = bone_points(world, action) * (1 + potion_effect(player, "prayer").get("bones", 0)) + player["pp"] += points + summary["pp_gained"] += points + _use_charge(player, "prayer", summary) + xp = action["xp"] * count * SPOT_BURY_XP * skill_mods(world, action)["xp"] + gain_xp(world, player, "prayer", xp, now, summary) + return kept def ward(world: dict) -> float: @@ -2944,7 +2975,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R player["owned"].append(emblem) player["gold"] += gold summary["gold"] += gold - for item, qty in drops.items(): + for item, qty in bury_on_the_spot(world, player, drops, now, summary).items(): outputs[item] = outputs.get(item, 0) + qty if action.get("special") == "sabotage": raid = upcoming_raid(world) @@ -4887,7 +4918,7 @@ FITTINGS = [ ("bellows", "Great bellows", "smithy", 3, 50, {"maple_plank": 30, "bear_leather": 15, "mithril_brackets": 6}, "Smithing 10% faster", "bellows", "#d0663a"), ("carved_altar", "Carved altar", "temple", 2, 60, {"yew_plank": 30, "cut_stone": 60, "adamant_chain": 4}, - "+1 prayer point per bone, ward +15", "star-altar", "#6f9a4a"), + "+25% prayer points from bones, ward +15", "star-altar", "#6f9a4a"), ("enchanted_chests", "Enchanted chests", "storehouse", 4, 75, {"magic_plank": 40, "rune_lock": 6}, "+500 stack size", "open-treasure-chest", "#a77be8"), ("enchanted_hearths", "Enchanted hearths", "houses", 4, 80, {"magic_plank": 50, "mammoth_leather": 5, "adamant_chain": 6}, @@ -6162,6 +6193,11 @@ ORDER_RECIPES, ORDER_RAW = _order_sources() # What changed, shown on the Patch notes page. Newest first; every shipped change adds a # line here (new / balance / fix), and a new release a new entry on top. PATCH_NOTES = [ + {"date": "2026-10-27", "title": "Prayer", "notes": [ + ("new", "Bury on the spot: bones from combat turns into prayer points and 1/2 bury XP as they drop. Toggle in the Prayer menu."), + ("balance", "Prayer costs adjusted."), + ("balance", "Prayer potions now relevant for new costs."), + ]}, {"date": "2026-10-26", "title": "Fittings", "notes": [ ("new", "Guild Roster shows player activity."), ("new", "Make X: \"Make one\" takes a count (1–1,000), then back to what you were doing."), diff --git a/static/app.js b/static/app.js index 3a74514..b43e1b7 100644 --- a/static/app.js +++ b/static/app.js @@ -1231,6 +1231,13 @@ function drawFlame(cv, id, n) { ctx.globalAlpha = 1; } +// Temple levels and its carved altar: +25% prayer points from bones each. +function bonePercent() { + const temple = S.city.buildings.temple; + if (temple?.damaged) return 0; + return 25 *((temple?.level || 0) + (temple?.fittings || []).includes("carved_altar")); +} + function prayerPanel() { const me = S.me, city = S.city, level = me.levels.prayer; const cost = C.prayers.filter((p) => me.prayers.includes(p.id)).reduce((sum, p) => sum + p.cost, 0); @@ -1244,16 +1251,19 @@ function prayerPanel() { ${locked ? `Lv ${p.level}` : `${p.cost} pt${p.cost > 1 ? "s" : ""}/fight`} `; }).join(""); - const temple = city.buildings.temple.level; + const temple = city.buildings.temple.level, boost = bonePercent(); + const bury = me.bury !== false; return `

Prayers · ${me.prayers.length} / ${C.max_prayers} active

${skillOf("prayer").icon} ${fmt(me.pp)} prayer points${cost ? ` · ${cost} per fight (~${fmt(me.pp / cost)} fights)` : ""}
${cards}
-

Prayers cost points each fight. Bury bones for more.

+
+

Prayers cost points each fight; bones refill them.

Temple

${temple ? `

Devotion ${fmt(city.devotion)} · ward +${city.ward} defence (max +${15 * temple})

-

Offerings ward the next raid. +${temple} prayer point${temple > 1 ? "s" : ""} per bone buried.

` +

Offerings ward the next raid. +${boost}% prayer points from bones.

` : `

No temple yet.

`}
`; } @@ -1724,7 +1734,7 @@ function actionCard(a) { if (a.special === "labour") { extra = `
${skillOf("construction").icon} +${me.labour_per_action} labour per action
`; } - if (a.special === "bury") extra = `
${skillOf("prayer").icon} +${a.pp + temple} prayer points
`; + if (a.special === "bury") extra = `
${skillOf("prayer").icon} +${+(a.pp * (1 + bonePercent() / 100)).toFixed(2)} prayer points
`; if (a.special === "offer") extra = `
${C.buildings.temple.icon} +${+(a.devotion * (1 + 0.25 * temple)).toFixed(1)} devotion
`; const gear = Object.keys(a.outputs).filter((id) => GEAR_SLOTS.includes(item(id).slot)); const made = Object.keys(a.outputs).filter((id) => !gear.includes(id)).map(itemStats).filter(Boolean); @@ -2925,6 +2935,7 @@ document.addEventListener("click", (e) => { return act("/api/prayers", { ids: on ? S.me.prayers.filter((x) => x !== d.pray) : [...S.me.prayers, d.pray] }); } if (d.overflow) return act("/api/settings", { overflow: d.overflow }); + if (d.bury) return act("/api/settings", { bury: d.bury === "on" }); if (d.class) { pickedClass = d.class; render(); return; } if ("repick" in d) return act("/api/class", { cls: pickedClass }); if (d.tab) { tabs[d.tab] = d.group; remember(`tab.${d.tab}`, d.group); render(); return; } diff --git a/tests/test_game.py b/tests/test_game.py index 5b16cac..2f1eb8d 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -1448,7 +1448,29 @@ class PrayerTest(unittest.TestCase): self.assertEqual(p["pp"], 10) self.assertEqual(p["xp"]["prayer"], game.XP_TABLE[1] + 20) world["city"]["buildings"]["temple"]["level"] = 2 - self.assertEqual(game.bone_points(world, game.ACTION_BY_ID["bury_bones"]), 4) + self.assertEqual(game.bone_points(world, game.ACTION_BY_ID["bury_bones"]), 3) + + def test_fight_bones_bury_on_the_spot(self): + world, p = self.priest(level=10) + world["city"]["buildings"]["temple"]["level"] = 2 + p["potions"]["prayer"] = {"item": "prayer_potion_2", "charges": 50} + summary = game.empty_summary() + kept = game.bury_on_the_spot(world, p, {"bones": 2, "big_bones": 1, "hide": 1}, T0, summary) + self.assertEqual(kept, {"big_bones": 1, "hide": 1}) # big bones need prayer 15 + self.assertEqual(p["pp"], 2 * 3 * 1.5) + self.assertEqual(p["potions"]["prayer"]["charges"], 48) + self.assertEqual(summary["xp"]["prayer"], 2 * game.ACTION_BY_ID["bury_bones"]["xp"] * game.SPOT_BURY_XP) + p["bury"] = False + self.assertEqual(game.bury_on_the_spot(world, p, {"bones": 2}, T0, summary), {"bones": 2}) + + def test_won_fights_bury_their_bones(self): + world, p = self.priest(level=10) + p["bank"] = {"bread": 200} + game.start_action(world, p, "fight_wolves", T0) + summary = game.process_player(world, p, T0 + 3600, random.Random(1)) + self.assertGreater(summary["wins"], 0) + self.assertNotIn("bones", p["bank"]) + self.assertGreaterEqual(summary["pp_gained"], 2 * summary["wins"]) def test_choosing_prayers(self): world, p = self.priest(level=10) @@ -1461,17 +1483,17 @@ class PrayerTest(unittest.TestCase): scouts = game.ACTION_BY_ID["fight_scouts"] plain = game.fight_odds(p, scouts)["damage"] game.set_prayers(p, ["thick_skin", "burst_of_strength"]) - p["pp"] = 4 + p["pp"], p["bury"] = 8, False blessed = game.blessing(p) - self.assertEqual(blessed["cost"], 2) + self.assertEqual(blessed["cost"], 4) self.assertLess(game.fight_odds(p, scouts)["damage"], plain * 0.9) p["bank"] = {"bread": 200} game.start_action(world, p, "fight_scouts", T0) summary = game.process_player(world, p, T0 + 600, random.Random(1)) self.assertEqual(p["pp"], 0) - self.assertEqual(summary["pp_spent"], 4) + self.assertEqual(summary["pp_spent"], 8) self.assertTrue(summary["prayer_out"]) - self.assertEqual(p["xp"]["prayer"], game.XP_TABLE[10] + 4 * game.PRAYER_XP_PER_POINT) + self.assertEqual(p["xp"]["prayer"], game.XP_TABLE[10] + 8 * game.PRAYER_XP_PER_POINT) def test_second_wind_saves_one_knockout(self): world, p = self.priest(level=40, combat=10)