honeytree 1.0.6 → 1.0.7
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/README.md +22 -169
- package/bin/honeydew.js +9 -12
- package/package.json +15 -8
- package/src/badge.js +16 -41
- package/src/commands/export.js +23 -0
- package/src/commands/init.js +54 -0
- package/src/commands/plant.js +12 -0
- package/src/commands/stats.js +48 -0
- package/src/commands/watch.js +117 -0
- package/src/core/animation.js +135 -0
- package/src/core/environment.js +324 -0
- package/src/core/progression.js +129 -0
- package/src/core/sprites.js +388 -0
- package/src/core/state.js +173 -0
- package/src/markdown.js +13 -44
- package/src/renderers/terminal.js +225 -0
- package/src/tracker/activity.js +43 -0
- package/src/tracker/files.js +44 -0
- package/src/tracker/git.js +77 -0
- package/src/init.js +0 -86
- package/src/plant.js +0 -108
- package/src/renderer.js +0 -349
- package/src/sprites.js +0 -245
- package/src/state.js +0 -53
- package/src/viewer.js +0 -169
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const SPEEDS = {
|
|
2
|
+
butterfly: 0.21,
|
|
3
|
+
rabbit: 0.13,
|
|
4
|
+
fox: 0.09,
|
|
5
|
+
deer: 0.06,
|
|
6
|
+
owl: 0.04,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function wrap(value, min, max) {
|
|
10
|
+
const span = max - min;
|
|
11
|
+
if (span <= 0) return min;
|
|
12
|
+
let result = value;
|
|
13
|
+
while (result < min) result += span;
|
|
14
|
+
while (result > max) result -= span;
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hash(seed) {
|
|
19
|
+
let value = seed >>> 0;
|
|
20
|
+
value = Math.imul((value >>> 16) ^ value, 0x45d9f3b) >>> 0;
|
|
21
|
+
value = Math.imul((value >>> 16) ^ value, 0x45d9f3b) >>> 0;
|
|
22
|
+
return ((value >>> 16) ^ value) >>> 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getClouds(width, tick = 0, skyPhase = "day") {
|
|
26
|
+
const count = skyPhase === "night" ? 2 : 3;
|
|
27
|
+
return Array.from({ length: count }, (_, index) => {
|
|
28
|
+
const length = 7 + (index % 3) * 3;
|
|
29
|
+
const offset = ((tick * (0.35 + index * 0.08)) + index * 17) % (width + length * 2);
|
|
30
|
+
return {
|
|
31
|
+
x: Math.round(offset) - length,
|
|
32
|
+
y: index % 2 === 0 ? 1 : 2,
|
|
33
|
+
length,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getStars(width, tick = 0) {
|
|
39
|
+
const stars = [];
|
|
40
|
+
for (let column = 0; column < width; column += 1) {
|
|
41
|
+
const seeded = hash(column * 97);
|
|
42
|
+
if (seeded % 7 !== 0) continue;
|
|
43
|
+
stars.push({
|
|
44
|
+
x: column,
|
|
45
|
+
y: seeded % 3,
|
|
46
|
+
twinkle: ((tick + seeded) % 20) / 20,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return stars;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getAnimatedAnimals(state, width, tick = 0) {
|
|
53
|
+
return state.animals.map((animal, index) => {
|
|
54
|
+
const speed = SPEEDS[animal.type] ?? 0.08;
|
|
55
|
+
const direction = animal.direction === "left" ? -1 : 1;
|
|
56
|
+
const rawX = animal.x_position + direction * tick * speed;
|
|
57
|
+
const x = wrap(rawX, -6, width + 6);
|
|
58
|
+
return {
|
|
59
|
+
...animal,
|
|
60
|
+
x,
|
|
61
|
+
y:
|
|
62
|
+
animal.type === "butterfly"
|
|
63
|
+
? 6 + Math.round(Math.sin((tick + index * 5) / 4))
|
|
64
|
+
: animal.type === "owl"
|
|
65
|
+
? 5 + Math.round(Math.sin((tick + index * 9) / 8))
|
|
66
|
+
: 11,
|
|
67
|
+
bob:
|
|
68
|
+
animal.type === "rabbit"
|
|
69
|
+
? Math.abs(Math.sin((tick + index * 6) / 3))
|
|
70
|
+
: Math.abs(Math.sin((tick + index * 7) / 6)) * 0.35,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getWeatherParticles(width, height, tick = 0, weather = "clear", season = "summer") {
|
|
76
|
+
if (weather === "rain") {
|
|
77
|
+
return Array.from({ length: Math.max(10, Math.floor(width / 4)) }, (_, index) => ({
|
|
78
|
+
kind: "rain",
|
|
79
|
+
x: (index * 11 + tick * 2) % width,
|
|
80
|
+
y: (index * 7 + tick) % height,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (weather === "aurora") {
|
|
85
|
+
return Array.from({ length: width }, (_, index) => ({
|
|
86
|
+
kind: "aurora",
|
|
87
|
+
x: index,
|
|
88
|
+
y: 1 + Math.sin((tick + index) / 8) * 1.5,
|
|
89
|
+
alpha: 0.2 + ((index + tick) % 10) / 30,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const particleKind =
|
|
94
|
+
season === "winter" ? "snow" : season === "spring" ? "petal" : season === "autumn" ? "leaf" : "firefly";
|
|
95
|
+
const count = season === "summer" ? Math.max(6, Math.floor(width / 10)) : Math.max(8, Math.floor(width / 8));
|
|
96
|
+
|
|
97
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
98
|
+
kind: particleKind,
|
|
99
|
+
x: wrap(index * 9 + tick * (particleKind === "firefly" ? 0.25 : 0.45), 0, width - 1),
|
|
100
|
+
y:
|
|
101
|
+
particleKind === "firefly"
|
|
102
|
+
? 6 + Math.sin((tick + index * 7) / 3) * 2
|
|
103
|
+
: (index * 5 + tick * 0.7) % height,
|
|
104
|
+
alpha: 0.3 + (Math.sin((tick + index) / 5) + 1) / 3,
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getLlamaAnimation(width, tick = 0, lastSaveTick = -Infinity) {
|
|
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) {
|
|
127
|
+
return {
|
|
128
|
+
clouds: getClouds(width, tick, environment.sky.name),
|
|
129
|
+
stars: environment.sky.name === "night" ? getStars(width, tick) : [],
|
|
130
|
+
animals: getAnimatedAnimals(state, width, tick),
|
|
131
|
+
particles: getWeatherParticles(width, 12, tick, environment.weather.name, environment.season.name),
|
|
132
|
+
llama: getLlamaAnimation(width, tick, lastSaveTick),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
const MINUTES_PER_DAY = 24 * 60;
|
|
2
|
+
const TRANSITION_MINUTES = 30;
|
|
3
|
+
|
|
4
|
+
const SKY_PHASES = {
|
|
5
|
+
sunrise: {
|
|
6
|
+
top: "#f48c6c",
|
|
7
|
+
mid: "#f7b267",
|
|
8
|
+
bottom: "#ffd7a1",
|
|
9
|
+
haze: "#ffe6c7",
|
|
10
|
+
celestial: "#ffd166",
|
|
11
|
+
label: "Sunrise",
|
|
12
|
+
},
|
|
13
|
+
day: {
|
|
14
|
+
top: "#77b7e8",
|
|
15
|
+
mid: "#9fd0f5",
|
|
16
|
+
bottom: "#dff1ff",
|
|
17
|
+
haze: "#ffffff",
|
|
18
|
+
celestial: "#ffe066",
|
|
19
|
+
label: "Day",
|
|
20
|
+
},
|
|
21
|
+
sunset: {
|
|
22
|
+
top: "#5c2a72",
|
|
23
|
+
mid: "#d66f6f",
|
|
24
|
+
bottom: "#ffbf69",
|
|
25
|
+
haze: "#ffd6a5",
|
|
26
|
+
celestial: "#ffd166",
|
|
27
|
+
label: "Sunset",
|
|
28
|
+
},
|
|
29
|
+
night: {
|
|
30
|
+
top: "#08111f",
|
|
31
|
+
mid: "#10223d",
|
|
32
|
+
bottom: "#16314f",
|
|
33
|
+
haze: "#b9d6ff",
|
|
34
|
+
celestial: "#f5f3d7",
|
|
35
|
+
label: "Night",
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const WEATHER_TIERS = [
|
|
40
|
+
{ maxStreak: 2, name: "clear", label: "Clear", icon: "☀" },
|
|
41
|
+
{ maxStreak: 6, name: "rain", label: "Gentle Rain", icon: "🌧" },
|
|
42
|
+
{ maxStreak: 13, name: "rainbow", label: "Rainbow", icon: "🌈" },
|
|
43
|
+
{ maxStreak: 29, name: "golden-hour", label: "Golden Hour", icon: "🌇" },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const SEASON_META = {
|
|
47
|
+
spring: { label: "Spring", icon: "🌱", particle: "petal" },
|
|
48
|
+
summer: { label: "Summer", icon: "☀", particle: "firefly" },
|
|
49
|
+
autumn: { label: "Autumn", icon: "🍂", particle: "leaf" },
|
|
50
|
+
winter: { label: "Winter", icon: "❄", particle: "snow" },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const BASE_PALETTE = {
|
|
54
|
+
trunkDark: "#5f3c24",
|
|
55
|
+
trunk: "#7c5231",
|
|
56
|
+
trunkLight: "#a06b45",
|
|
57
|
+
birchBark: "#f4f0e7",
|
|
58
|
+
birchShadow: "#d6d0c3",
|
|
59
|
+
leafDark: "#25543b",
|
|
60
|
+
leaf: "#3b7a57",
|
|
61
|
+
leafLight: "#67b07d",
|
|
62
|
+
pineDark: "#183f32",
|
|
63
|
+
pine: "#2d6a4f",
|
|
64
|
+
petal: "#ff8fab",
|
|
65
|
+
petalLight: "#ffcad4",
|
|
66
|
+
moss: "#799f55",
|
|
67
|
+
ground: "#447055",
|
|
68
|
+
groundDark: "#345344",
|
|
69
|
+
groundLight: "#5d8d69",
|
|
70
|
+
grass: "#69a65d",
|
|
71
|
+
grassLight: "#8bcb73",
|
|
72
|
+
flower: "#ff7aa2",
|
|
73
|
+
flowerLight: "#ffd3df",
|
|
74
|
+
flowerCenter: "#ffd166",
|
|
75
|
+
mushroomCap: "#cb6d51",
|
|
76
|
+
mushroomStem: "#f5e6d0",
|
|
77
|
+
stone: "#7c7f88",
|
|
78
|
+
wing: "#f6d365",
|
|
79
|
+
wingLight: "#fff1a8",
|
|
80
|
+
rabbit: "#d8d3cd",
|
|
81
|
+
rabbitLight: "#f4efea",
|
|
82
|
+
fox: "#d56b2d",
|
|
83
|
+
foxLight: "#f2c48f",
|
|
84
|
+
deer: "#8d5c3b",
|
|
85
|
+
deerLight: "#cba37b",
|
|
86
|
+
owl: "#5f5b6b",
|
|
87
|
+
owlLight: "#d8d7dd",
|
|
88
|
+
llama: "#e8ddd0",
|
|
89
|
+
llamaLight: "#f5f0ea",
|
|
90
|
+
llamaDark: "#c4b5a3",
|
|
91
|
+
llamaNose: "#f5a0b0",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const SEASON_OVERRIDES = {
|
|
95
|
+
spring: {
|
|
96
|
+
leafDark: "#2d6a4f",
|
|
97
|
+
leaf: "#40916c",
|
|
98
|
+
leafLight: "#74c69d",
|
|
99
|
+
ground: "#4a7c59",
|
|
100
|
+
groundDark: "#355742",
|
|
101
|
+
groundLight: "#6aa46d",
|
|
102
|
+
grass: "#74b86a",
|
|
103
|
+
grassLight: "#a4d98f",
|
|
104
|
+
flower: "#ff6f91",
|
|
105
|
+
flowerLight: "#ffd6e0",
|
|
106
|
+
petal: "#ff8fab",
|
|
107
|
+
petalLight: "#ffc2d1",
|
|
108
|
+
},
|
|
109
|
+
summer: {
|
|
110
|
+
leafDark: "#1f5a3f",
|
|
111
|
+
leaf: "#2d6a4f",
|
|
112
|
+
leafLight: "#52b788",
|
|
113
|
+
pineDark: "#133828",
|
|
114
|
+
pine: "#245642",
|
|
115
|
+
ground: "#3e6f4d",
|
|
116
|
+
groundDark: "#2c523a",
|
|
117
|
+
groundLight: "#5b9167",
|
|
118
|
+
grass: "#67a957",
|
|
119
|
+
grassLight: "#8ed36b",
|
|
120
|
+
},
|
|
121
|
+
autumn: {
|
|
122
|
+
leafDark: "#8b5a2b",
|
|
123
|
+
leaf: "#c97b39",
|
|
124
|
+
leafLight: "#f2b95d",
|
|
125
|
+
pineDark: "#35553a",
|
|
126
|
+
pine: "#4d724e",
|
|
127
|
+
ground: "#6b4f2d",
|
|
128
|
+
groundDark: "#4d381f",
|
|
129
|
+
groundLight: "#8f6938",
|
|
130
|
+
grass: "#8e6b2d",
|
|
131
|
+
grassLight: "#c49643",
|
|
132
|
+
flower: "#c2543f",
|
|
133
|
+
flowerLight: "#f0c29d",
|
|
134
|
+
},
|
|
135
|
+
winter: {
|
|
136
|
+
leafDark: "#8ea3b0",
|
|
137
|
+
leaf: "#b0c4cf",
|
|
138
|
+
leafLight: "#dce7ee",
|
|
139
|
+
pineDark: "#2f4b4e",
|
|
140
|
+
pine: "#4c7176",
|
|
141
|
+
ground: "#e4edf2",
|
|
142
|
+
groundDark: "#c8d5de",
|
|
143
|
+
groundLight: "#f5fbff",
|
|
144
|
+
grass: "#dbe6ee",
|
|
145
|
+
grassLight: "#f7fbff",
|
|
146
|
+
flower: "#b4c7d8",
|
|
147
|
+
flowerLight: "#eef6fb",
|
|
148
|
+
petal: "#d7e4ee",
|
|
149
|
+
petalLight: "#f5fbff",
|
|
150
|
+
mushroomCap: "#c3ccd4",
|
|
151
|
+
stone: "#a3adb8",
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
function clamp(value, min, max) {
|
|
156
|
+
return Math.min(max, Math.max(min, value));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseHex(hex) {
|
|
160
|
+
const normalized = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
161
|
+
return {
|
|
162
|
+
r: Number.parseInt(normalized.slice(0, 2), 16),
|
|
163
|
+
g: Number.parseInt(normalized.slice(2, 4), 16),
|
|
164
|
+
b: Number.parseInt(normalized.slice(4, 6), 16),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function toHex({ r, g, b }) {
|
|
169
|
+
const encode = (value) => clamp(Math.round(value), 0, 255).toString(16).padStart(2, "0");
|
|
170
|
+
return `#${encode(r)}${encode(g)}${encode(b)}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function lerpColor(from, to, amount) {
|
|
174
|
+
const start = parseHex(from);
|
|
175
|
+
const end = parseHex(to);
|
|
176
|
+
const mix = clamp(amount, 0, 1);
|
|
177
|
+
return toHex({
|
|
178
|
+
r: start.r + (end.r - start.r) * mix,
|
|
179
|
+
g: start.g + (end.g - start.g) * mix,
|
|
180
|
+
b: start.b + (end.b - start.b) * mix,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function lerpPalette(a, b, amount) {
|
|
185
|
+
const result = {};
|
|
186
|
+
for (const key of Object.keys(a)) {
|
|
187
|
+
result[key] = lerpColor(a[key], b[key], amount);
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getMinutesIntoDay(date) {
|
|
193
|
+
return date.getHours() * 60 + date.getMinutes();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getBasePhase(minutes) {
|
|
197
|
+
if (minutes >= 360 && minutes < 540) {
|
|
198
|
+
return { name: "sunrise", start: 360, end: 540, previous: "night", next: "day" };
|
|
199
|
+
}
|
|
200
|
+
if (minutes >= 540 && minutes < 1020) {
|
|
201
|
+
return { name: "day", start: 540, end: 1020, previous: "sunrise", next: "sunset" };
|
|
202
|
+
}
|
|
203
|
+
if (minutes >= 1020 && minutes < 1200) {
|
|
204
|
+
return { name: "sunset", start: 1020, end: 1200, previous: "day", next: "night" };
|
|
205
|
+
}
|
|
206
|
+
return { name: "night", start: 1200, end: MINUTES_PER_DAY + 360, previous: "sunset", next: "sunrise" };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function getSkyPhase(date = new Date()) {
|
|
210
|
+
const minutes = getMinutesIntoDay(date);
|
|
211
|
+
const phase = getBasePhase(minutes);
|
|
212
|
+
const minutesInPhase = phase.name === "night" && minutes < 360 ? minutes + MINUTES_PER_DAY - 1200 : minutes - phase.start;
|
|
213
|
+
const duration = phase.end - phase.start;
|
|
214
|
+
const progress = clamp(minutesInPhase / duration, 0, 1);
|
|
215
|
+
|
|
216
|
+
let colors = SKY_PHASES[phase.name];
|
|
217
|
+
if (minutesInPhase < TRANSITION_MINUTES) {
|
|
218
|
+
colors = lerpPalette(
|
|
219
|
+
SKY_PHASES[phase.previous],
|
|
220
|
+
SKY_PHASES[phase.name],
|
|
221
|
+
minutesInPhase / TRANSITION_MINUTES,
|
|
222
|
+
);
|
|
223
|
+
} else if (minutesInPhase > duration - TRANSITION_MINUTES) {
|
|
224
|
+
colors = lerpPalette(
|
|
225
|
+
SKY_PHASES[phase.name],
|
|
226
|
+
SKY_PHASES[phase.next],
|
|
227
|
+
(minutesInPhase - (duration - TRANSITION_MINUTES)) / TRANSITION_MINUTES,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
name: phase.name,
|
|
233
|
+
label: SKY_PHASES[phase.name].label,
|
|
234
|
+
progress,
|
|
235
|
+
...colors,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function getSeason(date = new Date()) {
|
|
240
|
+
const month = date.getMonth();
|
|
241
|
+
if (month >= 2 && month <= 4) return "spring";
|
|
242
|
+
if (month >= 5 && month <= 7) return "summer";
|
|
243
|
+
if (month >= 8 && month <= 10) return "autumn";
|
|
244
|
+
return "winter";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function getWeather(streak = 0, date = new Date()) {
|
|
248
|
+
const sky = getSkyPhase(date);
|
|
249
|
+
const tier = WEATHER_TIERS.find((entry) => streak <= entry.maxStreak);
|
|
250
|
+
if (tier) {
|
|
251
|
+
return { ...tier };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (sky.name === "night") {
|
|
255
|
+
return { name: "aurora", label: "Aurora", icon: "✨" };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { name: "golden-hour", label: "Golden Hour", icon: "🌇" };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function getSeasonPalette(season) {
|
|
262
|
+
return {
|
|
263
|
+
...BASE_PALETTE,
|
|
264
|
+
...(SEASON_OVERRIDES[season] ?? {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function getEnvironmentSnapshot(date = new Date(), streak = 0) {
|
|
269
|
+
const season = getSeason(date);
|
|
270
|
+
const sky = getSkyPhase(date);
|
|
271
|
+
const weather = getWeather(streak, date);
|
|
272
|
+
return {
|
|
273
|
+
season: {
|
|
274
|
+
name: season,
|
|
275
|
+
...SEASON_META[season],
|
|
276
|
+
},
|
|
277
|
+
sky,
|
|
278
|
+
weather,
|
|
279
|
+
palette: getSeasonPalette(season),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Legacy API compatibility functions for simpler use cases
|
|
284
|
+
|
|
285
|
+
export function getSkyColors(phase) {
|
|
286
|
+
const colors = SKY_PHASES[phase] ?? SKY_PHASES.day;
|
|
287
|
+
return {
|
|
288
|
+
top: colors.top,
|
|
289
|
+
mid: colors.mid,
|
|
290
|
+
bottom: colors.bottom,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function getSeasonalPalette(season) {
|
|
295
|
+
return getSeasonPalette(season);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function getCurrentEnvironment() {
|
|
299
|
+
const now = new Date();
|
|
300
|
+
const season = getSeason(now);
|
|
301
|
+
const sky = getSkyPhase(now);
|
|
302
|
+
const skyColors = getSkyColors(sky.name);
|
|
303
|
+
const palette = getSeasonalPalette(season);
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
season,
|
|
307
|
+
skyPhase: sky.name,
|
|
308
|
+
skyColors,
|
|
309
|
+
palette,
|
|
310
|
+
hour: now.getHours(),
|
|
311
|
+
month: now.getMonth(),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function getWindIntensity(tick) {
|
|
316
|
+
// Natural wind using overlapping sine waves for realistic gusts
|
|
317
|
+
const slow = Math.sin(tick * 0.002) * 0.5 + 0.5;
|
|
318
|
+
const fast = Math.sin(tick * 0.013) * 0.3 + 0.5;
|
|
319
|
+
const gust = Math.sin(tick * 0.037) * 0.2 + 0.5;
|
|
320
|
+
|
|
321
|
+
// Combine and normalize to 0-1 range
|
|
322
|
+
return clamp((slow * 0.5 + fast * 0.3 + gust * 0.2), 0, 1);
|
|
323
|
+
}
|
|
324
|
+
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { addMinutes } from "date-fns";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getAnimalTypeForMinutes,
|
|
5
|
+
getAnimalSprite,
|
|
6
|
+
getGroundElementSprite,
|
|
7
|
+
getGroundElementType,
|
|
8
|
+
getTreeSpeciesForCommit,
|
|
9
|
+
getTreeSprite,
|
|
10
|
+
} from "./sprites.js";
|
|
11
|
+
|
|
12
|
+
function hashString(value) {
|
|
13
|
+
let hash = 0;
|
|
14
|
+
for (const char of value) {
|
|
15
|
+
hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
|
|
16
|
+
}
|
|
17
|
+
return hash;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function seededNumber(seed) {
|
|
21
|
+
let value = hashString(String(seed));
|
|
22
|
+
value = Math.imul(value ^ (value >>> 15), 0x2c1b3c6d) >>> 0;
|
|
23
|
+
value = Math.imul(value ^ (value >>> 12), 0x297a2d39) >>> 0;
|
|
24
|
+
return (value >>> 0) / 0xffffffff;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function dayString(date) {
|
|
28
|
+
return date.toISOString().slice(0, 10);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function dayNumber(value) {
|
|
32
|
+
const [year, month, day] = String(value).split("-").map(Number);
|
|
33
|
+
return Date.UTC(year, month - 1, day) / 86_400_000;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function placeSprite(existing, spriteWidth, width, key) {
|
|
37
|
+
const safeWidth = Math.max(width, 40);
|
|
38
|
+
const half = Math.floor(spriteWidth / 2);
|
|
39
|
+
const minX = half + 1;
|
|
40
|
+
const maxX = Math.max(minX, safeWidth - half - 2);
|
|
41
|
+
|
|
42
|
+
for (let attempt = 0; attempt < 24; attempt += 1) {
|
|
43
|
+
const rand = seededNumber(`${key}:${attempt}`);
|
|
44
|
+
const x = Math.round(minX + rand * (maxX - minX));
|
|
45
|
+
const collides = existing.some((entry) => Math.abs(entry.x_position - x) < half + 2);
|
|
46
|
+
if (!collides || existing.length > Math.floor(safeWidth / 4)) {
|
|
47
|
+
return x;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return minX + (existing.length * 7) % Math.max(1, maxX - minX + 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getNextMinuteCheckpoint(totalMinutes) {
|
|
55
|
+
return Math.ceil((totalMinutes + 1) / 30) * 30;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function updateCommitStreak(currentStreak, lastActiveDate, now = new Date()) {
|
|
59
|
+
if (currentStreak <= 0 || !lastActiveDate) {
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const gap = dayNumber(dayString(now)) - dayNumber(lastActiveDate);
|
|
63
|
+
if (gap <= 0) {
|
|
64
|
+
return Math.max(1, currentStreak);
|
|
65
|
+
}
|
|
66
|
+
if (gap === 1) {
|
|
67
|
+
return Math.max(1, currentStreak) + 1;
|
|
68
|
+
}
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function applyCommit(state, { commitHash = "", now = new Date(), width = 80 } = {}) {
|
|
73
|
+
const nextTotal = state.total_commits + 1;
|
|
74
|
+
const species = getTreeSpeciesForCommit(nextTotal);
|
|
75
|
+
const sprite = getTreeSprite(species);
|
|
76
|
+
state.total_commits = nextTotal;
|
|
77
|
+
state.current_streak = updateCommitStreak(state.current_streak, state.last_active_date, now);
|
|
78
|
+
state.last_active_date = dayString(now);
|
|
79
|
+
state.last_commit_hash = commitHash || state.last_commit_hash;
|
|
80
|
+
state.trees.push({
|
|
81
|
+
species,
|
|
82
|
+
planted_at: now.toISOString(),
|
|
83
|
+
x_position: placeSprite(state.trees, sprite.width, width, `${species}:${nextTotal}`),
|
|
84
|
+
});
|
|
85
|
+
return state;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function applyFileSave(state, { now = new Date(), width = 80 } = {}) {
|
|
89
|
+
state.total_file_saves += 1;
|
|
90
|
+
if (state.total_file_saves % 50 === 0) {
|
|
91
|
+
const type = getGroundElementType(state.total_file_saves);
|
|
92
|
+
const sprite = getGroundElementSprite(type);
|
|
93
|
+
state.ground_elements.push({
|
|
94
|
+
type,
|
|
95
|
+
x_position: placeSprite(
|
|
96
|
+
state.ground_elements,
|
|
97
|
+
sprite.width,
|
|
98
|
+
width,
|
|
99
|
+
`${type}:${state.total_file_saves}`,
|
|
100
|
+
),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return state;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function applyActiveMinutes(state, { minutes = 1, now = new Date(), width = 80 } = {}) {
|
|
107
|
+
const before = state.total_minutes_coded;
|
|
108
|
+
const after = before + minutes;
|
|
109
|
+
state.total_minutes_coded = after;
|
|
110
|
+
|
|
111
|
+
let checkpoint = Math.floor(before / 30) * 30 + 30;
|
|
112
|
+
while (checkpoint <= after) {
|
|
113
|
+
const type = getAnimalTypeForMinutes(checkpoint);
|
|
114
|
+
const sprite = getAnimalSprite(type);
|
|
115
|
+
state.animals.push({
|
|
116
|
+
type,
|
|
117
|
+
x_position: placeSprite(state.animals, sprite.width, width, `${type}:${checkpoint}`),
|
|
118
|
+
direction: state.animals.length % 2 === 0 ? "right" : "left",
|
|
119
|
+
});
|
|
120
|
+
checkpoint += 30;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return state;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function applyActivityPulse(state, { now = new Date() } = {}) {
|
|
127
|
+
const updated = addMinutes(now, 0);
|
|
128
|
+
return state;
|
|
129
|
+
}
|