Files

295 lines
9.6 KiB
JavaScript

(function () {
const body = document.body;
const user = (() => {
try {
return JSON.parse(body.dataset.user || 'null');
} catch {
return null;
}
})();
const statusEl = document.getElementById('status');
const cardsGrid = document.getElementById('cards-grid');
const publicRoomsList = document.getElementById('public-rooms-list');
const setStatus = (text) => {
statusEl.textContent = text || '';
};
const safeCoverUrl = (url) => {
const value = String(url || '').trim();
if (!value) return '';
return value.replace(/["\\]/g, '\\$&');
};
const getJson = async (url) => {
const res = await fetch(url);
return res.json();
};
const postJson = async (url, payload) => {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return res.json();
};
const deleteJson = async (url) => {
const res = await fetch(url, { method: 'DELETE' });
return res.json();
};
const joinRoom = async (roomId) => {
const cachedPlayerId = localStorage.getItem(`echo-player-${roomId}`) || '';
const data = await postJson('/api/rooms/join', {
room_id: roomId,
player_id: cachedPlayerId,
});
if (!data.ok) {
setStatus(`加入失败: ${data.error}`);
return;
}
if (data.is_spectator) {
window.location.href = data.redirect;
return;
}
localStorage.setItem(`echo-player-${data.room_id}`, data.player_id);
localStorage.setItem('echo-last-name', data.player_name);
window.location.href = data.redirect;
};
const openStoreForCard = (card) => {
const title = String((card || {}).title || (card || {}).card_id || '').trim();
const query = new URLSearchParams();
query.set('type', 'game_card');
if (title) query.set('q', title);
window.location.href = `/store?${query.toString()}`;
};
const createRoomFromCard = async (card) => {
const cardId = typeof card === 'string' ? card : String((card || {}).card_id || '').trim();
if (!cardId) {
setStatus('游戏卡无效。');
return;
}
if (!user) {
setStatus('请先登录账号后再开局。');
return;
}
if (card && card.requires_purchase && !card.owned) {
setStatus('该游戏卡需要先购买,正在跳转积分商城。');
openStoreForCard(card);
return;
}
setStatus('正在创建房间...');
const data = await postJson('/api/rooms/create', { card_id: cardId });
if (!data.ok) {
if (String(data.error || '').includes('先购买')) {
setStatus(`创建失败: ${data.error} 正在跳转积分商城。`);
if (card && typeof card === 'object') openStoreForCard(card);
} else {
setStatus(`创建失败: ${data.error}`);
}
return;
}
localStorage.setItem(`echo-player-${data.room_id}`, data.player_id);
localStorage.setItem('echo-last-name', data.player_name);
window.location.href = data.redirect;
};
const renderCards = (cards) => {
cardsGrid.textContent = '';
cards.forEach((card) => {
const article = document.createElement('article');
article.className = 'game-card';
const cover = document.createElement('div');
cover.className = 'game-card-cover';
cover.style.backgroundImage = `url("${safeCoverUrl(card.cover_url)}")`;
const panel = document.createElement('div');
panel.className = 'game-card-body';
const h3 = document.createElement('h3');
h3.textContent = card.title || '未命名游戏卡';
panel.appendChild(h3);
const sub = document.createElement('p');
sub.className = 'game-card-sub';
sub.textContent = card.subtitle || '自定义冒险';
panel.appendChild(sub);
const summary = document.createElement('p');
summary.textContent = card.summary || '';
panel.appendChild(summary);
const owner = document.createElement('p');
owner.className = 'game-card-sub';
owner.textContent = card.is_official ? '官方卡' : '玩家创作';
panel.appendChild(owner);
const price = Number(card.shop_price || 0);
if (price > 0) {
const pricing = document.createElement('p');
pricing.className = 'game-card-sub';
pricing.textContent = card.owned
? `售价 ${Math.trunc(price)} 积分 · 已拥有`
: `售价 ${Math.trunc(price)} 积分 · 未拥有`;
panel.appendChild(pricing);
}
const pack = card.content_pack || {};
const tags = Array.isArray(pack.genre_tags) ? pack.genre_tags.slice(0, 5) : [];
const features = Array.isArray(pack.feature_badges) ? pack.feature_badges.slice(0, 5) : [];
if (tags.length) {
const tagRow = document.createElement('div');
tagRow.className = 'tag-row';
tags.forEach((tag) => {
const chip = document.createElement('span');
chip.textContent = tag;
tagRow.appendChild(chip);
});
panel.appendChild(tagRow);
}
if (features.length) {
const featureRow = document.createElement('div');
featureRow.className = 'tag-row feature';
features.forEach((tag) => {
const chip = document.createElement('span');
chip.textContent = tag;
featureRow.appendChild(chip);
});
panel.appendChild(featureRow);
}
const goals = document.createElement('ul');
goals.className = 'compact-list';
(card.starter_goals || []).slice(0, 3).forEach((goal) => {
const li = document.createElement('li');
li.textContent = goal;
goals.appendChild(li);
});
if (goals.childNodes.length) {
panel.appendChild(goals);
}
const actionsRow = document.createElement('div');
actionsRow.className = 'card-actions-row';
const startBtn = document.createElement('button');
startBtn.type = 'button';
startBtn.textContent = card.requires_purchase && !card.owned ? '前往商城购买' : '用此卡开一局';
startBtn.addEventListener('click', () => {
createRoomFromCard(card).catch((error) => setStatus(`网络错误: ${error}`));
});
actionsRow.appendChild(startBtn);
const isOwner = Boolean(user && String(card.creator_user_id || '') === String(user.user_id || ''));
const isAdmin = Boolean(user && String(user.role || '') === 'admin');
const canEdit = Boolean(user && (isOwner || isAdmin));
const canDelete = Boolean(user && !card.is_official && isOwner);
if (canEdit) {
const editBtn = document.createElement('a');
editBtn.className = 'link-btn';
editBtn.href = `/cards/${card.card_id}/edit`;
editBtn.textContent = '编辑游戏卡';
actionsRow.appendChild(editBtn);
}
if (canDelete) {
const delBtn = document.createElement('button');
delBtn.type = 'button';
delBtn.className = 'ghost-btn';
delBtn.textContent = '删除';
delBtn.addEventListener('click', async () => {
if (!confirm(`确认删除游戏卡「${card.title}」吗?`)) return;
setStatus('正在删除游戏卡...');
try {
const data = await deleteJson(`/api/cards/${card.card_id}`);
if (!data.ok) {
setStatus(`删除失败: ${data.error || '未知错误'}`);
return;
}
setStatus('删除成功。');
loadCards();
} catch (error) {
setStatus(`网络错误: ${error}`);
}
});
actionsRow.appendChild(delBtn);
}
panel.appendChild(actionsRow);
article.appendChild(cover);
article.appendChild(panel);
cardsGrid.appendChild(article);
});
};
const renderPublicRooms = (rooms) => {
if (!publicRoomsList) return;
publicRoomsList.textContent = '';
if (!rooms.length) {
const li = document.createElement('li');
li.textContent = '当前暂无公开房间';
publicRoomsList.appendChild(li);
return;
}
rooms.forEach((room) => {
const li = document.createElement('li');
const status = room.status === 'running' ? '进行中' : '准备中';
const title = room.card_title || '未知剧本';
const host = room.host_name || '未知房主';
li.textContent = `${room.room_id} · ${title} · ${status} · 房主:${host} · 在线:${room.online_count}/${room.player_count}`;
const actionBtn = document.createElement('button');
actionBtn.type = 'button';
actionBtn.className = 'secondary-btn';
actionBtn.style.width = 'auto';
actionBtn.style.marginTop = '0.4rem';
actionBtn.textContent = room.status === 'running' ? '观战/重连' : '加入房间';
actionBtn.addEventListener('click', () => {
joinRoom(room.room_id).catch((error) => setStatus(`网络错误: ${error}`));
});
li.appendChild(actionBtn);
publicRoomsList.appendChild(li);
});
};
const loadCards = async () => {
try {
const data = await getJson('/api/cards');
if (!data.ok) {
setStatus(data.error || '加载失败');
return;
}
renderCards(data.cards || []);
} catch (error) {
setStatus(`网络错误: ${error}`);
}
};
const loadPublicRooms = async () => {
try {
const data = await getJson('/api/rooms/public?limit=40');
if (!data.ok) {
setStatus(data.error || '加载联机广场失败');
return;
}
renderPublicRooms(data.rooms || []);
} catch (error) {
setStatus(`网络错误: ${error}`);
}
};
loadCards();
loadPublicRooms();
window.setInterval(loadPublicRooms, 12000);
})();