GDScript 116 lines
class_name AbilitySpec
extends RefCounted
## The immutable definition of one ability — its targeting, cost, cooldown, and
## effect. Pure tuning data, parsed from the ability catalog; it owns no state and
## never mutates the world (the executor reads a spec to act). Keeping abilities as
## data, not code, lets the whole roster live in one catalog and be unit-tested by
## value.
##
## Theria heroes are shapeshifters: every hero carries a human kit and an animal
## kit, and a spec belongs to exactly one `form`. A spec is only castable while its
## form is the caster's active form, which is what makes the two stances play as
## two distinct kits sharing one hero.
## Active form a spec belongs to. A hero swaps between the two with a TRANSFORM
## ability; cooldowns and the per-form resource carry across the swap.
const FORM_HUMAN := 0
const FORM_ANIMAL := 1
## How an ability picks what it hits.
## SELF — the caster (heals, transforms, self-buffs); no aim.
## SKILLSHOT — aimed: a point defines a direction, the shot lands at the clamped
## range along it and strikes there. Dodgeable.
## GROUND — a point on the field (clamped to range): an area lands there.
## UNIT — a locked target entity within range; the effect cannot miss.
const TARGET_SELF := 0
const TARGET_SKILLSHOT := 1
const TARGET_GROUND := 2
const TARGET_UNIT := 3
## What an ability does where it lands.
## DAMAGE — subtracts `power` hp from each enemy struck.
## HEAL — restores `power` hp to the caster (clamped to max_hp).
## TRANSFORM — swaps the caster to its other form (and that form's resource).
const EFFECT_DAMAGE := 0
const EFFECT_HEAL := 1
const EFFECT_TRANSFORM := 2
## A lingering status an ability leaves on each enemy it strikes, on top of its
## immediate effect — the schema behind the Verdani's venom and web flavor.
## NONE — leaves nothing (the default; every Solane ability and every heal/transform).
## DOT — venom: deals `status_power` hp every `status_interval` ticks for
## `status_duration` ticks, so the bite keeps biting after it lands.
## SLOW — web: scales the target's move speed by (100 - `status_power`) percent for
## `status_duration` ticks, so a snared enemy crawls.
## STUN — a hard lock: for `status_duration` ticks the target cannot move, cast, or
## auto-attack. A stun has no magnitude — `status_power` and `status_interval`
## are unused; it either holds the unit or it does not.
## A status rides on the ability's target selection — it is laid by the DAMAGE/area
## path on every enemy struck, so a SELF heal or transform never carries one.
const STATUS_NONE := 0
const STATUS_DOT := 1
const STATUS_SLOW := 2
const STATUS_STUN := 3
## Catalog id (unique across the roster) and display name.
var id: int = 0
var name: String = ""
## The form this spec is cast from, and the slot it occupies in that form's kit
## (0..3, the Q/W/E/R bar). One id per (form, slot) within a kit.
var form: int = FORM_HUMAN
var slot: int = 0
var target_kind: int = TARGET_SELF
## Maximum cast distance (world units) for an aimed/targeted ability; ignored for
## SELF. `radius` is the area struck around the landing point: every enemy inside
## the circle is hit. A SKILLSHOT or GROUND ability gives it a small-to-large value
## (a tight bolt up to a wide slam); SELF and UNIT abilities ignore it (UNIT strikes
## its one locked target).
var range: float = 0.0
var radius: float = 0.0
## Resource spent to cast (see SimEntity's per-form resource) and the cooldown, in
## ticks, the ability goes onto once cast.
var cost: int = 0
var cooldown_ticks: int = 0
var effect: int = EFFECT_DAMAGE
## Magnitude of the effect: hp for DAMAGE/HEAL. Unused by TRANSFORM, which always
## swaps to the caster's other form.
var power: int = 0
## The lingering status (above) laid on each enemy this ability strikes, and its
## tuning. `status` selects the kind (NONE leaves the ability instant-only).
## `status_power` is hp-per-interval for DOT, percent-slow for SLOW; `status_duration`
## is how long it lasts in ticks; `status_interval` is how often a DOT bites (clamped
## to >= 1 when applied, so it never ticks more than once a tick; ignored by SLOW).
var status: int = STATUS_NONE
var status_power: int = 0
var status_duration: int = 0
var status_interval: int = 0
## Builds a spec from one catalog row. Every field defaults, so a sparse row only
## states what it changes — keeping the catalog terse and the parse total.
static func from_dict(d: Dictionary) -> AbilitySpec:
var spec := AbilitySpec.new()
spec.id = d.get("id", 0)
spec.name = d.get("name", "")
spec.form = d.get("form", FORM_HUMAN)
spec.slot = d.get("slot", 0)
spec.target_kind = d.get("target_kind", TARGET_SELF)
spec.range = d.get("range", 0.0)
spec.radius = d.get("radius", 0.0)
spec.cost = d.get("cost", 0)
spec.cooldown_ticks = d.get("cooldown_ticks", 0)
spec.effect = d.get("effect", EFFECT_DAMAGE)
spec.power = d.get("power", 0)
spec.status = d.get("status", STATUS_NONE)
spec.status_power = d.get("status_power", 0)
spec.status_duration = d.get("status_duration", 0)
spec.status_interval = d.get("status_interval", 0)
return spec