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/package.json +1 -1
- package/src/commands/watch.js +39 -0
- package/src/core/biomeDetector.js +126 -0
- package/src/core/biomeMigration.js +22 -0
- package/src/core/biomes/aiml.js +36 -0
- package/src/core/biomes/backend.js +35 -0
- package/src/core/biomes/docs.js +35 -0
- package/src/core/biomes/frontend.js +35 -0
- package/src/core/biomes/general.js +34 -0
- package/src/core/biomes/index.js +54 -0
- package/src/core/biomes/infra.js +35 -0
- package/src/core/environment.js +14 -2
- package/src/core/progression.js +32 -10
- package/src/core/sessionDetector.js +92 -0
- package/src/core/sprites.js +1063 -83
- package/src/core/state.js +6 -0
- package/src/renderers/terminal.js +146 -10
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 =
|
|
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
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
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" ?
|
|
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
|
|
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
|
|
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
|
}
|