"""Sandboxed, data-only skill specs for HuggingWizards. A "skill" is a plain JSON object describing *what* happens and *when* — never executable code. The engine interprets a whitelisted set of triggers/effects, so a skill authored by the Game Master (Nemotron) at runtime can hot-plug onto a player without any `exec`/`eval` or arbitrary code execution. Schema (all fields optional except trigger/effect; everything is clamped): { "name": str, # display name "trigger": every_n_attacks|periodic|on_kill, "n": int, # for every_n_attacks "interval": float, # seconds, for periodic "effect": aoe_damage|projectile_nova|heal|summon_ally|shield, "radius": float, # aoe reach "damage": float, "count": int, # nova projectiles / allies summoned "amount": float, # heal / shield amount "slow": float, # 0..1 slow applied by aoe "color": str, # client tint (#rrggbb) } """ from __future__ import annotations TRIGGERS = {"every_n_attacks", "periodic", "on_kill"} EFFECTS = {"aoe_damage", "projectile_nova", "heal", "summon_ally", "shield"} # Per-field (lo, hi) clamps. Keeps any model output inside sane, balanced bounds. _BOUNDS = { "n": (2, 20), "interval": (1.0, 15.0), "radius": (40.0, 260.0), "damage": (0.0, 120.0), "count": (1, 12), "amount": (0.0, 80.0), "slow": (0.0, 0.8), } _DEFAULTS = { "every_n_attacks": {"n": 5}, "periodic": {"interval": 5.0}, "aoe_damage": {"radius": 120.0, "damage": 35.0, "slow": 0.0}, "projectile_nova": {"count": 8, "damage": 22.0}, "heal": {"amount": 25.0}, "shield": {"amount": 25.0}, "summon_ally": {"count": 1}, } _HEX = set("0123456789abcdefABCDEF") MAX_SKILLS_PER_PLAYER = 6 def _num(v, lo, hi, integer=False): try: v = float(v) except Exception: v = lo v = lo if v < lo else hi if v > hi else v return int(round(v)) if integer else round(v, 2) def _color(v): if isinstance(v, str) and v.startswith("#") and len(v) == 7 and all(c in _HEX for c in v[1:]): return v return "#b48cff" def validate_skill(spec: dict) -> dict | None: """Coerce an arbitrary dict into a safe, bounded skill — or None if unusable.""" if not isinstance(spec, dict): return None trigger = spec.get("trigger") effect = spec.get("effect") if trigger not in TRIGGERS or effect not in EFFECTS: return None out = { "name": str(spec.get("name", "Arcane Skill"))[:32] or "Arcane Skill", "trigger": trigger, "effect": effect, "color": _color(spec.get("color")), } src = {**_DEFAULTS.get(trigger, {}), **_DEFAULTS.get(effect, {}), **spec} if trigger == "every_n_attacks": out["n"] = _num(src.get("n"), *_BOUNDS["n"], integer=True) elif trigger == "periodic": out["interval"] = _num(src.get("interval"), *_BOUNDS["interval"]) if effect == "aoe_damage": out["radius"] = _num(src.get("radius"), *_BOUNDS["radius"]) out["damage"] = _num(src.get("damage"), *_BOUNDS["damage"]) out["slow"] = _num(src.get("slow"), *_BOUNDS["slow"]) elif effect == "projectile_nova": out["count"] = _num(src.get("count"), *_BOUNDS["count"], integer=True) out["damage"] = _num(src.get("damage"), *_BOUNDS["damage"]) elif effect in ("heal", "shield"): out["amount"] = _num(src.get("amount"), *_BOUNDS["amount"]) elif effect == "summon_ally": out["count"] = _num(src.get("count"), *_BOUNDS["count"], integer=True) return out # Predefined skills granted by the AoE / Summoner level-up cards. CARD_SKILLS = { "aoe": {"name": "Arcane Nova", "trigger": "every_n_attacks", "n": 6, "effect": "aoe_damage", "radius": 130, "damage": 40, "color": "#7ee0ff"}, "summoner": {"name": "Spirit Caller", "trigger": "every_n_attacks", "n": 8, "effect": "summon_ally", "count": 1, "color": "#a6ff8c"}, # ---- boss-exclusive skills (only offered while that boss reigns) ---- "war_drums": {"name": "War Drums", "trigger": "periodic", "interval": 7, "effect": "summon_ally", "count": 2, "color": "#ff9d6e"}, "savage_roar": {"name": "Savage Roar", "trigger": "on_kill", "effect": "aoe_damage", "radius": 110, "damage": 30, "color": "#ff6e6e"}, "iron_nova": {"name": "Iron Detonation", "trigger": "every_n_attacks", "n": 5, "effect": "aoe_damage", "radius": 160, "damage": 55, "slow": 0.3, "color": "#c9d4e0"}, "bulwark": {"name": "Bulwark", "trigger": "periodic", "interval": 5, "effect": "heal", "amount": 35, "color": "#9db4c9"}, "impaling_charge": {"name": "Impaling Charge", "trigger": "every_n_attacks", "n": 4, "effect": "projectile_nova", "count": 6, "damage": 38, "color": "#ffd45e"}, "arrow_storm": {"name": "Arrow Storm", "trigger": "periodic", "interval": 4, "effect": "projectile_nova", "count": 12, "damage": 20, "color": "#a6ff8c"}, # ---- rewards for defeating the special bosses (their own attack powers) ---- "holy_judgment": {"name": "Holy Judgment", "trigger": "periodic", "interval": 4, "effect": "aoe_damage", "radius": 180, "damage": 70, "color": "#ffe9a0"}, "glitch_rift": {"name": "Glitch Rift", "trigger": "periodic", "interval": 3.5, "effect": "projectile_nova", "count": 14, "damage": 30, "color": "#7CFC00"}, }