honeytree 1.0.8 → 1.1.0
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 +28 -16
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";
|
|
3
|
+
import { getDynamicScene, getCrystalPalette } from "../core/animation.js";
|
|
4
4
|
import { getEnvironmentSnapshot } 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";
|
|
@@ -129,8 +128,33 @@ function scaleX(virtualX, width) {
|
|
|
129
128
|
|
|
130
129
|
function drawForest(buffer, width, state, environment, tick) {
|
|
131
130
|
const treeBaseY = SKY_ROWS + FOREST_ROWS - 1;
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
const isSunsetSilhouette = environment.sky.name === "sunset";
|
|
132
|
+
const silhouettePalette = {};
|
|
133
|
+
if (isSunsetSilhouette) {
|
|
134
|
+
for (const key of Object.keys(environment.palette)) {
|
|
135
|
+
silhouettePalette[key] = "#1a1a2e";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const crystalPalette = getCrystalPalette(tick);
|
|
140
|
+
|
|
141
|
+
// Sort trees by row: back first, then mid, then front
|
|
142
|
+
const rowOrder = { back: 0, mid: 1, front: 2 };
|
|
143
|
+
const sortedTrees = [...state.trees].sort(
|
|
144
|
+
(a, b) => (rowOrder[a.row] ?? 2) - (rowOrder[b.row] ?? 2),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
for (const tree of sortedTrees) {
|
|
148
|
+
const row = tree.row || "front";
|
|
149
|
+
const rowY = row === "back" ? treeBaseY - 2 : row === "mid" ? treeBaseY - 1 : treeBaseY;
|
|
150
|
+
let palette = environment.palette;
|
|
151
|
+
if (tree.species === "crystal_tree") {
|
|
152
|
+
palette = { ...environment.palette, ...crystalPalette };
|
|
153
|
+
}
|
|
154
|
+
if (isSunsetSilhouette) {
|
|
155
|
+
palette = silhouettePalette;
|
|
156
|
+
}
|
|
157
|
+
compositeSprite(buffer, getTreeSprite(tree.species), scaleX(tree.x_position, width), rowY, palette);
|
|
134
158
|
}
|
|
135
159
|
|
|
136
160
|
for (const element of state.ground_elements) {
|
|
@@ -150,18 +174,6 @@ function drawForest(buffer, width, state, environment, tick) {
|
|
|
150
174
|
environment.palette,
|
|
151
175
|
);
|
|
152
176
|
}
|
|
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
177
|
}
|
|
166
178
|
|
|
167
179
|
function drawGround(buffer, width, environment) {
|