File size: 5,635 Bytes
7e5faa7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""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"},
}