@roost2d/chikn-rigs 0.5.0 → 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 +6 -2
- package/dist/actions.d.ts +24 -4
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +311 -101
- package/dist/actions.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- 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 -3
package/dist/actions.js
CHANGED
|
@@ -1,39 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
const RANGED = /(?:peacemaker|banger|zippo|boombox|floppy-disk|maverick|admiral)/;
|
|
4
|
-
const CASTING = /(?:wand|mage|all-seeing-eye|super-saiyan|saint|hypno|jell-o|beaker)/;
|
|
5
|
-
const TAIL_ATTACK = /(?:scorpion-king|sword-tail|hydra)/;
|
|
1
|
+
import { CURATED_TRAIT_PROFILES, curatedTraitProfile, } from './trait-profiles.js';
|
|
2
|
+
const DEFAULT_TARGET = Object.freeze({ x: 180, y: 0 });
|
|
6
3
|
const profileCache = new WeakMap();
|
|
7
4
|
const clipCache = new WeakMap();
|
|
8
|
-
|
|
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. */
|
|
9
31
|
export function createChiknTraitAnimationProfiles(definition) {
|
|
10
32
|
const cached = profileCache.get(definition);
|
|
11
33
|
if (cached)
|
|
12
34
|
return cached;
|
|
13
|
-
const species = definition
|
|
14
|
-
if (species !== 'chikn' && species !== 'roostr')
|
|
15
|
-
throw new Error(`Unsupported Chikn rig species: ${species}`);
|
|
35
|
+
const species = speciesOf(definition);
|
|
16
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}`);
|
|
17
40
|
const category = String(group.metadata?.category ?? group.slotId);
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
: BLUNT.test(token) ? 'blunt'
|
|
25
|
-
: 'accent';
|
|
26
|
-
const variation = stableVariation(group.id);
|
|
27
|
-
const special = specialFor(group.id);
|
|
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;
|
|
28
47
|
return Object.freeze({
|
|
29
48
|
species,
|
|
30
49
|
traitGroupId: group.id,
|
|
31
50
|
category,
|
|
32
|
-
motionFamily,
|
|
51
|
+
motionFamily: curated.motionFamily,
|
|
33
52
|
attachmentTargets: Object.freeze([...group.attachmentIds]),
|
|
34
53
|
supportedActions: Object.freeze(['punch', 'kick', ...(special ? ['special'] : [])]),
|
|
35
|
-
|
|
36
|
-
|
|
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 } : {}),
|
|
37
63
|
...(special ? { special } : {}),
|
|
38
64
|
});
|
|
39
65
|
}));
|
|
@@ -42,8 +68,10 @@ export function createChiknTraitAnimationProfiles(definition) {
|
|
|
42
68
|
}
|
|
43
69
|
export function validateChiknTraitAnimationProfiles(definition, profiles) {
|
|
44
70
|
const errors = [];
|
|
71
|
+
const species = definition.id === 'chikn' || definition.id === 'roostr' ? definition.id : undefined;
|
|
45
72
|
const groups = definition.attachmentGroups ?? {};
|
|
46
73
|
const seen = new Set();
|
|
74
|
+
const attachmentIds = new Set(definition.attachments.map(({ id }) => id));
|
|
47
75
|
const socketIds = new Set((definition.sockets ?? []).map(({ id }) => id));
|
|
48
76
|
for (const profile of profiles) {
|
|
49
77
|
if (seen.has(profile.traitGroupId))
|
|
@@ -58,16 +86,28 @@ export function validateChiknTraitAnimationProfiles(definition, profiles) {
|
|
|
58
86
|
errors.push(`${profile.traitGroupId}: species does not match ${definition.id}`);
|
|
59
87
|
if (profile.attachmentTargets.join('|') !== group.attachmentIds.join('|'))
|
|
60
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`);
|
|
61
91
|
if (!profile.supportedActions.includes('punch') || !profile.supportedActions.includes('kick') || profile.supportedActions.includes('special') !== Boolean(profile.special))
|
|
62
92
|
errors.push(`${profile.traitGroupId}: supported actions do not match its capabilities`);
|
|
63
93
|
if (!(profile.amplitude > 0) || !(profile.speed > 0))
|
|
64
94
|
errors.push(`${profile.traitGroupId}: adjustments must be positive`);
|
|
65
|
-
|
|
66
|
-
|
|
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}`);
|
|
67
102
|
}
|
|
68
103
|
for (const groupId of Object.keys(groups))
|
|
69
104
|
if (!seen.has(groupId))
|
|
70
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
|
+
}
|
|
71
111
|
return errors;
|
|
72
112
|
}
|
|
73
113
|
export function listChiknSpecials(recipe, definition) {
|
|
@@ -85,38 +125,57 @@ export function listAvailableChiknActions(recipe, definition) {
|
|
|
85
125
|
...listChiknSpecials(recipe, definition),
|
|
86
126
|
];
|
|
87
127
|
}
|
|
88
|
-
export function resolveChiknAction(recipe, definition, actionId) {
|
|
128
|
+
export function resolveChiknAction(recipe, definition, actionId, context = {}) {
|
|
89
129
|
assertRecipe(recipe, definition);
|
|
130
|
+
const targetOffset = normaliseTarget(context.targetOffset);
|
|
90
131
|
const clips = new Map(createChiknActionClips(definition).map((clip) => [clip.id, clip]));
|
|
91
132
|
const profiles = profileMap(definition);
|
|
92
133
|
const selected = recipe.traitGroupIds.map((id) => profiles.get(id)).filter(Boolean);
|
|
93
134
|
if (actionId === 'punch') {
|
|
94
|
-
const weapon = selected.find(({
|
|
135
|
+
const weapon = selected.find(({ punchPreset }) => punchPreset !== undefined);
|
|
95
136
|
const family = weapon?.motionFamily ?? 'unarmed';
|
|
96
|
-
const
|
|
97
|
-
const
|
|
137
|
+
const base = requiredClip(clips, `${definition.id}.action.punch.${family}`);
|
|
138
|
+
const clip = tailorActionClip(definition, base, selected, weapon);
|
|
98
139
|
return {
|
|
99
|
-
id: actionId,
|
|
100
|
-
|
|
101
|
-
|
|
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,
|
|
102
148
|
};
|
|
103
149
|
}
|
|
104
150
|
if (actionId === 'kick') {
|
|
105
|
-
const feet = selected.find(({
|
|
151
|
+
const feet = selected.find(({ kickPreset }) => kickPreset !== undefined);
|
|
106
152
|
const combined = feet ? (definition.attachmentGroups?.[feet.traitGroupId]?.attachmentIds.length ?? 0) === 1 : false;
|
|
107
153
|
const family = combined ? 'combined' : 'paired';
|
|
108
|
-
|
|
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
|
+
};
|
|
109
165
|
}
|
|
110
166
|
const specialChoice = listChiknSpecials(recipe, definition).find(({ id }) => id === actionId);
|
|
111
167
|
if (!specialChoice?.sourceTraitGroupId)
|
|
112
168
|
throw new Error(`Special is not available for this recipe: ${actionId}`);
|
|
113
169
|
const profile = profiles.get(specialChoice.sourceTraitGroupId);
|
|
114
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');
|
|
115
173
|
return {
|
|
116
174
|
...specialChoice,
|
|
117
175
|
motionFamily: special.family,
|
|
118
|
-
clip
|
|
119
|
-
effects:
|
|
176
|
+
clip,
|
|
177
|
+
effects: specialEffects(profile, targetOffset),
|
|
178
|
+
targetOffset,
|
|
120
179
|
};
|
|
121
180
|
}
|
|
122
181
|
/** Species-specific brawler and attack foundations generated from the rig setup pose. */
|
|
@@ -124,64 +183,103 @@ export function createChiknActionClips(definition) {
|
|
|
124
183
|
const cached = clipCache.get(definition);
|
|
125
184
|
if (cached)
|
|
126
185
|
return cached;
|
|
127
|
-
const species = definition
|
|
128
|
-
if (species !== 'chikn' && species !== 'roostr')
|
|
129
|
-
throw new Error(`Unsupported Chikn rig species: ${species}`);
|
|
186
|
+
const species = speciesOf(definition);
|
|
130
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 });
|
|
131
|
-
const track = (
|
|
132
|
-
const neutral = neutralTransform(definition,
|
|
133
|
-
return { target
|
|
134
|
-
};
|
|
135
|
-
const phaseCues = (contact, duration, release = contact) => {
|
|
136
|
-
const cues = [
|
|
137
|
-
{ id: 'anticipation', timeMs: 0, phase: 'anticipation' },
|
|
138
|
-
{ id: release === contact ? 'contact' : 'release', timeMs: release, phase: release === contact ? 'contact' : 'release' },
|
|
139
|
-
...(release === contact ? [] : [{ id: 'contact', timeMs: contact, phase: 'contact' }]),
|
|
140
|
-
{ id: 'recovery', timeMs: Math.min(duration - 1, Math.max(contact, release) + Math.round((duration - Math.max(contact, release)) * 0.35)), phase: 'recovery' },
|
|
141
|
-
{ id: 'complete', timeMs: duration, phase: 'complete' },
|
|
142
|
-
];
|
|
143
|
-
return cues.sort((a, b) => a.timeMs - b.timeMs);
|
|
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' })) };
|
|
144
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
|
+
];
|
|
145
199
|
const clips = [
|
|
146
|
-
make('combat_idle', 800, [
|
|
147
|
-
make('dash', 260, [
|
|
148
|
-
make('guard', 420, [
|
|
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)),
|
|
149
203
|
];
|
|
150
|
-
const punch = (family,
|
|
151
|
-
|
|
152
|
-
|
|
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));
|
|
153
216
|
clips.push(kick('paired'), kick('combined'));
|
|
154
|
-
const special = (family, release, duration) => make(`action.special.${family}`, duration, [
|
|
155
|
-
|
|
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
|
+
]));
|
|
156
238
|
const frozen = Object.freeze(clips.map(freezeClip));
|
|
157
239
|
clipCache.set(definition, frozen);
|
|
158
240
|
return frozen;
|
|
159
241
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
const durationMs = Math.max(1, Math.round(base.durationMs * timeScale));
|
|
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));
|
|
164
245
|
const scaleTime = (timeMs) => Math.min(durationMs, Math.round(timeMs * timeScale));
|
|
165
246
|
const accentAt = base.cues?.find(({ phase }) => phase === 'contact' || phase === 'release')?.timeMs ?? base.durationMs * .5;
|
|
166
247
|
const recoverAt = base.cues?.find(({ phase }) => phase === 'recovery')?.timeMs ?? base.durationMs * .72;
|
|
167
248
|
const traitTracks = [];
|
|
168
249
|
for (const profile of profiles) {
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const rotation = profile.amplitude * direction * (category === 'tail' ? .16 : category === 'neck' || category === 'head' ? .1 : .065);
|
|
172
|
-
const lift = profile.amplitude * (category === 'feet' ? -2.8 : -1.5);
|
|
173
|
-
for (const targetId of profile.attachmentTargets) {
|
|
250
|
+
const isDriver = profile === driver;
|
|
251
|
+
for (const [index, targetId] of profile.attachmentTargets.entries()) {
|
|
174
252
|
const neutral = neutralAttachmentTransform(definition, targetId);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 });
|
|
185
283
|
}
|
|
186
284
|
}
|
|
187
285
|
return {
|
|
@@ -189,11 +287,134 @@ function tailorActionClip(definition, base, profiles, driver) {
|
|
|
189
287
|
durationMs,
|
|
190
288
|
cues: base.cues?.map((cue) => ({ ...cue, timeMs: scaleTime(cue.timeMs) })),
|
|
191
289
|
tracks: [
|
|
192
|
-
...base.tracks.map((
|
|
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)) }) })) })),
|
|
193
291
|
...traitTracks,
|
|
194
292
|
],
|
|
195
293
|
};
|
|
196
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
|
+
}
|
|
197
418
|
function freezeClip(clip) {
|
|
198
419
|
for (const track of clip.tracks) {
|
|
199
420
|
for (const keyframe of track.keyframes)
|
|
@@ -222,32 +443,18 @@ function assertRecipe(recipe, definition) {
|
|
|
222
443
|
if (!definition.attachmentGroups?.[id])
|
|
223
444
|
throw new Error(`Unknown trait group ${id}`);
|
|
224
445
|
}
|
|
225
|
-
function specialFor(groupId) {
|
|
226
|
-
const id = groupId.toLowerCase();
|
|
227
|
-
if (id.includes('laser-eye'))
|
|
228
|
-
return { id: `${groupId}:laser`, label: `${displayName(groupId)} beam`, family: 'beam', socketId: 'eyes' };
|
|
229
|
-
if (TAIL_ATTACK.test(id))
|
|
230
|
-
return { id: `${groupId}:tail`, label: `${displayName(groupId)} strike`, family: 'tail', socketId: 'tail' };
|
|
231
|
-
if (/(?:golden-egg|very-fresh-egg|omelette)/.test(id))
|
|
232
|
-
return { id: `${groupId}:egg`, label: `${displayName(groupId)} throw`, family: 'projectile', socketId: id.startsWith('tail/') ? 'tail' : 'wing-front' };
|
|
233
|
-
if (CASTING.test(id))
|
|
234
|
-
return { id: `${groupId}:cast`, label: `${displayName(groupId)} special`, family: 'cast', socketId: id.startsWith('head/') ? 'eyes' : 'weapon' };
|
|
235
|
-
if (RANGED.test(id))
|
|
236
|
-
return { id: `${groupId}:shot`, label: `${displayName(groupId)} shot`, family: 'projectile', socketId: 'weapon' };
|
|
237
|
-
if (/(?:gas-guzzler|fat-pipes|smokestack|hothead|toot-toot)/.test(id))
|
|
238
|
-
return { id: `${groupId}:burst`, label: `${displayName(groupId)} burst`, family: 'burst', socketId: id.startsWith('tail/') ? 'tail' : 'weapon' };
|
|
239
|
-
return undefined;
|
|
240
|
-
}
|
|
241
|
-
function effectFor(kind, cueId, socketId, id) {
|
|
242
|
-
const colors = { beam: 0xff355e, slash: 0xffdf70, projectile: 0xfff2b2, burst: 0x9d71ff, trail: 0x74d7ff };
|
|
243
|
-
return { id, kind, cueId, socketId, durationMs: kind === 'beam' ? 260 : kind === 'projectile' ? 480 : 300, color: colors[kind], secondaryColor: 0xffffff, length: kind === 'beam' ? 180 : 90, width: kind === 'beam' ? 9 : 7, radius: kind === 'projectile' ? 10 : 18, distance: kind === 'projectile' ? 170 : 80 };
|
|
244
|
-
}
|
|
245
446
|
function neutralTransform(definition, slotId) {
|
|
246
447
|
const attachmentId = definition.defaultSkinId ? definition.skins?.[definition.defaultSkinId]?.[slotId] : undefined;
|
|
247
448
|
const attachment = definition.attachments.find(({ id }) => id === attachmentId) ?? definition.attachments.find(({ slotId: candidate }) => candidate === slotId);
|
|
248
449
|
const bone = definition.bones.find(({ id }) => id === attachment?.boneId);
|
|
249
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 };
|
|
250
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
|
+
}
|
|
251
458
|
function neutralAttachmentTransform(definition, attachmentId) {
|
|
252
459
|
const attachment = definition.attachments.find(({ id }) => id === attachmentId);
|
|
253
460
|
if (!attachment)
|
|
@@ -260,7 +467,10 @@ function requiredClip(clips, id) {
|
|
|
260
467
|
throw new Error(`Unknown Chikn action clip: ${id}`);
|
|
261
468
|
return clip;
|
|
262
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
|
+
}
|
|
263
475
|
function displayName(groupId) { return groupId.split('/').at(-1).split('-').map((word) => word ? word[0].toUpperCase() + word.slice(1) : '').join(' '); }
|
|
264
|
-
function stableVariation(value) { let hash = 2166136261; for (const char of value)
|
|
265
|
-
hash = Math.imul(hash ^ char.charCodeAt(0), 16777619); return (hash >>> 0) / 0xffffffff; }
|
|
266
476
|
//# sourceMappingURL=actions.js.map
|