├── VN Example ├── bg1.png ├── bg1T.png ├── char.png ├── music.mp3 ├── effect.mp3 ├── revChar.png ├── voices │ ├── voice1.mp3 │ └── voice2.wav ├── menu.txt ├── init.txt ├── states.lua ├── script.txt ├── dialogButtons.lua ├── main.lua ├── buttons.lua ├── menu.lua ├── sunclass.lua └── reader.lua ├── states.lua ├── ChangeLog.txt ├── dialogButtons.lua ├── main.lua ├── README.txt ├── buttons.lua ├── menu.lua ├── Calls.txt └── reader.lua /VN Example/bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkcube/LoveBook/HEAD/VN Example/bg1.png -------------------------------------------------------------------------------- /VN Example/bg1T.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkcube/LoveBook/HEAD/VN Example/bg1T.png -------------------------------------------------------------------------------- /VN Example/char.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkcube/LoveBook/HEAD/VN Example/char.png -------------------------------------------------------------------------------- /VN Example/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkcube/LoveBook/HEAD/VN Example/music.mp3 -------------------------------------------------------------------------------- /VN Example/effect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkcube/LoveBook/HEAD/VN Example/effect.mp3 -------------------------------------------------------------------------------- /VN Example/revChar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkcube/LoveBook/HEAD/VN Example/revChar.png -------------------------------------------------------------------------------- /VN Example/voices/voice1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkcube/LoveBook/HEAD/VN Example/voices/voice1.mp3 -------------------------------------------------------------------------------- /VN Example/voices/voice2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Linkcube/LoveBook/HEAD/VN Example/voices/voice2.wav -------------------------------------------------------------------------------- /VN Example/menu.txt: -------------------------------------------------------------------------------- 1 | Click on the left one to start/return to the game! 2 | Button add 200 160 Hi char.png 3 | Button add 80 160 Start revChar.png -------------------------------------------------------------------------------- /VN Example/init.txt: -------------------------------------------------------------------------------- 1 | -- Initialize game assets 2 | setResolution 1024 720 3 | setTitle VN Test 4 | setIco char.png 5 | Character Bill char.png 6 | Background bg1 bg1.png 7 | Character Bill addPose flip revChar.png 8 | Sound effect effect.mp3 9 | Music music music.mp3 10 | Font size 26 11 | VoiceTable voices -------------------------------------------------------------------------------- /states.lua: -------------------------------------------------------------------------------- 1 | class = require("sunclass") -- try out using sunclass from 'qwook' 2 | reader = require('reader') 3 | menu = require('menu') 4 | dialogButtons = require('dialogButtons') 5 | buttons = require('buttons') 6 | GameStatesClass = class("GameStatesClass") 7 | 8 | function GameStatesClass:initialize() 9 | self.current = 0 10 | self.states = {} 11 | end 12 | 13 | function GameStatesClass:Push(state) 14 | self.current = self.current + 1 15 | self.states[self.current] = state 16 | self.states[self.current]:Load() 17 | end 18 | 19 | function GameStatesClass:Pop() 20 | self.states[self.current] = self.states[self.current - 1] 21 | self.current = self.current - 1 22 | self.states[self.current + 1] = nil 23 | end 24 | -------------------------------------------------------------------------------- /VN Example/states.lua: -------------------------------------------------------------------------------- 1 | class = require("sunclass") -- try out using sunclass from 'qwook' 2 | reader = require('reader') 3 | menu = require('menu') 4 | dialogButtons = require('dialogButtons') 5 | buttons = require('buttons') 6 | GameStatesClass = class("GameStatesClass") 7 | 8 | function GameStatesClass:initialize() 9 | self.current = 0 10 | self.states = {} 11 | end 12 | 13 | function GameStatesClass:Push(state) 14 | self.current = self.current + 1 15 | self.states[self.current] = state 16 | self.states[self.current]:Load() 17 | end 18 | 19 | function GameStatesClass:Pop() 20 | self.states[self.current] = self.states[self.current - 1] 21 | self.current = self.current - 1 22 | self.states[self.current + 1] = nil 23 | end 24 | -------------------------------------------------------------------------------- /ChangeLog.txt: -------------------------------------------------------------------------------- 1 | V1.0 TBA 2 | 3 | To do: 4 | ##Completed## 5 | Multiple images per character [x] 6 | Fade transitions [x] 7 | Sound [x] 8 | Text wrapping [x] 9 | Font support [x] 10 | Progressive text [x] 11 | Scale Backgrounds [x] 12 | Speech stack [x] 13 | 14 | ##Bugged/Partly working## 15 | Menu/State system [x] # further functionality and programability to be added 16 | 17 | ##Not done## 18 | Standardising [] 19 | Automatic playing [] # can begin development soon 20 | Save/Load [] # can begin development soon 21 | Shaders [?] 22 | Slide move [] # should be done by now 23 | Advanced commands [] # may just have deeper documentation, and cleaner code 24 | In-game options [] # can begin development soon 25 | Multiple paths [] 26 | UI, alternate text and color schemes [] # can begin development soon -------------------------------------------------------------------------------- /VN Example/script.txt: -------------------------------------------------------------------------------- 1 | 2 | Hi there, I'm a character 3 | Really long string should be split really badly, like you know, multiple times. But i guess not cuz thats jsut how it is.. yea this is long 4 | --Conf 5 | Character Bill move middle 6 | Character Bill show 7 | Background bg1 set 8 | Music music play 9 | Music music volume .5 10 | --Conf 11 | See here this is a lot of text, hopefully this will get wrapped aronud and not be horrible to look at! 12 | --Clear 13 | New text 14 | --Conf 15 | Character Bill move right 16 | Character Bill pose flip 17 | --Conf 18 | He moved 19 | --Clear 20 | Clean. 21 | --Conf 22 | Character Bill move left 23 | Sound effect play 24 | --Conf 25 | --Clear 26 | Left! 27 | --Clear 28 | --Conf 29 | Character Bill move middle 30 | --Conf 31 | --Clear 32 | Middle!! 33 | --Clear 34 | --Conf 35 | Character Bill hide 36 | Music music stop 37 | --Conf -------------------------------------------------------------------------------- /dialogButtons.lua: -------------------------------------------------------------------------------- 1 | dButtons = {} 2 | 3 | function dButtons:Click(x, y, button) 4 | if button == 'l' then 5 | for k, v in pairs(dButtons.buttons) do 6 | if v:Contact(x, y) then 7 | dButtons.choosen = v.name 8 | GameStates:Pop() 9 | end 10 | end 11 | end 12 | end 13 | 14 | function dButtons:Update(dt) 15 | end 16 | 17 | function dButtons:lineSplit(s) 18 | split = {} 19 | o = 0 20 | for i in string.gmatch(s, "%S+") do 21 | split[o] = i 22 | o = o + 1 23 | end 24 | return split 25 | end 26 | 27 | function dButtons:Draw() 28 | Reader:Draw() 29 | r, g, b, a = love.graphics.getColor() 30 | love.graphics.setColor(255, 255, 255, 255) 31 | love.graphics.print(textToPrint) 32 | for k, v in pairs(dButtons.buttons) do 33 | v:Draw() 34 | end 35 | love.graphics.setColor(r, g, b, a) 36 | end 37 | 38 | function dButtons:Load() 39 | dButtons.buttons = {} 40 | dButtons.choosen = "" 41 | end -------------------------------------------------------------------------------- /VN Example/dialogButtons.lua: -------------------------------------------------------------------------------- 1 | dButtons = {} 2 | 3 | function dButtons:Click(x, y, button) 4 | if button == 'l' then 5 | for k, v in pairs(dButtons.buttons) do 6 | if v:Contact(x, y) then 7 | dButtons.choosen = v.name 8 | GameStates:Pop() 9 | end 10 | end 11 | end 12 | end 13 | 14 | function dButtons:Update(dt) 15 | end 16 | 17 | function dButtons:lineSplit(s) 18 | split = {} 19 | o = 0 20 | for i in string.gmatch(s, "%S+") do 21 | split[o] = i 22 | o = o + 1 23 | end 24 | return split 25 | end 26 | 27 | function dButtons:Draw() 28 | Reader:Draw() 29 | r, g, b, a = love.graphics.getColor() 30 | love.graphics.setColor(255, 255, 255, 255) 31 | love.graphics.print(textToPrint) 32 | for k, v in pairs(dButtons.buttons) do 33 | v:Draw() 34 | end 35 | love.graphics.setColor(r, g, b, a) 36 | end 37 | 38 | function dButtons:Load() 39 | dButtons.buttons = {} 40 | dButtons.choosen = "" 41 | end -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | local states = require('states') 2 | 3 | function love.load() 4 | GameStates = GameStatesClass:new() 5 | GameStates:Push(Reader) 6 | GameStates:Push(Menu) 7 | end 8 | 9 | function love.update(dt) 10 | GameStates.states[GameStates.current]:Update(dt) 11 | end 12 | 13 | function love.mousepressed(x, y, button) 14 | GameStates.states[GameStates.current]:Click(x, y, button) 15 | end 16 | 17 | function love.keypressed(key, isrepeat) -- Clean restart, clears all assets 18 | if key == 'r' then 19 | love.audio.stop() 20 | GameStates:Pop() 21 | package.loaded.states = nil 22 | states = require('states') 23 | GameStates = GameStatesClass:new() 24 | GameStates:Push(Reader) 25 | GameStates:Push(Menu) 26 | else 27 | GameStates.states[GameStates.current]:KeyPress(key, isrepeat) -- pass through if not reset 28 | end 29 | end 30 | 31 | function love.draw() 32 | GameStates.states[GameStates.current]:Draw() 33 | end 34 | -------------------------------------------------------------------------------- /VN Example/main.lua: -------------------------------------------------------------------------------- 1 | local states = require('states') 2 | 3 | function love.load() 4 | GameStates = GameStatesClass:new() 5 | GameStates:Push(Reader) 6 | GameStates:Push(Menu) 7 | end 8 | 9 | function love.update(dt) 10 | GameStates.states[GameStates.current]:Update(dt) 11 | end 12 | 13 | function love.mousepressed(x, y, button) 14 | GameStates.states[GameStates.current]:Click(x, y, button) 15 | end 16 | 17 | function love.keypressed(key, isrepeat) -- Clean restart, clears all assets 18 | if key == 'r' then 19 | love.audio.stop() 20 | GameStates:Pop() 21 | package.loaded.states = nil 22 | states = require('states') 23 | GameStates = GameStatesClass:new() 24 | GameStates:Push(Reader) 25 | GameStates:Push(Menu) 26 | else 27 | GameStates.states[GameStates.current]:KeyPress(key, isrepeat) -- pass through if not reset 28 | end 29 | end 30 | 31 | function love.draw() 32 | GameStates.states[GameStates.current]:Draw() 33 | end 34 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | These files are for the project "LoveBook", which is designed to be a visual novel engine running on love2d. 2 | 3 | Included in the github repository are the following files: 4 | - main.lua 5 | - menu.lua 6 | - reader.lua 7 | - states.lua 8 | - Calls.txt 9 | - menu.txt 10 | - ChangeLog.txt 11 | - VN Example 12 | 13 | # lua files 14 | Contains all the lua/love2d code, so far this is not an api but may become one at a later point once it is more complete. 15 | !!Must have all .lua files in order to work!! 16 | 17 | # Calls.txt 18 | Details the calls that can be made in script.txt and several notes on limitations of the system 19 | 20 | # ChangeLog.txt 21 | Contains different changes (later to be handled by github) but more importantly to handle goals and objectives of the system that need to be added later 22 | 23 | To make use of this code you will need to have the included lua files, either sunclass.lua or middleclass.lua, and a script.txt and init.txt, the latter 2 you will be writing. 24 | 25 | Please read through the example folder to get a basic idea of how to use this, then read through the calls.txt for more detailed information. 26 | 27 | # In-game Command - The key 'R' 28 | This will reload all assets for quick testing so that you don't need to re-run the lua code for changes... anywhere (except the main.lua) -------------------------------------------------------------------------------- /buttons.lua: -------------------------------------------------------------------------------- 1 | ButtonClass = class("ButtonClass") 2 | 3 | function ButtonClass:initialize(X, Y, W, H, N, V) 4 | self.x = X or 0 5 | self.y = Y or 0 6 | self.width = W or 0 7 | self.height = H or 0 8 | self.name = N or "" 9 | self.value = V or "" 10 | self.color = {255, 0, 255} 11 | end 12 | 13 | function ButtonClass:Contact(X, Y) 14 | if X >= self.x and X <= self.width + self.x then 15 | if Y >= self.y and Y <= self.height + self.y then 16 | return true 17 | end 18 | end 19 | return false 20 | end 21 | 22 | function ButtonClass:Draw() -- draw an empty box with text inside it 23 | local tmptable = { 24 | self.x, self.y, 25 | self.x + self.width, self.y, 26 | self.x + self.width, self.y + self.height, 27 | self.x, self.y + self.height, 28 | self.x, self.y 29 | } 30 | love.graphics.line(tmptable) 31 | love.graphics.print(self.v, self.x + 2, self.y + self.height/2) 32 | end 33 | 34 | -------------------------------------------------------------------------- 35 | 36 | IButtonClass = class("IButtonClass") 37 | 38 | function IButtonClass:initialize(X, Y, I, N) 39 | self.x = X or 0 40 | self.y = Y or 0 41 | self.image = love.graphics.newImage(I) or nil 42 | self.width = self.image:getWidth() or 0 43 | self.height = self.image:getHeight() or 0 44 | self.name = N or "" 45 | end 46 | 47 | function IButtonClass:Contact(X, Y) 48 | if X >= self.x and X <= self.width + self.x then 49 | if Y >= self.y and Y <= self.height + self.y then 50 | return true 51 | end 52 | end 53 | return false 54 | end 55 | 56 | function IButtonClass:Draw() 57 | love.graphics.draw(self.image, self.x, self.y) 58 | end -------------------------------------------------------------------------------- /VN Example/buttons.lua: -------------------------------------------------------------------------------- 1 | ButtonClass = class("ButtonClass") 2 | 3 | function ButtonClass:initialize(X, Y, W, H, N, V) 4 | self.x = X or 0 5 | self.y = Y or 0 6 | self.width = W or 0 7 | self.height = H or 0 8 | self.name = N or "" 9 | self.value = V or "" 10 | self.color = {255, 0, 255} 11 | end 12 | 13 | function ButtonClass:Contact(X, Y) 14 | if X >= self.x and X <= self.width + self.x then 15 | if Y >= self.y and Y <= self.height + self.y then 16 | return true 17 | end 18 | end 19 | return false 20 | end 21 | 22 | function ButtonClass:Draw() -- draw an empty box with text inside it 23 | local tmptable = { 24 | self.x, self.y, 25 | self.x + self.width, self.y, 26 | self.x + self.width, self.y + self.height, 27 | self.x, self.y + self.height, 28 | self.x, self.y 29 | } 30 | love.graphics.line(tmptable) 31 | love.graphics.print(self.v, self.x + 2, self.y + self.height/2) 32 | end 33 | 34 | -------------------------------------------------------------------------- 35 | 36 | IButtonClass = class("IButtonClass") 37 | 38 | function IButtonClass:initialize(X, Y, I, N) 39 | self.x = X or 0 40 | self.y = Y or 0 41 | self.image = love.graphics.newImage(I) or nil 42 | self.width = self.image:getWidth() or 0 43 | self.height = self.image:getHeight() or 0 44 | self.name = N or "" 45 | end 46 | 47 | function IButtonClass:Contact(X, Y) 48 | if X >= self.x and X <= self.width + self.x then 49 | if Y >= self.y and Y <= self.height + self.y then 50 | return true 51 | end 52 | end 53 | return false 54 | end 55 | 56 | function IButtonClass:Draw() 57 | love.graphics.draw(self.image, self.x, self.y) 58 | end -------------------------------------------------------------------------------- /menu.lua: -------------------------------------------------------------------------------- 1 | Menu = {} 2 | local textToPrint = "" 3 | 4 | function Menu:Click(x, y, button) 5 | if button == 'l' then 6 | for k, v in pairs(Menu.buttons) do 7 | if v:Contact(x, y) then 8 | if v.name == "Start" then -- Prepare to remove the menu state from the state stack 9 | for key, val in pairs(sounds) do 10 | if val:isPaused() then val:resume() end 11 | end 12 | for key, val in pairs(musics) do 13 | if val:isPaused() then val:resume() end 14 | end 15 | if voicePlay and voiceTable[voicePos-1] ~= nil then 16 | voiceTable[voicePos-1]:resume() 17 | end 18 | GameStates:Pop() -- Remove the menu state 19 | end 20 | end 21 | end 22 | end 23 | end 24 | 25 | function Menu:Update(dt) 26 | end 27 | 28 | function Menu:lineSplit(s) 29 | split = {} 30 | o = 0 31 | for i in string.gmatch(s, "%S+") do 32 | split[o] = i 33 | o = o + 1 34 | end 35 | return split 36 | end 37 | 38 | function Menu:Draw() 39 | r, g, b, a = love.graphics.getColor() 40 | love.graphics.setColor(255, 255, 255, 255) 41 | love.graphics.print(textToPrint) 42 | for k, v in pairs(Menu.buttons) do 43 | v:Draw() 44 | end 45 | love.graphics.setColor(r, g, b, a) 46 | end 47 | 48 | function Menu:KeyPress(key, isrepeat) 49 | end 50 | 51 | function Menu:Load() 52 | Menu.buttons = {} 53 | local buttonCount = 1 54 | for ls in love.filesystem.lines("menu.txt") do -- load assets 55 | lineS = Menu:lineSplit(ls) 56 | if lineS[0] == "Button" then 57 | if lineS[1] == "add" then 58 | Menu.buttons[buttonCount] = IButtonClass:new(tonumber(lineS[2]), tonumber(lineS[3]), lineS[5], lineS[4]) 59 | buttonCount = buttonCount + 1 60 | end 61 | else 62 | textToPrint = ls 63 | end 64 | end 65 | for k, v in pairs(sounds) do 66 | if v:isPlaying() then v:pause() end 67 | end 68 | for k, v in pairs(musics) do 69 | if v:isPlaying() then v:pause() end 70 | end 71 | if voicePlay and voiceTable[voicePos-1] ~= nil then 72 | if voiceTable[voicePos-1]:isPlaying() then voiceTable[voicePos-1]:pause() end 73 | end 74 | end -------------------------------------------------------------------------------- /VN Example/menu.lua: -------------------------------------------------------------------------------- 1 | Menu = {} 2 | local textToPrint = "" 3 | 4 | function Menu:Click(x, y, button) 5 | if button == 'l' then 6 | for k, v in pairs(Menu.buttons) do 7 | if v:Contact(x, y) then 8 | if v.name == "Start" then -- Prepare to remove the menu state from the state stack 9 | for key, val in pairs(sounds) do 10 | if val:isPaused() then val:resume() end 11 | end 12 | for key, val in pairs(musics) do 13 | if val:isPaused() then val:resume() end 14 | end 15 | if voicePlay and voiceTable[voicePos-1] ~= nil then 16 | voiceTable[voicePos-1]:resume() 17 | end 18 | GameStates:Pop() -- Remove the menu state 19 | end 20 | end 21 | end 22 | end 23 | end 24 | 25 | function Menu:Update(dt) 26 | end 27 | 28 | function Menu:lineSplit(s) 29 | split = {} 30 | o = 0 31 | for i in string.gmatch(s, "%S+") do 32 | split[o] = i 33 | o = o + 1 34 | end 35 | return split 36 | end 37 | 38 | function Menu:Draw() 39 | r, g, b, a = love.graphics.getColor() 40 | love.graphics.setColor(255, 255, 255, 255) 41 | love.graphics.print(textToPrint) 42 | for k, v in pairs(Menu.buttons) do 43 | v:Draw() 44 | end 45 | love.graphics.setColor(r, g, b, a) 46 | end 47 | 48 | function Menu:KeyPress(key, isrepeat) 49 | end 50 | 51 | function Menu:Load() 52 | Menu.buttons = {} 53 | local buttonCount = 1 54 | for ls in love.filesystem.lines("menu.txt") do -- load assets 55 | lineS = Menu:lineSplit(ls) 56 | if lineS[0] == "Button" then 57 | if lineS[1] == "add" then 58 | Menu.buttons[buttonCount] = IButtonClass:new(tonumber(lineS[2]), tonumber(lineS[3]), lineS[5], lineS[4]) 59 | buttonCount = buttonCount + 1 60 | end 61 | else 62 | textToPrint = ls 63 | end 64 | end 65 | for k, v in pairs(sounds) do 66 | if v:isPlaying() then v:pause() end 67 | end 68 | for k, v in pairs(musics) do 69 | if v:isPlaying() then v:pause() end 70 | end 71 | if voicePlay and voiceTable[voicePos-1] ~= nil then 72 | if voiceTable[voicePos-1]:isPlaying() then voiceTable[voicePos-1]:pause() end 73 | end 74 | end -------------------------------------------------------------------------------- /VN Example/sunclass.lua: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- 3 | -- The MIT License (MIT) 4 | 5 | -- Copyright (c) 2014 Henry Quoc Tran 6 | 7 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 8 | -- of this software and associated documentation files (the "Software"), to deal 9 | -- in the Software without restriction, including without limitation the rights 10 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | -- copies of the Software, and to permit persons to whom the Software is 12 | -- furnished to do so, subject to the following conditions: 13 | 14 | -- The above copyright notice and this permission notice shall be included in 15 | -- all copies or substantial portions of the Software. 16 | 17 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | -- THE SOFTWARE. 24 | 25 | -------------------------------------------------------------------------------- 26 | 27 | _R = _R or {} 28 | 29 | local function class(name, ...) 30 | 31 | -- if this class already exists, just load it up. 32 | if _R[name] then 33 | return _R[name] 34 | end 35 | 36 | local class = {} 37 | 38 | -- class index access 39 | class.__index = function(s, k) 40 | -- create a fake object for accessing superclass methods 41 | if (k) == "super" then 42 | return setmetatable({}, 43 | { 44 | __index = 45 | function(fake, k) 46 | local sp_array = class.__super 47 | for _, _sp in pairs(sp_array) do 48 | local sp = _sp 49 | while (sp[k] == nil and sp.__super) do 50 | sp = sp.__super 51 | end 52 | if type(sp[k]) == "function" then 53 | return function(fake, ...) 54 | local olindex = class.__index 55 | class.__index = sp.__index 56 | local ret = {sp[k](s, ...)} 57 | class.__index = olindex 58 | return unpack(ret) 59 | end 60 | end 61 | end 62 | end; 63 | __tostring = 64 | function() 65 | return "class Super of " .. class.__classname 66 | end 67 | }) 68 | end 69 | 70 | -- loop through super classes / mixins to find what we're looking for 71 | if class[k] == nil then 72 | local sp_array = class.__super 73 | for _, sp in pairs(sp_array) do 74 | local ret = sp.__index(s, k) 75 | if ret ~= nil then 76 | return ret 77 | end 78 | end 79 | end 80 | 81 | -- return normally 82 | return class[k] 83 | end 84 | -- end class index access 85 | 86 | class.new = function(s, ...) local n = {} local o = setmetatable(n, s) if o.initialize then o:initialize(...) end return o end 87 | class.__tostring = function() return "class " .. class.__classname end 88 | class.__classname = name 89 | class.__super = {...} 90 | 91 | class.instanceOf = function( self, c ) 92 | local mt = getmetatable(self) 93 | if mt == c then return true else 94 | -- using a queue, go through each of the supers 95 | -- try to match up a super with the given class c 96 | local supers = {} 97 | for k, v in pairs(mt.__super) do 98 | table.insert(supers, v) 99 | end 100 | while (#supers > 0) do 101 | for i = 1, #supers do 102 | local mt = supers[1] 103 | if c == mt then return true end 104 | for k, v in pairs(mt.__super) do 105 | table.insert(supers, v) 106 | end 107 | table.remove(supers, 1) 108 | end 109 | end 110 | end 111 | 112 | return false 113 | end 114 | 115 | _R[name] = class 116 | 117 | return class 118 | 119 | end 120 | 121 | return class -------------------------------------------------------------------------------- /Calls.txt: -------------------------------------------------------------------------------- 1 | Here are calls that can be made in the script.txt and init.txt 2 | 3 | init.txt: 4 | 5 | setResolution x y 6 | Sets the size of the screen to these proportions 7 | ex: setResolution 600 800 8 | 9 | setTitle s 10 | Sets the title of the window to a string, can include spaces 11 | ex: setTitle My first VN! 12 | 13 | setIcon path/to/image.png 14 | Sets the window's icon to a specific icon 15 | ex: setIcon myIco.png 16 | 17 | Character name path/to/image.png 18 | Creates a charater with using name and has the image specified as their default image to draw 19 | ex: Character Lemon characters/lemon/default.png 20 | 21 | Character name addPose pose path/to/image.png 22 | Adds a pose to the specified character (pose names the pose) with the image of the path 23 | ex: Character Lemon addPose awesome characters/lemon/awesome.jpeg 24 | 25 | Background name path/to/bg.png 26 | Adds a background name name with the image specified 27 | ex: Background sunnyDay backgrounds/mountains/sunny.png 28 | 29 | Sound name path/to/sound.ogg 30 | Adds a sound with name, can use most any file type (wav/mp3/flac/etc). Use short/small files only for this 31 | as if you load large files it can have performance problems, use music instead for those 32 | ex: Sound zap audio/sounds/zap.mp3 33 | 34 | Music name path/to/music.wav 35 | Adds music with a name, will loop automatically, good for large files. 36 | ex: Music bgm audio/music/happy.wav 37 | 38 | Font size # 39 | Sets the font to # size 40 | ex: Font size 12 41 | 42 | VoiceTable dir 43 | Creates the voice table from all files in dir, will play one sound file per text input, automatically turns on voice speech per line 44 | ex: VoiceTable voices 45 | 46 | ###### 47 | 48 | script.txt: 49 | 50 | Plain text 51 | Plain text will be interperited as text to be drawn onto the screen, and will be spaced apart as paragraphs 52 | for each line 53 | ex: Hi there, it's been a while hasn't it? 54 | 55 | --Conf 56 | Declares that the following lines until the next --Conf will be not text, instead changing variables and the 57 | stage 58 | ex: 59 | normal text 60 | --Conf 61 | ... 62 | code here (tab not required) 63 | --Conf 64 | back to text 65 | 66 | (The following until --Conf are only usable between --Conf) 67 | 68 | Character name move position 69 | The character of name will move to a part of the screen specified (left/middle/right) 70 | ex: Character Lemon move left 71 | 72 | Character name show 73 | Will add the character of name to the draw list (basically displays the character on the screen) 74 | ex: Character Lemon show 75 | 76 | Character name hide 77 | Will remove the character of name from the draw list (no longer displays the chracter on the screen) 78 | ex: Character Lemon hide 79 | 80 | Character name pose name2 81 | Will change the image of character name to a pose declared in init.txt 82 | ex: Character Lemon pose awesome 83 | 84 | Background name set 85 | Will set the current background to the one specified 86 | ex: Background sunnyDay set 87 | 88 | Background remove 89 | Will remove the current background, returning to a black background 90 | ex: Background remove 91 | 92 | Music name play 93 | Will play the music of name until stopped 94 | ex: Music bgm play 95 | 96 | Music name stop 97 | Will stop the music of name playing 98 | ex: Music bgm stop 99 | 100 | Music name volume # 101 | Will change the volume of music name to a value (0.0 to 1.0) where 1.0 is normal volume 102 | ex: Music bgm volume .5 103 | 104 | Sound name play 105 | Will play a sound of name (not on loop) 106 | ex: Sound zap play 107 | 108 | Sound name stop 109 | will stop a sound, good if you want to cut off after user input 110 | ex: Sound zap stop 111 | 112 | Sound name volume # 113 | Functions just as the volume change in music, just substitute Sound and a proper name 114 | ex: Sonud zap volume .2 115 | 116 | Options 117 | Will start the dialog buttons creation with each following line until /Options being a button 118 | 119 | x y h w n 120 | v 121 | Will create a dialog button with the coordinates of x,y and a height of h and width of w. The name is simply the caller id for the 122 | button and won't be visible. The v value is for the string to be displayed inside the button and is on the line below the coordinate data. 123 | ex: 120 320 40 40 Q1 124 | No, I wouldn't want to do that 125 | 126 | /Options 127 | Ends the creation of dialog buttons, if statement should go after (later) 128 | 129 | --Conf (Need a second call of this to go back to normal text) 130 | 131 | --Clear 132 | Clears the current text to display 133 | ex: --Clear 134 | 135 | #### 136 | menu.txt 137 | 138 | Plain text 139 | Will be printed out to the screen, would be much cleaner to do this with a picture through the button system, but here is a simple option. Will use the font loaded in the init.txt 140 | 141 | Button add x y name \path\to\image.png 142 | Will create a button (image) using the path provided, and at the x,y coordinates described. The name will never be displayed, but is used for interals such as pointing to actions to be taken when clicked (such as go to the game) 143 | ex: Button add 200 160 Start char.png 144 | Note: as of now, only Start will do anything (begin the game) but later on there will be more known-names such as options etc. Feel free to hack in your own by looking at the menu.lua file 145 | 146 | Notes: 147 | 148 | When there is text on the screen the rest of the screen will be dimmed for easier reading for the viewer, however 149 | when the text is cleared or not present then the rest of screen will return to normal brightness. 150 | 151 | The music will automatically loop unless otherwise specified 152 | 153 | Sounds are stored in memory, please do not use this for voices if you are planning on voicing more than a couple lines, 154 | instead use the voice stack (soon to be implemented) as it will be easier to handle as well as be more efficient. 155 | 156 | When going to the menu, all music and sounds are paused, and resumed upon leaving it. 157 | 158 | Pressing the 'r' key will re-load all assets for easy testing, will be changed to a debug/development toggle later on. -------------------------------------------------------------------------------- /reader.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- The MIT License (MIT) 3 | 4 | -- Copyright (c) 2014 Andrew Jon Yobs 5 | 6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 7 | -- of this software and associated documentation files (the "Software"), to deal 8 | -- in the Software without restriction, including without limitation the rights 9 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | -- copies of the Software, and to permit persons to whom the Software is 11 | -- furnished to do so, subject to the following conditions: 12 | 13 | -- The above copyright notice and this permission notice shall be included in 14 | -- all copies or substantial portions of the Software. 15 | 16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | -- THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- 25 | -- Visual Novel,Love Book 26 | -- Requires sunclass 27 | local class = require 'sunclass' -- oop functionality 28 | local cLine = 0 -- The current line being read from 29 | local txtDisplay = "" -- The text to display, soon to be changed 30 | local txtLimit = 0 -- The pixel width of text before wrapping 31 | local txtTable = {} -- The table to hold characters for fading in text 32 | local txtAlpha = {} -- Table for alpha values of txtTable 33 | local txtMax = 0 -- The highest count of characters in txtTable 34 | local bg = nil -- The background to be drawn 35 | local characters = {} -- The table of characters in the vn 36 | local backgrounds = {} -- The table of backgrounds in the vn 37 | local width, height = love.graphics.getDimensions() -- The initial height and width of the scren, is changed after loading in init.txt 38 | local line = {} -- The table of lines loaded from files 39 | local br = 255 -- The 'brightness' of to draw non-text 40 | musics = {} -- The table of music to be played 41 | sounds = {} -- The table of sounds to be played 42 | local inpt = true -- Whether the user input will be taken into account 43 | local toFade = {} -- The table of characters to fade alpha values 44 | local toShow = {} -- The table of characters to fade in alpha values 45 | voiceTable = {} -- The table of sound files dedicated to voices 46 | voicePos = 1 -- The voice file of voiceTable to play 47 | voicePlay = false -- Whether voice files should be played or not 48 | local sX = 1 -- The scale coefficient of the x-dimension of the background 49 | local sY = 1 -- The scale coefficient of the y-dimension of the background 50 | local click = 0 -- Click progression the player is at 51 | local Font = love.graphics.newFont(12) -- Default font 52 | local oldTxt = 1 -- Index to ignore when printing 53 | Reader = {} -- The state to be put onto the GameStates stack 54 | 55 | --[[ 56 | --The character class for characters to be drawn, known by a name and given various values for different modifications 57 | --caused by functions later detailed 58 | --]] 59 | local Character = class('Character') 60 | function Character:initialize(name, imagePath) 61 | self.poses = {} 62 | self.name = name -- The name to be called by 63 | self.poses["default"] = love.graphics.newImage(imagePath) -- The default image to display 64 | self.image = self.poses["default"] -- The default image to display 65 | self.x = 0 -- The initial x-position to be drawn from 66 | self.y = height - self.image:getHeight() -- The initial y-position to be drawn from, most likely won't change 67 | self.alpha = 0 -- The default alpha, 0 = invisible 68 | self.fade = 0 -- The direction to fade, 0 = no fade actoin 69 | end 70 | 71 | --[[ 72 | --Adds a 'pose'; another image that can be drawn in the character's place 73 | --@param pose The name for the pose to be called from in the character's pose table 74 | --@param path The image path for the pose to point to 75 | --]] 76 | function Character:addPose(pose, path) 77 | self.poses[pose] = love.graphics.newImage(path) 78 | end 79 | 80 | --[[ 81 | --Draws a character at the alpha of the character 82 | --@param pick The character in the characters table to use 83 | --]] 84 | function Character:Draw() 85 | love.graphics.setColor(br, br, br, self.alpha) 86 | love.graphics.draw(self.image, self.x, self.y) 87 | love.graphics.setColor(br, br, br, 255) 88 | end 89 | 90 | --[[ 91 | --Moves a character to a select position 92 | --@param chr The character to move 93 | --@param pos The position (left, right, middle) to move to the character 94 | --]] 95 | function Character:Move(pos) 96 | self.y = height - self.image:getHeight() 97 | if pos == "right" then 98 | self.x = (.8 * width) - self.image:getWidth() 99 | elseif pos == "left" then 100 | self.x = .2 * width 101 | elseif pos == "middle" then 102 | self.x = (width/2) - (self.image:getWidth()/2) 103 | end 104 | end 105 | 106 | --[[ 107 | --Initializes the sequence for text to fade in, and wraps the text 108 | --]] 109 | function Reader:printInit() 110 | local space = 0 111 | local tmpLength = 0 112 | txtMax = oldTxt 113 | txtLimit = width * .7 114 | for j=oldTxt+1, string.len(txtDisplay) do 115 | ch = string.sub(txtDisplay, j, j) 116 | txtMax = txtMax + 1 117 | txtAlpha[txtMax] = 0 118 | txtTable[txtMax] = ch 119 | tmpLength = tmpLength + Font:getWidth(txtTable[txtMax]) 120 | if tmpLength > txtLimit then -- If the line goes over the limit, go back to last space and make it a line break 121 | txtTable[space] = "\n" 122 | tmpLength = 0 123 | space = space + 1 124 | while space < txtMax do 125 | tmpLength = tmpLength + Font:getWidth(txtTable[space]) 126 | space = space + 1 127 | end 128 | end 129 | if ch == " " then space = txtMax end 130 | end 131 | txtAlpha[1] = 1 132 | end 133 | 134 | --[[ 135 | --Prints out the txtTable 136 | --]] 137 | function Reader:Print() 138 | local x = width * .15 139 | local y = 0 140 | for i=1, txtMax do 141 | if txtTable[i] ~= nil then 142 | if txtTable[i] == "\n" then 143 | x = width * .15 144 | y = y + Font:getHeight("T") 145 | else 146 | love.graphics.setColor(255, 255, 255, txtAlpha[i]) 147 | love.graphics.print(txtTable[i], x, y) 148 | x = x + Font:getWidth(txtTable[i]) 149 | end 150 | end 151 | end 152 | end 153 | 154 | --[[ 155 | --Splits a string into words 156 | --@param s The string to split 157 | --@return The table of words from 0,...length-1 158 | --]] 159 | function Reader:lineSplit(s) 160 | split = {} 161 | o = 0 162 | for i in string.gmatch(s, "%S+") do 163 | split[o] = i 164 | o = o + 1 165 | end 166 | return split 167 | end 168 | 169 | --[[ 170 | -- Interprets called for computing a script block that isn't simply adding text 171 | --]] 172 | function Reader:computeScript() 173 | cLine = cLine + 1 174 | while line[cLine] ~= "--Conf" do 175 | -- split the line and check 176 | lineS = Reader:lineSplit(line[cLine]) 177 | -- Characters 178 | if lineS[0] == "Character" then 179 | if lineS[2] == "move" then 180 | characters[lineS[1]]:Move(lineS[3]) 181 | elseif lineS[2] == "show" then 182 | characters[lineS[1]].fade = 1 183 | --characters[lineS[1]].alpha = 255 184 | elseif lineS[2] == "hide" then 185 | characters[lineS[1]].fade = -1 186 | elseif lineS[2] == "pose" then 187 | characters[lineS[1]].image = characters[lineS[1]].poses[lineS[3]] 188 | end 189 | -- Backgrounds 190 | elseif lineS[0] == "Background" then 191 | if lineS[2] == "set" then 192 | sX = width / backgrounds[lineS[1]]:getWidth() 193 | sY = height / backgrounds[lineS[1]]:getHeight() 194 | bg = backgrounds[lineS[1]] 195 | end 196 | if lineS[1] == "remove" then 197 | bg = nil 198 | end 199 | -- Sounds 200 | elseif lineS[0] == "Sound" then 201 | if lineS[2] == "play" then 202 | sounds[lineS[1]]:play() 203 | elseif lineS[2] == "stop" then 204 | sounds[lineS[1]]:stop() 205 | elseif lineS[2] == "volume" then 206 | sounds[lineS[1]]:setVolume(tonumber(lineS[3])) 207 | end 208 | -- Musics 209 | elseif lineS[0] == "Music" then 210 | if lineS[2] == "play" then 211 | musics[lineS[1]]:play() 212 | elseif lineS[2] == "stop" then 213 | musics[lineS[1]]:stop() 214 | elseif lineS[2] == "volume" then 215 | musics[lineS[1]]:setVolume(tonumber(lineS[3])) 216 | end 217 | -- Options 218 | elseif lineS[0] == "Options" then 219 | dButtons.buttons = {} 220 | Line = cLine + 1 221 | while line[cLine] ~= "/Options" do 222 | lineS = Reader:lineSplit(line[cLine]) 223 | local tmpName = lineS[4] 224 | local tmpButtonData = {tonumber(lineS[0]), tonumber(lineS[1]), tonumber(lineS[2]), tonumber(lineS[3]), lineS[4]} 225 | cLine = cLine + 1 226 | dButtons.buttons[tmpName] = ButtonClass:new(tmpButtonData, line[cLine]) 227 | cLine = cLine + 1 228 | end 229 | GameStates:Push(dButtons) 230 | end 231 | cLine = cLine + 1 232 | end 233 | end 234 | 235 | function Reader:Load() 236 | cLine = 0 -- The current line being read from 237 | txtDisplay = "" -- The text to display, soon to be changed 238 | txtLimit = 0 -- The pixel width of text before wrapping 239 | txtTable = {} -- The table to hold characters for fading in text 240 | txtAlpha = {} -- Table for alpha values of txtTable 241 | txtMax = 0 -- The highest count of characters in txtTable 242 | bg = nil -- The background to be drawn 243 | characters = {} -- The table of characters in the vn 244 | backgrounds = {} -- The table of backgrounds in the vn 245 | width, height = love.graphics.getDimensions() -- The initial height and width of the scren, is changed after loading in init.txt 246 | line = {} -- The table of lines loaded from files 247 | br = 255 -- The 'brightness' of to draw non-text 248 | musics = {} -- The table of music to be played 249 | sounds = {} -- The table of sounds to be played 250 | inpt = true -- Whether the user input will be taken into account 251 | toFade = {} -- The table of characters to fade alpha values 252 | toShow = {} -- The table of characters to fade in alpha values 253 | voiceTable = {} -- The table of sound files dedicated to voices 254 | voicePos = 1 -- The voice file of voiceTable to play 255 | voicePlay = false -- Whether voice files should be played or not 256 | sX = 1 -- The scale coefficient of the x-dimension of the background 257 | sY = 1 -- The scale coefficient of the y-dimension of the background 258 | click = 0 -- Click progression the player is at 259 | Font = love.graphics.newFont(12) -- Default font 260 | oldTxt = 1 -- Index to ignore when printing 261 | for ls in love.filesystem.lines("init.txt") do -- load assets 262 | lineS = Reader:lineSplit(ls) 263 | if lineS[0] == "Character" then 264 | if lineS[2] == "addPose" then 265 | characters[lineS[1]]:addPose(lineS[3], lineS[4]) 266 | else 267 | characters[lineS[1]] = Character:new(lineS[1], lineS[2]) 268 | end 269 | elseif lineS[0] == "Background" then 270 | backgrounds[lineS[1]] = love.graphics.newImage(lineS[2]) 271 | backgrounds[lineS[1]]:setFilter("nearest","nearest", 16) 272 | elseif lineS[0] == "setResolution" then 273 | success = love.window.setMode(lineS[1], lineS[2]) 274 | elseif lineS[0] == "setTitle" then 275 | love.window.setTitle(string.sub(ls, 10)) 276 | -- Sound 277 | elseif lineS[0] == "Sound" then 278 | sounds[lineS[1]] = love.audio.newSource(lineS[2], "static") 279 | -- Music 280 | elseif lineS[0] == "Music" then 281 | musics[lineS[1]] = love.audio.newSource(lineS[2], "stream") 282 | musics[lineS[1]]:setLooping(true) 283 | -- Font 284 | elseif lineS[0] == "Font" then 285 | Font = love.graphics.newFont(tonumber(lineS[2])) 286 | love.graphics.setFont(Font) 287 | -- Voice Table 288 | elseif lineS[0] == "VoiceTable" then 289 | for title, dir in pairs(love.filesystem.getDirectoryItems(lineS[1])) do 290 | voiceTable[title] = love.audio.newSource(lineS[1].."/"..dir, "stream") 291 | end 292 | voicePlay = true 293 | voicePos = 1 294 | -- Window Icon 295 | elseif lineS[0] == "setIcon" then 296 | success = love.window.setIcon(lineS[1]) 297 | end 298 | end 299 | width, height = love.graphics.getDimensions() 300 | ls = nil 301 | for ls in love.filesystem.lines("script.txt") do -- load script into memory 302 | line[cLine] = ls 303 | cLine = cLine + 1 304 | end 305 | lineCount = cLine 306 | cLine = 0 307 | txtDisplay = line[0] 308 | click = 1 309 | end 310 | 311 | function Reader:Update(dt) 312 | -- Fade out action, done in update to make it gradual 313 | for k,character in pairs(characters) do 314 | if character.fade == -1 then 315 | if character.alpha > 0 then 316 | character.alpha = character.alpha - (256 * dt * 2 / 1) -- Total value * delta time * balance / time to die 317 | end 318 | if character.alpha < 0 then 319 | character.alpha = 0 320 | character.fade = 0 321 | end 322 | elseif character.fade == 1 then 323 | if character.alpha < 255 then 324 | character.alpha = character.alpha + (256 * dt * 2 / 1) -- Total value * delta time * balance / time to die 325 | end 326 | if character.alpha > 255 then 327 | character.alpha = 255 328 | character.fade = 0 329 | end 330 | end 331 | end 332 | -- Fade in text 333 | for k = oldTxt, txtMax do 334 | if txtAlpha[k] ~= nil then 335 | if txtAlpha[k] > 0 and txtAlpha[k] < 255 then 336 | txtAlpha[k] = txtAlpha[k] + (256 * dt * 2.6 / 1) 337 | end 338 | if txtAlpha[k] > 255 then txtAlpha[k] = 255 end 339 | if txtAlpha[k] > 40 and txtAlpha[k+1] ~= nil and txtAlpha[k+1] == 0 then 340 | txtAlpha[k+1] = 1 341 | end 342 | end 343 | end 344 | if txtAlpha[txtMax] == 255 then click = 1 end -- If all text is shown, player click should move to new line 345 | end 346 | 347 | --[[ 348 | --Takes user input as moving onto the next chunk of text 349 | --@param x The x-pos of the mouse 350 | --@param y The y-pos of the mouse 351 | --@button Which button is being pressed 352 | --]] 353 | function Reader:Click(x, y, button) 354 | if inpt == true and button == 'r' then 355 | GameStates:Push(Menu) 356 | end 357 | if inpt == true and button == 'l' then 358 | if click == 0 then 359 | -- Finish fade on player demand 360 | for character,k in pairs(characters) do 361 | if k.fade == -1 then 362 | k.alpha = 0 363 | k.fade = 0 364 | elseif k.fade == 1 then 365 | k.alpha = 255 366 | k.fade = 1 367 | end 368 | end 369 | -- Finish fade on text 370 | for j=1, txtMax do 371 | txtAlpha[j] = 255 372 | end 373 | click = 1 374 | else -- Second click, fading et al is done 375 | click = 0 376 | -- Voice Cancel/Refresh 377 | if voicePlay then 378 | if voiceTable[voicePos-1] ~= nil then voiceTable[voicePos-1]:stop() end 379 | if voiceTable[voicePos] ~= nil then voiceTable[voicePos]:play() end 380 | voicePos = voicePos + 1 381 | end 382 | -- Start traversing the next chunk of lines 383 | cLine = cLine + 1 384 | if line[cLine] == "--Clear" then 385 | txtDisplay = "" 386 | oldTxt = 1 387 | y = 0 388 | Reader:printInit() 389 | elseif line[cLine] == "--Conf" then 390 | Reader:computeScript() 391 | elseif line[cLine] ~= nill then 392 | if txtDisplay ~= "" then 393 | oldTxt = txtMax 394 | end 395 | txtDisplay = txtDisplay .. "\n" .."\n" .."\n" .. line[cLine] 396 | Reader:printInit() 397 | end 398 | end 399 | end 400 | end 401 | 402 | function Reader:KeyPress(key, isrepeat) 403 | end 404 | 405 | function Reader:Draw() 406 | -- If there is text being displayed dim the rest of the screen 407 | if txtDisplay ~= "" then 408 | br = 125 409 | else 410 | br = 255 411 | end 412 | -- If there is a background then draw it 413 | if bg ~= nil then 414 | love.graphics.setColor(br, br, br, 255) 415 | love.graphics.draw(bg, 0, 0, 0, sX, sY) 416 | love.graphics.setColor(255, 255, 255, 255) 417 | end 418 | for k,character in pairs(characters) do 419 | character:Draw() 420 | end 421 | Reader:Print() 422 | --love.graphics.print(cLine, 400, 100) 423 | end -------------------------------------------------------------------------------- /VN Example/reader.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- The MIT License (MIT) 3 | 4 | -- Copyright (c) 2014 Andrew Jon Yobs 5 | 6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 7 | -- of this software and associated documentation files (the "Software"), to deal 8 | -- in the Software without restriction, including without limitation the rights 9 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | -- copies of the Software, and to permit persons to whom the Software is 11 | -- furnished to do so, subject to the following conditions: 12 | 13 | -- The above copyright notice and this permission notice shall be included in 14 | -- all copies or substantial portions of the Software. 15 | 16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | -- THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- 25 | -- Visual Novel,Love Book 26 | -- Requires sunclass 27 | local class = require 'sunclass' -- oop functionality 28 | local cLine = 0 -- The current line being read from 29 | local txtDisplay = "" -- The text to display, soon to be changed 30 | local txtLimit = 0 -- The pixel width of text before wrapping 31 | local txtTable = {} -- The table to hold characters for fading in text 32 | local txtAlpha = {} -- Table for alpha values of txtTable 33 | local txtMax = 0 -- The highest count of characters in txtTable 34 | local bg = nil -- The background to be drawn 35 | local characters = {} -- The table of characters in the vn 36 | local backgrounds = {} -- The table of backgrounds in the vn 37 | local width, height = love.graphics.getDimensions() -- The initial height and width of the scren, is changed after loading in init.txt 38 | local line = {} -- The table of lines loaded from files 39 | local br = 255 -- The 'brightness' of to draw non-text 40 | musics = {} -- The table of music to be played 41 | sounds = {} -- The table of sounds to be played 42 | local inpt = true -- Whether the user input will be taken into account 43 | local toFade = {} -- The table of characters to fade alpha values 44 | local toShow = {} -- The table of characters to fade in alpha values 45 | voiceTable = {} -- The table of sound files dedicated to voices 46 | voicePos = 1 -- The voice file of voiceTable to play 47 | voicePlay = false -- Whether voice files should be played or not 48 | local sX = 1 -- The scale coefficient of the x-dimension of the background 49 | local sY = 1 -- The scale coefficient of the y-dimension of the background 50 | local click = 0 -- Click progression the player is at 51 | local Font = love.graphics.newFont(12) -- Default font 52 | local oldTxt = 1 -- Index to ignore when printing 53 | Reader = {} -- The state to be put onto the GameStates stack 54 | 55 | --[[ 56 | --The character class for characters to be drawn, known by a name and given various values for different modifications 57 | --caused by functions later detailed 58 | --]] 59 | local Character = class('Character') 60 | function Character:initialize(name, imagePath) 61 | self.poses = {} 62 | self.name = name -- The name to be called by 63 | self.poses["default"] = love.graphics.newImage(imagePath) -- The default image to display 64 | self.image = self.poses["default"] -- The default image to display 65 | self.x = 0 -- The initial x-position to be drawn from 66 | self.y = height - self.image:getHeight() -- The initial y-position to be drawn from, most likely won't change 67 | self.alpha = 0 -- The default alpha, 0 = invisible 68 | self.fade = 0 -- The direction to fade, 0 = no fade actoin 69 | end 70 | 71 | --[[ 72 | --Adds a 'pose'; another image that can be drawn in the character's place 73 | --@param pose The name for the pose to be called from in the character's pose table 74 | --@param path The image path for the pose to point to 75 | --]] 76 | function Character:addPose(pose, path) 77 | self.poses[pose] = love.graphics.newImage(path) 78 | end 79 | 80 | --[[ 81 | --Draws a character at the alpha of the character 82 | --@param pick The character in the characters table to use 83 | --]] 84 | function Character:Draw() 85 | love.graphics.setColor(br, br, br, self.alpha) 86 | love.graphics.draw(self.image, self.x, self.y) 87 | love.graphics.setColor(br, br, br, 255) 88 | end 89 | 90 | --[[ 91 | --Moves a character to a select position 92 | --@param chr The character to move 93 | --@param pos The position (left, right, middle) to move to the character 94 | --]] 95 | function Character:Move(pos) 96 | self.y = height - self.image:getHeight() 97 | if pos == "right" then 98 | self.x = (.8 * width) - self.image:getWidth() 99 | elseif pos == "left" then 100 | self.x = .2 * width 101 | elseif pos == "middle" then 102 | self.x = (width/2) - (self.image:getWidth()/2) 103 | end 104 | end 105 | 106 | --[[ 107 | --Initializes the sequence for text to fade in, and wraps the text 108 | --]] 109 | function Reader:printInit() 110 | local space = 0 111 | local tmpLength = 0 112 | txtMax = oldTxt 113 | txtLimit = width * .7 114 | for j=oldTxt+1, string.len(txtDisplay) do 115 | ch = string.sub(txtDisplay, j, j) 116 | txtMax = txtMax + 1 117 | txtAlpha[txtMax] = 0 118 | txtTable[txtMax] = ch 119 | tmpLength = tmpLength + Font:getWidth(txtTable[txtMax]) 120 | if tmpLength > txtLimit then -- If the line goes over the limit, go back to last space and make it a line break 121 | txtTable[space] = "\n" 122 | tmpLength = 0 123 | space = space + 1 124 | while space < txtMax do 125 | tmpLength = tmpLength + Font:getWidth(txtTable[space]) 126 | space = space + 1 127 | end 128 | end 129 | if ch == " " then space = txtMax end 130 | end 131 | txtAlpha[1] = 1 132 | end 133 | 134 | --[[ 135 | --Prints out the txtTable 136 | --]] 137 | function Reader:Print() 138 | local x = width * .15 139 | local y = 0 140 | for i=1, txtMax do 141 | if txtTable[i] ~= nil then 142 | if txtTable[i] == "\n" then 143 | x = width * .15 144 | y = y + Font:getHeight("T") 145 | else 146 | love.graphics.setColor(255, 255, 255, txtAlpha[i]) 147 | love.graphics.print(txtTable[i], x, y) 148 | x = x + Font:getWidth(txtTable[i]) 149 | end 150 | end 151 | end 152 | end 153 | 154 | --[[ 155 | --Splits a string into words 156 | --@param s The string to split 157 | --@return The table of words from 0,...length-1 158 | --]] 159 | function Reader:lineSplit(s) 160 | split = {} 161 | o = 0 162 | for i in string.gmatch(s, "%S+") do 163 | split[o] = i 164 | o = o + 1 165 | end 166 | return split 167 | end 168 | 169 | --[[ 170 | -- Interprets called for computing a script block that isn't simply adding text 171 | --]] 172 | function Reader:computeScript() 173 | cLine = cLine + 1 174 | while line[cLine] ~= "--Conf" do 175 | -- split the line and check 176 | lineS = Reader:lineSplit(line[cLine]) 177 | -- Characters 178 | if lineS[0] == "Character" then 179 | if lineS[2] == "move" then 180 | characters[lineS[1]]:Move(lineS[3]) 181 | elseif lineS[2] == "show" then 182 | characters[lineS[1]].fade = 1 183 | --characters[lineS[1]].alpha = 255 184 | elseif lineS[2] == "hide" then 185 | characters[lineS[1]].fade = -1 186 | elseif lineS[2] == "pose" then 187 | characters[lineS[1]].image = characters[lineS[1]].poses[lineS[3]] 188 | end 189 | -- Backgrounds 190 | elseif lineS[0] == "Background" then 191 | if lineS[2] == "set" then 192 | sX = width / backgrounds[lineS[1]]:getWidth() 193 | sY = height / backgrounds[lineS[1]]:getHeight() 194 | bg = backgrounds[lineS[1]] 195 | end 196 | if lineS[1] == "remove" then 197 | bg = nil 198 | end 199 | -- Sounds 200 | elseif lineS[0] == "Sound" then 201 | if lineS[2] == "play" then 202 | sounds[lineS[1]]:play() 203 | elseif lineS[2] == "stop" then 204 | sounds[lineS[1]]:stop() 205 | elseif lineS[2] == "volume" then 206 | sounds[lineS[1]]:setVolume(tonumber(lineS[3])) 207 | end 208 | -- Musics 209 | elseif lineS[0] == "Music" then 210 | if lineS[2] == "play" then 211 | musics[lineS[1]]:play() 212 | elseif lineS[2] == "stop" then 213 | musics[lineS[1]]:stop() 214 | elseif lineS[2] == "volume" then 215 | musics[lineS[1]]:setVolume(tonumber(lineS[3])) 216 | end 217 | -- Options 218 | elseif lineS[0] == "Options" then 219 | dButtons.buttons = {} 220 | Line = cLine + 1 221 | while line[cLine] ~= "/Options" do 222 | lineS = Reader:lineSplit(line[cLine]) 223 | local tmpName = lineS[4] 224 | local tmpButtonData = {tonumber(lineS[0]), tonumber(lineS[1]), tonumber(lineS[2]), tonumber(lineS[3]), lineS[4]} 225 | cLine = cLine + 1 226 | dButtons.buttons[tmpName] = ButtonClass:new(tmpButtonData, line[cLine]) 227 | cLine = cLine + 1 228 | end 229 | GameStates:Push(dButtons) 230 | end 231 | cLine = cLine + 1 232 | end 233 | end 234 | 235 | function Reader:Load() 236 | cLine = 0 -- The current line being read from 237 | txtDisplay = "" -- The text to display, soon to be changed 238 | txtLimit = 0 -- The pixel width of text before wrapping 239 | txtTable = {} -- The table to hold characters for fading in text 240 | txtAlpha = {} -- Table for alpha values of txtTable 241 | txtMax = 0 -- The highest count of characters in txtTable 242 | bg = nil -- The background to be drawn 243 | characters = {} -- The table of characters in the vn 244 | backgrounds = {} -- The table of backgrounds in the vn 245 | width, height = love.graphics.getDimensions() -- The initial height and width of the scren, is changed after loading in init.txt 246 | line = {} -- The table of lines loaded from files 247 | br = 255 -- The 'brightness' of to draw non-text 248 | musics = {} -- The table of music to be played 249 | sounds = {} -- The table of sounds to be played 250 | inpt = true -- Whether the user input will be taken into account 251 | toFade = {} -- The table of characters to fade alpha values 252 | toShow = {} -- The table of characters to fade in alpha values 253 | voiceTable = {} -- The table of sound files dedicated to voices 254 | voicePos = 1 -- The voice file of voiceTable to play 255 | voicePlay = false -- Whether voice files should be played or not 256 | sX = 1 -- The scale coefficient of the x-dimension of the background 257 | sY = 1 -- The scale coefficient of the y-dimension of the background 258 | click = 0 -- Click progression the player is at 259 | Font = love.graphics.newFont(12) -- Default font 260 | oldTxt = 1 -- Index to ignore when printing 261 | for ls in love.filesystem.lines("init.txt") do -- load assets 262 | lineS = Reader:lineSplit(ls) 263 | if lineS[0] == "Character" then 264 | if lineS[2] == "addPose" then 265 | characters[lineS[1]]:addPose(lineS[3], lineS[4]) 266 | else 267 | characters[lineS[1]] = Character:new(lineS[1], lineS[2]) 268 | end 269 | elseif lineS[0] == "Background" then 270 | backgrounds[lineS[1]] = love.graphics.newImage(lineS[2]) 271 | backgrounds[lineS[1]]:setFilter("nearest","nearest", 16) 272 | elseif lineS[0] == "setResolution" then 273 | success = love.window.setMode(lineS[1], lineS[2]) 274 | elseif lineS[0] == "setTitle" then 275 | love.window.setTitle(string.sub(ls, 10)) 276 | -- Sound 277 | elseif lineS[0] == "Sound" then 278 | sounds[lineS[1]] = love.audio.newSource(lineS[2], "static") 279 | -- Music 280 | elseif lineS[0] == "Music" then 281 | musics[lineS[1]] = love.audio.newSource(lineS[2], "stream") 282 | musics[lineS[1]]:setLooping(true) 283 | -- Font 284 | elseif lineS[0] == "Font" then 285 | Font = love.graphics.newFont(tonumber(lineS[2])) 286 | love.graphics.setFont(Font) 287 | -- Voice Table 288 | elseif lineS[0] == "VoiceTable" then 289 | for title, dir in pairs(love.filesystem.getDirectoryItems(lineS[1])) do 290 | voiceTable[title] = love.audio.newSource(lineS[1].."/"..dir, "stream") 291 | end 292 | voicePlay = true 293 | voicePos = 1 294 | -- Window Icon 295 | elseif lineS[0] == "setIcon" then 296 | success = love.window.setIcon(lineS[1]) 297 | end 298 | end 299 | width, height = love.graphics.getDimensions() 300 | ls = nil 301 | for ls in love.filesystem.lines("script.txt") do -- load script into memory 302 | line[cLine] = ls 303 | cLine = cLine + 1 304 | end 305 | lineCount = cLine 306 | cLine = 0 307 | txtDisplay = line[0] 308 | click = 1 309 | end 310 | 311 | function Reader:Update(dt) 312 | -- Fade out action, done in update to make it gradual 313 | for k,character in pairs(characters) do 314 | if character.fade == -1 then 315 | if character.alpha > 0 then 316 | character.alpha = character.alpha - (256 * dt * 2 / 1) -- Total value * delta time * balance / time to die 317 | end 318 | if character.alpha < 0 then 319 | character.alpha = 0 320 | character.fade = 0 321 | end 322 | elseif character.fade == 1 then 323 | if character.alpha < 255 then 324 | character.alpha = character.alpha + (256 * dt * 2 / 1) -- Total value * delta time * balance / time to die 325 | end 326 | if character.alpha > 255 then 327 | character.alpha = 255 328 | character.fade = 0 329 | end 330 | end 331 | end 332 | -- Fade in text 333 | for k = oldTxt, txtMax do 334 | if txtAlpha[k] ~= nil then 335 | if txtAlpha[k] > 0 and txtAlpha[k] < 255 then 336 | txtAlpha[k] = txtAlpha[k] + (256 * dt * 2.6 / 1) 337 | end 338 | if txtAlpha[k] > 255 then txtAlpha[k] = 255 end 339 | if txtAlpha[k] > 40 and txtAlpha[k+1] ~= nil and txtAlpha[k+1] == 0 then 340 | txtAlpha[k+1] = 1 341 | end 342 | end 343 | end 344 | if txtAlpha[txtMax] == 255 then click = 1 end -- If all text is shown, player click should move to new line 345 | end 346 | 347 | --[[ 348 | --Takes user input as moving onto the next chunk of text 349 | --@param x The x-pos of the mouse 350 | --@param y The y-pos of the mouse 351 | --@button Which button is being pressed 352 | --]] 353 | function Reader:Click(x, y, button) 354 | if inpt == true and button == 'r' then 355 | GameStates:Push(Menu) 356 | end 357 | if inpt == true and button == 'l' then 358 | if click == 0 then 359 | -- Finish fade on player demand 360 | for character,k in pairs(characters) do 361 | if k.fade == -1 then 362 | k.alpha = 0 363 | k.fade = 0 364 | elseif k.fade == 1 then 365 | k.alpha = 255 366 | k.fade = 1 367 | end 368 | end 369 | -- Finish fade on text 370 | for j=1, txtMax do 371 | txtAlpha[j] = 255 372 | end 373 | click = 1 374 | else -- Second click, fading et al is done 375 | click = 0 376 | -- Voice Cancel/Refresh 377 | if voicePlay then 378 | if voiceTable[voicePos-1] ~= nil then voiceTable[voicePos-1]:stop() end 379 | if voiceTable[voicePos] ~= nil then voiceTable[voicePos]:play() end 380 | voicePos = voicePos + 1 381 | end 382 | -- Start traversing the next chunk of lines 383 | cLine = cLine + 1 384 | if line[cLine] == "--Clear" then 385 | txtDisplay = "" 386 | oldTxt = 1 387 | y = 0 388 | Reader:printInit() 389 | elseif line[cLine] == "--Conf" then 390 | Reader:computeScript() 391 | elseif line[cLine] ~= nill then 392 | if txtDisplay ~= "" then 393 | oldTxt = txtMax 394 | end 395 | txtDisplay = txtDisplay .. "\n" .."\n" .."\n" .. line[cLine] 396 | Reader:printInit() 397 | end 398 | end 399 | end 400 | end 401 | 402 | function Reader:KeyPress(key, isrepeat) 403 | end 404 | 405 | function Reader:Draw() 406 | -- If there is text being displayed dim the rest of the screen 407 | if txtDisplay ~= "" then 408 | br = 125 409 | else 410 | br = 255 411 | end 412 | -- If there is a background then draw it 413 | if bg ~= nil then 414 | love.graphics.setColor(br, br, br, 255) 415 | love.graphics.draw(bg, 0, 0, 0, sX, sY) 416 | love.graphics.setColor(255, 255, 255, 255) 417 | end 418 | for k,character in pairs(characters) do 419 | character:Draw() 420 | end 421 | Reader:Print() 422 | --love.graphics.print(cLine, 400, 100) 423 | end --------------------------------------------------------------------------------