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 ) ;
0 commit comments