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