-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrig.js
More file actions
266 lines (245 loc) · 11.5 KB
/
Copy pathrig.js
File metadata and controls
266 lines (245 loc) · 11.5 KB
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/* ============================================================================
* ZUO Motion Rig — shared animation core
* ----------------------------------------------------------------------------
* This is the SINGLE SOURCE OF TRUTH for how a 3x3 sliced character sheet is
* assembled and animated. The web preview (preview.js) and the exported GML
* script both implement the exact same math, so what you see on the site is
* what you get in GameMaker Studio 2.
*
* Coordinate system (character space):
* - Origin (0,0) is the character's ground point (between the feet).
* - +x is right, +y is DOWN (screen convention, matches GML draw + canvas).
* - All offsets below are in "cell units": 1.0 == one grid cell (size/3 px).
* - Rotations are in radians. Positive = clockwise (screen convention).
* ========================================================================== */
(function (root) {
"use strict";
// --- Grid mapping: which 3x3 cell holds which part -----------------------
// col/row are 0-indexed. Cell (col,row) source rect =
// left = col*cw, top = row*ch, width = cw, height = ch.
//
// [Front Hair 0,0] [Face 1,0] [Back Hair 2,0]
// [Left Arm 0,1] [Front Torso 1,1] [Right Arm 2,1]
// [Left Leg 0,2] [Back Torso 1,2] [Right Leg 2,2]
//
// group: which layout anchor the part hangs off (head/torso/armL/…).
// off: small fixed offset (cells) from that anchor.
// pivot: [px, py] pivot location inside the cell, as a fraction 0..1.
// head/face/torso -> cell center [0.5,0.5]; arms/legs -> cell top-center [0.5,0.0].
var PARTS = [
// key label col row group off pivot
{ key: "backhair", label: "Back Hair", col: 2, row: 0, group: "head", off: [0, -0.05], pivot: [0.5, 0.5] },
{ key: "backtorso", label: "Back Torso", col: 1, row: 2, group: "torso", off: [0, 0], pivot: [0.5, 0.5] },
{ key: "leftleg", label: "Left Leg", col: 0, row: 2, group: "legL", off: [0, 0], pivot: [0.5, 0.0] },
{ key: "rightleg", label: "Right Leg", col: 2, row: 2, group: "legR", off: [0, 0], pivot: [0.5, 0.0] },
{ key: "leftarm", label: "Left Arm", col: 0, row: 1, group: "armL", off: [0, 0], pivot: [0.5, 0.0] },
{ key: "fronttorso", label: "Front Torso", col: 1, row: 1, group: "torso", off: [0, 0], pivot: [0.5, 0.5] },
{ key: "rightarm", label: "Right Arm", col: 2, row: 1, group: "armR", off: [0, 0], pivot: [0.5, 0.0] },
{ key: "face", label: "Face", col: 1, row: 0, group: "head", off: [0, 0], pivot: [0.5, 0.5] },
{ key: "fronthair", label: "Front Hair", col: 0, row: 0, group: "head", off: [0, -0.05], pivot: [0.5, 0.5] }
];
// PARTS is already in back-to-front draw order.
// Handy lookup by key.
var PART_BY_KEY = {};
for (var i = 0; i < PARTS.length; i++) PART_BY_KEY[PARTS[i].key] = PARTS[i];
// --- Adjustable layout (cells). These are the values the web "Spacing"
// sliders edit and that get exported as GML macros. -------------------
var DEFAULT_LAYOUT = {
headY: -1.95, // head/face vertical (more negative = higher)
torsoY: -1.50, // torso vertical
armX: 0.090, // arm left-right spread
armY: -1.65, // arm vertical position
legX: 0.06, // leg left-right spread
legY: -1.40, // leg vertical position (hip height)
handLen: 0.30, // hand-to-weapon gap (from the right-arm anchor to the weapon, cells)
handRot: 1.00 // weapon angle (radians) — default is slightly raised
};
function groupAnchor(L, g) {
switch (g) {
case "head": return [0, L.headY];
case "torso": return [0, L.torsoY];
case "armL": return [-L.armX, L.armY];
case "armR": return [ L.armX, L.armY];
case "legL": return [-L.legX, L.legY];
case "legR": return [ L.legX, L.legY];
}
return [0, 0];
}
// Final base position (cells) of a part's pivot for a given layout.
function partBase(L, part) {
var a = groupAnchor(L, part.group);
return [a[0] + part.off[0], a[1] + part.off[1]];
}
// --- Weapon (a SEPARATE center-pivot image, not part of the 3x3 sheet) ----
// Always held in the RIGHT hand. Drawn BEHIND the right arm normally, and
// IN FRONT of it during attacks. Its placement (hand-to-weapon gap handLen,
// weapon angle handRot) lives in the layout so it's adjustable + exported to GML.
function isAttack(motion) { return motion.indexOf("attack") === 0; }
// Extra weapon rotation (radians) added to handRot for specific motions.
// Thrust: level the weapon out (less raised) as the jab extends.
function weaponRot(motion, t) {
if (motion === "attack3") {
var u = (t % 45) / 45;
var extend = smoothstep(clamp((u - 0.35) / 0.10, 0, 1));
var env = 1 - smoothstep(clamp((u - 0.72) / 0.25, 0, 1));
return -0.9 * extend * env;
}
return 0;
}
// --- Motion catalog (order = list order in the UI) -----------------------
var MOTIONS = [
{ key: "idle", label: "Idle" },
{ key: "walk", label: "Walk" },
{ key: "run", label: "Run" },
{ key: "jump", label: "Jump" },
{ key: "attack1", label: "Slash Down" },
{ key: "attack2", label: "Slash Up" },
{ key: "attack3", label: "Thrust" },
{ key: "hit", label: "Hit" }
];
// ------------------------------------------------------------------------
// Math helpers (mirrored 1:1 in GML)
// ------------------------------------------------------------------------
var PI = Math.PI;
function clamp(v, a, b) { return v < a ? a : (v > b ? b : v); }
function smoothstep(x) { x = clamp(x, 0, 1); return x * x * (3 - 2 * x); }
var sin = Math.sin, cos = Math.cos, abs = Math.abs, exp = Math.exp;
// Default (no-op) transform.
function T() { return { dx: 0, dy: 0, rot: 0, sx: 1, sy: 1 }; }
// ------------------------------------------------------------------------
// ROOT transform: applied to the whole character (after user x/y/scale).
// Returns { dx, dy, rot, sx, sy }.
// ------------------------------------------------------------------------
function rootTransform(motion, t) {
var r = T();
switch (motion) {
case "idle":
// grounded: no root translation, feet stay planted (breathing is in
// the torso/head part transforms).
break;
case "walk": {
var pw = t * 0.18;
r.rot = 0.02 * sin(pw); // slight lean, pivots at the feet
break;
}
case "run": {
r.rot = 0.14; // lean forward, pivots at the feet
break;
}
// jump: pose only (the actual airborne Y is handled by your game),
// so no root translation — see partTransform for the tuck pose.
// attacks are arm-driven (see partTransform) — no root motion.
case "hit": {
var uh = (t % 45) / 45;
var d = 1 - uh;
r.dx = -0.30 * d * sin(uh * PI * 4);
r.rot = -0.12 * d;
break;
}
}
return r;
}
// ------------------------------------------------------------------------
// PART transform: per-part animation on top of its base position.
// Returns { dx, dy, rot, sx, sy }.
// ------------------------------------------------------------------------
function partTransform(motion, t, key) {
var r = T();
switch (motion) {
case "idle": {
// very subtle: the whole upper body (head + torso + arms) drifts up
// and down together a tiny bit; legs/feet stay planted.
var b = sin(t * 0.06);
if (key === "face" || key === "fronthair" || key === "backhair") r.dy = -0.02 * b;
if (key === "fronttorso" || key === "backtorso") r.dy = -0.02 * b;
if (key === "leftarm") { r.dy = -0.02 * b; r.rot = 0.03 * b; }
if (key === "rightarm") { r.dy = -0.02 * b; r.rot = -0.03 * b; }
break;
}
case "walk": {
var s = sin(t * 0.18);
if (key === "leftleg") r.rot = 0.50 * s;
if (key === "rightleg") r.rot = -0.50 * s;
if (key === "leftarm") r.rot = -0.40 * s;
if (key === "rightarm") r.rot = 0.40 * s;
if (key === "fronttorso" || key === "backtorso") r.rot = 0.03 * sin(t * 0.36);
break;
}
case "run": {
var sr = sin(t * 0.30);
if (key === "leftleg") r.rot = 0.80 * sr;
if (key === "rightleg") r.rot = -0.80 * sr;
if (key === "leftarm") r.rot = -0.75 * sr - 0.2;
if (key === "rightarm") r.rot = 0.75 * sr - 0.2;
if (key === "face" || key === "fronthair" || key === "backhair") r.rot = 0.05 * sr;
break;
}
case "jump": {
var uj = (t % 60) / 60;
var arc = sin(uj * PI);
if (key === "leftleg") r.rot = 0.45 * arc;
if (key === "rightleg") r.rot = -0.45 * arc;
if (key === "leftarm") r.rot = -0.60 * arc - 0.3;
if (key === "rightarm") r.rot = 0.60 * arc + 0.3;
break;
}
// Attack timing: slow-ish windup → FAST strike (0.10) → hold → return.
case "attack1": { // Slash Down — quick overhead swing + hold
var u = (t % 45) / 45;
var wind = smoothstep(clamp(u / 0.35, 0, 1));
var strike = smoothstep(clamp((u - 0.35) / 0.10, 0, 1));
var env = 1 - smoothstep(clamp((u - 0.72) / 0.25, 0, 1));
if (key === "rightarm") r.rot = (-2.4 * wind + 3.1 * strike) * env;
if (key === "leftarm") r.rot = (0.4 * wind - 0.2 * strike) * env;
if (key === "fronttorso" || key === "backtorso") r.rot = (-0.04 * wind - 0.14 * strike) * env;
if (key === "face" || key === "fronthair" || key === "backhair") r.rot = (-0.03 * wind - 0.10 * strike) * env;
break;
}
case "attack2": { // Slash Up — quick upward swing + hold
var u = (t % 45) / 45;
var wind = smoothstep(clamp(u / 0.35, 0, 1));
var strike = smoothstep(clamp((u - 0.35) / 0.10, 0, 1));
var env = 1 - smoothstep(clamp((u - 0.72) / 0.25, 0, 1));
if (key === "rightarm") r.rot = (1.5 * wind - 3.1 * strike) * env;
if (key === "leftarm") r.rot = (-0.3 * wind + 0.2 * strike) * env;
if (key === "fronttorso" || key === "backtorso") r.rot = (0.05 * wind + 0.12 * strike) * env;
break;
}
case "attack3": { // Thrust — quick jab + hold
var u = (t % 45) / 45;
var raise = smoothstep(clamp(u / 0.35, 0, 1));
var extend = smoothstep(clamp((u - 0.35) / 0.10, 0, 1));
var env = 1 - smoothstep(clamp((u - 0.72) / 0.25, 0, 1));
if (key === "rightarm") { r.rot = (-1.15 * raise) * env; r.dx = 0.12 * extend * env; }
if (key === "leftarm") r.rot = 0.2 * raise * env;
if (key === "fronttorso" || key === "backtorso") r.rot = -0.06 * extend * env;
break;
}
case "hit": {
var uh = (t % 45) / 45;
var d = 1 - uh;
var sh = sin(uh * PI * 4);
if (key === "face" || key === "fronthair" || key === "backhair") r.rot = 0.22 * d * sh;
if (key === "leftarm") r.rot = -0.3 * d;
if (key === "rightarm") r.rot = 0.3 * d;
break;
}
}
return r;
}
root.ZUO = {
PARTS: PARTS,
PART_BY_KEY: PART_BY_KEY,
MOTIONS: MOTIONS,
FACE: -1, // default facing: -1 = left (art faces left; motions mirrored)
DEFAULT_LAYOUT: DEFAULT_LAYOUT,
isAttack: isAttack,
weaponRot: weaponRot,
groupAnchor: groupAnchor,
partBase: partBase,
rootTransform: rootTransform,
partTransform: partTransform,
clamp: clamp,
smoothstep: smoothstep
};
})(typeof window !== "undefined" ? window : this);