Skip to content

Commit 18cdc66

Browse files
committed
feat: archive game-2b89bbb3d8fd
1 parent 6361c18 commit 18cdc66

4 files changed

Lines changed: 517 additions & 1 deletion

File tree

games/game-2b89bbb3d8fd/game.js

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
window.__iis_game_boot_ok = true;
2+
const CONFIG = {"mode": "arcade_generic", "title": "Tele-Link: Portal Conversion", "genre": "arcade", "slug": "game-2b89bbb3d8fd", "accentColor": "#0EA5E9", "viewportWidth": 1280, "viewportHeight": 720, "safeAreaPadding": 24, "minFontSizePx": 14, "textOverflowPolicy": "ellipsis-clamp", "player_hp": 3, "player_speed": 350, "player_attack_cooldown": 0.3, "enemy_hp": 1, "enemy_speed_min": 150, "enemy_speed_max": 280, "enemy_spawn_rate": 0.8, "time_limit_sec": 90, "base_score_value": 10};
3+
const canvas = document.getElementById("game");
4+
const ctx = canvas.getContext("2d");
5+
const overlay = document.getElementById("overlay");
6+
const overlayText = document.getElementById("overlay-text");
7+
const scoreEl = document.getElementById("score");
8+
const timerEl = document.getElementById("timer");
9+
const hpEl = document.getElementById("hp");
10+
const keys = new Set();
11+
12+
const state = {
13+
running: true,
14+
score: 0,
15+
hp: CONFIG.player_hp || 3,
16+
timeLeft: CONFIG.time_limit_sec || 60,
17+
lastTime: 0,
18+
player: { x: canvas.width * 0.5, y: canvas.height * 0.8, w: 36, h: 56, vx: 0, vy: 0, lane: 1 },
19+
enemies: [],
20+
bullets: [],
21+
particles: [],
22+
spawnTimer: 0,
23+
enemyHp: CONFIG.enemy_hp || 1,
24+
attackCooldown: 0,
25+
};
26+
27+
document.addEventListener("keydown", (e) => {
28+
keys.add(e.key);
29+
if (!state.running && (e.key === "r" || e.key === "R")) restartGame();
30+
if (CONFIG.mode === "arena_shooter" && e.code === "Space") {
31+
e.preventDefault();
32+
fireBullet();
33+
}
34+
if (CONFIG.mode === "duel_brawler" && e.code === "Space") {
35+
e.preventDefault();
36+
performAttack();
37+
}
38+
});
39+
document.addEventListener("keyup", (e) => keys.delete(e.key));
40+
document.getElementById("restart-btn").addEventListener("click", restartGame);
41+
42+
function resetState() {
43+
state.running = true;
44+
state.score = 0;
45+
state.hp = CONFIG.player_hp || 3;
46+
state.timeLeft = CONFIG.time_limit_sec || 60;
47+
state.lastTime = 0;
48+
state.player = { x: canvas.width * 0.5, y: canvas.height * 0.8, w: 36, h: 56, vx: 0, vy: 0, lane: 1 };
49+
state.enemies = [];
50+
state.bullets = [];
51+
state.particles = [];
52+
state.spawnTimer = 0;
53+
state.enemyHp = CONFIG.enemy_hp || 1;
54+
state.attackCooldown = 0;
55+
overlay.classList.remove("show");
56+
updateHud();
57+
}
58+
59+
function restartGame() { resetState(); }
60+
61+
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
62+
function rand(min, max) { return Math.random() * (max - min) + min; }
63+
function rectsOverlap(a, b) {
64+
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
65+
}
66+
67+
function spawnEnemy() {
68+
const spdMin = CONFIG.enemy_speed_min || 100;
69+
const spdMax = CONFIG.enemy_speed_max || 220;
70+
if (CONFIG.mode === "lane_dodge_racer") {
71+
const lanes = [0.28, 0.5, 0.72];
72+
const lane = Math.floor(Math.random() * lanes.length);
73+
state.enemies.push({ x: canvas.width * lanes[lane] - 18, y: -70, w: 36, h: 70, speed: rand(spdMin, spdMax) });
74+
return;
75+
}
76+
if (CONFIG.mode === "arena_shooter") {
77+
state.enemies.push({ x: rand(40, canvas.width - 80), y: -40, w: 30, h: 30, speed: rand(spdMin, spdMax), hp: CONFIG.enemy_hp || 1 });
78+
return;
79+
}
80+
if (CONFIG.mode === "duel_brawler") {
81+
if (state.enemies.length === 0) {
82+
state.enemies.push({ x: canvas.width * 0.5 + 120, y: canvas.height * 0.5, w: 46, h: 72, hp: state.enemyHp, speed: spdMin });
83+
}
84+
return;
85+
}
86+
state.enemies.push({ x: rand(40, canvas.width - 80), y: -40, w: 26, h: 26, speed: rand(spdMin, spdMax) });
87+
}
88+
89+
function fireBullet() {
90+
if (!state.running) return;
91+
state.bullets.push({ x: state.player.x + state.player.w * 0.5 - 3, y: state.player.y, w: 6, h: 16, speed: 520 });
92+
}
93+
94+
function performAttack() {
95+
if (!state.running || state.attackCooldown > 0) return;
96+
state.attackCooldown = CONFIG.player_attack_cooldown || 0.5;
97+
const enemy = state.enemies[0];
98+
if (!enemy) return;
99+
const dx = (enemy.x + enemy.w / 2) - (state.player.x + state.player.w / 2);
100+
const dy = (enemy.y + enemy.h / 2) - (state.player.y + state.player.h / 2);
101+
const dist = Math.hypot(dx, dy);
102+
if (dist < 90) {
103+
enemy.hp -= 1;
104+
state.score += 45;
105+
burst(enemy.x + enemy.w / 2, enemy.y + enemy.h / 2, "#f59e0b", 10);
106+
if (enemy.hp <= 0) {
107+
state.score += 200;
108+
state.enemyHp += 3;
109+
state.enemies = [];
110+
burst(canvas.width / 2, canvas.height / 2, "#22c55e", 24);
111+
}
112+
}
113+
}
114+
115+
function burst(x, y, color, count) {
116+
for (let i = 0; i < count; i++) {
117+
state.particles.push({
118+
x, y, life: rand(0.2, 0.6), t: 0, color,
119+
vx: rand(-160, 160), vy: rand(-160, 160)
120+
});
121+
}
122+
}
123+
124+
function update(dt) {
125+
if (!state.running) return;
126+
state.timeLeft = Math.max(0, state.timeLeft - dt);
127+
state.spawnTimer += dt;
128+
state.attackCooldown = Math.max(0, state.attackCooldown - dt);
129+
const spawnRate = CONFIG.enemy_spawn_rate || 1.0;
130+
131+
if (CONFIG.mode === "lane_dodge_racer") {
132+
const laneXs = [canvas.width * 0.28, canvas.width * 0.5, canvas.width * 0.72];
133+
if ((keys.has("ArrowLeft") || keys.has("a")) && state.player.lane > 0) { state.player.lane -= 1; keys.delete("ArrowLeft"); keys.delete("a"); }
134+
if ((keys.has("ArrowRight") || keys.has("d")) && state.player.lane < 2) { state.player.lane += 1; keys.delete("ArrowRight"); keys.delete("d"); }
135+
const targetX = laneXs[state.player.lane] - state.player.w / 2;
136+
state.player.x += (targetX - state.player.x) * Math.min(1, dt * 10);
137+
if (state.spawnTimer > spawnRate) { state.spawnTimer = 0; spawnEnemy(); }
138+
for (const e of state.enemies) e.y += e.speed * dt;
139+
for (const e of state.enemies) {
140+
if (rectsOverlap(state.player, e)) {
141+
state.hp -= 1;
142+
e.y = canvas.height + 100;
143+
burst(state.player.x + state.player.w/2, state.player.y + state.player.h/2, "#ef4444", 12);
144+
}
145+
}
146+
state.enemies = state.enemies.filter((e) => {
147+
const passed = e.y > canvas.height + 80;
148+
if (passed) state.score += (CONFIG.base_score_value || 10);
149+
return !passed;
150+
});
151+
} else if (CONFIG.mode === "arena_shooter") {
152+
const speed = CONFIG.player_speed || 260;
153+
state.player.vx = (keys.has("ArrowRight") || keys.has("d") ? 1 : 0) - (keys.has("ArrowLeft") || keys.has("a") ? 1 : 0);
154+
state.player.vy = (keys.has("ArrowDown") || keys.has("s") ? 1 : 0) - (keys.has("ArrowUp") || keys.has("w") ? 1 : 0);
155+
state.player.x = clamp(state.player.x + state.player.vx * speed * dt, 20, canvas.width - state.player.w - 20);
156+
state.player.y = clamp(state.player.y + state.player.vy * speed * dt, 60, canvas.height - state.player.h - 20);
157+
if (state.spawnTimer > spawnRate) { state.spawnTimer = 0; spawnEnemy(); }
158+
for (const e of state.enemies) {
159+
e.y += e.speed * dt;
160+
if (e.y > canvas.height + 40) {
161+
e.y = canvas.height + 999;
162+
state.hp -= 1;
163+
}
164+
if (rectsOverlap(state.player, e)) {
165+
e.y = canvas.height + 999;
166+
state.hp -= 1;
167+
burst(state.player.x + state.player.w/2, state.player.y + state.player.h/2, "#ef4444", 14);
168+
}
169+
}
170+
for (const b of state.bullets) b.y -= b.speed * dt;
171+
for (const b of state.bullets) {
172+
for (const e of state.enemies) {
173+
if (e.y < canvas.height + 500 && rectsOverlap(b, e)) {
174+
e.y = canvas.height + 999;
175+
b.y = -999;
176+
state.score += (CONFIG.base_score_value || 10);
177+
burst(e.x + e.w/2, e.y + e.h/2, "#38bdf8", 8);
178+
}
179+
}
180+
}
181+
state.enemies = state.enemies.filter((e) => e.y < canvas.height + 120);
182+
state.bullets = state.bullets.filter((b) => b.y > -40);
183+
} else if (CONFIG.mode === "duel_brawler") {
184+
const speed = CONFIG.player_speed || 220;
185+
state.player.vx = (keys.has("ArrowRight") || keys.has("d") ? 1 : 0) - (keys.has("ArrowLeft") || keys.has("a") ? 1 : 0);
186+
state.player.vy = (keys.has("ArrowDown") || keys.has("s") ? 1 : 0) - (keys.has("ArrowUp") || keys.has("w") ? 1 : 0);
187+
state.player.x = clamp(state.player.x + state.player.vx * speed * dt, 20, canvas.width - state.player.w - 20);
188+
state.player.y = clamp(state.player.y + state.player.vy * speed * dt, 60, canvas.height - state.player.h - 20);
189+
if (state.enemies.length === 0) spawnEnemy();
190+
for (const e of state.enemies) {
191+
const dx = state.player.x - e.x;
192+
const dy = state.player.y - e.y;
193+
const len = Math.max(1, Math.hypot(dx, dy));
194+
e.x += (dx / len) * e.speed * dt;
195+
e.y += (dy / len) * e.speed * dt;
196+
if (rectsOverlap(state.player, e)) {
197+
state.hp -= 1;
198+
state.player.x = clamp(state.player.x - (dx / len) * 35, 20, canvas.width - state.player.w - 20);
199+
state.player.y = clamp(state.player.y - (dy / len) * 35, 60, canvas.height - state.player.h - 20);
200+
burst(state.player.x + state.player.w/2, state.player.y + state.player.h/2, "#ef4444", 10);
201+
}
202+
}
203+
state.score += dt * 8;
204+
} else {
205+
const speed = 240;
206+
state.player.vx = (keys.has("ArrowRight") ? 1 : 0) - (keys.has("ArrowLeft") ? 1 : 0);
207+
state.player.vy = (keys.has("ArrowDown") ? 1 : 0) - (keys.has("ArrowUp") ? 1 : 0);
208+
state.player.x = clamp(state.player.x + state.player.vx * speed * dt, 20, canvas.width - state.player.w - 20);
209+
state.player.y = clamp(state.player.y + state.player.vy * speed * dt, 60, canvas.height - state.player.h - 20);
210+
if (state.spawnTimer > 0.6) { state.spawnTimer = 0; spawnEnemy(); }
211+
for (const e of state.enemies) {
212+
e.y += e.speed * dt;
213+
if (rectsOverlap(state.player, e)) { state.hp -= 1; e.y = canvas.height + 999; }
214+
}
215+
state.enemies = state.enemies.filter((e) => e.y < canvas.height + 100);
216+
state.score += dt * 10;
217+
}
218+
219+
for (const p of state.particles) {
220+
p.t += dt;
221+
p.x += p.vx * dt;
222+
p.y += p.vy * dt;
223+
}
224+
state.particles = state.particles.filter((p) => p.t < p.life);
225+
226+
if (state.timeLeft <= 0 || state.hp <= 0) {
227+
endGame();
228+
}
229+
updateHud();
230+
}
231+
232+
function draw() {
233+
ctx.clearRect(0, 0, canvas.width, canvas.height);
234+
ctx.fillStyle = "#0b1220";
235+
ctx.fillRect(0, 0, canvas.width, canvas.height);
236+
237+
if (CONFIG.mode === "lane_dodge_racer") {
238+
ctx.fillStyle = "#1f2937";
239+
ctx.fillRect(canvas.width * 0.2, 0, canvas.width * 0.6, canvas.height);
240+
ctx.strokeStyle = "rgba(148,163,184,.35)";
241+
ctx.setLineDash([18, 26]);
242+
for (const x of [canvas.width * 0.4, canvas.width * 0.6]) {
243+
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
244+
}
245+
ctx.setLineDash([]);
246+
} else {
247+
const g = ctx.createLinearGradient(0, 0, 0, canvas.height);
248+
g.addColorStop(0, "#0a1020");
249+
g.addColorStop(1, "#070b16");
250+
ctx.fillStyle = g;
251+
ctx.fillRect(0, 0, canvas.width, canvas.height);
252+
for (let i = 0; i < 120; i++) {
253+
ctx.fillStyle = `rgba(148,163,184,${(i % 5) * 0.02})`;
254+
ctx.fillRect((i * 73) % canvas.width, (i * 41) % canvas.height, 2, 2);
255+
}
256+
}
257+
258+
for (const e of state.enemies) {
259+
ctx.fillStyle = CONFIG.mode === "duel_brawler" ? "#b91c1c" : "#ef4444";
260+
ctx.shadowBlur = 14;
261+
ctx.shadowColor = "rgba(239,68,68,0.45)";
262+
ctx.fillRect(e.x, e.y, e.w, e.h);
263+
}
264+
for (const b of state.bullets) {
265+
ctx.fillStyle = "#38bdf8";
266+
ctx.shadowBlur = 10;
267+
ctx.shadowColor = "rgba(56,189,248,0.55)";
268+
ctx.fillRect(b.x, b.y, b.w, b.h);
269+
}
270+
for (const p of state.particles) {
271+
const a = 1 - p.t / p.life;
272+
ctx.fillStyle = p.color.replace(")", `, ${a})`).replace("rgb", "rgba");
273+
ctx.globalAlpha = a;
274+
ctx.fillRect(p.x, p.y, 3, 3);
275+
ctx.globalAlpha = 1;
276+
}
277+
278+
ctx.shadowBlur = 18;
279+
ctx.shadowColor = "rgba(79,124,255,0.5)";
280+
ctx.fillStyle = "#38bdf8";
281+
ctx.fillRect(state.player.x, state.player.y, state.player.w, state.player.h);
282+
if (CONFIG.mode === "duel_brawler" && state.attackCooldown > 0) {
283+
ctx.strokeStyle = "#f59e0b";
284+
ctx.lineWidth = 3;
285+
ctx.beginPath();
286+
ctx.arc(state.player.x + state.player.w/2, state.player.y + state.player.h/2, 52, 0, Math.PI * 2);
287+
ctx.stroke();
288+
}
289+
ctx.shadowBlur = 0;
290+
}
291+
292+
function updateHud() {
293+
scoreEl.textContent = `Score: ${Math.floor(state.score)}`;
294+
timerEl.textContent = `Time: ${state.timeLeft.toFixed(1)}`;
295+
hpEl.textContent = `HP: ${Math.max(0, state.hp)}`;
296+
}
297+
298+
function endGame() {
299+
if (!state.running) return;
300+
state.running = false;
301+
overlayText.textContent = `최종 점수 ${Math.floor(state.score)} · 다시 시작하려면 R`;
302+
overlay.classList.add("show");
303+
}
304+
305+
function frame(ts) {
306+
if (!state.lastTime) state.lastTime = ts;
307+
const dt = Math.min(0.05, (ts - state.lastTime) / 1000);
308+
state.lastTime = ts;
309+
update(dt);
310+
draw();
311+
requestAnimationFrame(frame);
312+
}
313+
314+
async function submitScore(playerName, score, fingerprint) {
315+
const endpoint = window.__IIS_LEADERBOARD_ENDPOINT;
316+
const anonKey = window.__IIS_SUPABASE_ANON_KEY;
317+
const gameId = window.__IIS_GAME_ID;
318+
if (!endpoint || !anonKey || !gameId) return { status: "skipped", reason: "missing_env" };
319+
const controller = new AbortController();
320+
const timeout = setTimeout(() => controller.abort(), 8000);
321+
try {
322+
const response = await fetch(endpoint, {
323+
method: "POST",
324+
headers: {
325+
"Content-Type": "application/json",
326+
apikey: anonKey,
327+
Authorization: `Bearer ${anonKey}`,
328+
Prefer: "return=minimal",
329+
},
330+
body: JSON.stringify({
331+
game_id: gameId,
332+
player_name: playerName,
333+
score,
334+
player_fingerprint: fingerprint,
335+
}),
336+
signal: controller.signal,
337+
});
338+
if (!response.ok) return { status: "error", reason: `http_${response.status}` };
339+
return { status: "ok" };
340+
} catch (error) {
341+
return { status: "error", reason: String(error) };
342+
} finally {
343+
clearTimeout(timeout);
344+
}
345+
}
346+
347+
window.IISLeaderboard = { submitScore };
348+
resetState();
349+
requestAnimationFrame(frame);

games/game-2b89bbb3d8fd/index.html

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!doctype html>
2+
<html lang="ko">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width,initial-scale=1" />
6+
<title>Tele-Link: Portal Conversion</title>
7+
<link rel="stylesheet" href="./styles.css" />
8+
</head>
9+
<body>
10+
<main data-overflow-policy="ellipsis-clamp">
11+
<div class="hud-row">
12+
<div style="display:grid;gap:4px;min-width:0">
13+
<h1 class="title overflow-guard">Tele-Link: Portal Conversion</h1>
14+
<p class="sub overflow-guard">Genre: arcade · Mode: Arcade</p>
15+
</div>
16+
<span class="chip overflow-guard">game-2b89bbb3d8fd</span>
17+
</div>
18+
<div class="hud-row">
19+
<strong id="score" class="stat overflow-guard">Score: 0</strong>
20+
<strong id="timer" class="stat overflow-guard">Time: 60.0</strong>
21+
<strong id="hp" class="stat overflow-guard">HP: 3</strong>
22+
</div>
23+
<div class="stage">
24+
<canvas id="game" width="1280" height="720"></canvas>
25+
<div id="overlay" class="overlay">
26+
<div class="overlay-card">
27+
<h2 id="overlay-title" style="margin:0 0 6px">Game Over</h2>
28+
<p id="overlay-text" class="hint" style="margin:0 0 12px"></p>
29+
<button id="restart-btn" type="button">다시 시작 (R)</button>
30+
</div>
31+
</div>
32+
</div>
33+
<p class="hint overflow-guard">움직이며 위험 요소를 피하고 점수를 올리세요. / ← → ↑ ↓ 이동 / R 재시작</p>
34+
<p class="hint overflow-guard">Use IISLeaderboard.submitScore(playerName, score, fingerprint) when game over.</p>
35+
</main>
36+
<script src="./game.js"></script>
37+
</body>
38+
</html>

0 commit comments

Comments
 (0)