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/bin/honeydew.js +19 -1
- package/package.json +1 -1
- package/src/commands/watch.js +44 -3
- 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 +159 -11
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);
|
|
@@ -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 (
|
|
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
|
|
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
|
|
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
|
}
|