honeytree 1.1.0 → 1.1.2
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/badge.js +8 -1
- package/src/commands/watch.js +7 -3
- 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 +252 -69
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
|
@@ -27,6 +27,7 @@ function ForestWatchApp() {
|
|
|
27
27
|
const [tick, setTick] = useState(0);
|
|
28
28
|
const [state, setState] = useState(() => ensureState());
|
|
29
29
|
const [width, setWidth] = useState(stdout.columns || 80);
|
|
30
|
+
const [height, setHeight] = useState(stdout.rows || 24);
|
|
30
31
|
|
|
31
32
|
useInput((input, key) => {
|
|
32
33
|
if (input === "q" || key.escape || key.ctrl && input === "c") {
|
|
@@ -35,7 +36,10 @@ function ForestWatchApp() {
|
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
useEffect(() => {
|
|
38
|
-
const onResize = () =>
|
|
39
|
+
const onResize = () => {
|
|
40
|
+
setWidth(stdout.columns || 80);
|
|
41
|
+
setHeight(stdout.rows || 24);
|
|
42
|
+
};
|
|
39
43
|
stdout.on("resize", onResize);
|
|
40
44
|
return () => stdout.off("resize", onResize);
|
|
41
45
|
}, [stdout]);
|
|
@@ -94,8 +98,8 @@ function ForestWatchApp() {
|
|
|
94
98
|
}, []);
|
|
95
99
|
|
|
96
100
|
const frame = useMemo(
|
|
97
|
-
() => renderTerminalFrame(state, width, tick, { now: new Date() }),
|
|
98
|
-
[state, tick, width],
|
|
101
|
+
() => renderTerminalFrame(state, width, tick, { now: new Date(), termHeight: height }),
|
|
102
|
+
[state, tick, width, height],
|
|
99
103
|
);
|
|
100
104
|
const commandHint = useMemo(() => buildCommandHint(width), [width]);
|
|
101
105
|
|
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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
|
|
3
3
|
import { getDynamicScene, getCrystalPalette } from "../core/animation.js";
|
|
4
|
-
import { getEnvironmentSnapshot } from "../core/environment.js";
|
|
4
|
+
import { getEnvironmentSnapshot, lerpColor } from "../core/environment.js";
|
|
5
5
|
import { VIRTUAL_WIDTH } from "../core/progression.js";
|
|
6
6
|
import {
|
|
7
7
|
getAnimalSprite,
|
|
@@ -10,32 +10,102 @@ 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
|
+
}
|
|
47
|
+
|
|
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
|
+
}));
|
|
20
60
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 }, () =>
|
|
85
|
+
Array.from({ length: width }, () => ({ char: " ", color: null, bg: null })),
|
|
24
86
|
);
|
|
25
87
|
}
|
|
26
88
|
|
|
27
89
|
function paint(buffer, x, y, char, color) {
|
|
28
90
|
if (y < 0 || y >= buffer.length) return;
|
|
29
91
|
if (x < 0 || x >= buffer[0].length) return;
|
|
30
|
-
buffer[y][x] = { char, color };
|
|
92
|
+
buffer[y][x] = { char, color, bg: buffer[y][x].bg };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function paintBg(buffer, x, y, bg) {
|
|
96
|
+
if (y < 0 || y >= buffer.length) return;
|
|
97
|
+
if (x < 0 || x >= buffer[0].length) return;
|
|
98
|
+
buffer[y][x].bg = bg;
|
|
31
99
|
}
|
|
32
100
|
|
|
33
|
-
function compositeSprite(buffer, sprite, centerX, baseY, palette) {
|
|
101
|
+
function compositeSprite(buffer, sprite, centerX, baseY, palette, options = {}) {
|
|
34
102
|
const cells = materializeSprite(sprite, palette);
|
|
35
103
|
const offsetX = Math.round(centerX) - Math.floor(cells.width / 2);
|
|
104
|
+
const minY = options.minY ?? 0;
|
|
36
105
|
for (let rowIndex = 0; rowIndex < cells.rows.length; rowIndex += 1) {
|
|
37
106
|
const row = cells.rows[rowIndex];
|
|
38
107
|
const y = baseY - rowIndex;
|
|
108
|
+
if (y < minY) continue;
|
|
39
109
|
for (let columnIndex = 0; columnIndex < row.length; columnIndex += 1) {
|
|
40
110
|
const [char, color] = row[columnIndex];
|
|
41
111
|
if (!color) continue;
|
|
@@ -44,22 +114,58 @@ function compositeSprite(buffer, sprite, centerX, baseY, palette) {
|
|
|
44
114
|
}
|
|
45
115
|
}
|
|
46
116
|
|
|
47
|
-
function colorize(text, color, enabled) {
|
|
48
|
-
if (!enabled
|
|
49
|
-
|
|
117
|
+
function colorize(text, color, bg, enabled) {
|
|
118
|
+
if (!enabled) return text;
|
|
119
|
+
if (!color && !bg) return text;
|
|
120
|
+
let fn = chalk;
|
|
121
|
+
if (bg) fn = fn.bgHex(bg);
|
|
122
|
+
if (color) fn = fn.hex(color);
|
|
123
|
+
return fn(text);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function drawSkyBackground(buffer, width, environment, layout) {
|
|
127
|
+
const { skyRows, forestRows, artRows } = layout;
|
|
128
|
+
const sky = environment.sky;
|
|
129
|
+
// Sky rows: vertical gradient from top → mid → bottom
|
|
130
|
+
for (let y = 0; y < skyRows; y++) {
|
|
131
|
+
const t = y / Math.max(1, skyRows - 1);
|
|
132
|
+
let color;
|
|
133
|
+
if (t < 0.5) {
|
|
134
|
+
color = lerpColor(sky.top, sky.mid, t * 2);
|
|
135
|
+
} else {
|
|
136
|
+
color = lerpColor(sky.mid, sky.bottom, (t - 0.5) * 2);
|
|
137
|
+
}
|
|
138
|
+
for (let x = 0; x < width; x++) {
|
|
139
|
+
paintBg(buffer, x, y, color);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Forest rows: blend from sky.bottom toward ground
|
|
143
|
+
for (let y = skyRows; y < skyRows + forestRows; y++) {
|
|
144
|
+
const t = (y - skyRows) / Math.max(1, forestRows - 1);
|
|
145
|
+
const color = lerpColor(sky.bottom, environment.palette.groundDark, t * 0.7);
|
|
146
|
+
for (let x = 0; x < width; x++) {
|
|
147
|
+
paintBg(buffer, x, y, color);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
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
|
+
}
|
|
50
155
|
}
|
|
51
|
-
return chalk.hex(color)(text);
|
|
52
156
|
}
|
|
53
157
|
|
|
54
|
-
function drawSky(buffer, width, environment, tick) {
|
|
158
|
+
function drawSky(buffer, width, environment, tick, layout) {
|
|
159
|
+
const { skyRows } = layout;
|
|
160
|
+
|
|
55
161
|
if (environment.sky.name === "night") {
|
|
56
|
-
const stars = getDynamicScene({ animals: [] }, width, tick, environment).stars;
|
|
162
|
+
const stars = getDynamicScene({ animals: [] }, width, tick, environment, layout).stars;
|
|
57
163
|
for (const star of stars) {
|
|
58
164
|
paint(buffer, star.x, star.y, star.twinkle > 0.5 ? "✦" : "·", environment.sky.haze);
|
|
59
165
|
}
|
|
60
166
|
}
|
|
61
167
|
|
|
62
|
-
const clouds = getDynamicScene({ animals: [] }, width, tick, environment).clouds;
|
|
168
|
+
const clouds = getDynamicScene({ animals: [] }, width, tick, environment, layout).clouds;
|
|
63
169
|
for (const cloud of clouds) {
|
|
64
170
|
const glyph = environment.sky.name === "night" ? "░" : "▓";
|
|
65
171
|
for (let offset = 0; offset < cloud.length; offset += 1) {
|
|
@@ -68,7 +174,9 @@ function drawSky(buffer, width, environment, tick) {
|
|
|
68
174
|
}
|
|
69
175
|
|
|
70
176
|
const celestialX = Math.round(((width - 6) * environment.sky.progress) + 3);
|
|
71
|
-
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));
|
|
72
180
|
paint(
|
|
73
181
|
buffer,
|
|
74
182
|
celestialX,
|
|
@@ -78,8 +186,8 @@ function drawSky(buffer, width, environment, tick) {
|
|
|
78
186
|
);
|
|
79
187
|
}
|
|
80
188
|
|
|
81
|
-
function drawWeather(buffer, width, environment, tick) {
|
|
82
|
-
const scene = getDynamicScene({ animals: [] }, width, tick, environment);
|
|
189
|
+
function drawWeather(buffer, width, environment, tick, layout) {
|
|
190
|
+
const scene = getDynamicScene({ animals: [] }, width, tick, environment, layout);
|
|
83
191
|
if (environment.weather.name === "rain") {
|
|
84
192
|
for (const drop of scene.particles) {
|
|
85
193
|
if (drop.kind !== "rain") continue;
|
|
@@ -96,16 +204,17 @@ function drawWeather(buffer, width, environment, tick) {
|
|
|
96
204
|
|
|
97
205
|
if (environment.weather.name === "rainbow") {
|
|
98
206
|
const midpoint = Math.floor(width / 2);
|
|
207
|
+
const rainbowY = Math.max(1, Math.round(layout.skyRows * 0.3));
|
|
99
208
|
const colors = ["#f94144", "#f8961e", "#f9c74f", "#90be6d", "#577590"];
|
|
100
209
|
colors.forEach((color, index) => {
|
|
101
|
-
paint(buffer, midpoint - 2 + index,
|
|
102
|
-
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);
|
|
103
212
|
});
|
|
104
213
|
}
|
|
105
214
|
}
|
|
106
215
|
|
|
107
|
-
function drawSeasonParticles(buffer, width, environment, tick) {
|
|
108
|
-
const scene = getDynamicScene({ animals: [] }, width, tick, environment);
|
|
216
|
+
function drawSeasonParticles(buffer, width, environment, tick, layout) {
|
|
217
|
+
const scene = getDynamicScene({ animals: [] }, width, tick, environment, layout);
|
|
109
218
|
for (const particle of scene.particles) {
|
|
110
219
|
if (particle.kind === "petal") {
|
|
111
220
|
paint(buffer, Math.round(particle.x), Math.round(particle.y), "•", "#ffcad4");
|
|
@@ -126,43 +235,51 @@ function scaleX(virtualX, width) {
|
|
|
126
235
|
return Math.round((virtualX / VIRTUAL_WIDTH) * width);
|
|
127
236
|
}
|
|
128
237
|
|
|
129
|
-
function
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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;
|
|
136
246
|
}
|
|
137
247
|
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
138
250
|
|
|
251
|
+
function drawForest(buffer, width, viewportTrees, state, environment, tick, layout) {
|
|
252
|
+
const { skyRows, forestRows, artRows, groundRows } = layout;
|
|
253
|
+
const treeBaseY = skyRows + forestRows - 1;
|
|
139
254
|
const crystalPalette = getCrystalPalette(tick);
|
|
140
255
|
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
256
|
+
const rowOffset = { back: 4, mid: 2, front: 0 };
|
|
257
|
+
const rowDesaturation = { back: 0.3, mid: 0.1, front: 0 };
|
|
258
|
+
for (const targetRow of ["back", "mid", "front"]) {
|
|
259
|
+
const rowTrees = viewportTrees.filter((tree) => tree.row === targetRow);
|
|
260
|
+
const rowY = treeBaseY - rowOffset[targetRow];
|
|
146
261
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
262
|
+
for (const tree of rowTrees) {
|
|
263
|
+
let palette = environment.palette;
|
|
264
|
+
if (rowDesaturation[targetRow] > 0) {
|
|
265
|
+
palette = desaturatePalette(palette, environment.sky.bottom, rowDesaturation[targetRow]);
|
|
266
|
+
}
|
|
267
|
+
if (tree.species === "crystal_tree") {
|
|
268
|
+
palette = { ...palette, ...crystalPalette };
|
|
269
|
+
}
|
|
270
|
+
compositeSprite(buffer, getTreeSprite(tree.species), tree.screenX, rowY, palette, { minY: skyRows });
|
|
153
271
|
}
|
|
154
|
-
if (isSunsetSilhouette) {
|
|
155
|
-
palette = silhouettePalette;
|
|
156
|
-
}
|
|
157
|
-
compositeSprite(buffer, getTreeSprite(tree.species), scaleX(tree.x_position, width), rowY, palette);
|
|
158
272
|
}
|
|
159
273
|
|
|
274
|
+
const groundY = artRows - groundRows;
|
|
160
275
|
for (const element of state.ground_elements) {
|
|
161
|
-
compositeSprite(buffer, getGroundElementSprite(element.type), scaleX(element.x_position, width),
|
|
276
|
+
compositeSprite(buffer, getGroundElementSprite(element.type), scaleX(element.x_position, width), groundY, environment.palette);
|
|
162
277
|
}
|
|
163
278
|
|
|
164
|
-
const scene = getDynamicScene(state, width, tick, environment);
|
|
165
|
-
|
|
279
|
+
const scene = getDynamicScene(state, width, tick, environment, layout);
|
|
280
|
+
const maxAnimals = Math.max(2, Math.min(3, Math.floor(width / 40)));
|
|
281
|
+
const visibleAnimals = scene.animals.slice(0, maxAnimals);
|
|
282
|
+
for (const animal of visibleAnimals) {
|
|
166
283
|
if (animal.type === "owl" && environment.sky.name !== "night") {
|
|
167
284
|
continue;
|
|
168
285
|
}
|
|
@@ -170,29 +287,89 @@ function drawForest(buffer, width, state, environment, tick) {
|
|
|
170
287
|
buffer,
|
|
171
288
|
getAnimalSprite(animal.type),
|
|
172
289
|
animal.x,
|
|
173
|
-
Math.min(
|
|
290
|
+
Math.min(artRows - groundRows - 1, Math.round(animal.y)),
|
|
174
291
|
environment.palette,
|
|
175
292
|
);
|
|
176
293
|
}
|
|
177
294
|
}
|
|
178
295
|
|
|
179
|
-
function drawGround(buffer, width, environment) {
|
|
180
|
-
|
|
181
|
-
|
|
296
|
+
function drawGround(buffer, width, environment, layout) {
|
|
297
|
+
const { skyRows, forestRows, groundRows } = layout;
|
|
298
|
+
const groundStart = skyRows + forestRows;
|
|
299
|
+
|
|
300
|
+
for (let row = 0; row < groundRows; row += 1) {
|
|
301
|
+
const y = groundStart + row;
|
|
302
|
+
if (row === 0) {
|
|
303
|
+
for (let x = 0; x < width; x += 1) {
|
|
304
|
+
const char = x % 5 === 0 ? "▓" : "▄";
|
|
305
|
+
const color = x % 3 === 0 ? environment.palette.grassLight : environment.palette.grass;
|
|
306
|
+
paint(buffer, x, y, char, color);
|
|
307
|
+
}
|
|
308
|
+
} else if (row === 1) {
|
|
309
|
+
for (let x = 0; x < width; x += 1) {
|
|
310
|
+
const color = x % 7 === 0 ? environment.palette.groundLight : environment.palette.ground;
|
|
311
|
+
paint(buffer, x, y, "▄", color);
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
for (let x = 0; x < width; x += 1) {
|
|
315
|
+
paint(buffer, x, y, "▄", environment.palette.groundDark);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function drawFog(buffer, width, layout, environment) {
|
|
322
|
+
const { skyRows, forestRows } = layout;
|
|
323
|
+
const fogColor = "#5a7a5a";
|
|
324
|
+
const fogColumns = [
|
|
325
|
+
{ offset: 0, char: "▒", opacity: 0.6 },
|
|
326
|
+
{ offset: 1, char: "▒", opacity: 0.45 },
|
|
327
|
+
{ offset: 2, char: "░", opacity: 0.3 },
|
|
328
|
+
{ offset: 3, char: "░", opacity: 0.15 },
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
for (let y = skyRows; y < skyRows + forestRows; y += 1) {
|
|
332
|
+
for (const fog of fogColumns) {
|
|
333
|
+
const x = fog.offset;
|
|
334
|
+
if (x >= width) continue;
|
|
335
|
+
const cell = buffer[y][x];
|
|
336
|
+
if (cell.bg) {
|
|
337
|
+
paintBg(buffer, x, y, lerpColor(cell.bg, fogColor, fog.opacity));
|
|
338
|
+
}
|
|
339
|
+
if (cell.color) {
|
|
340
|
+
paint(buffer, x, y, cell.char, lerpColor(cell.color, fogColor, fog.opacity));
|
|
341
|
+
} else {
|
|
342
|
+
paint(buffer, x, y, fog.char, lerpColor(environment.sky.bottom, fogColor, fog.opacity));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const fog of fogColumns) {
|
|
347
|
+
const x = width - 1 - fog.offset;
|
|
348
|
+
if (x < 0) continue;
|
|
349
|
+
const cell = buffer[y][x];
|
|
350
|
+
if (cell.bg) {
|
|
351
|
+
paintBg(buffer, x, y, lerpColor(cell.bg, fogColor, fog.opacity));
|
|
352
|
+
}
|
|
353
|
+
if (cell.color) {
|
|
354
|
+
paint(buffer, x, y, cell.char, lerpColor(cell.color, fogColor, fog.opacity));
|
|
355
|
+
} else {
|
|
356
|
+
paint(buffer, x, y, fog.char, lerpColor(environment.sky.bottom, fogColor, fog.opacity));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
182
359
|
}
|
|
183
360
|
}
|
|
184
361
|
|
|
185
362
|
function buildStatsLine(state, environment, width, color) {
|
|
186
|
-
const trees = `${state.trees.length} tree${state.trees.length === 1 ? "" : "s"}`;
|
|
363
|
+
const trees = `${state.trees.length} tree${state.trees.length === 1 ? "" : "s"} in your forest`;
|
|
187
364
|
const animals = `${state.animals.length} animal${state.animals.length === 1 ? "" : "s"}`;
|
|
188
365
|
const streak = `${state.current_streak} day streak`;
|
|
189
366
|
const weather = `${environment.season.icon} ${environment.season.label} ${environment.weather.label}`;
|
|
190
|
-
const separator = colorize(" • ", "#6b7280", color);
|
|
367
|
+
const separator = colorize(" • ", "#6b7280", null, color);
|
|
191
368
|
const line = [
|
|
192
|
-
colorize("🌲 " + trees, "#7bd389", color),
|
|
193
|
-
colorize("🐇 " + animals, "#f4d35e", color),
|
|
194
|
-
colorize("🔥 " + streak, "#ee964b", color),
|
|
195
|
-
colorize(weather, "#d4d4d8", color),
|
|
369
|
+
colorize("🌲 " + trees, "#7bd389", null, color),
|
|
370
|
+
colorize("🐇 " + animals, "#f4d35e", null, color),
|
|
371
|
+
colorize("🔥 " + streak, "#ee964b", null, color),
|
|
372
|
+
colorize(weather, "#d4d4d8", null, color),
|
|
196
373
|
].join(separator);
|
|
197
374
|
|
|
198
375
|
if (line.length >= width) {
|
|
@@ -205,17 +382,24 @@ export function buildTerminalScene(state, termWidth = 80, tick = 0, options = {}
|
|
|
205
382
|
const width = Math.max(20, termWidth);
|
|
206
383
|
const date = options.now instanceof Date ? options.now : new Date();
|
|
207
384
|
const environment = getEnvironmentSnapshot(date, state.current_streak ?? 0);
|
|
208
|
-
const
|
|
385
|
+
const layout = computeLayout(options.termHeight || null);
|
|
386
|
+
const buffer = createBuffer(width, layout.artRows);
|
|
387
|
+
const startOfYear = new Date(date.getFullYear(), 0, 0);
|
|
388
|
+
const dayOfYear = Math.floor((date - startOfYear) / 86400000);
|
|
389
|
+
const viewportTrees = selectViewportTrees(state.trees, width, dayOfYear);
|
|
209
390
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
391
|
+
drawSkyBackground(buffer, width, environment, layout);
|
|
392
|
+
drawSky(buffer, width, environment, tick, layout);
|
|
393
|
+
drawWeather(buffer, width, environment, tick, layout);
|
|
394
|
+
drawSeasonParticles(buffer, width, environment, tick, layout);
|
|
395
|
+
drawForest(buffer, width, viewportTrees, state, environment, tick, layout);
|
|
396
|
+
drawGround(buffer, width, environment, layout);
|
|
397
|
+
drawFog(buffer, width, layout, environment);
|
|
215
398
|
|
|
216
399
|
return {
|
|
217
400
|
width,
|
|
218
401
|
environment,
|
|
402
|
+
layout,
|
|
219
403
|
buffer,
|
|
220
404
|
statsLine: buildStatsLine(state, environment, width, options.color !== false),
|
|
221
405
|
};
|
|
@@ -233,10 +417,9 @@ export function renderTerminalFrame(state, termWidth = 80, tick = 0, options = {
|
|
|
233
417
|
const scene = buildTerminalScene(state, termWidth, tick, options);
|
|
234
418
|
const lines = scene.buffer.map((row) =>
|
|
235
419
|
row
|
|
236
|
-
.map((cell) => colorize(cell.char, cell.color, options.color !== false))
|
|
420
|
+
.map((cell) => colorize(cell.char, cell.color, cell.bg, options.color !== false))
|
|
237
421
|
.join(""),
|
|
238
422
|
);
|
|
239
423
|
lines.push(scene.statsLine);
|
|
240
424
|
return lines.join("\n");
|
|
241
425
|
}
|
|
242
|
-
|