Tekken / index.html
KaiShin1885's picture
Update index.html
e5f4407 verified
<!DOCTYPE html>
<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 !important;
}
@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>