map + battle system

This commit is contained in:
Joowon Kim 2025-04-27 23:30:16 +09:00
parent e9bda6f7d3
commit f43d4314e9
2 changed files with 376 additions and 99 deletions

View File

@ -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]
};
}

447
sketch.js
View File

@ -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 && y<this.y+this.height && y+h>this.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);
}
}
}