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> |