Spaces:
Running
Running
<html> | |
<head> | |
<style> | |
#gameArea { | |
width: 800px; | |
height: 400px; | |
border: 2px solid black; | |
position: relative; | |
overflow: hidden; | |
background: #2a2a2a; | |
margin: auto; | |
} | |
.character { | |
width: 90px; | |
height: 200px; | |
position: absolute; | |
bottom: 0; | |
background-size: contain; | |
background-repeat: no-repeat; | |
background-position: center; | |
} | |
#player { | |
left: 100px; | |
background-image: url('kstand1.png'); | |
} | |
#enemy { | |
right: 100px; | |
background: red; | |
} | |
.healthBar { | |
width: 200px; | |
height: 20px; | |
background: #333; | |
position: fixed; | |
top: 20px; | |
border: 2px solid #fff; | |
} | |
.healthFill { | |
height: 100%; | |
width: 100%; | |
transition: width 0.1s; | |
} | |
#playerHealthBar { left: 20px; } | |
#enemyHealthBar { right: 20px; } | |
#playerHealthFill { background: linear-gradient(90deg, #44f, #88f); } | |
#enemyHealthFill { background: linear-gradient(90deg, #f44, #f88); } | |
#timer { | |
position: fixed; | |
top: 20px; | |
left: 50%; | |
transform: translateX(-50%); | |
font-size: 30px; | |
font-weight: bold; | |
color: #fff; | |
text-shadow: 2px 2px #000; | |
} | |
.attack { | |
position: absolute; | |
pointer-events: none; | |
} | |
.midAttack { | |
width: 30px; | |
height: 30px; | |
background: rgba(255,255,0,0.6); | |
border: 2px solid #ff0; | |
} | |
.highAttack { | |
width: 30px; | |
height: 30px; | |
background: rgba(255,128,0,0.6); | |
border: 2px solid #f80; | |
} | |
.ultimate { | |
width: 300px; | |
height: 100px; | |
background: rgba(255,0,0,0.6); | |
border: 3px solid #f00; | |
animation: ultimateEffect 0.3s linear; | |
} | |
.counter-effect { | |
position: absolute; | |
width: 45px; | |
height: 100px; | |
border: 2px solid #0ff; | |
animation: counterAnim 0.4s linear; | |
} | |
#instructions { | |
position: fixed; | |
right: 20px; | |
top: 60px; | |
padding: 15px; | |
background: rgba(0,0,0,0.8); | |
color: white; | |
border-radius: 5px; | |
font-family: monospace; | |
} | |
.facing-left { | |
transform: scaleX(-1); | |
} | |
.crouch { | |
height: 50px ; | |
} | |
@keyframes ultimateEffect { | |
0% { opacity: 0.3; } | |
50% { opacity: 0.8; } | |
100% { opacity: 0.3; } | |
} | |
@keyframes counterAnim { | |
0% { transform: scale(1); opacity: 1; } | |
100% { transform: scale(1.2); opacity: 0; } | |
} | |
</style> | |
</head> | |
<body> | |
<div id="gameArea"> | |
<div id="timer">60</div> | |
<div id="playerHealthBar" class="healthBar"> | |
<div id="playerHealthFill" class="healthFill"></div> | |
</div> | |
<div id="enemyHealthBar" class="healthBar"> | |
<div id="enemyHealthFill" class="healthFill"></div> | |
</div> | |
<div id="player" class="character"></div> | |
<div id="enemy" class="character"></div> | |
<div id="instructions"> | |
Controls:<br><br> | |
W - Jump<br> | |
A - Left<br> | |
D - Right<br> | |
S - Crouch<br> | |
J - Mid Attack (10dmg)<br> | |
K - High Attack (10dmg)<br> | |
Q - Ultimate (300dmg)<br> | |
L - Counter | |
</div> | |
</div> | |
<script> | |
const SETTINGS = { | |
FPS: 60, | |
FRAME_TIME: 1000 / 60, | |
MOVE_SPEED: 5, | |
JUMP_FORCE: 15, | |
GRAVITY: 0.8, | |
INITIAL_HEALTH: 1000, | |
DAMAGE: 10, | |
SPECIAL_DAMAGE: 300, | |
ATTACK_DELAY: 200, | |
COUNTER_WINDOW: 400, | |
COUNTER_COOLDOWN: 5000, | |
SPECIAL_COOLDOWN: 30000, | |
ANIMATION_INTERVAL: 500 | |
}; | |
const SPRITES = { | |
stand: ['kstand1.png', 'kstand2.png'], | |
midAttack: [ | |
'kmidattack1.png', | |
'kmidattack2.png', | |
'kmidattack3.png', | |
'kmidattack4.png', | |
'kmidattack5.png' | |
] | |
}; | |
class Character { | |
constructor(element, isPlayer = true) { | |
this.element = element; | |
this.isPlayer = isPlayer; | |
this.health = SETTINGS.INITIAL_HEALTH; | |
this.pos = { x: isPlayer ? 100 : 650, y: 0 }; | |
this.vel = { x: 0, y: 0 }; | |
this.direction = isPlayer ? 'right' : 'left'; | |
this.isMoving = false; | |
this.isAttacking = false; | |
this.isJumping = false; | |
this.isBlocking = false; | |
this.currentFrame = 0; | |
this.lastAnimationUpdate = 0; | |
this.midAttackHits = 0; | |
this.isInCombo = false; | |
this.currentAnimation = 'stand'; | |
this.animationFrame = 0; | |
this.lastAction = 0; // AI를 위한 마지막 행동 시간 추가 | |
if (isPlayer) { | |
this.element.style.backgroundImage = `url(${SPRITES.stand[0]})`; | |
} | |
} | |
updateAnimation(timestamp) { | |
if (!this.isPlayer) return; | |
if (timestamp - this.lastAnimationUpdate >= SETTINGS.ANIMATION_INTERVAL) { | |
if (this.currentAnimation === 'stand' && !this.isMoving && !this.isAttacking) { | |
this.currentFrame = (this.currentFrame + 1) % SPRITES.stand.length; | |
this.element.style.backgroundImage = `url(${SPRITES.stand[this.currentFrame]})`; | |
} | |
else if (this.currentAnimation === 'midAttack') { | |
this.animationFrame = (this.animationFrame + 1) % SPRITES.midAttack.length; | |
this.element.style.backgroundImage = `url(${SPRITES.midAttack[this.animationFrame]})`; | |
} | |
this.lastAnimationUpdate = timestamp; | |
} | |
} | |
move(direction) { | |
this.vel.x = direction * SETTINGS.MOVE_SPEED; | |
this.direction = direction > 0 ? 'right' : 'left'; | |
this.isMoving = true; | |
if (direction < 0) { | |
this.element.classList.add('facing-left'); | |
} else { | |
this.element.classList.remove('facing-left'); | |
} | |
} | |
stop() { | |
this.vel.x = 0; | |
this.isMoving = false; | |
} | |
updatePosition() { | |
// Update position based on velocity | |
this.pos.x += this.vel.x; | |
this.pos.x = Math.max(0, Math.min(755, this.pos.x)); | |
// Update vertical position if jumping | |
if (this.isJumping) { | |
this.vel.y += SETTINGS.GRAVITY; | |
this.pos.y = Math.max(0, this.pos.y - this.vel.y); | |
if (this.pos.y === 0) { | |
this.isJumping = false; | |
this.vel.y = 0; | |
} | |
} | |
// Update DOM element position | |
this.element.style.left = `${this.pos.x}px`; | |
this.element.style.bottom = `${this.pos.y}px`; | |
} | |
} | |
class Game { | |
constructor() { | |
this.lastFrameTime = 0; | |
this.gameTime = 60; | |
this.specialCooldown = Date.now() + SETTINGS.SPECIAL_COOLDOWN; | |
this.counterCooldown = 0; | |
this.canCounter = false; | |
this.lastHit = null; | |
this.isGameOver = false; | |
this.player = new Character(document.getElementById('player'), true); | |
this.enemy = new Character(document.getElementById('enemy'), false); | |
this.keys = {}; | |
this.setupControls(); | |
this.startGame(); | |
} | |
checkMidAttackHit() { | |
// 중단 공격이 성공했을 때만 호출됨 | |
if (this.player.currentAnimation === 'midAttack') { | |
this.player.midAttackHits++; | |
if (this.player.midAttackHits === 2) { | |
this.executeCombo(this.player, this.enemy); | |
} | |
} | |
} | |
executeCombo(attacker, defender) { | |
attacker.isInCombo = true; | |
// 첫번째 콤보 공격 (kmidattack3.png) | |
attacker.element.style.backgroundImage = `url(${SPRITES.midAttack[2]})`; | |
// 0.25초 후 두번째 이미지 | |
setTimeout(() => { | |
attacker.element.style.backgroundImage = `url(${SPRITES.midAttack[3]})`; | |
if (!defender.isBlocking) { | |
defender.health -= SETTINGS.DAMAGE; | |
this.updateHealthBars(); | |
} | |
}, 250); | |
// 0.5초 후 마지막 이미지 | |
setTimeout(() => { | |
attacker.element.style.backgroundImage = `url(${SPRITES.midAttack[4]})`; | |
if (!defender.isBlocking) { | |
defender.health -= SETTINGS.DAMAGE; | |
this.updateHealthBars(); | |
} | |
// 콤보 종료 | |
setTimeout(() => { | |
attacker.isInCombo = false; | |
attacker.midAttackHits = 0; | |
attacker.currentAnimation = 'stand'; | |
attacker.element.style.backgroundImage = `url(${SPRITES.stand[0]})`; | |
}, 250); | |
}, 500); | |
} | |
setupControls() { | |
document.addEventListener('keydown', (e) => { | |
if (this.isGameOver) return; | |
this.keys[e.key.toLowerCase()] = true; | |
switch(e.key.toLowerCase()) { | |
case 'j': | |
case 'k': | |
this.startAttack(this.player, this.enemy, | |
e.key === 'k' ? 'high' : 'mid'); | |
break; | |
case 'q': | |
if (Date.now() >= this.specialCooldown) { | |
this.useUltimate(this.player, this.enemy); | |
} | |
break; | |
case 'l': | |
this.tryCounter(); | |
break; | |
case 's': | |
this.player.element.classList.add('crouch'); | |
break; | |
} | |
}); | |
document.addEventListener('keyup', (e) => { | |
this.keys[e.key.toLowerCase()] = false; | |
if (e.key.toLowerCase() === 's') { | |
this.player.element.classList.remove('crouch'); | |
} | |
}); | |
} | |
startAttack(attacker, defender, type) { | |
if (attacker.isInCombo) return; // 콤보 중에는 새 공격 불가 | |
attacker.isAttacking = true; | |
// 중단 공격일 경우 애니메이션 처리 | |
if (type === 'mid' && attacker.isPlayer) { | |
attacker.currentAnimation = 'midAttack'; | |
attacker.element.style.backgroundImage = `url(${SPRITES.midAttack[0]})`; | |
setTimeout(() => { | |
attacker.element.style.backgroundImage = `url(${SPRITES.midAttack[1]})`; | |
}, 100); | |
} | |
const attackEl = document.createElement('div'); | |
attackEl.className = `attack ${type}Attack`; | |
const xOffset = attacker.direction === 'right' ? 45 : -30; | |
const yOffset = type === 'high' ? 70 : 35; | |
attackEl.style.left = `${attacker.pos.x + xOffset}px`; | |
attackEl.style.bottom = `${yOffset}px`; | |
document.getElementById('gameArea').appendChild(attackEl); | |
setTimeout(() => { | |
attackEl.remove(); | |
if (!defender.isBlocking) { | |
defender.health -= SETTINGS.DAMAGE; | |
if (type === 'mid' && attacker.isPlayer) { | |
this.checkMidAttackHit(); | |
} | |
this.updateHealthBars(); | |
this.checkGameOver(); | |
} | |
}, SETTINGS.ATTACK_DELAY); | |
setTimeout(() => { | |
attacker.isAttacking = false; | |
if (!attacker.isInCombo) { | |
attacker.currentAnimation = 'stand'; | |
attacker.element.style.backgroundImage = `url(${SPRITES.stand[0]})`; | |
} | |
}, SETTINGS.ATTACK_DELAY + 100); | |
} | |
useUltimate(attacker, defender) { | |
const ultimateEl = document.createElement('div'); | |
ultimateEl.className = 'attack ultimate'; | |
const xOffset = attacker.direction === 'right' ? 45 : -300; | |
ultimateEl.style.left = `${attacker.pos.x + xOffset}px`; | |
ultimateEl.style.bottom = '0px'; | |
document.getElementById('gameArea').appendChild(ultimateEl); | |
setTimeout(() => { | |
if (!defender.isBlocking) { | |
defender.health -= SETTINGS.SPECIAL_DAMAGE; | |
this.updateHealthBars(); | |
this.checkGameOver(); | |
} | |
ultimateEl.remove(); | |
}, 300); | |
this.specialCooldown = Date.now() + SETTINGS.SPECIAL_COOLDOWN; | |
} | |
tryCounter() { | |
if (Date.now() < this.counterCooldown) return; | |
const counterEl = document.createElement('div'); | |
counterEl.className = 'counter-effect'; | |
counterEl.style.left = `${this.player.pos.x}px`; | |
counterEl.style.bottom = '0px'; | |
document.getElementById('gameArea').appendChild(counterEl); | |
setTimeout(() => counterEl.remove(), 400); | |
if (this.lastHit && Date.now() - this.lastHit.time <= SETTINGS.COUNTER_WINDOW) { | |
this.player.isBlocking = true; | |
setTimeout(() => { | |
this.player.isBlocking = false; | |
}, 400); | |
} else { | |
this.counterCooldown = Date.now() + SETTINGS.COUNTER_COOLDOWN; | |
} | |
} | |
updateAI() { | |
if (Date.now() - this.enemy.lastAction < 300) return; | |
const distance = Math.abs(this.player.pos.x - this.enemy.pos.x); | |
const healthRatio = this.enemy.health / this.player.health; | |
if (healthRatio < 0.7 || distance < 80) { | |
this.enemy.vel.x = -SETTINGS.MOVE_SPEED; | |
} else if (distance > 150) { | |
this.enemy.vel.x = SETTINGS.MOVE_SPEED; | |
} else if (Math.random() < 0.05) { | |
this.startAttack(this.enemy, this.player, | |
Math.random() > 0.5 ? 'high' : 'mid'); | |
} | |
this.enemy.direction = this.player.pos.x > this.enemy.pos.x ? | |
'right' : 'left'; | |
} | |
update(timestamp) { | |
if (timestamp - this.lastFrameTime >= SETTINGS.FRAME_TIME) { | |
// Jump handling | |
if (this.keys['w'] && !this.player.isJumping) { | |
this.player.isJumping = true; | |
this.player.vel.y = -SETTINGS.JUMP_FORCE; | |
} | |
// Movement handling | |
if (this.keys['a']) { | |
this.player.vel.x = -SETTINGS.MOVE_SPEED; | |
this.player.direction = 'left'; | |
this.player.isMoving = true; | |
this.player.element.classList.add('facing-left'); | |
} else if (this.keys['d']) { | |
this.player.vel.x = SETTINGS.MOVE_SPEED; | |
this.player.direction = 'right'; | |
this.player.isMoving = true; | |
this.player.element.classList.remove('facing-left'); | |
} else { | |
this.player.isMoving = false; | |
this.player.vel.x = 0; // 키를 떼면 속도를 즉시 0으로 설정 | |
} | |
// Update characters | |
[this.player, this.enemy].forEach(char => { | |
if (char.isJumping) { | |
char.vel.y += SETTINGS.GRAVITY; | |
char.pos.y = Math.max(0, char.pos.y - char.vel.y); | |
if (char.pos.y === 0) { | |
char.isJumping = false; | |
char.vel.y = 0; | |
} | |
} | |
char.pos.x += char.vel.x; | |
char.pos.x = Math.max(0, Math.min(755, char.pos.x)); | |
// Remove this line: char.vel.x *= 0.8; | |
char.element.style.left = `${char.pos.x}px`; | |
char.element.style.bottom = `${char.pos.y}px`; | |
char.updateAnimation(timestamp); | |
}); | |
this.updateAI(); | |
this.lastFrameTime = timestamp; | |
} | |
if (!this.isGameOver) { | |
requestAnimationFrame(this.update.bind(this)); | |
} | |
} | |
updateHealthBars() { | |
document.getElementById('playerHealthFill').style.width = | |
`${(this.player.health / SETTINGS.INITIAL_HEALTH) * 100}%`; | |
document.getElementById('enemyHealthFill').style.width = | |
`${(this.enemy.health / SETTINGS.INITIAL_HEALTH) * 100}%`; | |
} | |
checkGameOver() { | |
if (this.player.health <= 0 || this.enemy.health <= 0) { | |
this.endGame(); | |
} | |
} | |
startGame() { | |
this.updateHealthBars(); | |
requestAnimationFrame(this.update.bind(this)); | |
const timer = setInterval(() => { | |
this.gameTime--; | |
document.getElementById('timer').textContent = this.gameTime; | |
if (this.gameTime <= 0) { | |
clearInterval(timer); | |
this.endGame(); | |
} | |
}, 1000); | |
} | |
endGame() { | |
this.isGameOver = true; | |
const winner = this.player.health > this.enemy.health ? 'Player' : 'Enemy'; | |
alert(`Game Over! ${winner} wins!`); | |
} | |
} | |
new Game(); | |
</script> | |
</body> | |
</html> |