HuggingWizards / game /skills.py
Quazim0t0's picture
Upload 127 files
7e5faa7 verified
Raw
History Blame
5.64 kB
"""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"},
}