let player1, player2; let tiles = [], decoTiles = []; let projectiles = [], specialProjectiles = [], bombs = []; let deathZoneY = 700; let groundY = [500, 400, 300, 200, 100]; let controlsP1, controlsP2; let breakEffects = []; let items = []; let nextItemFrame = 0; const TILE_H = 32; const mapLayout = [ // row 0 (y = groundY[0]–16): gb1 | question | gb1 [ { x1: 32, x2: 256, type: 'groundblock1' }, { x1: 272, x2: 528, type: null }, { x1: 544, x2: 768, type: 'groundblock1' }, ], // row 1 (y = groundY[1]–16): breakable | gb1 | breakable [ { x1: 32, x2: 256, type: 'breakableblock' }, { x1: 272, x2: 528, type: 'groundblock1' }, { x1: 544, x2: 768, type: 'breakableblock' }, ], // row 2 (y = groundY[2]–16): gb1 | question | gb1 [ { x1: 32, x2: 256, type: 'groundblock1' }, { x1: 272, x2: 528, type: 'breakableblock' }, { x1: 544, x2: 768, type: 'groundblock1' }, ], // row 3 (y = groundY[3]–16): breakable | gb1 | breakable [ { x1: 32, x2: 256, type: 'breakableblock' }, { x1: 272, x2: 528, type: 'groundblock1' }, { x1: 544, x2: 768, type: 'breakableblock' }, ], // row 4 (y = groundY[4]–16): gb1 | (빈칸) | gb1 [ { x1: 32, x2: 256, type: 'groundblock1' }, { x1: 272, x2: 528, type: null }, { x1: 544, x2: 768, type: 'groundblock1' }, ], ]; function preload() { preloadAssets(); } function setup() { createCanvas(800, 600); sliceAssets(); controlsP1 = { left: 'ArrowLeft', right: 'ArrowRight', jump: 'ArrowUp', attack: '[', bomb: ']', down: 'ArrowDown' }; controlsP2 = { left: 'a', right: 'd', jump: 'w', attack: 't', bomb: 'y', down: 's' }; player1 = new Player(100, 100, P1imgs, controlsP1); player2 = new Player(200, 100, P2imgs, controlsP2); for (let row = 0; row < groundY.length; row++) { const y = groundY[row] - TILE_H; for (let seg of mapLayout[row]) { if (!seg.type) continue; // null인 구간은 건너뛰고 for (let x = seg.x1; x <= seg.x2; x += TILE_H) { tiles.push(new Tile(x, y, seg.type)); } } } } function draw() { backgroundManager.draw(); decoTiles.forEach(t => t.draw()); tiles.forEach(t => t.draw()); player1.update(); player1.draw(); player2.update(); player2.draw(); handleProjectiles(); handleBombs(); handleSpecialProjectiles(); breakManager(); } function keyPressed() { if (key === ' ') backgroundManager.modeChange(); player1.handleKeyPressed(key); player2.handleKeyPressed(key); } function keyReleased() { player1.handleKeyReleased(key); player2.handleKeyReleased(key); } class Tile { constructor(x,y,type) { this.x=x; this.y=y; this.type=type; this.img=tileimgs[type][0]; this.width=32; this.height=32; } draw() { image(this.img,this.x,this.y,this.width,this.height); } collides(x,y,w,h) { return x < this.x+this.width && x+w>this.x && ythis.y; } } class Deco { constructor(x, y, type) { this.x = x; this.y = y; this.type = type; this.img = decoimgs[type][0]; this.width = 32; this.height= 32; } draw() { image(this.img, this.x, this.y, this.width, this.height); } } class BreakEffect { constructor(x, y) { this.x = x; this.y = y; this.frames = [ decoimgs.breakeffect1[0], decoimgs.breakeffect2[0], decoimgs.breakeffect3[0] ]; this.currentFrame = 0; this.frameTimer = 8; // 각 프레임을 몇 틱 동안 보여줄지 this.active = true; } update() { if (!this.active) return; this.frameTimer--; if (this.frameTimer <= 0) { this.currentFrame++; this.frameTimer = 8; if (this.currentFrame >= this.frames.length) { this.active = false; } } } draw() { if (!this.active) return; const img = this.frames[this.currentFrame]; image(img, this.x, this.y, TILE_H, TILE_H); } } class Background { constructor(dayImg, nightImg) { this.bgDay = dayImg; this.bgNight = nightImg; this.mode = 'day'; this.currentImg = this.bgDay; this.tileW = dayImg.width; this.tileH = dayImg.height; } setMode(mode) { if (mode === "day") { this.mode = "day"; this.currentImg = this.bgDay; } else if (mode === "night") { this.mode = "night"; this.currentImg = this.bgNight; } else { console.log("error"); } } modeChange(){ this.setMode(this.mode === "day" ? "night" : "day"); } draw() { for (let y = 0; y < height; y += this.tileH) { for (let x = 0; x < width; x += this.tileW) { image(this.currentImg, x, y); } } } } class Player { constructor(x, y, imgSet, controls) { this.x = x; this.y = y; this.vx = 0; this.vy = 0; this.knockbackVX = 0; this.width = 32; this.height = 32; this.onGround = false; this.jumpCount = 0; this.imgSet = imgSet; this.controls = controls; this.keys = {}; this.bombHoldStartTime = null; this.chargeTime = 0; this.maxCharge = 1000; this.facing = "right"; this.state = "idle"; this.frame = 0; this.attackTimer = 0; this.dropping = false; // drop-through state this.dropRowY = null; // current tile to drop through this.currentTileY = null; // current tile Y position for drop-through } update() { // Bomb charging if (this.keys[this.controls.bomb] && this.bombHoldStartTime !== null) { this.chargeTime = min(millis() - this.bombHoldStartTime, this.maxCharge); } // Attack cooldown if (this.attackTimer > 0) { this.attackTimer--; if (this.attackTimer === 0) this.state = this.onGround ? 'idle' : 'jump'; } // Horizontal movement let inputVX = 0; if (this.keys[this.controls.left]) { inputVX = -5; this.facing = 'left'; } else if (this.keys[this.controls.right]){ inputVX = 5; this.facing = 'right'; } this.vx = inputVX + this.knockbackVX; this.x += this.vx; // Gravity 적용 this.vy += gravity; let nextY = this.y + this.vy; // 위로 올라갈 때 드롭 취소 if (this.vy < 0 && this.dropRowY !== null) { this.dropRowY = null; this.dropping = false; } let landed = false; // 한 방향 플랫폼 충돌 처리 (아래로 떨어질 때만) if (this.vy > 0) { for (let tile of tiles) { // 드롭 중일 때, 지정된 행은 충돌 무시 if (this.dropping && tile.y === this.dropRowY) continue; if ( this.x + this.width > tile.x && this.x < tile.x + tile.width && this.y + this.height <= tile.y && nextY + this.height >= tile.y ) { // 착지 landed = true; this.y = tile.y - this.height; this.vy = 0; this.onGround = true; this.jumpCount = 0; // 나중에 DROP 키로 통과시킬 행 기록 this.currentTileY = tile.y; // 착지하면 드롭 상태 초기화 this.dropping = false; this.dropRowY = null; break; } } } if (landed) { if (this.attackTimer === 0) this.state = 'idle'; } else { this.y = nextY; this.onGround = false; if (this.attackTimer === 0) this.state = 'jump'; } // DOWN 키로 현재 플랫폼 행 통과 if ( this.keys[this.controls.down] && this.onGround && this.currentTileY !== null ) { this.dropping = true; this.dropRowY = this.currentTileY; this.onGround = false; this.currentTileY = null; this.jumpCount = 1; } // Knockback decay this.knockbackVX *= 0.9; if (abs(this.knockbackVX) < 0.1) this.knockbackVX = 0; // Respawn if fallen off map if (this.y > deathZoneY) this.respawn(); } handleKeyPressed(k) { this.keys[k] = true; if (k === this.controls.jump && this.onGround) this.jump(); if (k === this.controls.attack) this.shoot(); if (k === this.controls.bomb) this.bombHoldStartTime = millis(); } handleKeyReleased(k) { this.keys[k] = false; if (k === this.controls.bomb && this.bombHoldStartTime !== null) { const held = millis() - this.bombHoldStartTime; if (held >= this.maxCharge) this.fireBigMissile(); else this.dropBomb(); this.bombHoldStartTime = null; this.chargeTime = 0; } } respawn() { this.x = 100; this.y = 100; this.vx = this.vy = this.knockbackVX = 0; } jump() { if (this.jumpCount < 2) { this.vy = -12; this.jumpCount++; } } shoot() { const spawnX = this.facing === 'right' ? this.x + this.width : this.x - 16; const spawnY = this.y + this.height/2; const dir = this.facing === 'right' ? 20 : -20; projectiles.push(new Projectile(spawnX, spawnY, dir)); this.state = 'shoot'; this.attackTimer = 10; this.frame = 0; this.knockbackVX = this.facing === 'right' ? -2 : 2; } dropBomb() { const bx = this.facing === 'right' ? this.x + this.width : this.x - 32; const by = this.y - 8; const v_bomb = this.facing === 'right' ? 5 : -5; bombs.push(new Bomb(bx, by, v_bomb)); this.state = 'shoot'; this.attackTimer = 10; this.frame = 0; } fireBigMissile() { const dir = this.facing === 'right' ? 5 : -5; const mW = 64 * 2, mH = 64 * 2; const spawnX = this.facing === 'right' ? this.x + this.width : this.x - mW; const spawnY = this.y - this.height; specialProjectiles.push(new BigMissile(spawnX, spawnY, dir)); this.state = 'shoot'; this.attackTimer = 15; this.knockbackVX = this.facing === 'right' ? -5 : 5; this.frame = 0; } handleKeyPressed(k) { this.keys[k] = true; if (k === this.controls.jump) this.jump(); if (k === this.controls.attack)this.shoot(); if (k === this.controls.bomb) this.bombHoldStartTime = millis(); } handleKeyReleased(k) { this.keys[k] = false; if (k === this.controls.bomb && this.bombHoldStartTime !== null) { const held = millis() - this.bombHoldStartTime; if (held >= this.maxCharge ) this.fireBigMissile(); else this.dropBomb(); this.bombHoldStartTime = null; this.chargeTime = 0; } } draw() { // charge gauge 그리기 if (this.chargeTime > 0) { // 최대 너비를 this.width(32px)로 매핑 const w = map(this.chargeTime, 0, this.maxCharge, 0, this.width); // 충전 전: 노랑, 충전 완료 시: 빨강 if (this.chargeTime < this.maxCharge) { fill(255, 255, 0); } else { fill(255, 0, 0); } rect(this.x, this.y - 10, w, 5); noFill(); } const img = this.imgSet[this.state][this.frame]; push(); if (this.facing === 'left') { translate(this.x + this.width, this.y); scale(-1,1); image(img, 0,0, this.width, this.height); } else { image(img, this.x, this.y, this.width, this.height); } pop(); } } class Projectile { constructor(x, y, vx, w=8, h=8) { this.x = x; this.y = y; this.vx = vx; this.width = w; this.height = h; this.spawnTime = millis(); this.lifetime = 10000; this.shouldDestroy = false; } update(targets) { this.x += this.vx; for (const t of targets) { if (this.hits(t)) { t.knockbackVX += this.vx * 0.5; this.shouldDestroy = true; } } if (millis() - this.spawnTime > this.lifetime) { this.shouldDestroy = true; } } draw() { image(itemimgs.fire[0], this.x, this.y, this.width * 2, this.height * 2); } destroy() { return this.shouldDestroy; } hits(target) { return ( this.x < target.x + target.width && this.x + this.width > target.x && this.y < target.y + target.height && this.y + this.height > target.y ); } } class Bomb { constructor(x, y, vx) { this.x = x; this.y = y; this.vx = vx; this.vy = 0; this.width = 32; this.height = 32; this.timer = 120; this.explodeTimer = 15; this.exploded = false; this.warning = false; this.shouldRemove = false; this.radius = 100; this.stuck = false; // 착지 플래그 this.stuckY = null; // 멈춘 플랫폼의 y 좌표 } update() { // 1) 폭발 타이머 if (!this.exploded) { this.timer--; if (this.timer <= 60) this.warning = true; if (this.timer <= 0) { this.exploded = true; this.explode(); return; } } else { // 폭발 애니메이션 this.explodeTimer--; if (this.explodeTimer <= 0) this.shouldRemove = true; return; } // 2) 이미 착지(stuck) 상태면 위치만 고정 if (this.stuck) { this.vy = 0; // 멈춘 행의 y값 기준으로 위치 고정 this.y = this.stuckY - this.height; return; } // 3) 중력·이동 this.vy += gravity; this.x += this.vx; this.y += this.vy; const landThreshold = 1; // 4) 타일 바운스 처리 for (let tile of tiles) { if ( this.y + this.height >= tile.y && this.y + this.height - this.vy < tile.y && this.x + this.width > tile.x && this.x < tile.x + tile.width ) { // 타일 꼭대기로 위치 고정 this.y = tile.y - this.height; // 반사 감쇠 this.vy *= -0.5; this.vx *= 0.7; // 속도가 작아지면 진짜 착지 if (Math.abs(this.vy) < landThreshold) { this.vy = 0; this.stuck = true; this.stuckY = tile.y; // y좌표만 저장 } break; } } } explode() { // 폭발 반경 내 모든 플레이어에게 넉백 적용 [player1, player2].forEach(p => { const cx = this.x + this.width/2; const cy = this.y + this.height/2; const px = p.x + p.width/2; const py = p.y + p.height/2; const d = dist(cx, cy, px, py); if (d < this.radius) { const angle = atan2(py - cy, px - cx); const force = map(d, 0, this.radius, 20, 5); p.knockbackVX += cos(angle) * force * 2; p.vy += sin(angle) * force * 2; } }); for (let i = tiles.length - 1; i >= 0; i--) { const t = tiles[i]; if ((t.type === 'breakableblock' || t.type === 'questionblock')) { const cx = this.x + this.width/2; const cy = this.y + this.height/2; const tx = t.x + TILE_H/2; const ty = t.y + TILE_H/2; if (dist(cx, cy, tx, ty) < this.radius) { // 타일 제거 tiles.splice(i, 1); // 파괴 애니메이션 추가 breakEffects.push(new BreakEffect(t.x, t.y)); } } } } draw() { if (!this.exploded) { if(this.warning){ image(itemimgs.bomb_warning[0], this.x, this.y, this.width, this.height); } else { image(itemimgs.bomb[0], this.x, this.y, this.width, this.height); } } else { image(itemimgs.explosion[0], this.x - this.width, this.y - this.height, 3*this.width, 3*this.height); } } destroy() { return this.shouldRemove; } } class BigMissile { constructor(x, y, vx) { this.spawnTime = millis(); this.lifetime = 20000; this.shouldDestroy = false; this.width = 64; this.height = 64; this.vx = vx; this.x = x; this.y = y - this.height; } update(targets) { // 미사일 이동 this.x += this.vx; for (const t of targets) { // AABB 충돌 체크 const w = this.width * 2; const h = this.height * 2; const overlapX = this.x < t.x + t.width && this.x + w > t.x; const overlapY = this.y < t.y + t.height && this.y + h > t.y; if (!overlapX || !overlapY) continue; // 플레이어가 미사일 위에 서 있는지 검사 const playerBottom = t.y + t.height; const missileTop = this.y; // 아래로 충돌 시(떨어져서 올라탄 경우) if (t.vy > 0 && playerBottom <= missileTop + h * 0.1) { // 지면 위에 착지 처리 t.y = missileTop - t.height; t.vy = 0; } else { // 측면 충돌 시 겹침 방지용 강제 이동 if (this.vx > 0) { t.x = this.x + w; } else { t.x = this.x - t.width; } } break; } // 수명 검사 if (millis() - this.spawnTime > this.lifetime) { this.shouldDestroy = true; } } draw() { const img = itemimgs.bigmissile[0]; push(); if (this.vx > 0) { // 왼쪽 발사시 이미지 뒤집기 translate(this.x + this.width * 2, this.y); scale(-1, 1); image(img, 0, 0, this.width * 2, this.height * 2); } else { image(img, this.x, this.y, this.width * 2, this.height * 2); } pop(); } hits(target) { // 미사일 자체 크기로 충돌 영역 계산 (2배 확대된 폭) const w = this.width * 2; const h = this.height * 2; return ( this.x < target.x + target.width && this.x + w > target.x && this.y < target.y + target.height && this.y + h > target.y ); } destroy() { return this.shouldDestroy; } } class Item { constructor(type, x) { this.type = type; this.img = itemImgs[type][0]; this.x = x; this.y = -TILE_H; this.vy = 0; this.width = TILE_H; this.height = TILE_H; this.toRemove = false; } update() { // 중력 떨어뜨리기 this.vy += gravity; this.y += this.vy; // 화면 밖으로 나가면 제거 if (this.y > height) this.toRemove = true; // 플레이어 충돌 검사 (player1, player2) [player1, player2].forEach(p => { if (!this.toRemove && p.collides(this.x,this.y,this.width,this.height)) { p.applyItem(this.type); this.toRemove = true; } }); } draw() { image(this.img, this.x, this.y, this.width, this.height); } } function handleProjectiles() { for (let i = projectiles.length - 1; i >= 0; i--) { projectiles[i].update([player1, player2]); projectiles[i].draw(); if (projectiles[i].destroy()) { projectiles.splice(i, 1); console.log("shootend"); } } } function handleBombs() { for (let i = bombs.length - 1; i >= 0; i--) { const b = bombs[i]; b.update(); b.draw(); if (b.destroy()) { bombs.splice(i, 1); } } } function handleSpecialProjectiles() { for (let i = specialProjectiles.length - 1; i >= 0; i--) { specialProjectiles[i].update([player1, player2]); specialProjectiles[i].draw(); if (specialProjectiles[i].destroy()) { specialProjectiles.splice(i, 1); } } } function breakManager() { // 뒤에서부터 순회하며 업데이트·렌더·제거 처리 for (let i = breakEffects.length - 1; i >= 0; i--) { const e = breakEffects[i]; e.update(); e.draw(); if (!e.active) { breakEffects.splice(i, 1); } } }