drexler 0.2.17 → 0.2.18
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/package.json +1 -1
- package/src/ui/App.tsx +4 -9
- package/src/ui/MascotIntro.tsx +4 -7
- package/src/ui/PetPanel.tsx +264 -307
package/package.json
CHANGED
package/src/ui/App.tsx
CHANGED
|
@@ -326,12 +326,7 @@ export function App({
|
|
|
326
326
|
const petActivityTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
327
327
|
const petDecayTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
328
328
|
|
|
329
|
-
const petEnv
|
|
330
|
-
const h = new Date().getHours();
|
|
331
|
-
if (h >= 9 && h < 18) return "office";
|
|
332
|
-
if (h >= 6 && h < 23) return "home";
|
|
333
|
-
return "outdoors";
|
|
334
|
-
}, []);
|
|
329
|
+
const petEnv: Environment = "office";
|
|
335
330
|
|
|
336
331
|
const PET_MESSAGES = useMemo(() => ({
|
|
337
332
|
feed: [
|
|
@@ -1157,7 +1152,7 @@ export function App({
|
|
|
1157
1152
|
const isBusy =
|
|
1158
1153
|
requestInFlight || streaming !== null || thinking !== null || synergyEvent !== null;
|
|
1159
1154
|
const headerStatus = isBusy ? "streaming" : deskStatus;
|
|
1160
|
-
const renderDealDeskHeader = (width: number) => (
|
|
1155
|
+
const renderDealDeskHeader = (width: number, marginBottom = 1) => (
|
|
1161
1156
|
<DealDeskHeader
|
|
1162
1157
|
mood={mood}
|
|
1163
1158
|
messageCount={msgCount}
|
|
@@ -1165,7 +1160,7 @@ export function App({
|
|
|
1165
1160
|
compact={isCompact}
|
|
1166
1161
|
notice={!introActive ? deskNotice ?? undefined : undefined}
|
|
1167
1162
|
maxWidth={Math.max(1, width)}
|
|
1168
|
-
marginBottom={
|
|
1163
|
+
marginBottom={marginBottom}
|
|
1169
1164
|
/>
|
|
1170
1165
|
);
|
|
1171
1166
|
const dealDeskHeader = renderDealDeskHeader(chromeWidth);
|
|
@@ -1198,7 +1193,7 @@ export function App({
|
|
|
1198
1193
|
bar={introActive ? intro.bar : undefined}
|
|
1199
1194
|
barColor={introActive ? introBarColor : undefined}
|
|
1200
1195
|
mascotStatus={introActive ? intro.status : undefined}
|
|
1201
|
-
dealDesk={renderDealDeskHeader}
|
|
1196
|
+
dealDesk={(width) => renderDealDeskHeader(width, 0)}
|
|
1202
1197
|
/>
|
|
1203
1198
|
</Box>
|
|
1204
1199
|
) : showFallbackPetPanel ? (
|
package/src/ui/MascotIntro.tsx
CHANGED
|
@@ -171,7 +171,7 @@ const RIGHT_COLUMN_PAD_RIGHT = 1;
|
|
|
171
171
|
const LEFT_PANEL_MIN_COPY = 24;
|
|
172
172
|
const PET_STATS_MIN_WIDTH = 24;
|
|
173
173
|
const PET_STATS_MAX_WIDTH = 58;
|
|
174
|
-
const PET_SPLIT_DIVIDER_HEIGHT =
|
|
174
|
+
const PET_SPLIT_DIVIDER_HEIGHT = 15;
|
|
175
175
|
const PET_SPLIT_DIVIDER_ROWS: number[] = Array.from(
|
|
176
176
|
{ length: PET_SPLIT_DIVIDER_HEIGHT },
|
|
177
177
|
(_, i) => i,
|
|
@@ -846,13 +846,14 @@ function PetSceneReadout({
|
|
|
846
846
|
return (
|
|
847
847
|
<Box flexDirection="column" width={safeWidth} alignItems="center">
|
|
848
848
|
<Text bold color={t.primaryLight}>
|
|
849
|
-
{fitDisplayText(
|
|
849
|
+
{fitDisplayText("Drexler Pet Desk [office]", safeWidth)}
|
|
850
850
|
</Text>
|
|
851
851
|
<PetScene
|
|
852
852
|
stats={stats}
|
|
853
853
|
activity={activity}
|
|
854
854
|
env={env}
|
|
855
855
|
isPaused={isPaused}
|
|
856
|
+
width={safeWidth}
|
|
856
857
|
/>
|
|
857
858
|
</Box>
|
|
858
859
|
);
|
|
@@ -938,12 +939,10 @@ function PetDashboardStatBar({
|
|
|
938
939
|
function PetStatsReadout({
|
|
939
940
|
stats,
|
|
940
941
|
activity,
|
|
941
|
-
env,
|
|
942
942
|
width,
|
|
943
943
|
}: {
|
|
944
944
|
stats: PetStats;
|
|
945
945
|
activity: PetActivity;
|
|
946
|
-
env: Environment;
|
|
947
946
|
width: number;
|
|
948
947
|
}) {
|
|
949
948
|
const t = useTheme();
|
|
@@ -1007,7 +1006,7 @@ function PetStatsReadout({
|
|
|
1007
1006
|
color={t.primaryLight}
|
|
1008
1007
|
/>
|
|
1009
1008
|
<PetStatsBodyLine
|
|
1010
|
-
text={`activity ${activityLabel} ·
|
|
1009
|
+
text={`activity ${activityLabel} · office`}
|
|
1011
1010
|
width={panelWidth}
|
|
1012
1011
|
color={t.dim}
|
|
1013
1012
|
/>
|
|
@@ -1130,7 +1129,6 @@ function PetDashboard({
|
|
|
1130
1129
|
<PetStatsReadout
|
|
1131
1130
|
stats={stats}
|
|
1132
1131
|
activity={activity}
|
|
1133
|
-
env={env}
|
|
1134
1132
|
width={Math.min(PET_STATS_MAX_WIDTH, layout.dealDesk.width)}
|
|
1135
1133
|
/>
|
|
1136
1134
|
</Box>
|
|
@@ -1141,7 +1139,6 @@ function PetDashboard({
|
|
|
1141
1139
|
<PetStatsReadout
|
|
1142
1140
|
stats={stats}
|
|
1143
1141
|
activity={activity}
|
|
1144
|
-
env={env}
|
|
1145
1142
|
width={Math.max(COMPACT_PET_PANEL_MIN_WIDTH, layout.tips.width)}
|
|
1146
1143
|
/>
|
|
1147
1144
|
</Box>
|
package/src/ui/PetPanel.tsx
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { Box, Text } from "ink";
|
|
2
2
|
import { memo, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { getPetMood, type PetActivity, type PetStats } from "../pet/petState.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
BRIEFCASE_FINAL,
|
|
6
|
+
MASCOT_WIDTH,
|
|
7
|
+
renderMascotLines,
|
|
8
|
+
type MascotState,
|
|
9
|
+
} from "./MascotFrame.tsx";
|
|
10
|
+
import { displayWidth, fitDisplayText } from "./graphemes.ts";
|
|
5
11
|
import { useTheme } from "./ThemeContext.tsx";
|
|
6
12
|
import { type Theme } from "./themes.ts";
|
|
7
13
|
|
|
@@ -12,28 +18,17 @@ export type Environment = "office" | "home" | "outdoors";
|
|
|
12
18
|
|
|
13
19
|
const PANEL_BORDER_COLUMNS = 2;
|
|
14
20
|
const PANEL_PADDING_COLUMNS = 2;
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
export const PET_SCENE_WIDTH = CONTENT;
|
|
26
|
-
|
|
27
|
-
// Fixed sprite X — no left/right walking
|
|
28
|
-
const SPRITE_X: Record<PetActivity, number> = {
|
|
29
|
-
idle: 12, eating: 3, playing: 12, working: 18,
|
|
30
|
-
sleeping: 12, praised: 12, vibing: 12,
|
|
31
|
-
};
|
|
21
|
+
const SCENE_ROWS = 14;
|
|
22
|
+
const R_WALL = 0;
|
|
23
|
+
const R_WINDOW_TOP = 1;
|
|
24
|
+
const R_WINDOW_BOTTOM = 3;
|
|
25
|
+
const R_ACTIVITY = 4;
|
|
26
|
+
const R_MASCOT_START = 5;
|
|
27
|
+
const R_DESK_SURFACE = R_MASCOT_START + BRIEFCASE_FINAL.length;
|
|
28
|
+
const R_DESK_FRONT = R_DESK_SURFACE + 1;
|
|
29
|
+
|
|
30
|
+
export const PET_SCENE_WIDTH = 52;
|
|
32
31
|
|
|
33
|
-
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
34
|
-
// All scene glyphs (box-drawing + ASCII) are BMP single-units, so string
|
|
35
|
-
// splicing is safe and avoids the per-call char-array allocation `[...base]`
|
|
36
|
-
// would do on the hot frame loop.
|
|
37
32
|
function place(base: string, text: string, x: number): string {
|
|
38
33
|
if (x < 0 || x >= base.length) return base;
|
|
39
34
|
const end = Math.min(base.length, x + text.length);
|
|
@@ -41,321 +36,284 @@ function place(base: string, text: string, x: number): string {
|
|
|
41
36
|
return base.slice(0, x) + fit + base.slice(end);
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
i % 4 === 0 ? "░" : i % 4 === 2 ? "▒" : "─",
|
|
47
|
-
).join("");
|
|
48
|
-
|
|
49
|
-
// ─── sprite (8 wide × 6 tall) ────────────────────────────────────────────────
|
|
50
|
-
function buildSprite(activity: PetActivity, frame: number): string[] {
|
|
51
|
-
const smash = activity === "working" && frame % 32 >= 26;
|
|
52
|
-
|
|
53
|
-
let eyes: string;
|
|
54
|
-
if (smash) eyes = "║ X X ║";
|
|
55
|
-
else switch (activity) {
|
|
56
|
-
case "sleeping": eyes = "║ - - ║"; break;
|
|
57
|
-
case "working": eyes = "║ / \\ ║"; break;
|
|
58
|
-
case "playing": eyes = frame % 6 < 3 ? "║ * * ║" : "║ ◆ ◆ ║"; break;
|
|
59
|
-
case "vibing": eyes = frame % 4 < 2 ? "║ ~ ~ ║" : "║ ◆ ◆ ║"; break;
|
|
60
|
-
case "eating": eyes = frame % 4 < 2 ? "║ o o ║" : "║ ◆ ◆ ║"; break;
|
|
61
|
-
case "praised": eyes = "║ ◆ ◆ ║"; break;
|
|
62
|
-
default: eyes = frame % 22 === 0 ? "║ - - ║" : "║ ◆ ◆ ║"; break;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Flex top during playing peak
|
|
66
|
-
const top = (activity === "playing" && frame % 6 >= 3) ? "╠═╩══╩═╣" : "╔═╩══╩═╗";
|
|
67
|
-
|
|
68
|
-
let lock: string;
|
|
69
|
-
if (smash) lock = "║ ║**║ ║";
|
|
70
|
-
else switch (activity) {
|
|
71
|
-
case "eating": lock = frame % 2 === 0 ? "║ ║>>║ ║" : "║ ║<<║ ║"; break;
|
|
72
|
-
case "sleeping": lock = "║ ║"; break;
|
|
73
|
-
case "praised": lock = frame % 3 === 0 ? "║ ║**║ ║" : "║ ║$$║ ║"; break;
|
|
74
|
-
case "vibing": lock = frame % 4 < 2 ? "║ ║~~║ ║" : "║ ║$$║ ║"; break;
|
|
75
|
-
case "playing": lock = frame % 4 < 2 ? "║ ║!!║ ║" : "║ ║$$║ ║"; break;
|
|
76
|
-
default: lock = "║ ║$$║ ║"; break;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return [
|
|
80
|
-
" ╔══╗ ",
|
|
81
|
-
top,
|
|
82
|
-
eyes,
|
|
83
|
-
activity === "sleeping" ? "║ ║" : "║ ╔══╗ ║",
|
|
84
|
-
lock,
|
|
85
|
-
"╚══════╝",
|
|
86
|
-
];
|
|
39
|
+
function blankRow(width: number): string {
|
|
40
|
+
return " ".repeat(width);
|
|
87
41
|
}
|
|
88
42
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
rows[R_SKY] = "─".repeat(CONTENT);
|
|
96
|
-
|
|
97
|
-
// Window left (6 wide) + deals board right (12 wide at x=20)
|
|
98
|
-
const sky = isDay
|
|
99
|
-
? (frame % 10 < 5 ? "─^──" : "──^─")
|
|
100
|
-
: (frame % 10 < 5 ? "─*──" : "──*─");
|
|
101
|
-
rows[R_BGA] = place(rows[R_BGA], `╭${sky}╮`, 0);
|
|
102
|
-
rows[R_BGB] = place(rows[R_BGB], "╰────╯", 0);
|
|
43
|
+
function padDisplayText(input: string, width: number): string {
|
|
44
|
+
const safeWidth = Math.max(1, width);
|
|
45
|
+
const fitted = fitDisplayText(input, safeWidth);
|
|
46
|
+
return `${fitted}${" ".repeat(Math.max(0, safeWidth - displayWidth(fitted)))}`;
|
|
47
|
+
}
|
|
103
48
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
49
|
+
function overlayFitted(row: string, text: string, x: number, width: number): string {
|
|
50
|
+
return place(row, padDisplayText(text, width), x);
|
|
51
|
+
}
|
|
107
52
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
53
|
+
function centerText(row: string, text: string): string {
|
|
54
|
+
const safeText = fitDisplayText(text, row.length);
|
|
55
|
+
const x = Math.max(0, Math.floor((row.length - displayWidth(safeText)) / 2));
|
|
56
|
+
return place(row, safeText, x);
|
|
57
|
+
}
|
|
112
58
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
59
|
+
function sceneBoxTop(title: string, width: number): string {
|
|
60
|
+
const safeWidth = Math.max(4, width);
|
|
61
|
+
const label = ` ${fitDisplayText(title, Math.max(1, safeWidth - 4))} `;
|
|
62
|
+
const ruleWidth = Math.max(0, safeWidth - 2 - displayWidth(label));
|
|
63
|
+
return `╭${label}${"─".repeat(ruleWidth)}╮`;
|
|
117
64
|
}
|
|
118
65
|
|
|
119
|
-
function
|
|
120
|
-
const
|
|
121
|
-
|
|
66
|
+
function sceneBoxBody(text: string, width: number): string {
|
|
67
|
+
const safeWidth = Math.max(4, width);
|
|
68
|
+
return `│${padDisplayText(text, safeWidth - 2)}│`;
|
|
69
|
+
}
|
|
122
70
|
|
|
123
|
-
|
|
71
|
+
function sceneBoxBottom(width: number): string {
|
|
72
|
+
const safeWidth = Math.max(4, width);
|
|
73
|
+
return `╰${"─".repeat(safeWidth - 2)}╯`;
|
|
74
|
+
}
|
|
124
75
|
|
|
125
|
-
|
|
126
|
-
rows
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
76
|
+
function placeSceneBox(
|
|
77
|
+
rows: string[],
|
|
78
|
+
row: number,
|
|
79
|
+
x: number,
|
|
80
|
+
width: number,
|
|
81
|
+
title: string,
|
|
82
|
+
body: string,
|
|
83
|
+
): void {
|
|
84
|
+
rows[row] = place(rows[row] ?? "", sceneBoxTop(title, width), x);
|
|
85
|
+
rows[row + 1] = place(rows[row + 1] ?? "", sceneBoxBody(body, width), x);
|
|
86
|
+
rows[row + 2] = place(rows[row + 2] ?? "", sceneBoxBottom(width), x);
|
|
87
|
+
}
|
|
131
88
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
89
|
+
function cupForEnergy(energy: number): string {
|
|
90
|
+
if (energy > 60) return "coffee [c~]";
|
|
91
|
+
if (energy > 30) return "coffee [c-]";
|
|
92
|
+
return "coffee [c_]";
|
|
93
|
+
}
|
|
136
94
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
95
|
+
function progressTicker(frame: number): string {
|
|
96
|
+
const dots = ".............";
|
|
97
|
+
const idx = frame % dots.length;
|
|
98
|
+
return `[${dots.slice(0, idx)}o${dots.slice(idx + 1)}]`;
|
|
99
|
+
}
|
|
141
100
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
101
|
+
function buildActivityLine(activity: PetActivity, frame: number): string {
|
|
102
|
+
switch (activity) {
|
|
103
|
+
case "eating":
|
|
104
|
+
return frame % 4 < 2
|
|
105
|
+
? "deal snack memo routed"
|
|
106
|
+
: "lunch marked to market";
|
|
107
|
+
case "playing":
|
|
108
|
+
return frame % 6 < 3
|
|
109
|
+
? "boardroom putting drill"
|
|
110
|
+
: "team morale accretive";
|
|
111
|
+
case "working":
|
|
112
|
+
return `term sheet live ${progressTicker(frame)}`;
|
|
113
|
+
case "sleeping":
|
|
114
|
+
return frame % 4 < 2
|
|
115
|
+
? "lights dim · conference-line nap"
|
|
116
|
+
: "zzz · calendar defended";
|
|
117
|
+
case "praised":
|
|
118
|
+
return frame % 2 === 0
|
|
119
|
+
? "bonus memo approved * * *"
|
|
120
|
+
: "* * * board applause logged";
|
|
121
|
+
case "vibing":
|
|
122
|
+
return ["lo-fi deal room ~ ~ ~", "~ ~ valuation waves ~ ~"][frame % 2]!;
|
|
123
|
+
default:
|
|
124
|
+
return "calendar clear · office quiet";
|
|
125
|
+
}
|
|
146
126
|
}
|
|
147
127
|
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
128
|
+
function mascotStateForActivity(activity: PetActivity, frame: number): MascotState {
|
|
129
|
+
switch (activity) {
|
|
130
|
+
case "eating":
|
|
131
|
+
return {
|
|
132
|
+
walls: "on",
|
|
133
|
+
brows: "raised",
|
|
134
|
+
eyes: "open",
|
|
135
|
+
showLock: true,
|
|
136
|
+
dollars: frame % 4 < 2 ? "dim" : "on",
|
|
137
|
+
};
|
|
138
|
+
case "playing":
|
|
139
|
+
return {
|
|
140
|
+
walls: "on",
|
|
141
|
+
brows: frame % 6 < 3 ? "raised" : "normal",
|
|
142
|
+
eyes: "open",
|
|
143
|
+
showLock: true,
|
|
144
|
+
dollars: "on",
|
|
145
|
+
};
|
|
146
|
+
case "working":
|
|
147
|
+
return {
|
|
148
|
+
walls: "on",
|
|
149
|
+
brows: "focused",
|
|
150
|
+
eyes: "open",
|
|
151
|
+
showLock: true,
|
|
152
|
+
dollars: frame % 8 < 4 ? "on" : "dim",
|
|
153
|
+
};
|
|
154
|
+
case "sleeping":
|
|
155
|
+
return {
|
|
156
|
+
walls: "dim",
|
|
157
|
+
brows: "hidden",
|
|
158
|
+
eyes: "closed",
|
|
159
|
+
showLock: true,
|
|
160
|
+
dollars: "dim",
|
|
161
|
+
};
|
|
162
|
+
case "praised":
|
|
163
|
+
return {
|
|
164
|
+
walls: "on",
|
|
165
|
+
brows: "raised",
|
|
166
|
+
eyes: "open",
|
|
167
|
+
showLock: true,
|
|
168
|
+
dollars: "on",
|
|
169
|
+
};
|
|
170
|
+
case "vibing":
|
|
171
|
+
return {
|
|
172
|
+
walls: "on",
|
|
173
|
+
brows: "flat",
|
|
174
|
+
eyes: "open",
|
|
175
|
+
showLock: true,
|
|
176
|
+
dollars: frame % 4 < 2 ? "dim" : "on",
|
|
177
|
+
};
|
|
178
|
+
default:
|
|
179
|
+
return {
|
|
180
|
+
walls: "on",
|
|
181
|
+
brows: "normal",
|
|
182
|
+
eyes: frame > 0 && frame % 22 === 0 ? "closed" : "open",
|
|
183
|
+
showLock: true,
|
|
184
|
+
dollars: "on",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
151
188
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
rows[
|
|
189
|
+
function drawOfficeBackground(rows: string[], width: number, frame: number, stats: PetStats): void {
|
|
190
|
+
const wallLabel = "DREXLER OFFICE";
|
|
191
|
+
const dealPct = `${Math.round(stats.deals).toString().padStart(3)}%`;
|
|
192
|
+
rows[R_WALL] = centerText("─".repeat(width), wallLabel);
|
|
193
|
+
rows[R_WALL] = place(
|
|
194
|
+
rows[R_WALL],
|
|
195
|
+
fitDisplayText(`pipe ${dealPct}`, Math.max(1, width - 2)),
|
|
196
|
+
Math.max(0, width - displayWidth(`pipe ${dealPct}`) - 1),
|
|
197
|
+
);
|
|
157
198
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
199
|
+
const windowWidth = Math.min(22, Math.max(16, Math.floor(width * 0.36)));
|
|
200
|
+
const boardWidth = Math.min(28, Math.max(20, width - windowWidth - 6));
|
|
201
|
+
const boardX = Math.max(windowWidth + 4, width - boardWidth - 2);
|
|
202
|
+
const marketPulse = frame % 10 < 5 ? "market tape + + +" : "market tape + +";
|
|
203
|
+
|
|
204
|
+
placeSceneBox(rows, R_WINDOW_TOP, 1, windowWidth, "Market Window", marketPulse);
|
|
205
|
+
placeSceneBox(
|
|
206
|
+
rows,
|
|
207
|
+
R_WINDOW_TOP,
|
|
208
|
+
boardX,
|
|
209
|
+
Math.min(boardWidth, width - boardX),
|
|
210
|
+
"Deal Board",
|
|
211
|
+
`DL:${dealPct} fees:${Math.round(stats.happiness)}%`,
|
|
212
|
+
);
|
|
161
213
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
(
|
|
165
|
-
)
|
|
214
|
+
rows[R_ACTIVITY] = centerText(
|
|
215
|
+
"─".repeat(width),
|
|
216
|
+
buildActivityLine("idle", frame),
|
|
217
|
+
);
|
|
166
218
|
}
|
|
167
219
|
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
220
|
+
function drawActivityAccents(
|
|
221
|
+
rows: string[],
|
|
222
|
+
width: number,
|
|
223
|
+
activity: PetActivity,
|
|
224
|
+
frame: number,
|
|
225
|
+
mascotX: number,
|
|
226
|
+
): void {
|
|
227
|
+
rows[R_ACTIVITY] = centerText(
|
|
228
|
+
"─".repeat(width),
|
|
229
|
+
buildActivityLine(activity, frame),
|
|
230
|
+
);
|
|
173
231
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
232
|
+
const mascotRight = mascotX + MASCOT_WIDTH;
|
|
233
|
+
const leftAccentX = Math.max(1, mascotX - 10);
|
|
234
|
+
const rightAccentX = Math.min(width - 10, mascotRight + 2);
|
|
177
235
|
|
|
178
236
|
switch (activity) {
|
|
179
|
-
case "
|
|
180
|
-
|
|
181
|
-
const chars: ["z", "z", "Z"] = ["z", "z", "Z"];
|
|
182
|
-
for (let i = 0; i < 3; i++) {
|
|
183
|
-
const age = (frame + i * 4) % 14;
|
|
184
|
-
if (age < 10) {
|
|
185
|
-
const py = R_DECO - Math.floor(age / 3);
|
|
186
|
-
const px = sx + 3 + i;
|
|
187
|
-
if (py >= 0 && px < CONTENT) rows[py] = place(rows[py], chars[i] ?? "z", px);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
237
|
+
case "eating":
|
|
238
|
+
rows[R_DESK_SURFACE] = place(rows[R_DESK_SURFACE], "[$ memo]", rightAccentX);
|
|
190
239
|
break;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// Arms progressively extend each phase
|
|
195
|
-
const phase = frame % 6;
|
|
196
|
-
if (phase >= 1) {
|
|
197
|
-
rows[R_SP0 + 1] = place(rows[R_SP0 + 1], "\\", Math.max(0, sx - 1));
|
|
198
|
-
rows[R_SP0 + 1] = place(rows[R_SP0 + 1], "/", sx + SPRITE_W);
|
|
199
|
-
}
|
|
200
|
-
if (phase >= 2) {
|
|
201
|
-
rows[R_SP0] = place(rows[R_SP0], "\\", Math.max(0, sx - 2));
|
|
202
|
-
rows[R_SP0] = place(rows[R_SP0], "/", Math.min(CONTENT - 1, sx + SPRITE_W + 1));
|
|
203
|
-
}
|
|
204
|
-
if (phase >= 3) {
|
|
205
|
-
rows[R_SP0] = place(rows[R_SP0], "\\", Math.max(0, sx - 3));
|
|
206
|
-
rows[R_SP0] = place(rows[R_SP0], "/", Math.min(CONTENT - 1, sx + SPRITE_W + 2));
|
|
207
|
-
// Stars burst at peak flex
|
|
208
|
-
rows[R_BGB] = place(rows[R_BGB], "*", Math.max(0, sx - 4));
|
|
209
|
-
rows[R_BGB] = place(rows[R_BGB], "*", Math.min(CONTENT - 1, sx + SPRITE_W + 3));
|
|
210
|
-
rows[R_BGA] = place(rows[R_BGA], "*", Math.max(0, sx - 2));
|
|
211
|
-
rows[R_BGA] = place(rows[R_BGA], "*", Math.min(CONTENT - 1, sx + SPRITE_W + 1));
|
|
212
|
-
}
|
|
240
|
+
case "playing":
|
|
241
|
+
rows[R_MASCOT_START + 2] = place(rows[R_MASCOT_START + 2], "*", leftAccentX);
|
|
242
|
+
rows[R_MASCOT_START + 2] = place(rows[R_MASCOT_START + 2], "*", rightAccentX + 6);
|
|
213
243
|
break;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const smash = frame % 32 >= 26;
|
|
218
|
-
if (smash) {
|
|
219
|
-
const sf = frame % 32 - 26;
|
|
220
|
-
if (sf < 2) rows[R_DECO] = place(rows[R_DECO], " !!! SMASH !!! ", 7);
|
|
221
|
-
else if (sf < 4) { rows[R_BGB] = place(rows[R_BGB], " * B O O M * ", 9); }
|
|
222
|
-
else rows[R_BGA] = place(rows[R_BGA], " . . . . ", 10);
|
|
223
|
-
} else {
|
|
224
|
-
// 3 staggered $ streams raining from sky through deco row
|
|
225
|
-
const cols = [7, 14, 25] as const;
|
|
226
|
-
for (let i = 0; i < 3; i++) {
|
|
227
|
-
const colX = cols[i] ?? 7;
|
|
228
|
-
for (const off of [0, 9] as const) {
|
|
229
|
-
const age = (frame + i * 6 + off) % 18;
|
|
230
|
-
const py = Math.floor(age / 2);
|
|
231
|
-
if (py <= R_DECO) rows[py] = place(rows[py], "$", colX);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
244
|
+
case "working":
|
|
245
|
+
rows[R_MASCOT_START + 1] = place(rows[R_MASCOT_START + 1], "$", leftAccentX);
|
|
246
|
+
rows[R_MASCOT_START + 3] = place(rows[R_MASCOT_START + 3], "$", rightAccentX + 6);
|
|
235
247
|
break;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
case "eating": {
|
|
239
|
-
// Deal memo arcs smoothly from right toward sprite
|
|
240
|
-
const memos: [string, number][] = [["$", 0], ["%", 4]];
|
|
241
|
-
for (const [char, off] of memos) {
|
|
242
|
-
const p = (frame + off) % 8;
|
|
243
|
-
if (p < 6) {
|
|
244
|
-
const endX = sx + SPRITE_W + 1;
|
|
245
|
-
const startX = CONTENT - 3;
|
|
246
|
-
const px = Math.round(startX - (p / 5) * (startX - endX));
|
|
247
|
-
const row = R_SP0 + 2 + (off > 0 ? 1 : 0);
|
|
248
|
-
if (px >= endX && row < R_FLOOR) rows[row] = place(rows[row], char, px);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
248
|
+
case "sleeping":
|
|
249
|
+
rows[R_MASCOT_START] = place(rows[R_MASCOT_START], "z z Z", rightAccentX);
|
|
251
250
|
break;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// Expanding star burst radiating outward over 5 frames
|
|
256
|
-
const cx = sx + Math.floor(SPRITE_W / 2);
|
|
257
|
-
const phase = frame % 5;
|
|
258
|
-
if (phase === 0) {
|
|
259
|
-
rows[R_DECO] = place(rows[R_DECO], "*", cx);
|
|
260
|
-
} else if (phase === 1) {
|
|
261
|
-
rows[R_DECO] = place(rows[R_DECO], "*", Math.max(0, cx - 2));
|
|
262
|
-
rows[R_DECO] = place(rows[R_DECO], "*", Math.min(CONTENT - 1, cx + 2));
|
|
263
|
-
rows[R_BGB] = place(rows[R_BGB], "*", cx);
|
|
264
|
-
} else if (phase === 2) {
|
|
265
|
-
rows[R_DECO] = place(rows[R_DECO], "*", Math.max(0, cx - 4));
|
|
266
|
-
rows[R_DECO] = place(rows[R_DECO], "*", Math.min(CONTENT - 1, cx + 4));
|
|
267
|
-
rows[R_BGB] = place(rows[R_BGB], "*", Math.max(0, cx - 2));
|
|
268
|
-
rows[R_BGB] = place(rows[R_BGB], "*", Math.min(CONTENT - 1, cx + 2));
|
|
269
|
-
rows[R_BGA] = place(rows[R_BGA], "*", cx);
|
|
270
|
-
} else if (phase === 3) {
|
|
271
|
-
rows[R_SKY] = place(rows[R_SKY], "*", Math.max(0, cx - 6));
|
|
272
|
-
rows[R_SKY] = place(rows[R_SKY], "*", cx);
|
|
273
|
-
rows[R_SKY] = place(rows[R_SKY], "*", Math.min(CONTENT - 1, cx + 6));
|
|
274
|
-
}
|
|
275
|
-
// phase 4 = clear
|
|
251
|
+
case "praised":
|
|
252
|
+
rows[R_MASCOT_START + 1] = place(rows[R_MASCOT_START + 1], "* *", leftAccentX);
|
|
253
|
+
rows[R_MASCOT_START + 1] = place(rows[R_MASCOT_START + 1], "* *", rightAccentX + 4);
|
|
276
254
|
break;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
rows[R_SP0 + 2] = place(rows[R_SP0 + 2], "~", sx + SPRITE_W);
|
|
285
|
-
}
|
|
286
|
-
if (wave >= 2) {
|
|
287
|
-
if (sx - 2 >= 0) rows[R_SP0 + 1] = place(rows[R_SP0 + 1], "~", sx - 2);
|
|
288
|
-
rows[R_SP0 + 1] = place(rows[R_SP0 + 1], "~", Math.min(CONTENT - 1, sx + SPRITE_W + 1));
|
|
289
|
-
if (sx - 1 >= 0) rows[R_DECO] = place(rows[R_DECO], "~", sx - 1);
|
|
290
|
-
rows[R_DECO] = place(rows[R_DECO], "~", Math.min(CONTENT - 1, sx + SPRITE_W));
|
|
291
|
-
}
|
|
292
|
-
if (wave >= 3) {
|
|
293
|
-
if (sx - 3 >= 0) rows[R_BGB] = place(rows[R_BGB], "~", sx - 3);
|
|
294
|
-
rows[R_BGB] = place(rows[R_BGB], "~", Math.min(CONTENT - 1, sx + SPRITE_W + 2));
|
|
295
|
-
}
|
|
296
|
-
if (wave >= 4) {
|
|
297
|
-
if (sx - 4 >= 0) rows[R_BGA] = place(rows[R_BGA], "~", sx - 4);
|
|
298
|
-
rows[R_BGA] = place(rows[R_BGA], "~", Math.min(CONTENT - 1, sx + SPRITE_W + 3));
|
|
299
|
-
}
|
|
255
|
+
case "vibing":
|
|
256
|
+
rows[R_MASCOT_START + 3] = place(rows[R_MASCOT_START + 3], "~ ~", leftAccentX);
|
|
257
|
+
rows[R_MASCOT_START + 3] = place(rows[R_MASCOT_START + 3], "~ ~", rightAccentX + 4);
|
|
258
|
+
break;
|
|
259
|
+
default:
|
|
260
|
+
rows[R_MASCOT_START + 2] = place(rows[R_MASCOT_START + 2], "[IN]", 2);
|
|
261
|
+
rows[R_MASCOT_START + 2] = place(rows[R_MASCOT_START + 2], "[OUT]", Math.max(2, width - 8));
|
|
300
262
|
break;
|
|
301
|
-
}
|
|
302
263
|
}
|
|
303
264
|
}
|
|
304
265
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
case "praised": return frame % 2 === 0 ? " * * * * * * *" : " * * * * * * * ";
|
|
315
|
-
case "working": {
|
|
316
|
-
if (frame % 32 >= 26) return ""; // smash handled by particles
|
|
317
|
-
const d = ["[............]","[o...........]","[.o..........]","[..o.........]",
|
|
318
|
-
"[...o........]","[....o.......]","[.....o......]","[......o.....]",
|
|
319
|
-
"[.......o....]","[........o...]","[.........o..]","[..........o.]",
|
|
320
|
-
"[...........o]"];
|
|
321
|
-
return d[frame % d.length] ?? "[............]";
|
|
322
|
-
}
|
|
323
|
-
case "vibing": return ["~ ~ ~ ~ ~ ~ ~"," ~ ~ ~ ~ ~ ~ "," ~ ~ ~ ~ ~ "][frame % 3] ?? "~";
|
|
324
|
-
default: return "";
|
|
266
|
+
function drawMascot(rows: string[], width: number, activity: PetActivity, frame: number): number {
|
|
267
|
+
const mascot = renderMascotLines(mascotStateForActivity(activity, frame));
|
|
268
|
+
const mascotX = Math.max(0, Math.floor((width - MASCOT_WIDTH) / 2));
|
|
269
|
+
for (let i = 0; i < mascot.length; i++) {
|
|
270
|
+
rows[R_MASCOT_START + i] = place(
|
|
271
|
+
rows[R_MASCOT_START + i] ?? blankRow(width),
|
|
272
|
+
mascot[i] ?? "",
|
|
273
|
+
mascotX,
|
|
274
|
+
);
|
|
325
275
|
}
|
|
276
|
+
return mascotX;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function drawDesk(rows: string[], width: number, stats: PetStats): void {
|
|
280
|
+
const deskX = width > PET_SCENE_WIDTH ? 2 : 1;
|
|
281
|
+
const deskWidth = Math.max(4, width - deskX * 2);
|
|
282
|
+
const deskInner = Math.max(1, deskWidth - 2);
|
|
283
|
+
const surface = `laptop [▣] papers [///] ${cupForEnergy(stats.energy)}`;
|
|
284
|
+
const front = `DESK pipeline ${Math.round(stats.deals)}% covenants OK`;
|
|
285
|
+
|
|
286
|
+
rows[R_DESK_SURFACE] = place(
|
|
287
|
+
rows[R_DESK_SURFACE],
|
|
288
|
+
`╭${padDisplayText(surface, deskInner)}╮`,
|
|
289
|
+
deskX,
|
|
290
|
+
);
|
|
291
|
+
rows[R_DESK_FRONT] = place(
|
|
292
|
+
rows[R_DESK_FRONT],
|
|
293
|
+
`╰${padDisplayText(front, deskInner)}╯`,
|
|
294
|
+
deskX,
|
|
295
|
+
);
|
|
326
296
|
}
|
|
327
297
|
|
|
328
|
-
// ─── full scene builder ───────────────────────────────────────────────────────
|
|
329
298
|
function buildScene(
|
|
330
299
|
activity: PetActivity,
|
|
331
300
|
frame: number,
|
|
332
301
|
stats: PetStats,
|
|
333
|
-
|
|
302
|
+
width: number,
|
|
334
303
|
): string[] {
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const dx = Math.max(0, Math.min(CONTENT - deco.length, sx - Math.floor((deco.length - SPRITE_W) / 2)));
|
|
344
|
-
rows[R_DECO] = place(rows[R_DECO], deco, dx);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const sprite = buildSprite(activity, frame);
|
|
348
|
-
const sx = SPRITE_X[activity];
|
|
349
|
-
for (let i = 0; i < sprite.length; i++) {
|
|
350
|
-
rows[R_SP0 + i] = place(rows[R_SP0 + i], sprite[i] ?? "", sx);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return rows;
|
|
304
|
+
const sceneWidth = Math.max(PET_SCENE_WIDTH, Math.floor(width));
|
|
305
|
+
const rows: string[] = Array.from({ length: SCENE_ROWS }, () => blankRow(sceneWidth));
|
|
306
|
+
|
|
307
|
+
drawOfficeBackground(rows, sceneWidth, frame, stats);
|
|
308
|
+
drawDesk(rows, sceneWidth, stats);
|
|
309
|
+
const mascotX = drawMascot(rows, sceneWidth, activity, frame);
|
|
310
|
+
drawActivityAccents(rows, sceneWidth, activity, frame, mascotX);
|
|
311
|
+
return rows.map((row) => overlayFitted(blankRow(sceneWidth), row, 0, sceneWidth));
|
|
354
312
|
}
|
|
355
313
|
|
|
356
314
|
// ─── row colors ───────────────────────────────────────────────────────────────
|
|
357
315
|
function rowColor(i: number, activity: PetActivity, frame: number, t: Theme): string {
|
|
358
|
-
if (i >=
|
|
316
|
+
if (i >= R_MASCOT_START && i < R_MASCOT_START + BRIEFCASE_FINAL.length) {
|
|
359
317
|
if (activity === "sleeping") return t.dim;
|
|
360
318
|
if (activity === "praised") return t.primaryLight;
|
|
361
319
|
if (activity === "eating") return t.warning;
|
|
@@ -363,7 +321,7 @@ function rowColor(i: number, activity: PetActivity, frame: number, t: Theme): st
|
|
|
363
321
|
if (activity === "working" && frame % 32 >= 26) return t.error;
|
|
364
322
|
return t.primary;
|
|
365
323
|
}
|
|
366
|
-
if (i ===
|
|
324
|
+
if (i === R_ACTIVITY) {
|
|
367
325
|
if (activity === "working" && frame % 32 >= 26) return t.error;
|
|
368
326
|
if (activity === "sleeping") return t.dim;
|
|
369
327
|
if (activity === "praised") return t.warning;
|
|
@@ -371,8 +329,9 @@ function rowColor(i: number, activity: PetActivity, frame: number, t: Theme): st
|
|
|
371
329
|
if (activity === "playing") return t.primaryLight;
|
|
372
330
|
return t.primaryLight;
|
|
373
331
|
}
|
|
374
|
-
if (i ===
|
|
375
|
-
if (i ===
|
|
332
|
+
if (i === R_DESK_SURFACE || i === R_DESK_FRONT) return t.primaryDim;
|
|
333
|
+
if (i === R_WALL) return t.dim;
|
|
334
|
+
if (i >= R_WINDOW_TOP && i <= R_WINDOW_BOTTOM) return t.primaryDim;
|
|
376
335
|
return t.primaryDim;
|
|
377
336
|
}
|
|
378
337
|
|
|
@@ -434,6 +393,7 @@ interface PetSceneProps {
|
|
|
434
393
|
activity: PetActivity;
|
|
435
394
|
env?: Environment;
|
|
436
395
|
isPaused?: boolean;
|
|
396
|
+
width?: number;
|
|
437
397
|
}
|
|
438
398
|
|
|
439
399
|
interface CompactPetPanelProps extends PetSceneProps {
|
|
@@ -442,12 +402,10 @@ interface CompactPetPanelProps extends PetSceneProps {
|
|
|
442
402
|
|
|
443
403
|
function usePetFrame({
|
|
444
404
|
activity,
|
|
445
|
-
env,
|
|
446
405
|
isPaused,
|
|
447
406
|
dead,
|
|
448
407
|
}: {
|
|
449
408
|
activity: PetActivity;
|
|
450
|
-
env: Environment;
|
|
451
409
|
isPaused: boolean;
|
|
452
410
|
dead: boolean;
|
|
453
411
|
}) {
|
|
@@ -455,7 +413,7 @@ function usePetFrame({
|
|
|
455
413
|
|
|
456
414
|
useEffect(() => {
|
|
457
415
|
setFrame(0);
|
|
458
|
-
}, [activity
|
|
416
|
+
}, [activity]);
|
|
459
417
|
|
|
460
418
|
useEffect(() => {
|
|
461
419
|
// Skip frame ticks when paused or when the pet has died — DeathScreen
|
|
@@ -474,23 +432,23 @@ function usePetFrame({
|
|
|
474
432
|
export function PetScene({
|
|
475
433
|
stats,
|
|
476
434
|
activity,
|
|
477
|
-
env = "office",
|
|
478
435
|
isPaused = false,
|
|
436
|
+
width = PET_SCENE_WIDTH,
|
|
479
437
|
}: PetSceneProps) {
|
|
480
438
|
const t = useTheme();
|
|
439
|
+
const sceneWidth = Math.max(PET_SCENE_WIDTH, Math.floor(width));
|
|
481
440
|
const frame = usePetFrame({
|
|
482
441
|
activity,
|
|
483
|
-
env,
|
|
484
442
|
isPaused,
|
|
485
443
|
dead: stats.dead === true,
|
|
486
444
|
});
|
|
487
445
|
const scene = useMemo(
|
|
488
|
-
() => buildScene(activity, frame, stats,
|
|
489
|
-
[activity, frame,
|
|
446
|
+
() => buildScene(activity, frame, stats, sceneWidth),
|
|
447
|
+
[activity, frame, sceneWidth, stats],
|
|
490
448
|
);
|
|
491
449
|
|
|
492
450
|
return (
|
|
493
|
-
<Box flexDirection="column" width={
|
|
451
|
+
<Box flexDirection="column" width={sceneWidth} flexShrink={0}>
|
|
494
452
|
{scene.map((row, i) => (
|
|
495
453
|
<Text key={i} color={rowColor(i, activity, frame, t)}>{row}</Text>
|
|
496
454
|
))}
|
|
@@ -546,7 +504,6 @@ function pickWorstStat(stats: PetStats): WorstStat {
|
|
|
546
504
|
function CompactPetPanelView({
|
|
547
505
|
stats,
|
|
548
506
|
activity,
|
|
549
|
-
env = "office",
|
|
550
507
|
isPaused = false,
|
|
551
508
|
width,
|
|
552
509
|
}: CompactPetPanelProps) {
|
|
@@ -566,7 +523,7 @@ function CompactPetPanelView({
|
|
|
566
523
|
|
|
567
524
|
const mood = getPetMood(stats);
|
|
568
525
|
const profile = compactStatProfile(stats);
|
|
569
|
-
const activityCopy = activity === "idle" ?
|
|
526
|
+
const activityCopy = activity === "idle" ? "office" : `office / ${activity}`;
|
|
570
527
|
const title = "Drexler Pet Desk";
|
|
571
528
|
const statLine = [
|
|
572
529
|
`happy ${STAT_LEVEL_LABEL[profile.happiness]}`,
|