@roost2d/chikn-rigs 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/actions.d.ts +68 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +476 -0
- package/dist/actions.js.map +1 -0
- package/dist/data/chikn-rig.json +12 -64
- package/dist/data/roostr-rig.json +23 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -6
- package/dist/index.js.map +1 -1
- package/dist/trait-profiles.d.ts +16 -0
- package/dist/trait-profiles.d.ts.map +1 -0
- package/dist/trait-profiles.js +344 -0
- package/dist/trait-profiles.js.map +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -36,4 +36,19 @@ Both species ship the same 40 animation names, prefixed with `chikn.` or `roostr
|
|
|
36
36
|
|
|
37
37
|
Loop and loop-mode metadata travels with every clip. One-shots preserve their authored end pose, so call `resetPose()` before switching independently previewed actions; persistent states such as `knockdown` can instead flow directly into `get_up`.
|
|
38
38
|
|
|
39
|
+
Trait-aware brawler actions are resolved from the same recipe:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const choices = listAvailableChiknActions(recipe, definition);
|
|
43
|
+
const action = resolveChiknAction(recipe, definition, choices[0].id, {
|
|
44
|
+
targetOffset: { x: 210, y: -24 }, // fighter-local; positive X is current forward
|
|
45
|
+
});
|
|
46
|
+
const playback = new RigActionController(rig).play(action.clip, { controlled: true });
|
|
47
|
+
playback.advance(frameMs);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Every attachment group receives one checked-in immutable profile with its supported actions, driver attachments, calibrated offsets and timing, functional preset, and explicit rigid/soft/dangling/cloth/feather/bulky/elastic secondary motion. Weapons replace Punch with their own draw, slash, thrust, golf, pickaxe, shot, swing, or casting motion. Feet select tailored paired or combined kicks, and `listChiknSpecials` returns only specials granted by equipped traits.
|
|
51
|
+
|
|
52
|
+
Golden Egg and Very Fresh Egg use a dedicated 1,000 ms action: the inner `pose` bone turns away while game-owned root facing stays untouched, the bird bends, the displayed egg releases at 400 ms, and a texture-identical detached clone follows an aimed arc until presentation contact at 800 ms. The equipped egg regrows and the setup pose returns at 1,000 ms. Floppy Disk and Omelette also launch exact equipped art; Omelette uses its pan motion and is never classified as an egg.
|
|
53
|
+
|
|
39
54
|
Resolve each `definition.attachments[].texture.assetId` through a separately hosted Chikn runtime manifest before creating the display factory. [Chikn integration tutorial](https://github.com/Roost2D/roost2d/blob/main/docs/chikn-assets.md).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AnimationClipV1, RigDefinitionV1 } from '@roost2d/contracts';
|
|
2
|
+
import type { EffectOrigin, EffectPoint, ProceduralEffectDescriptor } from '@roost2d/effects';
|
|
3
|
+
import { type ChiknKickPreset, type ChiknPunchPreset, type ChiknSecondaryMotion, type ChiknSpecialPreset } from './trait-profiles.js';
|
|
4
|
+
export type ChiknMotionFamily = 'accent' | 'footwear' | 'blade' | 'blunt' | 'ranged' | 'casting' | 'tail';
|
|
5
|
+
export type ChiknActionInput = 'punch' | 'kick' | 'special';
|
|
6
|
+
export interface ChiknTraitAnimationProfile {
|
|
7
|
+
species: 'chikn' | 'roostr';
|
|
8
|
+
traitGroupId: string;
|
|
9
|
+
category: string;
|
|
10
|
+
motionFamily: ChiknMotionFamily;
|
|
11
|
+
readonly attachmentTargets: readonly string[];
|
|
12
|
+
readonly supportedActions: readonly ChiknActionInput[];
|
|
13
|
+
secondaryMotion: ChiknSecondaryMotion;
|
|
14
|
+
amplitude: number;
|
|
15
|
+
speed: number;
|
|
16
|
+
gripOffset: EffectPoint;
|
|
17
|
+
tipOffset: EffectPoint;
|
|
18
|
+
originOffset: EffectPoint;
|
|
19
|
+
timing: {
|
|
20
|
+
anticipationMs: number;
|
|
21
|
+
releaseMs: number;
|
|
22
|
+
recoveryMs: number;
|
|
23
|
+
};
|
|
24
|
+
punchPreset?: ChiknPunchPreset;
|
|
25
|
+
kickPreset?: ChiknKickPreset;
|
|
26
|
+
special?: {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
family: 'beam' | 'projectile' | 'cast' | 'burst' | 'tail';
|
|
30
|
+
preset: ChiknSpecialPreset;
|
|
31
|
+
origin: EffectOrigin;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface ChiknActionChoice {
|
|
35
|
+
id: string;
|
|
36
|
+
input: ChiknActionInput;
|
|
37
|
+
label: string;
|
|
38
|
+
sourceTraitGroupId?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ChiknActionContext {
|
|
41
|
+
targetOffset?: EffectPoint;
|
|
42
|
+
}
|
|
43
|
+
export interface ResolvedChiknEffect extends ProceduralEffectDescriptor {
|
|
44
|
+
cueId: string;
|
|
45
|
+
/** @deprecated Prefer origin. Kept for v0.5 consumers. */
|
|
46
|
+
socketId: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ResolvedChiknAction extends ChiknActionChoice {
|
|
49
|
+
motionFamily: string;
|
|
50
|
+
clip: AnimationClipV1;
|
|
51
|
+
effects: ResolvedChiknEffect[];
|
|
52
|
+
targetOffset: EffectPoint;
|
|
53
|
+
}
|
|
54
|
+
export interface ChiknActionRecipe {
|
|
55
|
+
species: 'chikn' | 'roostr';
|
|
56
|
+
traitGroupIds: readonly string[];
|
|
57
|
+
/** Informational only. Facing remains owned by the consuming game. */
|
|
58
|
+
mirrored?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** Returns one checked-in, immutable motion profile for every attachment group in a rig. */
|
|
61
|
+
export declare function createChiknTraitAnimationProfiles(definition: RigDefinitionV1): readonly ChiknTraitAnimationProfile[];
|
|
62
|
+
export declare function validateChiknTraitAnimationProfiles(definition: RigDefinitionV1, profiles: readonly ChiknTraitAnimationProfile[]): string[];
|
|
63
|
+
export declare function listChiknSpecials(recipe: ChiknActionRecipe, definition: RigDefinitionV1): ChiknActionChoice[];
|
|
64
|
+
export declare function listAvailableChiknActions(recipe: ChiknActionRecipe, definition: RigDefinitionV1): ChiknActionChoice[];
|
|
65
|
+
export declare function resolveChiknAction(recipe: ChiknActionRecipe, definition: RigDefinitionV1, actionId: string, context?: ChiknActionContext): ResolvedChiknAction;
|
|
66
|
+
/** Species-specific brawler and attack foundations generated from the rig setup pose. */
|
|
67
|
+
export declare function createChiknActionClips(definition: RigDefinitionV1): readonly AnimationClipV1[];
|
|
68
|
+
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { CURATED_TRAIT_PROFILES, curatedTraitProfile, } from './trait-profiles.js';
|
|
2
|
+
const DEFAULT_TARGET = Object.freeze({ x: 180, y: 0 });
|
|
3
|
+
const profileCache = new WeakMap();
|
|
4
|
+
const clipCache = new WeakMap();
|
|
5
|
+
const MOTION_ADJUSTMENTS = {
|
|
6
|
+
rigid: { amplitude: .72, speed: 1.08 },
|
|
7
|
+
soft: { amplitude: 1, speed: .96 },
|
|
8
|
+
dangling: { amplitude: 1.16, speed: .92 },
|
|
9
|
+
cloth: { amplitude: 1.12, speed: .9 },
|
|
10
|
+
feather: { amplitude: 1.06, speed: 1 },
|
|
11
|
+
bulky: { amplitude: .82, speed: .88 },
|
|
12
|
+
elastic: { amplitude: 1.22, speed: .94 },
|
|
13
|
+
};
|
|
14
|
+
const PRESET_CALIBRATION = {
|
|
15
|
+
katana: { grip: { x: -8, y: 4 }, tip: { x: 38, y: -2 }, origin: { x: 30, y: -4 }, timing: { anticipationMs: 90, releaseMs: 185, recoveryMs: 290 } },
|
|
16
|
+
sword: { grip: { x: -7, y: 5 }, tip: { x: 34, y: -4 }, origin: { x: 28, y: -3 }, timing: { anticipationMs: 135, releaseMs: 255, recoveryMs: 390 } },
|
|
17
|
+
snips: { grip: { x: -5, y: 2 }, tip: { x: 25, y: 0 }, origin: { x: 22, y: 0 }, timing: { anticipationMs: 120, releaseMs: 240, recoveryMs: 360 } },
|
|
18
|
+
thrust: { grip: { x: -8, y: 5 }, tip: { x: 36, y: 0 }, origin: { x: 32, y: 0 }, timing: { anticipationMs: 150, releaseMs: 275, recoveryMs: 390 } },
|
|
19
|
+
golf: { grip: { x: -8, y: 6 }, tip: { x: 42, y: 8 }, origin: { x: 38, y: 8 }, timing: { anticipationMs: 190, releaseMs: 350, recoveryMs: 470 } },
|
|
20
|
+
pickaxe: { grip: { x: -7, y: 5 }, tip: { x: 34, y: 10 }, origin: { x: 30, y: 8 }, timing: { anticipationMs: 180, releaseMs: 330, recoveryMs: 470 } },
|
|
21
|
+
gun: { grip: { x: -8, y: 7 }, tip: { x: 34, y: -3 }, origin: { x: 31, y: -3 }, timing: { anticipationMs: 120, releaseMs: 220, recoveryMs: 330 } },
|
|
22
|
+
egg: { grip: { x: 0, y: 0 }, tip: { x: 0, y: 0 }, origin: { x: 0, y: 3 }, timing: { anticipationMs: 160, releaseMs: 400, recoveryMs: 800 } },
|
|
23
|
+
laser: { grip: { x: 0, y: 0 }, tip: { x: 14, y: -3 }, origin: { x: 13, y: -3 }, timing: { anticipationMs: 170, releaseMs: 280, recoveryMs: 440 } },
|
|
24
|
+
disk: { grip: { x: -5, y: 2 }, tip: { x: 18, y: 0 }, origin: { x: 10, y: -2 }, timing: { anticipationMs: 180, releaseMs: 320, recoveryMs: 590 } },
|
|
25
|
+
pan: { grip: { x: -7, y: 4 }, tip: { x: 22, y: 2 }, origin: { x: 13, y: -1 }, timing: { anticipationMs: 190, releaseMs: 330, recoveryMs: 600 } },
|
|
26
|
+
sonic: { grip: { x: -4, y: 4 }, tip: { x: 24, y: 0 }, origin: { x: 19, y: -2 }, timing: { anticipationMs: 170, releaseMs: 300, recoveryMs: 500 } },
|
|
27
|
+
flame: { grip: { x: -4, y: 4 }, tip: { x: 22, y: 0 }, origin: { x: 18, y: 0 }, timing: { anticipationMs: 160, releaseMs: 290, recoveryMs: 490 } },
|
|
28
|
+
exhaust: { grip: { x: 0, y: 0 }, tip: { x: -18, y: 2 }, origin: { x: -16, y: 2 }, timing: { anticipationMs: 120, releaseMs: 260, recoveryMs: 430 } },
|
|
29
|
+
};
|
|
30
|
+
/** Returns one checked-in, immutable motion profile for every attachment group in a rig. */
|
|
31
|
+
export function createChiknTraitAnimationProfiles(definition) {
|
|
32
|
+
const cached = profileCache.get(definition);
|
|
33
|
+
if (cached)
|
|
34
|
+
return cached;
|
|
35
|
+
const species = speciesOf(definition);
|
|
36
|
+
const profiles = Object.freeze(Object.values(definition.attachmentGroups ?? {}).map((group) => {
|
|
37
|
+
const curated = curatedTraitProfile(species, group.id);
|
|
38
|
+
if (!curated)
|
|
39
|
+
throw new Error(`Trait group has no curated animation profile: ${species}:${group.id}`);
|
|
40
|
+
const category = String(group.metadata?.category ?? group.slotId);
|
|
41
|
+
const adjustment = MOTION_ADJUSTMENTS[curated.secondaryMotion];
|
|
42
|
+
const preset = curated.specialPreset ?? curated.punchPreset ?? curated.kickPreset;
|
|
43
|
+
const calibration = preset ? PRESET_CALIBRATION[preset] : undefined;
|
|
44
|
+
const special = curated.specialPreset
|
|
45
|
+
? specialFor(curated.specialPreset, group.id, group.attachmentIds[0], category, calibration?.origin ?? { x: 0, y: 0 })
|
|
46
|
+
: undefined;
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
species,
|
|
49
|
+
traitGroupId: group.id,
|
|
50
|
+
category,
|
|
51
|
+
motionFamily: curated.motionFamily,
|
|
52
|
+
attachmentTargets: Object.freeze([...group.attachmentIds]),
|
|
53
|
+
supportedActions: Object.freeze(['punch', 'kick', ...(special ? ['special'] : [])]),
|
|
54
|
+
secondaryMotion: curated.secondaryMotion,
|
|
55
|
+
amplitude: adjustment.amplitude,
|
|
56
|
+
speed: adjustment.speed,
|
|
57
|
+
gripOffset: calibration?.grip ?? { x: 0, y: 0 },
|
|
58
|
+
tipOffset: calibration?.tip ?? { x: 0, y: 0 },
|
|
59
|
+
originOffset: calibration?.origin ?? { x: 0, y: 0 },
|
|
60
|
+
timing: calibration?.timing ?? { anticipationMs: 140, releaseMs: 260, recoveryMs: 420 },
|
|
61
|
+
...(curated.punchPreset ? { punchPreset: curated.punchPreset } : {}),
|
|
62
|
+
...(curated.kickPreset ? { kickPreset: curated.kickPreset } : {}),
|
|
63
|
+
...(special ? { special } : {}),
|
|
64
|
+
});
|
|
65
|
+
}));
|
|
66
|
+
profileCache.set(definition, profiles);
|
|
67
|
+
return profiles;
|
|
68
|
+
}
|
|
69
|
+
export function validateChiknTraitAnimationProfiles(definition, profiles) {
|
|
70
|
+
const errors = [];
|
|
71
|
+
const species = definition.id === 'chikn' || definition.id === 'roostr' ? definition.id : undefined;
|
|
72
|
+
const groups = definition.attachmentGroups ?? {};
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const attachmentIds = new Set(definition.attachments.map(({ id }) => id));
|
|
75
|
+
const socketIds = new Set((definition.sockets ?? []).map(({ id }) => id));
|
|
76
|
+
for (const profile of profiles) {
|
|
77
|
+
if (seen.has(profile.traitGroupId))
|
|
78
|
+
errors.push(`duplicate trait animation profile ${profile.traitGroupId}`);
|
|
79
|
+
seen.add(profile.traitGroupId);
|
|
80
|
+
const group = groups[profile.traitGroupId];
|
|
81
|
+
if (!group) {
|
|
82
|
+
errors.push(`profile references unknown trait group ${profile.traitGroupId}`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (profile.species !== definition.id)
|
|
86
|
+
errors.push(`${profile.traitGroupId}: species does not match ${definition.id}`);
|
|
87
|
+
if (profile.attachmentTargets.join('|') !== group.attachmentIds.join('|'))
|
|
88
|
+
errors.push(`${profile.traitGroupId}: attachment targets do not match the rig group`);
|
|
89
|
+
if (profile.attachmentTargets.some((id) => !attachmentIds.has(id)))
|
|
90
|
+
errors.push(`${profile.traitGroupId}: unknown driver attachment`);
|
|
91
|
+
if (!profile.supportedActions.includes('punch') || !profile.supportedActions.includes('kick') || profile.supportedActions.includes('special') !== Boolean(profile.special))
|
|
92
|
+
errors.push(`${profile.traitGroupId}: supported actions do not match its capabilities`);
|
|
93
|
+
if (!(profile.amplitude > 0) || !(profile.speed > 0))
|
|
94
|
+
errors.push(`${profile.traitGroupId}: adjustments must be positive`);
|
|
95
|
+
const numbers = [profile.gripOffset.x, profile.gripOffset.y, profile.tipOffset.x, profile.tipOffset.y, profile.originOffset.x, profile.originOffset.y, profile.timing.anticipationMs, profile.timing.releaseMs, profile.timing.recoveryMs];
|
|
96
|
+
if (numbers.some((value) => !Number.isFinite(value)))
|
|
97
|
+
errors.push(`${profile.traitGroupId}: calibration values must be finite`);
|
|
98
|
+
if (profile.special?.origin.target === 'attachment' && !attachmentIds.has(profile.special.origin.targetId))
|
|
99
|
+
errors.push(`${profile.traitGroupId}: unknown special attachment ${profile.special.origin.targetId}`);
|
|
100
|
+
if (profile.special?.origin.target === 'socket' && !socketIds.has(profile.special.origin.targetId))
|
|
101
|
+
errors.push(`${profile.traitGroupId}: unknown special socket ${profile.special.origin.targetId}`);
|
|
102
|
+
}
|
|
103
|
+
for (const groupId of Object.keys(groups))
|
|
104
|
+
if (!seen.has(groupId))
|
|
105
|
+
errors.push(`trait group has no animation profile ${groupId}`);
|
|
106
|
+
if (species)
|
|
107
|
+
for (const curated of CURATED_TRAIT_PROFILES.values()) {
|
|
108
|
+
if (curated.species === species && !Object.hasOwn(groups, curated.traitGroupId))
|
|
109
|
+
errors.push(`curated profile references unknown ${species} trait group ${curated.traitGroupId}`);
|
|
110
|
+
}
|
|
111
|
+
return errors;
|
|
112
|
+
}
|
|
113
|
+
export function listChiknSpecials(recipe, definition) {
|
|
114
|
+
assertRecipe(recipe, definition);
|
|
115
|
+
const profiles = profileMap(definition);
|
|
116
|
+
return recipe.traitGroupIds.flatMap((traitGroupId) => {
|
|
117
|
+
const special = profiles.get(traitGroupId)?.special;
|
|
118
|
+
return special ? [{ id: special.id, input: 'special', label: special.label, sourceTraitGroupId: traitGroupId }] : [];
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
export function listAvailableChiknActions(recipe, definition) {
|
|
122
|
+
return [
|
|
123
|
+
{ id: 'punch', input: 'punch', label: 'Punch' },
|
|
124
|
+
{ id: 'kick', input: 'kick', label: 'Flying spin kick' },
|
|
125
|
+
...listChiknSpecials(recipe, definition),
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
export function resolveChiknAction(recipe, definition, actionId, context = {}) {
|
|
129
|
+
assertRecipe(recipe, definition);
|
|
130
|
+
const targetOffset = normaliseTarget(context.targetOffset);
|
|
131
|
+
const clips = new Map(createChiknActionClips(definition).map((clip) => [clip.id, clip]));
|
|
132
|
+
const profiles = profileMap(definition);
|
|
133
|
+
const selected = recipe.traitGroupIds.map((id) => profiles.get(id)).filter(Boolean);
|
|
134
|
+
if (actionId === 'punch') {
|
|
135
|
+
const weapon = selected.find(({ punchPreset }) => punchPreset !== undefined);
|
|
136
|
+
const family = weapon?.motionFamily ?? 'unarmed';
|
|
137
|
+
const base = requiredClip(clips, `${definition.id}.action.punch.${family}`);
|
|
138
|
+
const clip = tailorActionClip(definition, base, selected, weapon);
|
|
139
|
+
return {
|
|
140
|
+
id: actionId,
|
|
141
|
+
input: 'punch',
|
|
142
|
+
label: weapon ? `${displayName(weapon.traitGroupId)} attack` : 'Wing punch',
|
|
143
|
+
...(weapon ? { sourceTraitGroupId: weapon.traitGroupId } : {}),
|
|
144
|
+
motionFamily: family,
|
|
145
|
+
clip,
|
|
146
|
+
effects: weapon ? punchEffects(weapon, targetOffset) : [],
|
|
147
|
+
targetOffset,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (actionId === 'kick') {
|
|
151
|
+
const feet = selected.find(({ kickPreset }) => kickPreset !== undefined);
|
|
152
|
+
const combined = feet ? (definition.attachmentGroups?.[feet.traitGroupId]?.attachmentIds.length ?? 0) === 1 : false;
|
|
153
|
+
const family = combined ? 'combined' : 'paired';
|
|
154
|
+
const clip = tailorActionClip(definition, requiredClip(clips, `${definition.id}.action.kick.${family}`), selected, feet);
|
|
155
|
+
return {
|
|
156
|
+
id: actionId,
|
|
157
|
+
input: 'kick',
|
|
158
|
+
label: feet ? `${displayName(feet.traitGroupId)} kick` : 'Flying spin kick',
|
|
159
|
+
...(feet ? { sourceTraitGroupId: feet.traitGroupId } : {}),
|
|
160
|
+
motionFamily: family,
|
|
161
|
+
clip,
|
|
162
|
+
effects: [proceduralEffect('trail', 'contact', socketOrigin('foot-front'), `kick:${feet?.kickPreset ?? family}`, targetOffset, 'follow')],
|
|
163
|
+
targetOffset,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const specialChoice = listChiknSpecials(recipe, definition).find(({ id }) => id === actionId);
|
|
167
|
+
if (!specialChoice?.sourceTraitGroupId)
|
|
168
|
+
throw new Error(`Special is not available for this recipe: ${actionId}`);
|
|
169
|
+
const profile = profiles.get(specialChoice.sourceTraitGroupId);
|
|
170
|
+
const special = profile.special;
|
|
171
|
+
const clipId = special.preset === 'egg' ? `${definition.id}.action.special.egg` : `${definition.id}.action.special.${special.family}`;
|
|
172
|
+
const clip = tailorActionClip(definition, requiredClip(clips, clipId), selected, profile, special.preset === 'egg');
|
|
173
|
+
return {
|
|
174
|
+
...specialChoice,
|
|
175
|
+
motionFamily: special.family,
|
|
176
|
+
clip,
|
|
177
|
+
effects: specialEffects(profile, targetOffset),
|
|
178
|
+
targetOffset,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** Species-specific brawler and attack foundations generated from the rig setup pose. */
|
|
182
|
+
export function createChiknActionClips(definition) {
|
|
183
|
+
const cached = clipCache.get(definition);
|
|
184
|
+
if (cached)
|
|
185
|
+
return cached;
|
|
186
|
+
const species = speciesOf(definition);
|
|
187
|
+
const make = (id, durationMs, tracks, cues = [], loop = false) => ({ schema: 'roost2d.animation/v1', id: `${species}.${id}`, durationMs, loop, ...(loop ? { loopMode: 'repeat' } : {}), defaultLayer: id.includes('action.') ? 'action' : 'base', cues, tracks });
|
|
188
|
+
const track = (target, id, frames) => {
|
|
189
|
+
const neutral = target === 'bone' ? neutralBoneTransform(definition, id) : neutralTransform(definition, id);
|
|
190
|
+
return { target, targetId: id, keyframes: frames.map(([timeMs, delta, durationMs]) => ({ timeMs: durationMs === undefined ? timeMs : Math.max(0, timeMs - durationMs), ...(durationMs === undefined ? {} : { durationMs }), x: neutral.x + (delta.x ?? 0), y: neutral.y + (delta.y ?? 0), rotation: neutral.rotation + (delta.rotation ?? 0), scaleX: neutral.scaleX * (delta.scaleX ?? 1), scaleY: neutral.scaleY * (delta.scaleY ?? 1), ease: 'power2.out' })) };
|
|
191
|
+
};
|
|
192
|
+
const slot = (id, frames) => track('slot', id, frames);
|
|
193
|
+
const cues = (contact, duration, release = contact) => [
|
|
194
|
+
{ id: 'anticipation', timeMs: 0, phase: 'anticipation' },
|
|
195
|
+
...(release === contact ? [{ id: 'contact', timeMs: contact, phase: 'contact' }] : [{ id: 'release', timeMs: release, phase: 'release' }, { id: 'contact', timeMs: contact, phase: 'contact' }]),
|
|
196
|
+
{ id: 'recovery', timeMs: Math.min(duration - 1, Math.max(contact, release) + Math.round((duration - Math.max(contact, release)) * .35)), phase: 'recovery' },
|
|
197
|
+
{ id: 'complete', timeMs: duration, phase: 'complete' },
|
|
198
|
+
];
|
|
199
|
+
const clips = [
|
|
200
|
+
make('combat_idle', 800, [slot('Torso', [[0, {}], [400, { y: -1, rotation: -.025 }, 400], [800, {}, 400]]), slot('Wing A', [[0, { rotation: .18 }], [400, { rotation: .25 }, 400], [800, { rotation: .18 }, 400]])], [], true),
|
|
201
|
+
make('dash', 260, [slot('Torso', [[0, { x: -3, rotation: -.12 }], [90, { x: 12, rotation: -.18 }, 90], [260, {}, 170]]), slot('Tail', [[0, { rotation: .25 }], [260, {}, 260]])], cues(90, 260)),
|
|
202
|
+
make('guard', 420, [slot('Wing A', [[0, {}], [120, { x: 7, y: -4, rotation: -1.05 }, 120], [420, {}, 180]]), slot('Head', [[0, {}], [120, { y: 3, rotation: .08 }, 120], [420, {}, 180]])], cues(120, 420)),
|
|
203
|
+
];
|
|
204
|
+
const punch = (family, rotation, duration) => make(`action.punch.${family}`, duration, [
|
|
205
|
+
slot('Torso', [[0, {}], [duration * .3, { x: -3, rotation: -.18 }, duration * .3], [duration * .52, { x: 5, rotation: .12 }, duration * .22], [duration, {}, duration * .48]]),
|
|
206
|
+
slot('Wing A', [[0, {}], [duration * .3, { rotation: -.55 }, duration * .3], [duration * .52, { x: 10, rotation }, duration * .22], [duration, {}, duration * .48]]),
|
|
207
|
+
slot('Head', [[0, {}], [duration * .52, { rotation: .08 }, duration * .52], [duration, {}, duration * .48]]),
|
|
208
|
+
], cues(Math.round(duration * .52), duration));
|
|
209
|
+
clips.push(punch('unarmed', -1.35, 420), punch('blade', -2.15, 500), punch('blunt', -1.65, 560), punch('ranged', -1.05, 480), punch('casting', -1.25, 560));
|
|
210
|
+
const kick = (family) => make(`action.kick.${family}`, 760, [
|
|
211
|
+
slot('Torso', [[0, { y: 3, rotation: -.12 }], [170, { y: -10, rotation: -.45 }, 170], [390, { y: -15, rotation: 2.7 }, 220], [560, { y: -7, rotation: 5.7 }, 170], [760, {}, 200]]),
|
|
212
|
+
slot('LegUpper A', [[0, { rotation: -.2 }], [170, { rotation: -.75 }, 170], [390, { rotation: family === 'paired' ? 1.45 : .85 }, 220], [560, { rotation: .3 }, 170], [760, {}, 200]]),
|
|
213
|
+
slot('LegUpper B', [[0, { rotation: .2 }], [170, { rotation: .65 }, 170], [390, { rotation: family === 'paired' ? -.85 : .35 }, 220], [760, {}, 370]]),
|
|
214
|
+
slot('Wing A', [[0, {}], [390, { rotation: -1.2 }, 390], [760, {}, 370]]),
|
|
215
|
+
], cues(390, 760));
|
|
216
|
+
clips.push(kick('paired'), kick('combined'));
|
|
217
|
+
const special = (family, release, duration) => make(`action.special.${family}`, duration, [
|
|
218
|
+
slot('Torso', [[0, {}], [release - 80, { x: -3, rotation: -.12 }, release - 80], [release, { x: 3, rotation: .08 }, 80], [duration, {}, duration - release]]),
|
|
219
|
+
slot(family === 'beam' ? 'Head' : family === 'projectile' || family === 'tail' ? 'Tail' : 'Wing A', [[0, {}], [release, { rotation: family === 'beam' ? -.16 : family === 'tail' ? 1.65 : -1.25, scaleX: 1.08, scaleY: 1.08 }, release], [duration, {}, duration - release]]),
|
|
220
|
+
], cues(release + 20, duration, release));
|
|
221
|
+
clips.push(special('beam', 280, 640), special('projectile', 320, 740), special('cast', 340, 760), special('burst', 300, 700), special('tail', 260, 640));
|
|
222
|
+
clips.push(make('action.special.egg', 1000, [
|
|
223
|
+
track('bone', 'pose', [[0, {}], [160, { scaleX: -1 }, 160], [800, { scaleX: -1 }, 640], [1000, {}, 200]]),
|
|
224
|
+
slot('Torso', [[0, {}], [160, { y: 1, rotation: .12 }, 160], [360, { y: 8, rotation: .34, scaleY: .9 }, 200], [800, { y: 5, rotation: .22, scaleY: .94 }, 440], [1000, {}, 200]]),
|
|
225
|
+
slot('Head', [[0, {}], [360, { x: -3, y: 7, rotation: .28 }, 360], [800, { x: -1, y: 3, rotation: .12 }, 440], [1000, {}, 200]]),
|
|
226
|
+
slot('Tail', [[0, {}], [360, { x: -3, y: -3, rotation: -.42 }, 360], [400, { x: -1, y: -5, rotation: -.6 }, 40], [800, { rotation: -.2 }, 400], [1000, {}, 200]]),
|
|
227
|
+
slot('LegUpper A', [[0, {}], [360, { rotation: -.38, scaleY: .92 }, 360], [800, { rotation: -.18 }, 440], [1000, {}, 200]]),
|
|
228
|
+
slot('LegUpper B', [[0, {}], [360, { rotation: .3, scaleY: .92 }, 360], [800, { rotation: .12 }, 440], [1000, {}, 200]]),
|
|
229
|
+
], [
|
|
230
|
+
{ id: 'anticipation', timeMs: 0, phase: 'anticipation' },
|
|
231
|
+
{ id: 'turn-away', timeMs: 160, phase: 'anticipation' },
|
|
232
|
+
{ id: 'release', timeMs: 400, phase: 'release' },
|
|
233
|
+
{ id: 'contact', timeMs: 800, phase: 'contact' },
|
|
234
|
+
{ id: 'recovery', timeMs: 800, phase: 'recovery' },
|
|
235
|
+
{ id: 'turn-back', timeMs: 920, phase: 'recovery' },
|
|
236
|
+
{ id: 'complete', timeMs: 1000, phase: 'complete' },
|
|
237
|
+
]));
|
|
238
|
+
const frozen = Object.freeze(clips.map(freezeClip));
|
|
239
|
+
clipCache.set(definition, frozen);
|
|
240
|
+
return frozen;
|
|
241
|
+
}
|
|
242
|
+
function tailorActionClip(definition, base, profiles, driver, preserveTiming = false) {
|
|
243
|
+
const timeScale = preserveTiming ? 1 : 1 / (driver?.speed ?? 1);
|
|
244
|
+
const durationMs = preserveTiming ? base.durationMs : Math.max(1, Math.round(base.durationMs * timeScale));
|
|
245
|
+
const scaleTime = (timeMs) => Math.min(durationMs, Math.round(timeMs * timeScale));
|
|
246
|
+
const accentAt = base.cues?.find(({ phase }) => phase === 'contact' || phase === 'release')?.timeMs ?? base.durationMs * .5;
|
|
247
|
+
const recoverAt = base.cues?.find(({ phase }) => phase === 'recovery')?.timeMs ?? base.durationMs * .72;
|
|
248
|
+
const traitTracks = [];
|
|
249
|
+
for (const profile of profiles) {
|
|
250
|
+
const isDriver = profile === driver;
|
|
251
|
+
for (const [index, targetId] of profile.attachmentTargets.entries()) {
|
|
252
|
+
const neutral = neutralAttachmentTransform(definition, targetId);
|
|
253
|
+
const motion = attachmentMotion(profile, isDriver, index);
|
|
254
|
+
let keyframes = [
|
|
255
|
+
{ timeMs: 0, durationMs: 0, ...neutral },
|
|
256
|
+
{ timeMs: 0, durationMs: Math.max(1, scaleTime(accentAt)), x: neutral.x + motion.x, y: neutral.y + motion.y, rotation: neutral.rotation + motion.rotation, scaleX: neutral.scaleX * motion.scaleX, scaleY: neutral.scaleY * motion.scaleY, ease: 'power2.out' },
|
|
257
|
+
{ timeMs: scaleTime(accentAt), durationMs: Math.max(1, scaleTime(recoverAt - accentAt)), x: neutral.x, y: neutral.y + motion.y * .25, rotation: neutral.rotation - motion.rotation * .2, scaleX: neutral.scaleX, scaleY: neutral.scaleY, ease: 'power2.out' },
|
|
258
|
+
{ timeMs: scaleTime(recoverAt), durationMs: Math.max(1, durationMs - scaleTime(recoverAt)), ...neutral, ease: 'power2.out' },
|
|
259
|
+
];
|
|
260
|
+
if (isDriver && profile.special?.preset === 'egg') {
|
|
261
|
+
keyframes = [
|
|
262
|
+
{ timeMs: 0, durationMs: 0, ...neutral, visible: true },
|
|
263
|
+
{ timeMs: 0, durationMs: 360, ...neutral, y: neutral.y + 2, rotation: neutral.rotation - .18, visible: true, ease: 'power2.out' },
|
|
264
|
+
{ timeMs: 360, durationMs: 40, ...neutral, y: neutral.y - 2, rotation: neutral.rotation - .28, scaleX: neutral.scaleX * .82, scaleY: neutral.scaleY * .82, visible: true, ease: 'power2.out' },
|
|
265
|
+
{ timeMs: 400, durationMs: 0, ...neutral, y: neutral.y - 2, rotation: neutral.rotation - .28, scaleX: neutral.scaleX * .82, scaleY: neutral.scaleY * .82, visible: false, ease: 'none' },
|
|
266
|
+
{ timeMs: 800, durationMs: 0, ...neutral, scaleX: neutral.scaleX * .2, scaleY: neutral.scaleY * .2, visible: true, ease: 'none' },
|
|
267
|
+
{ timeMs: 800, durationMs: 80, ...neutral, scaleX: neutral.scaleX * 1.14, scaleY: neutral.scaleY * 1.14, visible: true, ease: 'back.out(2)' },
|
|
268
|
+
{ timeMs: 880, durationMs: 120, ...neutral, visible: true, ease: 'power2.out' },
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
else if (isDriver && (profile.special?.preset === 'disk' || profile.special?.preset === 'pan')) {
|
|
272
|
+
const release = scaleTime(accentAt);
|
|
273
|
+
const recovery = scaleTime(recoverAt);
|
|
274
|
+
keyframes = [
|
|
275
|
+
{ timeMs: 0, durationMs: 0, ...neutral, visible: true },
|
|
276
|
+
{ timeMs: 0, durationMs: Math.max(1, release), x: neutral.x + motion.x, y: neutral.y + motion.y, rotation: neutral.rotation + motion.rotation, scaleX: neutral.scaleX * motion.scaleX, scaleY: neutral.scaleY * motion.scaleY, visible: true, ease: 'power2.out' },
|
|
277
|
+
{ timeMs: release, durationMs: 0, x: neutral.x + motion.x, y: neutral.y + motion.y, rotation: neutral.rotation + motion.rotation, scaleX: neutral.scaleX * motion.scaleX, scaleY: neutral.scaleY * motion.scaleY, visible: false, ease: 'none' },
|
|
278
|
+
{ timeMs: recovery, durationMs: 0, ...neutral, scaleX: neutral.scaleX * .2, scaleY: neutral.scaleY * .2, visible: true, ease: 'none' },
|
|
279
|
+
{ timeMs: recovery, durationMs: Math.max(1, durationMs - recovery), ...neutral, visible: true, ease: 'back.out(2)' },
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
traitTracks.push({ target: 'attachment', targetId, keyframes });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
...base,
|
|
287
|
+
durationMs,
|
|
288
|
+
cues: base.cues?.map((cue) => ({ ...cue, timeMs: scaleTime(cue.timeMs) })),
|
|
289
|
+
tracks: [
|
|
290
|
+
...base.tracks.map((entry) => ({ ...entry, keyframes: entry.keyframes.map((keyframe) => ({ ...keyframe, timeMs: scaleTime(keyframe.timeMs), ...(keyframe.durationMs === undefined ? {} : { durationMs: Math.max(0, scaleTime(keyframe.durationMs)) }) })) })),
|
|
291
|
+
...traitTracks,
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function attachmentMotion(profile, driver, index) {
|
|
296
|
+
const paired = index % 2 === 0 ? 1 : -1;
|
|
297
|
+
if (driver && profile.punchPreset) {
|
|
298
|
+
const values = {
|
|
299
|
+
katana: [14, -5, -2.65, 1.04, .98], sword: [12, -7, -2.2, 1.04, .98], snips: [11, paired * 3, paired * .82, 1.08, 1.02], thrust: [20, -2, -.25, 1.02, 1], golf: [8, -9, -2.8, 1.04, .98], pickaxe: [7, -11, -2.45, 1.04, .98], 'heavy-swing': [8, -7, -1.9, 1.04, 1.02], 'light-swing': [12, -5, -2.25, 1.05, .98], gun: [5, 1, -.18, 1.02, 1], wand: [9, -6, -1.35, 1.05, 1],
|
|
300
|
+
};
|
|
301
|
+
const [x, y, rotation, scaleX, scaleY] = values[profile.punchPreset];
|
|
302
|
+
return { x, y, rotation, scaleX, scaleY };
|
|
303
|
+
}
|
|
304
|
+
if (driver && profile.kickPreset) {
|
|
305
|
+
const values = {
|
|
306
|
+
natural: [12 * paired, -5, 1.15 * paired, 1.08, 1.04], fast: [18 * paired, -8, 1.7 * paired, 1.08, 1.02], heavy: [15 * paired, -3, 1.15 * paired, 1.12, 1.08], roller: [17 * paired, -7, 2.5 * paired, 1.08, 1.04], stiletto: [21 * paired, -2, .85 * paired, 1.1, .98], spur: [20 * paired, -5, 1.35 * paired, 1.1, 1], pendulum: [13 * paired, 5, 2.2 * paired, 1.12, 1.08], ironclaw: [19 * paired, -4, 1.65 * paired, 1.1, 1],
|
|
307
|
+
};
|
|
308
|
+
const [x, y, rotation, scaleX, scaleY] = values[profile.kickPreset];
|
|
309
|
+
return { x, y, rotation, scaleX, scaleY };
|
|
310
|
+
}
|
|
311
|
+
if (driver && profile.special?.preset?.startsWith('tail-'))
|
|
312
|
+
return { x: -11, y: -4, rotation: 1.65, scaleX: 1.1, scaleY: 1.04 };
|
|
313
|
+
const categoryDirection = profile.category === 'Tail' ? -1 : profile.category === 'Feet' ? paired : 1;
|
|
314
|
+
const rotations = { rigid: .04, soft: .1, dangling: .16, cloth: .2, feather: .14, bulky: .06, elastic: .22 };
|
|
315
|
+
return { x: 0, y: -1.5 * profile.amplitude, rotation: rotations[profile.secondaryMotion] * categoryDirection, scaleX: 1 + .025 * profile.amplitude, scaleY: 1 + .025 * profile.amplitude };
|
|
316
|
+
}
|
|
317
|
+
function punchEffects(profile, target) {
|
|
318
|
+
const origin = attachmentOrigin(profile);
|
|
319
|
+
if (profile.punchPreset === 'gun')
|
|
320
|
+
return [
|
|
321
|
+
proceduralEffect('projectile', 'contact', origin, `${profile.traitGroupId}:round`, target, 'detached', { kind: 'linear', targetOffset: target, rotationTurns: 0 }, { color: 0xffd36b, radius: 4, durationMs: 360 }),
|
|
322
|
+
proceduralEffect('burst', 'contact', origin, `${profile.traitGroupId}:muzzle`, target, 'follow', undefined, { color: 0xffa726, secondaryColor: 0xffffdc, radius: 12, durationMs: 150 }),
|
|
323
|
+
];
|
|
324
|
+
if (profile.motionFamily === 'blade')
|
|
325
|
+
return [proceduralEffect('slash', 'contact', origin, `${profile.traitGroupId}:slash`, target, 'follow', undefined, { color: 0xffe79a, length: 100, width: profile.punchPreset === 'katana' ? 5 : 8 })];
|
|
326
|
+
if (profile.motionFamily === 'casting')
|
|
327
|
+
return [proceduralEffect('burst', 'contact', origin, `${profile.traitGroupId}:cast`, target, 'follow')];
|
|
328
|
+
if (profile.motionFamily === 'blunt')
|
|
329
|
+
return [proceduralEffect('trail', 'contact', origin, `${profile.traitGroupId}:swing`, target, 'follow', undefined, { width: profile.punchPreset === 'heavy-swing' ? 11 : 7 })];
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
function specialEffects(profile, target) {
|
|
333
|
+
const special = profile.special;
|
|
334
|
+
const origin = special.origin;
|
|
335
|
+
const preset = special.preset;
|
|
336
|
+
if (preset === 'egg')
|
|
337
|
+
return [cloneEffect(profile.attachmentTargets[0], 'release', origin, special.id, target, 400, 28, 1)];
|
|
338
|
+
if (preset === 'disk')
|
|
339
|
+
return [cloneEffect(profile.attachmentTargets[0], 'release', origin, special.id, target, 470, 36, 2)];
|
|
340
|
+
if (preset === 'pan')
|
|
341
|
+
return [cloneEffect(profile.attachmentTargets[0], 'release', origin, special.id, target, 500, 42, 1)];
|
|
342
|
+
if (preset === 'laser')
|
|
343
|
+
return [proceduralEffect('beam', 'release', origin, special.id, target, 'detached', undefined, { color: 0xff335a, secondaryColor: 0xffffff, length: Math.hypot(target.x, target.y), width: 9, durationMs: 260 })];
|
|
344
|
+
if (preset === 'sonic')
|
|
345
|
+
return [proceduralEffect('burst', 'release', origin, special.id, target, 'follow', undefined, { color: 0x73dcff, secondaryColor: 0xffffff, radius: 24, durationMs: 360 })];
|
|
346
|
+
if (preset === 'flame')
|
|
347
|
+
return [proceduralEffect('trail', 'release', origin, special.id, target, 'follow', { kind: 'linear', targetOffset: { x: target.x * .55, y: target.y * .55 } }, { color: 0xff7a24, secondaryColor: 0xffed6a, width: 18, durationMs: 390 })];
|
|
348
|
+
if (preset === 'exhaust')
|
|
349
|
+
return [proceduralEffect('trail', 'release', origin, special.id, { x: -Math.max(90, Math.abs(target.x) * .55), y: -target.y * .2 }, 'follow', undefined, { color: 0x7b8194, secondaryColor: 0xd9deeb, width: 18, durationMs: 360 })];
|
|
350
|
+
if (preset === 'tail-stab' || preset === 'tail-sweep' || preset === 'tail-snap')
|
|
351
|
+
return [proceduralEffect('slash', 'release', origin, special.id, target, 'follow', undefined, { color: 0xb8f26a, width: preset === 'tail-stab' ? 5 : 10, radius: 30, durationMs: 300 })];
|
|
352
|
+
if (preset === 'tail-slam')
|
|
353
|
+
return [proceduralEffect('burst', 'contact', origin, special.id, target, 'follow', undefined, { color: 0x67c9ff, radius: 34, width: 12, durationMs: 360 })];
|
|
354
|
+
const colors = { wand: 0x9d71ff, energy: 0xffd84f, liquid: 0x5ddcff };
|
|
355
|
+
const kind = special.family === 'cast' ? 'burst' : special.family === 'tail' ? 'slash' : special.family;
|
|
356
|
+
return [proceduralEffect(kind, 'release', origin, special.id, target, 'follow', undefined, { color: colors[preset] ?? 0x9d71ff })];
|
|
357
|
+
}
|
|
358
|
+
function cloneEffect(attachmentId, cueId, origin, id, target, durationMs, arcHeight, rotationTurns) {
|
|
359
|
+
return proceduralEffect('projectile', cueId, origin, id, target, 'detached', { kind: 'arc', targetOffset: target, arcHeight, rotationTurns }, { durationMs, radius: 10, color: 0xffffff, visual: { kind: 'attachment-clone', attachmentId } });
|
|
360
|
+
}
|
|
361
|
+
function proceduralEffect(kind, cueId, origin, id, target, space, trajectory, values = {}) {
|
|
362
|
+
const colors = { beam: 0xff355e, slash: 0xffdf70, projectile: 0xfff2b2, burst: 0x9d71ff, trail: 0x74d7ff };
|
|
363
|
+
const actualTrajectory = trajectory ?? ((kind === 'projectile' || kind === 'trail' || kind === 'beam') ? { kind: 'linear', targetOffset: target } : undefined);
|
|
364
|
+
return {
|
|
365
|
+
id,
|
|
366
|
+
kind,
|
|
367
|
+
cueId,
|
|
368
|
+
socketId: origin.target === 'socket' ? origin.targetId : 'weapon',
|
|
369
|
+
origin,
|
|
370
|
+
space,
|
|
371
|
+
...(actualTrajectory ? { trajectory: actualTrajectory } : {}),
|
|
372
|
+
visual: values.visual ?? { kind: 'procedural' },
|
|
373
|
+
durationMs: values.durationMs ?? (kind === 'beam' ? 260 : kind === 'projectile' ? 480 : 300),
|
|
374
|
+
color: values.color ?? colors[kind],
|
|
375
|
+
secondaryColor: values.secondaryColor ?? 0xffffff,
|
|
376
|
+
length: values.length ?? (kind === 'beam' ? Math.hypot(target.x, target.y) : 90),
|
|
377
|
+
width: values.width ?? (kind === 'beam' ? 9 : 7),
|
|
378
|
+
radius: values.radius ?? (kind === 'projectile' ? 10 : 18),
|
|
379
|
+
distance: values.distance ?? Math.hypot(target.x, target.y),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function specialFor(preset, groupId, attachmentId, category, offset) {
|
|
383
|
+
const names = {
|
|
384
|
+
egg: { suffix: 'egg', label: 'lay and launch', family: 'projectile' },
|
|
385
|
+
laser: { suffix: 'laser', label: 'beam', family: 'beam' },
|
|
386
|
+
flame: { suffix: 'flame', label: 'flame', family: 'burst' },
|
|
387
|
+
sonic: { suffix: 'sonic', label: 'sonic pulse', family: 'burst' },
|
|
388
|
+
disk: { suffix: 'disk', label: 'disk throw', family: 'projectile' },
|
|
389
|
+
pan: { suffix: 'pan', label: 'pan throw', family: 'projectile' },
|
|
390
|
+
wand: { suffix: 'cast', label: 'wand cast', family: 'cast' },
|
|
391
|
+
energy: { suffix: 'energy', label: 'energy burst', family: 'cast' },
|
|
392
|
+
liquid: { suffix: 'liquid', label: 'beaker splash', family: 'projectile' },
|
|
393
|
+
exhaust: { suffix: 'exhaust', label: 'exhaust burst', family: 'burst' },
|
|
394
|
+
'tail-stab': { suffix: 'tail', label: 'tail stab', family: 'tail' },
|
|
395
|
+
'tail-sweep': { suffix: 'tail', label: 'tail sweep', family: 'tail' },
|
|
396
|
+
'tail-snap': { suffix: 'tail', label: 'hydra snap', family: 'tail' },
|
|
397
|
+
'tail-slam': { suffix: 'tail', label: 'tail slam', family: 'tail' },
|
|
398
|
+
};
|
|
399
|
+
const named = names[preset];
|
|
400
|
+
const useAttachment = preset === 'egg' || preset === 'disk' || preset === 'pan' || preset === 'sonic' || preset === 'flame' || preset === 'wand' || preset === 'tail-stab' || preset === 'tail-sweep' || preset === 'tail-snap' || preset === 'tail-slam';
|
|
401
|
+
const socketId = preset === 'laser' || (preset === 'energy' && category === 'Head') ? 'eyes' : preset === 'exhaust' ? 'tail' : category === 'Tail' ? 'tail' : 'weapon';
|
|
402
|
+
return {
|
|
403
|
+
id: `${groupId}:${named.suffix}`,
|
|
404
|
+
label: `${displayName(groupId)} ${named.label}`,
|
|
405
|
+
family: named.family,
|
|
406
|
+
preset,
|
|
407
|
+
origin: useAttachment ? { target: 'attachment', targetId: attachmentId, x: offset.x, y: offset.y } : socketOrigin(socketId, offset),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function socketOrigin(targetId, offset = { x: 0, y: 0 }) { return { target: 'socket', targetId, x: offset.x, y: offset.y }; }
|
|
411
|
+
function attachmentOrigin(profile) { return { target: 'attachment', targetId: profile.attachmentTargets[0], x: profile.originOffset.x, y: profile.originOffset.y }; }
|
|
412
|
+
function normaliseTarget(value) {
|
|
413
|
+
const target = value ?? DEFAULT_TARGET;
|
|
414
|
+
if (!Number.isFinite(target.x) || !Number.isFinite(target.y))
|
|
415
|
+
throw new Error('targetOffset must contain finite x and y values');
|
|
416
|
+
return Object.freeze({ x: target.x, y: target.y });
|
|
417
|
+
}
|
|
418
|
+
function freezeClip(clip) {
|
|
419
|
+
for (const track of clip.tracks) {
|
|
420
|
+
for (const keyframe of track.keyframes)
|
|
421
|
+
Object.freeze(keyframe);
|
|
422
|
+
Object.freeze(track.keyframes);
|
|
423
|
+
Object.freeze(track);
|
|
424
|
+
}
|
|
425
|
+
for (const cue of clip.cues ?? [])
|
|
426
|
+
Object.freeze(cue);
|
|
427
|
+
if (clip.cues)
|
|
428
|
+
Object.freeze(clip.cues);
|
|
429
|
+
Object.freeze(clip.tracks);
|
|
430
|
+
return Object.freeze(clip);
|
|
431
|
+
}
|
|
432
|
+
function profileMap(definition) {
|
|
433
|
+
const profiles = createChiknTraitAnimationProfiles(definition);
|
|
434
|
+
const errors = validateChiknTraitAnimationProfiles(definition, profiles);
|
|
435
|
+
if (errors.length)
|
|
436
|
+
throw new Error(`Invalid Chikn trait animation profiles:\n${errors.join('\n')}`);
|
|
437
|
+
return new Map(profiles.map((profile) => [profile.traitGroupId, profile]));
|
|
438
|
+
}
|
|
439
|
+
function assertRecipe(recipe, definition) {
|
|
440
|
+
if (recipe.species !== definition.id)
|
|
441
|
+
throw new Error(`Recipe species ${recipe.species} does not match rig ${definition.id}`);
|
|
442
|
+
for (const id of recipe.traitGroupIds)
|
|
443
|
+
if (!definition.attachmentGroups?.[id])
|
|
444
|
+
throw new Error(`Unknown trait group ${id}`);
|
|
445
|
+
}
|
|
446
|
+
function neutralTransform(definition, slotId) {
|
|
447
|
+
const attachmentId = definition.defaultSkinId ? definition.skins?.[definition.defaultSkinId]?.[slotId] : undefined;
|
|
448
|
+
const attachment = definition.attachments.find(({ id }) => id === attachmentId) ?? definition.attachments.find(({ slotId: candidate }) => candidate === slotId);
|
|
449
|
+
const bone = definition.bones.find(({ id }) => id === attachment?.boneId);
|
|
450
|
+
return { x: bone?.x ?? attachment?.x ?? 0, y: bone?.y ?? attachment?.y ?? 0, rotation: bone?.rotation ?? attachment?.rotation ?? 0, scaleX: bone?.scaleX ?? attachment?.scaleX ?? 1, scaleY: bone?.scaleY ?? attachment?.scaleY ?? 1 };
|
|
451
|
+
}
|
|
452
|
+
function neutralBoneTransform(definition, boneId) {
|
|
453
|
+
const bone = definition.bones.find(({ id }) => id === boneId);
|
|
454
|
+
if (!bone)
|
|
455
|
+
throw new Error(`Unknown action bone: ${boneId}`);
|
|
456
|
+
return { x: bone.x, y: bone.y, rotation: bone.rotation, scaleX: bone.scaleX, scaleY: bone.scaleY };
|
|
457
|
+
}
|
|
458
|
+
function neutralAttachmentTransform(definition, attachmentId) {
|
|
459
|
+
const attachment = definition.attachments.find(({ id }) => id === attachmentId);
|
|
460
|
+
if (!attachment)
|
|
461
|
+
throw new Error(`Unknown profile attachment target: ${attachmentId}`);
|
|
462
|
+
return { x: attachment.x ?? 0, y: attachment.y ?? 0, rotation: attachment.rotation ?? 0, scaleX: attachment.scaleX ?? 1, scaleY: attachment.scaleY ?? 1 };
|
|
463
|
+
}
|
|
464
|
+
function requiredClip(clips, id) {
|
|
465
|
+
const clip = clips.get(id);
|
|
466
|
+
if (!clip)
|
|
467
|
+
throw new Error(`Unknown Chikn action clip: ${id}`);
|
|
468
|
+
return clip;
|
|
469
|
+
}
|
|
470
|
+
function speciesOf(definition) {
|
|
471
|
+
if (definition.id !== 'chikn' && definition.id !== 'roostr')
|
|
472
|
+
throw new Error(`Unsupported Chikn rig species: ${definition.id}`);
|
|
473
|
+
return definition.id;
|
|
474
|
+
}
|
|
475
|
+
function displayName(groupId) { return groupId.split('/').at(-1).split('-').map((word) => word ? word[0].toUpperCase() + word.slice(1) : '').join(' '); }
|
|
476
|
+
//# sourceMappingURL=actions.js.map
|