Initial Commit - 1.0.0k

This commit is contained in:
2024-02-27 23:47:25 +08:00
commit 9d14fb1dcb
197 changed files with 99467 additions and 0 deletions

107
engine/animatedsprite.lua Normal file
View File

@@ -0,0 +1,107 @@
--Class
AnimatedSprite = Sprite:extend()
--Class Methods
function AnimatedSprite:init(X, Y, W, H, new_sprite_atlas, sprite_pos)
Sprite.init(self,X, Y, W, H, new_sprite_atlas, sprite_pos)
self.offset = {x = 0, y = 0}
table.insert(G.ANIMATIONS, self)
if getmetatable(self) == AnimatedSprite then
table.insert(G.I.SPRITE, self)
end
end
function AnimatedSprite:rescale()
self.scale_mag = math.min(self.scale.x/self.T.w,self.scale.y/self.T.h)
end
function AnimatedSprite:reset()
self.atlas = G.ANIMATION_ATLAS[self.atlas.name]
self:set_sprite_pos({x = self.animation.x, y = self.animation.y})
end
function AnimatedSprite:set_sprite_pos(sprite_pos)
self.animation = {
x= sprite_pos and sprite_pos.x or 0,
y=sprite_pos and sprite_pos.y or 0,
frames=self.atlas.frames,current=0,
w=self.scale.x, h=self.scale.y}
self.frame_offset = 0
self.current_animation = {
current = 0,
frames = self.animation.frames,
w = self.animation.w,
h = self.animation.h}
self.image_dims = self.image_dims or {}
self.image_dims[1], self.image_dims[2] = self.atlas.image:getDimensions()
self.sprite = love.graphics.newQuad(
0,
self.animation.h*self.animation.y,
self.animation.w,
self.animation.h,
self.image_dims[1], self.image_dims[2])
self.offset_seconds = G.TIMERS.REAL
end
function AnimatedSprite:get_pos_pixel()
self.RETS.get_pos_pixel = self.RETS.get_pos_pixel or {}
self.RETS.get_pos_pixel[1] = self.current_animation.current
self.RETS.get_pos_pixel[2] = self.animation.y
self.RETS.get_pos_pixel[3] = self.animation.w
self.RETS.get_pos_pixel[4] = self.animation.h
return self.RETS.get_pos_pixel
end
function AnimatedSprite:draw_self()
if not self.states.visible then return end
prep_draw(self, 1)
love.graphics.scale(1/self.scale_mag)
love.graphics.setColor(G.C.WHITE)
love.graphics.draw(
self.atlas.image,
self.sprite,
0 ,0,
0,
self.VT.w/(self.T.w),
self.VT.h/(self.T.h)
)
love.graphics.pop()
end
function AnimatedSprite:animate()
local new_frame = math.floor(G.ANIMATION_FPS*(G.TIMERS.REAL - self.offset_seconds))%self.current_animation.frames
if new_frame ~= self.current_animation.current then
self.current_animation.current = new_frame
self.frame_offset = math.floor(self.animation.w*(self.current_animation.current))
self.sprite:setViewport(
self.frame_offset,
self.animation.h*self.animation.y,
self.animation.w,
self.animation.h)
end
if self.float then
self.T.r = 0.02*math.sin(2*G.TIMERS.REAL+self.T.x)
self.offset.y = -(1+0.3*math.sin(0.666*G.TIMERS.REAL+self.T.y))*self.shadow_parrallax.y
self.offset.x = -(0.7+0.2*math.sin(0.666*G.TIMERS.REAL+self.T.x))*self.shadow_parrallax.x
end
end
function AnimatedSprite:remove()
for _, v in pairs(G.ANIMATIONS) do
if v == self then
table.remove(G.ANIMATIONS, k)
end
end
for _, v in pairs(G.I.SPRITE) do
if v == self then
table.remove(G.I.SPRITE, k)
end
end
Sprite.remove(self)
end

1380
engine/controller.lua Normal file

File diff suppressed because it is too large Load Diff

195
engine/event.lua Normal file
View File

@@ -0,0 +1,195 @@
--Class
Event = Object:extend()
--Class Methods
function Event:init(config)
self.trigger = config.trigger or 'immediate'
if config.blocking ~= nil then
self.blocking = config.blocking
else
self.blocking = true
end
if config.blockable ~= nil then
self.blockable = config.blockable
else
self.blockable = true
end
self.complete = false
self.start_timer = config.start_timer or false
self.func = config.func or function() return true end
self.delay = config.delay or 0
self.no_delete = config.no_delete
self.created_on_pause = config.pause_force or G.SETTINGS.paused
self.timer = config.timer or (self.created_on_pause and 'REAL') or 'TOTAL'
if self.trigger == 'ease' then
self.ease = {
type = config.ease or 'lerp',
ref_table = config.ref_table,
ref_value = config.ref_value,
start_val = config.ref_table[config.ref_value],
end_val = config.ease_to,
start_time = nil,
end_time = nil,
}
self.func = config.func or function(t) return t end
end
if self.trigger == 'condition' then
self.condition = {
ref_table = config.ref_table,
ref_value = config.ref_value,
stop_val = config.stop_val,
}
self.func = config.func or function() return self.condition.ref_table[self.condition.ref_value] == self.condition.stop_val end
end
self.time = G.TIMERS[self.timer]
end
function Event:handle(_results)
_results.blocking, _results.completed = self.blocking, self.complete
if self.created_on_pause == false and G.SETTINGS.paused then _results.pause_skip = true; return end
if not self.start_timer then self.time = G.TIMERS[self.timer]; self.start_timer = true end
if self.trigger == 'after' then
if self.time + self.delay <= G.TIMERS[self.timer] then
_results.time_done = true
_results.completed = self.func()
end
end
if self.trigger == 'ease' then
if not self.ease.start_time then
self.ease.start_time = G.TIMERS[self.timer]
self.ease.end_time = G.TIMERS[self.timer] + self.delay
self.ease.start_val = self.ease.ref_table[self.ease.ref_value]
end
if not self.complete then
if self.ease.end_time >= G.TIMERS[self.timer] then
local percent_done = ((self.ease.end_time - G.TIMERS[self.timer])/(self.ease.end_time - self.ease.start_time))
if self.ease.type == 'lerp' then
self.ease.ref_table[self.ease.ref_value] = self.func(percent_done*self.ease.start_val + (1-percent_done)*self.ease.end_val)
end
if self.ease.type == 'elastic' then
percent_done = -math.pow(2, 10 * percent_done - 10) * math.sin((percent_done * 10 - 10.75) * 2*math.pi/3);
self.ease.ref_table[self.ease.ref_value] = self.func(percent_done*self.ease.start_val + (1-percent_done)*self.ease.end_val)
end
if self.ease.type == 'quad' then
percent_done = percent_done * percent_done;
self.ease.ref_table[self.ease.ref_value] = self.func(percent_done*self.ease.start_val + (1-percent_done)*self.ease.end_val)
end
else
self.ease.ref_table[self.ease.ref_value] = self.func(self.ease.end_val)
self.complete = true
_results.completed = true
_results.time_done = true
end
end
end
if self.trigger == 'condition' then
if not self.complete then _results.completed = self.func() end
_results.time_done = true
end
if self.trigger == 'before' then
if not self.complete then _results.completed = self.func() end
if self.time + self.delay <= G.TIMERS[self.timer] then
_results.time_done = true
end
end
if self.trigger == 'immediate' then
_results.completed = self.func()
_results.time_done = true
end
if _results.completed then self.complete = true end
end
--Class
EventManager = Object:extend()
--Class Methods
function EventManager:init()
self.queues = {
unlock = {},
base ={},
tutorial = {},
achievement = {},
other = {}
}
self.queue_timer = G.TIMERS.REAL
self.queue_dt = 1/60
self.queue_last_processed = G.TIMERS.REAL
end
function EventManager:add_event(event, queue, front)
queue = queue or 'base'
if event:is(Event) then
if front then
table.insert(self.queues[queue], 1, event)
else
self.queues[queue][#self.queues[queue]+1] = event
end
end
end
function EventManager:clear_queue(queue, exception)
if not queue then
--clear all queues
for k, v in pairs(self.queues) do
local i=1
while i <= #v do
if not v[i].no_delete then
table.remove(v, i)
else
i = i + 1
end
end
end
elseif exception then --clear all but exception
for k, v in pairs(self.queues) do
if k ~= exception then
local i=1
while i <= #v do
if not v[i].no_delete then
table.remove(v, i)
else
i = i + 1
end
end
end
end
else
local i=1
while i <= #self.queues[queue] do
if not self.queues[queue][i].no_delete then
table.remove(self.queues[queue], i)
else
i = i + 1
end
end
end
end
function EventManager:update(dt, forced)
self.queue_timer = self.queue_timer+dt
if self.queue_timer >= self.queue_last_processed + self.queue_dt or forced then
self.queue_last_processed = self.queue_last_processed + (forced and 0 or self.queue_dt)
for k, v in pairs(self.queues) do
local blocked = false
local i=1
while i <= #v do
G.ARGS.event_manager_update = G.ARGS.event_manager_update or {}
local results = G.ARGS.event_manager_update
results.blocking, results.completed, results.time_done, results.pause_skip = false, false, false, false
if (not blocked or not v[i].blockable) then v[i]:handle(results) end
if results.pause_skip then
i = i + 1
else if not blocked and results.blocking then blocked = true end
if results.completed and results.time_done then
table.remove(v, i)
else
i = i + 1
end
end
end
end
end
end

23
engine/http_manager.lua Normal file
View File

@@ -0,0 +1,23 @@
require "love.system"
HTTPS = require('https')
local httpencode = function(str)
str = str..''
local char_to_hex = function(c)
return string.format("%%%02X", string.byte(c))
end
str = str:gsub("\n", "\r\n"):gsub("([^%w _%%%-%.~])", char_to_hex):gsub(" ", "+")
return str
end
if (love.system.getOS() == 'OS X' )and (jit.arch == 'arm64' or jit.arch == 'arm' or true) then jit.off() end
IN_CHANNEL = love.thread.getChannel("http_request")
OUT_CHANNEL = love.thread.getChannel("http_response")
while true do
--Monitor the channel for any new requests
local request = IN_CHANNEL:demand() -- Value from channel
if request then
end
end

502
engine/moveable.lua Normal file
View File

@@ -0,0 +1,502 @@
---@class Moveable: Node
Moveable = Node:extend()
--Moveable represents any game object that has the ability to move about the gamespace.\
--All Moveables have a T (transform) that describes their desired transform in game units, as\
--well as a VT (Visible Transform) that eases to T over time. This allows for simplified movement where\
--we only need to set T.x, T.y, etc. to their final position and the engine will ensure the Moveable\
--VT eases to that final location, regargless of any events or timing.
--
---@param args {T: table, container: Node}
--**T** The transform ititializer, with keys of x|1, y|2, w|3, h|4, r|5\
--**container** optional container for this Node, defaults to G.ROOM
function Moveable:init(X,Y,W,H)
local args = (type(X) == 'table') and X or {T ={X or 0,Y or 0,W or 0,H or 0}}
Node.init(self, args)
--The Visible transform is initally set to the same values as the transform T.
--Note that the VT has an extra 'scale' factor, this is used to manipulate the center-adjusted
--scale of any objects that need to be drawn larger or smaller
self.VT = {
x = self.T.x,
y = self.T.y,
w = self.T.w,
h = self.T.h,
r = self.T.r,
scale = self.T.scale
}
--To determine location of VT, we need to keep track of the velocity of VT as it approaches T for the next frame
self.velocity = {x = 0, y = 0, r = 0, scale = 0, mag = 0}
--For more robust drawing, attaching, movement and fewer redundant movement calculations, Moveables each have a 'role'
--that describes a heirarchy of move() calls. Any Moveables with 'Major' role type behave normally, essentially recalculating their
--VT every frame to ensure smooth movement. Moveables can be set to 'Minor' role and attached to some 'Major' moveable
--to weld the Minor moveable to the Major moveable. This makes the dependent moveable set their T and VT to be equal to
--the corresponding 'Major' T and VT, plus some defined offset.
--For finer control over what parts of T and VT are inherited, xy_bond, wh_bond, and r_bond can be set to one of
--'Strong' or 'Weak'. Strong simply copies the values, Weak allows the 'Minor' moveable to calculate their own.
self.role = {
role_type = 'Major', --Major dictates movement, Minor is welded to some major
offset = {x = 0, y = 0}, --Offset from Minor to Major
major = nil,
draw_major = self,
xy_bond = 'Strong',
wh_bond = 'Strong',
r_bond = 'Strong',
scale_bond = 'Strong'
}
self.alignment = {
type = 'a',
offset = {x = 0, y = 0},
prev_type = '',
prev_offset = {x = 0, y = 0},
}
--the pinch table is used to modify the VT.w and VT.h compared to T.w and T.h. If either x or y pinch is
--set to true, the VT width and or height will ease to 0. If pinch is false, they ease to T.w or T.h
self.pinch = {x = false, y = false}
--Keep track of the last time this Moveable was moved via :move(dt). When it is successfully moved, set to equal
--the current G.TIMERS.REAL, and if it is called again this frame, doesn't recalculate move(dt)
self.last_moved = -1
self.last_aligned = -1
self.static_rotation = false
self.offset = {x=0, y=0}
self.Mid = self
self.shadow_parrallax = {x = 0, y = -1.5}
self.layered_parallax = {x = 0, y = 0}
self.shadow_height = 0.2
self:calculate_parrallax()
table.insert(G.MOVEABLES, self)
if getmetatable(self) == Moveable then
table.insert(G.I.MOVEABLE, self)
end
end
function Moveable:draw()
Node.draw(self)
self:draw_boundingrect()
end
--Sets the alignment of moveable using roles
--
---@param args {major: Moveable, bond: string, offset: table, type: string}
--**major** The moveable this moveable will attach to\
--**bond** The bond type, either 'Strong' or 'Weak'. Strong instantly adjusts VT, Weak manually calculates VT changes\
--**offset** {x , y} offset from the alignment\
--**type** the alignment type. Vertical options: c - center, t - top, b - bottom. Horizontal options: l - left, m - middle, r - right. i for inner
function Moveable:set_alignment(args)
args = args or {}
if args.major then
self:set_role({
role_type = 'Minor',
major = args.major,
xy_bond = args.bond or args.xy_bond or 'Weak',
wh_bond = args.wh_bond or self.role.wh_bond,
r_bond = args.r_bond or self.role.r_bond,
scale_bond = args.scale_bond or self.role.scale_bond,
})
end
self.alignment.type = args.type or self.alignment.type
if args.offset and (type(args.offset)=='table' and not (args.offset.y and args.offset.x)) or type(args.offset) ~= 'table' then
args.offset = nil
end
self.alignment.offset = args.offset or self.alignment.offset
end
function Moveable:align_to_major()
if self.alignment.type ~= self.alignment.prev_type then
self.alignment.type_list = {
a = self.alignment.type == 'a',
m = string.find(self.alignment.type, "m"),
c = string.find(self.alignment.type, "c"),
b = string.find(self.alignment.type, "b"),
t = string.find(self.alignment.type, "t"),
l = string.find(self.alignment.type, "l"),
r = string.find(self.alignment.type, "r"),
i = string.find(self.alignment.type, "i"),
}
end
if self.alignment.prev_offset.x == self.alignment.offset.x and
self.alignment.prev_offset.y == self.alignment.offset.y and
self.alignment.prev_type == self.alignment.type then return end
self.NEW_ALIGNMENT = true
if self.alignment.type ~= self.alignment.prev_type then
self.alignment.prev_type = self.alignment.type
end
if self.alignment.type_list.a or not self.role.major then return end
if self.alignment.type_list.m then
self.role.offset.x = 0.5*self.role.major.T.w - (self.Mid.T.w)/2 + self.alignment.offset.x - self.Mid.T.x + self.T.x
end
if self.alignment.type_list.c then
self.role.offset.y = 0.5*self.role.major.T.h - (self.Mid.T.h)/2 + self.alignment.offset.y - self.Mid.T.y + self.T.y
end
if self.alignment.type_list.b then
if self.alignment.type_list.i then
self.role.offset.y = self.alignment.offset.y + self.role.major.T.h - self.T.h
else
self.role.offset.y = self.alignment.offset.y + self.role.major.T.h
end
end
if self.alignment.type_list.r then
if self.alignment.type_list.i then
self.role.offset.x = self.alignment.offset.x + self.role.major.T.w - self.T.w
else
self.role.offset.x = self.alignment.offset.x + self.role.major.T.w
end
end
if self.alignment.type_list.t then
if self.alignment.type_list.i then
self.role.offset.y = self.alignment.offset.y
else
self.role.offset.y = self.alignment.offset.y - self.T.h
end
end
if self.alignment.type_list.l then
if self.alignment.type_list.i then
self.role.offset.x = self.alignment.offset.x
else
self.role.offset.x = self.alignment.offset.x - self.T.w
end
end
self.role.offset.x = self.role.offset.x or 0
self.role.offset.y = self.role.offset.y or 0
self.T.x = self.role.major.T.x + self.role.offset.x
self.T.y = self.role.major.T.y + self.role.offset.y
self.alignment.prev_offset = self.alignment.prev_offset or {}
self.alignment.prev_offset.x, self.alignment.prev_offset.y = self.alignment.offset.x, self.alignment.offset.y
end
function Moveable:hard_set_T(X, Y, W, H)
self.T.x = X
self.T.y = Y
self.T.w = W
self.T.h = H
self.velocity.x = 0
self.velocity.y = 0
self.velocity.r = 0
self.velocity.scale = 0
self.VT.x = X
self.VT.y = Y
self.VT.w = W
self.VT.h = H
self.VT.r = self.T.r
self.VT.scale = self.T.scale
self:calculate_parrallax()
end
function Moveable:hard_set_VT()
self.VT.x = self.T.x
self.VT.y = self.T.y
self.VT.w = self.T.w
self.VT.h = self.T.h
end
function Moveable:drag(offset)
if self.states.drag.can or offset then
self.ARGS.drag_cursor_trans = self.ARGS.drag_cursor_trans or {}
self.ARGS.drag_translation = self.ARGS.drag_translation or {}
local _p = self.ARGS.drag_cursor_trans
local _t = self.ARGS.drag_translation
_p.x = G.CONTROLLER.cursor_position.x/(G.TILESCALE*G.TILESIZE)
_p.y = G.CONTROLLER.cursor_position.y/(G.TILESCALE*G.TILESIZE)
_t.x, _t.y = -self.container.T.w/2, -self.container.T.h/2
point_translate(_p, _t)
point_rotate(_p, self.container.T.r)
_t.x, _t.y = self.container.T.w/2-self.container.T.x, self.container.T.h/2-self.container.T.y
point_translate(_p, _t)
if not offset then
offset = self.click_offset
end
self.T.x = _p.x - offset.x
self.T.y = _p.y - offset.y
self.NEW_ALIGNMENT = true
for k, v in pairs(self.children) do
v:drag(offset)
end
end
if self.states.drag.can then
Node.drag(self)
end
end
function Moveable:juice_up(amount, rot_amt)
local amount = amount or 0.4
local end_time = G.TIMERS.REAL + 0.4
local start_time = G.TIMERS.REAL
self.juice = {
scale = 0,
scale_amt = amount,
r = 0,
r_amt = ((rot_amt or pseudorandom_element({0.6*amount, -0.6*amount})) or 0),
start_time = start_time,
end_time = end_time
}
self.VT.scale = 1-0.6*amount
end
function Moveable:move_juice(dt)
if self.juice and not self.juice.handled_elsewhere then
if self.juice.end_time < G.TIMERS.REAL then
self.juice = nil
else
self.juice.scale = self.juice.scale_amt*math.sin(50.8*(G.TIMERS.REAL-self.juice.start_time))*math.max(0, ((self.juice.end_time - G.TIMERS.REAL)/(self.juice.end_time - self.juice.start_time))^3)
self.juice.r = self.juice.r_amt*math.sin(40.8*(G.TIMERS.REAL-self.juice.start_time))*math.max(0, ((self.juice.end_time - G.TIMERS.REAL)/(self.juice.end_time - self.juice.start_time))^2)
end
end
end
function Moveable:move(dt)
if self.FRAME.MOVE >= G.FRAMES.MOVE then return end
self.FRAME.MAJOR = nil
self.FRAME.MOVE = G.FRAMES.MOVE
if not self.created_on_pause and G.SETTINGS.paused then return end
--WHY ON EARTH DOES THIS LINE MAKE IT RUN 2X AS FAST???
-------------------------------------------------------
local timestart = love.timer.getTime()
-------------------------------------------------------
self:align_to_major()
self.CALCING = nil
if self.role.role_type == 'Glued' then
if self.role.major then self:glue_to_major(self.role.major) end
elseif self.role.role_type == 'Minor' and self.role.major then
if self.role.major.FRAME.MOVE < G.FRAMES.MOVE then self.role.major:move(dt) end
self.STATIONARY = self.role.major.STATIONARY
if (not self.STATIONARY) or self.NEW_ALIGNMENT or
self.config.refresh_movement or
self.juice or
self.role.xy_bond == 'Weak' or
self.role.r_bond == 'Weak' then
self.CALCING = true
self:move_with_major(dt)
end
elseif self.role.role_type == 'Major' then
self.STATIONARY = true
self:move_juice(dt)
self:move_xy(dt)
self:move_r(dt, self.velocity)
self:move_scale(dt)
self:move_wh(dt)
self:calculate_parrallax()
end
self.NEW_ALIGNMENT = false
end
function Moveable:glue_to_major(major_tab)
self.T = major_tab.T
self.VT.x = major_tab.VT.x + (0.5*(1 - major_tab.VT.w/(major_tab.T.w))*self.T.w)
self.VT.y = major_tab.VT.y
self.VT.w = major_tab.VT.w
self.VT.h = major_tab.VT.h
self.VT.r = major_tab.VT.r
self.VT.scale = major_tab.VT.scale
self.pinch = major_tab.pinch
self.shadow_parrallax = major_tab.shadow_parrallax
end
function Moveable:move_with_major(dt)
if self.role.role_type ~= 'Minor' then return end
local major_tab = self.role.major:get_major()
self:move_juice(dt)
if not MWM then
MWM = {
rotated_offset = {},
angles = {},
WH = {},
offs = {},
}
end
if self.role.r_bond == 'Weak' then
MWM.rotated_offset.x, MWM.rotated_offset.y = self.role.offset.x + major_tab.offset.x,self.role.offset.y+major_tab.offset.y
else
if major_tab.major.VT.r < 0.0001 and major_tab.major.VT.r > -0.0001 then
MWM.rotated_offset.x = self.role.offset.x + major_tab.offset.x
MWM.rotated_offset.y = self.role.offset.y + major_tab.offset.y
else
MWM.angles.cos, MWM.angles.sin = math.cos(major_tab.major.VT.r),math.sin(major_tab.major.VT.r)
MWM.WH.w, MWM.WH.h = -self.T.w/2 + major_tab.major.T.w/2,-self.T.h/2 + major_tab.major.T.h/2
MWM.offs.x, MWM.offs.y = self.role.offset.x + major_tab.offset.x - MWM.WH.w,self.role.offset.y + major_tab.offset.y - MWM.WH.h
MWM.rotated_offset.x = MWM.offs.x*MWM.angles.cos - MWM.offs.y*MWM.angles.sin + MWM.WH.w
MWM.rotated_offset.y = MWM.offs.x*MWM.angles.sin + MWM.offs.y*MWM.angles.cos + MWM.WH.h
end
end
self.T.x = major_tab.major.T.x + MWM.rotated_offset.x
self.T.y = major_tab.major.T.y + MWM.rotated_offset.y
if self.role.xy_bond == 'Strong' then
self.VT.x = major_tab.major.VT.x + MWM.rotated_offset.x
self.VT.y = major_tab.major.VT.y + MWM.rotated_offset.y
elseif self.role.xy_bond == 'Weak' then
self:move_xy(dt)
end
if self.role.r_bond == 'Strong' then
self.VT.r = self.T.r + major_tab.major.VT.r + (self.juice and self.juice.r or 0)
elseif self.role.r_bond == 'Weak' then
self:move_r(dt, self.velocity)
end
if self.role.scale_bond == 'Strong' then
self.VT.scale = self.T.scale*(major_tab.major.VT.scale/major_tab.major.T.scale) + (self.juice and self.juice.scale or 0)
elseif self.role.scale_bond == 'Weak' then
self:move_scale(dt)
end
if self.role.wh_bond == 'Strong' then
self.VT.x = self.VT.x + (0.5*(1 - major_tab.major.VT.w/(major_tab.major.T.w))*self.T.w)
self.VT.w = (self.T.w)*(major_tab.major.VT.w/major_tab.major.T.w)
self.VT.h = (self.T.h)*(major_tab.major.VT.h/major_tab.major.T.h)
elseif self.role.wh_bond == 'Weak' then
self:move_wh(dt)
end
self:calculate_parrallax()
end
function Moveable:move_xy(dt)
if (self.T.x ~= self.VT.x or math.abs(self.velocity.x) > 0.01) or
(self.T.y ~= self.VT.y or math.abs(self.velocity.y) > 0.01) then
self.velocity.x = G.exp_times.xy*self.velocity.x + (1-G.exp_times.xy)*(self.T.x - self.VT.x)*35*dt
self.velocity.y = G.exp_times.xy*self.velocity.y + (1-G.exp_times.xy)*(self.T.y - self.VT.y)*35*dt
if self.velocity.x*self.velocity.x + self.velocity.y*self.velocity.y > G.exp_times.max_vel*G.exp_times.max_vel then
local actual_vel = math.sqrt(self.velocity.x*self.velocity.x + self.velocity.y*self.velocity.y)
self.velocity.x = G.exp_times.max_vel*self.velocity.x/actual_vel
self.velocity.y = G.exp_times.max_vel*self.velocity.y/actual_vel
end
self.STATIONARY = false
self.VT.x = self.VT.x + self.velocity.x
self.VT.y = self.VT.y + self.velocity.y
if math.abs(self.VT.x - self.T.x) < 0.01 and math.abs(self.velocity.x) < 0.01 then self.VT.x = self.T.x; self.velocity.x = 0 end
if math.abs(self.VT.y - self.T.y) < 0.01 and math.abs(self.velocity.y) < 0.01 then self.VT.y = self.T.y; self.velocity.y = 0 end
end
end
function Moveable:move_scale(dt)
local des_scale = self.T.scale + (self.zoom and ((self.states.drag.is and 0.1 or 0) + (self.states.hover.is and 0.05 or 0)) or 0) + (self.juice and self.juice.scale or 0)
if des_scale ~= self.VT.scale or
math.abs(self.velocity.scale) > 0.001 then
self.STATIONARY = false
self.velocity.scale = G.exp_times.scale*self.velocity.scale + (1-G.exp_times.scale)*(des_scale - self.VT.scale)
self.VT.scale = self.VT.scale + self.velocity.scale
end
end
function Moveable:move_wh(dt)
if (self.T.w ~= self.VT.w and not self.pinch.x) or
(self.T.h ~= self.VT.h and not self.pinch.y) or
(self.VT.w > 0 and self.pinch.x) or
(self.VT.h > 0 and self.pinch.y) then
self.STATIONARY = false
self.VT.w = self.VT.w + (8*dt)*(self.pinch.x and -1 or 1)*self.T.w
self.VT.h = self.VT.h + (8*dt)*(self.pinch.y and -1 or 1)*self.T.h
self.VT.w = math.max(math.min(self.VT.w, self.T.w), 0)
self.VT.h = math.max(math.min(self.VT.h, self.T.h), 0)
end
end
function Moveable:move_r(dt, vel)
local des_r = self.T.r +0.015*vel.x/dt + (self.juice and self.juice.r*2 or 0)
if des_r ~= self.VT.r or
math.abs(self.velocity.r) > 0.001 then
self.STATIONARY = false
self.velocity.r = G.exp_times.r*self.velocity.r + (1-G.exp_times.r)*(des_r - self.VT.r)
self.VT.r = self.VT.r + self.velocity.r
end
if math.abs(self.VT.r - self.T.r) < 0.001 and math.abs(self.velocity.r) < 0.001 then self.VT.r = self.T.r; self.velocity.r = 0 end
end
function Moveable:calculate_parrallax()
if not G.ROOM then return end
self.shadow_parrallax.x = (self.T.x + self.T.w/2 - G.ROOM.T.w/2)/(G.ROOM.T.w/2)*1.5
end
function Moveable:set_role(args)
if args.major and not args.major.set_role then return end
if args.offset and (type(args.offset)=='table' and not (args.offset.y and args.offset.x)) or type(args.offset) ~= 'table' then
args.offset = nil
end
self.role = {
role_type = args.role_type or self.role.role_type,
offset = args.offset or self.role.offset,
major = args.major or self.role.major,
xy_bond = args.xy_bond or self.role.xy_bond,
wh_bond = args.wh_bond or self.role.wh_bond,
r_bond = args.r_bond or self.role.r_bond,
scale_bond = args.scale_bond or self.role.scale_bond,
draw_major = args.draw_major or self.role.draw_major,
}
if self.role.role_type == 'Major' then self.role.major = nil end
end
function Moveable:get_major()
if ( self.role.role_type ~= 'Major' and self.role.major ~= self) and (self.role.xy_bond ~= 'Weak' and self.role.r_bond ~= 'Weak') then
--First, does the major already have their offset precalculated for this frame?
if not self.FRAME.MAJOR or (G.REFRESH_FRAME_MAJOR_CACHE) then
self.FRAME.MAJOR = EMPTY(self.FRAME.MAJOR)
local major = self.role.major:get_major()
self.FRAME.MAJOR.major = major.major
self.FRAME.MAJOR.offset = self.FRAME.MAJOR.offset or {}
self.FRAME.MAJOR.offset.x, self.FRAME.MAJOR.offset.y = major.offset.x + self.role.offset.x + self.layered_parallax.x, major.offset.y + self.role.offset.y + self.layered_parallax.y
end
return self.FRAME.MAJOR
else
self.ARGS.get_major = self.ARGS.get_major or {}
self.ARGS.get_major.major = self
self.ARGS.get_major.offset = self.ARGS.get_major.offset or {}
self.ARGS.get_major.offset.x, self.ARGS.get_major.offset.y = 0,0
return self.ARGS.get_major
end
end
function Moveable:remove()
for k, v in ipairs(G.MOVEABLES) do
if v == self then
table.remove(G.MOVEABLES, k)
end
end
for k, v in ipairs(G.I.MOVEABLE) do
if v == self then
table.remove(G.I.MOVEABLE, k)
end
end
Node.remove(self)
end

389
engine/node.lua Normal file
View File

@@ -0,0 +1,389 @@
---@class Node
Node = Object:extend()
--Node represent any game object that needs to have some transform available in the game itself.\
--Everything that you see in the game is a Node, and some invisible things like the G.ROOM are also\
--represented here.
--
---@param args {T: table, container: Node}
--**T** The transform ititializer, with keys of x|1, y|2, w|3, h|4, r|5\
--**container** optional container for this Node, defaults to G.ROOM
function Node:init(args)
--From args, set the values of self transform
args = args or {}
args.T = args.T or {}
--Store all argument and return tables here for reuse, because Lua likes to generate garbage
self.ARGS = self.ARGS or {}
self.RETS = {}
--Config table used for any metadata about this node
self.config = self.config or {}
--For transform init, accept params in the form x|1, y|2, w|3, h|4, r|5
self.T = {
x = args.T.x or args.T[1] or 0,
y = args.T.y or args.T[2] or 0,
w = args.T.w or args.T[3] or 1,
h = args.T.h or args.T[4] or 1,
r = args.T.r or args.T[5] or 0,
scale = args.T.scale or args.T[6] or 1,
}
--Transform to use for collision detection
self.CT = self.T
--Create the offset tables, used to determine things like drag offset and 3d shader effects
self.click_offset = {x = 0, y = 0}
self.hover_offset = {x = 0, y = 0}
--To keep track of all nodes created on pause. If true, this node moves normally even when the G.TIMERS.TOTAL doesn't increment
self.created_on_pause = G.SETTINGS.paused
--ID tracker, every Node has a unique ID
G.ID = G.ID or 1
self.ID = G.ID
G.ID = G.ID + 1
--Frame tracker to aid in not doing too many extra calculations
self.FRAME = {
DRAW = -1,
MOVE = -1
}
--The states for this Node and all derived nodes. This is how we control the visibility and interactibility of any object
--All nodes do not collide by default. This reduces the size of n for the O(n^2) collision detection
self.states = {
visible = true,
collide = {can = false, is = false},
focus = {can = false, is = false},
hover = {can = true, is = false},
click = {can = true, is = false},
drag = {can = true, is = false},
release_on = {can = true, is = false}
}
--If we provide a container, all nodes within that container are translated with that container as the reference frame.
--For example, if G.ROOM is set at x = 5 and y = 5, and we create a new game object at 0, 0, it will actually be drawn at
--5, 5. This allows us to control things like screen shake, room positioning, rotation, padding, etc. without needing to modify
--every game object that we need to draw
self.container = args.container or G.ROOM
--The list of children give Node a treelike structure. This can be used for things like drawing, deterministice movement and parallax
--calculations when child nodes rely on updated information from parents, and inherited attributes like button click functions
if not self.children then
self.children = {}
end
--Add this object to the appropriate instance table only if the metatable matches with NODE
if getmetatable(self) == Node then
table.insert(G.I.NODE, self)
end
--Unless node was created during a stage transition (when G.STAGE_OBJECT_INTERRUPT is true), add all nodes to their appropriate
--stage object table so they can be easily deleted on stage transition
if not G.STAGE_OBJECT_INTERRUPT then
table.insert(G.STAGE_OBJECTS[G.STAGE], self)
end
end
--Draw a bounding rectangle representing the transform of this node. Used in debugging.
function Node:draw_boundingrect()
self.under_overlay = G.under_overlay
if G.DEBUG then
local transform = self.VT or self.T
love.graphics.push()
love.graphics.scale(G.TILESCALE, G.TILESCALE)
love.graphics.translate(transform.x*G.TILESIZE+transform.w*G.TILESIZE*0.5,
transform.y*G.TILESIZE+transform.h*G.TILESIZE*0.5)
love.graphics.rotate(transform.r)
love.graphics.translate(-transform.w*G.TILESIZE*0.5,
-transform.h*G.TILESIZE*0.5)
if self.DEBUG_VALUE then
love.graphics.setColor(1, 1, 0, 1)
love.graphics.print((self.DEBUG_VALUE or ''), transform.w*G.TILESIZE,transform.h*G.TILESIZE, nil, 1/G.TILESCALE)
end
love.graphics.setLineWidth(1 + (self.states.focus.is and 1 or 0))
if self.states.collide.is then
love.graphics.setColor(0, 1, 0, 0.3)
else
love.graphics.setColor(1, 0, 0, 0.3)
end
if self.states.focus.can then
love.graphics.setColor(G.C.GOLD)
love.graphics.setLineWidth(1)
end
if self.CALCING then
love.graphics.setColor({0,0,1,1})
love.graphics.setLineWidth(3)
end
love.graphics.rectangle('line', 0, 0, transform.w*G.TILESIZE,transform.h*G.TILESIZE, 3)
love.graphics.pop()
end
end
--Draws self, then adds self the the draw hash, then draws all children
function Node:draw()
self:draw_boundingrect()
if self.states.visible then
add_to_drawhash(self)
for _, v in pairs(self.children) do
v:draw()
end
end
end
--Determines if this node collides with some point. Applies any container translations and rotations, then\
--applies translations and rotations specific to this node. This means the collision detection effectively\
--determines if some point intersects this node regargless of rotation.
--
---@param point {x: number, y: number}
--**x and y** The coordinates of the cursor transformed into game units
function Node:collides_with_point(point)
--First reset the collision state to false
if self.container then
local T = self.CT or self.T
self.ARGS.collides_with_point_point = self.ARGS.collides_with_point_point or {}
self.ARGS.collides_with_point_translation = self.ARGS.collides_with_point_translation or {}
self.ARGS.collides_with_point_rotation = self.ARGS.collides_with_point_rotation or {}
local _p = self.ARGS.collides_with_point_point
local _t = self.ARGS.collides_with_point_translation
local _r = self.ARGS.collides_with_point_rotation
local _b = self.states.hover.is and G.COLLISION_BUFFER or 0
_p.x, _p.y = point.x, point.y
if self.container ~= self then --if there is some valid container, we need to apply all translations and rotations for the container first
if math.abs(self.container.T.r) < 0.1 then
--Translate to normalize this Node to the center of the container
_t.x, _t.y = -self.container.T.w/2, -self.container.T.h/2
point_translate(_p, _t)
--Rotate node about the center of the container
point_rotate(_p, self.container.T.r)
--Translate node to undo the container translation, essentially reframing it in 'container' space
_t.x, _t.y = self.container.T.w/2-self.container.T.x, self.container.T.h/2-self.container.T.y
point_translate(_p, _t)
else
--Translate node to undo the container translation, essentially reframing it in 'container' space
_t.x, _t.y = -self.container.T.x, -self.container.T.y
point_translate(_p, _t)
end
end
if math.abs(T.r) < 0.1 then
--If we can essentially disregard transform rotation, just treat it like a normal rectangle
if _p.x >= T.x - _b and _p.y >= T.y - _b and _p.x <= T.x + T.w + _b and _p.y <= T.y + T.h + _b then
return true
end
else
--Otherwise we need to do some silly point rotation garbage to determine if the point intersects the rotated rectangle
_r.cos, _r.sin = math.cos(T.r+math.pi/2), math.sin(T.r+math.pi/2)
_p.x, _p.y = _p.x - (T.x + 0.5*(T.w)), _p.y - (T.y + 0.5*(T.h))
_t.x, _t.y = _p.y*_r.cos - _p.x*_r.sin, _p.y*_r.sin + _p.x*_r.cos
_p.x, _p.y = _t.x + (T.x + 0.5*(T.w)), _t.y + (T.y + 0.5*(T.h))
if _p.x >= T.x - _b and _p.y >= T.y - _b
and _p.x <= T.x + T.w + _b and _p.y <= T.y + T.h + _b then
return true
end
end
end
end
--Sets the offset of passed point in terms of this nodes T.x and T.y
--
---@param point {x: number, y: number}
---@param type string
--**x and y** The coordinates of the cursor transformed into game units
--**type** the type of offset to set for this Node, either 'Click' or 'Hover'
function Node:set_offset(point, type)
self.ARGS.set_offset_point = self.ARGS.set_offset_point or {}
self.ARGS.set_offset_translation = self.ARGS.set_offset_translation or {}
local _p = self.ARGS.set_offset_point
local _t = self.ARGS.set_offset_translation
_p.x, _p.y = point.x, point.y
--Translate to middle of the container
_t.x = -self.container.T.w/2
_t.y = -self.container.T.h/2
point_translate(_p, _t)
--Rotate about the container midpoint according to node rotation
point_rotate(_p, self.container.T.r)
--Translate node to undo the container translation, essentially reframing it in 'container' space
_t.x = self.container.T.w/2-self.container.T.x
_t.y = self.container.T.h/2-self.container.T.y
point_translate(_p, _t)
if type == 'Click' then
self.click_offset.x = (_p.x - self.T.x)
self.click_offset.y = (_p.y - self.T.y)
elseif type == 'Hover' then
self.hover_offset.x = (_p.x - self.T.x)
self.hover_offset.y = (_p.y - self.T.y)
end
end
--If the current container is being 'Dragged', usually by a cursor, determine if any drag popups need to be generated and do so
function Node:drag()
if self.config and self.config.d_popup then
if not self.children.d_popup then
self.children.d_popup = UIBox{
definition = self.config.d_popup,
config = self.config.d_popup_config
}
self.children.h_popup.states.collide.can = false
table.insert(G.I.POPUP, self.children.d_popup)
self.children.d_popup.states.drag.can = true
end
end
end
--Determines if this Node can be dragged. This is a simple function but more complex objects may redefine this to return a parent\
--if the parent needs to drag other children with it
function Node:can_drag()
return self.states.drag.can and self or nil
end
--Called by the CONTROLLER when this node is no longer being dragged, removes any d_popups
function Node:stop_drag()
if self.children.d_popup then
for k, v in pairs(G.I.POPUP) do
if v == self.children.d_popup then
table.remove(G.I.POPUP, k)
end
end
self.children.d_popup:remove()
self.children.d_popup = nil
end
end
--If the current container is being 'Hovered', usually by a cursor, determine if any hover popups need to be generated and do so
function Node:hover()
if self.config and self.config.h_popup then
if not self.children.h_popup then
self.config.h_popup_config.instance_type = 'POPUP'
self.children.h_popup = UIBox{
definition = self.config.h_popup,
config = self.config.h_popup_config,
}
self.children.h_popup.states.collide.can = false
self.children.h_popup.states.drag.can = true
end
end
end
--Called by the CONTROLLER when this node is no longer being hovered, removes any h_popups
function Node:stop_hover()
if self.children.h_popup then
self.children.h_popup:remove()
self.children.h_popup = nil
end
end
--Called by the CONTROLLER to determine the position the cursor should be set to for this node
function Node:put_focused_cursor()
return (self.T.x + self.T.w/2 + self.container.T.x)*(G.TILESCALE*G.TILESIZE), (self.T.y + self.T.h/2 + self.container.T.y)*(G.TILESCALE*G.TILESIZE)
end
--Sets the container of this node and all child nodes to be a new container node
--
---@param container Node The new node that will behave as this nodes container
function Node:set_container(container)
if self.children then
for _, v in pairs(self.children) do
v:set_container(container)
end
end
self.container = container
end
--Translation function used before any draw calls, translates this node according to the transform of the container node
function Node:translate_container()
if self.container and self.container ~= self then
love.graphics.translate(self.container.T.w*G.TILESCALE*G.TILESIZE*0.5, self.container.T.h*G.TILESCALE*G.TILESIZE*0.5)
love.graphics.rotate(self.container.T.r)
love.graphics.translate(
-self.container.T.w*G.TILESCALE*G.TILESIZE*0.5 + self.container.T.x*G.TILESCALE*G.TILESIZE,
-self.container.T.h*G.TILESCALE*G.TILESIZE*0.5 + self.container.T.y*G.TILESCALE*G.TILESIZE)
end
end
--When this Node needs to be deleted, removes self from any tables it may have been added to to destroy any weak references\
--Also calls the remove method of all children to have them do the same
function Node:remove()
for k, v in ipairs(G.I.POPUP) do
if v == self then
table.remove(G.I.POPUP, k)
break;
end
end
for k, v in ipairs(G.I.NODE) do
if v == self then
table.remove(G.I.NODE, k)
break;
end
end
for k, v in ipairs(G.STAGE_OBJECTS[G.STAGE]) do
if v == self then
table.remove(G.STAGE_OBJECTS[G.STAGE], k)
break;
end
end
if self.children then
for k, v in pairs(self.children) do
v:remove()
end
end
if G.CONTROLLER.clicked.target ==self then
G.CONTROLLER.clicked.target = nil
end
if G.CONTROLLER.focused.target ==self then
G.CONTROLLER.focused.target = nil
end
if G.CONTROLLER.dragging.target ==self then
G.CONTROLLER.dragging.target = nil
end
if G.CONTROLLER.hovering.target ==self then
G.CONTROLLER.hovering.target = nil
end
if G.CONTROLLER.released_on.target ==self then
G.CONTROLLER.released_on.target = nil
end
if G.CONTROLLER.cursor_down.target ==self then
G.CONTROLLER.cursor_down.target = nil
end
if G.CONTROLLER.cursor_up.target ==self then
G.CONTROLLER.cursor_up.target = nil
end
if G.CONTROLLER.cursor_hover.target ==self then
G.CONTROLLER.cursor_hover.target = nil
end
self.REMOVED = true
end
--returns the squared(fast) distance in game units from the center of this node to the center of another node
--
---@param other_node Node to measure the distance from
function Node:fast_mid_dist(other_node)
return math.sqrt((other_node.T.x + 0.5*other_node.T.w) - (self.T.x + self.T.w))^2 + ((other_node.T.y + 0.5*other_node.T.h) - (self.T.y + self.T.h))^2
end
--Prototype for a click release function, when the cursor is released on this node
function Node:release(dragged) end
--Prototype for a click function
function Node:click() end
--Prototype animation function for any frame manipulation needed
function Node:animate() end
--Prototype update function for any object specific logic that needs to occur every frame
function Node:update(dt) end

37
engine/object.lua Normal file
View File

@@ -0,0 +1,37 @@
--||--
--This Object implementation was taken from SNKRX (MIT license). Slightly modified, this is a very simple OOP base
Object = {}
Object.__index = Object
function Object:init()
end
function Object:extend()
local cls = {}
for k, v in pairs(self) do
if k:find("__") == 1 then
cls[k] = v
end
end
cls.__index = cls
cls.super = self
setmetatable(cls, self)
return cls
end
function Object:is(T)
local mt = getmetatable(self)
while mt do
if mt == T then
return true
end
mt = getmetatable(mt)
end
return false
end
function Object:__call(...)
local obj = setmetatable({}, self)
obj:init(...)
return obj
end

177
engine/particles.lua Normal file
View File

@@ -0,0 +1,177 @@
---@class Particles: Moveable
Particles = Moveable:extend()
--Class Methods
function Particles:init(X, Y, W, H, config)
config = config or {}
Moveable.init(self,X, Y, W, H)
self.fill = config.fill
self.padding = config.padding or 0
if config.attach then
self:set_alignment{
major = config.attach,
type = 'cm',
bond = 'Strong'
}
table.insert(self.role.major.children,self)
self.parent = self.role.major
self.T.x = self.role.major.T.x + self.padding
self.T.y = self.role.major.T.y + self.padding
if self.fill then
self.T.w = self.role.major.T.w - self.padding
self.T.h = self.role.major.T.h - self.padding
end
end
self.states.hover.can = false
self.states.click.can = false
self.states.collide.can = false
self.states.drag.can = false
self.states.release_on.can = false
self.timer = config.timer or 0.5
self.timer_type = (self.created_on_pause and 'REAL') or config.timer_type or 'REAL'
self.last_real_time = G.TIMERS[self.timer_type] - self.timer
self.last_drawn = 0
self.lifespan = config.lifespan or 1
self.fade_alpha = 0
self.speed = config.speed or 1
self.max = config.max or 1000000000000000
self.pulse_max = math.min(20, config.pulse_max or 0)
self.pulsed = 0
self.vel_variation = config.vel_variation or 1
self.particles = {}
self.scale = config.scale or 1
self.colours = config.colours or {G.C.BACKGROUND.D}
if config.initialize then
for i = 1, 60 do
self.last_real_time = self.last_real_time - 15/60
self:update(15/60)
self:move(15/60)
end
end
if getmetatable(self) == Particles then
table.insert(G.I.MOVEABLE, self)
end
end
function Particles:update(dt)
if G.SETTINGS.paused and not self.created_on_pause then self.last_real_time = G.TIMERS[self.timer_type] ; return end
local added_this_frame = 0
while G.TIMERS[self.timer_type] > self.last_real_time + self.timer and (#self.particles < self.max or self.pulsed < self.pulse_max) and added_this_frame < 20 do
self.last_real_time = self.last_real_time + self.timer
local new_offset = {
x=self.fill and (0.5-math.random())*self.T.w or 0,
y=self.fill and (0.5-math.random())*self.T.h or 0
}
if self.fill and self.T.r < 0.1 and self.T.r > -0.1 then
local newer_offset = {
x = math.sin(self.T.r)*new_offset.y + math.cos(self.T.r)*new_offset.x,
y = math.sin(self.T.r)*new_offset.x + math.cos(self.T.r)*new_offset.y,
}
new_offset = newer_offset
end
table.insert(self.particles, {
draw = false,
dir = math.random()*2*math.pi,
facing = math.random()*2*math.pi,
size = math.random()*0.5+0.1,
age = 0,
velocity = self.speed*(self.vel_variation*math.random() + (1-self.vel_variation))*0.7,
r_vel = 0.2*(0.5 - math.random()),
e_prev = 0,
e_curr = 0,
scale = 0,
visible_scale = 0,
time = G.TIMERS[self.timer_type],
colour = pseudorandom_element(self.colours),
offset = new_offset
})
added_this_frame = added_this_frame + 1
if self.pulsed <= self.pulse_max then self.pulsed = self.pulsed + 1 end
end
end
function Particles:move(dt)
if G.SETTINGS.paused and not self.created_on_pause then return end
Moveable.move(self, dt)
if self.timer_type ~= 'REAL' then dt = dt*G.SPEEDFACTOR end
for i=#self.particles,1,-1 do
self.particles[i].draw = true
self.particles[i].e_vel = self.particles[i].e_vel or dt*self.scale
self.particles[i].e_prev = self.particles[i].e_curr
self.particles[i].age = self.particles[i].age + dt
self.particles[i].e_curr = math.min(2*math.min((self.particles[i].age/self.lifespan)*self.scale, self.scale*((self.lifespan - self.particles[i].age)/self.lifespan)), self.scale)
self.particles[i].e_vel = (self.particles[i].e_curr - self.particles[i].e_prev)*self.scale*dt + (1-self.scale*dt)*self.particles[i].e_vel
self.particles[i].scale = self.particles[i].scale + self.particles[i].e_vel
self.particles[i].scale = math.min(2*math.min((self.particles[i].age/self.lifespan)*self.scale, self.scale*((self.lifespan - self.particles[i].age)/self.lifespan)), self.scale)
if self.particles[i].scale < 0 then
table.remove(self.particles, i)
else
self.particles[i].offset.x = self.particles[i].offset.x + self.particles[i].velocity*math.sin(self.particles[i].dir)*dt
self.particles[i].offset.y = self.particles[i].offset.y + self.particles[i].velocity*math.cos(self.particles[i].dir)*dt
self.particles[i].facing = self.particles[i].facing + self.particles[i].r_vel*dt
self.particles[i].velocity = math.max(0, self.particles[i].velocity - self.particles[i].velocity*0.07*dt)
end
end
end
function Particles:fade(delay, to)
G.E_MANAGER:add_event(Event({
trigger = 'ease',
timer = self.timer_type,
blockable = false,
blocking = false,
ref_value = 'fade_alpha',
ref_table = self,
ease_to = to or 1,
delay = delay
}))
end
function Particles:draw(alpha)
alpha = alpha or 1
prep_draw(self, 1)
love.graphics.translate(self.T.w/2, self.T.h/2)
for k, v in pairs(self.particles) do
if v.draw then
love.graphics.push()
love.graphics.setColor(v.colour[1], v.colour[2], v.colour[3], v.colour[4]*alpha*(1-self.fade_alpha))
love.graphics.translate(v.offset.x, v.offset.y)
love.graphics.rotate(v.facing)
love.graphics.rectangle('fill', -v.scale/2, -v.scale/2, v.scale, v.scale) -- origin in the middle
love.graphics.pop()
end
end
love.graphics.pop()
add_to_drawhash(self)
self:draw_boundingrect()
end
function Particles:remove()
if self.role.major then
for k, v in pairs(self.role.major.children) do
if v == self and type(k) == 'number' then
table.remove(self.role.major.children, k)
end
end
end
remove_all(self.children)
Moveable.remove(self)
end

188
engine/profile.lua Normal file
View File

@@ -0,0 +1,188 @@
local clock = os.clock
--- Simple profiler written in Lua.
-- @module profile
-- @alias profile
local profile = {}
-- function labels
local _labeled = {}
-- function definitions
local _defined = {}
-- time of last call
local _tcalled = {}
-- total execution time
local _telapsed = {}
-- number of calls
local _ncalls = {}
-- list of internal profiler functions
local _internal = {}
--- This is an internal function.
-- @tparam string event Event type
-- @tparam number line Line number
-- @tparam[opt] table info Debug info table
function profile.hooker(event, line, info)
info = info or debug.getinfo(2, 'fnS')
local f = info.func
-- ignore the profiler itself
if _internal[f] or info.what ~= "Lua" then
return
end
-- get the function name if available
if info.name then
_labeled[f] = info.name
end
-- find the line definition
if not _defined[f] then
_defined[f] = info.short_src..":"..info.linedefined
_ncalls[f] = 0
_telapsed[f] = 0
end
if _tcalled[f] then
local dt = clock() - _tcalled[f]
_telapsed[f] = _telapsed[f] + dt
_tcalled[f] = nil
end
if event == "tail call" then
local prev = debug.getinfo(3, 'fnS')
profile.hooker("return", line, prev)
profile.hooker("call", line, info)
elseif event == 'call' then
_tcalled[f] = clock()
else
_ncalls[f] = _ncalls[f] + 1
end
end
--- Sets a clock function to be used by the profiler.
-- @tparam function func Clock function that returns a number
function profile.setclock(f)
assert(type(f) == "function", "clock must be a function")
clock = f
end
--- Starts collecting data.
function profile.start()
if rawget(_G, 'jit') then
jit.off()
jit.flush()
end
debug.sethook(profile.hooker, "cr")
end
--- Stops collecting data.
function profile.stop()
debug.sethook()
for f in pairs(_tcalled) do
local dt = clock() - _tcalled[f]
_telapsed[f] = _telapsed[f] + dt
_tcalled[f] = nil
end
-- merge closures
local lookup = {}
for f, d in pairs(_defined) do
local id = (_labeled[f] or '?')..d
local f2 = lookup[id]
if f2 then
_ncalls[f2] = _ncalls[f2] + (_ncalls[f] or 0)
_telapsed[f2] = _telapsed[f2] + (_telapsed[f] or 0)
_defined[f], _labeled[f] = nil, nil
_ncalls[f], _telapsed[f] = nil, nil
else
lookup[id] = f
end
end
collectgarbage('collect')
end
--- Resets all collected data.
function profile.reset()
for f in pairs(_ncalls) do
_ncalls[f] = 0
end
for f in pairs(_telapsed) do
_telapsed[f] = 0
end
for f in pairs(_tcalled) do
_tcalled[f] = nil
end
collectgarbage('collect')
end
--- This is an internal function.
-- @tparam function a First function
-- @tparam function b Second function
function profile.comp(a, b)
local dt = _telapsed[b] - _telapsed[a]
if dt == 0 then
return _ncalls[b] < _ncalls[a]
end
return dt < 0
end
--- Iterates all functions that have been called since the profile was started.
-- @tparam[opt] number limit Maximum number of rows
function profile.query(limit)
local t = {}
for f, n in pairs(_ncalls) do
if n > 0 then
t[#t + 1] = f
end
end
table.sort(t, profile.comp)
if limit then
while #t > limit do
table.remove(t)
end
end
for i, f in ipairs(t) do
local dt = 0
if _tcalled[f] then
dt = clock() - _tcalled[f]
end
t[i] = { i, _labeled[f] or '?', _ncalls[f], _telapsed[f] + dt, _defined[f] }
end
return t
end
local cols = { 3, 29, 11, 24, 32 }
--- Generates a text report.
-- @tparam[opt] number limit Maximum number of rows
function profile.report(n)
local out = {}
local report = profile.query(n)
for i, row in ipairs(report) do
for j = 1, 5 do
local s = row[j]
local l2 = cols[j]
s = tostring(s)
local l1 = s:len()
if l1 < l2 then
s = s..(' '):rep(l2-l1)
elseif l1 > l2 then
s = s:sub(l1 - l2 + 1, l1)
end
row[j] = s
end
out[i] = table.concat(row, ' | ')
end
local row = " +-----+-------------------------------+-------------+--------------------------+----------------------------------+ \n"
local col = " | # | Function | Calls | Time | Code | \n"
local sz = row..col..row
if #out > 0 then
sz = sz..' | '..table.concat(out, ' | \n | ')..' | \n'
end
return '\n'..sz..row
end
-- store all internal profiler functions
for _, v in pairs(profile) do
if type(v) == "function" then
_internal[v] = true
end
end
return profile

84
engine/save_manager.lua Normal file
View File

@@ -0,0 +1,84 @@
require "love.system"
if (love.system.getOS() == 'OS X' ) and (jit.arch == 'arm64' or jit.arch == 'arm' or true) then jit.off() end
require "love.timer"
require "love.thread"
require 'love.filesystem'
require "engine/object"
require "engine/string_packer"
--vars needed for sound manager thread
CHANNEL = love.thread.getChannel("save_request")
while true do
--Monitor the channel for any new requests
local request = CHANNEL:demand() -- Value from channel
if request then
--Saves progress for settings, unlocks, alerts and discoveries
if request.type == 'save_progress' then
local prefix_profile = (request.save_progress.SETTINGS.profile or 1)..''
if not love.filesystem.getInfo(prefix_profile) then love.filesystem.createDirectory( prefix_profile ) end
prefix_profile = prefix_profile..'/'
if not love.filesystem.getInfo(prefix_profile..'meta.jkr') then
love.filesystem.append( prefix_profile..'meta.jkr', 'return {}' )
end
local meta = STR_UNPACK(get_compressed(prefix_profile..'meta.jkr') or 'return {}')
meta.unlocked = meta.unlocked or {}
meta.discovered = meta.discovered or {}
meta.alerted = meta.alerted or {}
local _append = false
for k, v in pairs(request.save_progress.UDA) do
if string.find(v, 'u') and not meta.unlocked[k] then
meta.unlocked[k] = true
_append = true
end
if string.find(v, 'd') and not meta.discovered[k] then
meta.discovered[k] = true
_append = true
end
if string.find(v, 'a') and not meta.alerted[k] then
meta.alerted[k] = true
_append = true
end
end
if _append then compress_and_save( prefix_profile..'meta.jkr', STR_PACK(meta)) end
compress_and_save('settings.jkr', request.save_progress.SETTINGS)
compress_and_save(prefix_profile..'profile.jkr', request.save_progress.PROFILE)
CHANNEL:push('done')
--Saves the settings file
elseif request.type == 'save_settings' then
compress_and_save('settings.jkr', request.save_settings)
compress_and_save(request.profile_num..'/profile.jkr', request.save_profile)
--Saves the metrics file
elseif request.type == 'save_metrics' then
compress_and_save('metrics.jkr', request.save_metrics)
--Saves any notifications
elseif request.type == 'save_notify' then
local prefix_profile = (request.profile_num or 1)..''
if not love.filesystem.getInfo(prefix_profile) then love.filesystem.createDirectory( prefix_profile ) end
prefix_profile = prefix_profile..'/'
if not love.filesystem.getInfo(prefix_profile..'unlock_notify.jkr') then love.filesystem.append( prefix_profile..'unlock_notify.jkr', '') end
local unlock_notify = get_compressed(prefix_profile..'unlock_notify.jkr') or ''
if request.save_notify and not string.find(unlock_notify, request.save_notify) then
compress_and_save( prefix_profile..'unlock_notify.jkr', unlock_notify..request.save_notify..'\n')
end
--Saves the run
elseif request.type == 'save_run' then
local prefix_profile = (request.profile_num or 1)..''
if not love.filesystem.getInfo(prefix_profile) then love.filesystem.createDirectory( prefix_profile ) end
prefix_profile = prefix_profile..'/'
compress_and_save(prefix_profile..'save.jkr', request.save_table)
end
end
end

207
engine/sound_manager.lua Normal file
View File

@@ -0,0 +1,207 @@
require "love.audio"
require "love.sound"
require "love.system"
if (love.system.getOS() == 'OS X' )and (jit.arch == 'arm64' or jit.arch == 'arm' or true) then jit.off() end
--vars needed for sound manager thread
CHANNEL = love.thread.getChannel("sound_request")
LOAD_CHANNEL = love.thread.getChannel('load_channel')
LOAD_CHANNEL:push('audio thread start')
DISABLE_SFX = false
--create all sounds from resources and play one each to load into mem
SOURCES = {}
local sound_files = love.filesystem.getDirectoryItems("resources/sounds")
for _, filename in ipairs(sound_files) do
local extension = string.sub(filename, -4)
for i = 1, 1 do
if extension == '.ogg' then
LOAD_CHANNEL:push('audio file - '..filename)
local sound_code = string.sub(filename, 1, -5)
local s = {
sound = love.audio.newSource("resources/sounds/"..filename,string.find(sound_code,'music') and "stream" or 'static'),
filepath = "resources/sounds/"..filename
}
SOURCES[sound_code] = {}
table.insert(SOURCES[sound_code], s)
s.sound_code = sound_code
s.sound:setVolume(0)
love.audio.play(s.sound)
s.sound:stop()
end
end
end
function PLAY_SOUND(args)
args.per = args.per or 1
args.vol = args.vol or 1
SOURCES[args.sound_code] = SOURCES[args.sound_code] or {}
for _, s in ipairs(SOURCES[args.sound_code]) do
if s.sound and not s.sound:isPlaying() then
s.original_pitch = args.per
s.original_volume = args.vol
s.created_on_pause = args.overlay_menu
s.created_on_state = args.state
s.sfx_handled = 0
s.transition_timer = 0
SET_SFX(s, args)
love.audio.play(s.sound)
return s
end
end
local should_stream = (string.find(args.sound_code,'music') or string.find(args.sound_code,'ambient'))
local s = {sound = love.audio.newSource("resources/sounds/"..args.sound_code..'.ogg', should_stream and "stream" or 'static')}
table.insert(SOURCES[args.sound_code], s)
s.sound_code = args.sound_code
s.original_pitch = args.per or 1
s.original_volume = args.vol or 1
s.created_on_pause = (args.overlay_menu and true or false)
s.created_on_state = args.state
s.sfx_handled = 0
s.transition_timer = 0
SET_SFX(s, args)
love.audio.play(s.sound)
return s
end
function STOP_AUDIO()
for _, source in pairs(SOURCES) do
for _, s in pairs(source) do
if s.sound:isPlaying() then
s.sound:stop()
end
end
end
end
function SET_SFX(s, args)
if string.find(s.sound_code,'music') then
if s.sound_code == args.desired_track then
s.current_volume = s.current_volume or 1
s.current_volume = 1*(args.dt*3) + (1-(args.dt*3))*s.current_volume
else
s.current_volume = s.current_volume or 0
s.current_volume = 0*(args.dt*3) + (1-(args.dt*3))*s.current_volume
end
s.sound:setVolume(s.current_volume*s.original_volume*(args.sound_settings.volume/100.0)*(args.sound_settings.music_volume/100.0))
s.sound:setPitch(s.original_pitch*args.pitch_mod)
else
if s.temp_pitch ~= s.original_pitch then
s.sound:setPitch(s.original_pitch)
s.temp_pitch = s.original_pitch
end
local sound_vol = s.original_volume*(args.sound_settings.volume/100.0)*(args.sound_settings.game_sounds_volume/100.0)
if s.created_on_state == 13 then sound_vol = sound_vol*args.splash_vol end
if sound_vol <= 0 then
s.sound:stop()
else
s.sound:setVolume(sound_vol)
end
end
end
function MODULATE(args)
for k, v in pairs(SOURCES) do
if (string.find(k,'music') and (args.desired_track ~= '')) then
if v[1] and v[1].sound and v[1].sound:isPlaying() then
else
RESTART_MUSIC(args)
break;
end
end
end
for k, v in pairs(SOURCES) do
local i=1
while i <= #v do
if not v[i].sound:isPlaying() then
v[i].sound:release()
table.remove(v, i)
else
i = i + 1
end
end
for _, s in pairs(v) do
if s.sound and s.sound:isPlaying() and s.original_volume then
SET_SFX(s, args)
end
end
end
end
function RESTART_MUSIC(args)
for k, v in pairs(SOURCES) do
if string.find(k,'music') then
for i, s in ipairs(v) do
s.sound:stop()
end
SOURCES[k] = {}
args.per = 0.7
args.vol = 0.6
args.sound_code = k
local s = PLAY_SOUND(args)
s.initialized = true
end
end
end
function AMBIENT(args)
for k, v in pairs(SOURCES) do
if args.ambient_control[k] then
local start_ambient = args.ambient_control[k].vol*(args.sound_settings.volume/100.0)*(args.sound_settings.game_sounds_volume/100.0) > 0
for i, s in ipairs(v) do
if s.sound and s.sound:isPlaying() and s.original_volume then
s.original_volume = args.ambient_control[k].vol
SET_SFX(s, args)
start_ambient = false
end
end
if start_ambient then
args.sound_code = k
args.vol = args.ambient_control[k].vol
args.per = args.ambient_control[k].per
PLAY_SOUND(args)
end
end
end
end
function RESET_STATES(state)
for k, v in pairs(SOURCES) do
for i, s in ipairs(v) do
s.created_on_state = state
end
end
end
LOAD_CHANNEL:push('finished')
while true do
--Monitor the channel for any new requests
local request = CHANNEL:demand() -- Value from channel
if request then
--If the request is for an update to the music track, handle it here
if false then elseif request.type == 'sound' then
PLAY_SOUND(request)
elseif request.type == 'stop' then
STOP_AUDIO()
elseif request.type == 'modulate' then
MODULATE(request)
if request.ambient_control then AMBIENT(request) end
elseif request.type == 'restart_music' then
RESTART_MUSIC()
elseif request.type == 'reset_states' then
for k, v in pairs(SOURCES) do
for i, s in ipairs(v) do
s.created_on_state = request.state
end
end
end
end
end

215
engine/sprite.lua Normal file
View File

@@ -0,0 +1,215 @@
--Class
Sprite = Moveable:extend()
--Class Methods
function Sprite:init(X, Y, W, H, new_sprite_atlas, sprite_pos)
Moveable.init(self,X, Y, W, H)
self.CT = self.VT
self.atlas = new_sprite_atlas
self.scale = {x=self.atlas.px, y=self.atlas.py}
self.scale_mag = math.min(self.scale.x/W,self.scale.y/H)
self.zoom = true
self:set_sprite_pos(sprite_pos)
if getmetatable(self) == Sprite then
table.insert(G.I.SPRITE, self)
end
end
function Sprite:reset()
self.atlas = G.ASSET_ATLAS[self.atlas.name]
self:set_sprite_pos(self.sprite_pos)
end
function Sprite:set_sprite_pos(sprite_pos)
if sprite_pos and sprite_pos.v then
self.sprite_pos = {x = (math.random(sprite_pos.v)-1), y = sprite_pos.y}
else
self.sprite_pos = sprite_pos or {x=0,y=0}
end
self.sprite_pos_copy = {x = self.sprite_pos.x, y = self.sprite_pos.y}
self.sprite = love.graphics.newQuad(
self.sprite_pos.x*self.atlas.px,
self.sprite_pos.y*self.atlas.py,
self.scale.x,
self.scale.y, self.atlas.image:getDimensions())
self.image_dims = {}
self.image_dims[1], self.image_dims[2] = self.atlas.image:getDimensions()
end
function Sprite:get_pos_pixel()
self.RETS.get_pos_pixel = self.RETS.get_pos_pixel or {}
self.RETS.get_pos_pixel[1] = self.sprite_pos.x
self.RETS.get_pos_pixel[2] = self.sprite_pos.y
self.RETS.get_pos_pixel[3] = self.atlas.px --self.scale.x
self.RETS.get_pos_pixel[4] = self.atlas.py --self.scale.y
return self.RETS.get_pos_pixel
end
function Sprite:get_image_dims()
return self.image_dims
end
function Sprite:define_draw_steps(draw_step_definitions)
self.draw_steps = EMPTY(self.draw_steps)
for k, v in ipairs(draw_step_definitions) do
self.draw_steps[#self.draw_steps+1] = {
shader = v.shader or 'dissolve',
shadow_height = v.shadow_height or nil,
send = v.send or nil,
no_tilt = v.no_tilt or nil,
other_obj = v.other_obj or nil,
ms = v.ms or nil,
mr = v.mr or nil,
mx = v.mx or nil,
my = v.my or nil
}
end
end
function Sprite:draw_shader(_shader, _shadow_height, _send, _no_tilt, other_obj, ms, mr, mx, my, custom_shader, tilt_shadow)
local _draw_major = self.role.draw_major or self
if _shadow_height then
self.VT.y = self.VT.y - _draw_major.shadow_parrallax.y*_shadow_height
self.VT.x = self.VT.x - _draw_major.shadow_parrallax.x*_shadow_height
self.VT.scale = self.VT.scale*(1-0.2*_shadow_height)
end
if custom_shader then
if _send then
for k, v in ipairs(_send) do
G.SHADERS[_shader]:send(v.name, v.val or (v.func and v.func()) or v.ref_table[v.ref_value])
end
end
elseif _shader == 'vortex' then
G.SHADERS['vortex']:send('vortex_amt', G.TIMERS.REAL - (G.vortex_time or 0))
else
self.ARGS.prep_shader = self.ARGS.prep_shader or {}
self.ARGS.prep_shader.cursor_pos = self.ARGS.prep_shader.cursor_pos or {}
self.ARGS.prep_shader.cursor_pos[1] = _draw_major.tilt_var and _draw_major.tilt_var.mx*G.CANV_SCALE or G.CONTROLLER.cursor_position.x*G.CANV_SCALE
self.ARGS.prep_shader.cursor_pos[2] = _draw_major.tilt_var and _draw_major.tilt_var.my*G.CANV_SCALE or G.CONTROLLER.cursor_position.y*G.CANV_SCALE
G.SHADERS[_shader or 'dissolve']:send('mouse_screen_pos', self.ARGS.prep_shader.cursor_pos)
G.SHADERS[_shader or 'dissolve']:send('screen_scale', G.TILESCALE*G.TILESIZE*(_draw_major.mouse_damping or 1)*G.CANV_SCALE)
G.SHADERS[_shader or 'dissolve']:send('hovering',((_shadow_height and not tilt_shadow) or _no_tilt) and 0 or (_draw_major.hover_tilt or 0)*(tilt_shadow or 1))
G.SHADERS[_shader or 'dissolve']:send("dissolve",math.abs(_draw_major.dissolve or 0))
G.SHADERS[_shader or 'dissolve']:send("time",123.33412*(_draw_major.ID/1.14212 or 12.5123152)%3000)
G.SHADERS[_shader or 'dissolve']:send("texture_details",self:get_pos_pixel())
G.SHADERS[_shader or 'dissolve']:send("image_details",self:get_image_dims())
G.SHADERS[_shader or 'dissolve']:send("burn_colour_1",_draw_major.dissolve_colours and _draw_major.dissolve_colours[1] or G.C.CLEAR)
G.SHADERS[_shader or 'dissolve']:send("burn_colour_2",_draw_major.dissolve_colours and _draw_major.dissolve_colours[2] or G.C.CLEAR)
G.SHADERS[_shader or 'dissolve']:send("shadow",(not not _shadow_height))
if _send then G.SHADERS[_shader or 'dissolve']:send(_shader,_send) end
end
love.graphics.setShader( G.SHADERS[_shader or 'dissolve'], G.SHADERS[_shader or 'dissolve'])
if other_obj then
self:draw_from(other_obj, ms, mr, mx, my)
else
self:draw_self()
end
love.graphics.setShader()
if _shadow_height then
self.VT.y = self.VT.y + _draw_major.shadow_parrallax.y*_shadow_height
self.VT.x = self.VT.x + _draw_major.shadow_parrallax.x*_shadow_height
self.VT.scale = self.VT.scale/(1-0.2*_shadow_height)
end
end
function Sprite:draw_self(overlay)
if not self.states.visible then return end
if self.sprite_pos.x ~= self.sprite_pos_copy.x or self.sprite_pos.y ~= self.sprite_pos_copy.y then
self:set_sprite_pos(self.sprite_pos)
end
prep_draw(self, 1)
love.graphics.scale(1/(self.scale.x/self.VT.w), 1/(self.scale.y/self.VT.h))
love.graphics.setColor(overlay or G.BRUTE_OVERLAY or G.C.WHITE)
if self.video then
self.video_dims = self.video_dims or {
w = self.video:getWidth(),
h = self.video:getHeight(),
}
love.graphics.draw(
self.video,
0 ,0,
0,
self.VT.w/(self.T.w)/(self.video_dims.w/self.scale.x),
self.VT.h/(self.T.h)/(self.video_dims.h/self.scale.y)
)
else
love.graphics.draw(
self.atlas.image,
self.sprite,
0 ,0,
0,
self.VT.w/(self.T.w),
self.VT.h/(self.T.h)
)
end
love.graphics.pop()
add_to_drawhash(self)
self:draw_boundingrect()
if self.shader_tab then love.graphics.setShader() end
end
function Sprite:draw(overlay)
if not self.states.visible then return end
if self.draw_steps then
for k, v in ipairs(self.draw_steps) do
self:draw_shader(v.shader, v.shadow_height, v.send, v.no_tilt, v.other_obj, v.ms, v.mr, v.mx, v.my, not not v.send)
end
else
self:draw_self(overlay)
end
add_to_drawhash(self)
for k, v in pairs(self.children) do
if k ~= 'h_popup' then v:draw() end
end
add_to_drawhash(self)
self:draw_boundingrect()
end
function Sprite:draw_from(other_obj, ms, mr, mx, my)
self.ARGS.draw_from_offset = self.ARGS.draw_from_offset or {}
self.ARGS.draw_from_offset.x = mx or 0
self.ARGS.draw_from_offset.y = my or 0
prep_draw(other_obj, (1 + (ms or 0)), (mr or 0), self.ARGS.draw_from_offset, true)
love.graphics.scale(1/(other_obj.scale_mag or other_obj.VT.scale))
love.graphics.setColor(G.BRUTE_OVERLAY or G.C.WHITE)
love.graphics.draw(
self.atlas.image,
self.sprite,
-(other_obj.T.w/2 -other_obj.VT.w/2)*10,
0,
0,
other_obj.VT.w/(other_obj.T.w),
other_obj.VT.h/(other_obj.T.h)
)
self:draw_boundingrect()
love.graphics.pop()
end
function Sprite:remove()
if self.video then
self.video:release()
end
for k, v in pairs(G.ANIMATIONS) do
if v == self then
table.remove(G.ANIMATIONS, k)
end
end
for k, v in pairs(G.I.SPRITE) do
if v == self then
table.remove(G.I.SPRITE, k)
end
end
Moveable.remove(self)
end

72
engine/string_packer.lua Normal file
View File

@@ -0,0 +1,72 @@
--[[
MIT License
Copyright (c) 2017 Robert Herlihy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
--I modified this A LOT. Needed to make it quicker if it is being saved to file every few seconds during a game
function STR_PACK(data, recursive)
local ret_str = (recursive and "" or "return ").."{"
for i, v in pairs(data) do
local type_i, type_v = type(i), type(v)
assert((type_i ~= "table"), "Data table cannot have an table as a key reference")
if type_i == "string" then
i = '['..string.format("%q",i)..']'
else
i = "["..i.."]"
end
if type_v == "table" then
if v.is and v:is(Object) then
v = [["]].."MANUAL_REPLACE"..[["]]
else
v = STR_PACK(v, true)
end
else
if type_v == "string" then v = string.format("%q", v) end
if type_v == "boolean" then v = v and "true" or "false" end
end
ret_str = ret_str..i.."="..v..","
end
return ret_str.."}"
end
function STR_UNPACK(str)
return assert(loadstring(str))()
end
function get_compressed(_file)
local file_data = love.filesystem.getInfo(_file)
if file_data ~= nil then
local file_string = love.filesystem.read(_file)
if file_string ~= '' then
if string.sub(file_string, 1, 6) ~= 'return' then
local success = nil
success, file_string = pcall(love.data.decompress, 'string', 'deflate', file_string)
if not success then return nil end
end
return file_string
end
end
end
function compress_and_save(_file, _data)
local save_string = type(_data) == 'table' and STR_PACK(_data) or _data
save_string = love.data.compress('string', 'deflate', save_string, 1)
love.filesystem.write(_file,save_string)
end

315
engine/text.lua Normal file
View File

@@ -0,0 +1,315 @@
--Class
DynaText = Moveable:extend()
--Class Methods
function DynaText:init(config)
config = config or {}
self.config = config
self.shadow = config.shadow
self.scale = config.scale or 1
self.pop_in_rate = config.pop_in_rate or 3
self.bump_rate = config.bump_rate or 2.666
self.bump_amount = config.bump_amount or 1
self.font = config.font or G.LANG.font
if config.string and type(config.string) ~= 'table' then config.string = {config.string} end
self.string = (config.string and type(config.string) == 'table' and config.string[1]) or {'HELLO WORLD'}
self.text_offset = {
x = self.font.TEXT_OFFSET.x*self.scale + (self.config.x_offset or 0),
y = self.font.TEXT_OFFSET.y*self.scale + (self.config.y_offset or 0),
}
self.colours = config.colours or {G.C.RED}
self.created_time = G.TIMERS.REAL
self.silent = (config.silent)
self.start_pop_in = self.config.pop_in
config.W = 0
config.H = 0
self.strings = {}
self.focused_string = 1
self:update_text(true)
if self.config.maxw and self.config.W > self.config.maxw then
self.start_pop_in = self.config.pop_in
self.scale = self.scale*(self.config.maxw/self.config.W)
self:update_text(true)
end
if #self.strings > 1 then
self.pop_delay = self.config.pop_delay or 1.5
self:pop_out(4)
end
Moveable.init(self,config.X or 0, config.Y or 0, config.W, config.H)
self.T.r = self.config.text_rot or 0
self.states.hover.can = false
self.states.click.can = false
self.states.collide.can = false
self.states.drag.can = false
self.states.release_on.can = false
self:set_role{
wh_bond = 'Weak',
scale_bond = 'Weak'
}
if getmetatable(self) == DynaText then
table.insert(G.I.MOVEABLE, self)
end
end
function DynaText:update(dt)
self:update_text()
self:align_letters()
end
function DynaText:update_text(first_pass)
self.config.W = 0
self.config.H = 0
for k, v in ipairs(self.config.string) do
if (type(v) == 'table' and v.ref_table) or first_pass then
local part_a, part_b = 0,1000000
local new_string = v
local outer_colour = nil
local inner_colour = nil
local part_scale = 1
if type(v) == 'table' and (v.ref_table or v.string) then
new_string = (v.prefix or '')..tostring(v.ref_table and v.ref_table[v.ref_value] or v.string)..(v.suffix or '')
part_a = #(v.prefix or '')
part_b = #new_string - #(v.suffix or '')
if v.scale then part_scale = v.scale end
if first_pass then
outer_colour = v.outer_colour or nil
inner_colour = v.colour or nil
end
v = new_string
end
self.strings[k] = self.strings[k] or {}
local old_string = self.strings[k].string
if old_string ~= new_string or first_pass then
if self.start_pop_in then self.reset_pop_in = true end
self.reset_pop_in = self.reset_pop_in or self.config.reset_pop_in
if not self.reset_pop_in then
self.config.pop_out = nil
self.config.pop_in = nil
else
self.config.pop_in = self.config.pop_in or 0
self.created_time = G.TIMERS.REAL
end
self.strings[k].string = v
local old_letters = self.strings[k].letters
local tempW = 0
local tempH = 0
local current_letter = 1
self.strings[k].letters = {}--EMPTY(self.strings[k].letters)
for _, c in utf8.chars(v) do
local old_letter = old_letters and old_letters[current_letter] or nil
local let_tab = {letter = love.graphics.newText(self.font.FONT, c), char = c, scale = old_letter and old_letter.scale or part_scale}
self.strings[k].letters[current_letter] = let_tab
local tx = self.font.FONT:getWidth(c)*self.scale*part_scale*G.TILESCALE*self.font.FONTSCALE + 2.7*(self.config.spacing or 0)*G.TILESCALE*self.font.FONTSCALE
local ty = self.font.FONT:getHeight(c)*self.scale*part_scale*G.TILESCALE*self.font.FONTSCALE*self.font.TEXT_HEIGHT_SCALE
let_tab.offset = old_letter and old_letter.offset or {x = 0, y = 0}
let_tab.dims = {x = tx/(self.font.FONTSCALE*G.TILESCALE), y = ty/(self.font.FONTSCALE*G.TILESCALE)}
let_tab.pop_in = first_pass and (old_letter and old_letter.pop_in or (self.config.pop_in and 0 or 1)) or 1
let_tab.prefix = current_letter <= part_a and outer_colour or nil
let_tab.suffix = current_letter > part_b and outer_colour or nil
let_tab.colour = inner_colour or nil
if k > 1 then let_tab.pop_in = 0 end
tempW = tempW + tx/(G.TILESIZE*G.TILESCALE)
tempH = math.max(ty/(G.TILESIZE*G.TILESCALE), tempH)
current_letter = current_letter + 1
end
self.strings[k].W = tempW
self.strings[k].H = tempH
end
end
if self.strings[k].W > self.config.W then self.config.W = self.strings[k].W; self.strings[k].W_offset = 0 end
if self.strings[k].H > self.config.H then self.config.H = self.strings[k].H; self.strings[k].H_offset = 0 end
end
if self.T then
if (self.T.w ~= self.config.W or self.T.h ~= self.config.H) and (not first_pass or self.reset_pop_in) then
self.ui_object_updated = true
self.non_recalc = self.config.non_recalc
end
self.T.w = self.config.W
self.T.h = self.config.H
end
self.reset_pop_in = false
self.start_pop_in = false
for k, v in ipairs(self.strings) do
v.W_offset = 0.5*(self.config.W - v.W)
v.H_offset = 0.5*(self.config.H - v.H + (self.config.offset_y or 0))
end
end
function DynaText:pop_out(pop_out_timer)
self.config.pop_out = pop_out_timer or 1
self.pop_out_time = G.TIMERS.REAL + (self.pop_delay or 0)
end
function DynaText:pop_in(pop_in_timer)
self.reset_pop_in = true
self.config.pop_out = nil
self.config.pop_in = pop_in_timer or 0
self.created_time = G.TIMERS.REAL
for k, letter in ipairs(self.strings[self.focused_string].letters) do
letter.pop_in = 0
end
self:update_text()
end
function DynaText:align_letters()
if self.pop_cycle then
self.focused_string = (self.config.random_element and math.random(1, #self.strings)) or self.focused_string == #self.strings and 1 or self.focused_string+1
self.pop_cycle = false
for k, letter in ipairs(self.strings[self.focused_string].letters) do
letter.pop_in = 0
end
self.config.pop_in = 0.1
self.config.pop_out = nil
self.created_time = G.TIMERS.REAL
end
self.string = self.strings[self.focused_string].string
for k, letter in ipairs(self.strings[self.focused_string].letters) do
if self.config.pop_out then
letter.pop_in = math.min(1, math.max((self.config.min_cycle_time or 1)-(G.TIMERS.REAL - self.pop_out_time)*self.config.pop_out/(self.config.min_cycle_time or 1), 0))
letter.pop_in = letter.pop_in*letter.pop_in
if k == #self.strings[self.focused_string].letters and letter.pop_in <= 0 and #self.strings > 1 then self.pop_cycle = true end
elseif self.config.pop_in then
local prev_pop_in = letter.pop_in
letter.pop_in = math.min(1, math.max((G.TIMERS.REAL - self.config.pop_in - self.created_time)*#self.string*self.pop_in_rate - k + 1, self.config.min_cycle_time == 0 and 1 or 0))
letter.pop_in = letter.pop_in*letter.pop_in
if prev_pop_in <=0 and letter.pop_in > 0 and not self.silent and
(#self.string < 10 or k%2 == 0) then
if self.T.x > G.ROOM.T.w+2 or
self.T.y > G.ROOM.T.h+2 or
self.T.x <-2 or
self.T.y <-2 then else
play_sound('paper1', 0.45+0.05*math.random()+(0.3/#self.string)*k + (self.config.pitch_shift or 0))
end
end
if k == #self.strings[self.focused_string].letters and letter.pop_in >= 1 then
if #self.strings > 1 then
self.pop_delay = (G.TIMERS.REAL - self.config.pop_in - self.created_time + (self.config.pop_delay or 1.5))
self:pop_out(4)
else
self.config.pop_in = nil
end
end
end
letter.r = 0
letter.scale = 1
if self.config.rotate then letter.r = (self.config.rotate == 2 and -1 or 1)*(0.2*(-#self.strings[self.focused_string].letters/2 - 0.5 + k)/(#self.strings[self.focused_string].letters)+ 0.02*math.sin(2*G.TIMERS.REAL+k)) end
if self.config.pulse then
letter.scale = letter.scale + (1/self.config.pulse.width)*self.config.pulse.amount*(math.max(
math.min((self.config.pulse.start - G.TIMERS.REAL)*self.config.pulse.speed + k + self.config.pulse.width,
(G.TIMERS.REAL - self.config.pulse.start)*self.config.pulse.speed - k + self.config.pulse.width+ 2),
0))
letter.r = letter.r + (letter.scale - 1)*(0.02*(-#self.strings[self.focused_string].letters/2 - 0.5 + k))
if self.config.pulse.start > G.TIMERS.REAL + 2*self.config.pulse.speed*#self.strings[self.focused_string].letters then
self.config.pulse = nil
end
end
if self.config.quiver then
letter.scale = letter.scale + (0.1*self.config.quiver.amount)
letter.r = letter.r + 0.3*self.config.quiver.amount*(
math.sin(41.12342*G.TIMERS.REAL*self.config.quiver.speed + k*1223.2) +
math.cos(63.21231*G.TIMERS.REAL*self.config.quiver.speed + k*1112.2)*math.sin(36.1231*G.TIMERS.REAL*self.config.quiver.speed) +
math.cos(95.123*G.TIMERS.REAL*self.config.quiver.speed + k*1233.2) -
math.sin(30.133421*G.TIMERS.REAL*self.config.quiver.speed + k*123.2))
end
if self.config.float then letter.offset.y = math.sqrt(self.scale)*(2+(self.font.FONTSCALE/G.TILESIZE)*2000*math.sin(2.666*G.TIMERS.REAL+200*k)) + 60*(letter.scale-1) end
if self.config.bump then letter.offset.y = self.bump_amount*math.sqrt(self.scale)*7*math.max(0, (5+self.bump_rate)*math.sin(self.bump_rate*G.TIMERS.REAL+200*k) - 3 - self.bump_rate) end
end
end
function DynaText:set_quiver(amt)
self.config.quiver = {
speed = 0.5,
amount = amt or 0.7,
silent = false
}
end
function DynaText:pulse(amt)
self.config.pulse = {
speed = 40,
width = 2.5,
start = G.TIMERS.REAL,
amount = amt or 0.2,
silent = false
}
end
function DynaText:draw()
if self.children.particle_effect then self.children.particle_effect:draw() end
if self.shadow then
prep_draw(self, 1)
love.graphics.translate(self.strings[self.focused_string].W_offset + self.text_offset.x*self.font.FONTSCALE/G.TILESIZE, self.strings[self.focused_string].H_offset + self.text_offset.y*self.font.FONTSCALE/G.TILESIZE)
if self.config.spacing then love.graphics.translate(self.config.spacing*self.font.FONTSCALE/G.TILESIZE, 0) end
if self.config.shadow_colour then
love.graphics.setColor(self.config.shadow_colour)
else
love.graphics.setColor(0, 0, 0, 0.3*self.colours[1][4])
end
for k, letter in ipairs(self.strings[self.focused_string].letters) do
local real_pop_in = self.config.min_cycle_time == 0 and 1 or letter.pop_in
love.graphics.draw(
letter.letter,
0.5*(letter.dims.x - letter.offset.x)*self.font.FONTSCALE/G.TILESIZE -self.shadow_parrallax.x*self.scale/(G.TILESIZE),
0.5*(letter.dims.y)*self.font.FONTSCALE/G.TILESIZE -self.shadow_parrallax.y*self.scale/(G.TILESIZE),
letter.r or 0,
real_pop_in*self.scale*self.font.FONTSCALE/G.TILESIZE,
real_pop_in*self.scale*self.font.FONTSCALE/G.TILESIZE,
0.5*letter.dims.x/self.scale,
0.5*letter.dims.y/self.scale
)
love.graphics.translate(letter.dims.x*self.font.FONTSCALE/G.TILESIZE, 0)
end
love.graphics.pop()
end
prep_draw(self, 1)
love.graphics.translate(self.strings[self.focused_string].W_offset + self.text_offset.x*self.font.FONTSCALE/G.TILESIZE, self.strings[self.focused_string].H_offset + self.text_offset.y*self.font.FONTSCALE/G.TILESIZE)
if self.config.spacing then love.graphics.translate(self.config.spacing*self.font.FONTSCALE/G.TILESIZE, 0) end
self.ARGS.draw_shadow_norm = self.ARGS.draw_shadow_norm or {}
local _shadow_norm = self.ARGS.draw_shadow_norm
_shadow_norm.x, _shadow_norm.y =
self.shadow_parrallax.x/math.sqrt(self.shadow_parrallax.y*self.shadow_parrallax.y + self.shadow_parrallax.x*self.shadow_parrallax.x)*self.font.FONTSCALE/G.TILESIZE,
self.shadow_parrallax.y/math.sqrt(self.shadow_parrallax.y*self.shadow_parrallax.y + self.shadow_parrallax.x*self.shadow_parrallax.x)*self.font.FONTSCALE/G.TILESIZE
for k, letter in ipairs(self.strings[self.focused_string].letters) do
local real_pop_in = self.config.min_cycle_time == 0 and 1 or letter.pop_in
love.graphics.setColor(letter.prefix or letter.suffix or letter.colour or self.colours[k%#self.colours + 1])
love.graphics.draw(
letter.letter,
0.5*(letter.dims.x - letter.offset.x)*self.font.FONTSCALE/G.TILESIZE + _shadow_norm.x,
0.5*(letter.dims.y - letter.offset.y)*self.font.FONTSCALE/G.TILESIZE + _shadow_norm.y,
letter.r or 0,
real_pop_in*letter.scale*self.scale*self.font.FONTSCALE/G.TILESIZE,
real_pop_in*letter.scale*self.scale*self.font.FONTSCALE/G.TILESIZE,
0.5*letter.dims.x/(self.scale),
0.5*letter.dims.y/(self.scale)
)
love.graphics.translate(letter.dims.x*self.font.FONTSCALE/G.TILESIZE, 0)
end
love.graphics.pop()
add_to_drawhash(self)
self:draw_boundingrect()
end

1049
engine/ui.lua Normal file

File diff suppressed because it is too large Load Diff