yukinovel
Version:
Yukinovel is a simple web visual novel engine.
182 lines (181 loc) • 6.7 kB
JavaScript
export class DialogueRenderer {
constructor(game, dialogueContainer, choicesContainer) {
// Typewriter effect properties
this.isTyping = false;
this.currentTypewriterTimeout = null;
this.typewriterSpeed = 20;
this.currentDialogueText = '';
this.currentCharacterName = '';
this.currentCharacterColor = '#fff';
this.justSkippedTyping = false;
this.htmlNodes = [];
this.currentNodeIndex = 0;
this.currentTextIndex = 0;
this.game = game;
this.dialogueContainer = dialogueContainer;
this.choicesContainer = choicesContainer;
}
updateDialogue(dialogue) {
const characterName = dialogue.character;
const character = characterName ? this.game.getScript().characters[characterName] : null;
this.stopTyping();
const localizedText = this.game.getLanguageManager().getLocalizedText(dialogue.text);
this.currentDialogueText = localizedText;
this.currentCharacterName = character ? character.name : '';
this.currentCharacterColor = character ? character.color || '#fff' : '#fff';
this.parseHtmlContent(localizedText);
this.startTypewriter();
this.choicesContainer.style.display = 'none';
}
showChoices(choices) {
const langManager = this.game.getLanguageManager();
this.dialogueContainer.style.display = 'none';
this.choicesContainer.style.display = 'block';
let html = '';
choices.forEach((choice, index) => {
const choiceText = langManager.getLocalizedText(choice.text);
html += `
<button
class="vn-choice-button"
data-index="${index}"
>
${choiceText}
</button>
`;
});
this.choicesContainer.innerHTML = html;
this.choicesContainer.querySelectorAll('.vn-choice-button').forEach((button, index) => {
button.addEventListener('click', () => {
this.game.makeChoice(choices[index]);
});
});
}
handleNext() {
if (this.isTyping) {
this.skipTyping();
}
else if (!this.justSkippedTyping) {
this.game.next();
}
}
setTypewriterSpeed(speed) {
this.typewriterSpeed = speed;
}
getTypewriterSpeed() {
return this.typewriterSpeed;
}
parseHtmlContent(htmlText) {
this.htmlNodes = [];
this.currentNodeIndex = 0;
this.currentTextIndex = 0;
const htmlRegex = /<[^>]+>/g;
let lastIndex = 0;
let match;
while ((match = htmlRegex.exec(htmlText)) !== null) {
if (match.index > lastIndex) {
const textContent = htmlText.substring(lastIndex, match.index);
if (textContent) {
this.htmlNodes.push({ type: 'text', content: textContent });
}
}
this.htmlNodes.push({ type: 'html', content: match[0] });
lastIndex = match.index + match[0].length;
}
if (lastIndex < htmlText.length) {
const textContent = htmlText.substring(lastIndex);
if (textContent) {
this.htmlNodes.push({ type: 'text', content: textContent });
}
}
if (this.htmlNodes.length === 0) {
this.htmlNodes.push({ type: 'text', content: htmlText });
}
}
// Typewriter effect methods
startTypewriter() {
this.isTyping = true;
this.justSkippedTyping = false;
this.dialogueContainer.style.display = 'block';
this.currentNodeIndex = 0;
this.currentTextIndex = 0;
this.typeHtmlText();
}
typeHtmlText() {
if (this.currentNodeIndex >= this.htmlNodes.length) {
this.isTyping = false;
return;
}
const currentNode = this.htmlNodes[this.currentNodeIndex];
if (currentNode.type === 'html') {
this.currentNodeIndex++;
this.currentTextIndex = 0;
this.updateDialogueDisplay();
this.currentTypewriterTimeout = window.setTimeout(() => {
this.typeHtmlText();
}, 1);
}
else {
if (this.currentTextIndex >= currentNode.content.length) {
this.currentNodeIndex++;
this.currentTextIndex = 0;
this.currentTypewriterTimeout = window.setTimeout(() => {
this.typeHtmlText();
}, this.typewriterSpeed);
}
else {
this.currentTextIndex++;
this.updateDialogueDisplay();
this.currentTypewriterTimeout = window.setTimeout(() => {
this.typeHtmlText();
}, this.typewriterSpeed);
}
}
}
updateDialogueDisplay() {
let displayText = '';
for (let i = 0; i <= this.currentNodeIndex; i++) {
const node = this.htmlNodes[i];
if (!node)
break;
if (i < this.currentNodeIndex) {
displayText += node.content;
}
else if (i === this.currentNodeIndex) {
if (node.type === 'html') {
displayText += node.content;
}
else {
displayText += node.content.substring(0, this.currentTextIndex);
}
}
}
let html = '';
if (this.currentCharacterName) {
html += `<div class="vn-dialogue-character-name" style="color: ${this.currentCharacterColor};">${this.currentCharacterName}</div>`;
}
html += `<div class="vn-dialogue-text">${displayText}</div>`;
this.dialogueContainer.innerHTML = html;
}
stopTyping() {
if (this.currentTypewriterTimeout) {
clearTimeout(this.currentTypewriterTimeout);
this.currentTypewriterTimeout = null;
}
}
skipTyping() {
if (this.isTyping) {
this.stopTyping();
this.isTyping = false;
this.justSkippedTyping = true;
let html = '';
if (this.currentCharacterName) {
html += `<div class="vn-dialogue-character-name" style="color: ${this.currentCharacterColor};">${this.currentCharacterName}</div>`;
}
html += `<div class="vn-dialogue-text">${this.currentDialogueText}</div>`;
this.dialogueContainer.innerHTML = html;
setTimeout(() => {
this.justSkippedTyping = false;
}, 100);
}
}
}