drexler 0.2.16 → 0.2.17
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 +3 -2
- package/package.json +1 -1
- package/src/commands.ts +3 -0
- package/src/index.ts +1 -0
- package/src/ui/App.tsx +77 -49
- package/src/ui/MascotIntro.tsx +388 -0
- package/src/ui/PetPanel.tsx +41 -90
package/README.md
CHANGED
|
@@ -89,9 +89,9 @@ rm -rf ~/.config/drexler # optional: wipe stored key + settings
|
|
|
89
89
|
|
|
90
90
|
### Interactive UI
|
|
91
91
|
|
|
92
|
-
Drexler runs as an Ink terminal UI when both stdin and stdout are TTYs. The normal launch shows one integrated startup panel with the mascot, tips, a **Mood** readout, and the **Drexler Deal Desk**. Short terminals automatically suppress oversized startup chrome so the chat stays usable.
|
|
92
|
+
Drexler runs as an Ink terminal UI when both stdin and stdout are TTYs. The normal launch shows one integrated startup panel with the mascot, tips, a **Mood** readout, and the **Drexler Deal Desk**. After startup, that dashboard remains the primary top chrome. Short terminals automatically suppress oversized startup chrome so the chat stays usable.
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Type `/pet` to toggle pet mode for the current session. Pet mode keeps the top dashboard frame and swaps the normal mascot/tips/deal desk layout for a Drexler scene plus pet stats. `/pet on` and `/pet off` set it explicitly. Pet stats still decay and pet commands still work when the pet dashboard is hidden.
|
|
95
95
|
|
|
96
96
|
The startup panel is designed to stay stable while it boots: the mascot loading bar and Mood gauge animate without changing width, greeting copy is held in a fixed slot, and the Mood and Deal Desk boxes stay aligned when the greeting wraps. After boot, Mood resolves into a rotating Drexler-flavored posture with a short satirical subtext line.
|
|
97
97
|
|
|
@@ -136,6 +136,7 @@ Keyboard notes:
|
|
|
136
136
|
| `/clear` | shred conversation history (system prompt pinned) |
|
|
137
137
|
| `/exit` | meeting adjourned |
|
|
138
138
|
| `/synergy` | run a rotating animated morale event |
|
|
139
|
+
| `/pet [on\|off]` | toggle pet dashboard mode for this session |
|
|
139
140
|
| `/feed` | feed Drexler a deal memo |
|
|
140
141
|
| `/play` | corporate synergy game with Drexler |
|
|
141
142
|
| `/work` | Drexler grinds the deal pipeline |
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -47,6 +47,7 @@ const HELP_TEXT = `New memo to staff! Drexler permit following directives:
|
|
|
47
47
|
/clear - shred all documents (reset history)
|
|
48
48
|
/exit - meeting adjourned
|
|
49
49
|
/synergy - SYNERGY!
|
|
50
|
+
/pet [on|off] - toggle pet dashboard mode
|
|
50
51
|
/feed - feed Drexler a deal memo
|
|
51
52
|
/play - corporate synergy game with Drexler
|
|
52
53
|
/work - Drexler grinds the deal pipeline
|
|
@@ -86,6 +87,7 @@ export const COMMAND_PALETTE: ReadonlyArray<SlashCommand> = [
|
|
|
86
87
|
{ name: "/clear", description: "Reset conversation", group: "directives" },
|
|
87
88
|
{ name: "/exit", description: "Adjourn meeting", group: "directives" },
|
|
88
89
|
{ name: "/synergy", description: "SYNERGY!", group: "directives" },
|
|
90
|
+
{ name: "/pet", description: "Toggle pet dashboard mode", group: "directives" },
|
|
89
91
|
{ name: "/feed", description: "Feed Drexler a deal memo", group: "directives" },
|
|
90
92
|
{ name: "/play", description: "Play with Drexler", group: "directives" },
|
|
91
93
|
{ name: "/work", description: "Drexler grinds deals", group: "directives" },
|
|
@@ -369,6 +371,7 @@ export function dispatch(input: string, ctx: CommandContext): CommandAction {
|
|
|
369
371
|
);
|
|
370
372
|
return { type: "continue" };
|
|
371
373
|
|
|
374
|
+
case "pet":
|
|
372
375
|
case "feed":
|
|
373
376
|
case "play":
|
|
374
377
|
case "work":
|
package/src/index.ts
CHANGED
package/src/ui/App.tsx
CHANGED
|
@@ -31,8 +31,6 @@ import {
|
|
|
31
31
|
CompactPetPanel,
|
|
32
32
|
COMPACT_PET_PANEL_MIN_WIDTH,
|
|
33
33
|
COMPACT_PET_PANEL_ROWS,
|
|
34
|
-
PetPanel,
|
|
35
|
-
PET_PANEL_WIDTH,
|
|
36
34
|
TINY_PET_PANEL_ROWS,
|
|
37
35
|
type Environment,
|
|
38
36
|
} from "./PetPanel.tsx";
|
|
@@ -93,10 +91,6 @@ import {
|
|
|
93
91
|
import { getActiveTheme } from "./themes.ts";
|
|
94
92
|
|
|
95
93
|
const TRANSCRIPT_CHROME_ROWS = 12;
|
|
96
|
-
const PET_PANEL_MIN_MAIN_COLUMNS = 75;
|
|
97
|
-
const PET_PANEL_GAP_COLUMNS = 1;
|
|
98
|
-
const PET_PANEL_MIN_COLUMNS =
|
|
99
|
-
PET_PANEL_WIDTH + PET_PANEL_GAP_COLUMNS + PET_PANEL_MIN_MAIN_COLUMNS;
|
|
100
94
|
|
|
101
95
|
export function transcriptRowsForTerminalRows(rows: number): number {
|
|
102
96
|
return Math.max(1, Math.min(24, rows - TRANSCRIPT_CHROME_ROWS));
|
|
@@ -198,6 +192,7 @@ interface AppProps {
|
|
|
198
192
|
fetchFn?: FetchFn;
|
|
199
193
|
greeting?: string;
|
|
200
194
|
showIntroChrome?: boolean;
|
|
195
|
+
introInitiallyDone?: boolean;
|
|
201
196
|
}
|
|
202
197
|
|
|
203
198
|
export function App({
|
|
@@ -207,6 +202,7 @@ export function App({
|
|
|
207
202
|
fetchFn,
|
|
208
203
|
greeting,
|
|
209
204
|
showIntroChrome = false,
|
|
205
|
+
introInitiallyDone = false,
|
|
210
206
|
}: AppProps) {
|
|
211
207
|
const { exit } = useApp();
|
|
212
208
|
const { stdout } = useStdout();
|
|
@@ -228,36 +224,38 @@ export function App({
|
|
|
228
224
|
const mode = useMemo(() => pickLayout(cols), [cols]);
|
|
229
225
|
const chromeWidth = useMemo(() => Math.max(1, cols), [cols]);
|
|
230
226
|
const isCompact = mode === "very-narrow";
|
|
231
|
-
const [introDone, setIntroDone] = useState(
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const compactPetRowBudget = showCompactPetPanel
|
|
227
|
+
const [introDone, setIntroDone] = useState(introInitiallyDone);
|
|
228
|
+
const dashboardAllowed = showIntroChrome && typeof greeting === "string";
|
|
229
|
+
const showFullDashboard = dashboardAllowed && rows >= 32;
|
|
230
|
+
const introActive = showFullDashboard && !introDone;
|
|
231
|
+
const [petMode, setPetMode] = useState(false);
|
|
232
|
+
const petModeRef = useRef(false);
|
|
233
|
+
const showFallbackPetPanel = petMode && !showFullDashboard;
|
|
234
|
+
const fallbackPetRowBudget = showFallbackPetPanel
|
|
240
235
|
? cols >= COMPACT_PET_PANEL_MIN_WIDTH
|
|
241
236
|
? COMPACT_PET_PANEL_ROWS
|
|
242
237
|
: TINY_PET_PANEL_ROWS
|
|
243
238
|
: 0;
|
|
244
|
-
const
|
|
245
|
-
? PET_PANEL_WIDTH + PET_PANEL_GAP_COLUMNS
|
|
246
|
-
: 0;
|
|
247
|
-
const contentWidth = showPetSidePanel
|
|
248
|
-
? Math.max(1, cols - petPanelReservedWidth)
|
|
249
|
-
: chromeWidth;
|
|
239
|
+
const contentWidth = chromeWidth;
|
|
250
240
|
const contentInputWidth = Math.max(1, contentWidth);
|
|
251
241
|
const contentStatusWidth = Math.max(1, contentInputWidth - 2);
|
|
252
|
-
const
|
|
253
|
-
|
|
242
|
+
const dashboardRowBudget =
|
|
243
|
+
showFullDashboard
|
|
244
|
+
? chromeWidth >= 112
|
|
245
|
+
? 14
|
|
246
|
+
: chromeWidth >= 72
|
|
247
|
+
? 26
|
|
248
|
+
: 6
|
|
249
|
+
: 0;
|
|
254
250
|
const maxTranscriptRows = useMemo(
|
|
255
251
|
() =>
|
|
256
252
|
Math.max(
|
|
257
253
|
1,
|
|
258
|
-
transcriptRowsForTerminalRows(rows) -
|
|
254
|
+
transcriptRowsForTerminalRows(rows) -
|
|
255
|
+
dashboardRowBudget -
|
|
256
|
+
fallbackPetRowBudget,
|
|
259
257
|
),
|
|
260
|
-
[
|
|
258
|
+
[dashboardRowBudget, fallbackPetRowBudget, rows],
|
|
261
259
|
);
|
|
262
260
|
|
|
263
261
|
const [items, setItems] = useState<ChatItem[]>([]);
|
|
@@ -315,7 +313,7 @@ export function App({
|
|
|
315
313
|
}, []);
|
|
316
314
|
const intro = useIntroAnimation(
|
|
317
315
|
chromeWidth,
|
|
318
|
-
|
|
316
|
+
introActive,
|
|
319
317
|
handleIntroComplete,
|
|
320
318
|
);
|
|
321
319
|
|
|
@@ -391,6 +389,10 @@ export function App({
|
|
|
391
389
|
},
|
|
392
390
|
[],
|
|
393
391
|
);
|
|
392
|
+
const setDashboardPetMode = useCallback((next: boolean) => {
|
|
393
|
+
petModeRef.current = next;
|
|
394
|
+
setPetMode(next);
|
|
395
|
+
}, []);
|
|
394
396
|
const updatePetStats = useCallback((updater: (stats: PetStats) => PetStats) => {
|
|
395
397
|
setPetStats((stats) => {
|
|
396
398
|
const next = updater(stats);
|
|
@@ -687,6 +689,38 @@ export function App({
|
|
|
687
689
|
runSynergyEvent();
|
|
688
690
|
return;
|
|
689
691
|
}
|
|
692
|
+
if (slashCommand === "/pet") {
|
|
693
|
+
const arg = lower.slice("/pet".length).trim();
|
|
694
|
+
if (arg === "") {
|
|
695
|
+
const next = !petModeRef.current;
|
|
696
|
+
setDashboardPetMode(next);
|
|
697
|
+
addItem(
|
|
698
|
+
"system",
|
|
699
|
+
next
|
|
700
|
+
? "Pet dashboard enabled. Deal desk converted to habitat."
|
|
701
|
+
: "Pet dashboard disabled. Deal desk restored.",
|
|
702
|
+
);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (arg === "on" || arg === "off") {
|
|
706
|
+
const next = arg === "on";
|
|
707
|
+
const changed = petModeRef.current !== next;
|
|
708
|
+
setDashboardPetMode(next);
|
|
709
|
+
addItem(
|
|
710
|
+
"system",
|
|
711
|
+
changed
|
|
712
|
+
? next
|
|
713
|
+
? "Pet dashboard enabled. Deal desk converted to habitat."
|
|
714
|
+
: "Pet dashboard disabled. Deal desk restored."
|
|
715
|
+
: next
|
|
716
|
+
? "Pet dashboard already enabled."
|
|
717
|
+
: "Pet dashboard already disabled.",
|
|
718
|
+
);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
addItem("system", "Usage: /pet, /pet on, or /pet off.");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
690
724
|
const isPetCommand =
|
|
691
725
|
slashCommand === "/feed" ||
|
|
692
726
|
slashCommand === "/play" ||
|
|
@@ -861,11 +895,11 @@ export function App({
|
|
|
861
895
|
config,
|
|
862
896
|
activeTheme,
|
|
863
897
|
model,
|
|
864
|
-
petStats,
|
|
865
898
|
PET_MESSAGES,
|
|
866
899
|
removeLastAssistantItem,
|
|
867
900
|
runLLM,
|
|
868
901
|
runSynergyEvent,
|
|
902
|
+
setDashboardPetMode,
|
|
869
903
|
triggerExit,
|
|
870
904
|
triggerPetActivity,
|
|
871
905
|
updatePetStats,
|
|
@@ -1129,9 +1163,9 @@ export function App({
|
|
|
1129
1163
|
messageCount={msgCount}
|
|
1130
1164
|
status={headerStatus}
|
|
1131
1165
|
compact={isCompact}
|
|
1132
|
-
notice={!
|
|
1166
|
+
notice={!introActive ? deskNotice ?? undefined : undefined}
|
|
1133
1167
|
maxWidth={Math.max(1, width)}
|
|
1134
|
-
marginBottom={
|
|
1168
|
+
marginBottom={introActive ? 0 : 1}
|
|
1135
1169
|
/>
|
|
1136
1170
|
);
|
|
1137
1171
|
const dealDeskHeader = renderDealDeskHeader(chromeWidth);
|
|
@@ -1148,24 +1182,26 @@ export function App({
|
|
|
1148
1182
|
return (
|
|
1149
1183
|
<ThemeProvider value={activeTheme}>
|
|
1150
1184
|
<Box flexDirection="column">
|
|
1151
|
-
{
|
|
1185
|
+
{showFullDashboard && typeof greeting === "string" ? (
|
|
1152
1186
|
<Box marginBottom={1}>
|
|
1153
1187
|
<MascotDashboard
|
|
1154
1188
|
greeting={greeting}
|
|
1155
1189
|
width={chromeWidth}
|
|
1156
1190
|
mood={mood}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1191
|
+
mode={petMode && !introActive ? "pet" : "normal"}
|
|
1192
|
+
petStats={petStats}
|
|
1193
|
+
petActivity={petActivity}
|
|
1194
|
+
petEnv={petEnv}
|
|
1195
|
+
petPaused={isBusy}
|
|
1196
|
+
bootProgress={introActive ? intro.progress : 1}
|
|
1197
|
+
state={introActive ? intro.state : undefined}
|
|
1198
|
+
bar={introActive ? intro.bar : undefined}
|
|
1199
|
+
barColor={introActive ? introBarColor : undefined}
|
|
1200
|
+
mascotStatus={introActive ? intro.status : undefined}
|
|
1162
1201
|
dealDesk={renderDealDeskHeader}
|
|
1163
1202
|
/>
|
|
1164
1203
|
</Box>
|
|
1165
|
-
) : (
|
|
1166
|
-
dealDeskHeader
|
|
1167
|
-
)}
|
|
1168
|
-
{showCompactPetPanel && (
|
|
1204
|
+
) : showFallbackPetPanel ? (
|
|
1169
1205
|
<Box marginBottom={1}>
|
|
1170
1206
|
<CompactPetPanel
|
|
1171
1207
|
stats={petStats}
|
|
@@ -1175,18 +1211,10 @@ export function App({
|
|
|
1175
1211
|
width={chromeWidth}
|
|
1176
1212
|
/>
|
|
1177
1213
|
</Box>
|
|
1214
|
+
) : (
|
|
1215
|
+
dealDeskHeader
|
|
1178
1216
|
)}
|
|
1179
1217
|
<Box flexDirection="row" alignItems="flex-start">
|
|
1180
|
-
{showPetSidePanel && (
|
|
1181
|
-
<Box marginRight={PET_PANEL_GAP_COLUMNS}>
|
|
1182
|
-
<PetPanel
|
|
1183
|
-
stats={petStats}
|
|
1184
|
-
activity={petActivity}
|
|
1185
|
-
env={petEnv}
|
|
1186
|
-
isPaused={isBusy}
|
|
1187
|
-
/>
|
|
1188
|
-
</Box>
|
|
1189
|
-
)}
|
|
1190
1218
|
<Box flexDirection="column" flexGrow={1}>
|
|
1191
1219
|
<TranscriptViewport
|
|
1192
1220
|
items={items}
|
package/src/ui/MascotIntro.tsx
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
2
2
|
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
3
|
+
import {
|
|
4
|
+
formatTenure,
|
|
5
|
+
getPetMood,
|
|
6
|
+
getPetRank,
|
|
7
|
+
petTenureMs,
|
|
8
|
+
rankLabel,
|
|
9
|
+
type PetActivity,
|
|
10
|
+
type PetStats,
|
|
11
|
+
} from "../pet/petState.ts";
|
|
3
12
|
import { STARTUP_TIPS } from "../startupTips.ts";
|
|
4
13
|
import {
|
|
5
14
|
MascotFrame,
|
|
@@ -7,6 +16,14 @@ import {
|
|
|
7
16
|
type MascotState,
|
|
8
17
|
} from "./MascotFrame.tsx";
|
|
9
18
|
import { displayWidth, fitDisplayText } from "./graphemes.ts";
|
|
19
|
+
import {
|
|
20
|
+
COMPACT_PET_PANEL_MIN_WIDTH,
|
|
21
|
+
CompactPetPanel,
|
|
22
|
+
getPetStatusMessage,
|
|
23
|
+
PetScene,
|
|
24
|
+
PET_SCENE_WIDTH,
|
|
25
|
+
type Environment,
|
|
26
|
+
} from "./PetPanel.tsx";
|
|
10
27
|
import { useTheme } from "./ThemeContext.tsx";
|
|
11
28
|
|
|
12
29
|
interface IntroFrame extends MascotState {
|
|
@@ -152,6 +169,13 @@ const MAX_MOOD_PANEL_WIDTH = 44;
|
|
|
152
169
|
const RIGHT_COLUMN_INSET = 1;
|
|
153
170
|
const RIGHT_COLUMN_PAD_RIGHT = 1;
|
|
154
171
|
const LEFT_PANEL_MIN_COPY = 24;
|
|
172
|
+
const PET_STATS_MIN_WIDTH = 24;
|
|
173
|
+
const PET_STATS_MAX_WIDTH = 58;
|
|
174
|
+
const PET_SPLIT_DIVIDER_HEIGHT = 12;
|
|
175
|
+
const PET_SPLIT_DIVIDER_ROWS: number[] = Array.from(
|
|
176
|
+
{ length: PET_SPLIT_DIVIDER_HEIGHT },
|
|
177
|
+
(_, i) => i,
|
|
178
|
+
);
|
|
155
179
|
|
|
156
180
|
export type MascotLayoutMode = "tiny" | "compact" | "stacked" | "split";
|
|
157
181
|
|
|
@@ -257,6 +281,11 @@ interface MascotDashboardProps {
|
|
|
257
281
|
greeting: string;
|
|
258
282
|
width: number;
|
|
259
283
|
mood?: string;
|
|
284
|
+
mode?: "normal" | "pet";
|
|
285
|
+
petStats?: PetStats;
|
|
286
|
+
petActivity?: PetActivity;
|
|
287
|
+
petEnv?: Environment;
|
|
288
|
+
petPaused?: boolean;
|
|
260
289
|
bootProgress?: number;
|
|
261
290
|
state?: MascotState;
|
|
262
291
|
bar?: string;
|
|
@@ -780,10 +809,357 @@ function MoodReadout({
|
|
|
780
809
|
);
|
|
781
810
|
}
|
|
782
811
|
|
|
812
|
+
function padDisplayText(input: string, width: number): string {
|
|
813
|
+
const safeWidth = Math.max(1, width);
|
|
814
|
+
const fitted = fitDisplayText(input, safeWidth);
|
|
815
|
+
return `${fitted}${" ".repeat(Math.max(0, safeWidth - displayWidth(fitted)))}`;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function PetSceneReadout({
|
|
819
|
+
stats,
|
|
820
|
+
activity,
|
|
821
|
+
env,
|
|
822
|
+
isPaused,
|
|
823
|
+
width,
|
|
824
|
+
}: {
|
|
825
|
+
stats: PetStats;
|
|
826
|
+
activity: PetActivity;
|
|
827
|
+
env: Environment;
|
|
828
|
+
isPaused: boolean;
|
|
829
|
+
width: number;
|
|
830
|
+
}) {
|
|
831
|
+
const t = useTheme();
|
|
832
|
+
const safeWidth = Math.max(1, Math.floor(width));
|
|
833
|
+
|
|
834
|
+
if (safeWidth < PET_SCENE_WIDTH) {
|
|
835
|
+
return (
|
|
836
|
+
<CompactPetPanel
|
|
837
|
+
stats={stats}
|
|
838
|
+
activity={activity}
|
|
839
|
+
env={env}
|
|
840
|
+
isPaused={isPaused}
|
|
841
|
+
width={safeWidth}
|
|
842
|
+
/>
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return (
|
|
847
|
+
<Box flexDirection="column" width={safeWidth} alignItems="center">
|
|
848
|
+
<Text bold color={t.primaryLight}>
|
|
849
|
+
{fitDisplayText(`Drexler Pet Desk [${env}]`, safeWidth)}
|
|
850
|
+
</Text>
|
|
851
|
+
<PetScene
|
|
852
|
+
stats={stats}
|
|
853
|
+
activity={activity}
|
|
854
|
+
env={env}
|
|
855
|
+
isPaused={isPaused}
|
|
856
|
+
/>
|
|
857
|
+
</Box>
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function PetStatsBodyLine({
|
|
862
|
+
text,
|
|
863
|
+
width,
|
|
864
|
+
color,
|
|
865
|
+
}: {
|
|
866
|
+
text: string;
|
|
867
|
+
width: number;
|
|
868
|
+
color: string;
|
|
869
|
+
}) {
|
|
870
|
+
const t = useTheme();
|
|
871
|
+
const innerWidth = Math.max(1, width - 4);
|
|
872
|
+
const content = padDisplayText(text, innerWidth);
|
|
873
|
+
return (
|
|
874
|
+
<Text>
|
|
875
|
+
<Text color={t.primary}>│ </Text>
|
|
876
|
+
<Text color={color}>{content}</Text>
|
|
877
|
+
<Text color={t.primary}> │</Text>
|
|
878
|
+
</Text>
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function PetDashboardStatBar({
|
|
883
|
+
label,
|
|
884
|
+
value,
|
|
885
|
+
width,
|
|
886
|
+
}: {
|
|
887
|
+
label: string;
|
|
888
|
+
value: number;
|
|
889
|
+
width: number;
|
|
890
|
+
}) {
|
|
891
|
+
const t = useTheme();
|
|
892
|
+
const innerWidth = Math.max(1, width - 4);
|
|
893
|
+
const pct = `${Math.round(value).toString().padStart(3)}%`;
|
|
894
|
+
const labelText = padDisplayText(label, Math.min(7, innerWidth));
|
|
895
|
+
const prefixWidth = displayWidth(labelText);
|
|
896
|
+
const barWidth = Math.max(
|
|
897
|
+
1,
|
|
898
|
+
innerWidth - prefixWidth - displayWidth(pct) - 2,
|
|
899
|
+
);
|
|
900
|
+
const bounded = Math.max(0, Math.min(100, value));
|
|
901
|
+
const filled = Math.max(
|
|
902
|
+
0,
|
|
903
|
+
Math.min(barWidth, Math.round((bounded / 100) * barWidth)),
|
|
904
|
+
);
|
|
905
|
+
const empty = Math.max(0, barWidth - filled);
|
|
906
|
+
const bar = `${"█".repeat(filled)}${"░".repeat(empty)}`;
|
|
907
|
+
const used = prefixWidth + 1 + displayWidth(bar) + 1 + displayWidth(pct);
|
|
908
|
+
const isLow = value < 25;
|
|
909
|
+
const barColor = isLow
|
|
910
|
+
? t.warning
|
|
911
|
+
: label === "deals"
|
|
912
|
+
? t.primaryDim
|
|
913
|
+
: t.primaryLight;
|
|
914
|
+
|
|
915
|
+
if (innerWidth < 14) {
|
|
916
|
+
return (
|
|
917
|
+
<PetStatsBodyLine
|
|
918
|
+
text={`${label} ${pct}`}
|
|
919
|
+
width={width}
|
|
920
|
+
color={isLow ? t.warning : t.text}
|
|
921
|
+
/>
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return (
|
|
926
|
+
<Text>
|
|
927
|
+
<Text color={t.primary}>│ </Text>
|
|
928
|
+
<Text color={t.dim}>{labelText} </Text>
|
|
929
|
+
<Text color={barColor}>{bar}</Text>
|
|
930
|
+
<Text color={isLow ? t.warning : t.dim}> {pct}</Text>
|
|
931
|
+
<Text color={t.primary}>
|
|
932
|
+
{" ".repeat(Math.max(0, innerWidth - used))} │
|
|
933
|
+
</Text>
|
|
934
|
+
</Text>
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function PetStatsReadout({
|
|
939
|
+
stats,
|
|
940
|
+
activity,
|
|
941
|
+
env,
|
|
942
|
+
width,
|
|
943
|
+
}: {
|
|
944
|
+
stats: PetStats;
|
|
945
|
+
activity: PetActivity;
|
|
946
|
+
env: Environment;
|
|
947
|
+
width: number;
|
|
948
|
+
}) {
|
|
949
|
+
const t = useTheme();
|
|
950
|
+
const panelWidth = Math.max(
|
|
951
|
+
1,
|
|
952
|
+
Math.min(PET_STATS_MAX_WIDTH, Math.floor(width)),
|
|
953
|
+
);
|
|
954
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
955
|
+
const mood = getPetMood(stats);
|
|
956
|
+
const rank = rankLabel(getPetRank(stats));
|
|
957
|
+
const name = stats.name ?? "Drexler";
|
|
958
|
+
const activityLabel = activity === "idle" ? "idle" : activity;
|
|
959
|
+
const title = "Pet Stats";
|
|
960
|
+
const topPrefix = "╭─ ";
|
|
961
|
+
const topSuffix = " ";
|
|
962
|
+
const topRule = "─".repeat(
|
|
963
|
+
Math.max(
|
|
964
|
+
0,
|
|
965
|
+
panelWidth -
|
|
966
|
+
displayWidth(topPrefix) -
|
|
967
|
+
displayWidth(title) -
|
|
968
|
+
displayWidth(topSuffix) -
|
|
969
|
+
displayWidth("╮"),
|
|
970
|
+
),
|
|
971
|
+
);
|
|
972
|
+
const memo = `memo ${getPetStatusMessage(stats, 0)}`;
|
|
973
|
+
|
|
974
|
+
if (panelWidth < PET_STATS_MIN_WIDTH) {
|
|
975
|
+
return (
|
|
976
|
+
<Box flexDirection="column" width={panelWidth}>
|
|
977
|
+
<Text bold color={t.primaryLight}>
|
|
978
|
+
{fitDisplayText("Pet Stats", panelWidth)}
|
|
979
|
+
</Text>
|
|
980
|
+
<Text color={t.text}>
|
|
981
|
+
{fitDisplayText(`${name} / ${rank}`, panelWidth)}
|
|
982
|
+
</Text>
|
|
983
|
+
<Text color={t.dim}>
|
|
984
|
+
{fitDisplayText(`${mood} / ${activityLabel}`, panelWidth)}
|
|
985
|
+
</Text>
|
|
986
|
+
</Box>
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return (
|
|
991
|
+
<Box flexDirection="column" width={panelWidth}>
|
|
992
|
+
<Text color={t.primary}>
|
|
993
|
+
{topPrefix}
|
|
994
|
+
<Text bold color={t.primaryLight}>{title}</Text>
|
|
995
|
+
{topSuffix}
|
|
996
|
+
{topRule}
|
|
997
|
+
╮
|
|
998
|
+
</Text>
|
|
999
|
+
<PetStatsBodyLine
|
|
1000
|
+
text={`name ${name}`}
|
|
1001
|
+
width={panelWidth}
|
|
1002
|
+
color={t.text}
|
|
1003
|
+
/>
|
|
1004
|
+
<PetStatsBodyLine
|
|
1005
|
+
text={`rank ${rank} · mood ${mood}`}
|
|
1006
|
+
width={panelWidth}
|
|
1007
|
+
color={t.primaryLight}
|
|
1008
|
+
/>
|
|
1009
|
+
<PetStatsBodyLine
|
|
1010
|
+
text={`activity ${activityLabel} · env ${env}`}
|
|
1011
|
+
width={panelWidth}
|
|
1012
|
+
color={t.dim}
|
|
1013
|
+
/>
|
|
1014
|
+
<PetStatsBodyLine
|
|
1015
|
+
text={`tenure ${formatTenure(petTenureMs(stats))}`}
|
|
1016
|
+
width={panelWidth}
|
|
1017
|
+
color={t.dim}
|
|
1018
|
+
/>
|
|
1019
|
+
<PetStatsBodyLine
|
|
1020
|
+
text={"─".repeat(innerWidth)}
|
|
1021
|
+
width={panelWidth}
|
|
1022
|
+
color={t.primaryDim}
|
|
1023
|
+
/>
|
|
1024
|
+
<PetDashboardStatBar
|
|
1025
|
+
label="happy"
|
|
1026
|
+
value={stats.happiness}
|
|
1027
|
+
width={panelWidth}
|
|
1028
|
+
/>
|
|
1029
|
+
<PetDashboardStatBar
|
|
1030
|
+
label="hunger"
|
|
1031
|
+
value={stats.hunger}
|
|
1032
|
+
width={panelWidth}
|
|
1033
|
+
/>
|
|
1034
|
+
<PetDashboardStatBar
|
|
1035
|
+
label="energy"
|
|
1036
|
+
value={stats.energy}
|
|
1037
|
+
width={panelWidth}
|
|
1038
|
+
/>
|
|
1039
|
+
<PetDashboardStatBar
|
|
1040
|
+
label="deals"
|
|
1041
|
+
value={stats.deals}
|
|
1042
|
+
width={panelWidth}
|
|
1043
|
+
/>
|
|
1044
|
+
<PetStatsBodyLine
|
|
1045
|
+
text={memo}
|
|
1046
|
+
width={panelWidth}
|
|
1047
|
+
color={t.dim}
|
|
1048
|
+
/>
|
|
1049
|
+
<Text color={t.primary}>{titledPanelBottom(panelWidth)}</Text>
|
|
1050
|
+
</Box>
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function PetDashboard({
|
|
1055
|
+
layout,
|
|
1056
|
+
stats,
|
|
1057
|
+
activity,
|
|
1058
|
+
env,
|
|
1059
|
+
isPaused,
|
|
1060
|
+
}: {
|
|
1061
|
+
layout: MascotLayout;
|
|
1062
|
+
stats: PetStats;
|
|
1063
|
+
activity: PetActivity;
|
|
1064
|
+
env: Environment;
|
|
1065
|
+
isPaused: boolean;
|
|
1066
|
+
}) {
|
|
1067
|
+
const t = useTheme();
|
|
1068
|
+
const sideBySide = layout.mode === "split";
|
|
1069
|
+
|
|
1070
|
+
if (layout.mode === "tiny" || layout.mode === "compact") {
|
|
1071
|
+
return (
|
|
1072
|
+
<Box
|
|
1073
|
+
marginLeft={layout.leftPanel.inset}
|
|
1074
|
+
width={layout.available}
|
|
1075
|
+
flexDirection="column"
|
|
1076
|
+
>
|
|
1077
|
+
<CompactPetPanel
|
|
1078
|
+
stats={stats}
|
|
1079
|
+
activity={activity}
|
|
1080
|
+
env={env}
|
|
1081
|
+
isPaused={isPaused}
|
|
1082
|
+
width={Math.max(1, layout.available)}
|
|
1083
|
+
/>
|
|
1084
|
+
</Box>
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return (
|
|
1089
|
+
<Box width={layout.available}>
|
|
1090
|
+
<Box
|
|
1091
|
+
width={layout.available}
|
|
1092
|
+
borderStyle="round"
|
|
1093
|
+
borderColor={t.primary}
|
|
1094
|
+
paddingX={1}
|
|
1095
|
+
flexDirection={sideBySide ? "row" : "column"}
|
|
1096
|
+
alignItems={sideBySide ? "flex-start" : "center"}
|
|
1097
|
+
>
|
|
1098
|
+
<Box
|
|
1099
|
+
flexDirection="column"
|
|
1100
|
+
width={layout.leftPanel.width}
|
|
1101
|
+
alignItems="center"
|
|
1102
|
+
>
|
|
1103
|
+
<PetSceneReadout
|
|
1104
|
+
stats={stats}
|
|
1105
|
+
activity={activity}
|
|
1106
|
+
env={env}
|
|
1107
|
+
isPaused={isPaused}
|
|
1108
|
+
width={layout.leftPanel.width}
|
|
1109
|
+
/>
|
|
1110
|
+
</Box>
|
|
1111
|
+
{sideBySide ? (
|
|
1112
|
+
<>
|
|
1113
|
+
<Box
|
|
1114
|
+
flexDirection="column"
|
|
1115
|
+
width={SPLIT_DIVIDER_WIDTH}
|
|
1116
|
+
flexShrink={0}
|
|
1117
|
+
>
|
|
1118
|
+
{PET_SPLIT_DIVIDER_ROWS.map((idx) => (
|
|
1119
|
+
<Text key={idx} color={t.primaryDim}>
|
|
1120
|
+
{" │ "}
|
|
1121
|
+
</Text>
|
|
1122
|
+
))}
|
|
1123
|
+
</Box>
|
|
1124
|
+
<Box
|
|
1125
|
+
flexDirection="column"
|
|
1126
|
+
width={layout.rightColumn.width}
|
|
1127
|
+
paddingRight={RIGHT_COLUMN_PAD_RIGHT}
|
|
1128
|
+
>
|
|
1129
|
+
<Box marginLeft={layout.dealDesk.inset}>
|
|
1130
|
+
<PetStatsReadout
|
|
1131
|
+
stats={stats}
|
|
1132
|
+
activity={activity}
|
|
1133
|
+
env={env}
|
|
1134
|
+
width={Math.min(PET_STATS_MAX_WIDTH, layout.dealDesk.width)}
|
|
1135
|
+
/>
|
|
1136
|
+
</Box>
|
|
1137
|
+
</Box>
|
|
1138
|
+
</>
|
|
1139
|
+
) : (
|
|
1140
|
+
<Box marginTop={1} width={layout.tips.width} alignItems="center">
|
|
1141
|
+
<PetStatsReadout
|
|
1142
|
+
stats={stats}
|
|
1143
|
+
activity={activity}
|
|
1144
|
+
env={env}
|
|
1145
|
+
width={Math.max(COMPACT_PET_PANEL_MIN_WIDTH, layout.tips.width)}
|
|
1146
|
+
/>
|
|
1147
|
+
</Box>
|
|
1148
|
+
)}
|
|
1149
|
+
</Box>
|
|
1150
|
+
</Box>
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
783
1154
|
export function MascotDashboard({
|
|
784
1155
|
greeting,
|
|
785
1156
|
width,
|
|
786
1157
|
mood,
|
|
1158
|
+
mode = "normal",
|
|
1159
|
+
petStats,
|
|
1160
|
+
petActivity = "idle",
|
|
1161
|
+
petEnv = "office",
|
|
1162
|
+
petPaused = false,
|
|
787
1163
|
bootProgress = 1,
|
|
788
1164
|
state = INTRO_FRAMES[INTRO_FRAMES.length - 1]!,
|
|
789
1165
|
bar = introBootBar(INTRO_FRAMES.length - 1, INTRO_FRAMES.length),
|
|
@@ -799,6 +1175,18 @@ export function MascotDashboard({
|
|
|
799
1175
|
? fixedDisplayRows(greeting, layout.copy.width, 2)
|
|
800
1176
|
: [];
|
|
801
1177
|
|
|
1178
|
+
if (mode === "pet" && petStats) {
|
|
1179
|
+
return (
|
|
1180
|
+
<PetDashboard
|
|
1181
|
+
layout={layout}
|
|
1182
|
+
stats={petStats}
|
|
1183
|
+
activity={petActivity}
|
|
1184
|
+
env={petEnv}
|
|
1185
|
+
isPaused={petPaused}
|
|
1186
|
+
/>
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
802
1190
|
if (layout.mode === "tiny") {
|
|
803
1191
|
return (
|
|
804
1192
|
<Box width={layout.available} flexDirection="column">
|
package/src/ui/PetPanel.tsx
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
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 { fitDisplayText } from "./graphemes.ts";
|
|
5
5
|
import { useTheme } from "./ThemeContext.tsx";
|
|
6
6
|
import { type Theme } from "./themes.ts";
|
|
7
7
|
|
|
8
|
-
export const PET_PANEL_WIDTH = 36;
|
|
9
|
-
export const PET_PANEL_ROWS = 22;
|
|
10
8
|
export const COMPACT_PET_PANEL_ROWS = 5;
|
|
11
9
|
export const TINY_PET_PANEL_ROWS = 1;
|
|
12
10
|
export const COMPACT_PET_PANEL_MIN_WIDTH = 48;
|
|
@@ -14,9 +12,8 @@ export type Environment = "office" | "home" | "outdoors";
|
|
|
14
12
|
|
|
15
13
|
const PANEL_BORDER_COLUMNS = 2;
|
|
16
14
|
const PANEL_PADDING_COLUMNS = 2;
|
|
17
|
-
const CONTENT =
|
|
15
|
+
const CONTENT = 32;
|
|
18
16
|
const SPRITE_W = 8;
|
|
19
|
-
const DIVIDER_LINE = "─".repeat(CONTENT);
|
|
20
17
|
|
|
21
18
|
const R_SKY = 0;
|
|
22
19
|
const R_BGA = 1;
|
|
@@ -25,6 +22,7 @@ const R_DECO = 3;
|
|
|
25
22
|
const R_SP0 = 4; // sprite occupies rows 4–9
|
|
26
23
|
const R_FLOOR = 10;
|
|
27
24
|
const SCENE_ROWS = 11;
|
|
25
|
+
export const PET_SCENE_WIDTH = CONTENT;
|
|
28
26
|
|
|
29
27
|
// Fixed sprite X — no left/right walking
|
|
30
28
|
const SPRITE_X: Record<PetActivity, number> = {
|
|
@@ -42,10 +40,6 @@ function place(base: string, text: string, x: number): string {
|
|
|
42
40
|
const fit = text.slice(0, end - x);
|
|
43
41
|
return base.slice(0, x) + fit + base.slice(end);
|
|
44
42
|
}
|
|
45
|
-
function pad(s: string, n: number): string {
|
|
46
|
-
const fitted = fitDisplayText(s, n);
|
|
47
|
-
return fitted + " ".repeat(Math.max(0, n - displayWidth(fitted)));
|
|
48
|
-
}
|
|
49
43
|
|
|
50
44
|
// Hoisted constant: floor pattern for home doesn't depend on frame.
|
|
51
45
|
const HOME_CARPET = Array.from({ length: CONTENT }, (_, i) =>
|
|
@@ -430,40 +424,33 @@ function getStatusMsg(stats: PetStats, frame: number): string {
|
|
|
430
424
|
return msgs[Math.floor(frame / 10) % msgs.length] ?? "Operational.";
|
|
431
425
|
}
|
|
432
426
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
label, value, barColor, labelColor, warnColor,
|
|
436
|
-
}: {
|
|
437
|
-
label: string; value: number; barColor: string; labelColor: string; warnColor: string;
|
|
438
|
-
}) {
|
|
439
|
-
const filled = Math.round((value / 100) * 14);
|
|
440
|
-
const bar = "█".repeat(filled) + "░".repeat(14 - filled);
|
|
441
|
-
const pct = String(Math.round(value)).padStart(3);
|
|
442
|
-
const isLow = value < 25;
|
|
443
|
-
return (
|
|
444
|
-
<Text>
|
|
445
|
-
<Text color={labelColor}>{pad(label, 6)}</Text>
|
|
446
|
-
<Text color={isLow ? warnColor : barColor}>{bar}</Text>
|
|
447
|
-
<Text color={isLow ? warnColor : labelColor}> {pct}%</Text>
|
|
448
|
-
</Text>
|
|
449
|
-
);
|
|
427
|
+
export function getPetStatusMessage(stats: PetStats, frame = 0): string {
|
|
428
|
+
return getStatusMsg(stats, frame);
|
|
450
429
|
}
|
|
451
|
-
const StatBar = memo(StatBarInner);
|
|
452
430
|
|
|
453
431
|
// ─── component ────────────────────────────────────────────────────────────────
|
|
454
|
-
interface
|
|
432
|
+
interface PetSceneProps {
|
|
455
433
|
stats: PetStats;
|
|
456
434
|
activity: PetActivity;
|
|
457
435
|
env?: Environment;
|
|
458
436
|
isPaused?: boolean;
|
|
459
437
|
}
|
|
460
438
|
|
|
461
|
-
interface CompactPetPanelProps extends
|
|
439
|
+
interface CompactPetPanelProps extends PetSceneProps {
|
|
462
440
|
width: number;
|
|
463
441
|
}
|
|
464
442
|
|
|
465
|
-
function
|
|
466
|
-
|
|
443
|
+
function usePetFrame({
|
|
444
|
+
activity,
|
|
445
|
+
env,
|
|
446
|
+
isPaused,
|
|
447
|
+
dead,
|
|
448
|
+
}: {
|
|
449
|
+
activity: PetActivity;
|
|
450
|
+
env: Environment;
|
|
451
|
+
isPaused: boolean;
|
|
452
|
+
dead: boolean;
|
|
453
|
+
}) {
|
|
467
454
|
const [frame, setFrame] = useState(0);
|
|
468
455
|
|
|
469
456
|
useEffect(() => {
|
|
@@ -474,79 +461,43 @@ function PetPanelView({ stats, activity, env = "office", isPaused = false }: Pet
|
|
|
474
461
|
// Skip frame ticks when paused or when the pet has died — DeathScreen
|
|
475
462
|
// takes over the UI, no point burning a setInterval that mutates state
|
|
476
463
|
// nothing will read.
|
|
477
|
-
if (isPaused ||
|
|
464
|
+
if (isPaused || dead) return;
|
|
478
465
|
const id = setInterval(() => {
|
|
479
466
|
setFrame((f) => f + 1);
|
|
480
467
|
}, 800);
|
|
481
468
|
return () => clearInterval(id);
|
|
482
|
-
}, [
|
|
469
|
+
}, [dead, isPaused]);
|
|
483
470
|
|
|
471
|
+
return frame;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function PetScene({
|
|
475
|
+
stats,
|
|
476
|
+
activity,
|
|
477
|
+
env = "office",
|
|
478
|
+
isPaused = false,
|
|
479
|
+
}: PetSceneProps) {
|
|
480
|
+
const t = useTheme();
|
|
481
|
+
const frame = usePetFrame({
|
|
482
|
+
activity,
|
|
483
|
+
env,
|
|
484
|
+
isPaused,
|
|
485
|
+
dead: stats.dead === true,
|
|
486
|
+
});
|
|
484
487
|
const scene = useMemo(
|
|
485
488
|
() => buildScene(activity, frame, stats, env),
|
|
486
489
|
[activity, frame, stats, env],
|
|
487
490
|
);
|
|
488
|
-
const mood = useMemo(() => getPetMood(stats), [stats]);
|
|
489
|
-
const status = useMemo(
|
|
490
|
-
() => pad(`memo ${getStatusMsg(stats, frame)}`, CONTENT),
|
|
491
|
-
[stats, frame],
|
|
492
|
-
);
|
|
493
|
-
const title = fitDisplayText(`DREXLER PET DESK [${env}]`, CONTENT);
|
|
494
|
-
const activityLabel = activity !== "idle" ? ` / ${activity}` : "";
|
|
495
|
-
const moodLabel = `mood ${mood}`;
|
|
496
|
-
const fittedMood = activityLabel
|
|
497
|
-
? fitDisplayText(moodLabel, Math.max(1, CONTENT - displayWidth(activityLabel)))
|
|
498
|
-
: fitDisplayText(moodLabel, CONTENT);
|
|
499
|
-
const fittedActivity = activityLabel && displayWidth(fittedMood) < CONTENT
|
|
500
|
-
? fitDisplayText(activityLabel, CONTENT - displayWidth(fittedMood))
|
|
501
|
-
: "";
|
|
502
491
|
|
|
503
492
|
return (
|
|
504
|
-
<Box
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
borderStyle="round"
|
|
509
|
-
borderColor={t.primaryDim}
|
|
510
|
-
>
|
|
511
|
-
<Box paddingX={1} justifyContent="center">
|
|
512
|
-
<Text color={t.primary} bold>{title}</Text>
|
|
513
|
-
</Box>
|
|
514
|
-
|
|
515
|
-
<Box flexDirection="column" paddingX={1}>
|
|
516
|
-
{scene.map((row, i) => (
|
|
517
|
-
<Text key={i} color={rowColor(i, activity, frame, t)}>{row}</Text>
|
|
518
|
-
))}
|
|
519
|
-
</Box>
|
|
520
|
-
|
|
521
|
-
<Box paddingX={1}>
|
|
522
|
-
<Text color={t.primaryDim}>{DIVIDER_LINE}</Text>
|
|
523
|
-
</Box>
|
|
524
|
-
|
|
525
|
-
<Box flexDirection="column" paddingX={1}>
|
|
526
|
-
<StatBar label="happy" value={stats.happiness} barColor={t.primary} labelColor={t.dim} warnColor={t.error} />
|
|
527
|
-
<StatBar label="hungr" value={stats.hunger} barColor={t.primaryLight} labelColor={t.dim} warnColor={t.warning} />
|
|
528
|
-
<StatBar label="enrgy" value={stats.energy} barColor={t.primaryLight} labelColor={t.dim} warnColor={t.warning} />
|
|
529
|
-
<StatBar label="deals" value={stats.deals} barColor={t.primaryDim} labelColor={t.dim} warnColor={t.warning} />
|
|
530
|
-
</Box>
|
|
531
|
-
|
|
532
|
-
<Box paddingX={1}>
|
|
533
|
-
<Text color={t.primaryDim}>{DIVIDER_LINE}</Text>
|
|
534
|
-
</Box>
|
|
535
|
-
|
|
536
|
-
<Box paddingX={1}>
|
|
537
|
-
<Text color={t.dim}>{status}</Text>
|
|
538
|
-
</Box>
|
|
539
|
-
|
|
540
|
-
<Box paddingX={1}>
|
|
541
|
-
<Text color={t.dim}>{fittedMood}</Text>
|
|
542
|
-
{fittedActivity && <Text color={t.primaryDim}>{fittedActivity}</Text>}
|
|
543
|
-
</Box>
|
|
493
|
+
<Box flexDirection="column" width={PET_SCENE_WIDTH}>
|
|
494
|
+
{scene.map((row, i) => (
|
|
495
|
+
<Text key={i} color={rowColor(i, activity, frame, t)}>{row}</Text>
|
|
496
|
+
))}
|
|
544
497
|
</Box>
|
|
545
498
|
);
|
|
546
499
|
}
|
|
547
500
|
|
|
548
|
-
export const PetPanel = memo(PetPanelView);
|
|
549
|
-
|
|
550
501
|
function pct(value: number): string {
|
|
551
502
|
return `${Math.round(value)}%`;
|
|
552
503
|
}
|
|
@@ -580,7 +531,7 @@ interface WorstStat {
|
|
|
580
531
|
value: number;
|
|
581
532
|
}
|
|
582
533
|
|
|
583
|
-
|
|
534
|
+
function pickWorstStat(stats: PetStats): WorstStat {
|
|
584
535
|
const entries: WorstStat[] = [
|
|
585
536
|
{ key: "hunger", value: stats.hunger },
|
|
586
537
|
{ key: "happiness", value: stats.happiness },
|