Herblore: herbs, potions with charges, one per skill

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-18 23:12:57 +01:00
parent 278c5f4283
commit 090b202188
5 changed files with 245 additions and 19 deletions

144
game.py
View file

@ -176,6 +176,65 @@ for _raw, _cooked, _level, _action_id, _verb in FISH:
ITEMS[_raw[0]] = {"name": _raw[1], "icon": _raw[2], "value": _raw[3]}
ITEMS[_cooked[0]] = {"name": _cooked[1], "icon": _cooked[2], "value": _cooked[3], "heal": _cooked[4], "food": _cooked[5]}
# --- herblore -------------------------------------------------------------------
# Herbs from Farming, brewed with a secondary into potions with charges. One
# potion per skill at a time, as in Melvor; each action in that skill uses a
# charge, and an empty potion is replaced from the bank if there's another.
# id, name, icon, farming level, seconds, xp, value
HERBS = [
("guam_leaf", "Guam leaf", "🍀", 5, 45, 12, 3),
("marrentill", "Marrentill", "🌼", 15, 50, 20, 5),
("tarromin", "Tarromin", "🌺", 25, 55, 30, 8),
("harralander", "Harralander", "🌻", 35, 60, 42, 12),
("ranarr", "Ranarr", "🪻", 45, 65, 55, 18),
("snapdragon", "Snapdragon", "🌸", 60, 70, 75, 28),
]
POTION_CHARGES = 50
# id, name, herblore level, herb, secondary, skill, effect
POTIONS = [
("trackers_tincture", "Tracker's tincture", 3, "guam_leaf", "feathers", "hunting", {"speed": 0.2}),
("lumberjacks_tonic", "Lumberjack's tonic", 6, "guam_leaf", "logs", "woodcutting", {"speed": 0.2}),
("strength_potion", "Strength potion", 10, "guam_leaf", "bones", "combat", {"power": 6}),
("anglers_brew", "Angler's brew", 15, "marrentill", "raw_shrimp", "fishing", {"speed": 0.2}),
("prospectors_draught", "Prospector's draught", 18, "marrentill", "copper_ore", "mining", {"double": 0.15}),
("growth_potion", "Growth potion", 20, "marrentill", "wheat", "farming", {"speed": 0.2}),
("defence_potion", "Defence potion", 25, "tarromin", "iron_ore", "combat", {"reduce": 0.15}),
("chefs_spice", "Chef's spice", 28, "tarromin", "vegetables", "cooking", {"keep": 0.15}),
("bonedust_tonic", "Bonedust tonic", 30, "tarromin", "big_bones", "prayer", {"bones": 0.5}),
("smiths_focus", "Smith's focus", 35, "harralander", "coal", "smithing", {"keep": 0.15}),
("masons_patience", "Mason's patience", 38, "harralander", "cut_stone", "masonry", {"keep": 0.15}),
("tanners_oil", "Tanner's oil", 40, "harralander", "raw_meat", "leatherworking", {"keep": 0.15}),
("regeneration_potion", "Regeneration potion", 45, "ranarr", "cooked_trout", "combat", {"heal": 4}),
("super_strength", "Super strength", 50, "ranarr", "dragon_bones", "combat", {"power": 14}),
("super_defence", "Super defence", 60, "snapdragon", "blue_dragonhide", "combat", {"reduce": 0.30}),
]
def describe_potion(skill: str, effect: dict) -> str:
name = "combat" if skill == "combat" else skill
if "speed" in effect:
return f"{name.title()} {round(effect['speed'] * 100)}% faster"
if "double" in effect:
return f"{name.title()}: {round(effect['double'] * 100)}% chance of double yield"
if "keep" in effect:
return f"{name.title()}: {round(effect['keep'] * 100)}% chance to keep your materials"
if "bones" in effect:
return f"+{round(effect['bones'] * 100)}% prayer points and devotion from bones"
if "power" in effect:
return f"+{effect['power']} combat power"
if "reduce" in effect:
return f"-{round(effect['reduce'] * 100)}% damage taken in fights"
return f"Heals {effect['heal']} HP every fight"
for _id, _name, _icon, _level, _time, _xp, _value in HERBS:
ITEMS[_id] = {"name": _name, "icon": _icon, "value": _value}
for _id, _name, _level, _herb, _second, _skill, _effect in POTIONS:
ITEMS[_id] = {"name": _name, "icon": "⚗️" if _skill == "combat" else "🧪",
"value": ITEMS[_herb]["value"] * 3 + ITEMS[_second]["value"],
"potion": {"skill": _skill, **_effect, "text": describe_potion(_skill, _effect)}}
# What the city eats first: plain food before the good stuff.
FOOD_ORDER = tuple(sorted((item for item, spec in ITEMS.items() if "food" in spec), key=lambda i: ITEMS[i]["heal"]))
EQUIP_SLOTS = ["weapon", "shield", "helmet", "body", "legs"] + [f"tool_{skill}" for skill in TOOLS]
@ -190,13 +249,14 @@ SKILLS = [
{"id": "masonry", "name": "Masonry", "icon": "🧱", "group": "Crafting"},
{"id": "cooking", "name": "Cooking", "icon": "🍳", "group": "Crafting"},
{"id": "leatherworking", "name": "Leatherworking", "icon": "🪡", "group": "Crafting"},
{"id": "herblore", "name": "Herblore", "icon": "🧪", "group": "Crafting"},
{"id": "combat", "name": "Combat", "icon": "⚔️", "group": "Combat"},
{"id": "prayer", "name": "Prayer", "icon": "🙏", "group": "Combat"},
{"id": "reconnaissance", "name": "Reconnaissance", "icon": "🔭", "group": "Signature", "class": "scout"},
]
SKILL_IDS = [skill["id"] for skill in SKILLS]
GATHERING = {"mining", "woodcutting", "farming", "hunting", "fishing"}
CRAFTING = {"smithing", "masonry", "cooking", "leatherworking"}
CRAFTING = {"smithing", "masonry", "cooking", "leatherworking", "herblore"}
SURVEYED = {"mining", "woodcutting", "farming"} # what the pioneer's double yield applies to
CLASSES = {
@ -281,6 +341,11 @@ ACTIONS = [
_action("mine_stone", "mining", "Quarry stone", 1, 20, 5, outputs={"stone": 1}),
_action("farm_wheat", "farming", "Grow wheat", 1, 25, 6, outputs={"wheat": 2}),
_action("farm_veg", "farming", "Grow vegetables", 12, 35, 13, outputs={"vegetables": 2}),
*[_action(f"grow_{herb}", "farming", f"Grow {name.lower()}", level, seconds, xp, outputs={herb: 1})
for herb, name, _icon, level, seconds, xp, _value in HERBS],
*[_action(f"brew_{potion}", "herblore", name, level, 30 + level // 3, round(4 + level * 1.6),
{herb: 1, second: 1}, {potion: 1}, group="Combat potions" if skill == "combat" else "Skill potions")
for potion, name, level, herb, second, skill, _effect in POTIONS],
_action("hunt_birds", "hunting", "Snare birds", 1, 25, 6, outputs={"feathers": 3}),
_action("hunt_deer", "hunting", "Hunt deer", 8, 40, 13, outputs={"raw_meat": 2, "hide": 1}),
_action("hunt_boar", "hunting", "Hunt boar", 25, 50, 30, outputs={"raw_meat": 2, "boar_hide": 1}),
@ -398,7 +463,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows
# --- world ---------------------------------------------------------------
WORLD_VERSION = 5
WORLD_VERSION = 6
def new_world(now: float, speed: float = 1.0) -> dict:
@ -459,6 +524,11 @@ def migrate(world: dict, now: float | None = None) -> dict:
world["city"]["buildings"].setdefault("temple", {"level": 0, "damaged": False, "fund": {}})
world["city"].setdefault("devotion", 0)
world["version"] = 5
if world["version"] < 6:
for player in world["players"].values():
player["xp"].setdefault("herblore", 0)
player.setdefault("potions", {})
world["version"] = 6
return world
@ -555,7 +625,7 @@ def new_player(uid: str, name: str, cls: str, now: float) -> dict:
"bank": {}, "gold": 0, "favour": 0,
"equip": {slot: None for slot in EQUIP_SLOTS},
"food": None, # preferred food in fights; None eats the cheapest first
"pp": 0, "prayers": [],
"pp": 0, "prayers": [], "potions": {},
"hp": 44, "hp_at": now,
"action": None, "stopped": None,
"last_active": now, "overflow": "city",
@ -764,9 +834,50 @@ def action_time(world: dict, player: dict, action: dict) -> float:
seconds *= 1 - ITEMS[tool]["speed"]
if action["skill"] == "smithing":
seconds *= 1 - 0.08 * building_level(world, "smithy")
seconds *= 1 - potion_effect(player, action["skill"]).get("speed", 0)
return seconds / speed(world)
def with_potion(blessed: dict, brew: dict) -> dict:
"""A fight's prayer boosts with a combat potion's added on top."""
return {**blessed, "power": blessed["power"] + brew.get("power", 0),
"taken": blessed["taken"] * (1 - brew.get("reduce", 0))}
def potion_effect(player: dict, skill: str) -> dict:
active = player.get("potions", {}).get(skill)
return ITEMS[active["item"]]["potion"] if active else {}
def use_potion(player: dict, item: str) -> str | None:
"""Drink a potion from the bank into its skill's slot (topping up the same potion)."""
spec = ITEMS.get(item, {}).get("potion")
if not spec:
return "That isn't a potion"
if player["bank"].get(item, 0) < 1:
return "You don't have one"
bank_take(player, item, 1)
current = player["potions"].get(spec["skill"])
charges = current["charges"] if current and current["item"] == item else 0
player["potions"][spec["skill"]] = {"item": item, "charges": charges + POTION_CHARGES}
return None
def _use_charge(player: dict, skill: str, summary: dict) -> None:
active = player["potions"].get(skill)
if not active:
return
active["charges"] -= 1
if active["charges"] > 0:
return
del player["potions"][skill]
if player["bank"].get(active["item"], 0):
use_potion(player, active["item"])
summary["potions_drunk"][active["item"]] = summary["potions_drunk"].get(active["item"], 0) + 1
else:
summary["potion_out"].append(active["item"])
def upcoming_raid(world: dict) -> dict | None:
for event in world["events"]:
if event["status"] == "upcoming" and event["kind"] == "raid":
@ -805,8 +916,12 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
def _complete(world: dict, player: dict, action: dict, now: float, rng: random.Random, summary: dict) -> None:
for item, qty in action["inputs"].items():
bank_take(player, item, qty)
brew = potion_effect(player, action["skill"])
if action["inputs"] and rng.random() < brew.get("keep", 0):
summary["kept"] += 1
else:
for item, qty in action["inputs"].items():
bank_take(player, item, qty)
xp = action["xp"]
lucky = (
(player["cls"] == "artisan" and action["skill"] in CRAFTING)
@ -815,6 +930,10 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
outputs = {item: qty * (2 if lucky else 1) for item, qty in action["outputs"].items()}
if lucky:
summary["lucky"] += 1
if brew.get("double") and rng.random() < brew["double"]:
for item, qty in action["outputs"].items():
outputs[item] += qty
summary["lucky"] += 1
if action["skill"] == "combat":
blessed = blessing(player)
@ -825,11 +944,12 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
summary["pp_spent"] += blessed["cost"]
gain_xp(world, player, "prayer", blessed["cost"] * PRAYER_XP_PER_POINT, now, summary)
regen(world, player, now, blessed["regen"])
blessed = with_potion(blessed, brew)
auto_eat(player, summary)
chance = 0.5 + (combat_power(player) + blessed["power"] - action["difficulty"]) * 0.04
won = rng.random() < min(0.97, max(0.15, chance))
hurt = fight_damage(player, action, blessed) * rng.uniform(0.6, 1.4) * (1 if won else 1.3)
player["hp"] -= hurt
player["hp"] = min(max_hp(player), player["hp"] - hurt + brew.get("heal", 0))
summary["damage"] += hurt
auto_eat(player, summary)
outing = player["action"]
@ -863,12 +983,14 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
event = upcoming_event(world)
if event:
event["recon"] += action["recon"]
bonus = 1 + brew.get("bones", 0)
if action.get("special") == "bury":
player["pp"] += bone_points(world, action)
summary["pp_gained"] += bone_points(world, action)
player["pp"] += bone_points(world, action) * bonus
summary["pp_gained"] += bone_points(world, action) * bonus
if action.get("special") == "offer":
city = world["city"]
city["devotion"] = city.get("devotion", 0) + action["devotion"] * (1 + 0.25 * building_level(world, "temple"))
city["devotion"] = city.get("devotion", 0) + action["devotion"] * (1 + 0.25 * building_level(world, "temple")) * bonus
_use_charge(player, action["skill"], summary)
for item, qty in outputs.items():
kept = bank_add(world, player, item, qty, now, summary)
@ -894,7 +1016,8 @@ 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": {}, "damage": 0,
"gold": 0, "wins": 0, "losses": 0, "lucky": 0, "stopped": None, "resumed": None,
"sold": {}, "sold_gold": 0, "pp_spent": 0, "pp_gained": 0, "prayer_out": False, "second_wind": 0}
"sold": {}, "sold_gold": 0, "pp_spent": 0, "pp_gained": 0, "prayer_out": False, "second_wind": 0,
"kept": 0, "potions_drunk": {}, "potion_out": []}
def process_player(world: dict, player: dict, now: float, rng: random.Random | None = None) -> dict:
@ -1579,5 +1702,6 @@ def content() -> dict:
"buildings": buildings, "xp_table": XP_TABLE, "offline_cap": OFFLINE_CAP, "tiers": TIERS,
"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,
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "regen_per_hour": REGEN_PER_HOUR,
}