honeytree 1.0.8 → 1.1.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honeytree",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "description": "A living pixel art forest that grows while you code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useMemo, useState } from "react";
2
2
  import { Box, Text, render, useApp, useInput, useStdout } from "ink";
3
3
 
4
- import { applyActiveMinutes, applyActivityPulse, applyCommit, applyFileSave } from "../core/progression.js";
4
+ import { applyActiveMinutes, applyCommit, applyFileSave } from "../core/progression.js";
5
5
  import { ensureState, readState, updateState } from "../core/state.js";
6
6
  import { renderTerminalFrame } from "../renderers/terminal.js";
7
7
  import { createActivityTracker } from "../tracker/activity.js";
@@ -70,10 +70,7 @@ function ForestWatchApp() {
70
70
  cwd: process.cwd(),
71
71
  onSave() {
72
72
  activity.markActive();
73
- const nextState = updateState((draft) => {
74
- applyActivityPulse(draft);
75
- return applyFileSave(draft);
76
- });
73
+ const nextState = updateState((draft) => applyFileSave(draft));
77
74
  setState(nextState);
78
75
  },
79
76
  });
@@ -105,31 +105,42 @@ export function getWeatherParticles(width, height, tick = 0, weather = "clear",
105
105
  }));
106
106
  }
107
107
 
108
- export function getLlamaAnimation(width, tick = 0, lastSaveTick = -Infinity) {
109
- const speed = 0.06;
110
- const cycle = width * 2;
111
- const raw = (tick * speed) % cycle;
112
- const x = raw < width ? raw : cycle - raw;
113
- const direction = raw < width ? "right" : "left";
114
-
115
- const ticksSinceSave = tick - lastSaveTick;
116
- let pose = "walking";
117
- if (ticksSinceSave < 8) {
118
- pose = "happy";
119
- } else if (ticksSinceSave > 300) {
120
- pose = "sitting";
121
- }
122
-
123
- return { x: Math.round(x), y: 11, direction, pose };
124
- }
125
-
126
- export function getDynamicScene(state, width, tick, environment, lastSaveTick = -Infinity) {
108
+ export function getDynamicScene(state, width, tick, environment) {
127
109
  return {
128
110
  clouds: getClouds(width, tick, environment.sky.name),
129
111
  stars: environment.sky.name === "night" ? getStars(width, tick) : [],
130
112
  animals: getAnimatedAnimals(state, width, tick),
131
113
  particles: getWeatherParticles(width, 12, tick, environment.weather.name, environment.season.name),
132
- llama: getLlamaAnimation(width, tick, lastSaveTick),
114
+ };
115
+ }
116
+
117
+ export function getCrystalPalette(tick) {
118
+ const CRYSTAL_COLORS = ["#00e5ff", "#e040fb", "#69f0ae"];
119
+ const cycle = (tick * 0.02) % 3;
120
+ const index = Math.floor(cycle);
121
+ const next = (index + 1) % 3;
122
+ const t = cycle - index;
123
+ function lerpChannel(a, b, amount) {
124
+ return Math.round(a + (b - a) * amount);
125
+ }
126
+ function parseHex(hex) {
127
+ const n = hex.slice(1);
128
+ return [parseInt(n.slice(0, 2), 16), parseInt(n.slice(2, 4), 16), parseInt(n.slice(4, 6), 16)];
129
+ }
130
+ function toHex(r, g, b) {
131
+ return "#" + [r, g, b].map((v) => Math.min(255, Math.max(0, v)).toString(16).padStart(2, "0")).join("");
132
+ }
133
+ const from = parseHex(CRYSTAL_COLORS[index]);
134
+ const to = parseHex(CRYSTAL_COLORS[next]);
135
+ const mixed = toHex(
136
+ lerpChannel(from[0], to[0], t),
137
+ lerpChannel(from[1], to[1], t),
138
+ lerpChannel(from[2], to[2], t),
139
+ );
140
+ return {
141
+ crystal1: mixed,
142
+ crystal2: CRYSTAL_COLORS[next],
143
+ crystal3: CRYSTAL_COLORS[(next + 1) % 3],
133
144
  };
134
145
  }
135
146
 
@@ -85,10 +85,9 @@ const BASE_PALETTE = {
85
85
  deerLight: "#cba37b",
86
86
  owl: "#5f5b6b",
87
87
  owlLight: "#d8d7dd",
88
- llama: "#e8ddd0",
89
- llamaLight: "#f5f0ea",
90
- llamaDark: "#c4b5a3",
91
- llamaNose: "#f5a0b0",
88
+ crystal1: "#00e5ff",
89
+ crystal2: "#e040fb",
90
+ crystal3: "#69f0ae",
92
91
  };
93
92
 
94
93
  const SEASON_OVERRIDES = {
@@ -1,5 +1,3 @@
1
- import { addMinutes } from "date-fns";
2
-
3
1
  import {
4
2
  getAnimalTypeForMinutes,
5
3
  getAnimalSprite,
@@ -72,10 +70,28 @@ export function updateCommitStreak(currentStreak, lastActiveDate, now = new Date
72
70
  return 1;
73
71
  }
74
72
 
73
+ function chooseRow(trees) {
74
+ const counts = { back: 0, mid: 0, front: 0 };
75
+ for (const tree of trees) {
76
+ const row = tree.row || "front";
77
+ counts[row] = (counts[row] || 0) + 1;
78
+ }
79
+ if (counts.back <= counts.mid && counts.back <= counts.front) return "back";
80
+ if (counts.mid <= counts.front) return "mid";
81
+ return "front";
82
+ }
83
+
84
+ const ROW_SCALE = { back: 0.6, mid: 0.85, front: 1.0 };
85
+
75
86
  export function applyCommit(state, { commitHash = "", now = new Date() } = {}) {
76
87
  const nextTotal = state.total_commits + 1;
77
- const species = getTreeSpeciesForCommit(nextTotal);
88
+ let species = getTreeSpeciesForCommit(nextTotal);
89
+ // Only one crystal tree allowed
90
+ if (species === "crystal_tree" && state.trees.some((t) => t.species === "crystal_tree")) {
91
+ species = "ancient_oak";
92
+ }
78
93
  const sprite = getTreeSprite(species);
94
+ const row = chooseRow(state.trees);
79
95
  state.total_commits = nextTotal;
80
96
  state.current_streak = updateCommitStreak(state.current_streak, state.last_active_date, now);
81
97
  state.last_active_date = dayString(now);
@@ -83,14 +99,20 @@ export function applyCommit(state, { commitHash = "", now = new Date() } = {}) {
83
99
  state.trees.push({
84
100
  species,
85
101
  planted_at: now.toISOString(),
86
- x_position: placeSprite(state.trees, sprite.width, `${species}:${nextTotal}`),
102
+ x_position: placeSprite(
103
+ state.trees.filter((t) => t.row === row),
104
+ sprite.width,
105
+ `${species}:${nextTotal}`,
106
+ ),
107
+ row,
108
+ scale: ROW_SCALE[row],
87
109
  });
88
110
  return state;
89
111
  }
90
112
 
91
113
  export function applyFileSave(state, { now = new Date() } = {}) {
92
114
  state.total_file_saves += 1;
93
- if (state.total_file_saves % 50 === 0) {
115
+ if (state.total_file_saves % 100 === 0) {
94
116
  const type = getGroundElementType(state.total_file_saves);
95
117
  const sprite = getGroundElementSprite(type);
96
118
  state.ground_elements.push({
@@ -125,7 +147,3 @@ export function applyActiveMinutes(state, { minutes = 1, now = new Date() } = {}
125
147
  return state;
126
148
  }
127
149
 
128
- export function applyActivityPulse(state, { now = new Date() } = {}) {
129
- const updated = addMinutes(now, 0);
130
- return state;
131
- }
@@ -8,12 +8,11 @@ export const TREE_SPECIES = [
8
8
  "pine",
9
9
  "willow",
10
10
  "ancient_oak",
11
+ "crystal_tree",
11
12
  ];
12
13
 
13
14
  export const ANIMAL_TYPES = ["butterfly", "rabbit", "fox", "deer", "owl"];
14
15
 
15
- export const LLAMA_POSES = ["walking", "sitting", "happy"];
16
-
17
16
  export const GROUND_ELEMENT_TYPES = ["flower", "mushroom", "rock", "tall_grass"];
18
17
 
19
18
  function parseSprite(template, legend) {
@@ -57,17 +56,18 @@ export function materializeSprite(sprite, palette, char = BLOCK) {
57
56
  }
58
57
 
59
58
  export function getTreeSpeciesForCommit(totalCommits) {
60
- if (totalCommits <= 5) return "sapling";
61
- if (totalCommits <= 15) return "birch";
62
- if (totalCommits <= 30) return "oak";
63
- if (totalCommits <= 50) return "cherry";
64
- if (totalCommits <= 75) return "pine";
65
- if (totalCommits <= 100) return "willow";
66
- return "ancient_oak";
59
+ if (totalCommits <= 2) return "sapling";
60
+ if (totalCommits <= 8) return "birch";
61
+ if (totalCommits <= 18) return "oak";
62
+ if (totalCommits <= 30) return "cherry";
63
+ if (totalCommits <= 45) return "pine";
64
+ if (totalCommits <= 65) return "willow";
65
+ if (totalCommits <= 100) return "ancient_oak";
66
+ return "crystal_tree";
67
67
  }
68
68
 
69
69
  export function getGroundElementType(totalFileSaves) {
70
- const tier = Math.max(0, Math.floor(totalFileSaves / 50) - 1);
70
+ const tier = Math.max(0, Math.floor(totalFileSaves / 100) - 1);
71
71
  return GROUND_ELEMENT_TYPES[tier % GROUND_ELEMENT_TYPES.length];
72
72
  }
73
73
 
@@ -82,9 +82,11 @@ export function getAnimalTypeForMinutes(totalMinutesCoded) {
82
82
  const TREE_SPRITES = {
83
83
  sapling: parseSprite(
84
84
  `
85
- l
86
- lL
87
- t
85
+ l
86
+ lLl
87
+ l
88
+ t
89
+ t
88
90
  `,
89
91
  {
90
92
  l: "leaf",
@@ -94,11 +96,14 @@ const TREE_SPRITES = {
94
96
  ),
95
97
  birch: parseSprite(
96
98
  `
97
- ll
98
- lLL
99
- llLLl
100
- bb
101
- bB
99
+ ll
100
+ lLLl
101
+ lLLLLl
102
+ lLLl
103
+ bb
104
+ bB
105
+ bb
106
+ bB
102
107
  `,
103
108
  {
104
109
  l: "leaf",
@@ -109,12 +114,16 @@ const TREE_SPRITES = {
109
114
  ),
110
115
  oak: parseSprite(
111
116
  `
112
- dd
113
- dllll
114
- dllLLlld
115
- llllll
116
- tt
117
- tTT
117
+ ddd
118
+ ddlllld
119
+ dlllLLllld
120
+ dllLLLLlld
121
+ dlllllld
122
+ llllll
123
+ tt
124
+ tTTt
125
+ tTTt
126
+ tt
118
127
  `,
119
128
  {
120
129
  d: "leafDark",
@@ -126,12 +135,16 @@ const TREE_SPRITES = {
126
135
  ),
127
136
  cherry: parseSprite(
128
137
  `
129
- pp
130
- pPPPP
131
- pPPpPPP
132
- PPPPP
133
- tt
134
- tTT
138
+ ppp
139
+ ppPPPPp
140
+ pPPPpPPPp
141
+ pPPPPPPPp
142
+ pPPPPPp
143
+ PPPPp
144
+ tt
145
+ tTTt
146
+ tTTt
147
+ tt
135
148
  `,
136
149
  {
137
150
  p: "petal",
@@ -142,13 +155,16 @@ const TREE_SPRITES = {
142
155
  ),
143
156
  pine: parseSprite(
144
157
  `
145
- p
146
- pPp
147
- pPPPp
148
- pPPPPPp
149
- PPPPP
150
- tT
151
- tT
158
+ p
159
+ pPp
160
+ pPPPp
161
+ PPP
162
+ pPPPp
163
+ pPPPPPp
164
+ PPPPP
165
+ tTt
166
+ tTt
167
+ t
152
168
  `,
153
169
  {
154
170
  p: "pineDark",
@@ -159,13 +175,18 @@ const TREE_SPRITES = {
159
175
  ),
160
176
  willow: parseSprite(
161
177
  `
162
- llll
163
- llLLLLll
164
- llLllllLll
165
- ll ll
166
- ll ll
167
- tTT
168
- tTT
178
+ llll
179
+ llLLLLll
180
+ llLLllLLll
181
+ lLl lLl
182
+ ll ll
183
+ ll ll
184
+ l l
185
+ tTTt
186
+ tTTt
187
+ tT
188
+ tT
189
+ tt
169
190
  `,
170
191
  {
171
192
  l: "leaf",
@@ -176,14 +197,20 @@ const TREE_SPRITES = {
176
197
  ),
177
198
  ancient_oak: parseSprite(
178
199
  `
179
- dd
180
- ddllllld
181
- dllLLLllld
182
- ddllLLLllld
183
- llllllll
184
- mmm
185
- tTTT
186
- ttTTtt
200
+ ddd
201
+ dddllllddd
202
+ ddllLLLLlldd
203
+ dddllLLLLllddd
204
+ ddlllLLLllldd
205
+ ddlllllllldd
206
+ mllllllm
207
+ mmmm
208
+ tTTTTt
209
+ tTTTTt
210
+ ttTTTTtt
211
+ tt tt
212
+ t t
213
+ t t
187
214
  `,
188
215
  {
189
216
  d: "leafDark",
@@ -194,13 +221,37 @@ const TREE_SPRITES = {
194
221
  T: "trunk",
195
222
  },
196
223
  ),
224
+ crystal_tree: parseSprite(
225
+ `
226
+ cc
227
+ ccCCCc
228
+ cCCcCCCc
229
+ cCCCCCCc
230
+ cCCCCc
231
+ CCCc
232
+ tt
233
+ tTTt
234
+ tTTt
235
+ tT
236
+ tt
237
+ tt
238
+ `,
239
+ {
240
+ c: "crystal1",
241
+ C: "crystal2",
242
+ t: "trunkDark",
243
+ T: "trunk",
244
+ },
245
+ ),
197
246
  };
198
247
 
199
248
  const ANIMAL_SPRITES = {
200
249
  butterfly: parseSprite(
201
250
  `
202
- w w
203
- W
251
+ w w
252
+ wW Ww
253
+ WWW
254
+ W
204
255
  `,
205
256
  {
206
257
  w: "wing",
@@ -209,9 +260,10 @@ w w
209
260
  ),
210
261
  rabbit: parseSprite(
211
262
  `
212
- ee
213
- rrre
214
- rr
263
+ ee
264
+ rrre
265
+ rrrrr
266
+ rr r
215
267
  `,
216
268
  {
217
269
  r: "rabbit",
@@ -220,9 +272,11 @@ rrre
220
272
  ),
221
273
  fox: parseSprite(
222
274
  `
223
- ff
224
- ffff
225
- fllf
275
+ ff
276
+ ffff
277
+ ffffff
278
+ fl lf
279
+ f f
226
280
  `,
227
281
  {
228
282
  f: "fox",
@@ -231,20 +285,26 @@ fllf
231
285
  ),
232
286
  deer: parseSprite(
233
287
  `
288
+ a a
234
289
  aa
235
- dddd
236
- d dd
290
+ dddddd
291
+ dl ld
292
+ d d
293
+ d d
237
294
  `,
238
295
  {
239
296
  a: "deerLight",
240
297
  d: "deer",
298
+ l: "deerLight",
241
299
  },
242
300
  ),
243
301
  owl: parseSprite(
244
302
  `
245
- oo
246
- OooO
247
- tt
303
+ oo
304
+ OooO
305
+ OooooO
306
+ oooo
307
+ tt
248
308
  `,
249
309
  {
250
310
  o: "owl",
@@ -254,47 +314,6 @@ OooO
254
314
  ),
255
315
  };
256
316
 
257
- const LLAMA_SPRITES = {
258
- walking: parseSprite(
259
- `
260
- ee
261
- lllN
262
- llll
263
- l l
264
- `,
265
- {
266
- e: "llamaLight",
267
- l: "llama",
268
- N: "llamaNose",
269
- },
270
- ),
271
- sitting: parseSprite(
272
- `
273
- ee
274
- lllN
275
- lll
276
- ll
277
- `,
278
- {
279
- e: "llamaLight",
280
- l: "llama",
281
- N: "llamaNose",
282
- },
283
- ),
284
- happy: parseSprite(
285
- `
286
- ee
287
- lllN
288
- llll
289
- l l
290
- `,
291
- {
292
- e: "llamaLight",
293
- l: "llama",
294
- N: "llamaNose",
295
- },
296
- ),
297
- };
298
317
 
299
318
  const GROUND_SPRITES = {
300
319
  flower: parseSprite(
@@ -364,13 +383,6 @@ export function getGroundElementSprite(type) {
364
383
  return sprite;
365
384
  }
366
385
 
367
- export function getLlamaSprite(pose = "walking") {
368
- const sprite = LLAMA_SPRITES[pose];
369
- if (!sprite) {
370
- throw new Error(`Unknown llama pose: ${pose}`);
371
- }
372
- return sprite;
373
- }
374
386
 
375
387
  export function getRandomGroundElement() {
376
388
  return GROUND_ELEMENT_TYPES[Math.floor(Math.random() * GROUND_ELEMENT_TYPES.length)];
package/src/core/state.js CHANGED
@@ -40,17 +40,30 @@ export function createEmptyState(now = new Date()) {
40
40
  current_streak: 0,
41
41
  last_active_date: todayString(now),
42
42
  last_commit_hash: "",
43
+ global_trees: 0,
43
44
  trees: [],
44
45
  animals: [],
45
46
  ground_elements: [],
46
47
  };
47
48
  }
48
49
 
49
- function normalizeTree(tree) {
50
+ function assignRowByIndex(index, total) {
51
+ if (total <= 0) return "front";
52
+ const third = total / 3;
53
+ if (index < third) return "back";
54
+ if (index < third * 2) return "mid";
55
+ return "front";
56
+ }
57
+
58
+ function normalizeTree(tree, index, total) {
59
+ const row = tree?.row || assignRowByIndex(index, total);
60
+ const scaleMap = { back: 0.6, mid: 0.85, front: 1.0 };
50
61
  return {
51
62
  species: tree?.species ?? tree?.type ?? "sapling",
52
63
  planted_at: tree?.planted_at ?? tree?.plantedAt ?? new Date().toISOString(),
53
64
  x_position: Number.isFinite(tree?.x_position) ? tree.x_position : tree?.x ?? 12,
65
+ row,
66
+ scale: scaleMap[row] ?? 1.0,
54
67
  };
55
68
  }
56
69
 
@@ -88,7 +101,8 @@ function rescalePositions(items) {
88
101
 
89
102
  export function normalizeState(input = {}) {
90
103
  const base = createEmptyState();
91
- const trees = Array.isArray(input.trees) ? input.trees.map(normalizeTree) : [];
104
+ const rawTrees = Array.isArray(input.trees) ? input.trees : [];
105
+ const trees = rawTrees.map((tree, index) => normalizeTree(tree, index, rawTrees.length));
92
106
  const animals = Array.isArray(input.animals) ? input.animals.map(normalizeAnimal) : [];
93
107
  const ground_elements = Array.isArray(input.ground_elements)
94
108
  ? input.ground_elements.map(normalizeGroundElement)
@@ -107,6 +121,7 @@ export function normalizeState(input = {}) {
107
121
  ? input.last_active_date
108
122
  : input.lastActiveDate ?? base.last_active_date,
109
123
  last_commit_hash: typeof input.last_commit_hash === "string" ? input.last_commit_hash : "",
124
+ global_trees: Number.isFinite(input.global_trees) ? input.global_trees : 0,
110
125
  trees: rescalePositions(trees),
111
126
  animals: rescalePositions(animals),
112
127
  ground_elements: rescalePositions(ground_elements),
@@ -118,7 +133,8 @@ export function migrateOldForest(oldForest = {}) {
118
133
  migrated.total_commits = oldForest.totalPrompts ?? oldForest.trees?.length ?? 0;
119
134
  migrated.current_streak = oldForest.streak ?? 0;
120
135
  migrated.last_active_date = oldForest.lastActiveDate ?? todayString();
121
- migrated.trees = Array.isArray(oldForest.trees) ? oldForest.trees.map(normalizeTree) : [];
136
+ const rawTrees = Array.isArray(oldForest.trees) ? oldForest.trees : [];
137
+ migrated.trees = rawTrees.map((tree, index) => normalizeTree(tree, index, rawTrees.length));
122
138
  return migrated;
123
139
  }
124
140
 
@@ -1,12 +1,11 @@
1
1
  import chalk from "chalk";
2
2
 
3
- import { getDynamicScene } from "../core/animation.js";
3
+ import { getDynamicScene, getCrystalPalette } from "../core/animation.js";
4
4
  import { getEnvironmentSnapshot } from "../core/environment.js";
5
5
  import { VIRTUAL_WIDTH } from "../core/progression.js";
6
6
  import {
7
7
  getAnimalSprite,
8
8
  getGroundElementSprite,
9
- getLlamaSprite,
10
9
  getTreeSprite,
11
10
  materializeSprite,
12
11
  } from "../core/sprites.js";
@@ -129,8 +128,33 @@ function scaleX(virtualX, width) {
129
128
 
130
129
  function drawForest(buffer, width, state, environment, tick) {
131
130
  const treeBaseY = SKY_ROWS + FOREST_ROWS - 1;
132
- for (const tree of state.trees) {
133
- compositeSprite(buffer, getTreeSprite(tree.species), scaleX(tree.x_position, width), treeBaseY, environment.palette);
131
+ const isSunsetSilhouette = environment.sky.name === "sunset";
132
+ const silhouettePalette = {};
133
+ if (isSunsetSilhouette) {
134
+ for (const key of Object.keys(environment.palette)) {
135
+ silhouettePalette[key] = "#1a1a2e";
136
+ }
137
+ }
138
+
139
+ const crystalPalette = getCrystalPalette(tick);
140
+
141
+ // Sort trees by row: back first, then mid, then front
142
+ const rowOrder = { back: 0, mid: 1, front: 2 };
143
+ const sortedTrees = [...state.trees].sort(
144
+ (a, b) => (rowOrder[a.row] ?? 2) - (rowOrder[b.row] ?? 2),
145
+ );
146
+
147
+ for (const tree of sortedTrees) {
148
+ const row = tree.row || "front";
149
+ const rowY = row === "back" ? treeBaseY - 2 : row === "mid" ? treeBaseY - 1 : treeBaseY;
150
+ let palette = environment.palette;
151
+ if (tree.species === "crystal_tree") {
152
+ palette = { ...environment.palette, ...crystalPalette };
153
+ }
154
+ if (isSunsetSilhouette) {
155
+ palette = silhouettePalette;
156
+ }
157
+ compositeSprite(buffer, getTreeSprite(tree.species), scaleX(tree.x_position, width), rowY, palette);
134
158
  }
135
159
 
136
160
  for (const element of state.ground_elements) {
@@ -150,18 +174,6 @@ function drawForest(buffer, width, state, environment, tick) {
150
174
  environment.palette,
151
175
  );
152
176
  }
153
-
154
- // Llama companion
155
- const llama = scene.llama;
156
- if (llama) {
157
- compositeSprite(
158
- buffer,
159
- getLlamaSprite(llama.pose),
160
- llama.x,
161
- Math.min(ART_ROWS - 2, llama.y),
162
- environment.palette,
163
- );
164
- }
165
177
  }
166
178
 
167
179
  function drawGround(buffer, width, environment) {