new RPG system
This commit is contained in:
+111
-16
@@ -154,12 +154,63 @@ export function ensureMessageActionsLast(wrapper) {
|
||||
if (actions) wrapper.appendChild(actions);
|
||||
}
|
||||
|
||||
function renderNewArcRollButton(wrapper, choice) {
|
||||
removeChoiceRows(wrapper);
|
||||
const row = document.createElement('div');
|
||||
row.className = 'choice-row choice-row-new-arc';
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'new-arc-roll';
|
||||
|
||||
const hdr = document.createElement('div');
|
||||
hdr.className = 'new-arc-roll-header';
|
||||
hdr.textContent = choice?.label || 'Начать новую арку';
|
||||
panel.appendChild(hdr);
|
||||
|
||||
const hint = document.createElement('div');
|
||||
hint.className = 'new-arc-roll-hint';
|
||||
hint.textContent = 'Арка завершена. Можно продолжить эпилог или начать новую — кто ходит первым после инъекта рассказчика?';
|
||||
panel.appendChild(hint);
|
||||
|
||||
const split = document.createElement('div');
|
||||
split.className = 'new-arc-roll-split';
|
||||
|
||||
const mkHalf = (side, icon, title, subtitle, first) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = `new-arc-half new-arc-half-${side}`;
|
||||
btn.innerHTML = `<span class="new-arc-half-icon">${icon}</span><span class="new-arc-half-title">${title}</span><span class="new-arc-half-sub">${subtitle}</span>`;
|
||||
btn.addEventListener('click', () => {
|
||||
removeChoiceRows(wrapper);
|
||||
sendMessage(choice?.label || 'Начать новую арку', {
|
||||
isNarratorChoice: true,
|
||||
newArcFirst: first,
|
||||
});
|
||||
});
|
||||
return btn;
|
||||
};
|
||||
|
||||
split.appendChild(mkHalf('user', '👤', 'Игрок', 'первый ход после инъекта', 'user'));
|
||||
split.appendChild(mkHalf('character', '🤖', 'Персонаж', 'открывает новую арку', 'character'));
|
||||
panel.appendChild(split);
|
||||
row.appendChild(panel);
|
||||
wrapper.appendChild(row);
|
||||
ensureMessageActionsLast(wrapper);
|
||||
}
|
||||
|
||||
export function renderChoices(wrapper, choices) {
|
||||
if (!choices?.length || !wrapper) return;
|
||||
removeChoiceRows(wrapper);
|
||||
|
||||
const plotChoices = choices.filter(c => c?.source === 'plot_beat');
|
||||
const otherChoices = choices.filter(c => c?.source !== 'plot_beat');
|
||||
const newArc = choices.find(c => c?.type === 'new_arc_roll');
|
||||
if (newArc) {
|
||||
renderNewArcRollButton(wrapper, newArc);
|
||||
return;
|
||||
}
|
||||
|
||||
const isPlot = c => c?.source === 'plot_beat' || c?.source === 'plot_step';
|
||||
const plotChoices = choices.filter(isPlot);
|
||||
const otherChoices = choices.filter(c => !isPlot(c));
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'choice-row';
|
||||
@@ -167,7 +218,7 @@ export function renderChoices(wrapper, choices) {
|
||||
const appendBtn = (container, c) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = c.source === 'plot_beat' ? 'choice-btn choice-btn-plot' : 'choice-btn';
|
||||
btn.className = isPlot(c) ? 'choice-btn choice-btn-plot' : 'choice-btn';
|
||||
const label = c.label || '';
|
||||
btn.textContent = label;
|
||||
if (c.beat_title) btn.title = c.beat_title;
|
||||
@@ -251,10 +302,11 @@ function showNarratorActivityHint(wrapper, meta) {
|
||||
if (meta.choices_count > 0) parts.push(`🔘 ${meta.choices_count} выборов`);
|
||||
if (meta.status_quo) parts.push('🌍 status_quo');
|
||||
if (meta.beats_replenished) parts.push(`📜 +${meta.beats_replenished} beats`);
|
||||
if (meta.beat_mode === 'after_dice') parts.push('📜 beat (d20)');
|
||||
if (meta.beat_mode === 'llm') parts.push('📜 beat (AI)');
|
||||
if (meta.beat_mode === 'stuck_recovery') parts.push('📜 beat (recovery)');
|
||||
if (meta.beat_mode === 'trigger') parts.push('📜 beat (keywords)');
|
||||
if (meta.step_advanced) parts.push('📜 шаг +1');
|
||||
if (meta.arc_completed) parts.push('🏁 арка завершена');
|
||||
if (meta.new_arc_rolled) parts.push('📖 новая арка');
|
||||
if (meta.story_step) parts.push(`📜 ${meta.story_step}`);
|
||||
if (meta.rp_language) parts.push(`🌐 ${meta.rp_language}`);
|
||||
if (meta.arc_pruned) parts.push(`🧹 −${meta.arc_pruned} beat`);
|
||||
if (meta.facts_added) parts.push(`📌 +${meta.facts_added} фактов`);
|
||||
}
|
||||
@@ -299,13 +351,27 @@ function syncQuestActionButtons() {
|
||||
if (failBtn) failBtn.disabled = !active;
|
||||
}
|
||||
|
||||
export function updateQuestPanel(quests) {
|
||||
export function updateQuestPanel(quests, storyArc = null) {
|
||||
const list = document.getElementById('questList');
|
||||
const actions = document.getElementById('questPanelActions');
|
||||
const header = document.getElementById('questPanelHeader');
|
||||
if (!list) return;
|
||||
_questsCache = quests || [];
|
||||
list.innerHTML = '';
|
||||
|
||||
if (header && storyArc) {
|
||||
const steps = storyArc.steps || [];
|
||||
const idx = Number(storyArc.current_step_index ?? 0);
|
||||
const cur = steps.length ? Math.min(idx + 1, steps.length) : 0;
|
||||
const title = storyArc.title || 'Арка';
|
||||
const status = storyArc.status === 'completed' ? ' · завершена' : '';
|
||||
header.textContent = `Арка: ${title} · шаг ${cur}/${steps.length || '?'}${status}`;
|
||||
header.classList.remove('hidden');
|
||||
} else if (header) {
|
||||
header.textContent = '';
|
||||
header.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (!_questsCache.length) {
|
||||
_selectedQuestId = null;
|
||||
syncQuestActionButtons();
|
||||
@@ -577,7 +643,10 @@ export async function reloadChatFromServer(id) {
|
||||
try {
|
||||
const data = typeof m.content === 'string' ? JSON.parse(m.content) : m.content;
|
||||
if (data?.text) renderNarratorMessage(data);
|
||||
} catch { /* ignore bad narrator payload */ }
|
||||
} catch {
|
||||
const plain = (m.content || '').trim();
|
||||
if (plain) renderNarratorMessage({ text: plain });
|
||||
}
|
||||
return;
|
||||
}
|
||||
addMessage(
|
||||
@@ -694,8 +763,11 @@ async function consumeStream(res) {
|
||||
data.assistant_message_id,
|
||||
data.choices,
|
||||
);
|
||||
} else if (data.choices?.length && bubble) {
|
||||
renderChoices(bubble.parentElement, data.choices);
|
||||
} else if (data.choices?.length) {
|
||||
const choiceHost = bubble?.parentElement
|
||||
|| [...dom.messagesEl.querySelectorAll('.message.narrator')].pop()
|
||||
|| [...dom.messagesEl.querySelectorAll('.message.assistant')].pop();
|
||||
if (choiceHost) renderChoices(choiceHost, data.choices);
|
||||
}
|
||||
if (data.debug) renderDebugBlocks(bubble?.parentElement || dom.messagesEl, data.debug);
|
||||
if (data.narrator_meta && bubble?.parentElement) {
|
||||
@@ -703,7 +775,9 @@ async function consumeStream(res) {
|
||||
}
|
||||
if (data.affinity !== undefined) updateAffinityDisplay(data.affinity);
|
||||
if (data.narrative_stats) updateStatsDisplay(data.narrative_stats);
|
||||
if (data.quests?.length) updateQuestPanel(data.quests);
|
||||
if (data.quests !== undefined) {
|
||||
updateQuestPanel(data.quests, data.story_arc ?? null);
|
||||
}
|
||||
|
||||
_pendingUserBubble = null;
|
||||
|
||||
@@ -822,21 +896,42 @@ export function clearMessages() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendMessage(text, isNarratorChoice = false) {
|
||||
export async function sendMessage(text, choiceOpts = false) {
|
||||
if (typeof text !== 'string') text = dom.inputEl.value.trim();
|
||||
if (!text || !sessionId) return;
|
||||
|
||||
let isNarratorChoice = false;
|
||||
let newArcFirst = null;
|
||||
if (typeof choiceOpts === 'boolean') {
|
||||
isNarratorChoice = choiceOpts;
|
||||
} else if (choiceOpts && typeof choiceOpts === 'object') {
|
||||
isNarratorChoice = !!choiceOpts.isNarratorChoice;
|
||||
newArcFirst = choiceOpts.newArcFirst || null;
|
||||
}
|
||||
|
||||
dom.inputEl.value = '';
|
||||
dom.inputEl.style.height = 'auto';
|
||||
dom.sendBtn.disabled = true;
|
||||
const userContent = isNarratorChoice ? `[Player chose: ${text}]` : text;
|
||||
dom.messagesEl.querySelectorAll('.message.assistant').forEach(w => removeChoiceRows(w));
|
||||
let userContent = text;
|
||||
if (isNarratorChoice && newArcFirst) {
|
||||
const who = newArcFirst === 'user' ? 'игрок' : 'персонаж';
|
||||
userContent = `[Player chose: Начать новую арку — первый ход: ${who}]`;
|
||||
} else if (isNarratorChoice) {
|
||||
userContent = `[Player chose: ${text}]`;
|
||||
}
|
||||
dom.messagesEl.querySelectorAll('.message.assistant, .message.narrator').forEach(w => removeChoiceRows(w));
|
||||
_pendingUserBubble = addMessage('user', userContent);
|
||||
showTyping();
|
||||
try {
|
||||
const res = await fetch('/chat/stream', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: text, session_id: sessionId, is_narrator_choice: isNarratorChoice }),
|
||||
body: JSON.stringify({
|
||||
message: text,
|
||||
session_id: sessionId,
|
||||
is_narrator_choice: isNarratorChoice,
|
||||
new_arc_first: newArcFirst,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error('Ошибка сервера: ' + res.status);
|
||||
removeTyping();
|
||||
|
||||
Reference in New Issue
Block a user