diff --git a/sonic-radiance.love/core/libs/talkies.lua b/sonic-radiance.love/core/libs/talkies.lua new file mode 100644 index 0000000..e6d91ed --- /dev/null +++ b/sonic-radiance.love/core/libs/talkies.lua @@ -0,0 +1,390 @@ +-- +-- talkies +-- +-- Copyright (c) 2017 twentytwoo, tanema +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- +local utf8 = require("utf8") + +local function playSound(sound, pitch) + if type(sound) == "userdata" then + sound:setPitch(pitch or 1) + sound:play() + end +end + +local function parseSpeed(speed) + if speed == "fast" then return 0.01 + elseif speed == "medium" then return 0.04 + elseif speed == "slow" then return 0.08 + else + assert(tonumber(speed), "setSpeed() - Expected number, got " .. tostring(speed)) + return speed + end +end + +local Fifo = {} +function Fifo.new () return setmetatable({first=1,last=0},{__index=Fifo}) end +function Fifo:peek() return self[self.first] end +function Fifo:len() return (self.last+1)-self.first end + +function Fifo:push(value) + self.last = self.last + 1 + self[self.last] = value +end + +function Fifo:pop() + if self.first > self.last then return end + local value = self[self.first] + self[self.first] = nil + self.first = self.first + 1 + return value +end + +local Typer = {} +function Typer.new(msg, speed) + local timeToType = parseSpeed(speed) + return setmetatable({ + msg = msg, complete = false, paused = false, + timer = timeToType, max = timeToType, position = 0, visible = "", + },{__index=Typer}) +end + +function Typer:resume() + if not self.paused then return end + self.msg = self.msg:gsub("%-%-", " ", 1) + self.paused = false +end + +function Typer:finish() + if self.complete then return end + self.msg = self.msg:gsub("%-%-", " ") + self.visible = self.msg + self.complete = true +end + +function Typer:update(dt) + local typed = false + if self.complete then return typed end + if not self.paused then + self.timer = self.timer - dt + while not self.paused and not self.complete and self.timer <= 0 do + typed = string.sub(self.msg, self.position, self.position) ~= " " + self.position = self.position + 1 + + self.timer = self.timer + self.max + self.visible = string.sub(self.msg, 0, utf8.offset(self.msg, self.position) - 1) + self.complete = (self.visible == self.msg) + self.paused = string.sub(self.msg, string.len(self.visible)+1, string.len(self.visible)+2) == "--" + end + end + + return typed +end + +local Talkies = { + _VERSION = '0.0.1', + _URL = 'https://github.com/tanema/talkies', + _DESCRIPTION = 'A simple messagebox system for LÖVE', + + -- Theme + indicatorCharacter = ">>", + optionCharacter = ">", + padding = 4, + talkSound = nil, + optionSwitchSound = nil, + inlineOptions = true, + + titleColor = {1, 1, 1}, + titleBackgroundColor = nil, + titleBorderColor = nil, + messageColor = {1, 1, 1}, + messageBackgroundColor = {0, 0, 0, 0.5}, + messageBorderColor = nil, + + rounding = 0, + thickness = 0, + + textSpeed = 1 / 60, + font = love.graphics.newFont(), + + typedNotTalked = true, + pitchValues = {0.7, 0.8, 1.0, 1.2, 1.3}, + + indicatorTimer = 0, + indicatorDelay = 3, + showIndicator = false, + dialogs = Fifo.new(), +} + +function Talkies.say(title, messages, config) + config = config or {} + if type(messages) ~= "table" then + messages = { messages } + end + + msgFifo = Fifo.new() + + for i=1, #messages do + msgFifo:push(Typer.new(messages[i], config.textSpeed or Talkies.textSpeed)) + end + + local font = config.font or Talkies.font + + -- Insert the Talkies.new into its own instance (table) + local newDialog = { + title = title or "", + messages = msgFifo, + image = config.image, + options = config.options, + onstart = config.onstart or function(dialog) end, + onmessage = config.onmessage or function(dialog, left) end, + oncomplete = config.oncomplete or function(dialog) end, + + -- theme + indicatorCharacter = config.indicatorCharacter or Talkies.indicatorCharacter, + optionCharacter = config.optionCharacter or Talkies.optionCharacter, + padding = config.padding or Talkies.padding, + rounding = config.rounding or Talkies.rounding, + thickness = config.thickness or Talkies.thickness, + talkSound = config.talkSound or Talkies.talkSound, + optionSwitchSound = config.optionSwitchSound or Talkies.optionSwitchSound, + inlineOptions = config.inlineOptions or Talkies.inlineOptions, + font = font, + fontHeight = font:getHeight(" "), + typedNotTalked = config.typedNotTalked == nil and Talkies.typedNotTalked or config.typedNotTalked, + pitchValues = config.pitchValues or Talkies.pitchValues, + + optionIndex = 1, + + showOptions = function(dialog) return dialog.messages:len() == 1 and type(dialog.options) == "table" end, + isShown = function(dialog) return Talkies.dialogs:peek() == dialog end + } + + newDialog.messageBackgroundColor = config.messageBackgroundColor or Talkies.messageBackgroundColor + newDialog.titleBackgroundColor = config.titleBackgroundColor or Talkies.titleBackgroundColor or newDialog.messageBackgroundColor + + newDialog.messageColor = config.messageColor or Talkies.messageColor + newDialog.titleColor = config.titleColor or Talkies.titleColor or newDialog.messageColor + + newDialog.messageBorderColor = config.messageBorderColor or Talkies.messageBorderColor or newDialog.messageBackgroundColor + newDialog.titleBorderColor = config.titleBorderColor or Talkies.titleBorderColor or newDialog.messageBorderColor + + Talkies.dialogs:push(newDialog) + if Talkies.dialogs:len() == 1 then + Talkies.dialogs:peek():onstart() + end + + return newDialog +end + +function Talkies.update(dt) + local currentDialog = Talkies.dialogs:peek() + if currentDialog == nil then return end + local currentMessage = currentDialog.messages:peek() + + if currentMessage.paused or currentMessage.complete then + Talkies.indicatorTimer = Talkies.indicatorTimer + (10 * dt) + if Talkies.indicatorTimer > Talkies.indicatorDelay then + Talkies.showIndicator = not Talkies.showIndicator + Talkies.indicatorTimer = 0 + end + else + Talkies.showIndicator = false + end + + if currentMessage:update(dt) then + if currentDialog.typedNotTalked then + playSound(currentDialog.talkSound) + elseif not currentDialog.talkSound:isPlaying() then + local pitch = currentDialog.pitchValues[math.random(#currentDialog.pitchValues)] + playSound(currentDialog.talkSound, pitch) + end + end +end + +function Talkies.advanceMsg() + local currentDialog = Talkies.dialogs:peek() + if currentDialog == nil then return end + currentDialog:onmessage(currentDialog.messages:len() - 1) + if currentDialog.messages:len() == 1 then + Talkies.dialogs:pop() + currentDialog:oncomplete() + if Talkies.dialogs:len() == 0 then + Talkies.clearMessages() + else + Talkies.dialogs:peek():onstart() + end + end + currentDialog.messages:pop() +end + +function Talkies.isOpen() + return Talkies.dialogs:peek() ~= nil +end + +function Talkies.draw() + local currentDialog = Talkies.dialogs:peek() + if currentDialog == nil then return end + + local currentMessage = currentDialog.messages:peek() + + --love.graphics.push() + love.graphics.setDefaultFilter("nearest", "nearest") + + local function getDimensions() + return core.screen:getDimensions() + end + + local windowWidth, windowHeight = getDimensions() + + love.graphics.setLineWidth(currentDialog.thickness) + + -- message box + local boxW = windowWidth---(2*currentDialog.padding) + local boxH = (windowHeight/3)--(2*currentDialog.padding) + local boxX = 0--currentDialog.padding + local boxY = windowHeight-(boxH+currentDialog.padding) + + -- image + local imgX, imgY, imgW, imgScale = boxX+currentDialog.padding, boxY+currentDialog.padding, 0, 0 + if currentDialog.image ~= nil then + imgScale = (boxH - (currentDialog.padding * 2)) / currentDialog.image:getHeight() + imgW = currentDialog.image:getWidth() * imgScale + end + + -- title box + local textX, textY = imgX + imgW + currentDialog.padding, boxY + 4 + + love.graphics.setFont(currentDialog.font) + + if currentDialog.title ~= "" then + local titleBoxW = currentDialog.font:getWidth(currentDialog.title)+(4*currentDialog.padding) + local titleBoxH = currentDialog.fontHeight+currentDialog.padding + local titleBoxY = boxY-titleBoxH-(currentDialog.padding/2) + local titleBoxX = currentDialog.padding * 2 + local titleX, titleY = titleBoxX + currentDialog.padding*2, titleBoxY + 2 + + -- Message title + love.graphics.setColor(currentDialog.titleBackgroundColor) + love.graphics.rectangle("fill", titleBoxX, titleBoxY, titleBoxW, titleBoxH, titleBoxH/2, titleBoxH/2) + if currentDialog.thickness > 0 then + love.graphics.setColor(currentDialog.titleBorderColor) + love.graphics.rectangle("line", boxX, titleBoxY, titleBoxW, titleBoxH, currentDialog.rounding, currentDialog.rounding) + end + love.graphics.setColor(currentDialog.titleColor) + love.graphics.print(currentDialog.title, titleX, titleY) + end + + -- Main message box + love.graphics.setColor(currentDialog.messageBackgroundColor) + love.graphics.rectangle("fill", boxX, boxY, boxW, boxH, currentDialog.rounding, currentDialog.rounding) + if currentDialog.thickness > 0 then + love.graphics.setColor(currentDialog.messageBorderColor) + love.graphics.rectangle("line", boxX, boxY, boxW, boxH, currentDialog.rounding, currentDialog.rounding) + end + + -- Message avatar + if currentDialog.image ~= nil then + love.graphics.push() + love.graphics.setColor(1, 1, 1) + love.graphics.draw(currentDialog.image, imgX, imgY, 0, imgScale, imgScale) + love.graphics.pop() + end + + -- Message text + love.graphics.setColor(currentDialog.messageColor) + local textW = boxW - imgW - (4 * currentDialog.padding) + local _, modmsg = currentDialog.font:getWrap(currentMessage.msg, textW) + local catmsg = table.concat(modmsg, "\n") + + local display = string.sub(catmsg, 1, #currentMessage.visible + #modmsg - 1) + + love.graphics.print(display, textX, textY) + + -- Message options (when shown) + if currentDialog:showOptions() and currentMessage.complete then + if currentDialog.inlineOptions then + local optionsY = textY+currentDialog.font:getHeight(currentMessage.visible)-(currentDialog.padding/1.6) + local optionLeftPad = currentDialog.font:getWidth(currentDialog.optionCharacter.." ") + for k, option in pairs(currentDialog.options) do + love.graphics.print(option[1], optionLeftPad+textX+currentDialog.padding, optionsY+((k-1)*currentDialog.fontHeight)) + end + love.graphics.print( + currentDialog.optionCharacter.." ", + textX+currentDialog.padding, + optionsY+((currentDialog.optionIndex-1)*currentDialog.fontHeight)) + else + local optionWidth = 0 + + local optionText = "" + for k, option in pairs(currentDialog.options) do + local newText = (currentDialog.optionIndex == k and currentDialog.optionCharacter or " ") .. " " .. option[1] + optionWidth = math.max(optionWidth, currentDialog.font:getWidth(newText) ) + optionText = optionText .. newText .. "\n" + end + + local optionsH = (currentDialog.font:getHeight() * #currentDialog.options) + local optionsX = math.floor((windowWidth / 2) - (optionWidth / 2)) + local optionsY = math.floor((windowHeight / 3) - (optionsH / 2)) + + love.graphics.setColor(currentDialog.messageBackgroundColor) + love.graphics.rectangle("fill", optionsX - currentDialog.padding, optionsY - currentDialog.padding, optionWidth + currentDialog.padding * 2, optionsH + currentDialog.padding * 2, currentDialog.rounding, currentDialog.rounding) + + if currentDialog.thickness > 0 then + love.graphics.setColor(currentDialog.messageBorderColor) + love.graphics.rectangle("line", optionsX - currentDialog.padding, optionsY - currentDialog.padding, optionWidth + currentDialog.padding * 2, optionsH + currentDialog.padding * 2, currentDialog.rounding, currentDialog.rounding) + end + + love.graphics.setColor(currentDialog.messageColor) + love.graphics.print(optionText, optionsX, optionsY) + end + end + + -- Next message/continue indicator + if Talkies.showIndicator then + love.graphics.print(currentDialog.indicatorCharacter, boxX+boxW-(4*currentDialog.padding), boxY+boxH-currentDialog.fontHeight) + end + + --love.graphics.pop() +end + +function Talkies.prevOption() + local currentDialog = Talkies.dialogs:peek() + if currentDialog == nil or not currentDialog:showOptions() then return end + currentDialog.optionIndex = currentDialog.optionIndex - 1 + if currentDialog.optionIndex < 1 then currentDialog.optionIndex = #currentDialog.options end + playSound(currentDialog.optionSwitchSound) +end + +function Talkies.nextOption() + local currentDialog = Talkies.dialogs:peek() + if currentDialog == nil or not currentDialog:showOptions() then return end + currentDialog.optionIndex = currentDialog.optionIndex + 1 + if currentDialog.optionIndex > #currentDialog.options then currentDialog.optionIndex = 1 end + playSound(currentDialog.optionSwitchSound) +end + +function Talkies.onAction() + local currentDialog = Talkies.dialogs:peek() + if currentDialog == nil then return end + local currentMessage = currentDialog.messages:peek() + + if currentMessage.paused then currentMessage:resume() + elseif not currentMessage.complete then currentMessage:finish() + else + if currentDialog:showOptions() then + currentDialog.options[currentDialog.optionIndex][2]() -- Execute the selected function + playSound(currentDialog.optionSwitchSound) + end + Talkies.advanceMsg() + end +end + +function Talkies.clearMessages() + Talkies.dialogs = Fifo.new() +end + +return Talkies diff --git a/sonic-radiance.love/game/events/arguments.lua b/sonic-radiance.love/game/events/arguments.lua index ab4ded1..453672c 100644 --- a/sonic-radiance.love/game/events/arguments.lua +++ b/sonic-radiance.love/game/events/arguments.lua @@ -1,7 +1,7 @@ return { ["wait"] = {"duration"}, ["simpleMessage"] = {"message"}, - ["dialogBox"] = {"message"}, + ["dialogBox"] = {"message", "title", "avatar"}, --[name] = {args...}, } diff --git a/sonic-radiance.love/game/events/event/dialogbox.lua b/sonic-radiance.love/game/events/event/dialogbox.lua new file mode 100644 index 0000000..dfcb9a1 --- /dev/null +++ b/sonic-radiance.love/game/events/event/dialogbox.lua @@ -0,0 +1,32 @@ +local StepParent = require "game.events.event.parent" +local DialogBox = StepParent:extend() + +local Talkies = require('core.libs.talkies') + +function DialogBox:new(controller, args) + DialogBox.super.new(self, controller, args) + Talkies.font = love.graphics.newFont("assets/gui/fonts/PixelOperator.ttf", 16) +end + +function DialogBox:start() + Talkies.say(self.arguments.title, self.arguments.message) +end + +function DialogBox:update(dt) + Talkies.update(dt) + + if (not Talkies.isOpen()) then + self:finish() + end + local keys = self.events.scene.sources[1].keys + if (keys["up"].isPressed) then Talkies.prevOption() + elseif (keys["down"].isPressed) then Talkies.nextOption() + elseif (keys["A"].isPressed) then Talkies.onAction() + end +end + +function DialogBox:draw() + Talkies.draw() +end + +return DialogBox; diff --git a/sonic-radiance.love/game/events/event/init.lua b/sonic-radiance.love/game/events/event/init.lua index 44884aa..607095d 100644 --- a/sonic-radiance.love/game/events/event/init.lua +++ b/sonic-radiance.love/game/events/event/init.lua @@ -1,4 +1,5 @@ return { ["wait"] = require("game.events.event.wait"), - ["simpleMessage"] = require("game.events.event.simpleMessage") + ["simpleMessage"] = require("game.events.event.simpleMessage"), + ["dialogBox"] = require("game.events.event.dialogbox") } \ No newline at end of file