Initial Commit - 1.0.0k
This commit is contained in:
107
engine/animatedsprite.lua
Normal file
107
engine/animatedsprite.lua
Normal 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
1380
engine/controller.lua
Normal file
File diff suppressed because it is too large
Load Diff
195
engine/event.lua
Normal file
195
engine/event.lua
Normal 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
23
engine/http_manager.lua
Normal 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
502
engine/moveable.lua
Normal 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
389
engine/node.lua
Normal 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
37
engine/object.lua
Normal 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
177
engine/particles.lua
Normal 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
188
engine/profile.lua
Normal 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
84
engine/save_manager.lua
Normal 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
207
engine/sound_manager.lua
Normal 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
215
engine/sprite.lua
Normal 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
72
engine/string_packer.lua
Normal 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
315
engine/text.lua
Normal 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
1049
engine/ui.lua
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user