honeytree 1.1.1 → 1.1.3
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/badge.js +8 -1
- package/src/commands/watch.js +12 -6
- package/src/core/animation.js +21 -13
- package/src/core/state.js +3 -1
- package/src/markdown.js +2 -1
- package/src/renderers/terminal.js +214 -74
package/bin/honeydew.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
3
5
|
const command = process.argv[2];
|
|
6
|
+
const invokedAs = path.basename(process.argv[1] || "honeytree");
|
|
7
|
+
|
|
8
|
+
function printUsage() {
|
|
9
|
+
console.error("Usage:");
|
|
10
|
+
console.error(" honeytree show your forest");
|
|
11
|
+
console.error(" honeytree init create or migrate your forest");
|
|
12
|
+
console.error(" honeytree watch open the terminal forest");
|
|
13
|
+
console.error(" honeytree stats show a non-TUI summary");
|
|
14
|
+
console.error(" honeytree export write honeytree-badge.svg and FOREST.md");
|
|
15
|
+
}
|
|
4
16
|
|
|
5
17
|
if (command === "init") {
|
|
6
18
|
const { init } = await import("../src/commands/init.js");
|
|
@@ -11,11 +23,17 @@ if (command === "init") {
|
|
|
11
23
|
} else if (command === "watch" || !command) {
|
|
12
24
|
const { watch } = await import("../src/commands/watch.js");
|
|
13
25
|
await watch();
|
|
26
|
+
} else if (command === "stats") {
|
|
27
|
+
const { stats } = await import("../src/commands/stats.js");
|
|
28
|
+
await stats();
|
|
14
29
|
} else if (command === "plant") {
|
|
15
30
|
const { plant } = await import("../src/commands/plant.js");
|
|
16
31
|
await plant();
|
|
17
32
|
} else {
|
|
18
33
|
console.error(`Unknown command: ${command}`);
|
|
19
|
-
|
|
34
|
+
if (invokedAs === "honeydew") {
|
|
35
|
+
console.error("Run `honeydew watch` or `honeytree init`.");
|
|
36
|
+
}
|
|
37
|
+
printUsage();
|
|
20
38
|
process.exit(1);
|
|
21
39
|
}
|
package/package.json
CHANGED
package/src/badge.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { readState } from "./core/state.js";
|
|
5
5
|
import { buildTerminalScene } from "./renderers/terminal.js";
|
|
6
6
|
|
|
7
|
-
const CELL =
|
|
7
|
+
const CELL = 6;
|
|
8
8
|
const BG = "#0d1117";
|
|
9
9
|
const TEXT_PAD = 18;
|
|
10
10
|
|
|
@@ -63,4 +63,11 @@ export async function badge() {
|
|
|
63
63
|
writeBadgeSVG(state, outPath);
|
|
64
64
|
|
|
65
65
|
console.log(`Badge written to ${outPath}`);
|
|
66
|
+
console.log("");
|
|
67
|
+
console.log("Add this to your README to show your Honeytree forest.");
|
|
68
|
+
console.log("The badge links to https://github.com/Varun2009178/honeytree");
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log(
|
|
71
|
+
`[](https://github.com/Varun2009178/honeytree)`,
|
|
72
|
+
);
|
|
66
73
|
}
|
package/src/commands/watch.js
CHANGED
|
@@ -8,8 +8,9 @@ import { createActivityTracker } from "../tracker/activity.js";
|
|
|
8
8
|
import { startFileTracker } from "../tracker/files.js";
|
|
9
9
|
import { startGitTracker } from "../tracker/git.js";
|
|
10
10
|
|
|
11
|
-
const FRAME_MS =
|
|
12
|
-
const
|
|
11
|
+
const FRAME_MS = 150;
|
|
12
|
+
const STATE_SYNC_MS = 1_000;
|
|
13
|
+
const GIT_POLL_MS = 30_000;
|
|
13
14
|
const h = React.createElement;
|
|
14
15
|
|
|
15
16
|
function buildCommandHint(width) {
|
|
@@ -27,6 +28,7 @@ function ForestWatchApp() {
|
|
|
27
28
|
const [tick, setTick] = useState(0);
|
|
28
29
|
const [state, setState] = useState(() => ensureState());
|
|
29
30
|
const [width, setWidth] = useState(stdout.columns || 80);
|
|
31
|
+
const [height, setHeight] = useState(stdout.rows || 24);
|
|
30
32
|
|
|
31
33
|
useInput((input, key) => {
|
|
32
34
|
if (input === "q" || key.escape || key.ctrl && input === "c") {
|
|
@@ -35,7 +37,10 @@ function ForestWatchApp() {
|
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
useEffect(() => {
|
|
38
|
-
const onResize = () =>
|
|
40
|
+
const onResize = () => {
|
|
41
|
+
setWidth(stdout.columns || 80);
|
|
42
|
+
setHeight(stdout.rows || 24);
|
|
43
|
+
};
|
|
39
44
|
stdout.on("resize", onResize);
|
|
40
45
|
return () => stdout.off("resize", onResize);
|
|
41
46
|
}, [stdout]);
|
|
@@ -53,7 +58,7 @@ function ForestWatchApp() {
|
|
|
53
58
|
if (latest) {
|
|
54
59
|
setState(latest);
|
|
55
60
|
}
|
|
56
|
-
},
|
|
61
|
+
}, STATE_SYNC_MS);
|
|
57
62
|
return () => clearInterval(syncTimer);
|
|
58
63
|
}, []);
|
|
59
64
|
|
|
@@ -78,6 +83,7 @@ function ForestWatchApp() {
|
|
|
78
83
|
const gitTracker = startGitTracker({
|
|
79
84
|
cwd: process.cwd(),
|
|
80
85
|
lastCommitHash: state.last_commit_hash,
|
|
86
|
+
pollMs: GIT_POLL_MS,
|
|
81
87
|
async onCommit(commitHash) {
|
|
82
88
|
const nextState = updateState((draft) => applyCommit(draft, { commitHash }));
|
|
83
89
|
setState(nextState);
|
|
@@ -94,8 +100,8 @@ function ForestWatchApp() {
|
|
|
94
100
|
}, []);
|
|
95
101
|
|
|
96
102
|
const frame = useMemo(
|
|
97
|
-
() => renderTerminalFrame(state, width, tick, { now: new Date() }),
|
|
98
|
-
[state, tick, width],
|
|
103
|
+
() => renderTerminalFrame(state, width, tick, { now: new Date(), termHeight: height }),
|
|
104
|
+
[state, tick, width, height],
|
|
99
105
|
);
|
|
100
106
|
const commandHint = useMemo(() => buildCommandHint(width), [width]);
|
|
101
107
|
|
package/src/core/animation.js
CHANGED
|
@@ -22,34 +22,40 @@ function hash(seed) {
|
|
|
22
22
|
return ((value >>> 16) ^ value) >>> 0;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function getClouds(width, tick = 0, skyPhase = "day") {
|
|
25
|
+
export function getClouds(width, tick = 0, skyPhase = "day", skyRows = 4) {
|
|
26
26
|
const count = skyPhase === "night" ? 2 : 3;
|
|
27
27
|
return Array.from({ length: count }, (_, index) => {
|
|
28
28
|
const length = 7 + (index % 3) * 3;
|
|
29
29
|
const offset = ((tick * (0.35 + index * 0.08)) + index * 17) % (width + length * 2);
|
|
30
|
+
const maxY = Math.max(2, skyRows - 1);
|
|
30
31
|
return {
|
|
31
32
|
x: Math.round(offset) - length,
|
|
32
|
-
y: index % 2 === 0 ?
|
|
33
|
+
y: index % 2 === 0 ? Math.round(maxY * 0.3) : Math.round(maxY * 0.6),
|
|
33
34
|
length,
|
|
34
35
|
};
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export function getStars(width, tick = 0) {
|
|
39
|
+
export function getStars(width, tick = 0, skyRows = 4) {
|
|
39
40
|
const stars = [];
|
|
40
41
|
for (let column = 0; column < width; column += 1) {
|
|
41
42
|
const seeded = hash(column * 97);
|
|
42
43
|
if (seeded % 7 !== 0) continue;
|
|
43
44
|
stars.push({
|
|
44
45
|
x: column,
|
|
45
|
-
y: seeded %
|
|
46
|
+
y: seeded % skyRows,
|
|
46
47
|
twinkle: ((tick + seeded) % 20) / 20,
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
return stars;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
export function getAnimatedAnimals(state, width, tick = 0) {
|
|
53
|
+
export function getAnimatedAnimals(state, width, tick = 0, layout = null) {
|
|
54
|
+
const artRows = layout ? layout.artRows : 13;
|
|
55
|
+
const skyRows = layout ? layout.skyRows : 4;
|
|
56
|
+
const groundY = artRows - 2; // just above ground
|
|
57
|
+
const flyY = skyRows + 2; // flying creatures in upper forest
|
|
58
|
+
|
|
53
59
|
return state.animals.map((animal, index) => {
|
|
54
60
|
const speed = SPEEDS[animal.type] ?? 0.08;
|
|
55
61
|
const direction = animal.direction === "left" ? -1 : 1;
|
|
@@ -60,10 +66,10 @@ export function getAnimatedAnimals(state, width, tick = 0) {
|
|
|
60
66
|
x,
|
|
61
67
|
y:
|
|
62
68
|
animal.type === "butterfly"
|
|
63
|
-
?
|
|
69
|
+
? flyY + Math.round(Math.sin((tick + index * 5) / 4))
|
|
64
70
|
: animal.type === "owl"
|
|
65
|
-
?
|
|
66
|
-
:
|
|
71
|
+
? flyY - 1 + Math.round(Math.sin((tick + index * 9) / 8))
|
|
72
|
+
: groundY,
|
|
67
73
|
bob:
|
|
68
74
|
animal.type === "rabbit"
|
|
69
75
|
? Math.abs(Math.sin((tick + index * 6) / 3))
|
|
@@ -105,12 +111,14 @@ export function getWeatherParticles(width, height, tick = 0, weather = "clear",
|
|
|
105
111
|
}));
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
export function getDynamicScene(state, width, tick, environment) {
|
|
114
|
+
export function getDynamicScene(state, width, tick, environment, layout = null) {
|
|
115
|
+
const skyRows = layout ? layout.skyRows : 4;
|
|
116
|
+
const artRows = layout ? layout.artRows : 13;
|
|
109
117
|
return {
|
|
110
|
-
clouds: getClouds(width, tick, environment.sky.name),
|
|
111
|
-
stars: environment.sky.name === "night" ? getStars(width, tick) : [],
|
|
112
|
-
animals: getAnimatedAnimals(state, width, tick),
|
|
113
|
-
particles: getWeatherParticles(width,
|
|
118
|
+
clouds: getClouds(width, tick, environment.sky.name, skyRows),
|
|
119
|
+
stars: environment.sky.name === "night" ? getStars(width, tick, skyRows) : [],
|
|
120
|
+
animals: getAnimatedAnimals(state, width, tick, layout),
|
|
121
|
+
particles: getWeatherParticles(width, artRows, tick, environment.weather.name, environment.season.name),
|
|
114
122
|
};
|
|
115
123
|
}
|
|
116
124
|
|
package/src/core/state.js
CHANGED
|
@@ -13,7 +13,9 @@ function todayString(now = new Date()) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function resolveStateDir() {
|
|
16
|
-
|
|
16
|
+
if (process.env.HONEYTREE_DIR) return process.env.HONEYTREE_DIR;
|
|
17
|
+
// Per-project: state lives in the current project directory
|
|
18
|
+
return path.join(process.cwd(), STATE_DIR_NAME);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
function resolveLegacyDir() {
|
package/src/markdown.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getEnvironmentSnapshot } from "./core/environment.js";
|
|
|
5
5
|
import { readState } from "./core/state.js";
|
|
6
6
|
import { renderTerminalFrame } from "./renderers/terminal.js";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function buildMarkdown(state) {
|
|
9
9
|
const environment = getEnvironmentSnapshot(new Date(), state.current_streak);
|
|
10
10
|
const art = renderTerminalFrame(state, 80, 0, { color: false });
|
|
11
11
|
|
|
@@ -43,4 +43,5 @@ export async function generateForestMd() {
|
|
|
43
43
|
fs.writeFileSync(outPath, md);
|
|
44
44
|
|
|
45
45
|
console.log(`Written to ${outPath}`);
|
|
46
|
+
console.log("Tip: run `honeytree badge` to generate the badge SVG too.");
|
|
46
47
|
}
|
|
@@ -10,16 +10,78 @@ import {
|
|
|
10
10
|
materializeSprite,
|
|
11
11
|
} from "../core/sprites.js";
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
const FOREST_ROWS = 8;
|
|
15
|
-
const GROUND_ROWS = 1;
|
|
16
|
-
const ART_ROWS = SKY_ROWS + FOREST_ROWS + GROUND_ROWS;
|
|
17
|
-
const STATS_ROWS = 1;
|
|
13
|
+
export const SCENE_HEIGHT = 14;
|
|
18
14
|
|
|
19
|
-
export
|
|
15
|
+
export function computeLayout(termHeight) {
|
|
16
|
+
if (!termHeight || termHeight < 16) {
|
|
17
|
+
return { skyRows: 4, forestRows: 8, groundRows: 1, artRows: 13 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const available = termHeight - 2;
|
|
21
|
+
const skyRows = Math.max(4, Math.floor(available * 0.4));
|
|
22
|
+
const forestRows = Math.max(6, Math.floor(available * 0.45));
|
|
23
|
+
const groundRows = Math.max(2, Math.floor(available * 0.1));
|
|
24
|
+
|
|
25
|
+
const artRows = skyRows + forestRows + groundRows;
|
|
26
|
+
return { skyRows, forestRows, groundRows, artRows };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function mulberry32(seed) {
|
|
30
|
+
let s = seed | 0;
|
|
31
|
+
return function random() {
|
|
32
|
+
s = (s + 0x6d2b79f5) | 0;
|
|
33
|
+
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
34
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
35
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function shuffleSeeded(items, rng) {
|
|
40
|
+
const result = [...items];
|
|
41
|
+
for (let i = result.length - 1; i > 0; i -= 1) {
|
|
42
|
+
const j = Math.floor(rng() * (i + 1));
|
|
43
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
20
47
|
|
|
21
|
-
function
|
|
22
|
-
|
|
48
|
+
export function selectViewportTrees(trees, width, seed = 0) {
|
|
49
|
+
const maxVisible = 12;
|
|
50
|
+
if (trees.length === 0) return [];
|
|
51
|
+
|
|
52
|
+
const rng = mulberry32(seed);
|
|
53
|
+
const selected = shuffleSeeded(trees, rng).slice(0, Math.min(maxVisible, trees.length));
|
|
54
|
+
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
|
+
}));
|
|
60
|
+
|
|
61
|
+
for (const rowName of rowNames) {
|
|
62
|
+
const rowTrees = assigned.filter((tree) => tree.row === rowName);
|
|
63
|
+
const count = rowTrees.length;
|
|
64
|
+
if (count === 0) continue;
|
|
65
|
+
|
|
66
|
+
const margin = rowName === "front" ? 10 : rowName === "mid" ? 8 : 6;
|
|
67
|
+
const clampedMargin = Math.min(margin, Math.max(4, Math.floor(width / 4)));
|
|
68
|
+
const minX = Math.min(clampedMargin, Math.max(0, width - 1));
|
|
69
|
+
const maxX = Math.max(minX, width - clampedMargin);
|
|
70
|
+
const usable = Math.max(1, maxX - minX);
|
|
71
|
+
const segmentWidth = usable / count;
|
|
72
|
+
|
|
73
|
+
for (let index = 0; index < count; index += 1) {
|
|
74
|
+
const center = minX + segmentWidth * (index + 0.5);
|
|
75
|
+
const jitter = (rng() - 0.5) * segmentWidth * 0.4;
|
|
76
|
+
rowTrees[index].screenX = Math.round(Math.max(minX, Math.min(maxX, center + jitter)));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return assigned;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createBuffer(width, artRows) {
|
|
84
|
+
return Array.from({ length: artRows }, () =>
|
|
23
85
|
Array.from({ length: width }, () => ({ char: " ", color: null, bg: null })),
|
|
24
86
|
);
|
|
25
87
|
}
|
|
@@ -36,12 +98,14 @@ function paintBg(buffer, x, y, bg) {
|
|
|
36
98
|
buffer[y][x].bg = bg;
|
|
37
99
|
}
|
|
38
100
|
|
|
39
|
-
function compositeSprite(buffer, sprite, centerX, baseY, palette) {
|
|
101
|
+
function compositeSprite(buffer, sprite, centerX, baseY, palette, options = {}) {
|
|
40
102
|
const cells = materializeSprite(sprite, palette);
|
|
41
103
|
const offsetX = Math.round(centerX) - Math.floor(cells.width / 2);
|
|
104
|
+
const minY = options.minY ?? 0;
|
|
42
105
|
for (let rowIndex = 0; rowIndex < cells.rows.length; rowIndex += 1) {
|
|
43
106
|
const row = cells.rows[rowIndex];
|
|
44
107
|
const y = baseY - rowIndex;
|
|
108
|
+
if (y < minY) continue;
|
|
45
109
|
for (let columnIndex = 0; columnIndex < row.length; columnIndex += 1) {
|
|
46
110
|
const [char, color] = row[columnIndex];
|
|
47
111
|
if (!color) continue;
|
|
@@ -59,11 +123,12 @@ function colorize(text, color, bg, enabled) {
|
|
|
59
123
|
return fn(text);
|
|
60
124
|
}
|
|
61
125
|
|
|
62
|
-
function drawSkyBackground(buffer, width, environment) {
|
|
126
|
+
function drawSkyBackground(buffer, width, environment, layout) {
|
|
127
|
+
const { skyRows, forestRows, artRows } = layout;
|
|
63
128
|
const sky = environment.sky;
|
|
64
129
|
// Sky rows: vertical gradient from top → mid → bottom
|
|
65
|
-
for (let y = 0; y <
|
|
66
|
-
const t = y / Math.max(1,
|
|
130
|
+
for (let y = 0; y < skyRows; y++) {
|
|
131
|
+
const t = y / Math.max(1, skyRows - 1);
|
|
67
132
|
let color;
|
|
68
133
|
if (t < 0.5) {
|
|
69
134
|
color = lerpColor(sky.top, sky.mid, t * 2);
|
|
@@ -75,28 +140,32 @@ function drawSkyBackground(buffer, width, environment) {
|
|
|
75
140
|
}
|
|
76
141
|
}
|
|
77
142
|
// Forest rows: blend from sky.bottom toward ground
|
|
78
|
-
for (let y =
|
|
79
|
-
const t = (y -
|
|
143
|
+
for (let y = skyRows; y < skyRows + forestRows; y++) {
|
|
144
|
+
const t = (y - skyRows) / Math.max(1, forestRows - 1);
|
|
80
145
|
const color = lerpColor(sky.bottom, environment.palette.groundDark, t * 0.7);
|
|
81
146
|
for (let x = 0; x < width; x++) {
|
|
82
147
|
paintBg(buffer, x, y, color);
|
|
83
148
|
}
|
|
84
149
|
}
|
|
85
|
-
// Ground
|
|
86
|
-
for (let
|
|
87
|
-
|
|
150
|
+
// Ground rows: solid ground color
|
|
151
|
+
for (let y = skyRows + forestRows; y < artRows; y++) {
|
|
152
|
+
for (let x = 0; x < width; x++) {
|
|
153
|
+
paintBg(buffer, x, y, environment.palette.groundDark);
|
|
154
|
+
}
|
|
88
155
|
}
|
|
89
156
|
}
|
|
90
157
|
|
|
91
|
-
function drawSky(buffer, width, environment, tick) {
|
|
158
|
+
function drawSky(buffer, width, environment, tick, layout) {
|
|
159
|
+
const { skyRows } = layout;
|
|
160
|
+
|
|
92
161
|
if (environment.sky.name === "night") {
|
|
93
|
-
const stars = getDynamicScene({ animals: [] }, width, tick, environment).stars;
|
|
162
|
+
const stars = getDynamicScene({ animals: [] }, width, tick, environment, layout).stars;
|
|
94
163
|
for (const star of stars) {
|
|
95
164
|
paint(buffer, star.x, star.y, star.twinkle > 0.5 ? "✦" : "·", environment.sky.haze);
|
|
96
165
|
}
|
|
97
166
|
}
|
|
98
167
|
|
|
99
|
-
const clouds = getDynamicScene({ animals: [] }, width, tick, environment).clouds;
|
|
168
|
+
const clouds = getDynamicScene({ animals: [] }, width, tick, environment, layout).clouds;
|
|
100
169
|
for (const cloud of clouds) {
|
|
101
170
|
const glyph = environment.sky.name === "night" ? "░" : "▓";
|
|
102
171
|
for (let offset = 0; offset < cloud.length; offset += 1) {
|
|
@@ -105,7 +174,9 @@ function drawSky(buffer, width, environment, tick) {
|
|
|
105
174
|
}
|
|
106
175
|
|
|
107
176
|
const celestialX = Math.round(((width - 6) * environment.sky.progress) + 3);
|
|
108
|
-
const celestialY = environment.sky.name === "night"
|
|
177
|
+
const celestialY = environment.sky.name === "night"
|
|
178
|
+
? Math.max(1, Math.round(skyRows * 0.2))
|
|
179
|
+
: Math.max(0, Math.round(skyRows * 0.4) - Math.round(environment.sky.progress * 2));
|
|
109
180
|
paint(
|
|
110
181
|
buffer,
|
|
111
182
|
celestialX,
|
|
@@ -115,8 +186,8 @@ function drawSky(buffer, width, environment, tick) {
|
|
|
115
186
|
);
|
|
116
187
|
}
|
|
117
188
|
|
|
118
|
-
function drawWeather(buffer, width, environment, tick) {
|
|
119
|
-
const scene = getDynamicScene({ animals: [] }, width, tick, environment);
|
|
189
|
+
function drawWeather(buffer, width, environment, tick, layout) {
|
|
190
|
+
const scene = getDynamicScene({ animals: [] }, width, tick, environment, layout);
|
|
120
191
|
if (environment.weather.name === "rain") {
|
|
121
192
|
for (const drop of scene.particles) {
|
|
122
193
|
if (drop.kind !== "rain") continue;
|
|
@@ -133,16 +204,17 @@ function drawWeather(buffer, width, environment, tick) {
|
|
|
133
204
|
|
|
134
205
|
if (environment.weather.name === "rainbow") {
|
|
135
206
|
const midpoint = Math.floor(width / 2);
|
|
207
|
+
const rainbowY = Math.max(1, Math.round(layout.skyRows * 0.3));
|
|
136
208
|
const colors = ["#f94144", "#f8961e", "#f9c74f", "#90be6d", "#577590"];
|
|
137
209
|
colors.forEach((color, index) => {
|
|
138
|
-
paint(buffer, midpoint - 2 + index,
|
|
139
|
-
paint(buffer, midpoint + 1 - index,
|
|
210
|
+
paint(buffer, midpoint - 2 + index, rainbowY + (index % 2), "◜", color);
|
|
211
|
+
paint(buffer, midpoint + 1 - index, rainbowY + (index % 2), "◝", color);
|
|
140
212
|
});
|
|
141
213
|
}
|
|
142
214
|
}
|
|
143
215
|
|
|
144
|
-
function drawSeasonParticles(buffer, width, environment, tick) {
|
|
145
|
-
const scene = getDynamicScene({ animals: [] }, width, tick, environment);
|
|
216
|
+
function drawSeasonParticles(buffer, width, environment, tick, layout) {
|
|
217
|
+
const scene = getDynamicScene({ animals: [] }, width, tick, environment, layout);
|
|
146
218
|
for (const particle of scene.particles) {
|
|
147
219
|
if (particle.kind === "petal") {
|
|
148
220
|
paint(buffer, Math.round(particle.x), Math.round(particle.y), "•", "#ffcad4");
|
|
@@ -163,58 +235,60 @@ function scaleX(virtualX, width) {
|
|
|
163
235
|
return Math.round((virtualX / VIRTUAL_WIDTH) * width);
|
|
164
236
|
}
|
|
165
237
|
|
|
166
|
-
function
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
result
|
|
174
|
-
lastScreenX = screenX;
|
|
238
|
+
function desaturatePalette(palette, skyBottom, amount) {
|
|
239
|
+
const result = {};
|
|
240
|
+
for (const key of Object.keys(palette)) {
|
|
241
|
+
const value = palette[key];
|
|
242
|
+
if (typeof value === "string" && value.startsWith("#")) {
|
|
243
|
+
result[key] = lerpColor(value, skyBottom, amount);
|
|
244
|
+
} else {
|
|
245
|
+
result[key] = value;
|
|
175
246
|
}
|
|
176
247
|
}
|
|
177
248
|
return result;
|
|
178
249
|
}
|
|
179
250
|
|
|
180
|
-
function
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
if (isSunsetSilhouette) {
|
|
185
|
-
for (const key of Object.keys(environment.palette)) {
|
|
186
|
-
silhouettePalette[key] = "#1a1a2e";
|
|
187
|
-
}
|
|
251
|
+
function silhouettePalette(palette) {
|
|
252
|
+
const result = {};
|
|
253
|
+
for (const key of Object.keys(palette)) {
|
|
254
|
+
result[key] = key.toLowerCase().includes("light") ? "#24243f" : "#111827";
|
|
188
255
|
}
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
189
258
|
|
|
259
|
+
function drawForest(buffer, width, viewportTrees, state, environment, tick, layout) {
|
|
260
|
+
const { skyRows, forestRows, artRows, groundRows } = layout;
|
|
261
|
+
const treeBaseY = skyRows + forestRows - 1;
|
|
190
262
|
const crystalPalette = getCrystalPalette(tick);
|
|
263
|
+
const isSunsetSilhouette = environment.sky.name === "sunset";
|
|
191
264
|
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const spaced = spaceTrees(rowTrees, width, minGap[targetRow]);
|
|
198
|
-
const rowY = targetRow === "back" ? treeBaseY - 2 : targetRow === "mid" ? treeBaseY - 1 : treeBaseY;
|
|
265
|
+
const rowOffset = { back: 4, mid: 2, front: 0 };
|
|
266
|
+
const rowDesaturation = { back: 0.3, mid: 0.1, front: 0 };
|
|
267
|
+
for (const targetRow of ["back", "mid", "front"]) {
|
|
268
|
+
const rowTrees = viewportTrees.filter((tree) => tree.row === targetRow);
|
|
269
|
+
const rowY = treeBaseY - rowOffset[targetRow];
|
|
199
270
|
|
|
200
|
-
for (const tree of
|
|
271
|
+
for (const tree of rowTrees) {
|
|
201
272
|
let palette = environment.palette;
|
|
202
|
-
if (tree.species === "crystal_tree") {
|
|
203
|
-
palette = { ...environment.palette, ...crystalPalette };
|
|
204
|
-
}
|
|
205
273
|
if (isSunsetSilhouette) {
|
|
206
|
-
palette = silhouettePalette;
|
|
274
|
+
palette = silhouettePalette(palette);
|
|
275
|
+
} else if (rowDesaturation[targetRow] > 0) {
|
|
276
|
+
palette = desaturatePalette(palette, environment.sky.bottom, rowDesaturation[targetRow]);
|
|
277
|
+
}
|
|
278
|
+
if (tree.species === "crystal_tree") {
|
|
279
|
+
palette = { ...palette, ...crystalPalette };
|
|
207
280
|
}
|
|
208
|
-
compositeSprite(buffer, getTreeSprite(tree.species),
|
|
281
|
+
compositeSprite(buffer, getTreeSprite(tree.species), tree.screenX, rowY, palette, { minY: skyRows });
|
|
209
282
|
}
|
|
210
283
|
}
|
|
211
284
|
|
|
285
|
+
const groundY = artRows - groundRows;
|
|
212
286
|
for (const element of state.ground_elements) {
|
|
213
|
-
compositeSprite(buffer, getGroundElementSprite(element.type), scaleX(element.x_position, width),
|
|
287
|
+
compositeSprite(buffer, getGroundElementSprite(element.type), scaleX(element.x_position, width), groundY, environment.palette);
|
|
214
288
|
}
|
|
215
289
|
|
|
216
|
-
const scene = getDynamicScene(state, width, tick, environment);
|
|
217
|
-
const maxAnimals = Math.max(2, Math.min(
|
|
290
|
+
const scene = getDynamicScene(state, width, tick, environment, layout);
|
|
291
|
+
const maxAnimals = Math.max(2, Math.min(3, Math.floor(width / 40)));
|
|
218
292
|
const visibleAnimals = scene.animals.slice(0, maxAnimals);
|
|
219
293
|
for (const animal of visibleAnimals) {
|
|
220
294
|
if (animal.type === "owl" && environment.sky.name !== "night") {
|
|
@@ -224,20 +298,80 @@ function drawForest(buffer, width, state, environment, tick) {
|
|
|
224
298
|
buffer,
|
|
225
299
|
getAnimalSprite(animal.type),
|
|
226
300
|
animal.x,
|
|
227
|
-
Math.min(
|
|
301
|
+
Math.min(artRows - groundRows - 1, Math.round(animal.y)),
|
|
228
302
|
environment.palette,
|
|
229
303
|
);
|
|
230
304
|
}
|
|
231
305
|
}
|
|
232
306
|
|
|
233
|
-
function drawGround(buffer, width, environment) {
|
|
234
|
-
|
|
235
|
-
|
|
307
|
+
function drawGround(buffer, width, environment, layout) {
|
|
308
|
+
const { skyRows, forestRows, groundRows } = layout;
|
|
309
|
+
const groundStart = skyRows + forestRows;
|
|
310
|
+
|
|
311
|
+
for (let row = 0; row < groundRows; row += 1) {
|
|
312
|
+
const y = groundStart + row;
|
|
313
|
+
if (row === 0) {
|
|
314
|
+
for (let x = 0; x < width; x += 1) {
|
|
315
|
+
const char = x % 5 === 0 ? "▓" : "▄";
|
|
316
|
+
const color = x % 3 === 0 ? environment.palette.grassLight : environment.palette.grass;
|
|
317
|
+
paint(buffer, x, y, char, color);
|
|
318
|
+
}
|
|
319
|
+
} else if (row === 1) {
|
|
320
|
+
for (let x = 0; x < width; x += 1) {
|
|
321
|
+
const color = x % 7 === 0 ? environment.palette.groundLight : environment.palette.ground;
|
|
322
|
+
paint(buffer, x, y, "▄", color);
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
for (let x = 0; x < width; x += 1) {
|
|
326
|
+
paint(buffer, x, y, "▄", environment.palette.groundDark);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function drawFog(buffer, width, layout, environment) {
|
|
333
|
+
const { skyRows, forestRows } = layout;
|
|
334
|
+
const fogColor = "#5a7a5a";
|
|
335
|
+
const fogColumns = [
|
|
336
|
+
{ offset: 0, char: "▒", opacity: 0.6 },
|
|
337
|
+
{ offset: 1, char: "▒", opacity: 0.45 },
|
|
338
|
+
{ offset: 2, char: "░", opacity: 0.3 },
|
|
339
|
+
{ offset: 3, char: "░", opacity: 0.15 },
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
for (let y = skyRows; y < skyRows + forestRows; y += 1) {
|
|
343
|
+
for (const fog of fogColumns) {
|
|
344
|
+
const x = fog.offset;
|
|
345
|
+
if (x >= width) continue;
|
|
346
|
+
const cell = buffer[y][x];
|
|
347
|
+
if (cell.bg) {
|
|
348
|
+
paintBg(buffer, x, y, lerpColor(cell.bg, fogColor, fog.opacity));
|
|
349
|
+
}
|
|
350
|
+
if (cell.color) {
|
|
351
|
+
paint(buffer, x, y, cell.char, lerpColor(cell.color, fogColor, fog.opacity));
|
|
352
|
+
} else {
|
|
353
|
+
paint(buffer, x, y, fog.char, lerpColor(environment.sky.bottom, fogColor, fog.opacity));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const fog of fogColumns) {
|
|
358
|
+
const x = width - 1 - fog.offset;
|
|
359
|
+
if (x < 0) continue;
|
|
360
|
+
const cell = buffer[y][x];
|
|
361
|
+
if (cell.bg) {
|
|
362
|
+
paintBg(buffer, x, y, lerpColor(cell.bg, fogColor, fog.opacity));
|
|
363
|
+
}
|
|
364
|
+
if (cell.color) {
|
|
365
|
+
paint(buffer, x, y, cell.char, lerpColor(cell.color, fogColor, fog.opacity));
|
|
366
|
+
} else {
|
|
367
|
+
paint(buffer, x, y, fog.char, lerpColor(environment.sky.bottom, fogColor, fog.opacity));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
236
370
|
}
|
|
237
371
|
}
|
|
238
372
|
|
|
239
373
|
function buildStatsLine(state, environment, width, color) {
|
|
240
|
-
const trees = `${state.trees.length} tree${state.trees.length === 1 ? "" : "s"}`;
|
|
374
|
+
const trees = `${state.trees.length} tree${state.trees.length === 1 ? "" : "s"} in your forest`;
|
|
241
375
|
const animals = `${state.animals.length} animal${state.animals.length === 1 ? "" : "s"}`;
|
|
242
376
|
const streak = `${state.current_streak} day streak`;
|
|
243
377
|
const weather = `${environment.season.icon} ${environment.season.label} ${environment.weather.label}`;
|
|
@@ -259,18 +393,25 @@ export function buildTerminalScene(state, termWidth = 80, tick = 0, options = {}
|
|
|
259
393
|
const width = Math.max(20, termWidth);
|
|
260
394
|
const date = options.now instanceof Date ? options.now : new Date();
|
|
261
395
|
const environment = getEnvironmentSnapshot(date, state.current_streak ?? 0);
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
396
|
+
const layout = computeLayout(options.termHeight || null);
|
|
397
|
+
const buffer = createBuffer(width, layout.artRows);
|
|
398
|
+
const startOfYear = new Date(date.getFullYear(), 0, 0);
|
|
399
|
+
const dayOfYear = Math.floor((date - startOfYear) / 86400000);
|
|
400
|
+
const viewportTrees = selectViewportTrees(state.trees, width, dayOfYear);
|
|
401
|
+
|
|
402
|
+
drawSkyBackground(buffer, width, environment, layout);
|
|
403
|
+
drawSky(buffer, width, environment, tick, layout);
|
|
404
|
+
drawWeather(buffer, width, environment, tick, layout);
|
|
405
|
+
drawSeasonParticles(buffer, width, environment, tick, layout);
|
|
406
|
+
drawForest(buffer, width, viewportTrees, state, environment, tick, layout);
|
|
407
|
+
drawGround(buffer, width, environment, layout);
|
|
408
|
+
drawFog(buffer, width, layout, environment);
|
|
270
409
|
|
|
271
410
|
return {
|
|
272
411
|
width,
|
|
273
412
|
environment,
|
|
413
|
+
layout,
|
|
414
|
+
viewportTrees,
|
|
274
415
|
buffer,
|
|
275
416
|
statsLine: buildStatsLine(state, environment, width, options.color !== false),
|
|
276
417
|
};
|
|
@@ -294,4 +435,3 @@ export function renderTerminalFrame(state, termWidth = 80, tick = 0, options = {
|
|
|
294
435
|
lines.push(scene.statsLine);
|
|
295
436
|
return lines.join("\n");
|
|
296
437
|
}
|
|
297
|
-
|