From f43d4314e974314f28a16abc0199f4d00a953a02 Mon Sep 17 00:00:00 2001 From: joowon Date: Sun, 27 Apr 2025 23:30:16 +0900 Subject: [PATCH] map + battle system --- imageAsset.js | 28 +++- sketch.js | 447 +++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 376 insertions(+), 99 deletions(-) diff --git a/imageAsset.js b/imageAsset.js index 97cbd98..2313324 100644 --- a/imageAsset.js +++ b/imageAsset.js @@ -1,5 +1,5 @@ let spriteSheets = {}; -let P1imgs = {}, P2imgs = {}, itemimgs = {}, tileimgs = {}; +let P1imgs = {}, P2imgs = {}, itemimgs = {}, tileimgs = {}, decoimgs = {}; let backgroundManager; let gravity = 0.8; @@ -58,13 +58,15 @@ function sliceAssets() { const spsrc = spriteSheets.specialweapon; const ow=16, oh=16; const mush = createImage(ow,oh); mush.copy(src,1,2126,ow,oh,0,0,ow,oh); + const bombadd = createImage(ow,oh); bombadd.copy(spsrc, 39, 117, ow, oh, 0, 0, ow, oh); const poison= createImage(ow,oh); poison.copy(src,1,2143,ow,oh,0,0,ow,oh); const giant = createImage(2*ow,2*oh); giant.copy(src,35,2143,2*ow,2*oh,0,0,2*ow,2*oh); const fire = createImage(ow/2,oh/2); fire.copy(src,101,2177,ow/2,oh/2,0,0,ow/2,oh/2); + const fireinch = createImage(ow, oh); fireinch.copy(spsrc, 601, 751, ow, oh, 0, 0, ow, oh); const bomb = createImage(ow,oh); bomb.copy(src,194,2143,ow,oh,0,0,ow,oh); const bm = createImage(4*ow,4*oh); bm.copy(spsrc,127,356,4*ow,4*oh,0,0,4*ow,4*oh); const beffect = createImage(1.5*ow, 1.5*oh); beffect.copy(spsrc, 604, 413, 1.5*ow, 1.5*oh, 0, 0, 1.5*ow, 1.5*oh); - [mush,poison,giant,fire,bomb,bm, beffect].forEach(img=>applyChromaKey(img)); + [mush,bombadd,poison,giant,fire,fireinch,bomb,bm, beffect].forEach(img=>applyChromaKey(img)); function applyColorFilter(img, delta) { // img.pixels 에 접근해 기존 색상을 유지하며 R 증가, G 감소 img.loadPixels(); @@ -89,7 +91,7 @@ function sliceAssets() { applyColorFilter(bombWarn, {r: 150, g: 100, b:200} ); itemimgs = { - mush:[mush], poison:[poison], giant:[giant], + mush:[mush], poison:[poison], giant:[giant], bombadd:[bombadd], fire_inchant:[fireinch], fire:[fire], bomb:[bomb], bigmissile:[bm], bomb_warning:[bombWarn], explosion:[beffect] }; @@ -97,13 +99,27 @@ function sliceAssets() { const bb = createImage(ow, oh); bb.copy(tilesrc, 18, 23, ow, oh, 0, 0, ow, oh); const qb = createImage(ow, oh); qb.copy(tilesrc, 35, 23, ow, oh, 0, 0, ow, oh); const gb = createImage(ow, oh); gb.copy(tilesrc, 154, 142, ow, oh, 0, 0, ow, oh); - [bb, qb, gb].forEach(img => applyChromaKey(img)); + const gb1 = createImage(ow, oh); gb1.copy(tilesrc, 171, 74, ow, oh, 0, 0, ow, oh); + [bb, qb, gb, gb1].forEach(img => applyChromaKey(img)); tileimgs = { breakableblock: [bb], - qustionblock: [qb], - groundblock: [gb] + questionblock: [qb], + groundblock: [gb], + groundblock1: [gb1] + }; + const gb2 = createImage(ow, oh); gb2.copy(tilesrc, 171, 91, ow, oh, 0, 0, ow, oh); + const breffect1 = createImage(2*ow, 2*oh); breffect1.copy(spsrc, 640, 1, 2*ow, 2*oh, 0, 0, 2*ow, 2*oh); + const breffect2 = createImage(2*ow, 2*oh); breffect2.copy(spsrc, 673, 1, 2*ow, 2*oh, 0, 0, 2*ow, 2*oh); + const breffect3 = createImage(2*ow, 2*oh); breffect3.copy(spsrc, 706, 1, 2*ow, 2*oh, 0, 0, 2*ow, 2*oh); + [gb2, breffect1, breffect2, breffect3].forEach(img => applyChromaKey(img)); + decoimgs = { + groundblock2: [gb2], + breakeffect1: [breffect1], + breakeffect2: [breffect2], + breakeffect3: [breffect3] }; + } \ No newline at end of file diff --git a/sketch.js b/sketch.js index d7fe325..b96d2b9 100644 --- a/sketch.js +++ b/sketch.js @@ -1,7 +1,45 @@ let player1, player2; +let tiles = [], decoTiles = []; let projectiles = [], specialProjectiles = [], bombs = []; -let groundY = 500, deathZoneY = 600; +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(); @@ -11,16 +49,33 @@ function setup() { createCanvas(800, 600); sliceAssets(); - controlsP1 = { left:'ArrowLeft', right:'ArrowRight', jump:'ArrowUp', attack:'[', bomb:']' }; - controlsP2 = { left:'a', right:'d', jump:'w', attack:'t', bomb:'y' }; + 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(); - rect(0, groundY, width, 100); + decoTiles.forEach(t => t.draw()); + tiles.forEach(t => t.draw()); player1.update(); player1.draw(); player2.update(); player2.draw(); @@ -28,6 +83,7 @@ function draw() { handleProjectiles(); handleBombs(); handleSpecialProjectiles(); + breakManager(); } function keyPressed() { @@ -40,6 +96,63 @@ function keyReleased() { 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; @@ -74,25 +187,6 @@ class Background { } } } - -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); - } - // Full AABB for breakable & question blocks - collides(x,y,w,h) { - return ( - x < this.x+this.width && x+w > this.x && - y < this.y+this.height && y+h > this.y - ); - } -} - class Player { constructor(x, y, imgSet, controls) { this.x = x; this.y = y; @@ -101,57 +195,124 @@ class Player { this.width = 32; this.height = 32; this.onGround = false; this.jumpCount = 0; - this.deathCount = 10; this.imgSet = imgSet; this.controls = controls; this.keys = {}; this.bombHoldStartTime = null; - this.skill = { resist:false, fastFire:false, isGiant:false, itemCount:0 }; + 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() { - // decrement attack timer + // 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) { - // return to appropriate post-attack state - this.state = this.onGround ? 'idle' : 'jump'; + 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; + } } } - 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.vy += gravity; - this.x += this.vx; - this.y += this.vy; - - - if (this.y + this.height >= groundY) { - this.y = groundY - this.height; - this.vy = 0; - this.onGround = true; - this.jumpCount = 0; + if (landed) { if (this.attackTimer === 0) this.state = 'idle'; - } - else { + } 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; - if (this.y > deathZoneY) { - this.deathCount--; - this.state = "dead"; - this.respawn(); + // 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; } } @@ -175,11 +336,12 @@ class Player { 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; + const by = this.y - 8; const v_bomb = this.facing === 'right' ? 5 : -5; bombs.push(new Bomb(bx, by, v_bomb)); this.state = 'shoot'; @@ -197,6 +359,7 @@ class Player { specialProjectiles.push(new BigMissile(spawnX, spawnY, dir)); this.state = 'shoot'; this.attackTimer = 15; + this.knockbackVX = this.facing === 'right' ? -5 : 5; this.frame = 0; } @@ -211,13 +374,27 @@ class Player { this.keys[k] = false; if (k === this.controls.bomb && this.bombHoldStartTime !== null) { const held = millis() - this.bombHoldStartTime; - if (held >= 1000 ) this.fireBigMissile(); + 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') { @@ -277,47 +454,75 @@ class Projectile { class Bomb { constructor(x, y, vx) { - this.x = x; - this.y = y; + 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.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) { - // 타이머가 0 이 되면 폭발 시작 this.timer--; - this.vy += gravity; - this.x += this.vx; - this.y += this.vy; - // 바닥 튕김 - if (this.y + this.height >= groundY) { - this.y = groundY - this.height; - this.vy = -0.5 * this.vy; - this.vx = 0.7 * this.vx; - } - if(this.timer <= 60) { - this.warning = true; - } + if (this.timer <= 60) this.warning = true; if (this.timer <= 0) { this.exploded = true; - this.explode(); // 넉백 한 번 적용 + this.explode(); + return; } - } - else { - // 폭발 시각화가 끝나면 제거 + } else { + // 폭발 애니메이션 this.explodeTimer--; - if (this.explodeTimer <= 0) { - this.shouldRemove = true; + 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; } } } @@ -338,6 +543,22 @@ class Bomb { 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)); + } + } + } } @@ -352,14 +573,7 @@ class Bomb { } else { - const img = itemimgs.explosion[0]; - image( - img, - this.x - this.width, - this.y - this.height, - 3*this.width, - 3*this.height - ); + image(itemimgs.explosion[0], this.x - this.width, this.y - this.height, 3*this.width, 3*this.height); } } @@ -403,8 +617,13 @@ class BigMissile { } else { // 측면 충돌 시 겹침 방지용 강제 이동 - if (this.vx > 0) t.x = this.x + w; - else t.x = this.x - t.width; + if (this.vx > 0) { + t.x = this.x + w; + + } + else { + t.x = this.x - t.width; + } } break; } @@ -447,6 +666,37 @@ class BigMissile { } } 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--) { @@ -476,4 +726,15 @@ function handleSpecialProjectiles() { 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); + } + } } \ No newline at end of file