companions.py
"""Companion character database: 20x3★, 10x4★, 5x5★."""
from __future__ import annotations
import random
from data_types import *
from character import Character, JOB_BASE_STATS, JOB_GROWTH
from skills_db import SKILL_DB, JOB_SKILL_POOL
def _make_companion(name, job, weapon, element, rarity,
stat_mult=1.0, growth_mult=1.0) -> Character:
base = JOB_BASE_STATS[job].copy()
g = JOB_GROWTH[job]
# Apply rarity scaling
base.hp = int(base.hp * stat_mult)
base.mp = int(base.mp * stat_mult)
base.patk = int(base.patk * stat_mult)
base.matk = int(base.matk * stat_mult)
base.pdef = int(base.pdef * stat_mult)
base.mdef = int(base.mdef * stat_mult)
base.spd = int(base.spd * stat_mult)
# Scale growth rates
scaled_g = GrowthRates(
hp = min(0.99, g.hp * growth_mult),
mp = min(0.99, g.mp * growth_mult),
patk = min(0.99, g.patk * growth_mult),
matk = min(0.99, g.matk * growth_mult),
pdef = min(0.99, g.pdef * growth_mult),
mdef = min(0.99, g.mdef * growth_mult),
spd = min(0.99, g.spd * growth_mult),
eva = min(0.99, g.eva * growth_mult),
acc = min(0.99, g.acc * growth_mult),
crit = min(0.99, g.crit * growth_mult),
)
pool = JOB_SKILL_POOL[job]
basics = [sid for sid in pool if SKILL_DB[sid].tier == SkillTier.BASIC][:4]
inters = [sid for sid in pool if SKILL_DB[sid].tier == SkillTier.INTERMEDIATE][:4]
ultims = [sid for sid in pool if SKILL_DB[sid].tier == SkillTier.ULTIMATE][:2]
chosen_basic = random.sample(basics, min(2, len(basics)))
chosen_inter = random.sample(inters, min(2, len(inters)))
chosen_ultim = random.sample(ultims, min(1, len(ultims)))
skill_pool = chosen_basic + chosen_inter + chosen_ultim
unlocked = chosen_basic[:]
return Character(
name=name, job=job, weapon=weapon, element=element, rarity=rarity,
base_stats=base, growth=scaled_g,
skill_pool=skill_pool, unlocked_skills=unlocked
)
# ══════════════════════════════════════════════════════════════════════════
# COMPANION TEMPLATES (factories — called fresh each game)
# ══════════════════════════════════════════════════════════════════════════
# stat_mult: 3★=0.90~0.95, 4★=1.10~1.20, 5★=1.35~1.50
# growth_mult: 3★=0.85~0.90, 4★=1.05~1.15, 5★=1.25~1.40
THREE_STAR_TEMPLATES = [
# name, job, weapon, element, stat_mult, growth_mult
("Rook", JobClass.WARRIOR, WeaponType.SWORD, Element.FIRE, 0.90, 0.88),
("Gara", JobClass.KNIGHT, WeaponType.SWORD, Element.EARTH, 0.92, 0.87),
("Tifa", JobClass.MONK, WeaponType.AXE, Element.WIND, 0.91, 0.88),
("Sera", JobClass.CLERIC, WeaponType.STAFF, Element.LIGHT, 0.93, 0.88),
("Rex", JobClass.RANGER, WeaponType.BOW, Element.ICE, 0.90, 0.87),
("Bram", JobClass.BERSERKER, WeaponType.AXE, Element.FIRE, 0.89, 0.86),
("Nira", JobClass.WIND_MAGE, WeaponType.STAFF, Element.WIND, 0.92, 0.87),
("Dag", JobClass.SPEARMAN, WeaponType.SPEAR, Element.LIGHTNING, 0.91, 0.88),
("Ella", JobClass.FIRE_MAGE, WeaponType.STAFF, Element.FIRE, 0.90, 0.87),
("Wynn", JobClass.HUNTER, WeaponType.BOW, Element.EARTH, 0.93, 0.88),
("Bart", JobClass.EARTH_MAGE, WeaponType.STAFF, Element.EARTH, 0.91, 0.86),
("Yuna", JobClass.PRIEST, WeaponType.STAFF, Element.LIGHT, 0.92, 0.87),
("Kage", JobClass.ASSASSIN, WeaponType.DAGGER, Element.DARK, 0.90, 0.86),
("Coral", JobClass.ICE_MAGE, WeaponType.STAFF, Element.ICE, 0.91, 0.87),
("Miles", JobClass.WARRIOR, WeaponType.AXE, Element.WIND, 0.90, 0.86),
("Dora", JobClass.CLERIC, WeaponType.STAFF, Element.FIRE, 0.92, 0.87),
("Oryn", JobClass.STORM_MAGE, WeaponType.STAFF, Element.LIGHTNING, 0.90, 0.86),
("Fenn", JobClass.PALADIN, WeaponType.SWORD, Element.LIGHT, 0.93, 0.88),
("Zell", JobClass.DRAGOON, WeaponType.SPEAR, Element.WIND, 0.91, 0.87),
("Mira", JobClass.DARK_MAGE, WeaponType.STAFF, Element.DARK, 0.90, 0.86),
]
FOUR_STAR_TEMPLATES = [
("Cael", JobClass.KNIGHT, WeaponType.SWORD, Element.LIGHT, 1.12, 1.10),
("Lyra", JobClass.ARCANE_SAGE,WeaponType.STAFF, Element.LIGHTNING, 1.15, 1.12),
("Vance", JobClass.BERSERKER, WeaponType.AXE, Element.DARK, 1.18, 1.14),
("Shira", JobClass.ASSASSIN, WeaponType.DAGGER, Element.ICE, 1.12, 1.10),
("Bael", JobClass.DRAGOON, WeaponType.SPEAR, Element.FIRE, 1.14, 1.11),
("Noel", JobClass.PRIEST, WeaponType.STAFF, Element.LIGHT, 1.15, 1.12),
("Kira", JobClass.STORM_MAGE, WeaponType.STAFF, Element.LIGHTNING, 1.16, 1.13),
("Roan", JobClass.PALADIN, WeaponType.SWORD, Element.LIGHT, 1.14, 1.10),
("Zara", JobClass.HUNTER, WeaponType.BOW, Element.WIND, 1.12, 1.10),
("Drax", JobClass.MONK, WeaponType.AXE, Element.EARTH, 1.18, 1.14),
]
FIVE_STAR_TEMPLATES = [
("Seraph", JobClass.LIGHT_MAGE, WeaponType.STAFF, Element.LIGHT, 1.45, 1.38),
("Abyss", JobClass.DARK_MAGE, WeaponType.STAFF, Element.DARK, 1.42, 1.35),
("Titan", JobClass.BERSERKER, WeaponType.AXE, Element.EARTH, 1.50, 1.40),
("Oracle", JobClass.ARCANE_SAGE,WeaponType.STAFF, Element.LIGHTNING, 1.45, 1.40),
("Valkyrie",JobClass.PALADIN, WeaponType.SWORD, Element.LIGHT, 1.48, 1.38),
]
COMPANION_GACHA_POOL = {
3: THREE_STAR_TEMPLATES,
4: FOUR_STAR_TEMPLATES,
5: FIVE_STAR_TEMPLATES,
}
GACHA_RATES = {3: 0.65, 4: 0.30, 5: 0.05}
def summon_companion(target_level: int = 2) -> Character:
"""Roll a random companion via gacha, leveled to target_level."""
roll = random.random()
cumulative = 0.0
rarity = 3
for r, rate in GACHA_RATES.items():
cumulative += rate
if roll < cumulative:
rarity = r
break
templates = COMPANION_GACHA_POOL[rarity]
tmpl = random.choice(templates)
name, job, weapon, element, stat_mult, growth_mult = tmpl
char = _make_companion(name, job, weapon, element, rarity,
stat_mult, growth_mult)
# Level the character up silently
for _ in range(1, target_level):
char._level_up()
char.level = target_level
char.current_hp = char.max_hp
char.current_mp = char.max_mp
return char