Warhummer/static/js/game.js
2025-06-25 09:35:26 +08:00

2005 lines
66 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 游戏状态
const gameState = {
governor: '', // 玩家名称
reignTime: 0, // 当前统治时间
year: 41000, // 当前年份
stats: {
loyalty: 50, // 帝国忠诚度
chaos: 50, // 混沌侵染
population: 50, // 民众控制
military: 50, // 军事力量
resources: 50 // 资源储备
},
statusEffects: [], // 状态效果
currentCardIndex: 0, // 当前卡片索引
isDealing: false, // 是否正在发牌
isDragging: false, // 是否正在拖动
gameOver: false, // 游戏是否结束
gameOverReason: '', // 游戏结束原因
characters: [], // 角色数据
availableCharacters: [], // 可用角色池
currentEffects: null, // 当前卡片效果
currentEvent: null, // 当前事件
currentCharacter: null, // 当前角色
// 新增: 事件和状态相关属性
activeEvent: null, // 当前活跃事件
eventNode: null, // 当前事件节点
completedEvents: [], // 已完成事件
activeStatuses: [], // 当前活跃的状态效果
// 开发者模式属性
devMode: false, // 开发者模式标志
nextEventId: null, // 下一个要触发的事件ID
};
// DOM 元素
const startScreen = document.querySelector('.start-screen');
const startButton = document.querySelector('.start-button');
const gameContainer = document.querySelector('.game-container');
const cardContainer = document.getElementById('card-container');
const speechBubble = document.getElementById('speech-bubble');
const gameOverDialog = document.getElementById('game-over-dialog');
const gameOverReason = document.getElementById('game-over-reason');
const finalYears = document.getElementById('final-years');
const restartButton = document.getElementById('restart-button');
const reignYearsElement = document.getElementById('reign-years');
const currentYearElement = document.getElementById('current-year');
const playerNameElement = document.getElementById('player-name');
const statusBars = {
loyalty: document.getElementById('loyalty-bar'),
chaos: document.getElementById('chaos-bar'),
population: document.getElementById('population-bar'),
military: document.getElementById('military-bar'),
resources: document.getElementById('resources-bar')
};
const statusIcons = {
loyalty: document.getElementById('loyalty-icon'),
chaos: document.getElementById('chaos-icon'),
population: document.getElementById('population-icon'),
military: document.getElementById('military-icon'),
resources: document.getElementById('resources-icon')
};
// 新增: 状态和开发者工具相关的DOM元素
const activeStatusesContainer = document.getElementById('active-statuses');
const statusDetailsDialog = document.getElementById('status-details-dialog');
const closeStatusDetailsButton = document.getElementById('close-status-details');
const devToolsButton = document.getElementById('dev-tools-button');
const devToolsDialog = document.getElementById('dev-tools-dialog');
const closeDevToolsButton = document.getElementById('close-dev-tools');
const triggerEventButton = document.getElementById('trigger-event-btn');
const eventSelector = document.getElementById('event-selector');
const statusSelector = document.getElementById('status-selector');
const addStatusButton = document.getElementById('add-status-btn');
const removeStatusButton = document.getElementById('remove-status-btn');
const statSelector = document.getElementById('stat-selector');
const statValueInput = document.getElementById('stat-value');
const setStatButton = document.getElementById('set-stat-btn');
// 拖动相关变量
let startX = 0;
let currentX = 0;
let activeCard = null;
// 加载游戏数据
function loadGameData() {
if (gameData && gameData.cards) {
gameState.characters = gameData.cards.characters;
// 初始化可用角色池 - 仅包含常驻角色
gameState.availableCharacters = gameState.characters
.filter(char => char.type === 'resident')
.map(char => char.id);
}
// 获取上次保存的年份
if (gameData && gameData.lastYear) {
gameState.year = gameData.lastYear;
}
// 从服务器加载保存的游戏状态
fetch('/api/load_game')
.then(response => response.json())
.then(data => {
if (data.game_data) {
// 恢复保存的游戏状态
Object.assign(gameState, data.game_data);
updateUI();
} else if (data.lastYear) {
// 如果没有游戏状态但有最后年份,设置年份
gameState.year = data.lastYear;
updateUI();
}
})
.catch(error => console.error('加载游戏状态失败:', error));
// 设置初始总督名称
gameState.governor = document.querySelector('#player-name').textContent || '总督';
// 检查是否启用开发者模式
checkDevMode();
// 初始化开发者工具中的事件和状态选择器
initDevTools();
}
// 开始游戏事件
startButton.addEventListener('click', startGame);
// 开始游戏函数
function startGame() {
startScreen.style.opacity = '0';
setTimeout(() => {
startScreen.style.display = 'none';
gameContainer.style.opacity = '1';
initGame();
}, 1000);
}
// 初始化游戏
function initGame() {
// 保留当前年份
const currentYear = gameState.year;
// 重置游戏状态为默认值
gameState.reignTime = 0;
gameState.year = currentYear; // 使用之前的年份而非固定值
gameState.stats = {
loyalty: 50,
chaos: 50,
population: 50,
military: 50,
resources: 50
};
gameState.statusEffects = [];
gameState.currentCardIndex = 0;
gameState.isDealing = false;
gameState.isDragging = false;
gameState.gameOver = false;
gameState.gameOverReason = '';
gameState.currentEffects = null;
gameState.currentEvent = null;
gameState.currentCharacter = null;
// 重置事件和状态相关属性
gameState.activeEvent = null;
gameState.eventNode = null;
gameState.completedEvents = [];
gameState.activeStatuses = [];
// 重新洗牌可用角色
shuffleAvailableCharacters();
// 更新UI
updateUI();
// 清空卡容器
cardContainer.innerHTML = '';
// 隐藏对话气泡
speechBubble.classList.remove('visible');
speechBubble.textContent = '';
// 移除所有效果指示器
removeAllEffectIndicators();
// 清空状态图标区域
activeStatusesContainer.innerHTML = '';
// 开始发牌动画
startDealingCards();
}
// 检查是否启用开发者模式
function checkDevMode() {
if (gameState.governor === '1') {
gameState.devMode = true;
devToolsButton.style.display = 'flex';
} else {
gameState.devMode = false;
devToolsButton.style.display = 'none';
}
}
// 初始化开发者工具
function initDevTools() {
// 检查数据结构
if (!gameData) return;
// 清空现有选项
eventSelector.innerHTML = '<option value="">选择事件...</option>';
statusSelector.innerHTML = '<option value="">选择状态...</option>';
// 添加事件选项 - 考虑两种可能的数据结构
const events = gameData.events || (gameData.cards && gameData.cards.events);
if (events && events.length > 0) {
events.forEach(event => {
const option = document.createElement('option');
option.value = event.id;
option.textContent = event.name;
eventSelector.appendChild(option);
});
} else {
console.warn('未找到事件数据');
}
// 添加状态选项 - 考虑两种可能的数据结构
const statuses = gameData.statuses || (gameData.cards && gameData.cards.statuses);
if (statuses && statuses.length > 0) {
statuses.forEach(status => {
const option = document.createElement('option');
option.value = status.id;
option.textContent = status.name;
statusSelector.appendChild(option);
});
} else {
console.warn('未找到状态数据');
}
// 记录一下数据结构以进行调试
console.log('GameData structure:', gameData);
}
// 添加状态影响指示器
function addEffectIndicators() {
// 先移除所有现有指示器
removeAllEffectIndicators();
if (!gameState.currentEffects) return;
// 为每个状态添加指示器
for (const stat in gameState.currentEffects) {
const value = gameState.currentEffects[stat];
if (value === 0) continue; // 跳过无影响的属性
const iconElement = statusIcons[stat].parentElement;
// 创建指示器容器
const indicator = document.createElement('div');
indicator.className = 'status-indicator';
indicator.classList.add(value > 0 ? 'positive-effect' : 'negative-effect');
// 根据影响值大小决定显示大圆点还是小圆点
const absValue = Math.abs(value);
if (absValue >= 15) {
// 大影响,显示大圆点
const dot = document.createElement('div');
dot.className = 'indicator-dot large';
indicator.appendChild(dot);
} else if (absValue >= 10) {
// 中等影响,显示一个小圆点
const dot = document.createElement('div');
dot.className = 'indicator-dot small';
indicator.appendChild(dot);
} else {
// 小影响,不显示圆点
// 或者也可以显示一个更小的圆点
const dot = document.createElement('div');
dot.className = 'indicator-dot small';
dot.style.width = '5px';
dot.style.height = '5px';
indicator.appendChild(dot);
}
// 添加到状态图标元素
iconElement.appendChild(indicator);
}
}
// 移除所有效果指示器
function removeAllEffectIndicators() {
document.querySelectorAll('.status-indicator').forEach(indicator => {
indicator.remove();
});
}
// 重新构建可用角色池,考虑权重
function shuffleAvailableCharacters() {
// 清空可用角色池
gameState.availableCharacters = [];
// 筛选常驻角色
const residentCharacters = gameState.characters.filter(char => char.type === 'resident');
// 根据权重添加角色ID到可用池中
residentCharacters.forEach(character => {
const weight = character.weight || 100; // 默认权重为100
// 根据权重添加多次角色ID增加被选中概率
for (let i = 0; i < weight; i++) {
gameState.availableCharacters.push(character.id);
}
});
// 打乱角色顺序
gameState.availableCharacters.sort(() => Math.random() - 0.5);
}
// 显示对话气泡
function showSpeechBubble(text) {
speechBubble.textContent = text;
speechBubble.classList.add('visible');
}
// 隐藏对话气泡
function hideSpeechBubble() {
speechBubble.classList.remove('visible');
}
// 开始发牌动画
function startDealingCards() {
gameState.isDealing = true;
let cardsDealt = 0;
const totalCards = 5;
// 发5张牌
for (let i = 0; i < totalCards; i++) {
setTimeout(() => {
const card = document.createElement('div');
card.className = 'card card-back';
// 设置初始位置 - 左上角45度
card.style.transform = 'translate(-150px, -150px) rotate(-45deg) scale(0.8)';
card.style.opacity = '0';
card.style.transition = 'none';
// 设置层级,越后面的卡片层级越低
card.style.zIndex = `${5 - i}`;
cardContainer.appendChild(card);
// 强制重绘以确保过渡效果正常
void card.offsetWidth;
// 添加过渡属性
card.style.transition = 'transform 0.4s ease, opacity 0.4s ease';
// 设置终点位置 - 所有牌都落在中心位置,但有微小偏移以显示后面还有牌
setTimeout(() => {
// 计算每张牌的偏移,让它们有细微重叠
const offsetX = i * 2; // 每张牌向右偏移2px
const offsetY = i * 2; // 每张牌向下偏移2px
const randomAngle = (Math.random() - 0.5) * 2; // 添加微小随机角度
card.style.transform = `translate(${offsetX}px, ${offsetY}px) rotate(${randomAngle}deg)`;
card.style.opacity = '1';
// 给静态背景牌添加一个类,方便后续识别
if (i > 0) {
card.classList.add('static-background-card');
}
// 计数已发的牌
cardsDealt++;
// 全部发完后,等待一小段时间再翻转第一张牌
if (cardsDealt === totalCards) {
setTimeout(() => {
// 翻转最上面的牌
const topCard = cardContainer.querySelector('.card-back:not(.static-background-card)');
if (topCard) {
// 检查是否有指定的下一个事件
if (gameState.devMode && gameState.nextEventId) {
startEvent(gameState.nextEventId);
gameState.nextEventId = null; // 重置
} else {
// 检查是否应该触发随机事件
const eventToTrigger = checkEventTriggers();
if (eventToTrigger) {
startEvent(eventToTrigger);
} else {
flipTopCard(topCard);
}
}
}
// 结束发牌状态
gameState.isDealing = false;
}, 600);
}
}, 50);
}, i * 200); // 每张牌间隔200ms发出
}
}
// 检查是否应该触发事件
function checkEventTriggers() {
if (!gameData || !gameData.events) return null;
// 筛选可能触发的事件
const possibleEvents = gameData.events.filter(event => {
// 跳过已完成的事件
if (gameState.completedEvents.includes(event.id)) return false;
// 检查触发条件
if (event.trigger) {
if (event.trigger.type === 'random') {
// 随机触发事件 - 按概率判断
return Math.random() < (event.trigger.probability || 0.1);
} else if (event.trigger.type === 'condition') {
// 条件触发事件
const stat = event.trigger.stat;
const condition = event.trigger.condition;
const value = event.trigger.value;
if (stat && condition && value !== undefined) {
const currentValue = gameState.stats[stat];
switch (condition) {
case 'less_than':
return currentValue < value;
case 'greater_than':
return currentValue > value;
case 'equal':
return currentValue === value;
}
}
} else if (event.trigger.type === 'status') {
// 状态触发事件
const statusId = event.trigger.statusId;
const condition = event.trigger.condition || 'exists';
const hasStatus = gameState.activeStatuses.some(s => s.id === statusId);
return condition === 'exists' ? hasStatus : !hasStatus;
}
}
return false;
});
// 如果有可能触发的事件,随机选择一个
if (possibleEvents.length > 0) {
const randomIndex = Math.floor(Math.random() * possibleEvents.length);
return possibleEvents[randomIndex].id;
}
return null;
}
// 开始一个事件
function startEvent(eventId) {
if (!gameData || !gameData.events) return;
// 查找事件数据
const eventData = gameData.events.find(e => e.id === eventId);
if (!eventData || !eventData.nodes || eventData.nodes.length === 0) return;
// 设置当前事件
gameState.activeEvent = eventData;
gameState.eventNode = eventData.nodes[0]; // 从第一个节点开始
// 显示第一个节点的卡片
showEventCard(gameState.eventNode);
}
// 显示事件卡片
function showEventCard(node) {
if (!node) return;
// 查找角色数据
const character = gameState.characters.find(c => c.id === node.character);
if (!character) return;
// 清理现有卡片和信息
const existingFrontCard = document.getElementById('active-card');
if (existingFrontCard) {
existingFrontCard.remove();
}
const existingCharInfo = document.querySelector('.character-info');
if (existingCharInfo) {
existingCharInfo.remove();
}
// 创建角色信息区域
const characterInfo = document.createElement('div');
characterInfo.className = 'character-info';
characterInfo.innerHTML = `
<div class="character-name">${character.name}</div>
<div class="character-title">${character.title}</div>
`;
characterInfo.style.opacity = '0';
characterInfo.style.zIndex = '20';
// 检查角色是否有图片路径
const hasImage = character.avatarPath && character.avatarPath.length > 0;
const portraitContent = hasImage
? `<img src="/static/${character.avatarPath}" alt="${character.name}">`
: `<div class="fallback-avatar">${character.avatar}</div>`;
// 创建事件卡片
const eventCard = document.createElement('div');
eventCard.className = 'card';
eventCard.id = 'active-card';
eventCard.dataset.eventId = gameState.activeEvent.id;
eventCard.dataset.nodeId = node.id;
eventCard.style.zIndex = '15';
// 设置卡片内容 - 包括选项
eventCard.innerHTML = `
<div class="character-portrait">${portraitContent}</div>
<div class="option-overlay right-option-overlay">${node.options[0].text}</div>
<div class="option-overlay left-option-overlay">${node.options.length > 1 ? node.options[1].text : node.options[0].text}</div>
`;
// 添加到DOM
cardContainer.appendChild(characterInfo);
cardContainer.appendChild(eventCard);
// 设置卡片初始样式
eventCard.style.transform = 'rotateY(-90deg)';
eventCard.style.opacity = '0';
eventCard.style.transition = 'none';
// 强制重绘
void eventCard.offsetWidth;
// 寻找一张背面牌用于翻转效果
const topBackCard = cardContainer.querySelector('.card-back:not(.static-background-card)');
if (topBackCard) {
// 背面牌开始翻转
topBackCard.style.transition = 'all 0.4s ease';
topBackCard.style.transform = 'rotateY(90deg)';
topBackCard.style.opacity = '0';
topBackCard.style.zIndex = '5';
}
// 等待背面牌翻转到一半后,开始前面牌的翻转
setTimeout(() => {
// 前面卡片开始翻转
eventCard.style.transition = 'all 0.4s ease';
eventCard.style.transform = 'rotateY(0deg)';
eventCard.style.opacity = '1';
// 设置为当前活动卡片
activeCard = eventCard;
// 显示对话气泡
showSpeechBubble(node.text);
// 显示角色信息
setTimeout(() => {
characterInfo.style.opacity = '1';
// 添加拖动事件
setupCardDrag(eventCard, true); // 标记为事件卡片
// 移除原来的背面牌
if (topBackCard && topBackCard.parentNode) {
topBackCard.remove();
}
// 添加一张新的背面牌到牌堆顶部
addNewBackCard();
}, 400);
}, 200);
}
// 继续事件处理
function continueEvent(optionIndex) {
if (!gameState.activeEvent || !gameState.eventNode) {
console.error("无法继续事件:事件数据缺失");
return;
}
// 获取选中的选项
const options = gameState.eventNode.options;
if (!options || !options[optionIndex]) {
console.error("无法继续事件:选项数据缺失");
return;
}
const option = options[optionIndex];
// 应用选项的效果
if (option.effects) {
applyEffects(option.effects);
}
// 添加状态
if (option.add_status) {
addStatus(option.add_status);
}
// 移除状态
if (option.remove_status) {
removeStatus(option.remove_status);
}
// 检查下一个节点
if (option.next_node && option.next_node !== 'end') {
// 查找下一个节点
const nextNode = gameState.activeEvent.nodes.find(n => n.id === option.next_node);
if (nextNode) {
// 更新当前节点
gameState.eventNode = nextNode;
// 显示下一个节点的卡片
showEventCard(nextNode);
return;
}
}
// 如果没有下一个节点或者是结束节点,结束事件
endEvent();
}
// 结束当前事件
function endEvent() {
// 将事件添加到已完成列表
if (gameState.activeEvent) {
gameState.completedEvents.push(gameState.activeEvent.id);
}
// 重置事件状态
gameState.activeEvent = null;
gameState.eventNode = null;
// 继续游戏流程 - 翻转一张新卡片
setTimeout(() => {
if (!gameState.gameOver) {
flipTopCard();
}
}, 500);
}
// 找到flipTopCard函数修改角色选择部分以下是完整函数
function flipTopCard(topBackCard) {
// 如果没有可用角色,则重新洗牌
if (gameState.availableCharacters.length === 0) {
shuffleAvailableCharacters();
}
// 从可用角色池中随机选择一个角色ID权重已经在池中体现
const randomIndex = Math.floor(Math.random() * gameState.availableCharacters.length);
const characterId = gameState.availableCharacters[randomIndex];
// 从可用池中移除该角色ID
gameState.availableCharacters.splice(randomIndex, 1);
// 获取角色数据
const character = gameState.characters.find(c => c.id === characterId);
if (!character || !character.events || character.events.length === 0) {
// 如果角色无效或没有事件,则跳过并尝试下一个
console.warn("角色无效或没有事件:", characterId);
flipTopCard(topBackCard);
return;
}
// 从角色的事件中随机选择一个
const eventIndex = Math.floor(Math.random() * character.events.length);
const event = character.events[eventIndex];
// 保存当前事件和角色及其效果
gameState.currentEvent = event;
gameState.currentCharacter = character;
gameState.currentEffects = {
loyalty: event.optionA.effects.loyalty,
chaos: event.optionA.effects.chaos,
population: event.optionA.effects.population,
military: event.optionA.effects.military,
resources: event.optionA.effects.resources
};
// 如果没有传入顶部牌,则找到当前最上面的背面牌
if (!topBackCard) {
topBackCard = cardContainer.querySelector('.card-back:not(.static-background-card)');
if (!topBackCard) return; // 如果没有找到,直接返回
}
// 清理可能存在的旧元素
const existingFrontCard = document.getElementById('active-card');
if (existingFrontCard) {
existingFrontCard.remove();
}
const existingCharInfo = document.querySelector('.character-info');
if (existingCharInfo) {
existingCharInfo.remove();
}
// 创建角色信息区域
const characterInfo = document.createElement('div');
characterInfo.className = 'character-info';
characterInfo.innerHTML = `
<div class="character-name">${character.name}</div>
<div class="character-title">${character.title}</div>
`;
characterInfo.style.opacity = '0';
characterInfo.style.zIndex = '20';
// 检查角色是否有图片路径,如果没有则使用表情图标
const hasImage = character.avatarPath && character.avatarPath.length > 0;
const portraitContent = hasImage
? `<img src="/static/${character.avatarPath}" alt="${character.name}">`
: `<div class="fallback-avatar">${character.avatar}</div>`;
// 创建新卡片 (前面)
const frontCard = document.createElement('div');
frontCard.className = 'card';
frontCard.id = 'active-card';
frontCard.dataset.characterId = character.id;
frontCard.dataset.eventId = event.id;
frontCard.style.zIndex = '15';
// 设置卡片内容
frontCard.innerHTML = `
<div class="character-portrait">${portraitContent}</div>
<div class="option-overlay right-option-overlay">${event.optionA.text}</div>
<div class="option-overlay left-option-overlay">${event.optionB.text}</div>
`;
// 添加到DOM
cardContainer.appendChild(characterInfo);
cardContainer.appendChild(frontCard);
// 重要先创建元素但不添加到DOM
// 设置前面卡片初始样式 - 已经旋转到-90度
frontCard.style.transform = 'rotateY(-90deg)';
frontCard.style.opacity = '0';
frontCard.style.transition = 'none';
// 强制重绘以确保初始状态被应用
void frontCard.offsetWidth;
// 开始背面卡片的翻转动画
topBackCard.style.transition = 'all 0.4s ease';
topBackCard.style.transform = 'rotateY(90deg)';
topBackCard.style.opacity = '0';
// 等待背面卡片翻转到一半后,开始前面卡片的翻转
setTimeout(() => {
// 设置过渡效果并开始翻转
frontCard.style.transition = 'all 0.4s ease';
frontCard.style.transform = 'rotateY(0deg)';
frontCard.style.opacity = '1';
// 设置为当前活动卡片
activeCard = frontCard;
// 等动画完成后显示角色信息和添加事件
setTimeout(() => {
// 显示角色信息
characterInfo.style.transition = 'opacity 0.3s ease';
characterInfo.style.opacity = '1';
// 显示对话气泡
showSpeechBubble(event.text);
// 添加拖动事件
setupCardDrag(frontCard);
// 移除原来的背面牌
if (topBackCard.parentNode) {
topBackCard.remove();
}
// 添加一张新的背面牌到牌堆顶部
addNewBackCard();
}, 400);
}, 200);
}
// 添加一张新的背面牌到牌堆顶部
function addNewBackCard() {
// 查找所有背景牌
const backgroundCards = document.querySelectorAll('.static-background-card');
if (backgroundCards.length === 0) return;
// 创建一张新的背面牌
const newBackCard = document.createElement('div');
newBackCard.className = 'card card-back';
// 设置位置和样式,与其他背景牌保持一致的错开效果
// 从第一张背景牌获取位置参考
const referenceCard = backgroundCards[0];
const referenceRect = referenceCard.getBoundingClientRect();
const containerRect = cardContainer.getBoundingClientRect();
// 计算相对位置
const relativeX = referenceRect.left - containerRect.left;
const relativeY = referenceRect.top - containerRect.top;
// 设置新牌的位置,略微比参考牌更靠前
newBackCard.style.transform = `translate(${relativeX - 2}px, ${relativeY - 2}px)`;
newBackCard.style.opacity = '1';
newBackCard.style.zIndex = '5'; // 比背景牌高,但比内容牌低
// 添加到容器
cardContainer.insertBefore(newBackCard, cardContainer.firstChild);
}
// 替换 setupCardDrag 函数以更清晰地设置卡片类型
function setupCardDrag(card, isEventCard = false) {
// 标记卡片类型 - 确保这里明确设置了属性
if (isEventCard) {
card.dataset.cardType = 'event';
} else {
card.dataset.cardType = 'normal';
}
// 为每张卡单独添加事件
card.addEventListener('mousedown', function(e) {
onCardDragStart(e, this);
});
card.addEventListener('touchstart', function(e) {
onCardDragStart(e, this);
});
}
// 更新 onCardDragStart 不再需要传递 isEventCard 参数
function onCardDragStart(e, card) {
// 如果正在发牌或已经在拖动,则不处理
if (gameState.isDealing || gameState.isDragging || gameState.gameOver) return;
e.preventDefault();
gameState.isDragging = true;
// 记录起始位置
startX = e.type === 'mousedown' ? e.clientX : e.touches[0].clientX;
currentX = startX;
// 移除过渡效果使拖动更平滑
activeCard.style.transition = 'none';
// 添加全局事件
document.addEventListener('mousemove', onCardDragMove);
document.addEventListener('touchmove', onCardDragMove, { passive: false });
document.addEventListener('mouseup', onCardDragEnd);
document.addEventListener('touchend', onCardDragEnd);
}
// 卡片拖动中
function onCardDragMove(e) {
if (!gameState.isDragging) return;
// 阻止触摸事件的默认行为(滚动)
if (e.type === 'touchmove') {
e.preventDefault();
}
// 计算水平移动距离
currentX = e.type === 'mousemove' ? e.clientX : e.touches[0].clientX;
const deltaX = currentX - startX;
// 移动卡片,添加旋转效果
activeCard.style.transform = `translateX(${deltaX}px) rotate(${deltaX * 0.05}deg)`;
// 显示选择选项和效果
updateChoiceDisplay(deltaX);
}
// 更新选择选项显示和效果指示器
function updateChoiceDisplay(deltaX) {
if (!activeCard) return;
// 获取右上角和左上角选项区域
// 注意:左滑显示右上角选项,右滑显示左上角选项
const rightOption = activeCard.querySelector('.right-option-overlay'); // 左滑-同意(蓝色)
const leftOption = activeCard.querySelector('.left-option-overlay'); // 右滑-拒绝(红色)
if (!rightOption || !leftOption) return;
// 根据拖动距离计算透明度
// 左滑(负值)时显示右上角选项(蓝色)
// 右滑(正值)时显示左上角选项(红色)
const rightOptionOpacity = deltaX < 0 ? Math.min(1, Math.abs(deltaX) / 100) : 0;
const leftOptionOpacity = deltaX > 0 ? Math.min(1, Math.abs(deltaX) / 100) : 0;
rightOption.style.opacity = rightOptionOpacity;
leftOption.style.opacity = leftOptionOpacity;
// 根据拖动方向显示相应效果
// 检查是事件卡片还是普通卡片
const isEventCard = activeCard.dataset.cardType === 'event';
const isDeathCard = activeCard.classList.contains('death-card');
// 死亡卡片不显示效果
if (isDeathCard) {
removeAllEffectIndicators();
return;
}
if (isEventCard) {
// 事件卡片 - 检查当前节点选项
const node = gameState.eventNode;
if (!node) return;
if (deltaX < -50) {
// 左滑 - 选项0的效果
if (node.options[0].effects) {
showEventOptionEffects(node.options[0].effects);
} else {
removeAllEffectIndicators();
}
} else if (deltaX > 50) {
// 右滑 - 选项1的效果
const option = node.options.length > 1 ? node.options[1] : node.options[0];
if (option.effects) {
showEventOptionEffects(option.effects);
} else {
removeAllEffectIndicators();
}
} else {
// 回到中间,移除效果显示
removeAllEffectIndicators();
}
} else {
// 普通卡片
if (deltaX < -50) {
// 左滑 - 显示选项A的效果(蓝色)
showOptionEffects('A');
} else if (deltaX > 50) {
// 右滑 - 显示选项B的效果(红色)
showOptionEffects('B');
} else {
// 回到中间,移除效果显示
removeAllEffectIndicators();
}
}
}
// 显示事件选项效果
function showEventOptionEffects(effects) {
if (!effects) return;
// 移除现有指示器
removeAllEffectIndicators();
// 添加新指示器
for (const stat in effects) {
const value = effects[stat];
if (value === 0) continue; // 跳过无影响的属性
const iconElement = statusIcons[stat].parentElement;
// 创建指示器容器
const indicator = document.createElement('div');
indicator.className = 'status-indicator';
indicator.classList.add(value > 0 ? 'positive-effect' : 'negative-effect');
indicator.style.opacity = '1'; // 确保可见
// 根据影响值大小决定显示大圆点还是小圆点
const absValue = Math.abs(value);
if (absValue >= 15) {
// 大影响,显示大圆点
const dot = document.createElement('div');
dot.className = 'indicator-dot large';
indicator.appendChild(dot);
} else if (absValue >= 10) {
// 中等影响,显示一个小圆点
const dot = document.createElement('div');
dot.className = 'indicator-dot small';
indicator.appendChild(dot);
} else {
// 小影响,显示一个更小的圆点
const dot = document.createElement('div');
dot.className = 'indicator-dot small';
dot.style.width = '5px';
dot.style.height = '5px';
indicator.appendChild(dot);
}
// 添加到状态图标元素
iconElement.appendChild(indicator);
}
}
// 显示选项效果
function showOptionEffects(option) {
if (!gameState.currentEvent) return;
// 获取效果
const effects = option === 'A' ?
gameState.currentEvent.optionA.effects :
gameState.currentEvent.optionB.effects;
// 移除现有指示器
removeAllEffectIndicators();
// 添加新指示器
for (const stat in effects) {
const value = effects[stat];
if (value === 0) continue; // 跳过无影响的属性
const iconElement = statusIcons[stat].parentElement;
// 创建指示器容器
const indicator = document.createElement('div');
indicator.className = 'status-indicator';
indicator.classList.add(value > 0 ? 'positive-effect' : 'negative-effect');
indicator.style.opacity = '1'; // 确保可见
// 根据影响值大小决定显示大圆点还是小圆点
const absValue = Math.abs(value);
if (absValue >= 15) {
// 大影响,显示大圆点
const dot = document.createElement('div');
dot.className = 'indicator-dot large';
indicator.appendChild(dot);
} else if (absValue >= 10) {
// 中等影响,显示一个小圆点
const dot = document.createElement('div');
dot.className = 'indicator-dot small';
indicator.appendChild(dot);
} else {
// 小影响,显示一个更小的圆点
const dot = document.createElement('div');
dot.className = 'indicator-dot small';
dot.style.width = '5px';
dot.style.height = '5px';
indicator.appendChild(dot);
}
// 添加到状态图标元素
iconElement.appendChild(indicator);
}
}
// 替换 onCardDragEnd 函数
function onCardDragEnd(e, isEventCard) {
if (!gameState.isDragging || !activeCard) return;
// 计算最终移动距离
const deltaX = currentX - startX;
// 添加过渡效果
activeCard.style.transition = 'transform 0.5s ease, opacity 0.5s ease';
// 从数据集获取实际卡片类型,而不是依赖传递的参数
const cardType = activeCard.dataset.cardType;
const isCardEvent = cardType === 'event';
// 检查是否为死亡卡片
const isDeathCard = activeCard.classList.contains('death-card');
// 如果是死亡卡片直接调用swipeDeathCard
if (isDeathCard) {
const characterInfo = document.querySelector('.character-info');
swipeDeathCard(activeCard, characterInfo, activeCard.dataset.callback);
gameState.isDragging = false;
return;
}
// 处理正常卡片选择
if (deltaX < -100) {
// 左滑 - 选择左侧选项
if (isCardEvent && gameState.eventNode) {
selectEventChoice(0); // 事件选项0
} else if (gameState.currentEvent) {
selectChoice('left'); // 普通卡片左选项
} else {
// 恢复原位
resetCardPosition();
}
} else if (deltaX > 100) {
// 右滑 - 选择右侧选项
if (isCardEvent && gameState.eventNode) {
// 对于事件卡片,检查是否有第二个选项
const optionIndex = gameState.eventNode.options.length > 1 ? 1 : 0;
selectEventChoice(optionIndex);
} else if (gameState.currentEvent) {
selectChoice('right'); // 普通卡片右选项
} else {
// 恢复原位
resetCardPosition();
}
} else {
// 回到原位
resetCardPosition();
}
// 移除全局事件监听
document.removeEventListener('mousemove', onCardDragMove);
document.removeEventListener('touchmove', onCardDragMove);
document.removeEventListener('mouseup', onCardDragEnd);
document.removeEventListener('touchend', onCardDragEnd);
}
// 重置卡片位置
function resetCardPosition() {
if (!activeCard) return;
activeCard.style.transform = 'translateX(0) rotate(0)';
// 隐藏选项覆盖层
const rightOption = activeCard.querySelector('.right-option-overlay');
const leftOption = activeCard.querySelector('.left-option-overlay');
if (rightOption) rightOption.style.opacity = 0;
if (leftOption) leftOption.style.opacity = 0;
// 移除效果指示器
removeAllEffectIndicators();
// 重置拖动状态
gameState.isDragging = false;
}
// 选择事件选项
function selectEventChoice(optionIndex) {
if (!gameState.activeEvent || !gameState.eventNode) {
console.error('当前事件或节点数据丢失');
resetCardPosition();
return;
}
// 获取角色信息元素
const characterInfo = document.querySelector('.character-info');
// 选择飞出方向,左滑(选项0)向左,右滑(选项1)向右
if (optionIndex === 0) {
activeCard.style.transform = 'translateX(-1000px) rotate(-30deg)';
} else {
activeCard.style.transform = 'translateX(1000px) rotate(30deg)';
}
// 淡出并移除
activeCard.style.opacity = '0';
// 同时淡出角色信息和对话气泡
if (characterInfo) {
characterInfo.style.opacity = '0';
}
hideSpeechBubble();
// 移除效果指示器
removeAllEffectIndicators();
// 重置拖动状态
gameState.isDragging = false;
// 延迟后处理选项效果和显示下一张卡片
setTimeout(() => {
if (activeCard && activeCard.parentNode) {
activeCard.remove();
activeCard = null;
}
if (characterInfo && characterInfo.parentNode) {
characterInfo.remove();
}
// 继续事件流程
continueEvent(optionIndex);
}, 500);
}
// 选择一个选项
function selectChoice(direction) {
if (!gameState.currentEvent || !gameState.currentCharacter) {
console.error('当前事件或角色数据丢失');
resetCardPosition();
return;
}
// 应用效果
const effects = direction === 'left' ?
gameState.currentEvent.optionA.effects :
gameState.currentEvent.optionB.effects;
applyEffects(effects);
// 获取角色信息元素
const characterInfo = document.querySelector('.character-info');
// 向左或向右飞出
if (direction === 'left') {
activeCard.style.transform = 'translateX(-1000px) rotate(-30deg)';
} else {
activeCard.style.transform = 'translateX(1000px) rotate(30deg)';
}
// 淡出并移除
activeCard.style.opacity = '0';
// 同时淡出角色信息和对话气泡
if (characterInfo) {
characterInfo.style.opacity = '0';
}
hideSpeechBubble();
// 移除效果指示器
removeAllEffectIndicators();
// 重置拖动状态
gameState.isDragging = false;
// 延迟后显示下一张卡片
setTimeout(() => {
if (activeCard && activeCard.parentNode) {
activeCard.remove();
activeCard = null;
}
if (characterInfo && characterInfo.parentNode) {
characterInfo.remove();
}
// 重置当前事件和角色
gameState.currentEvent = null;
gameState.currentCharacter = null;
// 检查游戏是否结束
if (!gameState.gameOver) {
// 应用状态效果
applyStatusEffects();
// 更新状态持续时间
updateStatuses();
// 检查是否应该触发事件
if (gameState.devMode && gameState.nextEventId) {
startEvent(gameState.nextEventId);
gameState.nextEventId = null;
} else {
const eventToTrigger = checkEventTriggers();
if (eventToTrigger) {
startEvent(eventToTrigger);
} else {
// 翻转牌堆顶部的新牌
flipTopCard();
}
}
}
}, 500);
}
// 应用效果
function applyEffects(effects) {
// 更新统计数据
for (const [stat, value] of Object.entries(effects)) {
gameState.stats[stat] = Math.max(0, Math.min(100, gameState.stats[stat] + value));
}
// 增加年份和统治时间
gameState.year++;
gameState.reignTime++;
// 更新UI
updateUI();
// 检查游戏结束条件
checkGameOver();
// 每10年保存一次游戏状态
if (gameState.reignTime % 10 === 0) {
saveGameState();
}
}
// 应用状态效果
function applyStatusEffects() {
if (gameState.activeStatuses.length === 0) return;
// 记录效果总和
const totalEffects = {
loyalty: 0,
chaos: 0,
population: 0,
military: 0,
resources: 0
};
// 收集所有状态的每回合效果
gameState.activeStatuses.forEach(status => {
if (status.effects_per_turn) {
for (const [stat, value] of Object.entries(status.effects_per_turn)) {
totalEffects[stat] += value;
}
}
});
// 应用总效果
for (const [stat, value] of Object.entries(totalEffects)) {
if (value !== 0) {
gameState.stats[stat] = Math.max(0, Math.min(100, gameState.stats[stat] + value));
}
}
// 更新UI
updateUI();
// 检查游戏结束条件
checkGameOver();
}
// 更新状态持续时间
function updateStatuses() {
// 更新状态持续时间并检查是否结束
gameState.activeStatuses = gameState.activeStatuses.filter(status => {
// 减少持续时间
if (status.remainingDuration > 0) {
status.remainingDuration--;
}
// 检查是否应该移除状态
if (status.remainingDuration === 0 && status.duration > 0) {
// 状态持续时间结束
return false;
}
// 特定结束条件检查
if (status.one_time_save && status.save_trigger) {
// 检查是否已经触发了一次性救援
if (status.triggered) {
return false;
}
// 检查触发条件
if (status.save_trigger === 'military_zero' && gameState.stats.military <= 10) {
// 军事力量接近0触发救援
status.triggered = true;
// 应用救援效果
gameState.stats.military += 30;
// 显示救援消息
showSpeechBubble("阿斯塔特战士如约而至,解除了您的危机!");
// 状态仍然保留,直到下一回合被移除
return true;
}
}
return true;
});
// 更新状态显示
updateStatusDisplay();
}
// 添加状态
function addStatus(statusId) {
if (!gameData || !gameData.statuses) return;
// 查找状态数据
const statusData = gameData.statuses.find(s => s.id === statusId);
if (!statusData) return;
// 检查状态是否已经存在
const existingIndex = gameState.activeStatuses.findIndex(s => s.id === statusId);
if (existingIndex >= 0) {
// 已存在,重置持续时间
if (statusData.duration > 0) {
gameState.activeStatuses[existingIndex].remainingDuration = statusData.duration;
}
return;
}
// 创建新状态对象
const newStatus = {
id: statusData.id,
name: statusData.name,
icon: statusData.icon,
description: statusData.description,
effects_per_turn: statusData.effects_per_turn,
one_time_save: statusData.one_time_save,
save_trigger: statusData.save_trigger,
startYear: gameState.year,
triggered: false
};
// 设置持续时间
if (statusData.duration > 0) {
newStatus.remainingDuration = statusData.duration;
newStatus.duration = statusData.duration;
}
// 添加到活跃状态列表
gameState.activeStatuses.push(newStatus);
// 更新状态显示
updateStatusDisplay();
}
// 移除状态
function removeStatus(statusId) {
// 从活跃状态列表中移除指定ID的状态
gameState.activeStatuses = gameState.activeStatuses.filter(s => s.id !== statusId);
// 更新状态显示
updateStatusDisplay();
}
// 更新状态显示
function updateStatusDisplay() {
// 清空状态图标容器
activeStatusesContainer.innerHTML = '';
// 添加所有活跃状态的图标
gameState.activeStatuses.forEach(status => {
const statusItem = document.createElement('div');
statusItem.className = 'status-item';
statusItem.textContent = status.icon;
statusItem.dataset.statusId = status.id;
statusItem.dataset.tooltip = status.name;
// 如果是一次性救援状态且满足触发条件,添加警告效果
if (status.one_time_save && status.save_trigger === 'military_zero' && gameState.stats.military <= 15) {
statusItem.classList.add('warning');
}
// 点击显示详情
statusItem.addEventListener('click', () => {
showStatusDetails(status);
});
// 添加到容器
activeStatusesContainer.appendChild(statusItem);
});
}
// 显示状态详情
function showStatusDetails(status) {
// 设置详情对话框内容
document.getElementById('status-details-icon').textContent = status.icon;
document.getElementById('status-details-name').textContent = status.name;
document.getElementById('status-details-description').textContent = status.detailed_description || status.description;
// 显示效果
const effectsContainer = document.getElementById('status-details-effects');
effectsContainer.innerHTML = '';
if (status.effects_per_turn) {
const effectsTitle = document.createElement('div');
effectsTitle.textContent = '每回合效果:';
effectsTitle.style.marginBottom = '8px';
effectsContainer.appendChild(effectsTitle);
for (const [stat, value] of Object.entries(status.effects_per_turn)) {
if (value === 0) continue;
const effectItem = document.createElement('div');
effectItem.className = 'status-effect-item';
let statName = '';
let statIcon = '';
switch (stat) {
case 'loyalty':
statName = '帝国忠诚度';
statIcon = '🦅';
break;
case 'chaos':
statName = '混沌侵染';
statIcon = '🌀';
break;
case 'population':
statName = '民众控制';
statIcon = '👥';
break;
case 'military':
statName = '军事力量';
statIcon = '🔫';
break;
case 'resources':
statName = '资源储备';
statIcon = '⚙️';
break;
}
effectItem.innerHTML = `
<span class="icon">${statIcon}</span>
<span class="${value > 0 ? 'effect-positive' : 'effect-negative'}">${statName} ${value > 0 ? '+' : ''}${value}</span>
`;
effectsContainer.appendChild(effectItem);
}
} else if (status.one_time_save) {
const effectsInfo = document.createElement('div');
effectsInfo.textContent = '当军事力量接近为0时将提供一次性救援。';
effectsContainer.appendChild(effectsInfo);
} else {
const noEffectsInfo = document.createElement('div');
noEffectsInfo.textContent = '此状态没有直接的数值效果。';
effectsContainer.appendChild(noEffectsInfo);
}
// 显示持续时间
const durationElement = document.getElementById('status-details-duration');
if (status.duration) {
durationElement.textContent = `持续时间: ${status.remainingDuration}/${status.duration}`;
} else if (status.one_time_save) {
durationElement.textContent = '持续到触发一次救援';
} else {
durationElement.textContent = '持续时间: 无限期';
}
// 显示对话框
statusDetailsDialog.classList.add('show');
}
// 关闭状态详情对话框
closeStatusDetailsButton.addEventListener('click', () => {
statusDetailsDialog.classList.remove('show');
});
// 开发者工具按钮点击
devToolsButton.addEventListener('click', () => {
devToolsDialog.classList.add('show');
});
// 关闭开发者工具对话框
closeDevToolsButton.addEventListener('click', () => {
devToolsDialog.classList.remove('show');
});
// 触发事件按钮点击
triggerEventButton.addEventListener('click', () => {
const selectedEventId = eventSelector.value;
if (!selectedEventId) return;
// 设置下一个要触发的事件
gameState.nextEventId = selectedEventId;
// 关闭对话框
devToolsDialog.classList.remove('show');
// 如果当前有活动卡片,则移除它
if (activeCard) {
// 获取角色信息元素
const characterInfo = document.querySelector('.character-info');
// 淡出并移除当前卡片
activeCard.style.transition = 'all 0.3s ease';
activeCard.style.opacity = '0';
activeCard.style.transform = 'scale(0.8)';
if (characterInfo) {
characterInfo.style.opacity = '0';
}
hideSpeechBubble();
// 延迟后移除元素并开始新事件
setTimeout(() => {
if (activeCard && activeCard.parentNode) {
activeCard.remove();
activeCard = null;
}
if (characterInfo && characterInfo.parentNode) {
characterInfo.remove();
}
// 立即开始事件
startEvent(selectedEventId);
}, 300);
} else {
// 如果没有活动卡片,寻找一张背面牌进行翻转
const topBackCard = cardContainer.querySelector('.card-back:not(.static-background-card)');
if (topBackCard) {
startEvent(selectedEventId);
}
}
});
// 添加状态按钮点击
addStatusButton.addEventListener('click', () => {
const selectedStatusId = statusSelector.value;
if (!selectedStatusId) return;
// 添加状态
addStatus(selectedStatusId);
// 关闭对话框
devToolsDialog.classList.remove('show');
});
// 移除状态按钮点击
removeStatusButton.addEventListener('click', () => {
const selectedStatusId = statusSelector.value;
if (!selectedStatusId) return;
// 移除状态
removeStatus(selectedStatusId);
// 关闭对话框
devToolsDialog.classList.remove('show');
});
// 设置属性值按钮点击
setStatButton.addEventListener('click', () => {
const selectedStat = statSelector.value;
const value = parseInt(statValueInput.value);
if (!selectedStat || isNaN(value) || value < 0 || value > 100) return;
// 设置属性值
gameState.stats[selectedStat] = value;
// 更新UI
updateUI();
// 检查游戏结束条件
checkGameOver();
// 关闭对话框
devToolsDialog.classList.remove('show');
});
// 更新UI
function updateUI() {
// 更新年份和统治时间
reignYearsElement.textContent = gameState.reignTime;
currentYearElement.textContent = gameState.year.toLocaleString();
// 更新状态条
for (const [stat, bar] of Object.entries(statusBars)) {
bar.style.width = `${gameState.stats[stat]}%`;
// 根据状态值改变颜色
if (gameState.stats[stat] < 20) {
bar.style.opacity = '0.7'; // 危险状态时降低透明度
} else if (gameState.stats[stat] > 80) {
bar.style.opacity = '0.7'; // 危险状态时降低透明度
} else {
bar.style.opacity = '1';
}
}
// 更新状态图标
updateStatusDisplay();
}
// 检查游戏结束条件
function checkGameOver() {
// 检查各项指标是否达到极值
const endTypes = {
loyalty: {
low: "loyalty_low",
high: "loyalty_high"
},
chaos: {
low: "chaos_low",
high: "chaos_high"
},
population: {
low: "population_low",
high: "population_high"
},
military: {
low: "military_low",
high: "military_high"
},
resources: {
low: "resources_low",
high: "resources_high"
}
};
// 检查是否有阿斯塔特的守护承诺
const hasAstartesProtection = gameState.activeStatuses.some(s => s.id === 'astartes_protection' && !s.triggered);
// 检查每个指标
for (const [stat, types] of Object.entries(endTypes)) {
const value = gameState.stats[stat];
// 特殊处理军事力量属性,如果有阿斯塔特保护且未触发,不因为军事力量低结束游戏
if (stat === 'military' && value <= 10 && hasAstartesProtection) {
// 触发阿斯塔特保护效果,而不是结束游戏
let astartes = gameState.activeStatuses.find(s => s.id === 'astartes_protection');
if (astartes) {
astartes.triggered = true;
gameState.stats.military += 30;
updateUI();
showSpeechBubble("阿斯塔特战士如约而至,解除了您的危机!");
setTimeout(() => {
hideSpeechBubble();
}, 3000);
continue;
}
}
if (value <= 1) {
showDeathScenario(types.low);
return;
} else if (value >= 99) {
showDeathScenario(types.high);
return;
}
}
}
// 新增:显示死亡场景
function showDeathScenario(deathType) {
if (gameState.gameOver) return; // 防止多次触发
// 标记游戏已结束,但不立即显示游戏结束对话框
gameState.gameOver = true;
console.log(`尝试获取死亡场景: /api/death_scenario/${deathType}`);
// 从服务器获取随机死亡场景
fetch(`/api/death_scenario/${deathType}`)
.then(response => {
console.log('API响应状态:', response.status);
if (!response.ok) {
throw new Error(`API请求失败: ${response.status}`);
}
return response.json();
})
.then(scenario => {
console.log('成功获取死亡场景:', scenario);
// 保存死亡原因,后面显示游戏结束对话框时使用
gameState.gameOverReason = scenario.name;
// 显示第一张死亡卡片
showDeathCard(scenario.first_card, () => {
// 第一张卡片结束后,显示第二张
showDeathCard(scenario.second_card, () => {
// 第二张卡片结束后,显示游戏结束对话框
finalizeGameOver();
});
});
})
.catch(error => {
console.error('获取死亡场景失败:', error);
endGameWithMessage('游戏结束'); // 如果获取失败,使用简单的结束信息
});
}
// 显示死亡卡片
function showDeathCard(cardData, callback) {
// 获取角色数据
const character = cardData.character;
// 清理现有卡片和信息
const existingFrontCard = document.getElementById('active-card');
if (existingFrontCard) {
existingFrontCard.remove();
}
const existingCharInfo = document.querySelector('.character-info');
if (existingCharInfo) {
existingCharInfo.remove();
}
// 创建角色信息区域
const characterInfo = document.createElement('div');
characterInfo.className = 'character-info';
characterInfo.innerHTML = `
<div class="character-name">${character.name}</div>
<div class="character-title">${character.title}</div>
`;
characterInfo.style.opacity = '0';
characterInfo.style.zIndex = '20';
// 检查角色是否有图片路径
const hasImage = character.avatar_path && character.avatar_path.length > 0;
const portraitContent = hasImage
? `<img src="/static/${character.avatar_path}" alt="${character.name}" onerror="this.parentNode.innerHTML='<div class=\\'fallback-avatar\\'>${character.avatar}</div>'">`
: `<div class="fallback-avatar">${character.avatar}</div>`;
// 创建死亡卡片
const deathCard = document.createElement('div');
deathCard.className = 'card death-card';
deathCard.id = 'active-card';
deathCard.style.zIndex = '15';
// 存储回调函数,供后续使用
deathCard.dataset.callback = "death_callback_" + Math.random();
window[deathCard.dataset.callback] = callback;
// 设置卡片内容 - 包括选项
deathCard.innerHTML = `
<div class="character-portrait death-portrait">${portraitContent}</div>
<div class="option-overlay right-option-overlay">${cardData.options[0].text}</div>
<div class="option-overlay left-option-overlay">${cardData.options[1] ? cardData.options[1].text : cardData.options[0].text}</div>
`;
// 添加到DOM
cardContainer.appendChild(characterInfo);
cardContainer.appendChild(deathCard);
// 设置卡片初始样式
deathCard.style.transform = 'rotateY(-90deg)';
deathCard.style.opacity = '0';
deathCard.style.transition = 'none';
// 强制重绘
void deathCard.offsetWidth;
// 寻找一张背面牌用于翻转效果
const topBackCard = cardContainer.querySelector('.card-back:not(.static-background-card)');
if (topBackCard) {
// 背面牌开始翻转
topBackCard.style.transition = 'all 0.4s ease';
topBackCard.style.transform = 'rotateY(90deg)';
topBackCard.style.opacity = '0';
topBackCard.style.zIndex = '5';
}
// 等待背面牌翻转到一半后,开始前面牌的翻转
setTimeout(() => {
// 前面卡片开始翻转
deathCard.style.transition = 'all 0.4s ease';
deathCard.style.transform = 'rotateY(0deg)';
deathCard.style.opacity = '1';
// 设置为当前活动卡片
activeCard = deathCard;
// 显示对话气泡
showSpeechBubble(cardData.text);
// 显示角色信息
setTimeout(() => {
characterInfo.style.opacity = '1';
// 如果第二张卡片没有选项文本,自动划过
if (cardData.options[0].text === '' && (cardData.options.length < 2 || cardData.options[1].text === '')) {
// 延迟自动划过,给玩家时间阅读卡片
setTimeout(() => {
swipeDeathCard(deathCard, characterInfo, callback);
}, 3000);
} else {
// 添加卡片点击或拖动事件
setupDeathCardInteraction(deathCard, characterInfo, callback);
}
// 移除原来的背面牌
if (topBackCard && topBackCard.parentNode) {
topBackCard.remove();
}
}, 400);
}, 200);
}
// 设置死亡卡片交互
function setupDeathCardInteraction(card, charInfo, callback) {
// 点击卡片触发划过
card.addEventListener('click', () => {
swipeDeathCard(card, charInfo, callback);
});
// 也可以使用拖动
setupCardDrag(card);
}
// 划过死亡卡片
function swipeDeathCard(card, charInfo, callback) {
// 向右划出
card.style.transition = 'all 0.5s ease';
card.style.transform = 'translateX(1000px) rotate(30deg)';
card.style.opacity = '0';
// 隐藏角色信息和对话气泡
if (charInfo) {
charInfo.style.opacity = '0';
}
hideSpeechBubble();
// 等待动画完成后执行回调
setTimeout(() => {
if (card && card.parentNode) {
card.remove();
}
if (charInfo && charInfo.parentNode) {
charInfo.remove();
}
// 执行回调
if (callback && typeof callback === 'function') {
callback();
} else if (card.dataset.callback && window[card.dataset.callback]) {
window[card.dataset.callback]();
// 清理回调引用
delete window[card.dataset.callback];
}
}, 500);
}
// 使用特定消息结束游戏
function endGameWithMessage(message) {
gameState.gameOver = true;
gameState.gameOverReason = message;
finalizeGameOver();
}
// 完成游戏结束流程
function finalizeGameOver() {
// 更新高分
updateHighScore();
// 显示游戏结束对话框
gameOverReason.textContent = gameState.gameOverReason;
finalYears.textContent = gameState.reignTime;
gameOverDialog.classList.add('show');
}
// 更新高分
function updateHighScore() {
fetch('/api/update_score', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
score: gameState.reignTime,
year: gameState.year // 发送当前年份以便服务器保存
})
})
.catch(error => console.error('更新分数失败:', error));
}
// 保存游戏状态
function saveGameState() {
// 不保存已结束的游戏
if (gameState.gameOver) return;
// 创建要保存的游戏状态副本
const gameStateCopy = {
governor: gameState.governor,
reignTime: gameState.reignTime,
year: gameState.year, // 保存当前年份
stats: { ...gameState.stats },
statusEffects: [...gameState.statusEffects],
activeStatuses: [...gameState.activeStatuses],
completedEvents: [...gameState.completedEvents],
// 不保存临时状态
gameOver: false,
gameOverReason: ''
// 不要在这里设置achievements字段避免覆盖服务器上已有的成就
};
console.log("保存游戏状态...");
fetch('/api/save_game', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(gameStateCopy)
})
.then(response => {
if (!response.ok) {
throw new Error('保存游戏状态失败: ' + response.status);
}
return response.json();
})
.then(data => {
console.log("游戏状态保存成功");
})
.catch(error => {
console.error('保存游戏状态失败:', error);
});
}
// 重启游戏按钮事件
restartButton.addEventListener('click', () => {
// 隐藏游戏结束对话框
gameOverDialog.classList.remove('show');
// 重新初始化游戏
setTimeout(initGame, 500);
});
// 在游戏加载时初始化
document.addEventListener('DOMContentLoaded', () => {
// 公告横幅关闭按钮
const announcementBanner = document.querySelector('.announcement-banner');
const closeAnnouncementBtn = document.querySelector('.announcement-close');
if (closeAnnouncementBtn && announcementBanner) {
closeAnnouncementBtn.addEventListener('click', () => {
announcementBanner.style.display = 'none';
});
}
// 原有的初始化代码
loadGameData();
});
// 当窗口关闭或刷新时保存游戏状态
window.addEventListener('beforeunload', () => {
if (!gameState.gameOver) {
saveGameState();
}
});