From 71265b4f239f595d25854e79162e41a978561994 Mon Sep 17 00:00:00 2001 From: st2411020104 Date: Thu, 8 Jan 2026 15:10:40 +0800 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20=E4=B8=89=E5=9B=BD=E6=9D=80?= =?UTF-8?q?=E6=B8=B8=E6=88=8F=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game.js | 1801 ++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 183 ++++++ style.css | 879 +++++++++++++++++++++++++ 3 files changed, 2863 insertions(+) create mode 100644 game.js create mode 100644 index.html create mode 100644 style.css diff --git a/game.js b/game.js new file mode 100644 index 0000000..5d7ad37 --- /dev/null +++ b/game.js @@ -0,0 +1,1801 @@ +const CHARACTERS = [ + { name: '刘备', hp: 4, color: '#4169E1', skill: '仁德', skillDesc: '可以将任意手牌赠予其他角色' }, + { name: '曹操', hp: 4, color: '#8B4513', skill: '奸雄', skillDesc: '受到伤害后,可以获得造成伤害的牌' }, + { name: '孙权', hp: 4, color: '#228B22', skill: '制衡', skillDesc: '可以弃置任意张手牌,然后摸等量的牌' }, + { name: '关羽', hp: 4, color: '#DC143C', skill: '武圣', skillDesc: '可以将红色手牌当杀使用' }, + { name: '张飞', hp: 4, color: '#FF4500', skill: '咆哮', skillDesc: '出牌阶段可以使用任意张杀' }, + { name: '赵云', hp: 4, color: '#1E90FF', skill: '龙胆', skillDesc: '可以将杀当闪使用,或将闪当杀使用' }, + { name: '诸葛亮', hp: 3, color: '#9370DB', skill: '观星', skillDesc: '回合开始时可以查看牌堆顶的牌' }, + { name: '黄月英', hp: 3, color: '#FF69B4', skill: '集智', skillDesc: '使用锦囊牌后可以摸一张牌' }, + { name: '吕布', hp: 4, color: '#8B0000', skill: '无双', skillDesc: '使用杀时,目标需要使用两张闪' }, + { name: '貂蝉', hp: 3, color: '#FF1493', skill: '离间', skillDesc: '可以让两名男性角色决斗' } +]; + +const IDENTITIES = { + lord: { name: '主公', class: 'lord' }, + loyalist: { name: '忠臣', class: 'loyalist' }, + rebel: { name: '反贼', class: 'rebel' }, + spy: { name: '内奸', class: 'spy' } +}; + +const EQUIPMENT_EFFECTS = { + '诸葛连弩': { range: 1, effect: '每回合可以使用任意张杀' }, + '青龙偃月刀': { range: 3, effect: '攻击范围3' }, + '丈八蛇矛': { range: 3, effect: '攻击范围3' }, + '贯石斧': { range: 3, effect: '攻击范围3' }, + '方天画戟': { range: 4, effect: '攻击范围4' }, + '八卦阵': { effect: '需要判定是否受到伤害' }, + '仁王盾': { effect: '黑色杀无效' }, + '藤甲': { effect: '普通杀无效,火杀伤害+1' }, + '的卢': { effect: '其他角色计算距离+1' }, + '赤兔': { effect: '计算与其他角色距离-1' } +}; + +const CARD_IMAGES = { + '杀': { + background: 'linear-gradient(135deg, #8B0000 0%, #DC143C 50%, #FF6347 100%)', + icon: '⚔️', + pattern: 'attack' + }, + '闪': { + background: 'linear-gradient(135deg, #006400 0%, #228B22 50%, #32CD32 100%)', + icon: '🛡️', + pattern: 'dodge' + }, + '桃': { + background: 'linear-gradient(135deg, #FF69B4 0%, #FF1493 50%, #C71585 100%)', + icon: '🍑', + pattern: 'peach' + }, + '决斗': { + background: 'linear-gradient(135deg, #4B0082 0%, #8A2BE2 50%, #9370DB 100%)', + icon: '⚡', + pattern: 'duel' + }, + '过河拆桥': { + background: 'linear-gradient(135deg, #8B4513 0%, #A0522D 50%, #CD853F 100%)', + icon: '🌉', + pattern: 'destroy' + }, + '顺手牵羊': { + background: 'linear-gradient(135deg, #DAA520 0%, #FFD700 50%, #FFA500 100%)', + icon: '🐑', + pattern: 'steal' + }, + '万箭齐发': { + background: 'linear-gradient(135deg, #B22222 0%, #DC143C 50%, #FF4500 100%)', + icon: '🏹', + pattern: 'arrow' + }, + '南蛮入侵': { + background: 'linear-gradient(135deg, #2F4F4F 0%, #008080 50%, #20B2AA 100%)', + icon: '🏹', + pattern: 'barbarian' + }, + '无中生有': { + background: 'linear-gradient(135deg, #FF8C00 0%, #FFA500 50%, #FFD700 100%)', + icon: '✨', + pattern: 'create' + }, + '借刀杀人': { + background: 'linear-gradient(135deg, #800080 0%, #9370DB 50%, #BA55D3 100%)', + icon: '🗡️', + pattern: 'borrow' + }, + '无懈可击': { + background: 'linear-gradient(135deg, #191970 0%, #4169E1 50%, #6495ED 100%)', + icon: '🚫', + pattern: 'nullify' + }, + '桃园结义': { + background: 'linear-gradient(135deg, #FF69B4 0%, #FFB6C1 50%, #FFC0CB 100%)', + icon: '🌸', + pattern: 'peachGarden' + }, + '诸葛连弩': { + background: 'linear-gradient(135deg, #FF4500 0%, #FF6347 50%, #FF7F50 100%)', + icon: '🏹', + pattern: 'weapon' + }, + '青龙偃月刀': { + background: 'linear-gradient(135deg, #006400 0%, #228B22 50%, #32CD32 100%)', + icon: '🗡️', + pattern: 'weapon' + }, + '丈八蛇矛': { + background: 'linear-gradient(135deg, #8B0000 0%, #A52A2A 50%, #CD5C5C 100%)', + icon: '⚔️', + pattern: 'weapon' + }, + '贯石斧': { + background: 'linear-gradient(135deg, #4B0082 0%, #6A5ACD 50%, #7B68EE 100%)', + icon: '🪓', + pattern: 'weapon' + }, + '方天画戟': { + background: 'linear-gradient(135deg, #FFD700 0%, #FFA500 50%, #FF8C00 100%)', + icon: '🔱', + pattern: 'weapon' + }, + '八卦阵': { + background: 'linear-gradient(135deg, #000080 0%, #4169E1 50%, #6495ED 100%)', + icon: '☯️', + pattern: 'armor' + }, + '仁王盾': { + background: 'linear-gradient(135deg, #8B4513 0%, #A0522D 50%, #CD853F 100%)', + icon: '🛡️', + pattern: 'armor' + }, + '藤甲': { + background: 'linear-gradient(135deg, #556B2F 0%, #6B8E23 50%, #8FBC8F 100%)', + icon: '🌿', + pattern: 'armor' + }, + '的卢': { + background: 'linear-gradient(135deg, #808080 0%, #A9A9A9 50%, #C0C0C0 100%)', + icon: '🐎', + pattern: 'horse' + }, + '赤兔': { + background: 'linear-gradient(135deg, #DC143C 0%, #FF6347 50%, #FF7F50 100%)', + icon: '🐎', + pattern: 'horse' + }, + '五谷丰登': { + background: 'linear-gradient(135deg, #228B22 0%, #32CD32 50%, #90EE90 100%)', + icon: '🌾', + pattern: 'harvest' + }, + '闪电': { + background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)', + icon: '⚡', + pattern: 'lightning' + }, + '乐不思蜀': { + background: 'linear-gradient(135deg, #e94560 0%, #ff6b6b 50%, #feca57 100%)', + icon: '🎵', + pattern: 'happy' + }, + '兵粮寸断': { + background: 'linear-gradient(135deg, #5f27cd 0%, #341f97 50%, #222f3e 100%)', + icon: '🍚', + pattern: 'starvation' + }, + '铁索连环': { + background: 'linear-gradient(135deg, #2c3e50 0%, #34495e 50%, #7f8c8d 100%)', + icon: '⛓️', + pattern: 'chain' + }, + '火攻': { + background: 'linear-gradient(135deg, #e74c3c 0%, #c0392b 50%, #922b21 100%)', + icon: '🔥', + pattern: 'fireAttack' + }, + '火杀': { + background: 'linear-gradient(135deg, #ff4500 0%, #ff6347 50%, #ff7f50 100%)', + icon: '🔥', + pattern: 'fireAttack' + }, + '雷杀': { + background: 'linear-gradient(135deg, #4169e1 0%, #6495ed 50%, #87ceeb 100%)', + icon: '⚡', + pattern: 'thunderAttack' + } +}; + +class Card { + constructor(suit, rank, name, type, category = 'basic') { + this.suit = suit; + this.rank = rank; + this.name = name; + this.type = type; + this.category = category; + } + + getSuitSymbol() { + const suits = { + 'spade': '♠', + 'heart': '♥', + 'club': '♣', + 'diamond': '♦' + }; + return suits[this.suit] || ''; + } + + getDisplayRank() { + const ranks = { + 'A': 'A', '2': '2', '3': '3', '4': '4', '5': '5', + '6': '6', '7': '7', '8': '8', '9': '9', '10': '10', + 'J': 'J', 'Q': 'Q', 'K': 'K' + }; + return ranks[this.rank] || this.rank; + } +} + +class Player { + constructor(index, character, identity, isHuman = false) { + this.index = index; + this.character = character; + this.identity = identity; + this.maxHp = character.hp; + this.currentHp = character.hp; + this.hand = []; + this.equipment = { weapon: null, armor: null, horsePlus: null, horseMinus: null }; + this.isHuman = isHuman; + this.isAlive = true; + this.attackRange = 1; + this.statusEffects = { + lightning: false, + happy: false, + starvation: false, + chain: false + }; + } + + takeDamage(amount) { + this.currentHp = Math.max(0, this.currentHp - amount); + if (this.currentHp === 0) { + this.isAlive = false; + } + return this.currentHp; + } + + heal(amount) { + this.currentHp = Math.min(this.maxHp, this.currentHp + amount); + return this.currentHp; + } + + showDamageEffect() { + const playerCard = document.getElementById(`player-${this.index}`); + const damageEffect = document.createElement('div'); + damageEffect.className = 'damage-effect'; + damageEffect.textContent = '-1'; + playerCard.appendChild(damageEffect); + + setTimeout(() => { + damageEffect.remove(); + }, 500); + } + + showHealEffect() { + const playerCard = document.getElementById(`player-${this.index}`); + const healEffect = document.createElement('div'); + healEffect.className = 'heal-effect'; + healEffect.textContent = '+1'; + playerCard.appendChild(healEffect); + + setTimeout(() => { + healEffect.remove(); + }, 500); + } + + addCard(card) { + this.hand.push(card); + } + + removeCard(index) { + return this.hand.splice(index, 1)[0]; + } + + equipCard(card) { + if (card.type === 'weapon') { + this.equipment.weapon = card; + const weaponRanges = { + '诸葛连弩': 1, + '青龙偃月刀': 3, + '丈八蛇矛': 3, + '贯石斧': 3, + '方天画戟': 4 + }; + this.attackRange = weaponRanges[card.name] || 1; + } else if (card.type === 'armor') { + this.equipment.armor = card; + } else if (card.type === 'horsePlus') { + this.equipment.horsePlus = card; + } else if (card.type === 'horseMinus') { + this.equipment.horseMinus = card; + } + } +} + +class Game { + constructor() { + this.deck = []; + this.discardPile = []; + this.players = []; + this.currentPlayerIndex = 0; + this.phase = 'ready'; + this.turnCount = 1; + this.selectedCardIndex = -1; + this.selectedTargetIndex = -1; + this.hasDrawn = false; + this.hasPlayed = false; + this.attackCount = 0; + this.gameOver = false; + + this.initDeck(); + this.initPlayers(); + } + + initDeck() { + const suits = ['spade', 'heart', 'club', 'diamond']; + const ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']; + + for (let suit of suits) { + for (let rank of ranks) { + this.deck.push(new Card(suit, rank, '杀', 'attack', 'basic')); + this.deck.push(new Card(suit, rank, '闪', 'dodge', 'basic')); + } + } + + for (let suit of suits) { + for (let i = 0; i < 5; i++) { + this.deck.push(new Card(suit, ranks[i], '火杀', 'attack', 'basic')); + this.deck.push(new Card(suit, ranks[i], '雷杀', 'attack', 'basic')); + } + } + + for (let suit of suits) { + for (let i = 0; i < 3; i++) { + this.deck.push(new Card(suit, ranks[i], '桃', 'peach', 'basic')); + } + } + + const scrollCards = [ + { name: '过河拆桥', type: 'dismantlement' }, + { name: '顺手牵羊', type: 'steal' }, + { name: '决斗', type: 'duel' }, + { name: '借刀杀人', type: 'borrow' }, + { name: '无中生有', type: 'draw' }, + { name: '万箭齐发', type: 'arrow' }, + { name: '南蛮入侵', type: 'barbarian' }, + { name: '无懈可击', type: 'nullify' }, + { name: '桃园结义', type: 'peachGarden' }, + { name: '五谷丰登', type: 'harvest' }, + { name: '闪电', type: 'lightning' }, + { name: '乐不思蜀', type: 'happy' }, + { name: '兵粮寸断', type: 'starvation' }, + { name: '铁索连环', type: 'chain' }, + { name: '火攻', type: 'fireAttack' } + ]; + + for (let card of scrollCards) { + for (let suit of suits) { + for (let i = 0; i < 2; i++) { + this.deck.push(new Card(suit, ranks[i], card.name, card.type, 'scroll')); + } + } + } + + const equipCards = [ + { name: '诸葛连弩', type: 'weapon', range: 1 }, + { name: '青龙偃月刀', type: 'weapon', range: 3 }, + { name: '丈八蛇矛', type: 'weapon', range: 3 }, + { name: '贯石斧', type: 'weapon', range: 3 }, + { name: '方天画戟', type: 'weapon', range: 4 }, + { name: '八卦阵', type: 'armor' }, + { name: '仁王盾', type: 'armor' }, + { name: '藤甲', type: 'armor' }, + { name: '的卢', type: 'horsePlus' }, + { name: '绝影', type: 'horsePlus' }, + { name: '赤兔', type: 'horseMinus' }, + { name: '紫骍', type: 'horseMinus' } + ]; + + for (let card of equipCards) { + for (let suit of suits) { + this.deck.push(new Card(suit, 'A', card.name, card.type, 'equip')); + } + } + + this.shuffleDeck(); + } + + shuffleDeck() { + for (let i = this.deck.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [this.deck[i], this.deck[j]] = [this.deck[j], this.deck[i]]; + } + } + + initPlayers() { + const availableCharacters = [...CHARACTERS]; + const shuffledCharacters = availableCharacters.sort(() => Math.random() - 0.5); + + const identities = ['lord', 'loyalist', 'rebel', 'rebel', 'spy']; + const shuffledIdentities = identities.sort(() => Math.random() - 0.5); + + for (let i = 0; i < 5; i++) { + const isHuman = (i === 0); + const character = shuffledCharacters[i]; + const identity = shuffledIdentities[i]; + + this.players.push(new Player(i, character, identity, isHuman)); + + if (identity === 'lord') { + this.players[i].maxHp++; + this.players[i].currentHp++; + } + } + + for (let i = 0; i < 5; i++) { + for (let j = 0; j < 4; j++) { + const card = this.drawCard(); + if (card) { + this.players[i].addCard(card); + } + } + } + + const lordIndex = this.players.findIndex(p => p.identity === 'lord'); + if (lordIndex !== -1) { + this.currentPlayerIndex = lordIndex; + } + } + + drawCard() { + if (this.deck.length === 0) { + if (this.discardPile.length === 0) { + return null; + } + this.deck = [...this.discardPile]; + this.discardPile = []; + this.shuffleDeck(); + } + return this.deck.pop(); + } + + discardCard(card) { + this.discardPile.push(card); + } + + getCurrentPlayer() { + return this.players[this.currentPlayerIndex]; + } + + getNextAlivePlayer(startIndex, direction = 1) { + let index = startIndex; + do { + index = (index + direction + 5) % 5; + if (this.players[index].isAlive) { + return index; + } + } while (index !== startIndex); + return startIndex; + } + + startTurn() { + this.phase = 'draw'; + this.hasDrawn = false; + this.hasPlayed = false; + this.attackCount = 0; + this.selectedCardIndex = -1; + this.selectedTargetIndex = -1; + + const player = this.getCurrentPlayer(); + this.addLog(`${player.character.name} 的回合开始`, true); + + this.handleStatusEffects(player); + } + + handleStatusEffects(player) { + if (!player.isAlive) return; + + if (player.statusEffects.lightning) { + this.handleLightningEffect(player); + } + + if (player.statusEffects.happy) { + this.handleHappyEffect(player); + } + + if (player.statusEffects.starvation) { + this.handleStarvationEffect(player); + } + } + + handleLightningEffect(player) { + const judgmentCard = this.drawCard(); + this.addLog(`${player.character.name} 进行闪电判定,判定牌为 ${judgmentCard.getSuitSymbol()}${judgmentCard.getDisplayRank()}`); + + if (judgmentCard.suit === 'spade' && ['2', '3', '4', '5', '6', '7', '8', '9'].includes(judgmentCard.rank)) { + player.takeDamage(3); + player.showDamageEffect(); + this.addLog(`${player.character.name} 受到3点雷属性伤害!`); + this.checkDeath(player); + player.statusEffects.lightning = false; + } else { + this.addLog('闪电判定未通过,闪电传递给下家'); + player.statusEffects.lightning = false; + const nextPlayerIndex = this.getNextAlivePlayer(player.index); + this.players[nextPlayerIndex].statusEffects.lightning = true; + this.addLog(`${this.players[nextPlayerIndex].character.name} 被闪电标记`); + } + + this.discardCard(judgmentCard); + } + + handleHappyEffect(player) { + const judgmentCard = this.drawCard(); + this.addLog(`${player.character.name} 进行乐不思蜀判定,判定牌为 ${judgmentCard.getSuitSymbol()}${judgmentCard.getDisplayRank()}`); + + if (judgmentCard.suit !== 'heart') { + this.addLog(`${player.character.name} 乐不思蜀判定未通过,本回合不能使用牌`); + player.statusEffects.happy = true; + } else { + this.addLog(`${player.character.name} 乐不思蜀判定通过,可以正常使用牌`); + player.statusEffects.happy = false; + } + + this.discardCard(judgmentCard); + } + + handleStarvationEffect(player) { + const judgmentCard = this.drawCard(); + this.addLog(`${player.character.name} 进行兵粮寸断判定,判定牌为 ${judgmentCard.getSuitSymbol()}${judgmentCard.getDisplayRank()}`); + + if (judgmentCard.suit !== 'club') { + this.addLog(`${player.character.name} 兵粮寸断判定未通过,本回合不能摸牌`); + player.statusEffects.starvation = true; + } else { + this.addLog(`${player.character.name} 兵粮寸断判定通过,可以正常摸牌`); + player.statusEffects.starvation = false; + } + + this.discardCard(judgmentCard); + } + + drawPhase() { + if (this.hasDrawn) return; + + const player = this.getCurrentPlayer(); + + if (player.statusEffects.starvation) { + this.addLog(`${player.character.name} 兵粮寸断生效,本回合不能摸牌`); + player.statusEffects.starvation = false; + } else { + const drawCount = 2; + for (let i = 0; i < drawCount; i++) { + const card = this.drawCard(); + if (card) { + player.addCard(card); + this.addLog(`${player.character.name} 摸了一张牌`); + } + } + } + + this.hasDrawn = true; + this.phase = 'play'; + } + + useSkill() { + const player = this.getCurrentPlayer(); + + switch (player.character.name) { + case '刘备': + this.useLiuBeiSkill(); + break; + case '曹操': + this.addLog('奸雄技能在受到伤害后自动触发'); + break; + case '孙权': + this.useSunQuanSkill(); + break; + case '关羽': + this.addLog('武圣技能:红色手牌自动当作杀使用'); + break; + case '张飞': + this.addLog('咆哮技能:出牌阶段可以使用任意张杀'); + break; + case '赵云': + this.addLog('龙胆技能:杀闪互换自动触发'); + break; + case '诸葛亮': + this.useZhugeLiangSkill(); + break; + case '黄月英': + this.addLog('集智技能:使用锦囊牌后自动摸牌'); + break; + case '吕布': + this.addLog('无双技能:使用杀时目标需要两张闪'); + break; + case '貂蝉': + this.useDiaoChanSkill(); + break; + default: + this.addLog('该角色没有主动技能'); + } + } + + useLiuBeiSkill() { + const player = this.getCurrentPlayer(); + if (player.hand.length === 0) { + this.addLog('没有手牌可以赠送'); + return; + } + + const targetIndex = this.selectedTargetIndex; + if (targetIndex === -1 || targetIndex === player.index) { + this.addLog('请先选择一个目标'); + return; + } + + const target = this.players[targetIndex]; + if (!target.isAlive) { + this.addLog('目标已阵亡'); + return; + } + + const cardIndex = this.selectedCardIndex; + if (cardIndex === -1) { + this.addLog('请先选择一张手牌'); + return; + } + + const card = player.removeCard(cardIndex); + target.addCard(card); + this.addLog(`${player.character.name} 将【${card.name}】赠予 ${target.character.name}`); + this.selectedCardIndex = -1; + this.selectedTargetIndex = -1; + } + + useSunQuanSkill() { + const player = this.getCurrentPlayer(); + if (player.hand.length === 0) { + this.addLog('没有手牌可以弃置'); + return; + } + + const cardIndex = this.selectedCardIndex; + if (cardIndex === -1) { + this.addLog('请先选择要弃置的手牌'); + return; + } + + const card = player.removeCard(cardIndex); + this.discardCard(card); + this.addLog(`${player.character.name} 弃置了【${card.name}】`); + + const newCard = this.drawCard(); + if (newCard) { + player.addCard(newCard); + this.addLog(`${player.character.name} 摸了一张牌`); + } + + this.selectedCardIndex = -1; + } + + useZhugeLiangSkill() { + if (this.deck.length === 0) { + this.addLog('牌堆已空'); + return; + } + + const topCard = this.deck[this.deck.length - 1]; + this.addLog(`牌堆顶的牌是:【${topCard.name}】`); + } + + useDiaoChanSkill() { + const player = this.getCurrentPlayer(); + const malePlayers = this.players.filter(p => p.isAlive && p.index !== player.index && p.character.name !== '貂蝉'); + + if (malePlayers.length < 2) { + this.addLog('场上男性角色不足'); + return; + } + + const target1Index = this.selectedTargetIndex; + if (target1Index === -1) { + this.addLog('请先选择第一个决斗目标'); + return; + } + + const target1 = this.players[target1Index]; + if (!target1.isAlive || target1.character.name === '貂蝉') { + this.addLog('第一个目标无效'); + return; + } + + const target2 = malePlayers.find(p => p.index !== target1Index); + if (!target2) { + this.addLog('请选择第二个决斗目标'); + return; + } + + this.addLog(`${player.character.name} 发动【离间】,让 ${target1.character.name} 和 ${target2.character.name} 决斗`); + this.startDuel(target1, target2); + } + + playCard(cardIndex, targetIndex = -1) { + const player = this.getCurrentPlayer(); + const card = player.hand[cardIndex]; + + if (player.statusEffects.happy) { + this.addLog('乐不思蜀生效,本回合不能使用牌'); + return false; + } + + if (targetIndex === -1 && this.selectedTargetIndex !== -1) { + targetIndex = this.selectedTargetIndex; + } + + if (card.category === 'basic') { + return this.playBasicCard(cardIndex, targetIndex); + } else if (card.category === 'scroll') { + return this.playScrollCard(cardIndex, targetIndex); + } else if (card.category === 'equip') { + return this.playEquipCard(cardIndex); + } + + return false; + } + + playBasicCard(cardIndex, targetIndex = -1) { + const player = this.getCurrentPlayer(); + const card = player.hand[cardIndex]; + + if (card.type === 'attack') { + if (this.attackCount >= 1 && !this.hasWeaponWithEffect('zhugeliannu') && player.character.name !== '张飞') { + this.addLog('每回合只能使用一张杀'); + return false; + } + + if (targetIndex === -1) { + if (this.selectedTargetIndex !== -1) { + targetIndex = this.selectedTargetIndex; + } else { + targetIndex = this.getNextAlivePlayer(this.currentPlayerIndex); + } + } + + const target = this.players[targetIndex]; + if (!target.isAlive) { + this.addLog('目标已阵亡'); + return false; + } + + if (!this.isInRange(targetIndex)) { + this.addLog('目标不在攻击范围内'); + return false; + } + + const attackType = card.name === '火杀' ? '火' : (card.name === '雷杀' ? '雷' : ''); + this.addLog(`${player.character.name} 对 ${target.character.name} 使用了【${card.name}】`); + this.showAttackAnimation(targetIndex); + + let damage = 1; + let needsDodge = true; + + if (target.equipment.armor && target.equipment.armor.name === '藤甲') { + if (card.name === '火杀') { + damage = 2; + this.addLog(`${target.character.name} 的藤甲受到火杀伤害+1`); + } else if (card.name !== '雷杀') { + needsDodge = false; + this.addLog(`${target.character.name} 的藤甲使普通杀无效`); + } + } + + if (needsDodge && this.useDodge(target, player)) { + this.addLog(`${target.character.name} 使用了【闪】`); + } else { + target.takeDamage(damage); + target.showDamageEffect(); + this.addLog(`${target.character.name} 受到${damage}点${attackType}属性伤害`); + + if (target.statusEffects.chain) { + this.handleChainDamage(target, attackType); + } + + this.checkDeath(target); + } + + this.attackCount++; + } else if (card.type === 'peach') { + if (player.currentHp >= player.maxHp) { + this.addLog('体力已满,无法使用桃'); + return false; + } + + player.heal(1); + player.showHealEffect(); + this.addLog(`${player.character.name} 使用了【桃】,回复1点体力`); + } else if (card.type === 'dodge') { + this.addLog('闪只能在对方使用杀时使用'); + return false; + } + + const playedCard = player.removeCard(cardIndex); + this.discardCard(playedCard); + return true; + } + + playScrollCard(cardIndex, targetIndex = -1) { + const player = this.getCurrentPlayer(); + const card = player.hand[cardIndex]; + + if (card.type === 'dismantlement') { + if (targetIndex === -1) { + if (this.selectedTargetIndex !== -1) { + targetIndex = this.selectedTargetIndex; + } else { + targetIndex = this.getNextAlivePlayer(this.currentPlayerIndex); + } + } + + const target = this.players[targetIndex]; + if (!target.isAlive || targetIndex === this.currentPlayerIndex) { + this.addLog('无效的目标'); + return false; + } + + this.addLog(`${player.character.name} 对 ${target.character.name} 使用了【过河拆桥】`); + this.showCardUseAnimation(); + + if (target.hand.length > 0) { + const randomIndex = Math.floor(Math.random() * target.hand.length); + const removedCard = target.removeCard(randomIndex); + this.discardCard(removedCard); + this.addLog(`${target.character.name} 的一张牌被拆掉了`); + } + + } else if (card.type === 'steal') { + if (targetIndex === -1) { + if (this.selectedTargetIndex !== -1) { + targetIndex = this.selectedTargetIndex; + } else { + targetIndex = this.getNextAlivePlayer(this.currentPlayerIndex); + } + } + + const target = this.players[targetIndex]; + if (!target.isAlive || targetIndex === this.currentPlayerIndex) { + this.addLog('无效的目标'); + return false; + } + + this.addLog(`${player.character.name} 对 ${target.character.name} 使用了【顺手牵羊】`); + this.showCardUseAnimation(); + + if (target.hand.length > 0) { + const randomIndex = Math.floor(Math.random() * target.hand.length); + const stolenCard = target.removeCard(randomIndex); + player.addCard(stolenCard); + this.addLog(`${player.character.name} 获得了 ${target.character.name} 的一张牌`); + } + + } else if (card.type === 'draw') { + this.addLog(`${player.character.name} 使用了【无中生有】`); + this.showCardUseAnimation(); + for (let i = 0; i < 2; i++) { + const newCard = this.drawCard(); + if (newCard) { + player.addCard(newCard); + this.addLog(`${player.character.name} 摸了一张牌`); + } + } + + } else if (card.type === 'duel') { + if (targetIndex === -1) { + if (this.selectedTargetIndex !== -1) { + targetIndex = this.selectedTargetIndex; + } else { + targetIndex = this.getNextAlivePlayer(this.currentPlayerIndex); + } + } + + const target = this.players[targetIndex]; + if (!target.isAlive || targetIndex === this.currentPlayerIndex) { + this.addLog('无效的目标'); + return false; + } + + this.addLog(`${player.character.name} 对 ${target.character.name} 发起了【决斗】`); + this.showCardUseAnimation(); + this.handleDuel(player, target); + + } else if (card.type === 'peachGarden') { + this.addLog(`${player.character.name} 使用了【桃园结义】`); + this.showCardUseAnimation(); + for (let p of this.players) { + if (p.isAlive && p.currentHp < p.maxHp) { + p.heal(1); + p.showHealEffect(); + this.addLog(`${p.character.name} 回复1点体力`); + } + } + + } else if (card.type === 'nullify') { + this.addLog('无懈可击需要在锦囊牌使用时使用'); + return false; + } else if (card.type === 'lightning') { + if (targetIndex === -1) { + if (this.selectedTargetIndex !== -1) { + targetIndex = this.selectedTargetIndex; + } else { + targetIndex = this.getNextAlivePlayer(this.currentPlayerIndex); + } + } + + const target = this.players[targetIndex]; + if (!target.isAlive || targetIndex === this.currentPlayerIndex) { + this.addLog('无效的目标'); + return false; + } + + this.addLog(`${player.character.name} 对 ${target.character.name} 使用了【闪电】`); + this.showCardUseAnimation(); + target.statusEffects.lightning = true; + this.addLog(`${target.character.name} 被闪电标记`); + + } else if (card.type === 'happy') { + if (targetIndex === -1) { + if (this.selectedTargetIndex !== -1) { + targetIndex = this.selectedTargetIndex; + } else { + targetIndex = this.getNextAlivePlayer(this.currentPlayerIndex); + } + } + + const target = this.players[targetIndex]; + if (!target.isAlive || targetIndex === this.currentPlayerIndex) { + this.addLog('无效的目标'); + return false; + } + + this.addLog(`${player.character.name} 对 ${target.character.name} 使用了【乐不思蜀】`); + this.showCardUseAnimation(); + target.statusEffects.happy = true; + this.addLog(`${target.character.name} 被乐不思蜀标记`); + + } else if (card.type === 'starvation') { + if (targetIndex === -1) { + if (this.selectedTargetIndex !== -1) { + targetIndex = this.selectedTargetIndex; + } else { + targetIndex = this.getNextAlivePlayer(this.currentPlayerIndex); + } + } + + const target = this.players[targetIndex]; + if (!target.isAlive || targetIndex === this.currentPlayerIndex) { + this.addLog('无效的目标'); + return false; + } + + if (!this.isInRange(targetIndex)) { + this.addLog('目标不在攻击范围内'); + return false; + } + + this.addLog(`${player.character.name} 对 ${target.character.name} 使用了【兵粮寸断】`); + this.showCardUseAnimation(); + target.statusEffects.starvation = true; + this.addLog(`${target.character.name} 被兵粮寸断标记`); + + } else if (card.type === 'chain') { + if (targetIndex === -1) { + if (this.selectedTargetIndex !== -1) { + targetIndex = this.selectedTargetIndex; + } else { + targetIndex = this.getNextAlivePlayer(this.currentPlayerIndex); + } + } + + const target = this.players[targetIndex]; + if (!target.isAlive || targetIndex === this.currentPlayerIndex) { + this.addLog('无效的目标'); + return false; + } + + this.addLog(`${player.character.name} 对 ${target.character.name} 使用了【铁索连环】`); + this.showCardUseAnimation(); + target.statusEffects.chain = true; + this.addLog(`${target.character.name} 被铁索连环标记`); + + } else if (card.type === 'fireAttack') { + if (targetIndex === -1) { + if (this.selectedTargetIndex !== -1) { + targetIndex = this.selectedTargetIndex; + } else { + targetIndex = this.getNextAlivePlayer(this.currentPlayerIndex); + } + } + + const target = this.players[targetIndex]; + if (!target.isAlive || targetIndex === this.currentPlayerIndex) { + this.addLog('无效的目标'); + return false; + } + + this.addLog(`${player.character.name} 对 ${target.character.name} 使用了【火攻】`); + this.showCardUseAnimation(); + + const randomCardIndex = Math.floor(Math.random() * target.hand.length); + const randomCard = target.hand[randomCardIndex]; + this.addLog(`${target.character.name} 展示了一张 ${randomCard.name}`); + + if (randomCard.suit === 'heart' || randomCard.suit === 'diamond') { + target.takeDamage(1); + target.showDamageEffect(); + this.addLog(`${target.character.name} 受到1点火属性伤害`); + this.checkDeath(target); + } else { + this.addLog('火攻未造成伤害'); + } + } + + const playedCard = player.removeCard(cardIndex); + this.discardCard(playedCard); + + if (player.character.name === '黄月英') { + const newCard = this.drawCard(); + if (newCard) { + player.addCard(newCard); + this.addLog(`${player.character.name} 发动【集智】,摸了一张牌`); + } + } + + this.showCardUseAnimation(); + return true; + } + + showCardUseAnimation() { + const cardElements = document.querySelectorAll('.card.selected'); + cardElements.forEach(card => { + card.classList.add('use-animation'); + setTimeout(() => { + card.classList.remove('use-animation'); + }, 500); + }); + } + + playEquipCard(cardIndex) { + const player = this.getCurrentPlayer(); + const card = player.hand[cardIndex]; + + player.equipCard(card); + this.addLog(`${player.character.name} 装备了【${card.name}】`); + + const playedCard = player.removeCard(cardIndex); + this.discardCard(playedCard); + + this.showCardUseAnimation(); + return true; + } + + useDodge(target, attacker) { + let dodgeCount = 1; + + if (attacker && attacker.character.name === '吕布') { + dodgeCount = 2; + } + + let dodgesUsed = 0; + for (let i = 0; i < dodgeCount; i++) { + const dodgeIndex = target.hand.findIndex(c => c.type === 'dodge'); + if (dodgeIndex !== -1) { + if (target.isHuman) { + dodgesUsed++; + } else { + target.removeCard(dodgeIndex); + this.discardCard(target.hand[dodgeIndex]); + dodgesUsed++; + } + this.showShieldAnimation(target.index); + } + } + + return dodgesUsed >= dodgeCount; + } + + showShieldAnimation(playerIndex) { + const playerCard = document.getElementById(`player-${playerIndex}`); + const shieldAnimation = document.createElement('div'); + shieldAnimation.className = 'shield-animation'; + shieldAnimation.textContent = '🛡️'; + playerCard.appendChild(shieldAnimation); + + setTimeout(() => { + shieldAnimation.remove(); + }, 800); + } + + handleDuel(attacker, defender) { + let currentAttacker = attacker; + let currentDefender = defender; + + while (true) { + const attackIndex = currentAttacker.hand.findIndex(c => c.type === 'attack'); + if (attackIndex !== -1) { + currentAttacker.removeCard(attackIndex); + this.discardCard(currentAttacker.hand[attackIndex]); + this.showAttackAnimation(currentDefender.index); + + const defendIndex = currentDefender.hand.findIndex(c => c.type === 'attack'); + if (defendIndex !== -1) { + currentDefender.removeCard(defendIndex); + this.discardCard(currentDefender.hand[defendIndex]); + [currentAttacker, currentDefender] = [currentDefender, currentAttacker]; + } else { + currentDefender.takeDamage(1); + currentDefender.showDamageEffect(); + this.addLog(`${currentDefender.character.name} 受到1点伤害`); + this.checkDeath(currentDefender); + break; + } + } else { + break; + } + } + } + + startDuel(player1, player2) { + this.handleDuel(player1, player2); + } + + isInRange(targetIndex) { + const player = this.getCurrentPlayer(); + const target = this.players[targetIndex]; + + let range = player.attackRange; + if (player.equipment.horseMinus) { + range++; + } + + let distance = 0; + let currentIndex = this.currentPlayerIndex; + + while (currentIndex !== targetIndex) { + currentIndex = this.getNextAlivePlayer(currentIndex); + distance++; + if (this.players[currentIndex].equipment.horsePlus && currentIndex !== targetIndex) { + distance++; + } + } + + if (target.equipment.horsePlus) { + distance++; + } + + return distance <= range; + } + + hasWeaponWithEffect(effect) { + const player = this.getCurrentPlayer(); + return player.equipment.weapon && player.equipment.weapon.name === '诸葛连弩'; + } + + checkDeath(player) { + if (!player.isAlive) { + this.showKillEffect(player); + this.addLog(`${player.character.name} 阵亡!`, true); + + const playerCard = document.getElementById(`player-${player.index}`); + playerCard.classList.add('dead'); + + if (player.identity === 'lord') { + this.gameOver = true; + this.checkWinCondition(); + } else { + const lord = this.players.find(p => p.identity === 'lord'); + if (lord) { + if (player.identity === 'rebel') { + lord.addCard(this.drawCard()); + lord.addCard(this.drawCard()); + lord.addCard(this.drawCard()); + this.addLog(`${lord.character.name} 摸了3张牌`); + } + } + this.checkWinCondition(); + } + } + } + + showKillEffect(player) { + const killEffect = document.createElement('div'); + killEffect.className = 'kill-effect'; + killEffect.innerHTML = '
击杀!
'; + document.body.appendChild(killEffect); + + setTimeout(() => { + killEffect.remove(); + }, 1000); + + this.showDeathAnimation(player.index); + } + + showDeathAnimation(playerIndex) { + const playerCard = document.getElementById(`player-${playerIndex}`); + const deathAnimation = document.createElement('div'); + deathAnimation.className = 'death-animation'; + deathAnimation.innerHTML = '💀'; + playerCard.appendChild(deathAnimation); + + setTimeout(() => { + deathAnimation.remove(); + }, 800); + } + + showAttackAnimation(targetIndex) { + const targetCard = document.getElementById(`player-${targetIndex}`); + const attackAnimation = document.createElement('div'); + attackAnimation.className = 'attack-animation'; + attackAnimation.textContent = '⚔️'; + targetCard.appendChild(attackAnimation); + + setTimeout(() => { + attackAnimation.remove(); + }, 400); + } + + handleChainDamage(sourcePlayer, damageType) { + if (!sourcePlayer.statusEffects.chain) return; + + sourcePlayer.statusEffects.chain = false; + this.addLog(`${sourcePlayer.character.name} 的铁索连环传导伤害`); + + const chainedPlayers = this.players.filter(p => p.isAlive && p.statusEffects.chain); + + for (const player of chainedPlayers) { + player.takeDamage(1); + player.showDamageEffect(); + this.addLog(`${player.character.name} 受到1点${damageType}属性传导伤害`); + player.statusEffects.chain = false; + this.checkDeath(player); + } + } + + checkWinCondition() { + const alivePlayers = this.players.filter(p => p.isAlive); + const lord = this.players.find(p => p.identity === 'lord'); + + if (!lord || !lord.isAlive) { + const rebels = alivePlayers.filter(p => p.identity === 'rebel'); + const spy = alivePlayers.find(p => p.identity === 'spy'); + + if (rebels.length > 0) { + this.addLog('反贼获胜!', true); + } else if (spy && alivePlayers.length === 1 && alivePlayers[0].identity === 'spy') { + this.addLog('内奸获胜!', true); + } else { + this.addLog('反贼获胜!', true); + } + this.gameOver = true; + } else { + const rebels = alivePlayers.filter(p => p.identity === 'rebel'); + const loyalists = alivePlayers.filter(p => p.identity === 'loyalist'); + const spy = alivePlayers.find(p => p.identity === 'spy'); + + if (alivePlayers.length === 1 && alivePlayers[0].identity === 'spy') { + this.addLog('内奸获胜!', true); + this.gameOver = true; + } else if (rebels.length === 0 && (!spy || !spy.isAlive)) { + this.addLog('主公和忠臣获胜!', true); + this.gameOver = true; + } + } + } + + endTurn() { + if (this.gameOver) return; + + const player = this.getCurrentPlayer(); + this.addLog(`${player.character.name} 结束回合`); + + this.currentPlayerIndex = this.getNextAlivePlayer(this.currentPlayerIndex); + + if (this.currentPlayerIndex === 0) { + this.turnCount++; + } + + this.startTurn(); + + if (!this.getCurrentPlayer().isHuman) { + this.aiTurn(); + } + } + + aiTurn() { + const player = this.getCurrentPlayer(); + + setTimeout(() => { + this.drawPhase(); + updateUI(); + + setTimeout(() => { + this.aiPlayCards(player); + }, 300); + }, 300); + } + + aiPlayCards(player) { + const targets = this.players.filter(p => p.isAlive && p.index !== player.index); + + const canUseMultipleAttacks = this.hasWeaponWithEffect('zhugeliannu') || player.character.name === '张飞'; + + while (true) { + const attackCard = player.hand.findIndex(c => c.type === 'attack'); + if (attackCard !== -1 && (this.attackCount === 0 || canUseMultipleAttacks)) { + const target = this.aiSelectTarget(player, targets); + if (target !== -1) { + this.playCard(attackCard, target); + updateUI(); + continue; + } + } + break; + } + + const peachCard = player.hand.findIndex(c => c.type === 'peach'); + if (peachCard !== -1 && player.currentHp < player.maxHp) { + this.playCard(peachCard); + updateUI(); + } + + const dismantleCard = player.hand.findIndex(c => c.type === 'dismantlement'); + if (dismantleCard !== -1) { + const target = this.aiSelectTarget(player, targets); + if (target !== -1) { + this.playCard(dismantleCard, target); + updateUI(); + } + } + + const stealCard = player.hand.findIndex(c => c.type === 'steal'); + if (stealCard !== -1) { + const target = this.aiSelectTarget(player, targets); + if (target !== -1) { + this.playCard(stealCard, target); + updateUI(); + } + } + + const drawCard = player.hand.findIndex(c => c.type === 'draw'); + if (drawCard !== -1) { + this.playCard(drawCard); + updateUI(); + } + + const equipCard = player.hand.findIndex(c => c.category === 'equip'); + if (equipCard !== -1) { + this.playCard(equipCard); + updateUI(); + } + + setTimeout(() => { + this.endTurn(); + updateUI(); + }, 300); + } + + aiSelectTarget(player, targets) { + if (player.identity === 'lord') { + const rebel = targets.find(t => t.identity === 'rebel'); + if (rebel) return rebel.index; + const spy = targets.find(t => t.identity === 'spy'); + if (spy) return spy.index; + } else if (player.identity === 'loyalist') { + const rebel = targets.find(t => t.identity === 'rebel'); + if (rebel) return rebel.index; + } else if (player.identity === 'rebel') { + const lord = targets.find(t => t.identity === 'lord'); + if (lord) return lord.index; + const loyalist = targets.find(t => t.identity === 'loyalist'); + if (loyalist) return loyalist.index; + } else if (player.identity === 'spy') { + const weakTarget = targets.reduce((weakest, t) => + t.currentHp < weakest.currentHp ? t : weakest, targets[0]); + if (weakTarget) return weakTarget.index; + } + + return targets.length > 0 ? targets[0].index : -1; + } + + addLog(message, highlight = false) { + const logContainer = document.getElementById('game-log'); + const logEntry = document.createElement('div'); + logEntry.className = 'log-entry' + (highlight ? ' highlight' : ''); + logEntry.textContent = message; + logContainer.appendChild(logEntry); + logContainer.scrollTop = logContainer.scrollHeight; + } +} + +let game; + +function startGame() { + document.getElementById('start-screen').style.display = 'none'; + document.getElementById('game-container').style.display = 'block'; + game = new Game(); + updateUI(); + setupEventListeners(); + game.addLog('游戏初始化完成', true); + + const currentPlayer = game.getCurrentPlayer(); + if (!currentPlayer.isHuman) { + game.addLog(`${currentPlayer.character.name} 的回合开始`, true); + setTimeout(() => { + game.drawPhase(); + updateUI(); + setTimeout(() => { + game.aiPlayCards(currentPlayer); + }, 300); + }, 300); + } else { + game.addLog('点击"准备"开始游戏', true); + } +} + +function initGame() { + document.getElementById('start-btn').addEventListener('click', startGame); +} + +let updateUIPending = false; +let updateUITimer = null; + +function updateUI() { + if (updateUIPending) { + return; + } + + updateUIPending = true; + + if (updateUITimer) { + clearTimeout(updateUITimer); + } + + updateUITimer = setTimeout(() => { + updateUIPending = false; + renderUI(); + }, 16); +} + +function renderUI() { + const player = game.getCurrentPlayer(); + + document.getElementById('current-phase').textContent = `阶段: ${game.phase === 'ready' ? '准备' : game.phase === 'draw' ? '摸牌' : game.phase === 'play' ? '出牌' : '结束'}`; + document.getElementById('turn-count').textContent = `回合: ${game.turnCount}`; + document.getElementById('deck-count').textContent = game.deck.length; + + const attackRange = calculateAttackRange(player); + document.getElementById('attack-range').textContent = `攻击距离: ${attackRange}`; + + for (let i = 0; i < 5; i++) { + const p = game.players[i]; + + if (!playerInfoCache[i]) { + playerInfoCache[i] = { + name: null, + identity: null, + skill: null + }; + } + + const infoCache = playerInfoCache[i]; + + const playerCard = document.getElementById(`player-${i}`); + + playerCard.classList.remove('active', 'targetable', 'targeted', 'out-of-range'); + if (i === game.currentPlayerIndex) { + playerCard.classList.add('active'); + } + + if (game.selectedCardIndex !== -1 && player.isHuman && game.phase === 'play') { + const selectedCard = player.hand[game.selectedCardIndex]; + if (selectedCard && (selectedCard.type === 'attack' || selectedCard.category === 'scroll')) { + if (i !== game.currentPlayerIndex && p.isAlive) { + if (game.isInRange(i)) { + playerCard.classList.add('targetable'); + } else { + playerCard.classList.add('out-of-range'); + } + } + } + } + + if (game.selectedTargetIndex === i) { + playerCard.classList.add('targeted'); + } + + if (infoCache.name !== p.character.name) { + const avatar = document.getElementById(`avatar-${i}`); + avatar.src = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Crect width='60' height='60' fill='${p.character.color}'/%3E%3Ctext x='30' y='35' text-anchor='middle' fill='white' font-size='12'%3E${p.character.name}%3C/text%3E%3C/svg%3E`; + document.getElementById(`name-${i}`).textContent = p.character.name; + infoCache.name = p.character.name; + } + + const identityEl = document.getElementById(`identity-${i}`); + const identityInfo = IDENTITIES[p.identity]; + if (p.isHuman || p.identity === 'lord' || !p.isAlive) { + if (infoCache.identity !== identityInfo.name) { + identityEl.textContent = identityInfo.name; + identityEl.className = `identity ${identityInfo.class}`; + infoCache.identity = identityInfo.name; + } + } else { + if (infoCache.identity !== '???') { + identityEl.textContent = '???'; + identityEl.className = 'identity hidden'; + infoCache.identity = '???'; + } + } + + const hpFill = document.getElementById(`hp-bar-${i}`).querySelector('.hp-fill'); + hpFill.style.width = `${(p.currentHp / p.maxHp) * 100}%`; + hpFill.classList.remove('damaged'); + void hpFill.offsetWidth; + hpFill.classList.add('damaged'); + document.getElementById(`hp-value-${i}`).textContent = `${p.currentHp}/${p.maxHp}`; + document.getElementById(`cards-${i}`).textContent = p.hand.length; + + renderEquipment(i); + + const skillEl = document.getElementById(`skill-${i}`); + if (p.character.skill) { + if (infoCache.skill !== p.character.skill) { + skillEl.textContent = p.character.skill; + skillEl.title = p.character.skillDesc || ''; + infoCache.skill = p.character.skill; + } + } else { + if (infoCache.skill !== '') { + skillEl.textContent = ''; + infoCache.skill = ''; + } + } + + renderHand(i); + } + + const drawBtn = document.getElementById('draw-btn'); + const playBtn = document.getElementById('play-btn'); + const endTurnBtn = document.getElementById('end-turn-btn'); + const readyBtn = document.getElementById('ready-btn'); + const skillBtn = document.getElementById('skill-btn'); + + readyBtn.disabled = game.phase !== 'ready' || !player.isHuman || game.gameOver; + drawBtn.disabled = game.phase !== 'draw' || !player.isHuman || game.hasDrawn || game.gameOver; + skillBtn.disabled = game.phase !== 'play' || !player.isHuman || game.gameOver; + playBtn.disabled = game.phase !== 'play' || !player.isHuman || game.selectedCardIndex === -1 || game.gameOver; + endTurnBtn.disabled = game.gameOver || !player.isHuman || game.phase === 'ready'; +} + +const handCache = {}; +const equipmentCache = {}; +const playerInfoCache = {}; + +function renderHand(playerIndex) { + const player = game.players[playerIndex]; + const handContainer = document.getElementById(`hand-${playerIndex}`); + + if (!handCache[playerIndex]) { + handCache[playerIndex] = { + handLength: 0, + cards: [] + }; + } + + const cache = handCache[playerIndex]; + const currentHandLength = player.hand.length; + + if (currentHandLength === cache.handLength) { + if (player.isHuman) { + const existingCards = handContainer.querySelectorAll('.card'); + player.hand.forEach((card, index) => { + const cardElement = existingCards[index]; + const cardImage = CARD_IMAGES[card.name] || { background: '#fff', icon: '🃏', pattern: 'default' }; + + const isSelected = game.selectedCardIndex === index; + const wasSelected = cardElement.classList.contains('selected'); + + if (isSelected !== wasSelected) { + cardElement.classList.toggle('selected', isSelected); + } + }); + } + return; + } + + handContainer.innerHTML = ''; + cache.handLength = currentHandLength; + cache.cards = []; + + if (player.isHuman) { + player.hand.forEach((card, index) => { + const cardElement = document.createElement('div'); + const cardImage = CARD_IMAGES[card.name] || { background: '#fff', icon: '🃏', pattern: 'default' }; + + cardElement.className = `card ${card.category}` + (game.selectedCardIndex === index ? ' selected' : ''); + cardElement.style.background = cardImage.background; + cardElement.innerHTML = ` +
${cardImage.icon}
+
${card.getSuitSymbol()}
+
${card.getDisplayRank()}
+
${card.name}
+
${card.category === 'basic' ? '基本' : card.category === 'scroll' ? '锦囊' : '装备'}
+ `; + cardElement.onclick = () => selectCard(index); + handContainer.appendChild(cardElement); + cache.cards.push(card.name); + }); + } else { + for (let i = 0; i < player.hand.length; i++) { + const cardElement = document.createElement('div'); + cardElement.className = 'card-back'; + handContainer.appendChild(cardElement); + } + } +} + +function renderEquipment(playerIndex) { + const player = game.players[playerIndex]; + const equipmentEl = document.getElementById(`equipment-${playerIndex}`); + + if (!equipmentCache[playerIndex]) { + equipmentCache[playerIndex] = { + weapon: null, + armor: null, + horsePlus: null, + horseMinus: null + }; + } + + const cache = equipmentCache[playerIndex]; + + if (cache.weapon === player.equipment.weapon?.name && + cache.armor === player.equipment.armor?.name && + cache.horsePlus === player.equipment.horsePlus?.name && + cache.horseMinus === player.equipment.horseMinus?.name) { + return; + } + + equipmentEl.innerHTML = ''; + cache.weapon = player.equipment.weapon?.name || null; + cache.armor = player.equipment.armor?.name || null; + cache.horsePlus = player.equipment.horsePlus?.name || null; + cache.horseMinus = player.equipment.horseMinus?.name || null; + + if (player.equipment.weapon) { + const equipCard = document.createElement('div'); + equipCard.className = 'equipment-card weapon'; + equipCard.textContent = player.equipment.weapon.name.substring(0, 2); + const effect = EQUIPMENT_EFFECTS[player.equipment.weapon.name]; + if (effect) { + equipCard.title = `${player.equipment.weapon.name}\n效果: ${effect.effect}`; + } + equipmentEl.appendChild(equipCard); + } + if (player.equipment.armor) { + const equipCard = document.createElement('div'); + equipCard.className = 'equipment-card armor'; + equipCard.textContent = player.equipment.armor.name.substring(0, 2); + const effect = EQUIPMENT_EFFECTS[player.equipment.armor.name]; + if (effect) { + equipCard.title = `${player.equipment.armor.name}\n效果: ${effect.effect}`; + } + equipmentEl.appendChild(equipCard); + } + if (player.equipment.horsePlus) { + const equipCard = document.createElement('div'); + equipCard.className = 'equipment-card horse'; + equipCard.textContent = player.equipment.horsePlus.name.substring(0, 2); + const effect = EQUIPMENT_EFFECTS[player.equipment.horsePlus.name]; + if (effect) { + equipCard.title = `${player.equipment.horsePlus.name}\n效果: ${effect.effect}`; + } + equipmentEl.appendChild(equipCard); + } + if (player.equipment.horseMinus) { + const equipCard = document.createElement('div'); + equipCard.className = 'equipment-card horse'; + equipCard.textContent = player.equipment.horseMinus.name.substring(0, 2); + const effect = EQUIPMENT_EFFECTS[player.equipment.horseMinus.name]; + if (effect) { + equipCard.title = `${player.equipment.horseMinus.name}\n效果: ${effect.effect}`; + } + equipmentEl.appendChild(equipCard); + } +} + +function selectCard(index) { + if (game.phase !== 'play' || !game.getCurrentPlayer().isHuman) return; + + if (game.selectedCardIndex === index) { + game.selectedCardIndex = -1; + game.selectedTargetIndex = -1; + } else { + game.selectedCardIndex = index; + game.selectedTargetIndex = -1; + } + + updateUI(); +} + +function selectTarget(targetIndex) { + const player = game.getCurrentPlayer(); + if (!player.isHuman || game.phase !== 'play' || game.selectedCardIndex === -1) return; + + const target = game.players[targetIndex]; + if (!target.isAlive || targetIndex === game.currentPlayerIndex) return; + + const selectedCard = player.hand[game.selectedCardIndex]; + if (!selectedCard) return; + + if (!game.isInRange(targetIndex)) { + game.addLog('目标不在攻击范围内'); + return; + } + + if (game.selectedTargetIndex === targetIndex) { + game.selectedTargetIndex = -1; + } else { + game.selectedTargetIndex = targetIndex; + } + + updateUI(); +} + +function calculateAttackRange(player) { + let range = player.attackRange; + if (player.equipment.horseMinus) { + range++; + } + return Math.max(1, range); +} + +function setupEventListeners() { + document.getElementById('ready-btn').onclick = () => { + if (game.getCurrentPlayer().isHuman && game.phase === 'ready') { + game.drawPhase(); + updateUI(); + } + }; + + document.getElementById('draw-btn').onclick = () => { + if (game.getCurrentPlayer().isHuman && game.phase === 'draw') { + game.drawPhase(); + updateUI(); + } + }; + + document.getElementById('skill-btn').onclick = () => { + if (game.getCurrentPlayer().isHuman && game.phase === 'play') { + game.useSkill(); + updateUI(); + } + }; + + document.getElementById('play-btn').onclick = () => { + if (game.selectedCardIndex !== -1 && game.getCurrentPlayer().isHuman) { + if (game.playCard(game.selectedCardIndex)) { + game.selectedCardIndex = -1; + game.selectedTargetIndex = -1; + updateUI(); + } + } + }; + + document.getElementById('end-turn-btn').onclick = () => { + if (game.getCurrentPlayer().isHuman) { + game.endTurn(); + updateUI(); + } + }; + + const modal = document.getElementById('card-modal'); + const closeBtn = document.querySelector('.close'); + + closeBtn.onclick = () => { + modal.style.display = 'none'; + }; + + window.onclick = (event) => { + if (event.target === modal) { + modal.style.display = 'none'; + } + }; +} + +window.onload = initGame; \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..4a98b80 --- /dev/null +++ b/index.html @@ -0,0 +1,183 @@ + + + + + + 三国杀 - 五人身份局 + + + +
+
+

三国杀

+

五人身份局

+

主公、忠臣、反贼、内奸

+ +
+
+ + + + + + + \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..4e61b77 --- /dev/null +++ b/style.css @@ -0,0 +1,879 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Microsoft YaHei', Arial, sans-serif; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + min-height: 100vh; + color: #fff; + overflow-x: hidden; +} + +.start-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + z-index: 1000; +} + +.start-content { + text-align: center; + padding: 60px; + background: rgba(255, 255, 255, 0.1); + border-radius: 20px; + backdrop-filter: blur(10px); + border: 2px solid rgba(255, 215, 0, 0.3); + box-shadow: 0 0 30px rgba(255, 215, 0, 0.2); +} + +.start-content h1 { + font-size: 4em; + color: #ffd700; + text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.5); + margin-bottom: 20px; +} + +.start-content h2 { + font-size: 2em; + color: #fff; + margin-bottom: 15px; +} + +.start-content p { + font-size: 1.2em; + color: #ccc; + margin-bottom: 40px; +} + +.start-btn { + padding: 15px 50px; + font-size: 1.5em; + background: linear-gradient(135deg, #ffd700, #ff8c00); + color: #1a1a2e; + border: none; + border-radius: 10px; + cursor: pointer; + font-weight: bold; + transition: all 0.3s ease; + box-shadow: 0 5px 15px rgba(255, 215, 0, 0.3); +} + +.start-btn:hover { + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(255, 215, 0, 0.5); +} + +.start-btn:active { + transform: translateY(0); +} + +.game-container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +.header { + text-align: center; + margin-bottom: 20px; + padding: 20px; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + backdrop-filter: blur(10px); +} + +.header h1 { + font-size: 2.5em; + color: #ffd700; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); + margin-bottom: 10px; +} + +.game-info { + display: flex; + justify-content: center; + gap: 20px; + font-size: 1.1em; +} + +.game-info span { + background: rgba(255, 215, 0, 0.2); + padding: 5px 15px; + border-radius: 20px; + border: 1px solid #ffd700; +} + +.game-area { + display: flex; + flex-direction: column; + gap: 15px; + min-height: 700px; +} + +.top-row { + display: flex; + justify-content: center; + gap: 20px; +} + +.middle-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 20px; +} + +.bottom-row { + display: flex; + justify-content: center; + gap: 20px; +} + +.player-card { + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + padding: 15px; + border: 2px solid transparent; + transition: all 0.3s; +} + +.player-card.active { + border-color: #ffd700; + box-shadow: 0 0 20px rgba(255, 215, 0, 0.3); +} + +.player-card.main-player { + background: rgba(255, 215, 0, 0.1); + border-color: #ffd700; +} + +.player-card.targetable { + cursor: pointer; + animation: pulse 1.5s infinite; +} + +.player-card.targeted { + border-color: #ff4444; + box-shadow: 0 0 20px rgba(255, 68, 68, 0.5); +} + +.player-card.out-of-range { + opacity: 0.5; + cursor: not-allowed; +} + +@keyframes pulse { + 0%, 100% { + box-shadow: 0 0 10px rgba(255, 215, 0, 0.3); + } + 50% { + box-shadow: 0 0 20px rgba(255, 215, 0, 0.6); + } +} + +.player-info { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; +} + +.avatar img { + width: 60px; + height: 60px; + border-radius: 50%; + border: 3px solid #ffd700; +} + +.player-details h3 { + color: #ffd700; + margin-bottom: 5px; + font-size: 1.2em; +} + +.identity { + font-size: 0.9em; + padding: 2px 8px; + border-radius: 10px; + margin-bottom: 5px; + display: inline-block; +} + +.identity.lord { + background: linear-gradient(135deg, #ffd700, #ffed4a); + color: #000; +} + +.identity.loyalist { + background: linear-gradient(135deg, #4CAF50, #45a049); + color: #fff; +} + +.identity.rebel { + background: linear-gradient(135deg, #f44336, #da190b); + color: #fff; +} + +.identity.spy { + background: linear-gradient(135deg, #9c27b0, #7b1fa2); + color: #fff; +} + +.identity.hidden { + background: #666; + color: #fff; +} + +.hp { + display: flex; + align-items: center; + gap: 10px; + margin: 5px 0; +} + +.hp-label { + font-size: 0.9em; +} + +.hp-bar { + width: 80px; + height: 18px; + background: #333; + border-radius: 9px; + overflow: hidden; + border: 1px solid #666; +} + +.hp-fill { + height: 100%; + background: linear-gradient(90deg, #ff4444, #ff6b6b); + transition: width 0.3s ease; +} + +.hp-value { + font-size: 0.9em; + color: #ff6b6b; +} + +.cards-count { + font-size: 0.9em; + color: #aaa; +} + +.equipment { + display: flex; + gap: 5px; + margin-top: 5px; + flex-wrap: wrap; +} + +.equipment-card { + width: 40px; + height: 55px; + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.6em; + border: 1px solid; + cursor: pointer; + transition: transform 0.2s ease; +} + +.equipment-card:hover { + transform: scale(1.1); +} + +.equipment-card.weapon { + background: linear-gradient(135deg, #FF5722, #E64A19); + border-color: #FF8A65; +} + +.equipment-card.armor { + background: linear-gradient(135deg, #2196F3, #1976D2); + border-color: #64b5f6; +} + +.equipment-card.horse { + background: linear-gradient(135deg, #4CAF50, #45a049); + border-color: #81C784; +} + +.skill { + margin-top: 8px; + padding: 5px 10px; + background: linear-gradient(135deg, #9C27B0, #7B1FA2); + border-radius: 5px; + font-size: 0.75em; + text-align: center; + border: 1px solid #BA68C8; + color: #fff; + font-weight: bold; +} + +.player-hand { + display: flex; + justify-content: center; + gap: 5px; + min-height: 80px; + flex-wrap: wrap; +} + +.card-back { + width: 50px; + height: 75px; + background: linear-gradient(135deg, #8B4513 0%, #A0522D 100%); + border-radius: 6px; + border: 2px solid #ffd700; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7em; + cursor: pointer; + transition: transform 0.2s; +} + +.card-back:hover { + transform: translateY(-3px); +} + +.center-area { + display: flex; + flex-direction: column; + gap: 15px; + min-width: 250px; +} + +.deck-area { + display: flex; + justify-content: center; + align-items: center; + gap: 30px; + padding: 20px; + background: rgba(0, 0, 0, 0.2); + border-radius: 10px; +} + +.deck { + position: relative; + cursor: pointer; +} + +.deck .card-back { + width: 70px; + height: 105px; +} + +.deck-count { + position: absolute; + bottom: -20px; + left: 50%; + transform: translateX(-50%); + background: #ffd700; + color: #000; + padding: 2px 8px; + border-radius: 10px; + font-size: 0.8em; + font-weight: bold; +} + +.discard-pile { + width: 70px; + height: 105px; + border: 2px dashed #666; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + color: #666; + font-size: 0.7em; +} + +.game-log { + background: rgba(0, 0, 0, 0.3); + border-radius: 10px; + padding: 15px; + max-height: 250px; + overflow-y: auto; + min-height: 200px; +} + +.log-entry { + padding: 5px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + font-size: 0.85em; + line-height: 1.4; +} + +.log-entry:last-child { + border-bottom: none; +} + +.log-entry.highlight { + color: #ffd700; + font-weight: bold; +} + +.card { + width: 60px; + height: 85px; + background: linear-gradient(135deg, #fff 0%, #f0f0f0 100%); + border-radius: 6px; + border: 2px solid #333; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 4px; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + color: #333; + font-size: 0.9em; +} + +.card:hover { + transform: translateY(-8px) scale(1.05); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); +} + +.card.selected { + transform: translateY(-12px); + box-shadow: 0 0 15px #ffd700; + border-color: #ffd700; +} + +.card.basic { + border-color: #4CAF50; +} + +.card.scroll { + border-color: #9c27b0; +} + +.card.equip { + border-color: #2196F3; +} + +.card-suit { + font-size: 1em; + font-weight: bold; +} + +.card-rank { + font-size: 0.8em; + font-weight: bold; +} + +.card-name { + font-size: 0.6em; + text-align: center; + font-weight: bold; + padding: 2px; + line-height: 1.2; +} + +.card-type { + font-size: 0.5em; + padding: 1px 4px; + border-radius: 3px; + background: #333; + color: #fff; +} + +.action-buttons { + display: flex; + justify-content: center; + gap: 10px; + margin-top: 15px; +} + +.action-btn { + padding: 10px 25px; + font-size: 0.95em; + border: none; + border-radius: 20px; + cursor: pointer; + transition: all 0.3s; + font-weight: bold; +} + +.action-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +#draw-btn { + background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); + color: white; +} + +#draw-btn:hover:not(:disabled) { + transform: scale(1.05); + box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4); +} + +#play-btn { + background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%); + color: white; +} + +#play-btn:hover:not(:disabled) { + transform: scale(1.05); + box-shadow: 0 5px 15px rgba(33, 150, 243, 0.4); +} + +#end-turn-btn { + background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%); + color: white; +} + +#end-turn-btn:hover:not(:disabled) { + transform: scale(1.05); + box-shadow: 0 5px 15px rgba(255, 152, 0, 0.4); +} + +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); +} + +.modal-content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + margin: 10% auto; + padding: 20px; + border: 2px solid #ffd700; + border-radius: 15px; + width: 300px; + position: relative; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; +} + +.close:hover { + color: #fff; +} + +.card-detail { + text-align: center; +} + +.card-detail .card { + width: 120px; + height: 170px; + margin: 0 auto 20px; + font-size: 1.2em; +} + +.card-detail h3 { + color: #ffd700; + margin-bottom: 10px; +} + +.card-detail p { + color: #ccc; + line-height: 1.6; + font-size: 0.9em; +} + +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.3); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb { + background: #ffd700; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #ffed4a; +} + +.card-icon { + font-size: 1.5em; + margin-bottom: 2px; + text-shadow: 0 0 5px rgba(0, 0, 0, 0.5); +} + +.kill-effect { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 10000; + pointer-events: none; + animation: killAnimation 1s ease-out forwards; +} + +.kill-text { + font-size: 8em; + font-weight: bold; + color: #ff0000; + text-shadow: + 0 0 20px #ff0000, + 0 0 40px #ff0000, + 0 0 60px #ff0000, + 0 0 80px #ff0000; + animation: killTextAnimation 1s ease-out forwards; +} + +@keyframes killAnimation { + 0% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.5); + } + 20% { + opacity: 1; + transform: translate(-50%, -50%) scale(1.2); + } + 40% { + transform: translate(-50%, -50%) scale(1); + } + 100% { + opacity: 0; + transform: translate(-50%, -50%) scale(1.5); + } +} + +@keyframes killTextAnimation { + 0% { + transform: rotate(-10deg) scale(0.5); + letter-spacing: 0.1em; + } + 20% { + transform: rotate(0deg) scale(1.2); + letter-spacing: 0.2em; + } + 40% { + transform: rotate(5deg) scale(1); + letter-spacing: 0.15em; + } + 100% { + transform: rotate(0deg) scale(1.5); + letter-spacing: 0.3em; + } +} + +.damage-effect { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 3em; + font-weight: bold; + color: #ff0000; + text-shadow: 0 0 10px #ff0000; + animation: damageAnimation 0.5s ease-out forwards; + pointer-events: none; + z-index: 1000; +} + +@keyframes damageAnimation { + 0% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + 100% { + opacity: 0; + transform: translate(-50%, -150%) scale(1.5); + } +} + +.heal-effect { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 3em; + font-weight: bold; + color: #00ff00; + text-shadow: 0 0 10px #00ff00; + animation: healAnimation 0.5s ease-out forwards; + pointer-events: none; + z-index: 1000; +} + +@keyframes healAnimation { + 0% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + 100% { + opacity: 0; + transform: translate(-50%, -150%) scale(1.5); + } +} + +.player-card.dead { + animation: deathAnimation 1s ease-out forwards; + filter: grayscale(100%); + opacity: 0.5; +} + +@keyframes deathAnimation { + 0% { + transform: scale(1); + } + 50% { + transform: scale(0.9); + filter: brightness(2); + } + 100% { + transform: scale(0.8); + filter: grayscale(100%); + } +} + +.death-animation { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 4em; + animation: deathIconAnimation 0.8s ease-out forwards; + pointer-events: none; + z-index: 1000; +} + +@keyframes deathIconAnimation { + 0% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.5) rotate(0deg); + } + 20% { + opacity: 1; + transform: translate(-50%, -50%) scale(1.2) rotate(10deg); + } + 40% { + transform: translate(-50%, -50%) scale(1) rotate(-5deg); + } + 60% { + transform: translate(-50%, -50%) scale(1.1) rotate(5deg); + } + 100% { + opacity: 0; + transform: translate(-50%, -50%) scale(2) rotate(0deg); + } +} + +.hp-fill { + transition: width 0.5s ease-out, background 0.3s ease; +} + +.hp-fill.damaged { + animation: hpDamageAnimation 0.5s ease-out; +} + +@keyframes hpDamageAnimation { + 0% { + background: #ff0000; + } + 100% { + background: #4CAF50; + } +} + +.card.use-animation { + animation: cardUseAnimation 0.5s ease-out forwards; +} + +@keyframes cardUseAnimation { + 0% { + transform: translateY(0) scale(1); + } + 50% { + transform: translateY(-20px) scale(1.1); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5); + } + 100% { + transform: translateY(0) scale(1); + } +} + +.attack-animation { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 4em; + animation: attackAnimation 0.4s ease-out forwards; + pointer-events: none; + z-index: 1000; +} + +@keyframes attackAnimation { + 0% { + opacity: 1; + transform: translate(-50%, -50%) scale(0.5) rotate(-45deg); + } + 50% { + opacity: 1; + transform: translate(-50%, -50%) scale(1.2) rotate(0deg); + } + 100% { + opacity: 0; + transform: translate(-50%, -50%) scale(1.5) rotate(45deg); + } +} + +.shield-animation { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 4em; + animation: shieldAnimation 0.8s ease-out forwards; + pointer-events: none; + z-index: 1000; +} + +@keyframes shieldAnimation { + 0% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.5); + } + 50% { + opacity: 1; + transform: translate(-50%, -50%) scale(1.2); + } + 100% { + opacity: 0; + transform: translate(-50%, -50%) scale(1.5); + } +} + +@media (max-width: 1200px) { + .middle-row { + flex-direction: column; + align-items: center; + } + + .center-area { + order: -1; + } +} \ No newline at end of file