pacman-contribution-graph 1.0.14 → 2.0.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/README.md +59 -9
- package/dist/pacman-contribution-graph.min.js +1 -1
- package/package.json +44 -26
- package/.prettierrc +0 -8
- package/.vscode/extensions.json +0 -5
- package/.vscode/settings.json +0 -6
- package/assets/packman-demo.png +0 -0
- package/dist/pacman-contribution-graph.js +0 -1776
- package/dist/pacman-contribution-graph.js.map +0 -1
- package/embeded/canvas.html +0 -41
- package/github-action/action.yml +0 -16
- package/github-action/dist/index.js +0 -26901
- package/github-action/package.json +0 -14
- package/github-action/pnpm-lock.yaml +0 -77
- package/github-action/src/index.js +0 -47
- package/index.html +0 -528
- package/pacman.abozanona.me/index.js +0 -47
- package/pacman.abozanona.me/package.json +0 -8
- package/server/api/contributions/route.ts.z +0 -31
- package/server/page.zts.z +0 -13
- package/src/assets/images/ghosts/blinky.png +0 -0
- package/src/assets/images/ghosts/clyde.png +0 -0
- package/src/assets/images/ghosts/inky.png +0 -0
- package/src/assets/images/ghosts/pinky.png +0 -0
- package/src/assets/images/ghosts/scared.png +0 -0
- package/src/assets/sounds/pacman_beginning.wav +0 -0
- package/src/assets/sounds/pacman_chomp.wav +0 -0
- package/src/assets/sounds/pacman_death.wav +0 -0
- package/src/assets/sounds/pacman_eatghost.wav +0 -0
- package/src/canvas.ts +0 -244
- package/src/constants.ts +0 -102
- package/src/game.ts +0 -231
- package/src/grid.ts +0 -127
- package/src/index.ts +0 -48
- package/src/movement/ghosts-movement.ts +0 -183
- package/src/movement/movement-utils.ts +0 -40
- package/src/movement/pacman-movement.ts +0 -230
- package/src/music-player.ts +0 -119
- package/src/store.ts +0 -23
- package/src/svg.ts +0 -254
- package/src/types.ts +0 -78
- package/src/utils.ts +0 -81
- package/tsconfig.json +0 -11
- package/webpack.common.js +0 -19
- package/webpack.dev.js +0 -23
- package/webpack.prod.js +0 -18
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { GRID_HEIGHT, GRID_WIDTH, PACMAN_POWERUP_DURATION } from '../constants';
|
|
2
|
-
import { Point2d, StoreType } from '../types';
|
|
3
|
-
import { MovementUtils } from './movement-utils';
|
|
4
|
-
|
|
5
|
-
const RECENT_POSITIONS_LIMIT = 5;
|
|
6
|
-
|
|
7
|
-
const movePacman = (store: StoreType) => {
|
|
8
|
-
if (store.pacman.deadRemainingDuration) {
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const hasPowerup = !!store.pacman.powerupRemainingDuration;
|
|
13
|
-
const scaredGhosts = store.ghosts.filter((ghost) => ghost.scared);
|
|
14
|
-
|
|
15
|
-
let targetPosition: Point2d;
|
|
16
|
-
|
|
17
|
-
if (hasPowerup && scaredGhosts.length > 0) {
|
|
18
|
-
const ghostPosition = findClosestScaredGhost(store);
|
|
19
|
-
if (ghostPosition) {
|
|
20
|
-
targetPosition = ghostPosition;
|
|
21
|
-
} else {
|
|
22
|
-
targetPosition = findOptimalTarget(store);
|
|
23
|
-
}
|
|
24
|
-
} else if (store.pacman.target) {
|
|
25
|
-
if (store.pacman.target.x == store.pacman.x && store.pacman.target.y == store.pacman.y) {
|
|
26
|
-
targetPosition = findOptimalTarget(store);
|
|
27
|
-
store.pacman.target = { x: targetPosition?.x, y: targetPosition?.y };
|
|
28
|
-
} else {
|
|
29
|
-
targetPosition = store.pacman.target;
|
|
30
|
-
}
|
|
31
|
-
} else {
|
|
32
|
-
targetPosition = findOptimalTarget(store);
|
|
33
|
-
store.pacman.target = { x: targetPosition?.x, y: targetPosition?.y };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const nextPosition = calculateOptimalPath(store, targetPosition);
|
|
37
|
-
|
|
38
|
-
if (nextPosition) {
|
|
39
|
-
updatePacmanPosition(store, nextPosition);
|
|
40
|
-
} else {
|
|
41
|
-
makeDesperationMove(store);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
checkAndEatPoint(store);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const findClosestScaredGhost = (store: StoreType) => {
|
|
48
|
-
const scaredGhosts = store.ghosts.filter((ghost) => ghost.scared);
|
|
49
|
-
|
|
50
|
-
if (scaredGhosts.length === 0) return null;
|
|
51
|
-
|
|
52
|
-
return scaredGhosts.reduce(
|
|
53
|
-
(closest, ghost) => {
|
|
54
|
-
const distance = MovementUtils.calculateDistance(ghost.x, ghost.y, store.pacman.x, store.pacman.y);
|
|
55
|
-
return distance < closest.distance ? { x: ghost.x, y: ghost.y, distance } : closest;
|
|
56
|
-
},
|
|
57
|
-
{ x: store.pacman.x, y: store.pacman.y, distance: Infinity }
|
|
58
|
-
);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const findOptimalTarget = (store: StoreType) => {
|
|
62
|
-
let pointCells: { x: number; y: number; value: number }[] = [];
|
|
63
|
-
|
|
64
|
-
for (let x = 0; x < GRID_WIDTH; x++) {
|
|
65
|
-
for (let y = 0; y < GRID_HEIGHT; y++) {
|
|
66
|
-
if (store.grid[x][y].intensity > 0) {
|
|
67
|
-
const distance = MovementUtils.calculateDistance(x, y, store.pacman.x, store.pacman.y);
|
|
68
|
-
const value = store.grid[x][y].intensity / (distance + 1);
|
|
69
|
-
pointCells.push({ x, y, value });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
pointCells.sort((a, b) => b.value - a.value);
|
|
75
|
-
return pointCells[0];
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const calculateOptimalPath = (store: StoreType, target: Point2d) => {
|
|
79
|
-
const queue: { x: number; y: number; path: Point2d[]; score: number }[] = [
|
|
80
|
-
{ x: store.pacman.x, y: store.pacman.y, path: [], score: 0 }
|
|
81
|
-
];
|
|
82
|
-
const visited = new Set<string>();
|
|
83
|
-
visited.add(`${store.pacman.x},${store.pacman.y}`);
|
|
84
|
-
|
|
85
|
-
const dangerMap = createDangerMap(store);
|
|
86
|
-
|
|
87
|
-
while (queue.length > 0) {
|
|
88
|
-
queue.sort((a, b) => b.score - a.score);
|
|
89
|
-
const current = queue.shift()!;
|
|
90
|
-
const { x, y, path } = current;
|
|
91
|
-
|
|
92
|
-
if (x === target.x && y === target.y) {
|
|
93
|
-
return path.length > 0 ? path[0] : null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const validMoves = MovementUtils.getValidMoves(x, y);
|
|
97
|
-
|
|
98
|
-
for (const [dx, dy] of validMoves) {
|
|
99
|
-
const newX = x + dx;
|
|
100
|
-
const newY = y + dy;
|
|
101
|
-
const key = `${newX},${newY}`;
|
|
102
|
-
|
|
103
|
-
if (!visited.has(key)) {
|
|
104
|
-
const newPath = [...path, { x: newX, y: newY }];
|
|
105
|
-
const danger = dangerMap.get(key) || 0;
|
|
106
|
-
const pointValue = store.grid[newX][newY].intensity;
|
|
107
|
-
const distanceToTarget = MovementUtils.calculateDistance(newX, newY, target.x, target.y);
|
|
108
|
-
|
|
109
|
-
let revisitPenalty = 0;
|
|
110
|
-
if (store.pacman.recentPositions?.includes(key)) {
|
|
111
|
-
revisitPenalty += 100; // Penalize recently visited positions
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
queue.push({
|
|
115
|
-
x: newX,
|
|
116
|
-
y: newY,
|
|
117
|
-
path: newPath,
|
|
118
|
-
score: pointValue - danger - distanceToTarget / 10 - revisitPenalty
|
|
119
|
-
});
|
|
120
|
-
visited.add(key);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return null;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const createDangerMap = (store: StoreType) => {
|
|
129
|
-
const dangerMap = new Map<string, number>();
|
|
130
|
-
const hasPowerup = !!store.pacman.powerupRemainingDuration;
|
|
131
|
-
|
|
132
|
-
store.ghosts.forEach((ghost) => {
|
|
133
|
-
if (ghost.scared) return;
|
|
134
|
-
|
|
135
|
-
for (let dx = -5; dx <= 5; dx++) {
|
|
136
|
-
for (let dy = -5; dy <= 5; dy++) {
|
|
137
|
-
const x = ghost.x + dx;
|
|
138
|
-
const y = ghost.y + dy;
|
|
139
|
-
|
|
140
|
-
if (x >= 0 && x < GRID_WIDTH && y >= 0 && y < GRID_HEIGHT) {
|
|
141
|
-
const key = `${x},${y}`;
|
|
142
|
-
const distance = Math.abs(dx) + Math.abs(dy);
|
|
143
|
-
const dangerValue = 15 - distance;
|
|
144
|
-
|
|
145
|
-
if (dangerValue > 0) {
|
|
146
|
-
const currentDanger = dangerMap.get(key) || 0;
|
|
147
|
-
dangerMap.set(key, Math.max(currentDanger, dangerValue));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
if (hasPowerup) {
|
|
155
|
-
for (const [key, value] of dangerMap.entries()) {
|
|
156
|
-
dangerMap.set(key, value / 5);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return dangerMap;
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const makeDesperationMove = (store: StoreType) => {
|
|
164
|
-
const validMoves = MovementUtils.getValidMoves(store.pacman.x, store.pacman.y);
|
|
165
|
-
|
|
166
|
-
if (validMoves.length === 0) return;
|
|
167
|
-
|
|
168
|
-
const safestMove = validMoves.reduce(
|
|
169
|
-
(safest, [dx, dy]) => {
|
|
170
|
-
const newX = store.pacman.x + dx;
|
|
171
|
-
const newY = store.pacman.y + dy;
|
|
172
|
-
|
|
173
|
-
let minGhostDistance = Infinity;
|
|
174
|
-
|
|
175
|
-
store.ghosts.forEach((ghost) => {
|
|
176
|
-
if (!ghost.scared) {
|
|
177
|
-
const distance = MovementUtils.calculateDistance(ghost.x, ghost.y, newX, newY);
|
|
178
|
-
minGhostDistance = Math.min(minGhostDistance, distance);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
return minGhostDistance > safest.distance ? { dx, dy, distance: minGhostDistance } : safest;
|
|
183
|
-
},
|
|
184
|
-
{ dx: 0, dy: 0, distance: -Infinity }
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const newX = store.pacman.x + safestMove.dx;
|
|
188
|
-
const newY = store.pacman.y + safestMove.dy;
|
|
189
|
-
|
|
190
|
-
updatePacmanPosition(store, { x: newX, y: newY });
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const updatePacmanPosition = (store: StoreType, position: Point2d) => {
|
|
194
|
-
store.pacman.recentPositions ||= [];
|
|
195
|
-
store.pacman.recentPositions.push(`${position.x},${position.y}`);
|
|
196
|
-
if (store.pacman.recentPositions.length > RECENT_POSITIONS_LIMIT) {
|
|
197
|
-
store.pacman.recentPositions.shift();
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const dx = position.x - store.pacman.x;
|
|
201
|
-
const dy = position.y - store.pacman.y;
|
|
202
|
-
|
|
203
|
-
if (dx > 0) store.pacman.direction = 'right';
|
|
204
|
-
else if (dx < 0) store.pacman.direction = 'left';
|
|
205
|
-
else if (dy > 0) store.pacman.direction = 'down';
|
|
206
|
-
else if (dy < 0) store.pacman.direction = 'up';
|
|
207
|
-
|
|
208
|
-
store.pacman.x = position.x;
|
|
209
|
-
store.pacman.y = position.y;
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const checkAndEatPoint = (store: StoreType) => {
|
|
213
|
-
if (store.grid[store.pacman.x][store.pacman.y].intensity > 0) {
|
|
214
|
-
store.pacman.totalPoints += store.grid[store.pacman.x][store.pacman.y].commitsCount;
|
|
215
|
-
store.pacman.points++;
|
|
216
|
-
store.config.pointsIncreasedCallback(store.pacman.totalPoints);
|
|
217
|
-
store.grid[store.pacman.x][store.pacman.y].intensity = 0;
|
|
218
|
-
|
|
219
|
-
if (store.pacman.points >= 10) activatePowerUp(store);
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const activatePowerUp = (store: StoreType) => {
|
|
224
|
-
store.pacman.powerupRemainingDuration = PACMAN_POWERUP_DURATION;
|
|
225
|
-
store.ghosts.forEach((ghost) => (ghost.scared = true));
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
export const PacmanMovement = {
|
|
229
|
-
movePacman
|
|
230
|
-
};
|
package/src/music-player.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
export enum Sound {
|
|
2
|
-
DEFAULT = 'https://cdn.jsdelivr.net/npm/pacman-contribution-graph/src/assets/sounds/pacman_chomp.wav',
|
|
3
|
-
BEGINNING = 'https://cdn.jsdelivr.net/npm/pacman-contribution-graph/src/assets/sounds/pacman_beginning.wav',
|
|
4
|
-
GAME_OVER = 'https://cdn.jsdelivr.net/npm/pacman-contribution-graph/src/assets/sounds/pacman_death.wav',
|
|
5
|
-
EAT_GHOST = 'https://cdn.jsdelivr.net/npm/pacman-contribution-graph/src/assets/sounds/pacman_eatghost.wav'
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class MusicPlayer {
|
|
9
|
-
private static instance: MusicPlayer;
|
|
10
|
-
private audioContext: AudioContext;
|
|
11
|
-
private sounds: Map<Sound, AudioBuffer> = new Map();
|
|
12
|
-
private currentSource: AudioBufferSourceNode | null = null;
|
|
13
|
-
private defaultSource: AudioBufferSourceNode | null = null;
|
|
14
|
-
public isMuted: boolean = false;
|
|
15
|
-
|
|
16
|
-
private constructor() {
|
|
17
|
-
this.audioContext = new AudioContext();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
public static getInstance(): MusicPlayer {
|
|
21
|
-
if (!MusicPlayer.instance) {
|
|
22
|
-
MusicPlayer.instance = new MusicPlayer();
|
|
23
|
-
}
|
|
24
|
-
return MusicPlayer.instance;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
public async preloadSounds(): Promise<void> {
|
|
28
|
-
for (const sound of Object.values(Sound)) {
|
|
29
|
-
const response = await fetch(sound);
|
|
30
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
31
|
-
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
32
|
-
this.sounds.set(sound as Sound, audioBuffer);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
public async play(sound: Sound): Promise<void> {
|
|
37
|
-
if (this.isMuted) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (this.currentSource) {
|
|
41
|
-
try {
|
|
42
|
-
this.currentSource.stop();
|
|
43
|
-
} catch (ex) {}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const buffer = this.sounds.get(sound);
|
|
47
|
-
if (!buffer) {
|
|
48
|
-
console.error(`Sound ${sound} not found`);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
this.currentSource = this.audioContext.createBufferSource();
|
|
53
|
-
this.currentSource.buffer = buffer;
|
|
54
|
-
this.currentSource.connect(this.audioContext.destination);
|
|
55
|
-
|
|
56
|
-
if (!this.isMuted) {
|
|
57
|
-
this.currentSource.start();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return new Promise((resolve) => {
|
|
61
|
-
this.currentSource!.onended = () => {
|
|
62
|
-
this.currentSource = null;
|
|
63
|
-
resolve();
|
|
64
|
-
};
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
public startDefaultSound(): void {
|
|
69
|
-
if (this.defaultSource) {
|
|
70
|
-
try {
|
|
71
|
-
this.defaultSource.stop();
|
|
72
|
-
} catch (ex) {}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const buffer = this.sounds.get(Sound.DEFAULT);
|
|
76
|
-
if (!buffer) {
|
|
77
|
-
console.error('Default sound not found');
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
this.defaultSource = this.audioContext.createBufferSource();
|
|
82
|
-
this.defaultSource.buffer = buffer;
|
|
83
|
-
this.defaultSource.loop = true;
|
|
84
|
-
this.defaultSource.connect(this.audioContext.destination);
|
|
85
|
-
|
|
86
|
-
if (!this.isMuted) {
|
|
87
|
-
this.defaultSource.start();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
public stopDefaultSound(): void {
|
|
92
|
-
if (this.defaultSource) {
|
|
93
|
-
try {
|
|
94
|
-
this.defaultSource.stop();
|
|
95
|
-
} catch (ex) {}
|
|
96
|
-
this.defaultSource = null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
public mute(): void {
|
|
101
|
-
this.isMuted = true;
|
|
102
|
-
if (this.currentSource) {
|
|
103
|
-
this.currentSource.disconnect();
|
|
104
|
-
}
|
|
105
|
-
if (this.defaultSource) {
|
|
106
|
-
this.defaultSource.disconnect();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
public unmute(): void {
|
|
111
|
-
this.isMuted = false;
|
|
112
|
-
if (this.currentSource) {
|
|
113
|
-
this.currentSource.connect(this.audioContext.destination);
|
|
114
|
-
}
|
|
115
|
-
if (this.defaultSource) {
|
|
116
|
-
this.defaultSource.connect(this.audioContext.destination);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
package/src/store.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { Config, StoreType } from './types';
|
|
2
|
-
|
|
3
|
-
export const Store: StoreType = {
|
|
4
|
-
frameCount: 0,
|
|
5
|
-
contributions: [],
|
|
6
|
-
pacman: {
|
|
7
|
-
x: 0,
|
|
8
|
-
y: 0,
|
|
9
|
-
direction: 'right',
|
|
10
|
-
points: 0,
|
|
11
|
-
totalPoints: 0,
|
|
12
|
-
deadRemainingDuration: 0,
|
|
13
|
-
powerupRemainingDuration: 0,
|
|
14
|
-
recentPositions: []
|
|
15
|
-
},
|
|
16
|
-
ghosts: [],
|
|
17
|
-
grid: [],
|
|
18
|
-
monthLabels: [],
|
|
19
|
-
pacmanMouthOpen: true,
|
|
20
|
-
gameInterval: 0,
|
|
21
|
-
gameHistory: [],
|
|
22
|
-
config: undefined as unknown as Config
|
|
23
|
-
};
|
package/src/svg.ts
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CELL_SIZE,
|
|
3
|
-
DELTA_TIME,
|
|
4
|
-
GAP_SIZE,
|
|
5
|
-
GHOSTS,
|
|
6
|
-
GRID_HEIGHT,
|
|
7
|
-
GRID_WIDTH,
|
|
8
|
-
PACMAN_COLOR,
|
|
9
|
-
PACMAN_COLOR_DEAD,
|
|
10
|
-
PACMAN_COLOR_POWERUP,
|
|
11
|
-
WALLS
|
|
12
|
-
} from './constants';
|
|
13
|
-
import { AnimationData, StoreType } from './types';
|
|
14
|
-
import { Utils } from './utils';
|
|
15
|
-
|
|
16
|
-
const generateAnimatedSVG = (store: StoreType) => {
|
|
17
|
-
const svgWidth = GRID_WIDTH * (CELL_SIZE + GAP_SIZE);
|
|
18
|
-
const svgHeight = GRID_HEIGHT * (CELL_SIZE + GAP_SIZE) + 20;
|
|
19
|
-
let svg = `<svg width="${svgWidth}" height="${svgHeight}" xmlns="http://www.w3.org/2000/svg">`;
|
|
20
|
-
svg += `<desc>Generated with https://github.com/abozanona/pacman-contribution-graph on ${new Date()}</desc>`;
|
|
21
|
-
svg += `<rect width="100%" height="100%" fill="${Utils.getCurrentTheme(store).gridBackground}"/>`;
|
|
22
|
-
|
|
23
|
-
svg += generateGhostsPredefinition();
|
|
24
|
-
|
|
25
|
-
// Month labels
|
|
26
|
-
let lastMonth = '';
|
|
27
|
-
for (let y = 0; y < GRID_WIDTH; y++) {
|
|
28
|
-
if (store.monthLabels[y] !== lastMonth) {
|
|
29
|
-
const xPos = y * (CELL_SIZE + GAP_SIZE) + CELL_SIZE / 2;
|
|
30
|
-
svg += `<text x="${xPos}" y="10" text-anchor="middle" font-size="10" fill="${Utils.getCurrentTheme(store).textColor}">${store.monthLabels[y]}</text>`;
|
|
31
|
-
lastMonth = store.monthLabels[y];
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Grid
|
|
36
|
-
for (let x = 0; x < GRID_WIDTH; x++) {
|
|
37
|
-
for (let y = 0; y < GRID_HEIGHT; y++) {
|
|
38
|
-
const cellX = x * (CELL_SIZE + GAP_SIZE);
|
|
39
|
-
const cellY = y * (CELL_SIZE + GAP_SIZE) + 15;
|
|
40
|
-
const cellColorAnimation = generateChangingValuesAnimation(store, generateCellColorValues(store, x, y));
|
|
41
|
-
svg += `<rect id="c-${x}-${y}" x="${cellX}" y="${cellY}" width="${CELL_SIZE}" height="${CELL_SIZE}" rx="5" fill="${Utils.getCurrentTheme(store).emptyContributionBoxColor}">
|
|
42
|
-
<animate attributeName="fill" dur="${store.gameHistory.length * DELTA_TIME}ms" repeatCount="indefinite"
|
|
43
|
-
values="${cellColorAnimation.values}"
|
|
44
|
-
keyTimes="${cellColorAnimation.keyTimes}"/>
|
|
45
|
-
</rect>`;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Walls
|
|
50
|
-
for (let x = 0; x < GRID_WIDTH; x++) {
|
|
51
|
-
for (let y = 0; y < GRID_HEIGHT; y++) {
|
|
52
|
-
if (WALLS.horizontal[x][y].active) {
|
|
53
|
-
svg += `<rect id="wh-${x}-${y}" x="${x * (CELL_SIZE + GAP_SIZE) - GAP_SIZE}" y="${y * (CELL_SIZE + GAP_SIZE) - GAP_SIZE + 15}" width="${CELL_SIZE + GAP_SIZE}" height="${GAP_SIZE}" rx="5" fill="${Utils.getCurrentTheme(store).wallColor}"></rect>`;
|
|
54
|
-
}
|
|
55
|
-
if (WALLS.vertical[x][y].active) {
|
|
56
|
-
svg += `<rect id="wv-${x}-${y}" x="${x * (CELL_SIZE + GAP_SIZE) - GAP_SIZE}" y="${y * (CELL_SIZE + GAP_SIZE) - GAP_SIZE + 15}" width="${GAP_SIZE}" height="${CELL_SIZE + GAP_SIZE}" rx="5" fill="${Utils.getCurrentTheme(store).wallColor}"></rect>`;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Pacman
|
|
62
|
-
const pacmanColorAnimation = generateChangingValuesAnimation(store, generatePacManColors(store));
|
|
63
|
-
const pacmanPositionAnimation = generateChangingValuesAnimation(store, generatePacManPositions(store));
|
|
64
|
-
const pacmanRotationAnimation = generateChangingValuesAnimation(store, generatePacManRotations(store));
|
|
65
|
-
svg += `<path id="pacman" d="${generatePacManPath(0.55)}"
|
|
66
|
-
>
|
|
67
|
-
<animate attributeName="fill" dur="${store.gameHistory.length * DELTA_TIME}ms" repeatCount="indefinite"
|
|
68
|
-
keyTimes="${pacmanColorAnimation.keyTimes}"
|
|
69
|
-
values="${pacmanColorAnimation.values}"/>
|
|
70
|
-
<animateTransform attributeName="transform" type="translate" dur="${store.gameHistory.length * DELTA_TIME}ms" repeatCount="indefinite"
|
|
71
|
-
keyTimes="${pacmanPositionAnimation.keyTimes}"
|
|
72
|
-
values="${pacmanPositionAnimation.values}"
|
|
73
|
-
additive="sum"/>
|
|
74
|
-
<animateTransform attributeName="transform" type="rotate" dur="${store.gameHistory.length * DELTA_TIME}ms" repeatCount="indefinite"
|
|
75
|
-
keyTimes="${pacmanRotationAnimation.keyTimes}"
|
|
76
|
-
values="${pacmanRotationAnimation.values}"
|
|
77
|
-
additive="sum"/>
|
|
78
|
-
<animate attributeName="d" dur="0.5s" repeatCount="indefinite"
|
|
79
|
-
values="${generatePacManPath(0.55)};${generatePacManPath(0.05)};${generatePacManPath(0.55)}"/>
|
|
80
|
-
</path>`;
|
|
81
|
-
|
|
82
|
-
// Ghosts
|
|
83
|
-
store.ghosts.forEach((ghost, index) => {
|
|
84
|
-
const ghostPositionAnimation = generateChangingValuesAnimation(store, generateGhostPositions(store, index));
|
|
85
|
-
const ghostColorAnimation = generateChangingValuesAnimation(store, generateGhostColors(store, index));
|
|
86
|
-
svg += `<use id="ghost${index}" width="${CELL_SIZE}" height="${CELL_SIZE}" href="#ghost-${ghost.name}">
|
|
87
|
-
<animateTransform attributeName="transform" type="translate" dur="${store.gameHistory.length * DELTA_TIME}ms" repeatCount="indefinite"
|
|
88
|
-
keyTimes="${ghostPositionAnimation.keyTimes}"
|
|
89
|
-
values="${ghostPositionAnimation.values}"/>
|
|
90
|
-
<animate attributeName="href" dur="${store.gameHistory.length * DELTA_TIME}ms" repeatCount="indefinite"
|
|
91
|
-
keyTimes="${ghostColorAnimation.keyTimes}"
|
|
92
|
-
values="${ghostColorAnimation.values}"/>
|
|
93
|
-
</use>`;
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
svg += '</svg>';
|
|
97
|
-
return svg;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const generatePacManPath = (mouthAngle: number) => {
|
|
101
|
-
const radius = CELL_SIZE / 2;
|
|
102
|
-
const startAngle = mouthAngle;
|
|
103
|
-
const endAngle = 2 * Math.PI - mouthAngle;
|
|
104
|
-
|
|
105
|
-
return `M ${radius},${radius}
|
|
106
|
-
L ${radius + radius * Math.cos(startAngle)},${radius + radius * Math.sin(startAngle)}
|
|
107
|
-
A ${radius},${radius} 0 1,1 ${radius + radius * Math.cos(endAngle)},${radius + radius * Math.sin(endAngle)}
|
|
108
|
-
Z`;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const generatePacManPositions = (store: StoreType): string[] => {
|
|
112
|
-
return store.gameHistory.map((state) => {
|
|
113
|
-
const x = state.pacman.x * (CELL_SIZE + GAP_SIZE);
|
|
114
|
-
const y = state.pacman.y * (CELL_SIZE + GAP_SIZE) + 15;
|
|
115
|
-
return `${x},${y}`;
|
|
116
|
-
});
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const generatePacManRotations = (store: StoreType): string[] => {
|
|
120
|
-
const pivit = CELL_SIZE / 2;
|
|
121
|
-
return store.gameHistory.map((state) => {
|
|
122
|
-
switch (state.pacman.direction) {
|
|
123
|
-
case 'right':
|
|
124
|
-
return `0 ${pivit} ${pivit}`;
|
|
125
|
-
case 'left':
|
|
126
|
-
return `180 ${pivit} ${pivit}`;
|
|
127
|
-
case 'up':
|
|
128
|
-
return `270 ${pivit} ${pivit}`;
|
|
129
|
-
case 'down':
|
|
130
|
-
return `90 ${pivit} ${pivit}`;
|
|
131
|
-
default:
|
|
132
|
-
return `0 ${pivit} ${pivit}`;
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const generatePacManColors = (store: StoreType): string[] => {
|
|
138
|
-
return store.gameHistory.map((state) => {
|
|
139
|
-
if (state.pacman.deadRemainingDuration) {
|
|
140
|
-
return PACMAN_COLOR_DEAD;
|
|
141
|
-
} else if (state.pacman.powerupRemainingDuration) {
|
|
142
|
-
return PACMAN_COLOR_POWERUP;
|
|
143
|
-
} else {
|
|
144
|
-
return PACMAN_COLOR;
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const generateCellColorValues = (store: StoreType, x: number, y: number): string[] => {
|
|
150
|
-
return store.gameHistory.map((state) => {
|
|
151
|
-
const intensity = state.grid[x][y];
|
|
152
|
-
if (intensity > 0) {
|
|
153
|
-
const adjustedIntensity = intensity < 0.2 ? 0.3 : intensity;
|
|
154
|
-
return Utils.hexToHexAlpha(Utils.getCurrentTheme(store).contributionBoxColor, adjustedIntensity);
|
|
155
|
-
} else {
|
|
156
|
-
return Utils.getCurrentTheme(store).emptyContributionBoxColor;
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const generateGhostPositions = (store: StoreType, ghostIndex: number): string[] => {
|
|
162
|
-
return store.gameHistory.map((state) => {
|
|
163
|
-
const ghost = state.ghosts[ghostIndex];
|
|
164
|
-
const x = ghost.x * (CELL_SIZE + GAP_SIZE);
|
|
165
|
-
const y = ghost.y * (CELL_SIZE + GAP_SIZE) + 15;
|
|
166
|
-
return `${x},${y}`;
|
|
167
|
-
});
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const generateGhostColors = (store: StoreType, ghostIndex: number): string[] => {
|
|
171
|
-
return store.gameHistory.map((state) => {
|
|
172
|
-
const ghost = state.ghosts[ghostIndex];
|
|
173
|
-
return '#' + (ghost.scared ? ghostShort('scared') : ghostShort(ghost.name));
|
|
174
|
-
});
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const generateGhostsPredefinition = () => {
|
|
178
|
-
return `<defs>
|
|
179
|
-
<symbol id="${ghostShort('blinky')}" viewBox="0 0 100 100">
|
|
180
|
-
<image href="${GHOSTS['blinky'].imgDate}" width="100" height="100"/>
|
|
181
|
-
</symbol>
|
|
182
|
-
<symbol id="${ghostShort('clyde')}" viewBox="0 0 100 100">
|
|
183
|
-
<image href="${GHOSTS['clyde'].imgDate}" width="100" height="100"/>
|
|
184
|
-
</symbol>
|
|
185
|
-
<symbol id="${ghostShort('inky')}" viewBox="0 0 100 100">
|
|
186
|
-
<image href="${GHOSTS['inky'].imgDate}" width="100" height="100"/>
|
|
187
|
-
</symbol>
|
|
188
|
-
<symbol id="${ghostShort('pinky')}" viewBox="0 0 100 100">
|
|
189
|
-
<image href="${GHOSTS['pinky'].imgDate}" width="100" height="100"/>
|
|
190
|
-
</symbol>
|
|
191
|
-
<symbol id="${ghostShort('scared')}" viewBox="0 0 100 100">
|
|
192
|
-
<image href="${GHOSTS['scared'].imgDate}" width="100" height="100"/>
|
|
193
|
-
</symbol>
|
|
194
|
-
</defs>`;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const ghostShort = (ghostName: string): string => {
|
|
198
|
-
switch (ghostName) {
|
|
199
|
-
case 'blinky':
|
|
200
|
-
return 'gb';
|
|
201
|
-
case 'clyde':
|
|
202
|
-
return 'gc';
|
|
203
|
-
case 'inky':
|
|
204
|
-
return 'gi';
|
|
205
|
-
case 'pinky':
|
|
206
|
-
return 'gp';
|
|
207
|
-
case 'scared':
|
|
208
|
-
return 'gs';
|
|
209
|
-
default:
|
|
210
|
-
return ghostName;
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const generateChangingValuesAnimation = (store: StoreType, changingValues: string[]): AnimationData => {
|
|
215
|
-
if (store.gameHistory.length !== changingValues.length) {
|
|
216
|
-
throw new Error('The length of changingValues must match the length of gameHistory');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const totalFrames = store.gameHistory.length;
|
|
220
|
-
let keyTimes: number[] = [];
|
|
221
|
-
let values: string[] = [];
|
|
222
|
-
let lastValue: string | null = null;
|
|
223
|
-
let lastIndex: number | null = null;
|
|
224
|
-
|
|
225
|
-
changingValues.forEach((currentValue, index) => {
|
|
226
|
-
if (currentValue !== lastValue) {
|
|
227
|
-
if (lastValue !== null && lastIndex !== null && index - 1 !== lastIndex) {
|
|
228
|
-
// Add a keyframe right before the value change
|
|
229
|
-
keyTimes.push(Number(((index - 0.000001) / (totalFrames - 1)).toFixed(6)));
|
|
230
|
-
values.push(lastValue);
|
|
231
|
-
}
|
|
232
|
-
// Add the new value keyframe
|
|
233
|
-
keyTimes.push(Number((index / (totalFrames - 1)).toFixed(6)));
|
|
234
|
-
values.push(currentValue);
|
|
235
|
-
lastValue = currentValue;
|
|
236
|
-
lastIndex = index;
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// Ensure the last frame is always included
|
|
241
|
-
if (keyTimes[keyTimes.length - 1] !== 1) {
|
|
242
|
-
keyTimes.push(1);
|
|
243
|
-
values.push(lastValue!);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
keyTimes: keyTimes.join(';'),
|
|
248
|
-
values: values.join(';')
|
|
249
|
-
};
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
export const SVG = {
|
|
253
|
-
generateAnimatedSVG
|
|
254
|
-
};
|
package/src/types.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
export type Point2d = {
|
|
2
|
-
x: number;
|
|
3
|
-
y: number;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
export interface Pacman {
|
|
7
|
-
x: number;
|
|
8
|
-
y: number;
|
|
9
|
-
direction: 'right' | 'left' | 'up' | 'down';
|
|
10
|
-
points: number;
|
|
11
|
-
totalPoints: number;
|
|
12
|
-
deadRemainingDuration: number;
|
|
13
|
-
powerupRemainingDuration: number;
|
|
14
|
-
recentPositions: string[];
|
|
15
|
-
target?: Point2d;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type GhostName = 'blinky' | 'clyde' | 'inky' | 'pinky';
|
|
19
|
-
export interface Ghost {
|
|
20
|
-
x: number;
|
|
21
|
-
y: number;
|
|
22
|
-
name: GhostName;
|
|
23
|
-
scared: boolean;
|
|
24
|
-
target?: Point2d;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface Contribution {
|
|
28
|
-
date: Date;
|
|
29
|
-
count: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface StoreType {
|
|
33
|
-
frameCount: number;
|
|
34
|
-
contributions: Contribution[];
|
|
35
|
-
pacman: Pacman;
|
|
36
|
-
ghosts: Ghost[];
|
|
37
|
-
grid: { intensity: number; commitsCount: number }[][];
|
|
38
|
-
monthLabels: string[];
|
|
39
|
-
pacmanMouthOpen: boolean;
|
|
40
|
-
gameInterval: number;
|
|
41
|
-
gameHistory: {
|
|
42
|
-
pacman: Pacman;
|
|
43
|
-
ghosts: Ghost[];
|
|
44
|
-
grid: number[][];
|
|
45
|
-
}[];
|
|
46
|
-
config: Config;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface Config {
|
|
50
|
-
platform: 'github' | 'gitlab';
|
|
51
|
-
username: string;
|
|
52
|
-
canvas: HTMLCanvasElement;
|
|
53
|
-
outputFormat: 'canvas' | 'svg';
|
|
54
|
-
svgCallback: (blonUrl: string) => void;
|
|
55
|
-
gameOverCallback: () => void;
|
|
56
|
-
gameTheme: ThemeKeys;
|
|
57
|
-
gameSpeed: number;
|
|
58
|
-
enableSounds: boolean;
|
|
59
|
-
pointsIncreasedCallback: (pointsSum: number) => void;
|
|
60
|
-
githubSettings?: {
|
|
61
|
-
accessToken: string;
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export type ThemeKeys = 'github' | 'github-dark' | 'gitlab' | 'gitlab-dark';
|
|
66
|
-
|
|
67
|
-
export interface GameTheme {
|
|
68
|
-
textColor: string;
|
|
69
|
-
gridBackground: string;
|
|
70
|
-
contributionBoxColor: string;
|
|
71
|
-
emptyContributionBoxColor: string;
|
|
72
|
-
wallColor: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface AnimationData {
|
|
76
|
-
keyTimes: string;
|
|
77
|
-
values: string;
|
|
78
|
-
}
|