-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpatrol-engine.ts
More file actions
208 lines (170 loc) · 9.23 KB
/
patrol-engine.ts
File metadata and controls
208 lines (170 loc) · 9.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import type { PetState } from './engine.js';
import { PERSONALITY_PHRASES, PATROL_CONFIG, monthNames } from './config.js';
import { parseDateParts, getCommitCount, getL2Threshold } from './dom-utils.js';
/** Caches pools by month/year to avoid repeated filtering on every move */
const poolCache = new Map<string, HTMLElement[]>();
let lastViewedYear: string | null = null;
export function getPatrolPool(allDays: HTMLElement[], targetMonthName: string, targetYear: string): HTMLElement[] {
const cacheKey = `${targetMonthName}-${targetYear}`;
const cached = poolCache.get(cacheKey);
// Invalidate cache if elements are detached from the DOM
if (cached && cached.length > 0 && document.body.contains(cached[0])) {
return cached;
}
const now = new Date();
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
const targetMonthIndex = monthNames.indexOf(targetMonthName);
const yearNum = parseInt(targetYear, 10);
const monthStart = new Date(yearNum, targetMonthIndex, 1);
const monthEnd = new Date(yearNum, targetMonthIndex + 1, 0);
const bufferStart = new Date(monthStart);
bufferStart.setDate(bufferStart.getDate() - 4);
const bufferEnd = new Date(monthEnd);
bufferEnd.setDate(bufferEnd.getDate() + 4);
const pool = allDays.filter(day => {
const dateStr = day.getAttribute('data-date');
if (!dateStr) return false;
// Quick string comparison for date filtering
const isNotFuture = dateStr <= todayStr;
if (!isNotFuture) return false;
const [y, m, d] = dateStr.split('-').map(v => parseInt(v, 10));
const date = new Date(y, m - 1, d);
return date >= bufferStart && date <= bufferEnd;
});
poolCache.set(cacheKey, pool);
return pool;
}
export function startPatrol(petElement: HTMLElement, petState: PetState): void {
// Extract ID parts once to handle hyphenated usernames robustly
const idParts = petElement.id.replace('pet-', '').split('-');
const targetMonthName = idParts.find(p => monthNames.includes(p)) || "";
const targetYear = idParts.find(p => /^\d{4}$/.test(p)) || "";
function moveToRandomDay(): boolean {
// Use current parent as container (always set to relative by content.ts)
const graphContainer = petElement.parentElement;
if (!graphContainer || !document.body.contains(graphContainer)) return false;
const allDays = Array.from(graphContainer.querySelectorAll('.ContributionCalendar-day')) as HTMLElement[];
if (allDays.length === 0) return false;
let patrolPool = getPatrolPool(allDays, targetMonthName, targetYear);
if (patrolPool.length === 0) patrolPool = allDays;
const targetDay = patrolPool[Math.floor(Math.random() * patrolPool.length)];
if (!targetDay || !graphContainer.contains(targetDay)) {
poolCache.clear();
return false;
}
// Calculate position relative to parent container for robust cross-UI positioning
const parentRect = graphContainer.getBoundingClientRect();
const targetRect = targetDay.getBoundingClientRect();
// If the target has no size or parent has no size, skip movement (could be in a detached/hidden state)
if (targetRect.width === 0 || parentRect.width === 0) return false;
const targetX = (targetRect.left - parentRect.left) + (targetRect.width / 2) - 12;
const targetY = (targetRect.top - parentRect.top) + (targetRect.height / 2) - 12;
// 1. EXTRACT SQUARE COLOR
const style = window.getComputedStyle(targetDay);
const squareColor = style.getPropertyValue('--color-calendar-graph-day-bg') ||
style.fill || style.backgroundColor;
// 2. DYNAMIC GLOW
const currentStyle = petElement.getAttribute('style') || '';
const hueRotateMatch = currentStyle.match(/hue-rotate\([^)]+\)/);
const filterStr = hueRotateMatch ? `${hueRotateMatch[0]} ` : '';
petElement.style.filter = `${filterStr}drop-shadow(0 0 12px ${squareColor})`.trim();
const visual = petElement.querySelector('.pet-visual') as HTMLElement;
if (visual) {
petElement.classList.remove('is-eating');
void petElement.offsetWidth;
petElement.classList.add('is-eating');
setTimeout(() => { if (petElement) petElement.classList.remove('is-eating'); }, 1000);
}
// 3. WINKING
const eyes = petElement.querySelector('.pet-eyes') as HTMLElement;
if (eyes && Math.random() > 0.85) {
eyes.classList.add('is-winking');
setTimeout(() => { if (eyes) eyes.classList.remove('is-winking'); }, 600);
}
const count = getCommitCount(targetDay);
const l2Threshold = getL2Threshold();
const level = parseInt(targetDay.getAttribute('data-level') || '0', 10);
petElement.classList.remove('mood-scared', 'mood-happy', 'mood-ecstatic');
let currentMood = 'happy';
if (count === 0) {
petElement.classList.add('mood-scared');
currentMood = 'scared';
} else if (level >= 3) {
petElement.classList.add('mood-ecstatic');
currentMood = 'ecstatic';
} else if (count >= l2Threshold) {
petElement.classList.add('mood-happy');
currentMood = 'happy';
}
petElement.classList.add('is-moving');
// Calculate look direction relative to current position
const currentX = parseFloat(petElement.style.left) || targetX;
const currentY = parseFloat(petElement.style.top) || targetY;
const dx = targetX - currentX;
const dy = targetY - currentY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 5) {
const lookX = (dx / dist) * 2;
const lookY = (dy / dist) * 2;
petElement.style.setProperty('--look-x', `${lookX}px`);
petElement.style.setProperty('--look-y', `${lookY}px`);
}
petElement.style.left = `${targetX}px`;
petElement.style.top = `${targetY}px`;
if (Math.random() > PATROL_CONFIG.speechProbability) {
const speech = petElement.querySelector('#pet-speech') as HTMLElement;
if (speech && petState.personality && PERSONALITY_PHRASES[petState.personality]) {
let phrase = "";
if (Math.random() > 0.7 && (currentMood === 'happy' || currentMood === 'ecstatic')) {
const nextConsistency = petState.evolutionTier === 0 ? 7 : (petState.evolutionTier === 1 ? 14 : 21);
const nextCommits = petState.evolutionTier === 0 ? 20 : (petState.evolutionTier === 1 ? 50 : 100);
if (petState.evolutionTier < 3 && Math.random() > 0.5) {
const daysLeft = Math.max(0, nextConsistency - petState.dnaLength);
const commitsLeft = Math.max(0, nextCommits - petState.totalCommits);
if (daysLeft > 0 && (Math.random() > 0.5 || commitsLeft <= 0)) {
phrase = `Only ${daysLeft} more days until I grow!`;
} else if (commitsLeft > 0) {
phrase = `I need about ${commitsLeft} more commits to evolve!`;
}
} else if (petState.complexity < 5) {
const nextComplexityCommits = (petState.complexity + 1) * 15;
const complexityLeft = Math.max(0, nextComplexityCommits - petState.totalCommits);
if (complexityLeft > 0) {
phrase = `Feed me ${complexityLeft} more commits for more complexity!`;
}
}
}
if (!phrase) {
const phrases = PERSONALITY_PHRASES[petState.personality][currentMood];
if (phrases) {
phrase = phrases[Math.floor(Math.random() * phrases.length)];
}
}
if (phrase) {
speech.textContent = phrase;
speech.style.display = 'block';
setTimeout(() => { if (speech) speech.style.display = 'none'; }, 2500);
}
}
}
setTimeout(() => { if (petElement) petElement.classList.remove('is-moving'); }, PATROL_CONFIG.moveDuration);
return true;
}
function scheduleNextMove(delayOverride?: number) {
const delay = delayOverride || (PATROL_CONFIG.baseInterval + Math.random() * PATROL_CONFIG.randomVariance);
setTimeout(() => {
if (!document.body.contains(petElement)) return;
if (moveToRandomDay()) {
scheduleNextMove(); // Success, use normal delay
} else {
scheduleNextMove(500); // Failed (empty graph or zero size), retry soon
}
}, delay);
}
// Start the cycle
if (moveToRandomDay()) {
scheduleNextMove();
} else {
scheduleNextMove(500); // Retry soon if initial move failed
}
}