Initial commit: Plants turn-based battle game
This commit is contained in:
+192
@@ -0,0 +1,192 @@
|
||||
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
|
||||
Reference in New Issue
Block a user