Spaces:
Running on Zero
Running on Zero
| """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"}, | |
| } | |