honeytree 1.0.9 → 1.1.1
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 +2 -5
- package/src/core/animation.js +31 -20
- package/src/core/environment.js +3 -4
- package/src/core/progression.js +27 -9
- package/src/core/sprites.js +125 -113
- package/src/core/state.js +19 -3
- package/src/renderers/terminal.js +97 -30
package/package.json
CHANGED
package/src/commands/watch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { Box, Text, render, useApp, useInput, useStdout } from "ink";
|
|
3
3
|
|
|
4
|
-
import { applyActiveMinutes,
|
|
4
|
+
import { applyActiveMinutes, applyCommit, applyFileSave } from "../core/progression.js";
|
|
5
5
|
import { ensureState, readState, updateState } from "../core/state.js";
|
|
6
6
|
import { renderTerminalFrame } from "../renderers/terminal.js";
|
|
7
7
|
import { createActivityTracker } from "../tracker/activity.js";
|
|
@@ -70,10 +70,7 @@ function ForestWatchApp() {
|
|
|
70
70
|
cwd: process.cwd(),
|
|
71
71
|
onSave() {
|
|
72
72
|
activity.markActive();
|
|
73
|
-
const nextState = updateState((draft) =>
|
|
74
|
-
applyActivityPulse(draft);
|
|
75
|
-
return applyFileSave(draft);
|
|
76
|
-
});
|
|
73
|
+
const nextState = updateState((draft) => applyFileSave(draft));
|
|
77
74
|
setState(nextState);
|
|
78
75
|
},
|
|
79
76
|
});
|
package/src/core/animation.js
CHANGED
|
@@ -105,31 +105,42 @@ export function getWeatherParticles(width, height, tick = 0, weather = "clear",
|
|
|
105
105
|
}));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export function
|
|
109
|
-
const speed = 0.06;
|
|
110
|
-
const cycle = width * 2;
|
|
111
|
-
const raw = (tick * speed) % cycle;
|
|
112
|
-
const x = raw < width ? raw : cycle - raw;
|
|
113
|
-
const direction = raw < width ? "right" : "left";
|
|
114
|
-
|
|
115
|
-
const ticksSinceSave = tick - lastSaveTick;
|
|
116
|
-
let pose = "walking";
|
|
117
|
-
if (ticksSinceSave < 8) {
|
|
118
|
-
pose = "happy";
|
|
119
|
-
} else if (ticksSinceSave > 300) {
|
|
120
|
-
pose = "sitting";
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return { x: Math.round(x), y: 11, direction, pose };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function getDynamicScene(state, width, tick, environment, lastSaveTick = -Infinity) {
|
|
108
|
+
export function getDynamicScene(state, width, tick, environment) {
|
|
127
109
|
return {
|
|
128
110
|
clouds: getClouds(width, tick, environment.sky.name),
|
|
129
111
|
stars: environment.sky.name === "night" ? getStars(width, tick) : [],
|
|
130
112
|
animals: getAnimatedAnimals(state, width, tick),
|
|
131
113
|
particles: getWeatherParticles(width, 12, tick, environment.weather.name, environment.season.name),
|
|
132
|
-
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getCrystalPalette(tick) {
|
|
118
|
+
const CRYSTAL_COLORS = ["#00e5ff", "#e040fb", "#69f0ae"];
|
|
119
|
+
const cycle = (tick * 0.02) % 3;
|
|
120
|
+
const index = Math.floor(cycle);
|
|
121
|
+
const next = (index + 1) % 3;
|
|
122
|
+
const t = cycle - index;
|
|
123
|
+
function lerpChannel(a, b, amount) {
|
|
124
|
+
return Math.round(a + (b - a) * amount);
|
|
125
|
+
}
|
|
126
|
+
function parseHex(hex) {
|
|
127
|
+
const n = hex.slice(1);
|
|
128
|
+
return [parseInt(n.slice(0, 2), 16), parseInt(n.slice(2, 4), 16), parseInt(n.slice(4, 6), 16)];
|
|
129
|
+
}
|
|
130
|
+
function toHex(r, g, b) {
|
|
131
|
+
return "#" + [r, g, b].map((v) => Math.min(255, Math.max(0, v)).toString(16).padStart(2, "0")).join("");
|
|
132
|
+
}
|
|
133
|
+
const from = parseHex(CRYSTAL_COLORS[index]);
|
|
134
|
+
const to = parseHex(CRYSTAL_COLORS[next]);
|
|
135
|
+
const mixed = toHex(
|
|
136
|
+
lerpChannel(from[0], to[0], t),
|
|
137
|
+
lerpChannel(from[1], to[1], t),
|
|
138
|
+
lerpChannel(from[2], to[2], t),
|
|
139
|
+
);
|
|
140
|
+
return {
|
|
141
|
+
crystal1: mixed,
|
|
142
|
+
crystal2: CRYSTAL_COLORS[next],
|
|
143
|
+
crystal3: CRYSTAL_COLORS[(next + 1) % 3],
|
|
133
144
|
};
|
|
134
145
|
}
|
|
135
146
|
|
package/src/core/environment.js
CHANGED
|
@@ -85,10 +85,9 @@ const BASE_PALETTE = {
|
|
|
85
85
|
deerLight: "#cba37b",
|
|
86
86
|
owl: "#5f5b6b",
|
|
87
87
|
owlLight: "#d8d7dd",
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
llamaNose: "#f5a0b0",
|
|
88
|
+
crystal1: "#00e5ff",
|
|
89
|
+
crystal2: "#e040fb",
|
|
90
|
+
crystal3: "#69f0ae",
|
|
92
91
|
};
|
|
93
92
|
|
|
94
93
|
const SEASON_OVERRIDES = {
|
package/src/core/progression.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { addMinutes } from "date-fns";
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
getAnimalTypeForMinutes,
|
|
5
3
|
getAnimalSprite,
|
|
@@ -72,10 +70,28 @@ export function updateCommitStreak(currentStreak, lastActiveDate, now = new Date
|
|
|
72
70
|
return 1;
|
|
73
71
|
}
|
|
74
72
|
|
|
73
|
+
function chooseRow(trees) {
|
|
74
|
+
const counts = { back: 0, mid: 0, front: 0 };
|
|
75
|
+
for (const tree of trees) {
|
|
76
|
+
const row = tree.row || "front";
|
|
77
|
+
counts[row] = (counts[row] || 0) + 1;
|
|
78
|
+
}
|
|
79
|
+
if (counts.back <= counts.mid && counts.back <= counts.front) return "back";
|
|
80
|
+
if (counts.mid <= counts.front) return "mid";
|
|
81
|
+
return "front";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ROW_SCALE = { back: 0.6, mid: 0.85, front: 1.0 };
|
|
85
|
+
|
|
75
86
|
export function applyCommit(state, { commitHash = "", now = new Date() } = {}) {
|
|
76
87
|
const nextTotal = state.total_commits + 1;
|
|
77
|
-
|
|
88
|
+
let species = getTreeSpeciesForCommit(nextTotal);
|
|
89
|
+
// Only one crystal tree allowed
|
|
90
|
+
if (species === "crystal_tree" && state.trees.some((t) => t.species === "crystal_tree")) {
|
|
91
|
+
species = "ancient_oak";
|
|
92
|
+
}
|
|
78
93
|
const sprite = getTreeSprite(species);
|
|
94
|
+
const row = chooseRow(state.trees);
|
|
79
95
|
state.total_commits = nextTotal;
|
|
80
96
|
state.current_streak = updateCommitStreak(state.current_streak, state.last_active_date, now);
|
|
81
97
|
state.last_active_date = dayString(now);
|
|
@@ -83,14 +99,20 @@ export function applyCommit(state, { commitHash = "", now = new Date() } = {}) {
|
|
|
83
99
|
state.trees.push({
|
|
84
100
|
species,
|
|
85
101
|
planted_at: now.toISOString(),
|
|
86
|
-
x_position: placeSprite(
|
|
102
|
+
x_position: placeSprite(
|
|
103
|
+
state.trees.filter((t) => t.row === row),
|
|
104
|
+
sprite.width,
|
|
105
|
+
`${species}:${nextTotal}`,
|
|
106
|
+
),
|
|
107
|
+
row,
|
|
108
|
+
scale: ROW_SCALE[row],
|
|
87
109
|
});
|
|
88
110
|
return state;
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
export function applyFileSave(state, { now = new Date() } = {}) {
|
|
92
114
|
state.total_file_saves += 1;
|
|
93
|
-
if (state.total_file_saves %
|
|
115
|
+
if (state.total_file_saves % 100 === 0) {
|
|
94
116
|
const type = getGroundElementType(state.total_file_saves);
|
|
95
117
|
const sprite = getGroundElementSprite(type);
|
|
96
118
|
state.ground_elements.push({
|
|
@@ -125,7 +147,3 @@ export function applyActiveMinutes(state, { minutes = 1, now = new Date() } = {}
|
|
|
125
147
|
return state;
|
|
126
148
|
}
|
|
127
149
|
|
|
128
|
-
export function applyActivityPulse(state, { now = new Date() } = {}) {
|
|
129
|
-
const updated = addMinutes(now, 0);
|
|
130
|
-
return state;
|
|
131
|
-
}
|
package/src/core/sprites.js
CHANGED
|
@@ -8,12 +8,11 @@ export const TREE_SPECIES = [
|
|
|
8
8
|
"pine",
|
|
9
9
|
"willow",
|
|
10
10
|
"ancient_oak",
|
|
11
|
+
"crystal_tree",
|
|
11
12
|
];
|
|
12
13
|
|
|
13
14
|
export const ANIMAL_TYPES = ["butterfly", "rabbit", "fox", "deer", "owl"];
|
|
14
15
|
|
|
15
|
-
export const LLAMA_POSES = ["walking", "sitting", "happy"];
|
|
16
|
-
|
|
17
16
|
export const GROUND_ELEMENT_TYPES = ["flower", "mushroom", "rock", "tall_grass"];
|
|
18
17
|
|
|
19
18
|
function parseSprite(template, legend) {
|
|
@@ -57,17 +56,18 @@ export function materializeSprite(sprite, palette, char = BLOCK) {
|
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
export function getTreeSpeciesForCommit(totalCommits) {
|
|
60
|
-
if (totalCommits <=
|
|
61
|
-
if (totalCommits <=
|
|
62
|
-
if (totalCommits <=
|
|
63
|
-
if (totalCommits <=
|
|
64
|
-
if (totalCommits <=
|
|
65
|
-
if (totalCommits <=
|
|
66
|
-
return "ancient_oak";
|
|
59
|
+
if (totalCommits <= 2) return "sapling";
|
|
60
|
+
if (totalCommits <= 8) return "birch";
|
|
61
|
+
if (totalCommits <= 18) return "oak";
|
|
62
|
+
if (totalCommits <= 30) return "cherry";
|
|
63
|
+
if (totalCommits <= 45) return "pine";
|
|
64
|
+
if (totalCommits <= 65) return "willow";
|
|
65
|
+
if (totalCommits <= 100) return "ancient_oak";
|
|
66
|
+
return "crystal_tree";
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export function getGroundElementType(totalFileSaves) {
|
|
70
|
-
const tier = Math.max(0, Math.floor(totalFileSaves /
|
|
70
|
+
const tier = Math.max(0, Math.floor(totalFileSaves / 100) - 1);
|
|
71
71
|
return GROUND_ELEMENT_TYPES[tier % GROUND_ELEMENT_TYPES.length];
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -82,9 +82,11 @@ export function getAnimalTypeForMinutes(totalMinutesCoded) {
|
|
|
82
82
|
const TREE_SPRITES = {
|
|
83
83
|
sapling: parseSprite(
|
|
84
84
|
`
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
l
|
|
86
|
+
lLl
|
|
87
|
+
l
|
|
88
|
+
t
|
|
89
|
+
t
|
|
88
90
|
`,
|
|
89
91
|
{
|
|
90
92
|
l: "leaf",
|
|
@@ -94,11 +96,14 @@ const TREE_SPRITES = {
|
|
|
94
96
|
),
|
|
95
97
|
birch: parseSprite(
|
|
96
98
|
`
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
ll
|
|
100
|
+
lLLl
|
|
101
|
+
lLLLLl
|
|
102
|
+
lLLl
|
|
103
|
+
bb
|
|
104
|
+
bB
|
|
105
|
+
bb
|
|
106
|
+
bB
|
|
102
107
|
`,
|
|
103
108
|
{
|
|
104
109
|
l: "leaf",
|
|
@@ -109,12 +114,16 @@ const TREE_SPRITES = {
|
|
|
109
114
|
),
|
|
110
115
|
oak: parseSprite(
|
|
111
116
|
`
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
ddd
|
|
118
|
+
ddlllld
|
|
119
|
+
dlllLLllld
|
|
120
|
+
dllLLLLlld
|
|
121
|
+
dlllllld
|
|
122
|
+
llllll
|
|
123
|
+
tt
|
|
124
|
+
tTTt
|
|
125
|
+
tTTt
|
|
126
|
+
tt
|
|
118
127
|
`,
|
|
119
128
|
{
|
|
120
129
|
d: "leafDark",
|
|
@@ -126,12 +135,16 @@ const TREE_SPRITES = {
|
|
|
126
135
|
),
|
|
127
136
|
cherry: parseSprite(
|
|
128
137
|
`
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
ppp
|
|
139
|
+
ppPPPPp
|
|
140
|
+
pPPPpPPPp
|
|
141
|
+
pPPPPPPPp
|
|
142
|
+
pPPPPPp
|
|
143
|
+
PPPPp
|
|
144
|
+
tt
|
|
145
|
+
tTTt
|
|
146
|
+
tTTt
|
|
147
|
+
tt
|
|
135
148
|
`,
|
|
136
149
|
{
|
|
137
150
|
p: "petal",
|
|
@@ -142,13 +155,16 @@ const TREE_SPRITES = {
|
|
|
142
155
|
),
|
|
143
156
|
pine: parseSprite(
|
|
144
157
|
`
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
p
|
|
159
|
+
pPp
|
|
160
|
+
pPPPp
|
|
161
|
+
PPP
|
|
162
|
+
pPPPp
|
|
163
|
+
pPPPPPp
|
|
164
|
+
PPPPP
|
|
165
|
+
tTt
|
|
166
|
+
tTt
|
|
167
|
+
t
|
|
152
168
|
`,
|
|
153
169
|
{
|
|
154
170
|
p: "pineDark",
|
|
@@ -159,13 +175,18 @@ const TREE_SPRITES = {
|
|
|
159
175
|
),
|
|
160
176
|
willow: parseSprite(
|
|
161
177
|
`
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
llll
|
|
179
|
+
llLLLLll
|
|
180
|
+
llLLllLLll
|
|
181
|
+
lLl lLl
|
|
182
|
+
ll ll
|
|
183
|
+
ll ll
|
|
184
|
+
l l
|
|
185
|
+
tTTt
|
|
186
|
+
tTTt
|
|
187
|
+
tT
|
|
188
|
+
tT
|
|
189
|
+
tt
|
|
169
190
|
`,
|
|
170
191
|
{
|
|
171
192
|
l: "leaf",
|
|
@@ -176,14 +197,20 @@ const TREE_SPRITES = {
|
|
|
176
197
|
),
|
|
177
198
|
ancient_oak: parseSprite(
|
|
178
199
|
`
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
200
|
+
ddd
|
|
201
|
+
dddllllddd
|
|
202
|
+
ddllLLLLlldd
|
|
203
|
+
dddllLLLLllddd
|
|
204
|
+
ddlllLLLllldd
|
|
205
|
+
ddlllllllldd
|
|
206
|
+
mllllllm
|
|
207
|
+
mmmm
|
|
208
|
+
tTTTTt
|
|
209
|
+
tTTTTt
|
|
210
|
+
ttTTTTtt
|
|
211
|
+
tt tt
|
|
212
|
+
t t
|
|
213
|
+
t t
|
|
187
214
|
`,
|
|
188
215
|
{
|
|
189
216
|
d: "leafDark",
|
|
@@ -194,13 +221,37 @@ const TREE_SPRITES = {
|
|
|
194
221
|
T: "trunk",
|
|
195
222
|
},
|
|
196
223
|
),
|
|
224
|
+
crystal_tree: parseSprite(
|
|
225
|
+
`
|
|
226
|
+
cc
|
|
227
|
+
ccCCCc
|
|
228
|
+
cCCcCCCc
|
|
229
|
+
cCCCCCCc
|
|
230
|
+
cCCCCc
|
|
231
|
+
CCCc
|
|
232
|
+
tt
|
|
233
|
+
tTTt
|
|
234
|
+
tTTt
|
|
235
|
+
tT
|
|
236
|
+
tt
|
|
237
|
+
tt
|
|
238
|
+
`,
|
|
239
|
+
{
|
|
240
|
+
c: "crystal1",
|
|
241
|
+
C: "crystal2",
|
|
242
|
+
t: "trunkDark",
|
|
243
|
+
T: "trunk",
|
|
244
|
+
},
|
|
245
|
+
),
|
|
197
246
|
};
|
|
198
247
|
|
|
199
248
|
const ANIMAL_SPRITES = {
|
|
200
249
|
butterfly: parseSprite(
|
|
201
250
|
`
|
|
202
|
-
w
|
|
203
|
-
|
|
251
|
+
w w
|
|
252
|
+
wW Ww
|
|
253
|
+
WWW
|
|
254
|
+
W
|
|
204
255
|
`,
|
|
205
256
|
{
|
|
206
257
|
w: "wing",
|
|
@@ -209,9 +260,10 @@ w w
|
|
|
209
260
|
),
|
|
210
261
|
rabbit: parseSprite(
|
|
211
262
|
`
|
|
212
|
-
|
|
213
|
-
rrre
|
|
214
|
-
|
|
263
|
+
ee
|
|
264
|
+
rrre
|
|
265
|
+
rrrrr
|
|
266
|
+
rr r
|
|
215
267
|
`,
|
|
216
268
|
{
|
|
217
269
|
r: "rabbit",
|
|
@@ -220,9 +272,11 @@ rrre
|
|
|
220
272
|
),
|
|
221
273
|
fox: parseSprite(
|
|
222
274
|
`
|
|
223
|
-
|
|
224
|
-
ffff
|
|
225
|
-
|
|
275
|
+
ff
|
|
276
|
+
ffff
|
|
277
|
+
ffffff
|
|
278
|
+
fl lf
|
|
279
|
+
f f
|
|
226
280
|
`,
|
|
227
281
|
{
|
|
228
282
|
f: "fox",
|
|
@@ -231,20 +285,26 @@ fllf
|
|
|
231
285
|
),
|
|
232
286
|
deer: parseSprite(
|
|
233
287
|
`
|
|
288
|
+
a a
|
|
234
289
|
aa
|
|
235
|
-
|
|
236
|
-
|
|
290
|
+
dddddd
|
|
291
|
+
dl ld
|
|
292
|
+
d d
|
|
293
|
+
d d
|
|
237
294
|
`,
|
|
238
295
|
{
|
|
239
296
|
a: "deerLight",
|
|
240
297
|
d: "deer",
|
|
298
|
+
l: "deerLight",
|
|
241
299
|
},
|
|
242
300
|
),
|
|
243
301
|
owl: parseSprite(
|
|
244
302
|
`
|
|
245
|
-
|
|
246
|
-
OooO
|
|
247
|
-
|
|
303
|
+
oo
|
|
304
|
+
OooO
|
|
305
|
+
OooooO
|
|
306
|
+
oooo
|
|
307
|
+
tt
|
|
248
308
|
`,
|
|
249
309
|
{
|
|
250
310
|
o: "owl",
|
|
@@ -254,47 +314,6 @@ OooO
|
|
|
254
314
|
),
|
|
255
315
|
};
|
|
256
316
|
|
|
257
|
-
const LLAMA_SPRITES = {
|
|
258
|
-
walking: parseSprite(
|
|
259
|
-
`
|
|
260
|
-
ee
|
|
261
|
-
lllN
|
|
262
|
-
llll
|
|
263
|
-
l l
|
|
264
|
-
`,
|
|
265
|
-
{
|
|
266
|
-
e: "llamaLight",
|
|
267
|
-
l: "llama",
|
|
268
|
-
N: "llamaNose",
|
|
269
|
-
},
|
|
270
|
-
),
|
|
271
|
-
sitting: parseSprite(
|
|
272
|
-
`
|
|
273
|
-
ee
|
|
274
|
-
lllN
|
|
275
|
-
lll
|
|
276
|
-
ll
|
|
277
|
-
`,
|
|
278
|
-
{
|
|
279
|
-
e: "llamaLight",
|
|
280
|
-
l: "llama",
|
|
281
|
-
N: "llamaNose",
|
|
282
|
-
},
|
|
283
|
-
),
|
|
284
|
-
happy: parseSprite(
|
|
285
|
-
`
|
|
286
|
-
ee
|
|
287
|
-
lllN
|
|
288
|
-
llll
|
|
289
|
-
l l
|
|
290
|
-
`,
|
|
291
|
-
{
|
|
292
|
-
e: "llamaLight",
|
|
293
|
-
l: "llama",
|
|
294
|
-
N: "llamaNose",
|
|
295
|
-
},
|
|
296
|
-
),
|
|
297
|
-
};
|
|
298
317
|
|
|
299
318
|
const GROUND_SPRITES = {
|
|
300
319
|
flower: parseSprite(
|
|
@@ -364,13 +383,6 @@ export function getGroundElementSprite(type) {
|
|
|
364
383
|
return sprite;
|
|
365
384
|
}
|
|
366
385
|
|
|
367
|
-
export function getLlamaSprite(pose = "walking") {
|
|
368
|
-
const sprite = LLAMA_SPRITES[pose];
|
|
369
|
-
if (!sprite) {
|
|
370
|
-
throw new Error(`Unknown llama pose: ${pose}`);
|
|
371
|
-
}
|
|
372
|
-
return sprite;
|
|
373
|
-
}
|
|
374
386
|
|
|
375
387
|
export function getRandomGroundElement() {
|
|
376
388
|
return GROUND_ELEMENT_TYPES[Math.floor(Math.random() * GROUND_ELEMENT_TYPES.length)];
|
package/src/core/state.js
CHANGED
|
@@ -40,17 +40,30 @@ export function createEmptyState(now = new Date()) {
|
|
|
40
40
|
current_streak: 0,
|
|
41
41
|
last_active_date: todayString(now),
|
|
42
42
|
last_commit_hash: "",
|
|
43
|
+
global_trees: 0,
|
|
43
44
|
trees: [],
|
|
44
45
|
animals: [],
|
|
45
46
|
ground_elements: [],
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function
|
|
50
|
+
function assignRowByIndex(index, total) {
|
|
51
|
+
if (total <= 0) return "front";
|
|
52
|
+
const third = total / 3;
|
|
53
|
+
if (index < third) return "back";
|
|
54
|
+
if (index < third * 2) return "mid";
|
|
55
|
+
return "front";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeTree(tree, index, total) {
|
|
59
|
+
const row = tree?.row || assignRowByIndex(index, total);
|
|
60
|
+
const scaleMap = { back: 0.6, mid: 0.85, front: 1.0 };
|
|
50
61
|
return {
|
|
51
62
|
species: tree?.species ?? tree?.type ?? "sapling",
|
|
52
63
|
planted_at: tree?.planted_at ?? tree?.plantedAt ?? new Date().toISOString(),
|
|
53
64
|
x_position: Number.isFinite(tree?.x_position) ? tree.x_position : tree?.x ?? 12,
|
|
65
|
+
row,
|
|
66
|
+
scale: scaleMap[row] ?? 1.0,
|
|
54
67
|
};
|
|
55
68
|
}
|
|
56
69
|
|
|
@@ -88,7 +101,8 @@ function rescalePositions(items) {
|
|
|
88
101
|
|
|
89
102
|
export function normalizeState(input = {}) {
|
|
90
103
|
const base = createEmptyState();
|
|
91
|
-
const
|
|
104
|
+
const rawTrees = Array.isArray(input.trees) ? input.trees : [];
|
|
105
|
+
const trees = rawTrees.map((tree, index) => normalizeTree(tree, index, rawTrees.length));
|
|
92
106
|
const animals = Array.isArray(input.animals) ? input.animals.map(normalizeAnimal) : [];
|
|
93
107
|
const ground_elements = Array.isArray(input.ground_elements)
|
|
94
108
|
? input.ground_elements.map(normalizeGroundElement)
|
|
@@ -107,6 +121,7 @@ export function normalizeState(input = {}) {
|
|
|
107
121
|
? input.last_active_date
|
|
108
122
|
: input.lastActiveDate ?? base.last_active_date,
|
|
109
123
|
last_commit_hash: typeof input.last_commit_hash === "string" ? input.last_commit_hash : "",
|
|
124
|
+
global_trees: Number.isFinite(input.global_trees) ? input.global_trees : 0,
|
|
110
125
|
trees: rescalePositions(trees),
|
|
111
126
|
animals: rescalePositions(animals),
|
|
112
127
|
ground_elements: rescalePositions(ground_elements),
|
|
@@ -118,7 +133,8 @@ export function migrateOldForest(oldForest = {}) {
|
|
|
118
133
|
migrated.total_commits = oldForest.totalPrompts ?? oldForest.trees?.length ?? 0;
|
|
119
134
|
migrated.current_streak = oldForest.streak ?? 0;
|
|
120
135
|
migrated.last_active_date = oldForest.lastActiveDate ?? todayString();
|
|
121
|
-
|
|
136
|
+
const rawTrees = Array.isArray(oldForest.trees) ? oldForest.trees : [];
|
|
137
|
+
migrated.trees = rawTrees.map((tree, index) => normalizeTree(tree, index, rawTrees.length));
|
|
122
138
|
return migrated;
|
|
123
139
|
}
|
|
124
140
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
|
|
3
|
-
import { getDynamicScene } from "../core/animation.js";
|
|
4
|
-
import { getEnvironmentSnapshot } from "../core/environment.js";
|
|
3
|
+
import { getDynamicScene, getCrystalPalette } from "../core/animation.js";
|
|
4
|
+
import { getEnvironmentSnapshot, lerpColor } from "../core/environment.js";
|
|
5
5
|
import { VIRTUAL_WIDTH } from "../core/progression.js";
|
|
6
6
|
import {
|
|
7
7
|
getAnimalSprite,
|
|
8
8
|
getGroundElementSprite,
|
|
9
|
-
getLlamaSprite,
|
|
10
9
|
getTreeSprite,
|
|
11
10
|
materializeSprite,
|
|
12
11
|
} from "../core/sprites.js";
|
|
@@ -21,14 +20,20 @@ export const SCENE_HEIGHT = ART_ROWS + STATS_ROWS;
|
|
|
21
20
|
|
|
22
21
|
function createBuffer(width) {
|
|
23
22
|
return Array.from({ length: ART_ROWS }, () =>
|
|
24
|
-
Array.from({ length: width }, () => ({ char: " ", color: null })),
|
|
23
|
+
Array.from({ length: width }, () => ({ char: " ", color: null, bg: null })),
|
|
25
24
|
);
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
function paint(buffer, x, y, char, color) {
|
|
29
28
|
if (y < 0 || y >= buffer.length) return;
|
|
30
29
|
if (x < 0 || x >= buffer[0].length) return;
|
|
31
|
-
buffer[y][x] = { char, color };
|
|
30
|
+
buffer[y][x] = { char, color, bg: buffer[y][x].bg };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function paintBg(buffer, x, y, bg) {
|
|
34
|
+
if (y < 0 || y >= buffer.length) return;
|
|
35
|
+
if (x < 0 || x >= buffer[0].length) return;
|
|
36
|
+
buffer[y][x].bg = bg;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
function compositeSprite(buffer, sprite, centerX, baseY, palette) {
|
|
@@ -45,11 +50,42 @@ function compositeSprite(buffer, sprite, centerX, baseY, palette) {
|
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
function colorize(text, color, enabled) {
|
|
49
|
-
if (!enabled
|
|
50
|
-
|
|
53
|
+
function colorize(text, color, bg, enabled) {
|
|
54
|
+
if (!enabled) return text;
|
|
55
|
+
if (!color && !bg) return text;
|
|
56
|
+
let fn = chalk;
|
|
57
|
+
if (bg) fn = fn.bgHex(bg);
|
|
58
|
+
if (color) fn = fn.hex(color);
|
|
59
|
+
return fn(text);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function drawSkyBackground(buffer, width, environment) {
|
|
63
|
+
const sky = environment.sky;
|
|
64
|
+
// Sky rows: vertical gradient from top → mid → bottom
|
|
65
|
+
for (let y = 0; y < SKY_ROWS; y++) {
|
|
66
|
+
const t = y / Math.max(1, SKY_ROWS - 1);
|
|
67
|
+
let color;
|
|
68
|
+
if (t < 0.5) {
|
|
69
|
+
color = lerpColor(sky.top, sky.mid, t * 2);
|
|
70
|
+
} else {
|
|
71
|
+
color = lerpColor(sky.mid, sky.bottom, (t - 0.5) * 2);
|
|
72
|
+
}
|
|
73
|
+
for (let x = 0; x < width; x++) {
|
|
74
|
+
paintBg(buffer, x, y, color);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Forest rows: blend from sky.bottom toward ground
|
|
78
|
+
for (let y = SKY_ROWS; y < SKY_ROWS + FOREST_ROWS; y++) {
|
|
79
|
+
const t = (y - SKY_ROWS) / Math.max(1, FOREST_ROWS - 1);
|
|
80
|
+
const color = lerpColor(sky.bottom, environment.palette.groundDark, t * 0.7);
|
|
81
|
+
for (let x = 0; x < width; x++) {
|
|
82
|
+
paintBg(buffer, x, y, color);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Ground row: solid ground color
|
|
86
|
+
for (let x = 0; x < width; x++) {
|
|
87
|
+
paintBg(buffer, x, ART_ROWS - 1, environment.palette.groundDark);
|
|
51
88
|
}
|
|
52
|
-
return chalk.hex(color)(text);
|
|
53
89
|
}
|
|
54
90
|
|
|
55
91
|
function drawSky(buffer, width, environment, tick) {
|
|
@@ -127,10 +163,50 @@ function scaleX(virtualX, width) {
|
|
|
127
163
|
return Math.round((virtualX / VIRTUAL_WIDTH) * width);
|
|
128
164
|
}
|
|
129
165
|
|
|
166
|
+
function spaceTrees(trees, width, minGap) {
|
|
167
|
+
const sorted = [...trees].sort((a, b) => a.x_position - b.x_position);
|
|
168
|
+
const result = [];
|
|
169
|
+
let lastScreenX = -Infinity;
|
|
170
|
+
for (const tree of sorted) {
|
|
171
|
+
const screenX = Math.round((tree.x_position / VIRTUAL_WIDTH) * width);
|
|
172
|
+
if (screenX - lastScreenX >= minGap) {
|
|
173
|
+
result.push(tree);
|
|
174
|
+
lastScreenX = screenX;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
130
180
|
function drawForest(buffer, width, state, environment, tick) {
|
|
131
181
|
const treeBaseY = SKY_ROWS + FOREST_ROWS - 1;
|
|
132
|
-
|
|
133
|
-
|
|
182
|
+
const isSunsetSilhouette = environment.sky.name === "sunset";
|
|
183
|
+
const silhouettePalette = {};
|
|
184
|
+
if (isSunsetSilhouette) {
|
|
185
|
+
for (const key of Object.keys(environment.palette)) {
|
|
186
|
+
silhouettePalette[key] = "#1a1a2e";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const crystalPalette = getCrystalPalette(tick);
|
|
191
|
+
|
|
192
|
+
// Separate trees by row, then filter for spacing
|
|
193
|
+
const minGap = { back: 6, mid: 10, front: 14 };
|
|
194
|
+
const rows = ["back", "mid", "front"];
|
|
195
|
+
for (const targetRow of rows) {
|
|
196
|
+
const rowTrees = state.trees.filter((t) => (t.row || "front") === targetRow);
|
|
197
|
+
const spaced = spaceTrees(rowTrees, width, minGap[targetRow]);
|
|
198
|
+
const rowY = targetRow === "back" ? treeBaseY - 2 : targetRow === "mid" ? treeBaseY - 1 : treeBaseY;
|
|
199
|
+
|
|
200
|
+
for (const tree of spaced) {
|
|
201
|
+
let palette = environment.palette;
|
|
202
|
+
if (tree.species === "crystal_tree") {
|
|
203
|
+
palette = { ...environment.palette, ...crystalPalette };
|
|
204
|
+
}
|
|
205
|
+
if (isSunsetSilhouette) {
|
|
206
|
+
palette = silhouettePalette;
|
|
207
|
+
}
|
|
208
|
+
compositeSprite(buffer, getTreeSprite(tree.species), scaleX(tree.x_position, width), rowY, palette);
|
|
209
|
+
}
|
|
134
210
|
}
|
|
135
211
|
|
|
136
212
|
for (const element of state.ground_elements) {
|
|
@@ -138,7 +214,9 @@ function drawForest(buffer, width, state, environment, tick) {
|
|
|
138
214
|
}
|
|
139
215
|
|
|
140
216
|
const scene = getDynamicScene(state, width, tick, environment);
|
|
141
|
-
|
|
217
|
+
const maxAnimals = Math.max(2, Math.min(4, Math.floor(width / 25)));
|
|
218
|
+
const visibleAnimals = scene.animals.slice(0, maxAnimals);
|
|
219
|
+
for (const animal of visibleAnimals) {
|
|
142
220
|
if (animal.type === "owl" && environment.sky.name !== "night") {
|
|
143
221
|
continue;
|
|
144
222
|
}
|
|
@@ -150,18 +228,6 @@ function drawForest(buffer, width, state, environment, tick) {
|
|
|
150
228
|
environment.palette,
|
|
151
229
|
);
|
|
152
230
|
}
|
|
153
|
-
|
|
154
|
-
// Llama companion
|
|
155
|
-
const llama = scene.llama;
|
|
156
|
-
if (llama) {
|
|
157
|
-
compositeSprite(
|
|
158
|
-
buffer,
|
|
159
|
-
getLlamaSprite(llama.pose),
|
|
160
|
-
llama.x,
|
|
161
|
-
Math.min(ART_ROWS - 2, llama.y),
|
|
162
|
-
environment.palette,
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
231
|
}
|
|
166
232
|
|
|
167
233
|
function drawGround(buffer, width, environment) {
|
|
@@ -175,12 +241,12 @@ function buildStatsLine(state, environment, width, color) {
|
|
|
175
241
|
const animals = `${state.animals.length} animal${state.animals.length === 1 ? "" : "s"}`;
|
|
176
242
|
const streak = `${state.current_streak} day streak`;
|
|
177
243
|
const weather = `${environment.season.icon} ${environment.season.label} ${environment.weather.label}`;
|
|
178
|
-
const separator = colorize(" • ", "#6b7280", color);
|
|
244
|
+
const separator = colorize(" • ", "#6b7280", null, color);
|
|
179
245
|
const line = [
|
|
180
|
-
colorize("🌲 " + trees, "#7bd389", color),
|
|
181
|
-
colorize("🐇 " + animals, "#f4d35e", color),
|
|
182
|
-
colorize("🔥 " + streak, "#ee964b", color),
|
|
183
|
-
colorize(weather, "#d4d4d8", color),
|
|
246
|
+
colorize("🌲 " + trees, "#7bd389", null, color),
|
|
247
|
+
colorize("🐇 " + animals, "#f4d35e", null, color),
|
|
248
|
+
colorize("🔥 " + streak, "#ee964b", null, color),
|
|
249
|
+
colorize(weather, "#d4d4d8", null, color),
|
|
184
250
|
].join(separator);
|
|
185
251
|
|
|
186
252
|
if (line.length >= width) {
|
|
@@ -195,6 +261,7 @@ export function buildTerminalScene(state, termWidth = 80, tick = 0, options = {}
|
|
|
195
261
|
const environment = getEnvironmentSnapshot(date, state.current_streak ?? 0);
|
|
196
262
|
const buffer = createBuffer(width);
|
|
197
263
|
|
|
264
|
+
drawSkyBackground(buffer, width, environment);
|
|
198
265
|
drawSky(buffer, width, environment, tick);
|
|
199
266
|
drawWeather(buffer, width, environment, tick);
|
|
200
267
|
drawSeasonParticles(buffer, width, environment, tick);
|
|
@@ -221,7 +288,7 @@ export function renderTerminalFrame(state, termWidth = 80, tick = 0, options = {
|
|
|
221
288
|
const scene = buildTerminalScene(state, termWidth, tick, options);
|
|
222
289
|
const lines = scene.buffer.map((row) =>
|
|
223
290
|
row
|
|
224
|
-
.map((cell) => colorize(cell.char, cell.color, options.color !== false))
|
|
291
|
+
.map((cell) => colorize(cell.char, cell.color, cell.bg, options.color !== false))
|
|
225
292
|
.join(""),
|
|
226
293
|
);
|
|
227
294
|
lines.push(scene.statsLine);
|