Individual_Project/sketch.js
2025-04-27 23:30:16 +09:00

740 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 && 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;
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);
}
}
}