Prayer: bury on the spot, retuned costs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-27 08:39:34 +00:00
parent 40e5274c4e
commit a7ff97815f
5 changed files with 106 additions and 32 deletions

View file

@ -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,

3
app.py
View file

@ -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"]

72
game.py
View file

@ -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."),

View file

@ -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() {
<span class="small ${locked ? "why" : "muted"}">${locked ? `Lv ${p.level}` : `${p.cost} pt${p.cost > 1 ? "s" : ""}/fight`}</span>
</button>`;
}).join("");
const temple = city.buildings.temple.level;
const temple = city.buildings.temple.level, boost = bonePercent();
const bury = me.bury !== false;
return `<div class="cols">
<div class="panel"><h2>Prayers · ${me.prayers.length} / ${C.max_prayers} active</h2>
<div class="prayer-points">${skillOf("prayer").icon} <b>${fmt(me.pp)}</b> prayer points${cost ? ` · ${cost} per fight (~${fmt(me.pp / cost)} fights)` : ""}</div>
<div class="prayers">${cards}</div>
<p class="small muted">Prayers cost points each fight. Bury bones for more.</p>
<div class="row"><button class="btn sm ${bury ? "gold" : ""}" data-bury="${bury ? "off" : "on"}"
title="Fight bones turn into prayer points and ½ bury XP as they drop. Off: they go to the bank.">🦴 Bury on the spot: ${bury ? "on" : "off"}</button></div>
<p class="small muted">Prayers cost points each fight; bones refill them.</p>
</div>
<div class="panel"><h2>Temple</h2>
${temple ? `<p>Devotion <b>${fmt(city.devotion)}</b> · ward <b>+${city.ward}</b> defence (max +${15 * temple})</p>
<p class="small muted">Offerings ward the next raid. +${temple} prayer point${temple > 1 ? "s" : ""} per bone buried.</p>`
<p class="small muted">Offerings ward the next raid. +${boost}% prayer points from bones.</p>`
: `<p class="muted">No temple yet.</p>`}
</div></div>`;
}
@ -1724,7 +1734,7 @@ function actionCard(a) {
if (a.special === "labour") {
extra = `<div class="small makes">${skillOf("construction").icon} +${me.labour_per_action} labour per action</div>`;
}
if (a.special === "bury") extra = `<div class="small makes">${skillOf("prayer").icon} +${a.pp + temple} prayer points</div>`;
if (a.special === "bury") extra = `<div class="small makes">${skillOf("prayer").icon} +${+(a.pp * (1 + bonePercent() / 100)).toFixed(2)} prayer points</div>`;
if (a.special === "offer") extra = `<div class="small makes">${C.buildings.temple.icon} +${+(a.devotion * (1 + 0.25 * temple)).toFixed(1)} devotion</div>`;
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; }

View file

@ -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)