honeytree 1.1.0 → 1.1.1

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.1.0",
3
+ "version": "1.1.1",
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 chalk from "chalk";
2
2
 
3
3
  import { getDynamicScene, getCrystalPalette } from "../core/animation.js";
4
- import { getEnvironmentSnapshot } from "../core/environment.js";
4
+ import { getEnvironmentSnapshot, lerpColor } from "../core/environment.js";
5
5
  import { VIRTUAL_WIDTH } from "../core/progression.js";
6
6
  import {
7
7
  getAnimalSprite,
@@ -20,14 +20,20 @@ export const SCENE_HEIGHT = ART_ROWS + STATS_ROWS;
20
20
 
21
21
  function createBuffer(width) {
22
22
  return Array.from({ length: ART_ROWS }, () =>
23
- Array.from({ length: width }, () => ({ char: " ", color: null })),
23
+ Array.from({ length: width }, () => ({ char: " ", color: null, bg: null })),
24
24
  );
25
25
  }
26
26
 
27
27
  function paint(buffer, x, y, char, color) {
28
28
  if (y < 0 || y >= buffer.length) return;
29
29
  if (x < 0 || x >= buffer[0].length) return;
30
- buffer[y][x] = { char, color };
30
+ buffer[y][x] = { char, color, bg: buffer[y][x].bg };
31
+ }
32
+
33
+ function paintBg(buffer, x, y, bg) {
34
+ if (y < 0 || y >= buffer.length) return;
35
+ if (x < 0 || x >= buffer[0].length) return;
36
+ buffer[y][x].bg = bg;
31
37
  }
32
38
 
33
39
  function compositeSprite(buffer, sprite, centerX, baseY, palette) {
@@ -44,11 +50,42 @@ function compositeSprite(buffer, sprite, centerX, baseY, palette) {
44
50
  }
45
51
  }
46
52
 
47
- function colorize(text, color, enabled) {
48
- if (!enabled || !color) {
49
- return text;
53
+ function colorize(text, color, bg, enabled) {
54
+ if (!enabled) return text;
55
+ if (!color && !bg) return text;
56
+ let fn = chalk;
57
+ if (bg) fn = fn.bgHex(bg);
58
+ if (color) fn = fn.hex(color);
59
+ return fn(text);
60
+ }
61
+
62
+ function drawSkyBackground(buffer, width, environment) {
63
+ const sky = environment.sky;
64
+ // Sky rows: vertical gradient from top → mid → bottom
65
+ for (let y = 0; y < SKY_ROWS; y++) {
66
+ const t = y / Math.max(1, SKY_ROWS - 1);
67
+ let color;
68
+ if (t < 0.5) {
69
+ color = lerpColor(sky.top, sky.mid, t * 2);
70
+ } else {
71
+ color = lerpColor(sky.mid, sky.bottom, (t - 0.5) * 2);
72
+ }
73
+ for (let x = 0; x < width; x++) {
74
+ paintBg(buffer, x, y, color);
75
+ }
76
+ }
77
+ // Forest rows: blend from sky.bottom toward ground
78
+ for (let y = SKY_ROWS; y < SKY_ROWS + FOREST_ROWS; y++) {
79
+ const t = (y - SKY_ROWS) / Math.max(1, FOREST_ROWS - 1);
80
+ const color = lerpColor(sky.bottom, environment.palette.groundDark, t * 0.7);
81
+ for (let x = 0; x < width; x++) {
82
+ paintBg(buffer, x, y, color);
83
+ }
84
+ }
85
+ // Ground row: solid ground color
86
+ for (let x = 0; x < width; x++) {
87
+ paintBg(buffer, x, ART_ROWS - 1, environment.palette.groundDark);
50
88
  }
51
- return chalk.hex(color)(text);
52
89
  }
53
90
 
54
91
  function drawSky(buffer, width, environment, tick) {
@@ -126,6 +163,20 @@ function scaleX(virtualX, width) {
126
163
  return Math.round((virtualX / VIRTUAL_WIDTH) * width);
127
164
  }
128
165
 
166
+ function spaceTrees(trees, width, minGap) {
167
+ const sorted = [...trees].sort((a, b) => a.x_position - b.x_position);
168
+ const result = [];
169
+ let lastScreenX = -Infinity;
170
+ for (const tree of sorted) {
171
+ const screenX = Math.round((tree.x_position / VIRTUAL_WIDTH) * width);
172
+ if (screenX - lastScreenX >= minGap) {
173
+ result.push(tree);
174
+ lastScreenX = screenX;
175
+ }
176
+ }
177
+ return result;
178
+ }
179
+
129
180
  function drawForest(buffer, width, state, environment, tick) {
130
181
  const treeBaseY = SKY_ROWS + FOREST_ROWS - 1;
131
182
  const isSunsetSilhouette = environment.sky.name === "sunset";
@@ -138,23 +189,24 @@ function drawForest(buffer, width, state, environment, tick) {
138
189
 
139
190
  const crystalPalette = getCrystalPalette(tick);
140
191
 
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
- );
192
+ // Separate trees by row, then filter for spacing
193
+ const minGap = { back: 6, mid: 10, front: 14 };
194
+ const rows = ["back", "mid", "front"];
195
+ for (const targetRow of rows) {
196
+ const rowTrees = state.trees.filter((t) => (t.row || "front") === targetRow);
197
+ const spaced = spaceTrees(rowTrees, width, minGap[targetRow]);
198
+ const rowY = targetRow === "back" ? treeBaseY - 2 : targetRow === "mid" ? treeBaseY - 1 : treeBaseY;
146
199
 
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;
200
+ for (const tree of spaced) {
201
+ let palette = environment.palette;
202
+ if (tree.species === "crystal_tree") {
203
+ palette = { ...environment.palette, ...crystalPalette };
204
+ }
205
+ if (isSunsetSilhouette) {
206
+ palette = silhouettePalette;
207
+ }
208
+ compositeSprite(buffer, getTreeSprite(tree.species), scaleX(tree.x_position, width), rowY, palette);
156
209
  }
157
- compositeSprite(buffer, getTreeSprite(tree.species), scaleX(tree.x_position, width), rowY, palette);
158
210
  }
159
211
 
160
212
  for (const element of state.ground_elements) {
@@ -162,7 +214,9 @@ function drawForest(buffer, width, state, environment, tick) {
162
214
  }
163
215
 
164
216
  const scene = getDynamicScene(state, width, tick, environment);
165
- for (const animal of scene.animals) {
217
+ const maxAnimals = Math.max(2, Math.min(4, Math.floor(width / 25)));
218
+ const visibleAnimals = scene.animals.slice(0, maxAnimals);
219
+ for (const animal of visibleAnimals) {
166
220
  if (animal.type === "owl" && environment.sky.name !== "night") {
167
221
  continue;
168
222
  }
@@ -187,12 +241,12 @@ function buildStatsLine(state, environment, width, color) {
187
241
  const animals = `${state.animals.length} animal${state.animals.length === 1 ? "" : "s"}`;
188
242
  const streak = `${state.current_streak} day streak`;
189
243
  const weather = `${environment.season.icon} ${environment.season.label} ${environment.weather.label}`;
190
- const separator = colorize(" • ", "#6b7280", color);
244
+ const separator = colorize(" • ", "#6b7280", null, color);
191
245
  const line = [
192
- colorize("🌲 " + trees, "#7bd389", color),
193
- colorize("🐇 " + animals, "#f4d35e", color),
194
- colorize("🔥 " + streak, "#ee964b", color),
195
- colorize(weather, "#d4d4d8", color),
246
+ colorize("🌲 " + trees, "#7bd389", null, color),
247
+ colorize("🐇 " + animals, "#f4d35e", null, color),
248
+ colorize("🔥 " + streak, "#ee964b", null, color),
249
+ colorize(weather, "#d4d4d8", null, color),
196
250
  ].join(separator);
197
251
 
198
252
  if (line.length >= width) {
@@ -207,6 +261,7 @@ export function buildTerminalScene(state, termWidth = 80, tick = 0, options = {}
207
261
  const environment = getEnvironmentSnapshot(date, state.current_streak ?? 0);
208
262
  const buffer = createBuffer(width);
209
263
 
264
+ drawSkyBackground(buffer, width, environment);
210
265
  drawSky(buffer, width, environment, tick);
211
266
  drawWeather(buffer, width, environment, tick);
212
267
  drawSeasonParticles(buffer, width, environment, tick);
@@ -233,7 +288,7 @@ export function renderTerminalFrame(state, termWidth = 80, tick = 0, options = {
233
288
  const scene = buildTerminalScene(state, termWidth, tick, options);
234
289
  const lines = scene.buffer.map((row) =>
235
290
  row
236
- .map((cell) => colorize(cell.char, cell.color, options.color !== false))
291
+ .map((cell) => colorize(cell.char, cell.color, cell.bg, options.color !== false))
237
292
  .join(""),
238
293
  );
239
294
  lines.push(scene.statsLine);