drexler 0.2.20 → 0.2.22
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/commands.ts +18 -3
- package/src/config.ts +37 -16
- package/src/llm.ts +44 -3
- package/src/pet/petState.ts +10 -0
- package/src/repl.ts +8 -3
- package/src/ui/App.tsx +23 -1
- package/src/ui/CommandPalette.tsx +10 -1
- package/src/ui/MascotIntro.tsx +1 -1
- package/src/ui/PetPanel.tsx +127 -61
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { resolve as pathResolve } from "node:path";
|
|
3
|
+
import { isAbsolute, resolve as pathResolve, sep as pathSep } from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
getConfigPath,
|
|
6
6
|
getDrexlerVersion,
|
|
@@ -499,9 +499,24 @@ function resolveWriteTarget(
|
|
|
499
499
|
ctx.print(error(`Invalid path: ${pathArg} (no '..' segments allowed).`));
|
|
500
500
|
return null;
|
|
501
501
|
}
|
|
502
|
+
const cwd = process.cwd();
|
|
502
503
|
const target = pathArg
|
|
503
|
-
? pathResolve(pathArg)
|
|
504
|
-
: pathResolve(`${defaultPrefix}-${Date.now()}${requiredExt}`);
|
|
504
|
+
? pathResolve(cwd, pathArg)
|
|
505
|
+
: pathResolve(cwd, `${defaultPrefix}-${Date.now()}${requiredExt}`);
|
|
506
|
+
// Sandbox relative paths to cwd. Absolute paths are user-explicit
|
|
507
|
+
// (e.g. `/save /tmp/notes.md`) and trusted — drexler runs with the
|
|
508
|
+
// user's own permissions, so they can write anywhere they could write
|
|
509
|
+
// from the shell. Defense in depth catches relative paths that, after
|
|
510
|
+
// resolution, would land outside cwd via a sneaky construction.
|
|
511
|
+
if (pathArg && !isAbsolute(pathArg)) {
|
|
512
|
+
const cwdPrefix = cwd.endsWith(pathSep) ? cwd : cwd + pathSep;
|
|
513
|
+
if (target !== cwd && !target.startsWith(cwdPrefix)) {
|
|
514
|
+
ctx.print(
|
|
515
|
+
error(`Invalid path: ${pathArg} (resolves outside current directory).`),
|
|
516
|
+
);
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
505
520
|
if (!target.toLowerCase().endsWith(requiredExt)) {
|
|
506
521
|
ctx.print(error(`Target must end in ${requiredExt}: ${target}`));
|
|
507
522
|
return null;
|
package/src/config.ts
CHANGED
|
@@ -151,28 +151,49 @@ async function validatePersonaFile(
|
|
|
151
151
|
return resolved;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const path = existsSync(cp) ? cp : existsSync(lp) ? lp : null;
|
|
158
|
-
if (!path) return {};
|
|
154
|
+
async function readConfigPath(
|
|
155
|
+
path: string,
|
|
156
|
+
): Promise<{ raw: string; missing: false } | { raw: null; missing: true } | { raw: null; missing: false; error: NodeJS.ErrnoException }> {
|
|
159
157
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
return { raw: await readFile(path, "utf-8"), missing: false };
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
161
|
+
if (code === "ENOENT") return { raw: null, missing: true };
|
|
162
|
+
return { raw: null, missing: false, error: err as NodeJS.ErrnoException };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function loadConfigFile(): Promise<Partial<Config>> {
|
|
167
|
+
// Try canonical XDG path first, then legacy ~/.drexlerrc. Reading
|
|
168
|
+
// unconditionally (instead of gating on existsSync) means EACCES
|
|
169
|
+
// surfaces a warning rather than silently masquerading as "no file".
|
|
170
|
+
for (const path of [configPath(), legacyConfigPath()]) {
|
|
171
|
+
const result = await readConfigPath(path);
|
|
172
|
+
if (result.missing) continue;
|
|
173
|
+
if (result.raw === null) {
|
|
163
174
|
console.warn(
|
|
164
|
-
`Drexler config at ${path}
|
|
175
|
+
`Drexler config at ${path} could not be read (${result.error.code ?? result.error.message}); ignoring (defaults applied).`,
|
|
176
|
+
);
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const parsed: unknown = JSON.parse(result.raw);
|
|
181
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
182
|
+
console.warn(
|
|
183
|
+
`Drexler config at ${path} is not a JSON object; ignoring (defaults applied).`,
|
|
184
|
+
);
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
return parsed as Partial<Config>;
|
|
188
|
+
} catch (err) {
|
|
189
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
190
|
+
console.warn(
|
|
191
|
+
`Drexler config at ${path} could not be parsed (${msg}); ignoring (defaults applied).`,
|
|
165
192
|
);
|
|
166
193
|
return {};
|
|
167
194
|
}
|
|
168
|
-
return parsed as Partial<Config>;
|
|
169
|
-
} catch (err) {
|
|
170
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
171
|
-
console.warn(
|
|
172
|
-
`Drexler config at ${path} could not be read (${msg}); ignoring (defaults applied).`,
|
|
173
|
-
);
|
|
174
|
-
return {};
|
|
175
195
|
}
|
|
196
|
+
return {};
|
|
176
197
|
}
|
|
177
198
|
|
|
178
199
|
export async function saveConfig(partial: Partial<Config>): Promise<void> {
|
package/src/llm.ts
CHANGED
|
@@ -11,6 +11,32 @@ const STOP_SEQUENCES = [
|
|
|
11
11
|
];
|
|
12
12
|
const RETRY_DELAY_MS = 250;
|
|
13
13
|
|
|
14
|
+
function abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
15
|
+
return new Promise<void>((resolve, reject) => {
|
|
16
|
+
if (signal?.aborted) {
|
|
17
|
+
reject(new Error("aborted before retry"));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const timer = setTimeout(() => {
|
|
21
|
+
signal?.removeEventListener("abort", onAbort);
|
|
22
|
+
resolve();
|
|
23
|
+
}, ms);
|
|
24
|
+
const onAbort = () => {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
reject(new Error("aborted before retry"));
|
|
27
|
+
};
|
|
28
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function debugWarn(label: string, detail: string): void {
|
|
33
|
+
if (process.env.DREXLER_DEBUG && process.env.DREXLER_DEBUG !== "0") {
|
|
34
|
+
try {
|
|
35
|
+
process.stderr.write(`[drexler ${label}] ${detail}\n`);
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
export type FetchFn = (
|
|
15
41
|
url: string | URL | Request,
|
|
16
42
|
init?: RequestInit,
|
|
@@ -112,7 +138,18 @@ async function attempt(
|
|
|
112
138
|
try {
|
|
113
139
|
await res.text();
|
|
114
140
|
} catch {}
|
|
115
|
-
|
|
141
|
+
// Retry delay must be abortable — if the user hits Esc during the
|
|
142
|
+
// wait, we should bail immediately instead of firing the second
|
|
143
|
+
// request only to cancel it once issued.
|
|
144
|
+
try {
|
|
145
|
+
await abortableDelay(RETRY_DELAY_MS, opts.signal);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return {
|
|
148
|
+
status: "http_error",
|
|
149
|
+
content: "",
|
|
150
|
+
error: err instanceof Error ? err.message : String(err),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
116
153
|
return attempt(model, opts, fetchFn, true);
|
|
117
154
|
}
|
|
118
155
|
|
|
@@ -189,8 +226,12 @@ export async function parseSSEStream(
|
|
|
189
226
|
acc += tok;
|
|
190
227
|
onToken(tok);
|
|
191
228
|
}
|
|
192
|
-
} catch {
|
|
193
|
-
//
|
|
229
|
+
} catch (err) {
|
|
230
|
+
// Tolerate malformed chunks — OpenRouter occasionally emits
|
|
231
|
+
// partial JSON during slow connections. Visible only when
|
|
232
|
+
// DREXLER_DEBUG is set so production stays quiet.
|
|
233
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
234
|
+
debugWarn("sse parse", `${msg}: ${data.slice(0, 80)}`);
|
|
194
235
|
}
|
|
195
236
|
};
|
|
196
237
|
|
package/src/pet/petState.ts
CHANGED
|
@@ -33,10 +33,16 @@ export interface PetStats {
|
|
|
33
33
|
|
|
34
34
|
const MAX_NAME_LEN = 16;
|
|
35
35
|
const NAME_SANITIZE_RE = /[^\p{L}\p{N} ._'-]/gu;
|
|
36
|
+
// Strip ALL Unicode format/control marks (bidi overrides, ZWJ/ZWNJ,
|
|
37
|
+
// BOM, word joiner, mathematical invisibles) before character-class
|
|
38
|
+
// filtering — otherwise a name like "Max" can render as "xaM" via an
|
|
39
|
+
// embedded U+202E RIGHT-TO-LEFT OVERRIDE that survives NFKC.
|
|
40
|
+
const NAME_BIDI_STRIP_RE = /\p{Cf}/gu;
|
|
36
41
|
|
|
37
42
|
export function sanitizePetName(input: string): string {
|
|
38
43
|
const cleaned = input
|
|
39
44
|
.normalize("NFKC")
|
|
45
|
+
.replace(NAME_BIDI_STRIP_RE, "")
|
|
40
46
|
.replace(NAME_SANITIZE_RE, "")
|
|
41
47
|
.replace(/\s+/g, " ")
|
|
42
48
|
.trim();
|
|
@@ -76,6 +82,10 @@ export function actionCooldown(
|
|
|
76
82
|
return { ok: true, remainingMs: 0 };
|
|
77
83
|
}
|
|
78
84
|
const elapsed = now - last;
|
|
85
|
+
// Clock skew backwards (timestamp set in the future) shouldn't lock
|
|
86
|
+
// the user out for the cooldown window — treat as no cooldown and
|
|
87
|
+
// let the next stampAction overwrite the stale future value.
|
|
88
|
+
if (elapsed < 0) return { ok: true, remainingMs: 0 };
|
|
79
89
|
if (elapsed >= PET_COOLDOWN_MS) return { ok: true, remainingMs: 0 };
|
|
80
90
|
return { ok: false, remainingMs: PET_COOLDOWN_MS - elapsed };
|
|
81
91
|
}
|
package/src/repl.ts
CHANGED
|
@@ -69,13 +69,18 @@ export function buildMessagesWithReminder(conv: Conversation): Message[] {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Confusable letters that look like Latin "I" — fold to ASCII before regex
|
|
72
|
-
// so detection isn't bypassed by Cyrillic І, Turkish İ, fullwidth I,
|
|
73
|
-
|
|
72
|
+
// so detection isn't bypassed by Cyrillic І, Turkish İ, fullwidth I,
|
|
73
|
+
// Greek Iota Ι/ι, script ℐ, Roman numeral Ⅰ.
|
|
74
|
+
const I_CONFUSABLES_RE = /[ІіİıIℐⅠΙι]/g;
|
|
74
75
|
|
|
75
76
|
export function detectPersonaDrift(content: string): boolean {
|
|
76
77
|
const noCode = content
|
|
77
78
|
.replace(/```[\s\S]*?```/g, "")
|
|
78
|
-
.replace(/`[^`]*`/g, "")
|
|
79
|
+
.replace(/`[^`]*`/g, "")
|
|
80
|
+
// Strip LaTeX-style inline math $...$ and display math $$...$$ so
|
|
81
|
+
// `$I = mc^2$` doesn't trip drift detection.
|
|
82
|
+
.replace(/\$\$[\s\S]*?\$\$/g, "")
|
|
83
|
+
.replace(/\$[^\$\n]*\$/g, "");
|
|
79
84
|
const folded = noCode.normalize("NFKC").replace(I_CONFUSABLES_RE, "I");
|
|
80
85
|
return /\bI\b|\bI'm\b|\bI'll\b|\bI've\b|\bI'd\b/.test(folded);
|
|
81
86
|
}
|
package/src/ui/App.tsx
CHANGED
|
@@ -910,6 +910,21 @@ export function App({
|
|
|
910
910
|
return;
|
|
911
911
|
}
|
|
912
912
|
if (isSlash(line)) {
|
|
913
|
+
// Bare /theme, /model, /startup, /retry, /export — repopulate the
|
|
914
|
+
// input with "<cmd> " so the palette catches the argument chooser
|
|
915
|
+
// and the user picks via ↑↓ + Enter. Avoids the "print current
|
|
916
|
+
// value and dead-end" feeling of dispatching the base command.
|
|
917
|
+
const lower = line.toLowerCase();
|
|
918
|
+
if (isArgumentParentCommand(lower)) {
|
|
919
|
+
const filled = `${lower} `;
|
|
920
|
+
updateDraft({ value: filled, cursor: graphemeLength(filled) });
|
|
921
|
+
setPaletteIdx(0);
|
|
922
|
+
addItem(
|
|
923
|
+
"system",
|
|
924
|
+
`Pick a ${lower.slice(1)} option below — ↑↓ to choose, Enter to apply, Esc to cancel.`,
|
|
925
|
+
);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
913
928
|
await handleSlashWithMutation(line);
|
|
914
929
|
return;
|
|
915
930
|
}
|
|
@@ -918,7 +933,14 @@ export function App({
|
|
|
918
933
|
setMsgCount(conversation.length);
|
|
919
934
|
await runLLM();
|
|
920
935
|
},
|
|
921
|
-
[
|
|
936
|
+
[
|
|
937
|
+
addItem,
|
|
938
|
+
conversation,
|
|
939
|
+
handleSlashWithMutation,
|
|
940
|
+
runLLM,
|
|
941
|
+
setPaletteIdx,
|
|
942
|
+
updateDraft,
|
|
943
|
+
],
|
|
922
944
|
);
|
|
923
945
|
|
|
924
946
|
const reportSubmitError = useCallback(
|
|
@@ -10,7 +10,7 @@ interface Props {
|
|
|
10
10
|
width?: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const COMMAND_HINTS: Record<string, string> = {
|
|
13
|
+
export const COMMAND_HINTS: Record<string, string> = {
|
|
14
14
|
"/help": "open directive list",
|
|
15
15
|
"/clear": "reset transcript",
|
|
16
16
|
"/exit": "close session",
|
|
@@ -31,6 +31,15 @@ const COMMAND_HINTS: Record<string, string> = {
|
|
|
31
31
|
"/copy-last": "copy latest response",
|
|
32
32
|
"/setup": "show config + key source",
|
|
33
33
|
"/update": "bun update -g drexler --latest",
|
|
34
|
+
"/pet": "open pet deal desk",
|
|
35
|
+
"/feed": "feed Drexler a deal memo",
|
|
36
|
+
"/play": "corporate synergy game",
|
|
37
|
+
"/work": "grind the deal pipeline",
|
|
38
|
+
"/praise": "affirm Drexler's contributions",
|
|
39
|
+
"/rest": "strategic nap",
|
|
40
|
+
"/vibe": "let Drexler pick the move",
|
|
41
|
+
"/name": "/name Bartholomew",
|
|
42
|
+
"/profile": "print personnel file",
|
|
34
43
|
};
|
|
35
44
|
|
|
36
45
|
const ARGUMENT_TITLES: Record<string, { title: string; hint: string }> = {
|
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 = 19;
|
|
175
175
|
const PET_SPLIT_DIVIDER_ROWS: number[] = Array.from(
|
|
176
176
|
{ length: PET_SPLIT_DIVIDER_HEIGHT },
|
|
177
177
|
(_, i) => i,
|
package/src/ui/PetPanel.tsx
CHANGED
|
@@ -18,7 +18,7 @@ export type Environment = "office" | "home" | "outdoors";
|
|
|
18
18
|
|
|
19
19
|
const PANEL_BORDER_COLUMNS = 2;
|
|
20
20
|
const PANEL_PADDING_COLUMNS = 2;
|
|
21
|
-
const SCENE_ROWS =
|
|
21
|
+
const SCENE_ROWS = 18;
|
|
22
22
|
const R_WALL = 0;
|
|
23
23
|
const R_WINDOW_TOP = 1;
|
|
24
24
|
const R_WINDOW_BOTTOM = 4;
|
|
@@ -26,7 +26,9 @@ const R_ACTIVITY = 5;
|
|
|
26
26
|
const R_MASCOT_START = 6;
|
|
27
27
|
const R_DESK_SURFACE = R_MASCOT_START + BRIEFCASE_FINAL.length;
|
|
28
28
|
const R_DESK_FRONT = R_DESK_SURFACE + 1;
|
|
29
|
-
const
|
|
29
|
+
const R_DESK_DRAWERS = R_DESK_FRONT + 1;
|
|
30
|
+
const R_DESK_BOTTOM = R_DESK_DRAWERS + 1;
|
|
31
|
+
const R_FLOOR = R_DESK_BOTTOM + 1;
|
|
30
32
|
|
|
31
33
|
export const PET_SCENE_WIDTH = 52;
|
|
32
34
|
|
|
@@ -37,6 +39,14 @@ function place(base: string, text: string, x: number): string {
|
|
|
37
39
|
return base.slice(0, x) + fit + base.slice(end);
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
function placeSprite(rows: string[], row: number, x: number, sprite: readonly string[]): void {
|
|
43
|
+
for (let i = 0; i < sprite.length; i++) {
|
|
44
|
+
const targetRow = row + i;
|
|
45
|
+
if (targetRow < 0 || targetRow >= rows.length) continue;
|
|
46
|
+
rows[targetRow] = place(rows[targetRow] ?? "", sprite[i] ?? "", x);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
function blankRow(width: number): string {
|
|
41
51
|
return " ".repeat(width);
|
|
42
52
|
}
|
|
@@ -47,6 +57,13 @@ function padDisplayText(input: string, width: number): string {
|
|
|
47
57
|
return `${fitted}${" ".repeat(Math.max(0, safeWidth - displayWidth(fitted)))}`;
|
|
48
58
|
}
|
|
49
59
|
|
|
60
|
+
function centerPadDisplayText(input: string, width: number): string {
|
|
61
|
+
const safeWidth = Math.max(1, width);
|
|
62
|
+
const fitted = fitDisplayText(input, safeWidth);
|
|
63
|
+
const left = Math.max(0, Math.floor((safeWidth - displayWidth(fitted)) / 2));
|
|
64
|
+
return `${" ".repeat(left)}${fitted}${" ".repeat(Math.max(0, safeWidth - left - displayWidth(fitted)))}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
function overlayFitted(row: string, text: string, x: number, width: number): string {
|
|
51
68
|
return place(row, padDisplayText(text, width), x);
|
|
52
69
|
}
|
|
@@ -98,9 +115,20 @@ function placeBoxLines(
|
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
function cupForEnergy(energy: number): string {
|
|
101
|
-
if (energy > 60) return "
|
|
102
|
-
if (energy > 30) return "
|
|
103
|
-
return "
|
|
118
|
+
if (energy > 60) return "c~";
|
|
119
|
+
if (energy > 30) return "c-";
|
|
120
|
+
return "c_";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function analogClockSprite(frame: number): readonly string[] {
|
|
124
|
+
const hands = [
|
|
125
|
+
["│╲│ │", "│ └─│"],
|
|
126
|
+
["│ │╱│", "│─┘ │"],
|
|
127
|
+
["│ │ │", "│─┼─│"],
|
|
128
|
+
["│╲│ │", "│─┘ │"],
|
|
129
|
+
] as const;
|
|
130
|
+
const [upper, lower] = hands[Math.floor(frame / 2) % hands.length] ?? hands[0]!;
|
|
131
|
+
return ["╭───╮", upper, lower, "╰───╯"];
|
|
104
132
|
}
|
|
105
133
|
|
|
106
134
|
function progressTicker(frame: number): string {
|
|
@@ -207,17 +235,25 @@ function drawOfficeBackground(rows: string[], width: number, frame: number, stat
|
|
|
207
235
|
Math.max(0, width - displayWidth(`pipe ${dealPct}`) - 1),
|
|
208
236
|
);
|
|
209
237
|
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
|
|
238
|
+
const compact = width < 62;
|
|
239
|
+
const windowWidth = compact
|
|
240
|
+
? 17
|
|
241
|
+
: Math.min(30, Math.max(20, Math.floor(width * 0.32)));
|
|
242
|
+
const boardWidth = compact
|
|
243
|
+
? Math.min(25, Math.max(20, width - windowWidth - 5))
|
|
244
|
+
: Math.min(36, Math.max(26, Math.floor(width * 0.36)));
|
|
245
|
+
const boardX = Math.max(windowWidth + 3, width - boardWidth - 2);
|
|
246
|
+
const windowRight = 1 + windowWidth;
|
|
247
|
+
const gapWidth = boardX - windowRight;
|
|
213
248
|
const cloud = frame % 6 < 3 ? "(~~)" : " (~~)";
|
|
214
249
|
const sun = frame % 12 < 6 ? "\\o/" : "-o-";
|
|
250
|
+
const city = frame % 10 < 5 ? "▂▄▆ city" : "▃▅▇ city";
|
|
215
251
|
const tape = frame % 8 < 4 ? "▁▃▅▇" : "▂▄▆█";
|
|
216
252
|
const cursor = frame % 4 < 2 ? ">" : "*";
|
|
217
253
|
|
|
218
254
|
placeBoxLines(rows, R_WINDOW_TOP, 1, windowWidth, "Window", [
|
|
219
|
-
|
|
220
|
-
|
|
255
|
+
`╔╤╤╗ ${sun} ${cloud}`,
|
|
256
|
+
`║▥▥║ ${city}`,
|
|
221
257
|
]);
|
|
222
258
|
placeBoxLines(
|
|
223
259
|
rows,
|
|
@@ -226,11 +262,16 @@ function drawOfficeBackground(rows: string[], width: number, frame: number, stat
|
|
|
226
262
|
Math.min(boardWidth, width - boardX),
|
|
227
263
|
"Deal Board",
|
|
228
264
|
[
|
|
229
|
-
`DL
|
|
230
|
-
`PIPE ${tape} ${cursor}`,
|
|
265
|
+
`DL ${dealPct} FEE ${Math.round(stats.happiness).toString().padStart(3)}%`,
|
|
266
|
+
`PIPE ${tape} $ ${cursor}`,
|
|
231
267
|
],
|
|
232
268
|
);
|
|
233
269
|
|
|
270
|
+
if (gapWidth >= 5) {
|
|
271
|
+
const clockX = windowRight + Math.floor((gapWidth - 5) / 2);
|
|
272
|
+
placeSprite(rows, R_WINDOW_TOP, clockX, analogClockSprite(frame));
|
|
273
|
+
}
|
|
274
|
+
|
|
234
275
|
rows[R_ACTIVITY] = centerText(
|
|
235
276
|
"─".repeat(width),
|
|
236
277
|
buildActivityLine("idle", frame),
|
|
@@ -238,25 +279,30 @@ function drawOfficeBackground(rows: string[], width: number, frame: number, stat
|
|
|
238
279
|
}
|
|
239
280
|
|
|
240
281
|
function drawOfficeFurniture(rows: string[], width: number, frame: number): void {
|
|
241
|
-
const lampX =
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
rows
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
rows
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
282
|
+
const lampX = 1;
|
|
283
|
+
const cabinetX = Math.max(1, width - 8);
|
|
284
|
+
const shade = frame % 8 < 4 ? "╱____╲" : "╱ ╲";
|
|
285
|
+
const plantTop = frame % 6 < 3 ? " ╲│╱ " : " ╱│╲ ";
|
|
286
|
+
|
|
287
|
+
placeSprite(rows, R_MASCOT_START, lampX, [
|
|
288
|
+
" ╭──╮ ",
|
|
289
|
+
` ${shade}`,
|
|
290
|
+
" ╰─┬──╯",
|
|
291
|
+
" │ ",
|
|
292
|
+
" ╭─┴─╮ ",
|
|
293
|
+
" │IN │ ",
|
|
294
|
+
" ╰───╯ ",
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
placeSprite(rows, R_MASCOT_START, cabinetX, [
|
|
298
|
+
plantTop,
|
|
299
|
+
" │ ",
|
|
300
|
+
" ╭┴╮ ",
|
|
301
|
+
"╭FILE╮",
|
|
302
|
+
"│▤▤▤│",
|
|
303
|
+
"├────┤",
|
|
304
|
+
"╰────╯",
|
|
305
|
+
]);
|
|
260
306
|
}
|
|
261
307
|
|
|
262
308
|
function drawActivityAccents(
|
|
@@ -272,16 +318,16 @@ function drawActivityAccents(
|
|
|
272
318
|
);
|
|
273
319
|
|
|
274
320
|
const mascotRight = mascotX + MASCOT_WIDTH;
|
|
275
|
-
const leftAccentX = Math.max(1, mascotX -
|
|
276
|
-
const fileX = Math.max(1, width -
|
|
277
|
-
const rightAccentX = Math.min(fileX -
|
|
321
|
+
const leftAccentX = Math.max(1, mascotX - 3);
|
|
322
|
+
const fileX = Math.max(1, width - 8);
|
|
323
|
+
const rightAccentX = Math.min(fileX - 6, mascotRight + 2);
|
|
278
324
|
|
|
279
325
|
switch (activity) {
|
|
280
326
|
case "eating":
|
|
281
|
-
rows[R_MASCOT_START +
|
|
282
|
-
rows[R_MASCOT_START +
|
|
283
|
-
"[$]
|
|
284
|
-
Math.max(1, Math.min(fileX -
|
|
327
|
+
rows[R_MASCOT_START + 3] = place(
|
|
328
|
+
rows[R_MASCOT_START + 3],
|
|
329
|
+
"[$]",
|
|
330
|
+
Math.max(1, Math.min(fileX - 5, rightAccentX + 1)),
|
|
285
331
|
);
|
|
286
332
|
break;
|
|
287
333
|
case "playing":
|
|
@@ -330,33 +376,36 @@ function drawDesktopObjects(
|
|
|
330
376
|
): void {
|
|
331
377
|
const mascotX = Math.max(0, Math.floor((width - MASCOT_WIDTH) / 2));
|
|
332
378
|
const mascotRight = mascotX + MASCOT_WIDTH;
|
|
333
|
-
const
|
|
334
|
-
const laptopX = Math.max(8, mascotX -
|
|
335
|
-
const papersX = Math.min(
|
|
336
|
-
const
|
|
379
|
+
const cabinetX = Math.max(1, width - 8);
|
|
380
|
+
const laptopX = Math.max(8, mascotX - 9);
|
|
381
|
+
const papersX = Math.min(cabinetX - 10, mascotRight + 2);
|
|
382
|
+
const mugX = Math.min(cabinetX - 5, mascotRight + 6);
|
|
337
383
|
const cursor = frame % 2 === 0 ? "_" : " ";
|
|
338
384
|
const screen =
|
|
339
385
|
activity === "working"
|
|
340
|
-
? `$>${cursor}
|
|
386
|
+
? `$>${cursor}DL`
|
|
341
387
|
: activity === "sleeping"
|
|
342
388
|
? "zzz..."
|
|
343
389
|
: "DREX";
|
|
344
390
|
const steam = stats.energy > 30
|
|
345
391
|
? frame % 4 < 2 ? " ((" : " ))"
|
|
346
392
|
: " ";
|
|
347
|
-
const paperFace = frame % 6 < 3 ? "
|
|
393
|
+
const paperFace = frame % 6 < 3 ? "▱▱▱" : "▰▱▱";
|
|
348
394
|
|
|
349
|
-
rows
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
395
|
+
placeSprite(rows, R_MASCOT_START + 4, laptopX, [
|
|
396
|
+
"╭──────╮",
|
|
397
|
+
`│${padDisplayText(screen, 6)}│`,
|
|
398
|
+
"╰─┬──┬─╯",
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
if (papersX > mascotRight) {
|
|
402
|
+
rows[R_MASCOT_START + 6] = place(rows[R_MASCOT_START + 6], paperFace, papersX);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (mugX > mascotRight + 1) {
|
|
406
|
+
rows[R_MASCOT_START + 4] = place(rows[R_MASCOT_START + 4], steam, mugX + 1);
|
|
407
|
+
rows[R_MASCOT_START + 5] = place(rows[R_MASCOT_START + 5], `╭${cupForEnergy(stats.energy)}╮`, mugX);
|
|
408
|
+
rows[R_MASCOT_START + 6] = place(rows[R_MASCOT_START + 6], "╰──╯", mugX);
|
|
360
409
|
}
|
|
361
410
|
}
|
|
362
411
|
|
|
@@ -364,9 +413,11 @@ function drawDesk(rows: string[], width: number, stats: PetStats): void {
|
|
|
364
413
|
const deskX = width > PET_SCENE_WIDTH ? 2 : 1;
|
|
365
414
|
const deskWidth = Math.max(4, width - deskX * 2);
|
|
366
415
|
const deskInner = Math.max(1, deskWidth - 2);
|
|
367
|
-
const surface =
|
|
368
|
-
const
|
|
369
|
-
const
|
|
416
|
+
const surface = "▱▱▱ ▬▬▬▬▬ COV OK";
|
|
417
|
+
const front = `[IN] ║ DREXLER DEAL DESK ║ PIPE ${Math.round(stats.deals)}% ║ [OUT]`;
|
|
418
|
+
const drawers = width < 68
|
|
419
|
+
? "│▤▤│ │▤▤│ │▤▤│"
|
|
420
|
+
: "│▤▤│ │▤▤│ │▤▤│ │▤▤│";
|
|
370
421
|
|
|
371
422
|
rows[R_DESK_SURFACE] = place(
|
|
372
423
|
rows[R_DESK_SURFACE],
|
|
@@ -378,11 +429,20 @@ function drawDesk(rows: string[], width: number, stats: PetStats): void {
|
|
|
378
429
|
`│${padDisplayText(front, deskInner)}│`,
|
|
379
430
|
deskX,
|
|
380
431
|
);
|
|
381
|
-
rows[
|
|
382
|
-
rows[
|
|
432
|
+
rows[R_DESK_DRAWERS] = place(
|
|
433
|
+
rows[R_DESK_DRAWERS],
|
|
434
|
+
`│${centerPadDisplayText(drawers, deskInner)}│`,
|
|
435
|
+
deskX,
|
|
436
|
+
);
|
|
437
|
+
rows[R_DESK_BOTTOM] = place(
|
|
438
|
+
rows[R_DESK_BOTTOM],
|
|
383
439
|
`╰${"─".repeat(Math.max(0, deskInner))}╯`,
|
|
384
440
|
deskX,
|
|
385
441
|
);
|
|
442
|
+
rows[R_FLOOR] = centerText(
|
|
443
|
+
rows[R_FLOOR],
|
|
444
|
+
fitDisplayText("· · · · · · · · · · · · · · · · ·", width),
|
|
445
|
+
);
|
|
386
446
|
}
|
|
387
447
|
|
|
388
448
|
function buildScene(
|
|
@@ -421,7 +481,13 @@ function rowColor(i: number, activity: PetActivity, frame: number, t: Theme): st
|
|
|
421
481
|
if (activity === "playing") return t.primaryLight;
|
|
422
482
|
return t.primaryLight;
|
|
423
483
|
}
|
|
424
|
-
if (
|
|
484
|
+
if (
|
|
485
|
+
i === R_DESK_SURFACE ||
|
|
486
|
+
i === R_DESK_FRONT ||
|
|
487
|
+
i === R_DESK_DRAWERS ||
|
|
488
|
+
i === R_DESK_BOTTOM ||
|
|
489
|
+
i === R_FLOOR
|
|
490
|
+
) return t.primaryDim;
|
|
425
491
|
if (i === R_WALL) return t.dim;
|
|
426
492
|
if (i >= R_WINDOW_TOP && i <= R_WINDOW_BOTTOM) return t.primaryDim;
|
|
427
493
|
return t.primaryDim;
|