Files
Flora/engine/runes.py
T

192 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import random
# Rune kinds (gems). Each rune is an item equipped into a plant's slots.
RUNE_COLORS = ("sapphire", "ruby", "amethyst", "jade", "brown")
RUNE_NAMES = {
"sapphire": "Sapphire Rune",
"ruby": "Ruby Rune",
"amethyst": "Amethyst Rune",
"jade": "Jade Rune",
"brown": "Brown Rune",
}
# Tier classification: sapphire/ruby/amethyst/jade are Tier 1, brown is Tier 2.
TIER1_COLORS = ("sapphire", "ruby", "amethyst", "jade")
TIER2_COLORS = ("brown",)
RUNE_TIERS = {c: 1 for c in TIER1_COLORS}
RUNE_TIERS.update({c: 2 for c in TIER2_COLORS})
# Sprite file (without extension) per rune color.
RUNE_ICON_FILES = {
"sapphire": "sapphire_rune",
"ruby": "ruby_rune",
"jade": "jade_rune",
"amethyst": "amethyst_rune",
"brown": "rune_brown",
}
# Old color names kept in earlier saves, mapped to the current gem names.
RUNE_COLOR_ALIASES = {
"blue": "sapphire",
"red": "ruby",
"white": "amethyst",
"yellow": "jade",
}
def canonical_color(color: str) -> str:
"""Map an old rune color name (blue/red/white/yellow) to the current gem."""
return RUNE_COLOR_ALIASES.get(color, color)
# Primitive effects: each rune rolls its own stat and stacks additively with
# other equipped runes of the same effect. Brown is rarer and rolls lower, but
# turns part of the damage this plant deals into a Wilt.
RUNE_EFFECTS = {
"ruby": "damage", # +damage dealt
"sapphire": "shield", # +shielding
"jade": "healing", # +healing received
"amethyst": "damage_taken", # -damage taken
"brown": "wilt", # damage dealt also Wils the target
}
PCT_MIN = 25
PCT_MAX = 35
BROWN_PCT_MIN = 5
BROWN_PCT_MAX = 15
# a plant can't stack more than this many runes of the same type
MAX_RUNES_PER_TYPE = 2
# coin costs to buy a random rune of a tier in the runes shop
T1_RUNE_COST = 100
T2_RUNE_COST = 150
# brown drops less often than the other runes (relative weights)
RUNE_WEIGHTS = {"sapphire": 1.0, "ruby": 1.0, "amethyst": 1.0, "jade": 1.0, "brown": 0.75}
_EFFECT_TEXTS = {
"ruby": "+{pct}% damage dealt",
"sapphire": "+{pct}% shielding",
"jade": "+{pct}% healing",
"amethyst": "-{pct}% damage taken",
"brown": "deals {pct}% of damage as Wilt",
}
RUNE_SUMMARIES = {
"ruby": "Increases damage dealt",
"sapphire": "Increases shielding",
"jade": "Increases healing received",
"amethyst": "Reduces damage taken",
"brown": "Deals a portion of damage as Wilt",
}
MAX_RUNE_SLOTS = 3
# Drop chances (test build)
BATTLE_DROP_CHANCE = 0.15 # rare rune from a normal floor battle win
PACK_DROP_CHANCE = 0.05 # rare additional rune from a seed pack
def random_color() -> str:
"""Roll a rune color; brown is weighted rarer than the others."""
weights = [RUNE_WEIGHTS.get(c, 1.0) for c in RUNE_COLORS]
return random.choices(RUNE_COLORS, weights=weights, k=1)[0]
def roll_pct(color: str = None) -> int:
"""Roll a rune's stat percentage. Brown rolls 5-15; the rest 25-35."""
if color == "brown":
return random.randint(BROWN_PCT_MIN, BROWN_PCT_MAX)
return random.randint(PCT_MIN, PCT_MAX)
def name(color: str) -> str:
return RUNE_NAMES.get(color, "Rune")
def tier(color: str) -> int:
return RUNE_TIERS.get(color, 1)
def roll_ratio(rune) -> float:
"""How close a rune's roll is to its own maximum (0..1). Brown's 5-15 range
is compared to its 15 cap, so a maxed brown ranks equally with a maxed
yellow even though 15 < 35."""
max_pct = BROWN_PCT_MAX if rune.get("color") == "brown" else PCT_MAX
return rune.get("pct", 0) / max_pct
def effect_text(color: str, pct: int) -> str:
"""Human-readable effect for a rune of `color` with rolled stat `pct`."""
return _EFFECT_TEXTS.get(color, "").format(pct=pct)
_CARD_TEXTS = {
"ruby": "+{pct}% dmg",
"sapphire": "+{pct}% shield",
"jade": "+{pct}% heal",
"amethyst": "-{pct}% taken",
"brown": "Wilt {pct}%",
}
def card_text(color: str, pct: int) -> str:
"""Compact effect label that fits a small card (e.g. 'Wilt 12%')."""
return _CARD_TEXTS.get(color, "").format(pct=pct)
def effect_for(color: str) -> str:
return RUNE_EFFECTS.get(color, "")
_EFFECT_COLORS = {v: k for k, v in RUNE_EFFECTS.items()}
def color_for_effect(effect: str) -> str:
"""The rune colour whose effect is `effect` (for showing enemy runes)."""
return _EFFECT_COLORS.get(effect, "")
def summary(color: str) -> str:
return RUNE_SUMMARIES.get(color, "")
def roll_range(color: str) -> str:
"""The stat range a rune of `color` rolls between (brown is lower)."""
if color == "brown":
return f"{BROWN_PCT_MIN}–{BROWN_PCT_MAX}%"
return f"{PCT_MIN}–{PCT_MAX}%"
def slots_for_max_stage(max_stage: int) -> int:
"""Permanent rune slots unlocked by the highest evolution stage a plant has
ever reached (stage 1 -> 1 slot, stage 2 -> 2, stage 3 -> 3)."""
return min(MAX_RUNE_SLOTS, max(1, max_stage))
def enemy_rune_count(stage: int) -> int:
"""How many runes a tower enemy carries at `stage` (more as the tower ramps)."""
if stage < 3:
return 0
return min(3, 1 + (stage - 3) // 5)
def roll_enemy_runes(stage: int) -> list:
"""Roll the runes a single tower enemy carries at `stage`. Returns a list of
(effect_key, pct_fraction) pairs ready for `Unit.runes`. At most
MAX_RUNES_PER_TYPE runes of the same type are rolled (same rule as players)."""
count = enemy_rune_count(stage)
runes_list = []
counts = {}
while len(runes_list) < count:
color = random_color()
if counts.get(color, 0) >= MAX_RUNES_PER_TYPE:
continue # type already at the cap; roll another colour
counts[color] = counts.get(color, 0) + 1
runes_list.append((effect_for(color), roll_pct(color) / 100.0))
return runes_list
def per_rune_chance(source_chance: float) -> tuple:
"""Effective drop chance (low, high) for a specific rune colour from a drop
source, where brown's lower weight makes it the low end."""
weights = [RUNE_WEIGHTS.get(c, 1.0) for c in RUNE_COLORS]
total = sum(weights)
return source_chance * min(weights) / total, source_chance * max(weights) / total