@siegesailor/terminal-sigma 1.2.0 → 1.4.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 +84 -31
- package/build/app.js +252 -99
- package/build/app.js.map +1 -1
- package/build/components/autocomplete-input.js +57 -0
- package/build/components/autocomplete-input.js.map +1 -0
- package/build/components/character.js +16 -6
- package/build/components/character.js.map +1 -1
- package/build/components/choice.js +16 -4
- package/build/components/choice.js.map +1 -1
- package/build/components/entry-form.js +43 -11
- package/build/components/entry-form.js.map +1 -1
- package/build/components/logs-view.js +9 -3
- package/build/components/logs-view.js.map +1 -1
- package/build/components/multi-choice.js +45 -0
- package/build/components/multi-choice.js.map +1 -0
- package/build/components/today-panel.js +14 -8
- package/build/components/today-panel.js.map +1 -1
- package/build/content.js +207 -65
- package/build/content.js.map +1 -1
- package/build/foods.js +159 -0
- package/build/foods.js.map +1 -0
- package/build/i18n.js +262 -36
- package/build/i18n.js.map +1 -1
- package/build/pixel-art.js +476 -306
- package/build/pixel-art.js.map +1 -1
- package/build/progress.js +33 -0
- package/build/progress.js.map +1 -1
- package/build/quotes.js +404 -220
- package/build/quotes.js.map +1 -1
- package/package.json +1 -1
- package/build/components/menu-grid.js +0 -43
- package/build/components/menu-grid.js.map +0 -1
package/build/pixel-art.js
CHANGED
|
@@ -1,336 +1,506 @@
|
|
|
1
|
-
// The
|
|
2
|
-
// 2 stacked pixels with "▀", foreground for the top and
|
|
3
|
-
|
|
4
|
-
export const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const standingLegs = layer(5, 19, ".kpk.kpk", ".kPk.kppk", ".kPk.kppk", ".kPk.kppk", ".kPk.kppk", ".kbk.kbbk", "kbbk.kbbbk", "kkkk.kkkkk");
|
|
29
|
-
const stride = (back, front) => layer(1, 19, `.....k${back}${back}${front}${front}k`, `....k${back}k..k${front}${front}k`, `...k${back}k....k${front}${front}k`, `..k${back}k......k${front}k`, ".kbk.......kpk", "kbk........kbbk", "kk.........kkkk");
|
|
30
|
-
const passing = (raised, planted) => layer(4, 19, `..k${raised}${planted}${planted}k`, `.kbk${planted}${planted}k`, `kbbkk${planted}k`, `.kk.k${planted}${planted}k`, `....k${planted}k`, "....kbbk", "....kbbbk", "....kkkkk");
|
|
31
|
-
const runLegs = [
|
|
32
|
-
stride("P", "p"),
|
|
33
|
-
passing("P", "p"),
|
|
34
|
-
stride("p", "P"),
|
|
35
|
-
passing("p", "P"),
|
|
36
|
-
];
|
|
37
|
-
const armForward = upper(10, 13, "cc..", ".cck", "..mm");
|
|
38
|
-
const armBack = upper(7, 13, "..cc", ".cc.", "mm..");
|
|
39
|
-
const speedLines = [
|
|
40
|
-
layer(0, 14, "vv", "", "", "", "v", "", "", "vv"),
|
|
41
|
-
layer(0, 15, "v", "", "", "vv", "", "", "", "v"),
|
|
42
|
-
];
|
|
43
|
-
const table = layer(12, 18, "xxxxxxxx", "......x.", "......x.", "......x.", "......x.", "......x.", "......x.", ".....xxx");
|
|
44
|
-
const laptop = layer(13, 12, "....lL", "....lL", "....lL", "....lL", "....lL", "LLLLLL");
|
|
45
|
-
const typing = [
|
|
46
|
-
upper(10, 13, "cc..", ".cc.", "..cm", "...m"),
|
|
47
|
-
upper(10, 13, "cc..", ".cc.", "..cm", "..m."),
|
|
48
|
-
];
|
|
49
|
-
const codeBits = [
|
|
50
|
-
layer(14, 9, "y", "", "..y", "", ".y"),
|
|
51
|
-
layer(14, 7, ".y", "", "y", "", "..y"),
|
|
52
|
-
layer(15, 5, "y", "", ".y", "", "y"),
|
|
53
|
-
layer(14, 3, "..y", "", "y", "", ".y"),
|
|
54
|
-
];
|
|
55
|
-
const cupLow = upper(10, 13, "cc..", ".ccm", "..QQ", "..Qo");
|
|
56
|
-
const cupRaised = upper(10, 6, "...QQ", "...Qo", "..cm.", ".cc..", ".cc..", ".cc..");
|
|
57
|
-
const steam = [
|
|
58
|
-
layer(12, 12, ".v", "v.", ".v"),
|
|
59
|
-
layer(12, 11, "v.", ".v", "v."),
|
|
60
|
-
layer(14, 3, "v", ".v"),
|
|
61
|
-
];
|
|
62
|
-
const appleAtMouth = upper(10, 5, "...G.", "...FF", "...FF", "..cm.", ".cc..", ".cc..", ".cc..");
|
|
63
|
-
const appleInHand = upper(10, 13, "cc...", ".cmF.", "..FFG");
|
|
64
|
-
const bitten = upper(10, 13, "cc...", ".cmF.", "..F.G");
|
|
65
|
-
const dumbbellUp = upper(13, 0, "DDD", "DdD", "DDD", ".m.", ".c.", ".c.", ".c.", ".c.", ".c.", ".c.", ".c.", ".c.", "cc.");
|
|
66
|
-
const dumbbellRacked = upper(13, 8, "DDD", "DdD", "DDD", ".m.", ".c.", "cc.");
|
|
67
|
-
const sweat = [layer(16, 6, "u"), layer(16, 8, "u")];
|
|
68
|
-
const reachUp = upper(13, 1, ".m", ".c", ".c", ".c", ".c", ".c", ".c", ".c", ".c", ".c", ".c", "cc");
|
|
69
|
-
const reachForward = upper(10, 13, "ccccc", "....cmm");
|
|
70
|
-
const swordRaised = upper(13, 0, ".t.", ".t.", ".t.", ".t.", "ggg", ".m.", ".c.", ".c.", ".c.", ".c.", ".c.", ".c.", "cc.");
|
|
71
|
-
const sparkles = [
|
|
72
|
-
layer(0, 1, ".y..........y", "", "", "", "", "", "y.................y"),
|
|
73
|
-
layer(0, 3, "y.................y", "", "", "", "", "..y...........y"),
|
|
74
|
-
];
|
|
75
|
-
const breathing = (legs, front) => [
|
|
76
|
-
{ legs, behind: [tailA], front },
|
|
77
|
-
{ legs, behind: [tailB], front },
|
|
78
|
-
{ legs, behind: [tailC], front, breathe: true },
|
|
79
|
-
{ legs, behind: [tailB], front, breathe: true },
|
|
80
|
-
{ legs, behind: [tailA], front },
|
|
81
|
-
{ legs, behind: [tailB], front, blink: true },
|
|
82
|
-
{ legs, behind: [tailC], front, breathe: true },
|
|
83
|
-
{ legs, behind: [tailB], front, breathe: true },
|
|
84
|
-
];
|
|
85
|
-
const animations = {
|
|
86
|
-
idle: {
|
|
87
|
-
interval: 220,
|
|
88
|
-
frames: breathing(standingLegs, [hangingArm]),
|
|
89
|
-
},
|
|
90
|
-
focus: {
|
|
91
|
-
interval: 160,
|
|
92
|
-
frames: codeBits.map((bits, index) => ({
|
|
93
|
-
legs: standingLegs,
|
|
94
|
-
behind: [index % 2 === 0 ? tailA : tailB, table],
|
|
95
|
-
front: [laptop, typing[index % 2] ?? hangingArm, bits],
|
|
96
|
-
breathe: index >= 2,
|
|
97
|
-
})),
|
|
98
|
-
},
|
|
99
|
-
break: {
|
|
100
|
-
interval: 380,
|
|
101
|
-
frames: [
|
|
102
|
-
{ legs: standingLegs, behind: [tailA], front: [cupLow, steam[0]] },
|
|
103
|
-
{
|
|
104
|
-
legs: standingLegs,
|
|
105
|
-
behind: [tailB],
|
|
106
|
-
front: [cupLow, steam[1]],
|
|
107
|
-
breathe: true,
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
legs: standingLegs,
|
|
111
|
-
behind: [tailC],
|
|
112
|
-
front: [cupLow, steam[0]],
|
|
113
|
-
breathe: true,
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
legs: standingLegs,
|
|
117
|
-
behind: [tailB],
|
|
118
|
-
front: [cupRaised, steam[2]],
|
|
119
|
-
blink: true,
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
legs: standingLegs,
|
|
123
|
-
behind: [tailA],
|
|
124
|
-
front: [cupRaised, steam[2]],
|
|
125
|
-
blink: true,
|
|
126
|
-
},
|
|
127
|
-
{ legs: standingLegs, behind: [tailB], front: [cupLow, steam[1]] },
|
|
128
|
-
],
|
|
129
|
-
},
|
|
130
|
-
eat: {
|
|
131
|
-
interval: 300,
|
|
132
|
-
frames: [
|
|
133
|
-
{ legs: standingLegs, behind: [tailA], front: [appleInHand] },
|
|
134
|
-
{ legs: standingLegs, behind: [tailB], front: [appleAtMouth] },
|
|
135
|
-
{
|
|
136
|
-
legs: standingLegs,
|
|
137
|
-
behind: [tailC],
|
|
138
|
-
front: [appleAtMouth],
|
|
139
|
-
breathe: true,
|
|
140
|
-
},
|
|
141
|
-
{ legs: standingLegs, behind: [tailB], front: [bitten], breathe: true },
|
|
142
|
-
],
|
|
143
|
-
},
|
|
144
|
-
run: {
|
|
145
|
-
interval: 110,
|
|
146
|
-
frames: runLegs.map((legs, index) => ({
|
|
147
|
-
legs,
|
|
148
|
-
behind: [tailStream, speedLines[index % 2] ?? tailStream],
|
|
149
|
-
front: [index % 2 === 0 ? armForward : armBack],
|
|
150
|
-
dy: index % 2 === 0 ? 0 : -1,
|
|
151
|
-
})),
|
|
152
|
-
},
|
|
153
|
-
lift: {
|
|
154
|
-
interval: 420,
|
|
155
|
-
frames: [
|
|
156
|
-
{ legs: standingLegs, behind: [tailA], front: [dumbbellRacked] },
|
|
157
|
-
{
|
|
158
|
-
legs: standingLegs,
|
|
159
|
-
behind: [tailB],
|
|
160
|
-
front: [dumbbellUp, sweat[0]],
|
|
161
|
-
dy: -1,
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
legs: standingLegs,
|
|
165
|
-
behind: [tailC],
|
|
166
|
-
front: [dumbbellUp, sweat[1]],
|
|
167
|
-
dy: -1,
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
legs: standingLegs,
|
|
171
|
-
behind: [tailB],
|
|
172
|
-
front: [dumbbellRacked],
|
|
173
|
-
breathe: true,
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
},
|
|
177
|
-
stretch: {
|
|
178
|
-
interval: 520,
|
|
179
|
-
frames: [
|
|
180
|
-
{ legs: standingLegs, behind: [tailA], front: [reachUp], dy: -1 },
|
|
181
|
-
{ legs: standingLegs, behind: [tailB], front: [reachUp], dy: -1 },
|
|
182
|
-
{
|
|
183
|
-
legs: standingLegs,
|
|
184
|
-
behind: [tailC],
|
|
185
|
-
front: [reachForward],
|
|
186
|
-
breathe: true,
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
legs: standingLegs,
|
|
190
|
-
behind: [tailB],
|
|
191
|
-
front: [reachForward],
|
|
192
|
-
breathe: true,
|
|
193
|
-
},
|
|
194
|
-
],
|
|
195
|
-
},
|
|
196
|
-
levelUp: {
|
|
197
|
-
interval: 160,
|
|
198
|
-
frames: sparkles.flatMap((burst, index) => [
|
|
199
|
-
{
|
|
200
|
-
legs: standingLegs,
|
|
201
|
-
behind: [tailStream],
|
|
202
|
-
front: [swordRaised, burst],
|
|
203
|
-
dy: -1,
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
legs: standingLegs,
|
|
207
|
-
behind: [index === 0 ? tailA : tailC],
|
|
208
|
-
front: [swordRaised, burst],
|
|
209
|
-
},
|
|
210
|
-
]),
|
|
211
|
-
},
|
|
212
|
-
};
|
|
213
|
-
export const animationOf = (activity) => animations[activity];
|
|
214
|
-
// Each gear slot upgrades once from level 2 and again 4 levels later, staggered
|
|
215
|
-
// so every level from 2 to 9 changes exactly 1 piece.
|
|
1
|
+
// The pet lives on a stage as wide as its panel and 28 pixels tall. Each
|
|
2
|
+
// terminal cell draws 2 stacked pixels with "▀", foreground for the top and
|
|
3
|
+
// background for the bottom, so the stage takes 14 rows.
|
|
4
|
+
export const stageHeight = 28;
|
|
5
|
+
export const defaultLook = { size: 1, build: "average", gender: "other" };
|
|
6
|
+
export function lookOf(profile) {
|
|
7
|
+
if (!profile) {
|
|
8
|
+
return defaultLook;
|
|
9
|
+
}
|
|
10
|
+
const bmi = profile.weight / (profile.height / 100) ** 2;
|
|
11
|
+
let build = "average";
|
|
12
|
+
if (bmi < 20) {
|
|
13
|
+
build = "slim";
|
|
14
|
+
}
|
|
15
|
+
else if (bmi >= 27) {
|
|
16
|
+
build = "broad";
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
size: Math.min(Math.max(Math.floor((profile.height - 150) / 12), 0), 3),
|
|
20
|
+
build,
|
|
21
|
+
gender: profile.gender,
|
|
22
|
+
goal: profile.goal,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export const noBadges = { meal: false, workout: false, focus: false };
|
|
26
|
+
// Each equipment slot upgrades once from level 2 and again 4 levels later,
|
|
27
|
+
// staggered so every level from 2 to 9 changes exactly 1 piece.
|
|
216
28
|
export const gearOf = (level) => {
|
|
217
29
|
const tier = (slot) => Math.min(Math.max(Math.floor((level - 2 - slot) / 4) + 1, 0), 2);
|
|
218
|
-
return {
|
|
30
|
+
return { gadget: tier(0), wear: tier(1), headwear: tier(2), aura: tier(3) };
|
|
31
|
+
};
|
|
32
|
+
// The pet grows twice: a hatchling below level 4, an adult from level 7.
|
|
33
|
+
export const growthOf = (level) => level >= 7 ? 2 : level >= 4 ? 1 : 0;
|
|
34
|
+
const widthOf = { slim: 13, average: 15, broad: 18 };
|
|
35
|
+
const shapeOf = (look, level, squash = 0) => {
|
|
36
|
+
const growth = growthOf(level) - 1;
|
|
37
|
+
const width = widthOf[look.build] + growth * 2 - squash;
|
|
38
|
+
const height = 9 + look.size + growth * 2 + squash;
|
|
39
|
+
return { width, height, top: height + 1 };
|
|
40
|
+
};
|
|
41
|
+
const span = (put, from, to, y, key) => {
|
|
42
|
+
for (let x = from; x <= to; x++) {
|
|
43
|
+
put(x, y, key);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const icon = (rows, x, y) => (put) => {
|
|
47
|
+
for (const [offset, row] of rows.entries()) {
|
|
48
|
+
for (const [index, key] of [...row].entries()) {
|
|
49
|
+
put(x + index, y - offset, key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const sparkles = (points) => (put, shape) => {
|
|
54
|
+
for (const [x, y] of points) {
|
|
55
|
+
put(x < 0 ? x : shape.width - 1 + x, y + shape.top, "y");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const laptop = (put, { width }) => {
|
|
59
|
+
span(put, width + 3, width + 8, 0, "x");
|
|
60
|
+
for (let y = 1; y <= 2; y++) {
|
|
61
|
+
put(width + 7, y, "x");
|
|
62
|
+
}
|
|
63
|
+
span(put, width + 3, width + 7, 3, "L");
|
|
64
|
+
for (let y = 4; y <= 7; y++) {
|
|
65
|
+
put(width + 6, y, "l");
|
|
66
|
+
put(width + 7, y, "L");
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const held = (rows, dy) => (put, shape) => {
|
|
70
|
+
icon(rows, shape.width + 2, dy(shape))(put, shape, 0);
|
|
219
71
|
};
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
72
|
+
// Where the pet stands and what it does on every tick of `counter`.
|
|
73
|
+
function sceneOf(activity, counter, width, shape, mood) {
|
|
74
|
+
const home = 3;
|
|
75
|
+
const minimum = 3;
|
|
76
|
+
const maximum = Math.max(width - shape.width - 3, minimum + 1);
|
|
77
|
+
const travel = maximum - minimum;
|
|
78
|
+
const tick = counter % 4;
|
|
79
|
+
if (activity === "idle" && mood === "sleepy") {
|
|
80
|
+
// Napping in place: breathing, with Zs drifting up.
|
|
81
|
+
return {
|
|
82
|
+
x: Math.floor((width - shape.width) / 2),
|
|
83
|
+
facing: 1,
|
|
84
|
+
pose: { squash: tick < 2 ? 0 : 1, eyes: "closed" },
|
|
85
|
+
props: [
|
|
86
|
+
(put, { width: body, top }) => {
|
|
87
|
+
const rise = counter % 6;
|
|
88
|
+
icon(["zzzz", "..z.", ".z..", "zzzz"], body - 2 + rise, top + 4 + rise)(put, shape, 0);
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (activity === "idle") {
|
|
94
|
+
// Wander: pause, walk across, pause and look about, walk back.
|
|
95
|
+
const pause = 6;
|
|
96
|
+
const cycle = 2 * (pause + travel);
|
|
97
|
+
const at = counter % cycle;
|
|
98
|
+
const isHappy = mood === "happy";
|
|
99
|
+
const walk = (step) => ({
|
|
100
|
+
step: step % 2 === 0 ? 1 : 2,
|
|
101
|
+
look: 1,
|
|
102
|
+
lift: isHappy && step % 2 === 1 ? 1 : 0,
|
|
103
|
+
eyes: isHappy ? "happy" : "open",
|
|
104
|
+
});
|
|
105
|
+
const rest = (moment) => ({
|
|
106
|
+
squash: Math.floor(moment / 2) % 2,
|
|
107
|
+
eyes: isHappy ? "happy" : moment === 4 ? "closed" : "open",
|
|
108
|
+
arms: isHappy && moment % 2 === 0 ? ["up", "up"] : undefined,
|
|
109
|
+
});
|
|
110
|
+
if (at < pause) {
|
|
111
|
+
return {
|
|
112
|
+
x: minimum,
|
|
113
|
+
facing: at === 2 || at === 3 ? -1 : 1,
|
|
114
|
+
pose: rest(at),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (at < pause + travel) {
|
|
118
|
+
return { x: minimum + at - pause, facing: 1, pose: walk(at - pause) };
|
|
119
|
+
}
|
|
120
|
+
if (at < 2 * pause + travel) {
|
|
121
|
+
const moment = at - pause - travel;
|
|
122
|
+
return {
|
|
123
|
+
x: maximum,
|
|
124
|
+
facing: moment === 2 || moment === 3 ? 1 : -1,
|
|
125
|
+
pose: rest(moment),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const step = at - 2 * pause - travel;
|
|
129
|
+
return { x: maximum - step, facing: -1, pose: walk(step) };
|
|
130
|
+
}
|
|
131
|
+
switch (activity) {
|
|
132
|
+
case "run": {
|
|
133
|
+
const lap = Math.max(Math.ceil(travel / 2), 1);
|
|
134
|
+
const at = counter % (2 * lap);
|
|
135
|
+
const forward = at < lap;
|
|
136
|
+
const step = forward ? at : at - lap;
|
|
137
|
+
return {
|
|
138
|
+
x: forward
|
|
139
|
+
? Math.min(minimum + step * 2, maximum)
|
|
140
|
+
: Math.max(maximum - step * 2, minimum),
|
|
141
|
+
facing: forward ? 1 : -1,
|
|
142
|
+
pose: {
|
|
143
|
+
step: counter % 2 === 0 ? 1 : 2,
|
|
144
|
+
lift: counter % 2,
|
|
145
|
+
look: 1,
|
|
146
|
+
arms: counter % 2 === 0 ? ["up", "down"] : ["down", "up"],
|
|
147
|
+
},
|
|
148
|
+
props: [icon(["vv.", "...", ".vv"], -6, 6)],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
case "focus": {
|
|
152
|
+
return {
|
|
153
|
+
x: home,
|
|
154
|
+
facing: 1,
|
|
155
|
+
pose: {
|
|
156
|
+
look: 1,
|
|
157
|
+
squash: tick < 2 ? 0 : 1,
|
|
158
|
+
arms: ["down", tick % 2 === 0 ? "forward" : "mid"],
|
|
159
|
+
},
|
|
160
|
+
props: [
|
|
161
|
+
laptop,
|
|
162
|
+
(put, { width: body }) => {
|
|
163
|
+
put(body + 5 + (tick % 2), 9 + tick, "y");
|
|
164
|
+
put(body + 7 - (tick % 2), 9 + ((tick + 2) % 4), "y");
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
case "break": {
|
|
170
|
+
const step = counter % 6;
|
|
171
|
+
const sipping = step >= 4;
|
|
172
|
+
return {
|
|
173
|
+
x: home,
|
|
174
|
+
facing: 1,
|
|
175
|
+
pose: {
|
|
176
|
+
look: 1,
|
|
177
|
+
squash: step % 2,
|
|
178
|
+
eyes: sipping ? "closed" : "open",
|
|
179
|
+
arms: ["down", sipping ? "up" : "mid"],
|
|
180
|
+
},
|
|
181
|
+
props: [
|
|
182
|
+
held(["QQ", "Qc"], (shape) => sipping ? shape.top - 1 : Math.ceil(shape.height / 2) + 2),
|
|
183
|
+
...(sipping
|
|
184
|
+
? []
|
|
185
|
+
: [
|
|
186
|
+
held(step % 2 === 0 ? [".v", "v."] : ["v.", ".v"], (shape) => Math.ceil(shape.height / 2) + 5),
|
|
187
|
+
]),
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
case "eat": {
|
|
192
|
+
const atMouth = tick === 1 || tick === 2;
|
|
193
|
+
return {
|
|
194
|
+
x: home,
|
|
195
|
+
facing: 1,
|
|
196
|
+
pose: {
|
|
197
|
+
look: 1,
|
|
198
|
+
squash: tick === 2 ? 1 : 0,
|
|
199
|
+
eyes: tick === 2 ? "happy" : "open",
|
|
200
|
+
arms: ["down", atMouth ? "up" : "mid"],
|
|
201
|
+
},
|
|
202
|
+
props: [
|
|
203
|
+
held(tick === 3 ? [".G", "F.", "FF"] : [".G", "FF", "FF"], (shape) => atMouth ? shape.top - 2 : Math.ceil(shape.height / 2) + 2),
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
case "lift": {
|
|
208
|
+
const up = tick === 1 || tick === 2;
|
|
209
|
+
return {
|
|
210
|
+
x: home,
|
|
211
|
+
facing: 1,
|
|
212
|
+
pose: {
|
|
213
|
+
arms: up ? ["up", "up"] : ["mid", "mid"],
|
|
214
|
+
lift: tick === 2 ? 1 : 0,
|
|
215
|
+
squash: up ? 0 : 1,
|
|
216
|
+
},
|
|
217
|
+
props: [
|
|
218
|
+
(put, shape) => {
|
|
219
|
+
const y = up ? shape.top + 3 : Math.ceil(shape.height / 2) + 4;
|
|
220
|
+
span(put, -3, shape.width + 2, y, "d");
|
|
221
|
+
icon(["DD", "DD", "DD"], -4, y + 1)(put, shape, 0);
|
|
222
|
+
icon(["DD", "DD", "DD"], shape.width + 2, y + 1)(put, shape, 0);
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
case "stretch": {
|
|
228
|
+
return {
|
|
229
|
+
x: home,
|
|
230
|
+
facing: 1,
|
|
231
|
+
pose: {
|
|
232
|
+
arms: tick < 2 ? ["up", "up"] : ["mid", "mid"],
|
|
233
|
+
squash: tick < 2 ? -1 : 1,
|
|
234
|
+
eyes: tick === 1 ? "closed" : "open",
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
case "levelUp": {
|
|
239
|
+
return {
|
|
240
|
+
x: home,
|
|
241
|
+
facing: 1,
|
|
242
|
+
pose: {
|
|
243
|
+
arms: ["up", "up"],
|
|
244
|
+
lift: [0, 2, 3, 1][tick] ?? 0,
|
|
245
|
+
eyes: "happy",
|
|
246
|
+
},
|
|
247
|
+
props: [
|
|
248
|
+
sparkles(tick % 2 === 0
|
|
249
|
+
? [
|
|
250
|
+
[-3, 2],
|
|
251
|
+
[3, 4],
|
|
252
|
+
[-4, -3],
|
|
253
|
+
[4, -1],
|
|
254
|
+
]
|
|
255
|
+
: [
|
|
256
|
+
[-4, 4],
|
|
257
|
+
[4, 2],
|
|
258
|
+
[-3, -1],
|
|
259
|
+
[3, -3],
|
|
260
|
+
]),
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
export const intervalOf = (activity) => ({
|
|
267
|
+
idle: 220,
|
|
268
|
+
focus: 180,
|
|
269
|
+
break: 380,
|
|
270
|
+
eat: 300,
|
|
271
|
+
run: 110,
|
|
272
|
+
lift: 400,
|
|
273
|
+
stretch: 480,
|
|
274
|
+
levelUp: 160,
|
|
275
|
+
})[activity];
|
|
276
|
+
// How many ticks one loop of an activity takes on a stage of `width`.
|
|
277
|
+
export function cycleOf(activity, width) {
|
|
278
|
+
const travel = Math.max(width - 21, 4);
|
|
279
|
+
if (activity === "idle") {
|
|
280
|
+
return 2 * (6 + travel);
|
|
281
|
+
}
|
|
282
|
+
if (activity === "run") {
|
|
283
|
+
return 2 * Math.max(Math.ceil(travel / 2), 1);
|
|
284
|
+
}
|
|
285
|
+
return activity === "break" ? 6 : 4;
|
|
286
|
+
}
|
|
287
|
+
// Behind the body: a cape that flares and ripples, or sparkles of the aura.
|
|
288
|
+
function paintBehind({ put, shape, gear, counter }) {
|
|
289
|
+
if (gear.wear === 2) {
|
|
290
|
+
for (let y = 2; y <= shape.top - 2; y++) {
|
|
291
|
+
const flare = Math.floor((shape.top - y) / 3) + ((counter + y) % 4 < 2 ? 0 : 1);
|
|
292
|
+
span(put, -1 - flare, -1, y, y === 2 ? "s" : "S");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (gear.aura === 2) {
|
|
296
|
+
const sway = counter % 2;
|
|
297
|
+
put(-3 - sway, shape.top - 1, "y");
|
|
298
|
+
put(shape.width + 2 + sway, shape.top - 3, "y");
|
|
299
|
+
put(-2, 3 + sway, "y");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// 4 short legs, like the Claude Code mascot; sneakers for an endurance goal.
|
|
303
|
+
function paintLegs({ put, shape, look, pose }) {
|
|
304
|
+
const columns = [1, 3, shape.width - 4, shape.width - 2];
|
|
305
|
+
const foot = look.goal === "endurance" ? "w" : "o";
|
|
306
|
+
for (const [index, x] of columns.entries()) {
|
|
307
|
+
const isLifted = (pose.step === 1 && index % 2 === 0) ||
|
|
308
|
+
(pose.step === 2 && index % 2 === 1);
|
|
309
|
+
if (!isLifted) {
|
|
310
|
+
put(x, 0, foot);
|
|
311
|
+
}
|
|
312
|
+
put(x, 1, "o");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function paintBody({ put, shape, look, gear }) {
|
|
316
|
+
const { width, top } = shape;
|
|
317
|
+
for (let y = 2; y <= top; y++) {
|
|
318
|
+
const isEdge = y === 2 || y === top;
|
|
319
|
+
span(put, isEdge ? 1 : 0, isEdge ? width - 2 : width - 1, y, y === 2 ? "o" : "O");
|
|
320
|
+
}
|
|
321
|
+
span(put, 1, 2, top - 1, "h");
|
|
322
|
+
if (look.goal === "loseFat") {
|
|
323
|
+
span(put, 0, width - 1, top - 1, "w");
|
|
324
|
+
}
|
|
325
|
+
else if (look.goal === "maintain") {
|
|
326
|
+
const middle = Math.floor(width / 2);
|
|
327
|
+
icon(["r.r", "rrr", ".r."], middle - 1, 6)(put, shape, 0);
|
|
328
|
+
}
|
|
329
|
+
if (gear.wear === 1) {
|
|
330
|
+
span(put, 0, width - 1, top - 5, "S");
|
|
331
|
+
put(-1, top - 5, "S");
|
|
332
|
+
put(-2, top - 6, "s");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function paintFace({ put, shape, pose, counter }) {
|
|
336
|
+
const look = pose.look ?? 0;
|
|
337
|
+
const eyeY = shape.top - 3;
|
|
338
|
+
const eyes = [
|
|
339
|
+
Math.round(shape.width * 0.3) + look,
|
|
340
|
+
Math.round(shape.width * 0.7) - 1 + look,
|
|
341
|
+
];
|
|
342
|
+
for (const x of eyes) {
|
|
343
|
+
if (pose.eyes === "closed") {
|
|
344
|
+
span(put, x, x + 1, eyeY, "e");
|
|
345
|
+
}
|
|
346
|
+
else if (pose.eyes === "happy") {
|
|
347
|
+
put(x - 1, eyeY, "e");
|
|
348
|
+
put(x, eyeY + 1, "e");
|
|
349
|
+
put(x + 1, eyeY, "e");
|
|
350
|
+
put(x, eyeY - 2, "r");
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
put(x, eyeY, "e");
|
|
354
|
+
put(x, eyeY + 1, counter % 23 === 0 ? "O" : "e");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Side nubs; a muscle goal makes them bigger.
|
|
359
|
+
function paintArms({ put, shape, look, pose }) {
|
|
360
|
+
const size = look.goal === "buildMuscle" ? 3 : 2;
|
|
361
|
+
const [back, front] = pose.arms ?? ["down", "down"];
|
|
362
|
+
const rowOf = (arm) => ({
|
|
363
|
+
down: 3 + size - 1,
|
|
364
|
+
mid: Math.ceil(shape.height / 2) + size,
|
|
365
|
+
up: shape.top + size - 1,
|
|
366
|
+
forward: Math.ceil(shape.height / 2) + size,
|
|
367
|
+
})[arm];
|
|
368
|
+
const nub = (x, arm) => {
|
|
369
|
+
const reach = arm === "forward" ? 1 : 0;
|
|
370
|
+
for (let row = 0; row < size; row++) {
|
|
371
|
+
span(put, x + reach, x + reach + size - 1, rowOf(arm) - row, row === size - 1 ? "o" : "O");
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
nub(-size, back);
|
|
375
|
+
nub(shape.width, front);
|
|
376
|
+
}
|
|
377
|
+
// Gender sets an ornament at the back of the head, level the gear on top.
|
|
378
|
+
function paintHead({ put, shape, look, gear, counter }) {
|
|
379
|
+
const { width, top } = shape;
|
|
380
|
+
if (look.gender === "female") {
|
|
381
|
+
icon(["r.r", "rrr", "r.r"], 0, top + 3)(put, shape, 0);
|
|
382
|
+
}
|
|
383
|
+
else if (look.gender === "male") {
|
|
384
|
+
put(1, top + 1, "O");
|
|
385
|
+
put(2, top + 2, "O");
|
|
386
|
+
put(3, top + 1, "O");
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
put(2, top + 1, "G");
|
|
390
|
+
put(1, top + 2, "G");
|
|
391
|
+
put(3, top + 3, "G");
|
|
392
|
+
}
|
|
393
|
+
if (gear.gadget >= 1) {
|
|
394
|
+
span(put, 5, width - 2, top + 1, "k");
|
|
395
|
+
icon(["S", "S"], -1, top - 1)(put, shape, 0);
|
|
396
|
+
icon(["S", "S"], width, top - 1)(put, shape, 0);
|
|
397
|
+
}
|
|
398
|
+
if (gear.gadget === 2) {
|
|
399
|
+
put(width - 3, top + 2, "k");
|
|
400
|
+
put(width - 3, top + 3, counter % 2 === 0 ? "t" : "y");
|
|
401
|
+
}
|
|
402
|
+
if (gear.headwear === 1) {
|
|
403
|
+
span(put, 5, width - 3, top + 1, "S");
|
|
404
|
+
span(put, 6, width - 4, top + 2, "S");
|
|
405
|
+
span(put, width - 2, width, top + 1, "s");
|
|
406
|
+
}
|
|
407
|
+
else if (gear.headwear === 2) {
|
|
408
|
+
span(put, 6, width - 3, top + 1, "g");
|
|
409
|
+
put(6, top + 2, "g");
|
|
410
|
+
put(Math.floor((width + 3) / 2), top + 2, "g");
|
|
411
|
+
put(width - 3, top + 2, "g");
|
|
412
|
+
put(Math.floor((width + 3) / 2), top + 1, "t");
|
|
413
|
+
}
|
|
414
|
+
if (gear.aura >= 1) {
|
|
415
|
+
const bob = counter % 4 < 2 ? 0 : 1;
|
|
416
|
+
put(width + 4, top + 2 + bob, "t");
|
|
417
|
+
put(width + 4, top + 3 + bob, "y");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
export function paletteOf(palette) {
|
|
421
|
+
const { cloak, cloakShade, glow } = palette.sprite;
|
|
246
422
|
return new Map([
|
|
247
|
-
["
|
|
248
|
-
["a", armor],
|
|
249
|
-
["b", "#3B2D25"],
|
|
250
|
-
["C", cloak],
|
|
251
|
-
["c", cloakShade],
|
|
423
|
+
["c", "#5A3A25"],
|
|
252
424
|
["D", "#50555F"],
|
|
253
425
|
["d", "#8A8F99"],
|
|
254
|
-
["
|
|
426
|
+
["e", "#1E1E1E"],
|
|
255
427
|
["F", "#C8473F"],
|
|
256
|
-
["
|
|
257
|
-
["
|
|
258
|
-
["
|
|
259
|
-
["
|
|
260
|
-
["h", cloakShade],
|
|
261
|
-
["k", "#15121A"],
|
|
428
|
+
["G", "#6FA35A"],
|
|
429
|
+
["g", "#D9B45A"],
|
|
430
|
+
["h", palette.soft],
|
|
431
|
+
["k", "#2B2B2B"],
|
|
262
432
|
["L", "#4A505B"],
|
|
263
433
|
["l", glow],
|
|
264
|
-
["
|
|
265
|
-
["
|
|
266
|
-
["n", "#4A3628"],
|
|
267
|
-
["o", "#5A3A25"],
|
|
268
|
-
["P", "#23252F"],
|
|
269
|
-
["p", "#2E3140"],
|
|
434
|
+
["O", palette.accent],
|
|
435
|
+
["o", palette.deep],
|
|
270
436
|
["Q", "#E6E0D4"],
|
|
271
|
-
["
|
|
272
|
-
["
|
|
437
|
+
["r", "#E87A90"],
|
|
438
|
+
["S", cloak],
|
|
439
|
+
["s", cloakShade],
|
|
440
|
+
["T", "#E5533D"],
|
|
273
441
|
["t", glow],
|
|
274
|
-
["u", "#8FC6E8"],
|
|
275
442
|
["v", "#9AA0A8"],
|
|
276
|
-
["
|
|
277
|
-
["w", "#C9D3DE"],
|
|
443
|
+
["w", "#F2F2F2"],
|
|
278
444
|
["x", "#3D414B"],
|
|
279
|
-
["Y", "#E8DCC0"],
|
|
280
445
|
["y", palette.soft],
|
|
446
|
+
["z", "#C8C8C8"],
|
|
281
447
|
]);
|
|
282
448
|
}
|
|
283
|
-
// The pixel keys of 1 frame, row by row, "." where nothing is drawn.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (!frame) {
|
|
291
|
-
return canvas.map((row) => row.join(""));
|
|
292
|
-
}
|
|
293
|
-
const lowerDy = frame.dy ?? 0;
|
|
294
|
-
const upperDy = lowerDy + (frame.breathe ? 1 : 0);
|
|
295
|
-
const head = frame.blink
|
|
296
|
-
? { ...hood, rows: hood.rows.map((row) => row.replaceAll("E", "f")) }
|
|
297
|
-
: hood;
|
|
298
|
-
const layers = [
|
|
299
|
-
weapons[gear.weapon],
|
|
300
|
-
...(frame.behind ?? []),
|
|
301
|
-
frame.legs,
|
|
302
|
-
torso,
|
|
303
|
-
neckScarf,
|
|
304
|
-
head,
|
|
305
|
-
hoods[gear.hood],
|
|
306
|
-
pauldron,
|
|
307
|
-
...(frame.front ?? []),
|
|
308
|
-
...(relics[gear.relic]?.(orbBob[counter % orbBob.length] ?? 0) ?? []),
|
|
309
|
-
];
|
|
310
|
-
for (const item of layers) {
|
|
311
|
-
if (!item) {
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
const dy = item.upper ? upperDy : lowerDy;
|
|
315
|
-
for (const [offset, row] of item.rows.entries()) {
|
|
316
|
-
const target = canvas[item.y + offset + dy];
|
|
317
|
-
for (const [index, key] of [...row].entries()) {
|
|
318
|
-
if (target && key !== "." && item.x + index < spriteWidth) {
|
|
319
|
-
target[item.x + index] = key;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
449
|
+
// The pixel keys of 1 frame, row by row, "." where nothing is drawn.
|
|
450
|
+
export function composeScene({ activity, counter, level, look, mood, badges, width, }) {
|
|
451
|
+
const canvas = Array.from({ length: stageHeight }, () => Array.from({ length: width }, () => "."));
|
|
452
|
+
const plain = (column, y, key) => {
|
|
453
|
+
const row = canvas[stageHeight - 1 - y];
|
|
454
|
+
if (row && column >= 0 && column < width && key !== ".") {
|
|
455
|
+
row[column] = key;
|
|
322
456
|
}
|
|
457
|
+
};
|
|
458
|
+
const baseShape = shapeOf(look, level);
|
|
459
|
+
const scene = sceneOf(activity, counter, width, baseShape, mood);
|
|
460
|
+
const shape = shapeOf(look, level, scene.pose.squash ?? 0);
|
|
461
|
+
const lift = scene.pose.lift ?? 0;
|
|
462
|
+
// Local x runs from the body's left edge toward where the pet faces.
|
|
463
|
+
const put = (x, y, key) => {
|
|
464
|
+
const column = scene.facing === 1 ? scene.x + x : scene.x + baseShape.width - 1 - x;
|
|
465
|
+
plain(column, y + lift, key);
|
|
466
|
+
};
|
|
467
|
+
const paint = {
|
|
468
|
+
put,
|
|
469
|
+
shape,
|
|
470
|
+
look,
|
|
471
|
+
gear: gearOf(level),
|
|
472
|
+
pose: scene.pose,
|
|
473
|
+
counter,
|
|
474
|
+
};
|
|
475
|
+
paintBehind(paint);
|
|
476
|
+
paintLegs(paint);
|
|
477
|
+
paintBody(paint);
|
|
478
|
+
paintFace(paint);
|
|
479
|
+
paintArms(paint);
|
|
480
|
+
paintHead(paint);
|
|
481
|
+
for (const draw of scene.props ?? []) {
|
|
482
|
+
draw(put, shape, counter);
|
|
483
|
+
}
|
|
484
|
+
// Today's badges float in the top right corner, whatever the pet does.
|
|
485
|
+
const icons = [
|
|
486
|
+
badges.focus ? ["GGG", "TTT", "TTT"] : undefined,
|
|
487
|
+
badges.meal ? [".G.", "FFF", ".F."] : undefined,
|
|
488
|
+
badges.workout ? ["D.D", "DdD", "D.D"] : undefined,
|
|
489
|
+
].filter((rows) => rows !== undefined);
|
|
490
|
+
for (const [index, rows] of icons.entries()) {
|
|
491
|
+
icon(rows, width - 4 - index * 4, stageHeight - 2)(plain, shape, 0);
|
|
323
492
|
}
|
|
324
493
|
return canvas.map((row) => row.join(""));
|
|
325
494
|
}
|
|
326
495
|
// Pairs of pixel rows become 1 row of "▀"/"▄" cells, merged into styled runs.
|
|
327
496
|
export function toSegments(frame, palette) {
|
|
328
497
|
const lines = [];
|
|
498
|
+
const width = frame[0]?.length ?? 0;
|
|
329
499
|
for (let y = 0; y < frame.length; y += 2) {
|
|
330
500
|
const top = frame[y] ?? "";
|
|
331
501
|
const bottom = frame[y + 1] ?? "";
|
|
332
502
|
const line = [];
|
|
333
|
-
for (let x = 0; x <
|
|
503
|
+
for (let x = 0; x < width; x++) {
|
|
334
504
|
const upperColor = palette.get(top[x] ?? ".");
|
|
335
505
|
const lowerColor = palette.get(bottom[x] ?? ".");
|
|
336
506
|
const cell = upperColor === undefined
|