honeytree 1.0.5 → 1.0.7

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/src/renderer.js DELETED
@@ -1,302 +0,0 @@
1
- import chalk from "chalk";
2
-
3
- import { getSprite, TREE_TYPES } from "./sprites.js";
4
-
5
- const SKY_ROWS = 4;
6
- const TREE_ROWS = 7;
7
- const GROUND_ROWS = 2;
8
- const SPACER_ROWS = 1;
9
- const STATS_ROWS = 1;
10
-
11
- export const SCENE_HEIGHT =
12
- SKY_ROWS + TREE_ROWS + GROUND_ROWS + SPACER_ROWS + STATS_ROWS;
13
-
14
- const STATS_ACCENT = "#f5a50b";
15
- const STATS_TEXT = "#8e8a84";
16
- const STATS_WARN = "#c4653a";
17
- const STREAK_COLOR = "#e8a33a";
18
- const BAR_FILL = "#6cb95e";
19
- const BAR_EMPTY = "#3d3d3d";
20
- const MILESTONES = [10, 25, 50, 100, 250, 500, 1000];
21
-
22
- // Wilting — lerp toward dry brown when idle
23
- const WILT_TARGET = { r: 0x8a, g: 0x6a, b: 0x4a };
24
-
25
- function parseHex(hex) {
26
- const h = hex.startsWith("#") ? hex.slice(1) : hex;
27
- return {
28
- r: parseInt(h.slice(0, 2), 16),
29
- g: parseInt(h.slice(2, 4), 16),
30
- b: parseInt(h.slice(4, 6), 16),
31
- };
32
- }
33
-
34
- function toHex({ r, g, b }) {
35
- const c = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0");
36
- return `#${c(r)}${c(g)}${c(b)}`;
37
- }
38
-
39
- function wiltColor(hex, factor) {
40
- if (factor <= 0) return hex;
41
- const c = parseHex(hex);
42
- return toHex({
43
- r: c.r + (WILT_TARGET.r - c.r) * factor,
44
- g: c.g + (WILT_TARGET.g - c.g) * factor,
45
- b: c.b + (WILT_TARGET.b - c.b) * factor,
46
- });
47
- }
48
-
49
- export function getWiltFactor(lastActiveDate) {
50
- if (!lastActiveDate) return 0;
51
- const today = new Date().toISOString().slice(0, 10);
52
- const a = new Date(lastActiveDate + "T00:00:00");
53
- const b = new Date(today + "T00:00:00");
54
- const days = Math.round((b - a) / (24 * 60 * 60 * 1000));
55
- if (days <= 0) return 0;
56
- if (days === 1) return 0.25;
57
- if (days === 2) return 0.45;
58
- if (days === 3) return 0.65;
59
- return Math.min(0.85, 0.65 + (days - 3) * 0.05);
60
- }
61
-
62
- // Fog — procedural haze that thickens with inactivity
63
- const FOG_CHARS = ["░", "░", "▒"];
64
- const FOG_COLOR_UPPER = "#9a9a9a";
65
- const FOG_COLOR_LOWER = "#6a6a6a";
66
-
67
- function applyFog(buffer, wilt, width) {
68
- if (wilt <= 0) return;
69
- // Higher wilt → lower threshold → more fog
70
- const threshold = Math.max(3, Math.round(18 * (1 - wilt)));
71
- const fogStart = SKY_ROWS - 2; // creep into lower sky
72
- const fogEnd = SKY_ROWS + TREE_ROWS + GROUND_ROWS;
73
-
74
- for (let y = Math.max(0, fogStart); y < fogEnd; y += 1) {
75
- for (let x = 0; x < width; x += 1) {
76
- const h = hash(x * 31 + y * 97 + 12345);
77
- if (h % threshold !== 0) continue;
78
- const fogChar = FOG_CHARS[h % FOG_CHARS.length];
79
- const blend = (y - fogStart) / (fogEnd - fogStart);
80
- const fogColor = blend > 0.5 ? FOG_COLOR_LOWER : FOG_COLOR_UPPER;
81
- buffer[y][x] = { char: fogChar, color: fogColor };
82
- }
83
- }
84
- }
85
-
86
- // Biomes evolve as the forest grows — never resets, only gets richer
87
- const BIOMES = [
88
- { // 0-9: sparse clearing
89
- ground: ["#2a3a28", "#1e2d1c"],
90
- starGlyphs: ["·", ".", " ", " "],
91
- starDensity: 12,
92
- starColors: ["#3a3a3a", "#444444"],
93
- label: "clearing",
94
- },
95
- { // 10-24: young grove
96
- ground: ["#22492d", "#18361f"],
97
- starGlyphs: ["·", "·", "✦", "."],
98
- starDensity: 9,
99
- starColors: ["#444444", "#5d5d5d"],
100
- label: "grove",
101
- },
102
- { // 25-49: dense woodland
103
- ground: ["#1e4a28", "#163a1e"],
104
- starGlyphs: ["·", "✦", "✧", "·", "."],
105
- starDensity: 7,
106
- starColors: ["#4d4d4d", "#5d5d5d", "#6a6a55"],
107
- label: "woodland",
108
- },
109
- { // 50-99: old growth
110
- ground: ["#1a5230", "#124020"],
111
- starGlyphs: ["✦", "✧", "·", "·", "✦", "."],
112
- starDensity: 6,
113
- starColors: ["#5d5d5d", "#6d6d5a", "#7a7a60"],
114
- label: "old growth",
115
- },
116
- { // 100+: ancient forest
117
- ground: ["#165a32", "#0e4822"],
118
- starGlyphs: ["✦", "✧", "·", "✦", "⋆", "."],
119
- starDensity: 5,
120
- starColors: ["#6d6d5a", "#7a7a60", "#8a8a6a"],
121
- label: "ancient forest",
122
- },
123
- ];
124
-
125
- function getBiome(treeCount) {
126
- if (treeCount < 10) return BIOMES[0];
127
- if (treeCount < 25) return BIOMES[1];
128
- if (treeCount < 50) return BIOMES[2];
129
- if (treeCount < 100) return BIOMES[3];
130
- return BIOMES[4];
131
- }
132
-
133
- function createBuffer(width) {
134
- return Array.from({ length: SCENE_HEIGHT }, () =>
135
- Array.from({ length: width }, () => ({ char: " ", color: null })),
136
- );
137
- }
138
-
139
- function hash(seed) {
140
- let value = seed >>> 0;
141
- value = Math.imul((value >>> 16) ^ value, 0x45d9f3b) >>> 0;
142
- value = Math.imul((value >>> 16) ^ value, 0x45d9f3b) >>> 0;
143
- return ((value >>> 16) ^ value) >>> 0;
144
- }
145
-
146
- function generateStars(width, biome, twinkle = 0) {
147
- const stars = [];
148
- for (let x = 0; x < width; x += 1) {
149
- const seeded = hash(x + width * 17 + twinkle * 101);
150
- if (seeded % biome.starDensity !== 0) continue;
151
- stars.push({
152
- x,
153
- y: seeded % SKY_ROWS,
154
- char: biome.starGlyphs[seeded % biome.starGlyphs.length],
155
- color: biome.starColors[seeded % biome.starColors.length],
156
- });
157
- }
158
- return stars;
159
- }
160
-
161
- function compositeSprite(buffer, sprite, centerX, baseY) {
162
- const offsetX = centerX - Math.floor(sprite.width / 2);
163
- for (let rowIndex = 0; rowIndex < sprite.rows.length; rowIndex += 1) {
164
- const targetY = baseY - rowIndex;
165
- if (targetY < 0 || targetY >= buffer.length) continue;
166
- const row = sprite.rows[rowIndex];
167
- for (let columnIndex = 0; columnIndex < row.length; columnIndex += 1) {
168
- const targetX = offsetX + columnIndex;
169
- if (targetX < 0 || targetX >= buffer[0].length) continue;
170
- const [char, color] = row[columnIndex];
171
- if (!color) continue;
172
- buffer[targetY][targetX] = { char, color };
173
- }
174
- }
175
- }
176
-
177
- function getNextMilestone(treeCount) {
178
- return MILESTONES.find((value) => treeCount < value) ?? treeCount + 100;
179
- }
180
-
181
- function getNextTreeType(treeCount) {
182
- return TREE_TYPES[treeCount % TREE_TYPES.length];
183
- }
184
-
185
- function buildStreakSegment(forest) {
186
- const wilt = getWiltFactor(forest.lastActiveDate);
187
- const streak = forest.streak || 0;
188
-
189
- if (wilt > 0) {
190
- const a = new Date(forest.lastActiveDate + "T00:00:00");
191
- const b = new Date(new Date().toISOString().slice(0, 10) + "T00:00:00");
192
- const idle = Math.round((b - a) / (24 * 60 * 60 * 1000));
193
- return chalk.hex(STATS_WARN)(`wilting (${idle}d idle)`);
194
- }
195
-
196
- if (streak <= 0) return chalk.hex(STATS_TEXT)("no streak");
197
- return chalk.hex(STREAK_COLOR)(`${streak}-day streak`);
198
- }
199
-
200
- function buildStatsLine(forest, biome) {
201
- const treeCount = forest.trees.length;
202
- const milestone = getNextMilestone(treeCount);
203
- const progress = milestone === 0 ? 0 : treeCount / milestone;
204
- const barWidth = 12;
205
- const filledWidth = Math.max(0, Math.min(barWidth, Math.round(progress * barWidth)));
206
- const bar =
207
- chalk.hex(BAR_FILL)("█".repeat(filledWidth)) +
208
- chalk.hex(BAR_EMPTY)("░".repeat(barWidth - filledWidth));
209
-
210
- return (
211
- chalk.hex(STATS_ACCENT)(" honeytree") +
212
- chalk.hex(STATS_TEXT)(
213
- ` · ${treeCount} tree${treeCount === 1 ? "" : "s"} · `,
214
- ) +
215
- buildStreakSegment(forest) +
216
- chalk.hex(STATS_TEXT)(" · ") +
217
- bar +
218
- chalk.hex(STATS_TEXT)(` next: ${getNextTreeType(treeCount)}`) +
219
- chalk.hex("#555555")(` [${biome.label}]`)
220
- );
221
- }
222
-
223
- export function renderFrame(forest, termWidth = 80, options = {}) {
224
- const width = Math.max(40, termWidth);
225
- const buffer = createBuffer(width);
226
- const groundStart = SKY_ROWS + TREE_ROWS;
227
- const biome = getBiome(forest.trees.length);
228
- const wilt = getWiltFactor(forest.lastActiveDate);
229
-
230
- for (const star of generateStars(width, biome, options.twinkleSeed ?? 0)) {
231
- buffer[star.y][star.x] = { char: star.char, color: star.color };
232
- }
233
-
234
- for (let rowIndex = 0; rowIndex < GROUND_ROWS; rowIndex += 1) {
235
- for (let x = 0; x < width; x += 1) {
236
- buffer[groundStart + rowIndex][x] = {
237
- char: "█",
238
- color: biome.ground[rowIndex],
239
- };
240
- }
241
- }
242
-
243
- const treeBaseY = groundStart - 1;
244
- for (const tree of forest.trees) {
245
- compositeSprite(buffer, getSprite(tree.type, tree.growth), tree.x, treeBaseY);
246
- }
247
-
248
- applyFog(buffer, wilt, width);
249
-
250
- const lines = [];
251
- for (let y = 0; y < SCENE_HEIGHT - SPACER_ROWS - STATS_ROWS; y += 1) {
252
- let line = "";
253
- for (const cell of buffer[y]) {
254
- if (!cell.color) {
255
- line += cell.char;
256
- } else {
257
- // Apply wilting to tree rows and ground (skip sky)
258
- const color = wilt > 0 && y >= SKY_ROWS ? wiltColor(cell.color, wilt) : cell.color;
259
- line += chalk.hex(color)(cell.char);
260
- }
261
- }
262
- lines.push(line);
263
- }
264
-
265
- lines.push("");
266
- lines.push(buildStatsLine(forest, biome));
267
-
268
- return lines.join("\n");
269
- }
270
-
271
- export function renderPlainText(forest, width = 60) {
272
- const w = Math.max(40, Math.min(width, 80));
273
- const buffer = createBuffer(w);
274
- const groundStart = SKY_ROWS + TREE_ROWS;
275
- const biome = getBiome(forest.trees.length);
276
-
277
- for (const star of generateStars(w, biome, 0)) {
278
- buffer[star.y][star.x] = { char: star.char, color: star.color };
279
- }
280
-
281
- for (let rowIndex = 0; rowIndex < GROUND_ROWS; rowIndex += 1) {
282
- for (let x = 0; x < w; x += 1) {
283
- buffer[groundStart + rowIndex][x] = { char: "█", color: "#333" };
284
- }
285
- }
286
-
287
- const treeBaseY = groundStart - 1;
288
- for (const tree of forest.trees) {
289
- compositeSprite(buffer, getSprite(tree.type, tree.growth), tree.x, treeBaseY);
290
- }
291
-
292
- const lines = [];
293
- for (let y = 0; y < SCENE_HEIGHT - SPACER_ROWS - STATS_ROWS; y += 1) {
294
- let line = "";
295
- for (const cell of buffer[y]) {
296
- line += cell.char;
297
- }
298
- lines.push(line.trimEnd());
299
- }
300
-
301
- return lines.join("\n");
302
- }
package/src/sprites.js DELETED
@@ -1,245 +0,0 @@
1
- export const TREE_TYPES = ["oak", "pine", "birch", "willow", "cherry"];
2
-
3
- const COLORS = {
4
- canopyDark: "#3f7132",
5
- canopyMid: "#5b9a4a",
6
- canopyLight: "#7cc96a",
7
- canopyDeep: "#2d5b29",
8
- canopyBright: "#a4e28d",
9
- trunkDark: "#6f4c2f",
10
- trunkMid: "#8e6238",
11
- trunkLight: "#b18552",
12
- birchTrunk: "#d9d6d2",
13
- cherryPink: "#de93b8",
14
- cherryBloom: "#f0b7cf",
15
- };
16
-
17
- function parse(template, palette) {
18
- const lines = template.trim().split("\n");
19
- const width = Math.max(...lines.map((line) => line.length));
20
- const rows = lines
21
- .map((line) => line.padEnd(width, " "))
22
- .map((line) =>
23
- Array.from(line, (token) => {
24
- const color = palette[token] ?? null;
25
- return color ? ["█", color] : [" ", null];
26
- }),
27
- )
28
- .reverse();
29
-
30
- return { rows, width };
31
- }
32
-
33
- const SPRITES = {
34
- oak: {
35
- seed: parse(
36
- `
37
- g
38
- t
39
- `,
40
- { g: COLORS.canopyMid, t: COLORS.trunkMid },
41
- ),
42
- sapling: parse(
43
- `
44
- gg
45
- ggg
46
- t
47
- `,
48
- { g: COLORS.canopyMid, t: COLORS.trunkMid },
49
- ),
50
- young: parse(
51
- `
52
- gg
53
- gGGg
54
- ggGGgg
55
- tt
56
- tt
57
- `,
58
- { g: COLORS.canopyMid, G: COLORS.canopyDark, t: COLORS.trunkMid },
59
- ),
60
- full: parse(
61
- `
62
- gg
63
- gGGGG
64
- ggGGGGgg
65
- gGGGGg
66
- tt
67
- tt
68
- `,
69
- { g: COLORS.canopyMid, G: COLORS.canopyDark, t: COLORS.trunkMid },
70
- ),
71
- },
72
- pine: {
73
- seed: parse(
74
- `
75
- g
76
- t
77
- `,
78
- { g: COLORS.canopyDeep, t: COLORS.trunkDark },
79
- ),
80
- sapling: parse(
81
- `
82
- g
83
- gg
84
- ggg
85
- t
86
- `,
87
- { g: COLORS.canopyDeep, t: COLORS.trunkDark },
88
- ),
89
- young: parse(
90
- `
91
- g
92
- ggg
93
- gGGGg
94
- ggGGGG
95
- t
96
- t
97
- `,
98
- { g: COLORS.canopyDeep, G: COLORS.canopyDark, t: COLORS.trunkDark },
99
- ),
100
- full: parse(
101
- `
102
- g
103
- ggg
104
- gGGGg
105
- gGGGGGg
106
- ggGGGGGG
107
- gGGGGG
108
- t
109
- t
110
- `,
111
- { g: COLORS.canopyDeep, G: COLORS.canopyDark, t: COLORS.trunkDark },
112
- ),
113
- },
114
- birch: {
115
- seed: parse(
116
- `
117
- g
118
- b
119
- `,
120
- { g: COLORS.canopyLight, b: COLORS.birchTrunk },
121
- ),
122
- sapling: parse(
123
- `
124
- gg
125
- ghg
126
- b
127
- `,
128
- { g: COLORS.canopyLight, h: COLORS.canopyBright, b: COLORS.birchTrunk },
129
- ),
130
- young: parse(
131
- `
132
- hg
133
- hggg
134
- ggghhg
135
- bb
136
- bb
137
- `,
138
- { g: COLORS.canopyLight, h: COLORS.canopyBright, b: COLORS.birchTrunk },
139
- ),
140
- full: parse(
141
- `
142
- hh
143
- hgggh
144
- ggghhgg
145
- hgggh
146
- bb
147
- bb
148
- `,
149
- { g: COLORS.canopyLight, h: COLORS.canopyBright, b: COLORS.birchTrunk },
150
- ),
151
- },
152
- willow: {
153
- seed: parse(
154
- `
155
- g
156
- t
157
- `,
158
- { g: COLORS.canopyLight, t: COLORS.trunkMid },
159
- ),
160
- sapling: parse(
161
- `
162
- ggg
163
- ggggg
164
- ttt
165
- `,
166
- { g: COLORS.canopyLight, t: COLORS.trunkMid },
167
- ),
168
- young: parse(
169
- `
170
- gggg
171
- gggggg
172
- gg ggg gg
173
- gg gg
174
- tt
175
- tt
176
- `,
177
- { g: COLORS.canopyLight, t: COLORS.trunkMid },
178
- ),
179
- full: parse(
180
- `
181
- ggggg
182
- gggggggg
183
- gg ggggg gg
184
- gg ggg gg
185
- gg gg
186
- tt
187
- tt
188
- `,
189
- { g: COLORS.canopyLight, t: COLORS.trunkMid },
190
- ),
191
- },
192
- cherry: {
193
- seed: parse(
194
- `
195
- p
196
- t
197
- `,
198
- { p: COLORS.cherryPink, t: COLORS.trunkLight },
199
- ),
200
- sapling: parse(
201
- `
202
- pp
203
- pPp
204
- t
205
- `,
206
- { p: COLORS.cherryBloom, P: COLORS.cherryPink, t: COLORS.trunkLight },
207
- ),
208
- young: parse(
209
- `
210
- pP
211
- pPPp
212
- pPPpPP
213
- tt
214
- tt
215
- `,
216
- { p: COLORS.cherryBloom, P: COLORS.cherryPink, t: COLORS.trunkLight },
217
- ),
218
- full: parse(
219
- `
220
- pPp
221
- pPPPPp
222
- pPPpPPPp
223
- pPPPpp
224
- tt
225
- tt
226
- `,
227
- { p: COLORS.cherryBloom, P: COLORS.cherryPink, t: COLORS.trunkLight },
228
- ),
229
- },
230
- };
231
-
232
- function getGrowthStage(growth) {
233
- if (growth < 0.2) return "seed";
234
- if (growth < 0.5) return "sapling";
235
- if (growth < 0.8) return "young";
236
- return "full";
237
- }
238
-
239
- export function getSprite(type, growth) {
240
- const spriteSet = SPRITES[type];
241
- if (!spriteSet) {
242
- throw new Error(`Unknown tree type: ${type}`);
243
- }
244
- return spriteSet[getGrowthStage(growth)];
245
- }
package/src/state.js DELETED
@@ -1,53 +0,0 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
-
6
- function resolveHoneydewDir() {
7
- return process.env.HONEYDEW_DIR || path.join(os.homedir(), ".honeydew");
8
- }
9
-
10
- function resolveForestFile() {
11
- return path.join(resolveHoneydewDir(), "forest.json");
12
- }
13
-
14
- export const HONEYDEW_DIR = resolveHoneydewDir();
15
- export const FOREST_FILE = resolveForestFile();
16
-
17
- export function getHoneydewDir() {
18
- return resolveHoneydewDir();
19
- }
20
-
21
- export function getForestFile() {
22
- return resolveForestFile();
23
- }
24
-
25
- export function createEmptyForest() {
26
- return {
27
- trees: [],
28
- totalPrompts: 0,
29
- createdAt: new Date().toISOString(),
30
- lastActiveDate: new Date().toISOString().slice(0, 10),
31
- streak: 0,
32
- };
33
- }
34
-
35
- export function readForest() {
36
- try {
37
- return JSON.parse(fs.readFileSync(resolveForestFile(), "utf8"));
38
- } catch {
39
- return null;
40
- }
41
- }
42
-
43
- export function writeForest(state) {
44
- const dir = resolveHoneydewDir();
45
- const file = resolveForestFile();
46
- fs.mkdirSync(dir, { recursive: true });
47
- const tmpFile = path.join(
48
- dir,
49
- `forest.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`,
50
- );
51
- fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2));
52
- fs.renameSync(tmpFile, file);
53
- }