honeytree 1.1.2 → 1.1.4

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/core/state.js CHANGED
@@ -46,6 +46,8 @@ export function createEmptyState(now = new Date()) {
46
46
  trees: [],
47
47
  animals: [],
48
48
  ground_elements: [],
49
+ biome: null,
50
+ activeSession: null,
49
51
  };
50
52
  }
51
53
 
@@ -127,6 +129,10 @@ export function normalizeState(input = {}) {
127
129
  trees: rescalePositions(trees),
128
130
  animals: rescalePositions(animals),
129
131
  ground_elements: rescalePositions(ground_elements),
132
+ biome: typeof input.biome === "string" ? input.biome : null,
133
+ activeSession: input.activeSession && typeof input.activeSession.type === "string"
134
+ ? { type: input.activeSession.type, strength: input.activeSession.strength ?? 0 }
135
+ : null,
130
136
  };
131
137
  }
132
138
 
@@ -1,16 +1,20 @@
1
1
  import chalk from "chalk";
2
2
 
3
3
  import { getDynamicScene, getCrystalPalette } from "../core/animation.js";
4
+ import { getBiomeConfig } from "../core/biomes/index.js";
4
5
  import { getEnvironmentSnapshot, lerpColor } from "../core/environment.js";
5
6
  import { VIRTUAL_WIDTH } from "../core/progression.js";
6
7
  import {
7
8
  getAnimalSprite,
9
+ getBushSprite,
8
10
  getGroundElementSprite,
9
11
  getTreeSprite,
10
12
  materializeSprite,
11
13
  } from "../core/sprites.js";
12
14
 
13
15
  export const SCENE_HEIGHT = 14;
16
+ const MIN_VISIBLE_TREES = 8;
17
+ const MAX_VISIBLE_TREES = 18;
14
18
 
15
19
  export function computeLayout(termHeight) {
16
20
  if (!termHeight || termHeight < 16) {
@@ -46,24 +50,54 @@ function shuffleSeeded(items, rng) {
46
50
  }
47
51
 
48
52
  export function selectViewportTrees(trees, width, seed = 0) {
49
- const maxVisible = 12;
53
+ const maxVisible = Math.max(MIN_VISIBLE_TREES, Math.min(MAX_VISIBLE_TREES, Math.floor(width / 10)));
50
54
  if (trees.length === 0) return [];
51
55
 
52
56
  const rng = mulberry32(seed);
53
- const selected = shuffleSeeded(trees, rng).slice(0, Math.min(maxVisible, trees.length));
54
57
  const rowNames = ["back", "mid", "front"];
55
- const perRow = Math.ceil(selected.length / rowNames.length);
56
- const assigned = selected.map((tree, index) => ({
57
- ...tree,
58
- row: rowNames[Math.min(rowNames.length - 1, Math.floor(index / perRow))],
59
- }));
58
+ const visibleCount = Math.min(maxVisible, trees.length);
59
+ const basePerRow = Math.floor(visibleCount / rowNames.length);
60
+ const extraRows = visibleCount % rowNames.length;
61
+ const assigned = [];
62
+ const used = new Set();
63
+
64
+ rowNames.forEach((rowName, rowIndex) => {
65
+ const targetCount = basePerRow + (rowIndex < extraRows ? 1 : 0);
66
+ const rowTrees = shuffleSeeded(
67
+ trees.filter((tree, treeIndex) => (tree.row || "front") === rowName && !used.has(treeIndex)),
68
+ rng,
69
+ );
70
+
71
+ for (const tree of rowTrees.slice(0, targetCount)) {
72
+ const originalIndex = trees.indexOf(tree);
73
+ used.add(originalIndex);
74
+ assigned.push({ ...tree, row: rowName });
75
+ }
76
+ });
77
+
78
+ if (assigned.length < visibleCount) {
79
+ const remaining = shuffleSeeded(
80
+ trees
81
+ .map((tree, index) => ({ tree, index }))
82
+ .filter(({ index }) => !used.has(index)),
83
+ rng,
84
+ );
85
+ for (const { tree } of remaining.slice(0, visibleCount - assigned.length)) {
86
+ const counts = Object.fromEntries(rowNames.map((rowName) => [
87
+ rowName,
88
+ assigned.filter((item) => item.row === rowName).length,
89
+ ]));
90
+ const row = rowNames.reduce((best, rowName) => (counts[rowName] < counts[best] ? rowName : best), "back");
91
+ assigned.push({ ...tree, row });
92
+ }
93
+ }
60
94
 
61
95
  for (const rowName of rowNames) {
62
96
  const rowTrees = assigned.filter((tree) => tree.row === rowName);
63
97
  const count = rowTrees.length;
64
98
  if (count === 0) continue;
65
99
 
66
- const margin = rowName === "front" ? 10 : rowName === "mid" ? 8 : 6;
100
+ const margin = rowName === "front" ? 8 : rowName === "mid" ? 6 : 4;
67
101
  const clampedMargin = Math.min(margin, Math.max(4, Math.floor(width / 4)));
68
102
  const minX = Math.min(clampedMargin, Math.max(0, width - 1));
69
103
  const maxX = Math.max(minX, width - clampedMargin);
@@ -248,10 +282,71 @@ function desaturatePalette(palette, skyBottom, amount) {
248
282
  return result;
249
283
  }
250
284
 
285
+ function silhouettePalette(palette) {
286
+ const result = {};
287
+ for (const key of Object.keys(palette)) {
288
+ result[key] = key.toLowerCase().includes("light") ? "#24243f" : "#111827";
289
+ }
290
+ return result;
291
+ }
292
+
293
+ function applySessionEffects(environment, biomeConfig, activeSession) {
294
+ if (!activeSession) return { environment, density: biomeConfig.undergrowthDensity };
295
+
296
+ let density = biomeConfig.undergrowthDensity;
297
+ let palette = { ...environment.palette };
298
+
299
+ switch (activeSession.type) {
300
+ case "refactoring":
301
+ // Reduce undergrowth, brighten palette slightly
302
+ density *= 0.5;
303
+ for (const key of Object.keys(palette)) {
304
+ if (typeof palette[key] === "string" && palette[key].startsWith("#")) {
305
+ palette[key] = lerpColor(palette[key], "#ffffff", 0.05);
306
+ }
307
+ }
308
+ break;
309
+ case "bugfix":
310
+ // Reduce wilt effect (handled elsewhere), no density change
311
+ break;
312
+ case "docs":
313
+ // No palette change, lanterns added in ground elements
314
+ break;
315
+ case "testing":
316
+ // Slightly more structured — no visual change in palette
317
+ break;
318
+ }
319
+
320
+ return {
321
+ environment: { ...environment, palette },
322
+ density,
323
+ };
324
+ }
325
+
326
+ function drawFarTrees(buffer, width, state, environment, layout) {
327
+ const { skyRows, forestRows } = layout;
328
+ const farY = skyRows + Math.floor(forestRows * 0.3);
329
+ const rng = mulberry32(7 + state.trees.length);
330
+ const count = Math.min(state.trees.length, Math.floor(width / 6));
331
+
332
+ const farPalette = desaturatePalette(environment.palette, environment.sky.bottom, 0.5);
333
+
334
+ for (let i = 0; i < count; i++) {
335
+ const x = Math.floor(rng() * width);
336
+ try {
337
+ const sprite = getTreeSprite("sapling");
338
+ compositeSprite(buffer, sprite, x, farY, farPalette, { minY: skyRows });
339
+ } catch {
340
+ // skip if sprite not found
341
+ }
342
+ }
343
+ }
344
+
251
345
  function drawForest(buffer, width, viewportTrees, state, environment, tick, layout) {
252
346
  const { skyRows, forestRows, artRows, groundRows } = layout;
253
347
  const treeBaseY = skyRows + forestRows - 1;
254
348
  const crystalPalette = getCrystalPalette(tick);
349
+ const isSunsetSilhouette = environment.sky.name === "sunset";
255
350
 
256
351
  const rowOffset = { back: 4, mid: 2, front: 0 };
257
352
  const rowDesaturation = { back: 0.3, mid: 0.1, front: 0 };
@@ -261,7 +356,9 @@ function drawForest(buffer, width, viewportTrees, state, environment, tick, layo
261
356
 
262
357
  for (const tree of rowTrees) {
263
358
  let palette = environment.palette;
264
- if (rowDesaturation[targetRow] > 0) {
359
+ if (isSunsetSilhouette) {
360
+ palette = silhouettePalette(palette);
361
+ } else if (rowDesaturation[targetRow] > 0) {
265
362
  palette = desaturatePalette(palette, environment.sky.bottom, rowDesaturation[targetRow]);
266
363
  }
267
364
  if (tree.species === "crystal_tree") {
@@ -293,6 +390,33 @@ function drawForest(buffer, width, viewportTrees, state, environment, tick, layo
293
390
  }
294
391
  }
295
392
 
393
+ function drawUndergrowth(buffer, width, state, environment, layout, density) {
394
+ const biomeKey = state.biome || "general";
395
+ const biomeConfig = getBiomeConfig(biomeKey);
396
+ const bushTypes = biomeConfig.bushes;
397
+
398
+ if (!bushTypes || bushTypes.length === 0 || density <= 0) return;
399
+
400
+ const { skyRows, forestRows } = layout;
401
+ const groundY = skyRows + forestRows - 1;
402
+
403
+ // Use seeded RNG for deterministic placement
404
+ const rng = mulberry32(42 + width);
405
+ const count = Math.floor(width * density * 0.15);
406
+
407
+ for (let i = 0; i < count; i++) {
408
+ const bushType = bushTypes[Math.floor(rng() * bushTypes.length)];
409
+ const x = Math.floor(rng() * width);
410
+ let sprite;
411
+ try {
412
+ sprite = getBushSprite(bushType);
413
+ } catch {
414
+ continue;
415
+ }
416
+ compositeSprite(buffer, sprite, x, groundY, environment.palette, { minY: skyRows });
417
+ }
418
+ }
419
+
296
420
  function drawGround(buffer, width, environment, layout) {
297
421
  const { skyRows, forestRows, groundRows } = layout;
298
422
  const groundStart = skyRows + forestRows;
@@ -360,7 +484,8 @@ function drawFog(buffer, width, layout, environment) {
360
484
  }
361
485
 
362
486
  function buildStatsLine(state, environment, width, color) {
363
- const trees = `${state.trees.length} tree${state.trees.length === 1 ? "" : "s"} in your forest`;
487
+ const biomeLabel = state.biome ? getBiomeConfig(state.biome).name : "Forest";
488
+ const trees = `${state.trees.length} tree${state.trees.length === 1 ? "" : "s"} in your ${biomeLabel.toLowerCase()}`;
364
489
  const animals = `${state.animals.length} animal${state.animals.length === 1 ? "" : "s"}`;
365
490
  const streak = `${state.current_streak} day streak`;
366
491
  const weather = `${environment.season.icon} ${environment.season.label} ${environment.weather.label}`;
@@ -381,7 +506,26 @@ function buildStatsLine(state, environment, width, color) {
381
506
  export function buildTerminalScene(state, termWidth = 80, tick = 0, options = {}) {
382
507
  const width = Math.max(20, termWidth);
383
508
  const date = options.now instanceof Date ? options.now : new Date();
384
- const environment = getEnvironmentSnapshot(date, state.current_streak ?? 0);
509
+ const biomeKey = state.biome || null;
510
+ let environment = getEnvironmentSnapshot(date, state.current_streak ?? 0, biomeKey);
511
+
512
+ // Blend biome sky with time-of-day sky (biome is subtle tint, not full override)
513
+ const biomeConfig = biomeKey ? getBiomeConfig(biomeKey) : null;
514
+ if (biomeConfig?.sky?.top) {
515
+ environment.sky = {
516
+ ...environment.sky,
517
+ top: lerpColor(environment.sky.top, biomeConfig.sky.top, 0.3),
518
+ mid: lerpColor(environment.sky.mid, biomeConfig.sky.mid, 0.3),
519
+ bottom: lerpColor(environment.sky.bottom, biomeConfig.sky.bottom, 0.3),
520
+ };
521
+ }
522
+
523
+ const sessionBiomeConfig = biomeKey ? getBiomeConfig(biomeKey) : getBiomeConfig("general");
524
+
525
+ // Apply session effects
526
+ const sessionMods = applySessionEffects(environment, sessionBiomeConfig, state.activeSession);
527
+ environment = sessionMods.environment;
528
+
385
529
  const layout = computeLayout(options.termHeight || null);
386
530
  const buffer = createBuffer(width, layout.artRows);
387
531
  const startOfYear = new Date(date.getFullYear(), 0, 0);
@@ -392,7 +536,9 @@ export function buildTerminalScene(state, termWidth = 80, tick = 0, options = {}
392
536
  drawSky(buffer, width, environment, tick, layout);
393
537
  drawWeather(buffer, width, environment, tick, layout);
394
538
  drawSeasonParticles(buffer, width, environment, tick, layout);
539
+ drawFarTrees(buffer, width, state, environment, layout);
395
540
  drawForest(buffer, width, viewportTrees, state, environment, tick, layout);
541
+ drawUndergrowth(buffer, width, state, environment, layout, sessionMods.density);
396
542
  drawGround(buffer, width, environment, layout);
397
543
  drawFog(buffer, width, layout, environment);
398
544
 
@@ -400,6 +546,7 @@ export function buildTerminalScene(state, termWidth = 80, tick = 0, options = {}
400
546
  width,
401
547
  environment,
402
548
  layout,
549
+ viewportTrees,
403
550
  buffer,
404
551
  statsLine: buildStatsLine(state, environment, width, options.color !== false),
405
552
  };
@@ -410,6 +557,7 @@ export function renderTerminalFrame(state, termWidth = 80, tick = 0, options = {
410
557
  const environment = getEnvironmentSnapshot(
411
558
  options.now instanceof Date ? options.now : new Date(),
412
559
  state.current_streak ?? 0,
560
+ state.biome || null,
413
561
  );
414
562
  return buildStatsLine(state, environment, Math.max(20, termWidth), options.color !== false);
415
563
  }