honeytree 1.1.3 → 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);
@@ -256,6 +290,58 @@ function silhouettePalette(palette) {
256
290
  return result;
257
291
  }
258
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
+
259
345
  function drawForest(buffer, width, viewportTrees, state, environment, tick, layout) {
260
346
  const { skyRows, forestRows, artRows, groundRows } = layout;
261
347
  const treeBaseY = skyRows + forestRows - 1;
@@ -304,6 +390,33 @@ function drawForest(buffer, width, viewportTrees, state, environment, tick, layo
304
390
  }
305
391
  }
306
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
+
307
420
  function drawGround(buffer, width, environment, layout) {
308
421
  const { skyRows, forestRows, groundRows } = layout;
309
422
  const groundStart = skyRows + forestRows;
@@ -371,7 +484,8 @@ function drawFog(buffer, width, layout, environment) {
371
484
  }
372
485
 
373
486
  function buildStatsLine(state, environment, width, color) {
374
- 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()}`;
375
489
  const animals = `${state.animals.length} animal${state.animals.length === 1 ? "" : "s"}`;
376
490
  const streak = `${state.current_streak} day streak`;
377
491
  const weather = `${environment.season.icon} ${environment.season.label} ${environment.weather.label}`;
@@ -392,7 +506,26 @@ function buildStatsLine(state, environment, width, color) {
392
506
  export function buildTerminalScene(state, termWidth = 80, tick = 0, options = {}) {
393
507
  const width = Math.max(20, termWidth);
394
508
  const date = options.now instanceof Date ? options.now : new Date();
395
- 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
+
396
529
  const layout = computeLayout(options.termHeight || null);
397
530
  const buffer = createBuffer(width, layout.artRows);
398
531
  const startOfYear = new Date(date.getFullYear(), 0, 0);
@@ -403,7 +536,9 @@ export function buildTerminalScene(state, termWidth = 80, tick = 0, options = {}
403
536
  drawSky(buffer, width, environment, tick, layout);
404
537
  drawWeather(buffer, width, environment, tick, layout);
405
538
  drawSeasonParticles(buffer, width, environment, tick, layout);
539
+ drawFarTrees(buffer, width, state, environment, layout);
406
540
  drawForest(buffer, width, viewportTrees, state, environment, tick, layout);
541
+ drawUndergrowth(buffer, width, state, environment, layout, sessionMods.density);
407
542
  drawGround(buffer, width, environment, layout);
408
543
  drawFog(buffer, width, layout, environment);
409
544
 
@@ -422,6 +557,7 @@ export function renderTerminalFrame(state, termWidth = 80, tick = 0, options = {
422
557
  const environment = getEnvironmentSnapshot(
423
558
  options.now instanceof Date ? options.now : new Date(),
424
559
  state.current_streak ?? 0,
560
+ state.biome || null,
425
561
  );
426
562
  return buildStatsLine(state, environment, Math.max(20, termWidth), options.color !== false);
427
563
  }