@kevin5251984/guild 0.2.18 → 0.2.20

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.
@@ -0,0 +1,248 @@
1
+ import type { ModelEntry, ProviderEntry } from "@guild/protocol";
2
+ import { guildUserAgent } from "./version.ts";
3
+
4
+ /** Built-in alias `free` → this provider, matching Hermes Agent (2026-08). */
5
+ export const OPENCODE_FREE_PROVIDER_ID = "opencode-free";
6
+ export const OPENCODE_FREE_BASE_URL = "https://opencode.ai/zen/v1";
7
+
8
+ const ALIASES = new Set(["opencode-free", "free", "opencode_free"]);
9
+
10
+ /**
11
+ * Offline floor — Hermes Agent's curated catalog.
12
+ * Live GET /zen/v1/models is the source of truth when reachable.
13
+ * Known-delisted slugs must not stay here (they 401 keyless).
14
+ */
15
+ export const OPENCODE_FREE_FLOOR = [
16
+ "laguna-s-2.1-free",
17
+ "mimo-v2.5-free",
18
+ "nemotron-3.5-lightning-free",
19
+ "nemotron-3-ultra-free",
20
+ "muse-spark-1.2-contributor-free",
21
+ "ling-3.0-flash-fin-free",
22
+ "deepseek-v4-flash-free",
23
+ ] as const;
24
+
25
+ /** Default pick — user-facing Guild default, routed to `/v1/responses`. */
26
+ export const OPENCODE_FREE_DEFAULT_MODEL = "muse-spark-1.2-contributor-free";
27
+
28
+ /**
29
+ * Hermes `opencode_model_api_mode`: Muse Spark on Zen/Go is Responses-only.
30
+ * `/v1/chat/completions` 500s; `/v1/responses` completes.
31
+ */
32
+ export function usesZenResponses(model: string): boolean {
33
+ return String(model || "")
34
+ .trim()
35
+ .toLowerCase()
36
+ .startsWith("muse-spark");
37
+ }
38
+
39
+ /** Free slugs that do not end in `-free` (OpenCode's rotating stealth slot). */
40
+ const EXTRA_SLUGS = new Set(["big-pickle"]);
41
+
42
+ /** `-free` suffix but KEYED (Go subscription), not anonymous. */
43
+ const KEYED_FREE_SUFFIX = new Set(["ox-alpha-free"]);
44
+
45
+ export function isKeylessProvider(id: string): boolean {
46
+ return ALIASES.has(String(id || "").trim().toLowerCase());
47
+ }
48
+
49
+ export function prettyOpenCodeFreeName(id: string): string {
50
+ const bare = String(id || "").trim().split("/").pop() || String(id || "");
51
+ const slug = bare.replace(/-/g, " ").replace(/\s+/g, " ").trim();
52
+ if (!slug) return id;
53
+ return slug.replace(/\b([a-z])/g, (ch) => ch.toUpperCase());
54
+ }
55
+
56
+ export function openCodeFreeModels(ids: readonly string[] = OPENCODE_FREE_FLOOR): ModelEntry[] {
57
+ return ids.map((id) => ({ id, name: prettyOpenCodeFreeName(id) }));
58
+ }
59
+
60
+ export function openCodeFreeProvider(): ProviderEntry {
61
+ return {
62
+ name: "OpenCode Free",
63
+ baseUrl: OPENCODE_FREE_BASE_URL,
64
+ api: "openai-completions",
65
+ apiKey: "",
66
+ models: openCodeFreeModels(),
67
+ };
68
+ }
69
+
70
+ export function filterOpenCodeFreeIds(ids: string[]): string[] {
71
+ const out: string[] = [];
72
+ const seen = new Set<string>();
73
+ for (const raw of ids) {
74
+ const id = String(raw || "").trim();
75
+ if (!id) continue;
76
+ const bare = (id.split("/").pop() || id).toLowerCase();
77
+ const ok =
78
+ (bare.endsWith("-free") && !KEYED_FREE_SUFFIX.has(bare)) ||
79
+ EXTRA_SLUGS.has(bare);
80
+ if (!ok || seen.has(bare)) continue;
81
+ seen.add(bare);
82
+ out.push(id.includes("/") ? bare : id);
83
+ }
84
+ return out;
85
+ }
86
+
87
+ export function llmRequestHeaders(target: {
88
+ providerId: string;
89
+ apiKey: string;
90
+ headers?: Record<string, string>;
91
+ }): Record<string, string> {
92
+ const extra = { ...(target.headers ?? {}) };
93
+ if (isKeylessProvider(target.providerId)) {
94
+ const headers: Record<string, string> = {
95
+ "content-type": "application/json",
96
+ "http-referer": "https://github.com/Jakevin/guild",
97
+ "x-title": "Guild",
98
+ "user-agent": guildUserAgent(),
99
+ ...extra,
100
+ };
101
+ delete headers.authorization;
102
+ delete headers.Authorization;
103
+ return headers;
104
+ }
105
+ const headers: Record<string, string> = {
106
+ "content-type": "application/json",
107
+ ...extra,
108
+ };
109
+ if (!headers.authorization && !headers.Authorization) {
110
+ headers.authorization = `Bearer ${target.apiKey}`;
111
+ }
112
+ return headers;
113
+ }
114
+
115
+ let liveMemo: { at: number; ids: string[] | null } | null = null;
116
+ const LIVE_TTL_MS = 300_000;
117
+
118
+ export async function fetchOpenCodeFreeModels(
119
+ timeoutMs = 4_000,
120
+ force = false,
121
+ ): Promise<string[] | null> {
122
+ const now = Date.now();
123
+ if (!force && liveMemo && now - liveMemo.at < LIVE_TTL_MS) {
124
+ return liveMemo.ids ? [...liveMemo.ids] : null;
125
+ }
126
+ try {
127
+ const res = await fetch(`${OPENCODE_FREE_BASE_URL}/models`, {
128
+ headers: {
129
+ accept: "application/json",
130
+ "user-agent": guildUserAgent(),
131
+ "http-referer": "https://github.com/Jakevin/guild",
132
+ "x-title": "Guild",
133
+ },
134
+ signal: AbortSignal.timeout(timeoutMs),
135
+ });
136
+ if (!res.ok) {
137
+ liveMemo = { at: now, ids: null };
138
+ return null;
139
+ }
140
+ const body = (await res.json()) as { data?: { id?: string }[] } | { id?: string }[];
141
+ const rows = Array.isArray(body) ? body : body.data;
142
+ const ids = filterOpenCodeFreeIds(
143
+ (rows ?? [])
144
+ .map((row) => (row && typeof row.id === "string" ? row.id : ""))
145
+ .filter(Boolean),
146
+ );
147
+ const result = ids.length ? ids : null;
148
+ liveMemo = { at: now, ids: result };
149
+ return result ? [...result] : null;
150
+ } catch {
151
+ liveMemo = { at: now, ids: null };
152
+ return null;
153
+ }
154
+ }
155
+
156
+ /** Tests only. */
157
+ export function resetOpenCodeFreeMemo(): void {
158
+ liveMemo = null;
159
+ }
160
+
161
+ export type OpenCodeFreeProbe = {
162
+ id: string;
163
+ ok: boolean;
164
+ status: number;
165
+ reason?: string;
166
+ };
167
+
168
+ /** Health ping — not a turn timer. Muse /responses often needs ~7s. */
169
+ export const OPENCODE_FREE_PROBE_TIMEOUT_MS = 12_000;
170
+ const PROBE_CONCURRENCY = 4;
171
+
172
+ type FetchLike = typeof fetch;
173
+
174
+ export function selectOpenCodeFreeIds(
175
+ live: string[],
176
+ probe: OpenCodeFreeProbe[],
177
+ keepId?: string,
178
+ ): string[] {
179
+ const ok = new Set(probe.filter((row) => row.ok).map((row) => row.id));
180
+ const picked = live.filter((id) => ok.has(id));
181
+ if (keepId && live.includes(keepId) && !picked.includes(keepId) && picked.length) {
182
+ picked.push(keepId);
183
+ }
184
+ return picked;
185
+ }
186
+
187
+ export async function probeOpenCodeFreeModel(
188
+ id: string,
189
+ fetcher: FetchLike = fetch,
190
+ ): Promise<OpenCodeFreeProbe> {
191
+ const model = String(id || "").trim();
192
+ if (!model) return { id: model, ok: false, status: 0, reason: "empty" };
193
+ const responses = usesZenResponses(model);
194
+ const url = `${OPENCODE_FREE_BASE_URL}${responses ? "/responses" : "/chat/completions"}`;
195
+ const body = responses
196
+ ? { model, input: [{ role: "user", content: "ping" }] }
197
+ : {
198
+ model,
199
+ messages: [{ role: "user", content: "ping" }],
200
+ max_tokens: 16,
201
+ };
202
+ try {
203
+ const res = await fetcher(url, {
204
+ method: "POST",
205
+ headers: llmRequestHeaders({
206
+ providerId: OPENCODE_FREE_PROVIDER_ID,
207
+ apiKey: "",
208
+ }),
209
+ body: JSON.stringify(body),
210
+ signal: AbortSignal.timeout(OPENCODE_FREE_PROBE_TIMEOUT_MS),
211
+ });
212
+ if (res.status === 429) {
213
+ return { id: model, ok: true, status: 429, reason: "rate-limited" };
214
+ }
215
+ if (!res.ok) {
216
+ return { id: model, ok: false, status: res.status, reason: `HTTP ${res.status}` };
217
+ }
218
+ return { id: model, ok: true, status: res.status };
219
+ } catch (err) {
220
+ const message = err instanceof Error ? err.message : String(err);
221
+ const timeout = /timeout|aborted/i.test(message);
222
+ return {
223
+ id: model,
224
+ ok: false,
225
+ status: 0,
226
+ reason: timeout ? "timeout" : message || "error",
227
+ };
228
+ }
229
+ }
230
+
231
+ export async function probeOpenCodeFreeModels(
232
+ ids: string[],
233
+ fetcher: FetchLike = fetch,
234
+ ): Promise<OpenCodeFreeProbe[]> {
235
+ const list = ids.filter(Boolean);
236
+ const results: OpenCodeFreeProbe[] = new Array(list.length);
237
+ let next = 0;
238
+ const worker = async () => {
239
+ while (true) {
240
+ const i = next++;
241
+ if (i >= list.length) return;
242
+ results[i] = await probeOpenCodeFreeModel(list[i], fetcher);
243
+ }
244
+ };
245
+ const n = Math.min(PROBE_CONCURRENCY, Math.max(list.length, 0));
246
+ await Promise.all(Array.from({ length: n }, () => worker()));
247
+ return results;
248
+ }
@@ -0,0 +1,432 @@
1
+ /* 18×32 tavern buddy. Handle-seeded hair/skin/cloth + outline. Not a cast. */
2
+ (function (root) {
3
+ "use strict";
4
+ const W = 18;
5
+ const H = 32;
6
+ const OUT = [27, 21, 16];
7
+ const SHOE = [
8
+ [44, 36, 32],
9
+ [62, 44, 34],
10
+ ];
11
+ const PANTS = [
12
+ [42, 48, 62],
13
+ [58, 48, 40],
14
+ [46, 58, 48],
15
+ [72, 64, 58],
16
+ ];
17
+ const HAIR_RGB = [
18
+ [42, 28, 18],
19
+ [22, 18, 16],
20
+ [196, 163, 90],
21
+ [122, 52, 44],
22
+ [58, 72, 108],
23
+ [88, 56, 140],
24
+ [154, 86, 46],
25
+ [72, 44, 58],
26
+ ];
27
+ const SKIN = [
28
+ { hi: [255, 221, 189], base: [247, 201, 170], sh: [212, 158, 126], line: [168, 112, 82] },
29
+ { hi: [232, 182, 136], base: [214, 162, 116], sh: [176, 126, 86], line: [138, 92, 60] },
30
+ { hi: [196, 148, 108], base: [176, 128, 90], sh: [140, 98, 68], line: [104, 70, 48] },
31
+ { hi: [168, 118, 86], base: [146, 100, 70], sh: [112, 76, 52], line: [80, 54, 36] },
32
+ { hi: [130, 90, 64], base: [108, 74, 52], sh: [82, 56, 38], line: [56, 38, 26] },
33
+ ];
34
+ const L = 4;
35
+ const R = 13;
36
+
37
+ function clamp(v) {
38
+ return v < 0 ? 0 : v > 255 ? 255 : Math.round(v);
39
+ }
40
+ function shades(rgb, hi = 1.22, lo = 0.68) {
41
+ return [
42
+ [clamp(rgb[0] * hi), clamp(rgb[1] * hi), clamp(rgb[2] * hi)],
43
+ rgb,
44
+ [clamp(rgb[0] * lo), clamp(rgb[1] * lo), clamp(rgb[2] * lo)],
45
+ ];
46
+ }
47
+ function parseHex(hex) {
48
+ const h = String(hex || "#7c6af7").replace("#", "");
49
+ const full = h.length === 3 ? h[0] + h[0] + h[1] + h[1] + h[2] + h[2] : h;
50
+ return [
51
+ parseInt(full.slice(0, 2), 16) || 120,
52
+ parseInt(full.slice(2, 4), 16) || 100,
53
+ parseInt(full.slice(4, 6), 16) || 90,
54
+ ];
55
+ }
56
+ function set(buf, x, y, c, a = 255) {
57
+ if (x < 0 || x >= W || y < 0 || y >= H) return;
58
+ const i = (y * W + x) * 4;
59
+ buf[i] = c[0];
60
+ buf[i + 1] = c[1];
61
+ buf[i + 2] = c[2];
62
+ buf[i + 3] = a;
63
+ }
64
+ function alpha(buf, x, y) {
65
+ if (x < 0 || x >= W || y < 0 || y >= H) return 0;
66
+ return buf[(y * W + x) * 4 + 3];
67
+ }
68
+ function rect(buf, x0, y0, x1, y1, c) {
69
+ for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) set(buf, x, y, c);
70
+ }
71
+ function dots(buf, pts, c) {
72
+ for (const [x, y] of pts) set(buf, x, y, c);
73
+ }
74
+
75
+ function drawHead(buf, s) {
76
+ for (let y = 4; y <= 16; y++) {
77
+ for (let x = L; x <= R; x++) {
78
+ if ((x === L || x === R) && (y === 4 || y === 16)) continue;
79
+ if ((x === L + 1 || x === R - 1) && y === 4) continue;
80
+ set(buf, x, y, s.base);
81
+ }
82
+ }
83
+ for (let y = 6; y <= 11; y++) set(buf, 5, y, s.hi);
84
+ set(buf, 6, 5, s.hi);
85
+ set(buf, 7, 5, s.hi);
86
+ for (let y = 6; y <= 15; y++) set(buf, 12, y, s.sh);
87
+ for (const x of [7, 8, 9, 10, 11]) set(buf, x, 16, s.sh);
88
+ set(buf, 3, 9, s.base);
89
+ set(buf, 3, 10, s.base);
90
+ set(buf, 3, 11, s.sh);
91
+ set(buf, 14, 9, s.base);
92
+ set(buf, 14, 10, s.base);
93
+ set(buf, 14, 11, s.sh);
94
+ rect(buf, 7, 17, 10, 18, s.sh);
95
+ rect(buf, 7, 17, 9, 17, s.base);
96
+ }
97
+
98
+ function drawFace(buf, s, brow, mouth, blush, lashes) {
99
+ const white = [250, 248, 244];
100
+ const pup = [46, 38, 42];
101
+ const glint = [252, 250, 248];
102
+ const lash = [54, 40, 48];
103
+ const lip = [158, 86, 80];
104
+ set(buf, 5, 9, white);
105
+ set(buf, 6, 9, pup);
106
+ set(buf, 10, 9, pup);
107
+ set(buf, 11, 9, white);
108
+ set(buf, 5, 9, glint);
109
+ if (lashes) {
110
+ for (const x of [5, 6, 10, 11]) set(buf, x, 8, lash);
111
+ set(buf, 4, 8, lash);
112
+ set(buf, 12, 8, lash);
113
+ set(buf, 10, 9, glint);
114
+ }
115
+ if (brow === 0) for (const x of [5, 6, 10, 11]) set(buf, x, 7, s.line);
116
+ else if (brow === 1) {
117
+ set(buf, 5, 8, s.line);
118
+ set(buf, 6, 7, s.line);
119
+ set(buf, 10, 7, s.line);
120
+ set(buf, 11, 8, s.line);
121
+ } else if (brow === 2) for (const x of [5, 6, 10, 11]) set(buf, x, 6, s.line);
122
+ else {
123
+ set(buf, 5, 7, s.line);
124
+ set(buf, 11, 7, s.line);
125
+ set(buf, 6, 7, s.sh);
126
+ set(buf, 10, 7, s.sh);
127
+ }
128
+ set(buf, 8, 11, s.sh);
129
+ set(buf, 8, 12, s.sh);
130
+ set(buf, 7, 12, s.sh);
131
+ const mouths = [
132
+ [[7, 14], [8, 14], [9, 14], [10, 14]],
133
+ [[7, 14], [8, 14], [9, 14], [10, 14], [6, 13], [11, 13]],
134
+ [[7, 15], [8, 15], [9, 15], [10, 15]],
135
+ [[6, 14], [7, 14], [8, 14], [9, 14], [10, 14], [11, 14], [6, 13], [11, 13]],
136
+ ];
137
+ dots(buf, mouths[mouth], lip);
138
+ if (blush) {
139
+ set(buf, 5, 12, [228, 140, 128]);
140
+ set(buf, 12, 12, [228, 140, 128]);
141
+ }
142
+ }
143
+
144
+ function hairShort(buf, hi, base, sh) {
145
+ rect(buf, L, 2, R, 4, base);
146
+ rect(buf, L - 1, 3, R + 1, 5, base);
147
+ for (let y = 6; y <= 8; y++) {
148
+ set(buf, L - 1, y, base);
149
+ set(buf, L, y, base);
150
+ set(buf, R, y, base);
151
+ set(buf, R + 1, y, base);
152
+ }
153
+ for (let x = L; x <= R; x++) set(buf, x, 5, base);
154
+ for (let y = 2; y <= 5; y++) set(buf, 8, y, sh);
155
+ for (let x = L; x <= 7; x++) if (alpha(buf, x, 2)) set(buf, x, 2, hi);
156
+ for (let x = L; x <= 7; x++) if (alpha(buf, x, 3)) set(buf, x, 3, hi);
157
+ }
158
+ function hairFringe(buf, hi, base, sh, skinBase) {
159
+ hairShort(buf, hi, base, sh);
160
+ rect(buf, 6, 6, 11, 6, base);
161
+ set(buf, 8, 6, skinBase);
162
+ set(buf, 9, 6, skinBase);
163
+ set(buf, 7, 6, hi);
164
+ set(buf, 10, 6, base);
165
+ }
166
+ function hairLong(buf, hi, base, sh, skinBase) {
167
+ hairShort(buf, hi, base, sh);
168
+ rect(buf, 6, 6, 11, 6, base);
169
+ set(buf, 8, 6, skinBase);
170
+ set(buf, 9, 6, skinBase);
171
+ for (let y = 9; y <= 18; y++) {
172
+ set(buf, L - 1, y, base);
173
+ set(buf, R + 1, y, sh);
174
+ }
175
+ set(buf, L - 1, 19, base);
176
+ set(buf, R + 1, 19, sh);
177
+ }
178
+ function hairBun(buf, hi, base, sh, skinBase) {
179
+ rect(buf, L, 3, R, 5, base);
180
+ rect(buf, L - 1, 4, R + 1, 5, base);
181
+ rect(buf, 7, 1, 10, 2, base);
182
+ set(buf, 8, 0, hi);
183
+ set(buf, 9, 0, base);
184
+ rect(buf, 6, 6, 11, 6, base);
185
+ set(buf, 8, 6, skinBase);
186
+ set(buf, 9, 6, skinBase);
187
+ for (let y = 6; y <= 8; y++) {
188
+ set(buf, L, y, base);
189
+ set(buf, R, y, sh);
190
+ }
191
+ for (let x = L; x <= R; x++) if (alpha(buf, x, 3)) set(buf, x, 3, hi);
192
+ }
193
+ function hairCurly(buf, hi, base, sh, skinBase) {
194
+ rect(buf, L, 3, R, 5, base);
195
+ const pts = [
196
+ [4, 2], [5, 1], [6, 2], [7, 1], [8, 2], [9, 1], [10, 2], [11, 1], [12, 2], [13, 2],
197
+ [3, 3], [3, 4], [3, 5], [3, 6], [14, 3], [14, 4], [14, 5], [14, 6], [4, 6], [13, 6],
198
+ ];
199
+ dots(buf, pts, base);
200
+ rect(buf, 6, 6, 11, 6, base);
201
+ set(buf, 8, 6, skinBase);
202
+ set(buf, 9, 6, skinBase);
203
+ dots(buf, [[5, 1], [7, 1], [9, 1], [11, 1]], hi);
204
+ set(buf, R + 1, 5, sh);
205
+ }
206
+ function hairSpiky(buf, hi, base, sh, skinBase) {
207
+ rect(buf, L, 3, R, 5, base);
208
+ rect(buf, L - 1, 4, R + 1, 5, base);
209
+ dots(buf, [[5, 2], [6, 1], [7, 2], [8, 1], [9, 2], [10, 1], [11, 2], [12, 1], [13, 2]], base);
210
+ dots(buf, [[6, 1], [8, 1], [10, 1], [12, 1]], hi);
211
+ rect(buf, 6, 6, 11, 6, base);
212
+ set(buf, 8, 6, skinBase);
213
+ set(buf, 9, 6, skinBase);
214
+ for (let y = 6; y <= 7; y++) {
215
+ set(buf, L, y, base);
216
+ set(buf, R, y, sh);
217
+ }
218
+ }
219
+ function hairBob(buf, hi, base, sh, skinBase) {
220
+ hairShort(buf, hi, base, sh);
221
+ rect(buf, 5, 6, 12, 6, base);
222
+ set(buf, 8, 6, skinBase);
223
+ set(buf, 9, 6, skinBase);
224
+ for (let y = 7; y <= 12; y++) {
225
+ set(buf, L - 1, y, base);
226
+ set(buf, R + 1, y, sh);
227
+ }
228
+ set(buf, L - 1, 13, base);
229
+ set(buf, R + 1, 13, sh);
230
+ }
231
+ function hairPony(buf, hi, base, sh) {
232
+ hairShort(buf, hi, base, sh);
233
+ rect(buf, 11, 0, 13, 2, base);
234
+ set(buf, 12, 0, hi);
235
+ set(buf, 13, 3, sh);
236
+ set(buf, 14, 1, base);
237
+ set(buf, 14, 2, sh);
238
+ }
239
+
240
+ const HAIR_FNS = [hairShort, hairFringe, hairLong, hairBun, hairCurly, hairSpiky, hairBob, hairPony];
241
+
242
+ function drawFacial(buf, kind, color) {
243
+ const [, base, sh] = shades(color);
244
+ if (kind === 1) {
245
+ for (const x of [6, 7, 8, 9, 10]) set(buf, x, 13, base);
246
+ set(buf, 6, 12, base);
247
+ set(buf, 10, 12, base);
248
+ } else if (kind === 2) {
249
+ dots(buf, [[5, 14], [6, 15], [7, 15], [8, 15], [9, 15], [10, 15], [11, 14], [12, 13], [4, 13]], sh);
250
+ } else if (kind === 3) {
251
+ rect(buf, 8, 14, 9, 15, base);
252
+ rect(buf, 7, 13, 10, 13, base);
253
+ }
254
+ }
255
+
256
+ function drawGlasses(buf) {
257
+ const frame = [60, 54, 62];
258
+ const glint = [236, 240, 246];
259
+ for (const x of [5, 6]) {
260
+ set(buf, x, 8, frame);
261
+ set(buf, x, 10, frame);
262
+ }
263
+ set(buf, 4, 9, frame);
264
+ set(buf, 7, 9, frame);
265
+ for (const x of [10, 11]) {
266
+ set(buf, x, 8, frame);
267
+ set(buf, x, 10, frame);
268
+ }
269
+ set(buf, 9, 9, frame);
270
+ set(buf, 12, 9, frame);
271
+ set(buf, 8, 8, frame);
272
+ set(buf, 3, 9, frame);
273
+ set(buf, 13, 9, frame);
274
+ set(buf, 4, 8, glint);
275
+ set(buf, 9, 8, glint);
276
+ }
277
+
278
+ function drawBody(buf, cloth, c1, c2, skin) {
279
+ const [hi, base, sh] = shades(c1);
280
+ rect(buf, 4, 18, 13, 18, base);
281
+ rect(buf, 3, 19, 14, 19, base);
282
+ rect(buf, 4, 20, 13, 24, base);
283
+ for (let y = 20; y <= 24; y++) {
284
+ set(buf, 3, y, sh);
285
+ set(buf, 14, y, sh);
286
+ set(buf, 13, y, sh);
287
+ }
288
+ set(buf, 3, 24, skin.sh);
289
+ set(buf, 14, 24, skin.sh);
290
+ if (cloth === 0) {
291
+ for (const [x, y] of [[6, 18], [7, 18], [10, 18], [11, 18]]) set(buf, x, y, sh);
292
+ rect(buf, 7, 18, 10, 19, skin.sh);
293
+ } else if (cloth === 1) {
294
+ const inner = c2 ? shades(c2)[1] : [235, 233, 226];
295
+ for (let y = 18; y <= 24; y++) {
296
+ set(buf, 8, y, inner);
297
+ set(buf, 9, y, inner);
298
+ }
299
+ for (const [x, y] of [[6, 18], [7, 18], [10, 18], [11, 18]]) set(buf, x, y, sh);
300
+ } else if (cloth === 2) {
301
+ for (const [x, y] of [[6, 18], [7, 18], [10, 18], [11, 18]]) set(buf, x, y, hi);
302
+ set(buf, 8, 19, sh);
303
+ set(buf, 8, 21, sh);
304
+ } else if (cloth === 3) {
305
+ for (const x of [6, 7, 8, 9, 10, 11]) set(buf, x, 18, sh);
306
+ } else if (cloth === 4) {
307
+ const inner = c2 ? shades(c2)[1] : [238, 236, 228];
308
+ rect(buf, 7, 18, 10, 19, inner);
309
+ for (const [x, y] of [[6, 18], [7, 19], [11, 18], [10, 19]]) set(buf, x, y, sh);
310
+ for (let y = 20; y <= 24; y += 2) set(buf, 8, y, sh);
311
+ } else {
312
+ const apron = [236, 224, 196];
313
+ rect(buf, 7, 20, 10, 24, apron);
314
+ set(buf, 8, 19, apron);
315
+ set(buf, 9, 19, apron);
316
+ for (const [x, y] of [[6, 18], [7, 18], [10, 18], [11, 18]]) set(buf, x, y, hi);
317
+ }
318
+ }
319
+
320
+ function drawLegs(buf, pants, shoe, short) {
321
+ const top = short ? 26 : 25;
322
+ const foot = short ? 30 : 31;
323
+ const [, base, sh] = shades(pants);
324
+ rect(buf, 5, top, 7, foot - 1, base);
325
+ rect(buf, 10, top, 12, foot - 1, base);
326
+ for (let y = top; y <= foot - 1; y++) {
327
+ set(buf, 7, y, sh);
328
+ set(buf, 12, y, sh);
329
+ }
330
+ rect(buf, 5, foot, 7, foot, shoe);
331
+ rect(buf, 10, foot, 12, foot, shoe);
332
+ }
333
+
334
+ function elfEars(buf, s) {
335
+ dots(buf, [[2, 8], [2, 7], [3, 7], [3, 8]], s.base);
336
+ set(buf, 2, 7, s.hi);
337
+ dots(buf, [[15, 8], [15, 7], [14, 7], [14, 8]], s.base);
338
+ set(buf, 15, 7, s.hi);
339
+ }
340
+
341
+ function demiEars(buf, s) {
342
+ set(buf, 3, 8, s.base);
343
+ set(buf, 2, 9, s.sh);
344
+ set(buf, 14, 8, s.base);
345
+ set(buf, 15, 9, s.sh);
346
+ }
347
+
348
+ function dwarfStout(buf, clothRgb, skin) {
349
+ const sh = shades(clothRgb)[2];
350
+ for (let y = 19; y <= 24; y++) {
351
+ set(buf, 2, y, sh);
352
+ set(buf, 15, y, sh);
353
+ }
354
+ for (const x of [5, 6, 10, 11]) set(buf, x, 7, skin.line);
355
+ }
356
+
357
+ function outlinePass(buf) {
358
+ const pts = [];
359
+ for (let y = 0; y < H; y++) {
360
+ for (let x = 0; x < W; x++) {
361
+ if (alpha(buf, x, y) !== 0) continue;
362
+ if (
363
+ alpha(buf, x + 1, y) === 255 ||
364
+ alpha(buf, x - 1, y) === 255 ||
365
+ alpha(buf, x, y + 1) === 255 ||
366
+ alpha(buf, x, y - 1) === 255
367
+ ) {
368
+ pts.push([x, y]);
369
+ }
370
+ }
371
+ }
372
+ for (const [x, y] of pts) set(buf, x, y, OUT);
373
+ }
374
+
375
+ function fnv(key) {
376
+ let n = 2166136261;
377
+ const s = String(key || "");
378
+ for (let i = 0; i < s.length; i++) {
379
+ n ^= s.charCodeAt(i);
380
+ n = Math.imul(n, 16777619);
381
+ }
382
+ return n >>> 0;
383
+ }
384
+
385
+ function buddyPixels(shirt, seed) {
386
+ const n = typeof seed === "string" ? fnv(seed) : seed >>> 0;
387
+ const buf = new Uint8ClampedArray(W * H * 4);
388
+ const race = (n >>> 20) % 4;
389
+ const skin = SKIN[n % SKIN.length];
390
+ const hairc = HAIR_RGB[(n * 3) % HAIR_RGB.length];
391
+ const [hi, base, sh] = shades(hairc);
392
+ const cloth = n % 6;
393
+ const pants = PANTS[(n * 5) % PANTS.length];
394
+ const shoe = SHOE[n % SHOE.length];
395
+ const shirtRgb = parseHex(shirt);
396
+ const accent = shades(shirtRgb)[2];
397
+ const brow = race === 1 ? 0 : (n * 7) % 4;
398
+ const mouth = [1, 3, 0, 1][n % 4];
399
+ const lashes = cloth === 1 || cloth === 5 || (n % 5 === 2);
400
+ const blush = lashes && n % 3 !== 0;
401
+ const glasses = n % 19 === 0;
402
+ const facial = 0;
403
+ drawBody(buf, cloth, shirtRgb, accent, skin);
404
+ drawLegs(buf, pants, shoe, race === 1);
405
+ drawHead(buf, skin);
406
+ drawFace(buf, skin, brow, mouth, blush, lashes);
407
+ if (facial) drawFacial(buf, facial, hairc);
408
+ HAIR_FNS[n % HAIR_FNS.length](buf, hi, base, sh, skin.base);
409
+ if (glasses) drawGlasses(buf);
410
+ if (race === 1) dwarfStout(buf, shirtRgb, skin);
411
+ if (race === 2) elfEars(buf, skin);
412
+ if (race === 3) demiEars(buf, skin);
413
+ outlinePass(buf);
414
+ return buf;
415
+ }
416
+
417
+ function drawBuddy(canvas, shirt, seed) {
418
+ const ctx = canvas.getContext("2d");
419
+ canvas.width = W;
420
+ canvas.height = H;
421
+ ctx.imageSmoothingEnabled = false;
422
+ const img = ctx.createImageData(W, H);
423
+ img.data.set(buddyPixels(shirt, seed));
424
+ ctx.putImageData(img, 0, 0);
425
+ }
426
+
427
+ root.BUDDY_W = W;
428
+ root.BUDDY_H = H;
429
+ root.buddyFnv = fnv;
430
+ root.buddyPixels = buddyPixels;
431
+ root.drawBuddy = drawBuddy;
432
+ })(typeof globalThis !== "undefined" ? globalThis : this);