295 lines
9.6 KiB
JavaScript
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);
|
|
})();
|