Checkpoint Eibenreith ink architecture
This commit is contained in:
@@ -15,6 +15,9 @@ class ChoiceDisplayModule extends BaseModule {
|
||||
this.choices = [];
|
||||
this.inputMode = 'none';
|
||||
this.processState = document.documentElement.dataset.processState || 'loading';
|
||||
this.currentTurnId = 0;
|
||||
this.autoTurnCounter = 0;
|
||||
this.lastAutoTurn = new Map();
|
||||
this.template = {
|
||||
cells: {
|
||||
default: {
|
||||
@@ -39,8 +42,15 @@ class ChoiceDisplayModule extends BaseModule {
|
||||
'shuffleChoices',
|
||||
'randomInt',
|
||||
'assignLetters',
|
||||
'selectAutoChoice',
|
||||
'isAutoChoiceReady',
|
||||
'getAutoChoiceConfig',
|
||||
'getAutoChoiceKey',
|
||||
'getAutoChoiceDelay',
|
||||
'markAutoChoiceTriggered',
|
||||
'selectChoice',
|
||||
'getTagValue',
|
||||
'getTag',
|
||||
'getTemplateCell',
|
||||
'renderChoiceText'
|
||||
]);
|
||||
@@ -60,10 +70,23 @@ class ChoiceDisplayModule extends BaseModule {
|
||||
this.addEventListener(document, 'story:process-state', (event) => {
|
||||
this.handleProcessState(event.detail?.state || 'ready');
|
||||
});
|
||||
this.addEventListener(document, 'story:turn-start', () => {
|
||||
this.addEventListener(document, 'story:turn-start', (event) => {
|
||||
const turnId = Number(event.detail?.turnId);
|
||||
if (turnId === 1) {
|
||||
this.currentTurnId = 0;
|
||||
this.autoTurnCounter = 0;
|
||||
this.lastAutoTurn.clear();
|
||||
}
|
||||
this.processState = 'waiting-generating';
|
||||
this.render();
|
||||
});
|
||||
this.addEventListener(document, 'story:turn-complete', (event) => {
|
||||
const turnId = Number(event.detail?.turnId);
|
||||
if (Number.isInteger(turnId) && turnId !== this.currentTurnId) {
|
||||
this.currentTurnId = turnId;
|
||||
this.autoTurnCounter += 1;
|
||||
}
|
||||
});
|
||||
this.addEventListener(document, 'story:history-restoring', (event) => {
|
||||
document.documentElement.dataset.historyRestoring = event.detail?.active ? 'true' : 'false';
|
||||
this.render();
|
||||
@@ -130,7 +153,7 @@ class ChoiceDisplayModule extends BaseModule {
|
||||
}
|
||||
|
||||
const letter = event.key.toUpperCase();
|
||||
const choice = this.choices.find((item) => item.letter === letter);
|
||||
const choice = this.choices.find((item) => !item.auto && item.letter === letter);
|
||||
if (!choice) {
|
||||
return;
|
||||
}
|
||||
@@ -151,11 +174,20 @@ class ChoiceDisplayModule extends BaseModule {
|
||||
sourceOrder: order,
|
||||
optional: this.hasTag(tags, 'optional'),
|
||||
letter: '',
|
||||
templateCell: this.getTemplateCell({ ...choice, tags, category })
|
||||
templateCell: this.getTemplateCell({ ...choice, tags, category }),
|
||||
auto: this.hasTag(tags, 'auto')
|
||||
};
|
||||
});
|
||||
|
||||
return this.assignLetters(this.orderChoicesForPresentation(normalized));
|
||||
const autoChoices = normalized
|
||||
.filter((choice) => choice.auto)
|
||||
.sort((a, b) => a.sourceOrder - b.sourceOrder);
|
||||
const visibleChoices = normalized.filter((choice) => !choice.auto);
|
||||
|
||||
return [
|
||||
...autoChoices,
|
||||
...this.assignLetters(this.orderChoicesForPresentation(visibleChoices))
|
||||
];
|
||||
}
|
||||
|
||||
orderChoicesForPresentation(choices) {
|
||||
@@ -254,11 +286,17 @@ class ChoiceDisplayModule extends BaseModule {
|
||||
}
|
||||
|
||||
getTagValue(tags, key) {
|
||||
const normalizedKey = String(key).toLowerCase();
|
||||
const tag = tags.find((item) => String(item?.key || '').toLowerCase() === normalizedKey);
|
||||
const tag = this.getTag(tags, key);
|
||||
return tag?.value;
|
||||
}
|
||||
|
||||
getTag(tags, key) {
|
||||
const normalizedKey = String(key).toLowerCase();
|
||||
return Array.isArray(tags)
|
||||
? tags.find((item) => String(item?.key || '').toLowerCase() === normalizedKey)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
hasTag(tags, key) {
|
||||
const normalizedKey = String(key).toLowerCase();
|
||||
return Array.isArray(tags) && tags.some((item) => String(item?.key || '').toLowerCase() === normalizedKey);
|
||||
@@ -276,8 +314,15 @@ class ChoiceDisplayModule extends BaseModule {
|
||||
this.inputMode === 'choice' &&
|
||||
this.choices.length > 0 &&
|
||||
this.processState === 'ready';
|
||||
this.container.hidden = !readyForChoices;
|
||||
this.container.dataset.choiceReady = readyForChoices ? 'true' : 'false';
|
||||
|
||||
if (readyForChoices && this.selectAutoChoice()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleChoices = this.choices.filter((choice) => !choice.auto);
|
||||
const hasVisibleChoices = visibleChoices.length > 0;
|
||||
this.container.hidden = !readyForChoices || !hasVisibleChoices;
|
||||
this.container.dataset.choiceReady = readyForChoices && hasVisibleChoices ? 'true' : 'false';
|
||||
if (this.container.hidden) {
|
||||
return;
|
||||
}
|
||||
@@ -285,7 +330,7 @@ class ChoiceDisplayModule extends BaseModule {
|
||||
const list = document.createElement('ol');
|
||||
list.className = 'choice-list choice-template-default';
|
||||
|
||||
this.choices.forEach((choice) => {
|
||||
visibleChoices.forEach((choice) => {
|
||||
const item = document.createElement('li');
|
||||
item.className = 'choice-list-item';
|
||||
item.classList.toggle('choice-optional', Boolean(choice.optional));
|
||||
@@ -307,6 +352,75 @@ class ChoiceDisplayModule extends BaseModule {
|
||||
this.container.appendChild(list);
|
||||
}
|
||||
|
||||
selectAutoChoice() {
|
||||
const autoChoice = this.choices.find((choice) => choice.auto && this.isAutoChoiceReady(choice));
|
||||
if (!autoChoice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.markAutoChoiceTriggered(autoChoice);
|
||||
this.selectChoice(autoChoice.index);
|
||||
return true;
|
||||
}
|
||||
|
||||
isAutoChoiceReady(choice) {
|
||||
const config = this.getAutoChoiceConfig(choice);
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
if (config.delay <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lastTurn = this.lastAutoTurn.get(config.key);
|
||||
if (!Number.isFinite(lastTurn)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (this.autoTurnCounter - lastTurn) >= config.delay;
|
||||
}
|
||||
|
||||
getAutoChoiceConfig(choice) {
|
||||
const autoTag = this.getTag(choice.tags, 'auto');
|
||||
if (!autoTag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key: this.getAutoChoiceKey(autoTag),
|
||||
delay: this.getAutoChoiceDelay(autoTag)
|
||||
};
|
||||
}
|
||||
|
||||
getAutoChoiceKey(autoTag) {
|
||||
const value = String(autoTag?.value || '').trim();
|
||||
return value || '__global';
|
||||
}
|
||||
|
||||
getAutoChoiceDelay(autoTag) {
|
||||
const candidates = [autoTag?.param, autoTag?.value];
|
||||
for (const candidate of candidates) {
|
||||
const text = String(candidate || '').trim();
|
||||
if (!text) continue;
|
||||
|
||||
const namedMatch = text.match(/(?:turns?|delay|cooldown)\s*=\s*(\d+)/i);
|
||||
const plainMatch = text.match(/^\d+$/);
|
||||
const value = namedMatch ? Number(namedMatch[1]) : plainMatch ? Number(text) : NaN;
|
||||
if (Number.isFinite(value)) {
|
||||
return Math.max(0, Math.floor(value));
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
markAutoChoiceTriggered(choice) {
|
||||
const config = this.getAutoChoiceConfig(choice);
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
this.lastAutoTurn.set(config.key, this.autoTurnCounter);
|
||||
}
|
||||
|
||||
async selectChoice(index) {
|
||||
if (!this.socketClient) {
|
||||
this.socketClient = this.getModule('socket-client');
|
||||
|
||||
Reference in New Issue
Block a user