Last active
July 30, 2020 17:40
-
-
Save Akjosch/4d06a027a9300bfee51344ffa3bf6972 to your computer and use it in GitHub Desktop.
Simple dialog system
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.chat { | |
position: absolute; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
visibility: hidden; | |
} | |
.chat.chat-open { | |
visibility: visible; | |
} | |
.chat-overlay { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
z-index: 10; | |
background-color: #000; | |
opacity: 0.5; | |
} | |
.chat-portrait { | |
position: relative; | |
bottom: 9.5em; | |
z-index: 20; | |
} | |
.chat-portrait img { | |
height: 128px; | |
} | |
.chat-name { | |
position: absolute; | |
bottom: 9.9em; | |
left: 5em; | |
font-weight: bold; | |
text-shadow: 0 0 2px black; | |
z-index: 30; | |
} | |
.chat-text { | |
border: 2px solid white; | |
padding: 0.5em; | |
border-radius: 0.5em; | |
position: absolute; | |
height: 10em; | |
left: 0.1em; | |
right: 0.1em; | |
bottom: 0; | |
background: #555; | |
box-sizing: border-box; | |
z-index: 40; | |
overflow: auto; | |
} | |
.chat-text-next { | |
color: #3a3; | |
animation: blink-animation 0.5s steps(5, start) infinite; | |
} | |
@keyframes blink-animation { | |
to { | |
visibility: hidden; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function parseOpts(opts) { | |
const optsMatch = new RegExp('(?:(?<id>\\w+(?==))|(?<val>(?<==")[^"]*(?="(?:\\s|$))|(?<==)[^\\s"]\\S*(?=(?:\\s|$))))', 'gi'); | |
let opt = null; | |
let result = {}; | |
let lastId = null; | |
while(opt = optsMatch.exec(opts)) { | |
if(typeof opt.groups.id !== 'undefined') { | |
lastId = opt.groups.id; | |
result[lastId] = ''; | |
} else if(typeof opt.groups.val !== 'undefined' && lastId) { | |
result[lastId] = opt.groups.val; | |
lastId = null; | |
} | |
} | |
return result; | |
} | |
function parseDialog(passage) { | |
const pass = Story.get(passage); | |
if(!pass.tags.includes('dialog')) { | |
return []; | |
} | |
const lines = pass.text.split(/\n+/).filter((line) => !!line && !line.startsWith(";")); | |
const tokens = /^(?<command>#-|#(?=(?:[A-Za-z_]\w*|))|\*(?=[A-Za-z_]\w*)|\s*)(?<label>(?<=[#$])[A-Za-z_]\w*|)(?<text>.*)$/; | |
const commands = /\[(?<command>[A-Za-z_]\w*)(?:\s+(?<options>[^\]]*)|)\]/; | |
var blockId = 1; | |
var dialog = {}; | |
var meta = {} | |
dialog.$meta = meta; | |
var currentBlock = null; | |
var currentLine = 0; | |
function initBlock(char) { | |
if(!currentBlock) { | |
currentBlock = { id: String(blockId), char: char || "", text: [] }; | |
dialog[String(blockId)] = currentBlock; | |
currentLine = 0; | |
if(!meta.start) { | |
meta.start = String(blockId); | |
} | |
} | |
} | |
for(let l = 0; l < lines.length; ++ l) { | |
const groups = lines[l].match(tokens).groups; | |
switch(groups.command.trim()) { | |
case "#": | |
case "#-": | |
if(currentBlock) { | |
++ blockId; | |
currentBlock.next = String(blockId); | |
currentBlock = null; | |
} | |
initBlock(groups.command === '#' ? groups.label : '-'); | |
break; | |
case "*": | |
// TODO | |
break; | |
case "": | |
let textParts = groups.text.split(commands); | |
for(let p = 0; p < textParts.length; p += 3) { | |
if(textParts[p]) { | |
initBlock(""); | |
currentBlock.text[currentLine] = (currentBlock.text[currentLine] || '') + textParts[p]; | |
} | |
switch(textParts[p + 1]) { | |
case 'r': | |
initBlock(""); | |
currentBlock.text[currentLine] = (currentBlock.text[currentLine] || '') + '<br>'; | |
break; | |
case 'l': | |
initBlock(""); | |
++ currentLine; | |
break; | |
case 'char': | |
meta.chars = meta.chars || {}; | |
let char = parseOpts(textParts[p + 2]); | |
if(char.id) { | |
meta.chars[char.id] = char; | |
} | |
break; | |
} | |
} | |
break; | |
} | |
} | |
return dialog; | |
} | |
setup.parseDialog = parseDialog; | |
Macro.add("dialog", { | |
isAsync: true, | |
optRe: /^(\w+)=(.*)$/, | |
createDialog(dialog, dialogId, state, update) { | |
const $portrait = jQuery('<div class="chat-portrait"></div>'); | |
const $name = jQuery('<div class="chat-name"></div>'); | |
const $text = jQuery('<div class="chat-text"></div>'); | |
const $dialog = jQuery('<div class="chat"><div class="chat-overlay"></div></div>'); | |
$dialog.attr('id', dialogId); | |
$dialog.append($portrait, $name, $text); | |
state.currentDialog = dialog.$meta.start; | |
state.currentLine = 0; | |
$text.ariaClick(() => { | |
var data = dialog[state.currentDialog]; | |
state.currentLine = state.currentLine || 0; | |
++ state.currentLine; | |
if(state.currentLine >= data.text.length) { | |
state.currentLine = 0; | |
state.currentDialog = data.next; | |
data = dialog[state.currentDialog]; | |
} | |
if(data) { | |
update(dialog, state, $dialog); | |
} else { | |
state.currentDialog = dialog.$meta.start; | |
state.currentLine = 0; | |
$portrait.empty(); | |
$name.empty(); | |
$text.empty(); | |
$dialog.removeClass('chat-open'); | |
} | |
}); | |
return $dialog; | |
}, | |
updateDialog(dialog, state, $dialog) { | |
var data = dialog[state.currentDialog]; | |
var line = state.currentLine || 0; | |
console.debug(state); | |
if(data) { | |
var char = dialog.$meta.chars[data.char]; | |
if(char) { | |
$dialog.find(".chat-portrait").empty(); | |
if(char.img) { | |
$dialog.find(".chat-portrait").html("<img src='ext/chatportraits/" + char.img + "' />"); | |
} | |
$dialog.find(".chat-name").empty(); | |
if(char.name) { | |
$dialog.find(".chat-name").wiki(char.name); | |
} | |
} else if(data.char === '-') { | |
$dialog.find(".chat-portrait").empty(); | |
$dialog.find(".chat-name").empty(); | |
} | |
var $text = $dialog.find(".chat-text"); | |
var stop = $text[0].scrollTop; | |
$text.empty() | |
.wiki(data.text.slice(0, line + 1).join('')) | |
.wiki("<span class='chat-text-next'>▼</span>") | |
.scrollTop(stop).animate({scrollTop: $text[0].scrollHeight}, 500); | |
} | |
}, | |
handler() { | |
const opts = this.args | |
.map((txt) => typeof txt === 'string' && txt.match(this.self.optRe)) | |
.filter((opt) => !!opt) | |
.reduce((res, val) => { res[val[1]] = val[2]; return res; }, {}); | |
const args = this.args.filter((txt) => typeof txt !== 'string' || !txt.match(this.self.optRe)); | |
const passage = (args.length === 0 || typeof args[0] === 'undefined' ? '' : String(args[0]).trim()); | |
if(!passage) { | |
return this.error("Dialog passage name missing."); | |
} | |
const dialog = parseDialog(passage); | |
if(!dialog.$meta) { | |
return this.error("Passage \"" + JSON.stringify(passage) + "\" not a proper dialog passage or empty."); | |
} | |
const dialogId = 'dialog-' + Story.get(passage).domId; | |
const state = { currentDialog: dialog.$meta.start, currentLine: 0 }; | |
let $dialog = null; | |
if(document.getElementById(dialogId)) { | |
$dialog = jQuery(document.getElementById(dialogId)); | |
} else { | |
$dialog = this.self.createDialog(dialog, dialogId, state, this.self.updateDialog); | |
$dialog.appendTo(jQuery('.passage')); | |
} | |
this.self.updateDialog(dialog, state, $dialog); | |
setTimeout(() => { $dialog.addClass("chat-open"); }, 40); | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
:: Start | |
<<set $pc = { | |
name: 'Player' | |
}>> | |
<<set $ship = { | |
name: "Nostromo" | |
}>> | |
<<link "Call Ground Control">><<dialog test1.dialog>><</link>> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.passage { | |
position: relative; | |
min-height: calc(100vh - 5em); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
:: test1.dialog [dialog] | |
; Lines starting with a semicolon are comments | |
; Lines starting with # change the character doing the talking | |
; A "#" by itself simply clears the screen | |
; A "#-" resets the speaker to "nobody", with no image/portrait | |
; Lines starting with * denote a new label (for jumps and choices; not implemented yet) | |
; Lines starting with anything else, or a space, are text lines. Leading spaces are stripped. | |
; Empty lines are ignored | |
; [r] introduces a new line (you can also use <br>). [l] adds a pause waiting for user interaction. | |
; You can use most of HTML and SugarCube's syntax in the texts, including <<goto "Passagename">>, which ends the conversation. | |
; [char] registers a character with (printable) name and optional image; those can also be | |
; registered outside of this code, globally. | |
[char id=player name=$pc.name img=m_h1_e3_a1_01.png] | |
[char id=rgc name="Ground Control" img=f_h1_e1_a2_01.png] | |
#player | |
Regina Ground Control, this is S.S. //<<= $ship.name>>//, berth 94, with custom clearance number Alpha-Romeo-two-zero-zero-one. [l][r] | |
Request taxi instructions for orbital departure. | |
#rgc | |
//<<= $ship.name>>//, this Regina Ground. You are cleared to air taxi via lange Charlie to departure pod 4 North. [l][r] | |
Exercise caution for oversize traffic passing to your front on lane Delta. Contact Tower holding short. | |
#player | |
This is //<<= $ship.name>>//, traffic in sight, roger. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment