item + bgm

This commit is contained in:
Joowon Kim 2025-04-28 05:29:48 +09:00
parent f43d4314e9
commit dd82b978ef
58 changed files with 317 additions and 52 deletions

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
loop start: 43778 samples (0:01.368 seconds)
loop end: 66338 samples (0:02.073 seconds)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
loop start: 0 samples (0:00.000 seconds)
loop end: 2000 samples (0:00.063 seconds)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
loop start: 0 samples (0:00.000 seconds)
loop end: 34616 samples (0:01.082 seconds)

Binary file not shown.

View File

@ -0,0 +1,2 @@
loop start: 0 samples (0:00.000 seconds)
loop end: 34616 samples (0:01.082 seconds)

Binary file not shown.

View File

@ -0,0 +1,2 @@
loop start: 9226 samples (0:00.288 seconds)
loop end: 35018 samples (0:01.094 seconds)

Binary file not shown.

View File

@ -0,0 +1,2 @@
loop start: 0 samples (0:00.000 seconds)
loop end: 26050 samples (0:00.814 seconds)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
loop start: 0 samples (0:00.000 seconds)
loop end: 80563 samples (0:02.518 seconds)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -9,6 +9,9 @@ function preloadAssets() {
spriteSheets.characters = loadImage("assets/Mario-Character+Item.png");
spriteSheets.specialweapon = loadImage("assets/Mario-Enemy.png");
spriteSheets.tileset = loadImage("assets/Mario-Tileset.png");
soundFormats('mp3', 'wav');
}
function sliceAssets() {
@ -42,6 +45,7 @@ function sliceAssets() {
const mj = createImage(cw,ch); mj.copy(src,215,98, cw,ch,0,0, cw,ch);
const ma = createImage(cw,ch); ma.copy(src,627,98, cw,ch,0,0, cw,ch);
[mi,mw1,mw2,mw3,mj,ma].forEach(img=>applyChromaKey(img));
P1imgs = { idle:[mi], walk:[mw1,mw2,mw3], jump:[mj], shoot:[ma] };
// 5) 캐릭터 프레임 슬라이스 (Luigi)
@ -52,6 +56,7 @@ function sliceAssets() {
const lj = createImage(cw,ch); lj.copy(src,215,629,cw,ch,0,0,cw,ch);
const la = createImage(cw,ch); la.copy(src,627,629,cw,ch,0,0,cw,ch);
[li,lw1,lw2,lw3,lj,la].forEach(img=>applyChromaKey(img));
P2imgs = { idle:[li], walk:[lw1,lw2,lw3], jump:[lj], shoot:[la] };
// 6) 아이템 및 특수 투사체 프레임
@ -60,13 +65,13 @@ function sliceAssets() {
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 giant = createImage(ow,oh); giant.copy(src,52,2126,ow,oh,0,0,ow,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 fireench = createImage(ow, oh); fireench.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,bombadd,poison,giant,fire,fireinch,bomb,bm, beffect].forEach(img=>applyChromaKey(img));
[mush,bombadd,poison,giant,fire,fireench,bomb,bm, beffect].forEach(img=>applyChromaKey(img));
function applyColorFilter(img, delta) {
// img.pixels 에 접근해 기존 색상을 유지하며 R 증가, G 감소
img.loadPixels();
@ -91,7 +96,7 @@ function sliceAssets() {
applyColorFilter(bombWarn, {r: 150, g: 100, b:200} );
itemimgs = {
mush:[mush], poison:[poison], giant:[giant], bombadd:[bombadd], fire_inchant:[fireinch],
mush:[mush], poison:[poison], giant:[giant], bombadd:[bombadd], fire_enchant:[fireench],
fire:[fire], bomb:[bomb], bigmissile:[bm], bomb_warning:[bombWarn], explosion:[beffect]
};

View File

@ -15,7 +15,7 @@
</head>
<script src="imageAsset.js"></script>
<script src="soundAsset.js"></script>
<script defer src="sketch.js"></script>
<body></body>

321
sketch.js
View File

@ -7,7 +7,7 @@ let controlsP1, controlsP2;
let breakEffects = [];
let items = [];
let nextItemFrame = 0;
const TILE_H = 32;
const TILE_SIZE = 32;
const mapLayout = [
// row 0 (y = groundY[0]16): gb1 | question | gb1
[
@ -43,12 +43,15 @@ const mapLayout = [
function preload() {
preloadAssets();
preloadSounds()
}
function setup() {
createCanvas(800, 600);
sliceAssets();
bgm.bgmGround.setLoop(true);
bgm.bgmGround.setVolume(0.5);
bgm.bgmGround.loop();
controlsP1 = {
left: 'ArrowLeft', right: 'ArrowRight',
jump: 'ArrowUp', attack: '[', bomb: ']', down: 'ArrowDown'
@ -62,10 +65,10 @@ function setup() {
player2 = new Player(200, 100, P2imgs, controlsP2);
for (let row = 0; row < groundY.length; row++) {
const y = groundY[row] - TILE_H;
const y = groundY[row] - TILE_SIZE;
for (let seg of mapLayout[row]) {
if (!seg.type) continue; // null인 구간은 건너뛰고
for (let x = seg.x1; x <= seg.x2; x += TILE_H) {
for (let x = seg.x1; x <= seg.x2; x += TILE_SIZE) {
tiles.push(new Tile(x, y, seg.type));
}
}
@ -80,6 +83,8 @@ function draw() {
player1.update(); player1.draw();
player2.update(); player2.draw();
randomSpawnItem();
handleProjectiles();
handleBombs();
handleSpecialProjectiles();
@ -150,7 +155,7 @@ class BreakEffect {
draw() {
if (!this.active) return;
const img = this.frames[this.currentFrame];
image(img, this.x, this.y, TILE_H, TILE_H);
image(img, this.x, this.y, TILE_SIZE, TILE_SIZE);
}
}
class Background {
@ -204,15 +209,71 @@ class Player {
this.facing = "right";
this.state = "idle";
this.frame = 0;
this._animTimer = 0;
this._animInterval = 6;
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
this.itemCount = 0;
this.bombCount = 10;
this.bigMissileCount = 0;
// 효과 타이머들 (frame 단위)
this.fireTimer = 0;
this.poisonTimer = 0;
this.giantTimer = 0;
this.deathCount = 5; // 남은 목숨
this.invulnerable = false; // 무적 플래그
this.invTimer = 0; // 무적 남은 프레임
}
applyItem(type) {
this.itemCount++;
if (this.itemCount % 2 === 0) {
this.bigMissileCount++;
}
switch(type) {
case 'mush':
this.fireTimer = 8 * 60; // 8초간 (60fps 가정)
break;
case 'poison':
this.poisonTimer = 3 * 60; // 3초간
break;
case 'giant':
this.giantTimer = 5 * 60; // 5초간
break;
case 'bombadd':
this.bombCount += 5;
break;
}
}
update() {
// (1) poison 효과: 이동 불가
if (this.poisonTimer > 0) {
this.poisonTimer--;
// 이동 벡터 강제 0
this.vx = this.vy = 0;
return;
}
// (2) giant 효과: 크기 및 공격 크기 조정
if (this.giantTimer > 0) {
this.giantTimer--;
this.width = 64; this.height = 64;
}
else {
// 기본 크기로 복원
this.width = 32; this.height = 32;
}
// (3) mush 효과: fire 인챈트
if (this.fireTimer > 0) {
this.fireTimer--;
}
// Bomb charging
if (this.keys[this.controls.bomb] && this.bombHoldStartTime !== null) {
this.chargeTime = min(millis() - this.bombHoldStartTime, this.maxCharge);
@ -272,13 +333,27 @@ class Player {
}
if (landed) {
if (this.attackTimer === 0) this.state = 'idle';
} else {
if (this.attackTimer === 0) {
if(inputVX !== 0) {
this.state = 'walk'
}
else{
this.state = 'idle';
}
}
}
else {
this.y = nextY;
this.onGround = false;
if (this.attackTimer === 0) this.state = 'jump';
}
if (this.state === 'walk') {
this._animTimer++;
} else {
this._animTimer = 0;
}
// DOWN 키로 현재 플랫폼 행 통과
if (
this.keys[this.controls.down] &&
@ -317,8 +392,20 @@ class Player {
}
respawn() {
this.x = 100; this.y = 100;
this.vx = this.vy = this.knockbackVX = 0;
// 목숨 하나 감소
this.deathCount--;
if (this.deathCount > 0) {
// 맵 가운데 상단으로 리셋
this.x = width/2 - this.width/2;
this.y = 0;
this.vx = this.vy = this.knockbackVX = 0;
// 0.5초 무적
this.invulnerable = true;
this.invTimer = 30; // 60fps 기준 30프레임
} else {
// 목숨 모두 소진 → 게임 오버
gameOver = true;
}
}
jump() {
@ -331,8 +418,8 @@ class Player {
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));
const dir = this.facing === 'right' ? 15 : -15;
projectiles.push(new Projectile(spawnX, spawnY, dir, this.fireTimer > 0, this.giantTimer > 0));
this.state = 'shoot';
this.attackTimer = 10;
this.frame = 0;
@ -340,10 +427,14 @@ class Player {
}
dropBomb() {
if (this.bombCount <= 0) return;
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));
const normalizedV = 1 + map(this.chargeTime, 0, 1000, 0, 1);
const v_bombx = this.facing === 'right' ? 5 * normalizedV : -5 * normalizedV;
const v_bomby = - 2 * (normalizedV);
bombs.push(new Bomb(bx, by, v_bombx, v_bomby));
this.bombCount--;
this.state = 'shoot';
this.attackTimer = 10;
this.frame = 0;
@ -382,6 +473,13 @@ class Player {
}
draw() {
if (this.poisonTimer > 0) {
// R=255, G=B=150 정도: 연한 붉은빛으로
tint(200, 100, 255);
}
else {
noTint();
}
// charge gauge 그리기
if (this.chargeTime > 0) {
// 최대 너비를 this.width(32px)로 매핑
@ -395,6 +493,15 @@ class Player {
rect(this.x, this.y - 10, w, 5);
noFill();
}
if (this.state === 'walk') {
const seq = this.imgSet.walk; // [walk1, walk2, walk3]
this.frame = Math.floor(this._animTimer / this._animInterval) % seq.length;
}
else {
// walk 외 상태는 0번 프레임 고정 (idle 이나 shoot 등)
this.frame = 0;
}
const img = this.imgSet[this.state][this.frame];
push();
if (this.facing === 'left') {
@ -405,36 +512,77 @@ class Player {
image(img, this.x, this.y, this.width, this.height);
}
pop();
noTint();
}
}
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;
constructor(x, y, vx, enchanted = false, isgiant = false) {
this.x = x;
this.y = y;
this.vx = vx;
this.enchanted = enchanted;
this.isgiant = isgiant;
// 크기 세팅
if (enchanted) {
this.width = 16;
this.height = 16;
this.knockbackFactor = 1.0; // vx * knockbackFactor 계산 → 사실상 2배
this.sprite = itemimgs.fire_enchant[0];
}
else if(isgiant) {
this.width = 16;
this.height = 16;
this.knockbackFactor = 0.5;
this.sprite = itemimgs.fire[0];
}
else {
this.width = 8;
this.height = 8;
this.knockbackFactor = 0.5;
this.sprite = itemimgs.fire[0];
}
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 (!this.shouldDestroy && this.hits(t)) {
if (t.giantTimer > 0) {
// giant 상태면 넉백 없이 그냥 삭제
this.shouldDestroy = true;
}
else {
// giant 상태 아니면 넉백
t.knockbackVX += this.vx * this.knockbackFactor;
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);
image(
this.sprite,
this.x, this.y,
this.width, this.height
);
}
destroy() {
@ -443,9 +591,9 @@ class Projectile {
hits(target) {
return (
this.x < target.x + target.width &&
this.x + this.width > target.x &&
this.y < target.y + target.height &&
this.x < target.x + target.width &&
this.x + this.width > target.x &&
this.y < target.y + target.height &&
this.y + this.height > target.y
);
}
@ -453,9 +601,9 @@ class Projectile {
class Bomb {
constructor(x, y, vx) {
constructor(x, y, vx, vy) {
this.x = x; this.y = y;
this.vx = vx; this.vy = 0;
this.vx = vx; this.vy = vy;
this.width = 32; this.height = 32;
this.timer = 120;
@ -539,8 +687,15 @@ class Bomb {
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;
if(p.giantTimer > 0) {
p.knockbackVX += cos(angle) * force * 0.5;
p.vy += sin(angle) * force * 0.5;
}
else {
p.knockbackVX += cos(angle) * force * 2;
p.vy += sin(angle) * force * 2;
}
}
});
@ -549,8 +704,8 @@ class Bomb {
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;
const tx = t.x + TILE_SIZE/2;
const ty = t.y + TILE_SIZE/2;
if (dist(cx, cy, tx, ty) < this.radius) {
// 타일 제거
tiles.splice(i, 1);
@ -667,36 +822,85 @@ 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.type = type;
this.img = itemimgs[type][0];
this.x = x;
this.y = -TILE_SIZE;
this.vy = 0;
this.width = TILE_SIZE;
this.height = TILE_SIZE;
this.toRemove = false;
this.stuck = false; // 착지 플래그
}
/** 기존 hit 기능: target 오브젝트와의 겹침(충돌) 확인 */
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
);
}
/** 새로운 메서드: 타일 위 착지 판정만 담당 */
landOnTiles() {
// 아래로 떨어질 때만 검사
if (this.vy <= 0) return;
const nextY = this.y + this.vy;
for (let tile of tiles) {
const prevBot = this.y + this.height;
const currBot = nextY + this.height;
// “위→아래 궤적 교차” + 가로 범위 겹침
if (prevBot <= tile.y &&
currBot >= tile.y &&
this.x + this.width > tile.x &&
this.x < tile.x + tile.width
) {
// 딱 타일 위에 착지
this.y = tile.y - this.height;
this.vy = 0;
this.stuck = true;
return;
}
}
// 착지 못했으면 실제 y 갱신
this.y = nextY;
}
update() {
// 중력 떨어뜨리기
this.vy += gravity;
this.y += this.vy;
// 1) 폭발 같은 특별 로직이 없으므로 바로
// 착지 전이라면 중력+착지 판정
if (!this.stuck) {
this.vy += 0.5 * gravity;
this.landOnTiles();
}
// 화면 밖으로 나가면 제거
if (this.y > height) this.toRemove = true;
// 2) 화면 아래로 벗어나면 제거
if (this.y > height) {
this.toRemove = true;
return;
}
// 플레이어 충돌 검사 (player1, player2)
[player1, player2].forEach(p => {
if (!this.toRemove && p.collides(this.x,this.y,this.width,this.height)) {
// 3) 플레이어 충돌 판정 (hits 메서드 재사용)
for (let p of [player1, player2]) {
if (!this.toRemove && this.hits(p)) {
p.applyItem(this.type);
this.toRemove = true;
}
});
}
}
draw() {
image(this.img, this.x, this.y, this.width, this.height);
}
destroy() {
return this.toRemove;
}
}
function handleProjectiles() {
for (let i = projectiles.length - 1; i >= 0; i--) {
@ -738,3 +942,28 @@ function breakManager() {
}
}
}
function randomSpawnItem() {
// 1) 랜덤 타이밍에 새로운 아이템 스폰
if (frameCount >= nextItemFrame) {
// 타입 선택
const types = ['mush','poison','giant','bombadd'];
const type = random(types);
// X 위치는 화면 가로 범위 안에서 랜덤
const x = random(0, width - TILE_SIZE);
items.push(new Item(type, x));
// 다음 스폰 타이밍: 3~8초 뒤
nextItemFrame = frameCount + floor(random(3, 8) * 60);
}
// 2) 기존 아이템들 업데이트 → 드로우 → 필요 시 제거
for (let i = items.length - 1; i >= 0; i--) {
const it = items[i];
it.update();
it.draw();
if (it.toRemove) {
items.splice(i, 1);
}
}
}

17
soundAsset.js Normal file
View File

@ -0,0 +1,17 @@
let effectSound = {};
let bgm = {};
function preloadSounds() {
soundFormats('mp3','wav');
bgm.bgmGround = loadSound('assets/M1_BGM_Ground_Play.mp3');
bgm.bgmGroundHurry = loadSound('assets/M1_BGM_Ground_PlayHurry.mp3');
effectSound.jump = loadSound('assets/SMB1 Sounds/M1_SmallMarioJump.wav');
effectSound.fire = loadSound('assets/SMB1 Sounds/M1_fire.wav');
effectSound.dead = loadSound('assets/SMB1 Sounds/M1_HitFloor.wav');
effectSound.getItem = loadSound('assets/SMB1 Sounds/M1_PowerUp.wav');
effectSound.breakBlock = loadSound('assets/SMB1 Sounds/M1_BreakBlock.wav');
effectSound.bomb = loadSound('assets/SMB1 Sounds/SE_chargeshot.wav');
effectSound.bigmissile = loadSound('assets/SMB1 Sounds/M1_FireLong.wav');
effectSound.victory = loadSound('assets/SMB1 Sounds/M1_CourseClearFanfare.wav');
}