332 lines
12 KiB
Lua
332 lines
12 KiB
Lua
|
|
local error = require 'errors' 'SpriteSheet2'
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Util
|
|
|
|
local function calculate_animation_duration (self, frame_i)
|
|
local frame_i = frame_i or math.huge
|
|
assert(type(self) == 'table')
|
|
assert(type(frame_i) == 'number')
|
|
-- Easy if number
|
|
if type(self.time) == 'number' then return math.min(#self, frame_i) * self.time end
|
|
-- Sum if table
|
|
local sum = 0
|
|
for i = 1, math.min(#self, frame_i) do sum = sum + self.time[i] end
|
|
return sum
|
|
end
|
|
|
|
local function get_quad_based_on_time (l, t)
|
|
-- TODO: Reimplement as binary search. (Maybe only use the binary search version for very long animations?)
|
|
--
|
|
assert(type(l) == 'table')
|
|
assert(type(t) == 'number')
|
|
--
|
|
local time = l.time
|
|
for i = 1, #time do
|
|
if t <= time[i] then
|
|
return l[i]
|
|
end
|
|
end
|
|
--
|
|
return l[#l]
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
---- Sprite
|
|
|
|
local Sprite = {}
|
|
Sprite.__index = Sprite
|
|
|
|
function Sprite.new (quad, imagesheet)
|
|
return setmetatable({ quad = quad, imagesheet = imagesheet, is_sprite = true }, Sprite)
|
|
end
|
|
|
|
function Sprite:generateImage ()
|
|
local imagesheet, quad = self.imagesheet, self.quad
|
|
self.func = function (x, y)
|
|
love.graphics.draw(imagesheet.image, quad, math.floor(x), math.floor(y), 0, 1, 1, imagesheet.origin_x, imagesheet.origin_y)
|
|
end
|
|
end
|
|
|
|
function Sprite:getImage ()
|
|
if not self.func then self:generateImage() end
|
|
return self.func
|
|
end
|
|
|
|
function Sprite:getQuad ()
|
|
return self.quad
|
|
end
|
|
|
|
function Sprite:draw(...)
|
|
return self:getImage()(...)
|
|
end
|
|
|
|
function Sprite:getSheetDimensions()
|
|
return self.imagesheet.image:getDimensions()
|
|
end
|
|
|
|
setmetatable(Sprite, {__call = function(_, ...) return Sprite.new(...) end})
|
|
|
|
--------------------------------------------------------------------------------
|
|
---- Animation
|
|
|
|
local Animation = {}
|
|
Animation.__index = Animation
|
|
|
|
function Animation.new (self)
|
|
assert(type(self) == 'table')
|
|
assert(type(self.time) == 'table' or type(self.time) == 'number' and self.time > 0)
|
|
assert(#self > 0)
|
|
assert(type(self.time) == 'number' or #self == #self.time)
|
|
|
|
assert(self.wrap == nil or self.wrap == true or self.wrap == false)
|
|
|
|
local self = setmetatable(self, Animation)
|
|
self.duration = calculate_animation_duration(self)
|
|
self.is_animation = true
|
|
-- Contact frame?
|
|
if self.contact_frame then
|
|
-- why was this minus one? It screwed up the audio.
|
|
--self.contact_time = calculate_animation_duration(self, self.contact_frame - 1)
|
|
self.contact_time = calculate_animation_duration(self, self.contact_frame)
|
|
end
|
|
--
|
|
return self
|
|
end
|
|
|
|
function Animation:generateImage ()
|
|
self.func = function (x, y, t)
|
|
t = t or 0
|
|
assert(type(t) == 'number')
|
|
|
|
if self.wrap then t = t % self.duration end
|
|
local quad = get_quad_based_on_time(self, t)
|
|
if not quad then error.internal('Could not determine quad when drawing animation. Time was %f.', t) end
|
|
love.graphics.draw(self.imagesheet.image, quad, x, y, 0, 1, 1, self.imagesheet.origin_x, self.imagesheet.origin_y)
|
|
end
|
|
end
|
|
|
|
function Animation:getImage ()
|
|
if not self.func then self:generateImage() end
|
|
return self.func
|
|
end
|
|
|
|
function Animation:getQuad (i)
|
|
assert(i == nil or type(i) == 'number' and self[i])
|
|
return self[i or 1]
|
|
end
|
|
|
|
function Animation:getDuration ()
|
|
return self.duration
|
|
end
|
|
|
|
function Animation:draw(...)
|
|
return self:getImage()(...)
|
|
end
|
|
|
|
function Animation:getSheetDimensions()
|
|
return self.imagesheet.image:getDimensions()
|
|
end
|
|
|
|
setmetatable(Animation, {__call = function(_, ...) return Animation.new(...) end})
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Constants
|
|
|
|
local SPRITESHEET_ENV = { Anim = Animation }
|
|
local SPRITESHEET_DATA_FILETYPES = { ['.lua'] = true, ['.raw'] = true }
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function calculate_frame_times (time_list, nr_frames)
|
|
assert(type(time_list) == 'table' or type(time_list) == 'number')
|
|
assert(type(nr_frames) == 'number')
|
|
|
|
local frame_times = { [0] = -math.huge }
|
|
|
|
if type(time_list) == 'number' then
|
|
for i = 1, nr_frames do
|
|
frame_times[i] = i * time_list
|
|
end
|
|
else
|
|
frame_times[1] = time_list[1]
|
|
for i = 2, nr_frames do
|
|
frame_times[i] = frame_times[i-1] + time_list[i]
|
|
end
|
|
end
|
|
|
|
return frame_times
|
|
end
|
|
|
|
local function load_quads (image, quad_data, imagesheet)
|
|
assert(image.typeOf and image:typeOf('Image'))
|
|
assert(type(quad_data) == 'table')
|
|
|
|
local image_width, image_height = image:getDimensions()
|
|
local tile_width, tile_height = quad_data.tile_width, quad_data.tile_height
|
|
local tiles_pr_row = image_width/tile_width
|
|
local max_quad_id = tiles_pr_row * (image_height/tile_height) - 1
|
|
local quad_cache = {}
|
|
|
|
local function quad_from_id (id)
|
|
-- Error checking
|
|
if type(id) ~= 'number' then error('All quad ids must be natural numbers, but one was %s (%s)', id, type(id)) end
|
|
if id ~= math.floor(id) then error('All quad ids must be natural numbers, but one was %03.03f (floating point number)', id) end
|
|
if not (0 <= id and id <= max_quad_id) then error('All quad ids must - for this spritesheet ("%s") - be natural numbers equal/below %i, but one was %s = 0x%X', imagesheet.filename, max_quad_id, id, id) end
|
|
|
|
-- Calculate
|
|
if not quad_cache[id] then
|
|
quad_cache[id] = love.graphics.newQuad((id%tiles_pr_row)*tile_width, math.floor(id/tiles_pr_row)*tile_height, tile_width, tile_height, image_width, image_height)
|
|
end
|
|
return quad_cache[id]
|
|
end
|
|
|
|
local function visit_animation (t)
|
|
assert(type(t) == 'table' and t.is_animation)
|
|
for i = 1, #t do t[i] = quad_from_id(t[i]) end
|
|
t.time = calculate_frame_times(t.time, #t)
|
|
t.imagesheet = imagesheet
|
|
end
|
|
|
|
local function visit_quad (n)
|
|
assert(type(n) == 'number')
|
|
return Sprite(quad_from_id(n), imagesheet)
|
|
end
|
|
|
|
local function visit_node (t)
|
|
assert(type(t) == 'table')
|
|
|
|
for key, val in pairs(t) do
|
|
local val_type = type(val)
|
|
if val_type == 'number' then
|
|
t[key] = visit_quad(val)
|
|
elseif val_type == 'table' then
|
|
local visit_func = val.is_animation and visit_animation or visit_node
|
|
visit_func(val)
|
|
end
|
|
end
|
|
|
|
error.strict_table(t)
|
|
end
|
|
|
|
assert(type(quad_data) == 'table')
|
|
assert(type(quad_data.tile_names) == 'table')
|
|
|
|
local visit_func = quad_data.tile_names.is_animation and visit_animation or visit_node
|
|
visit_func(quad_data.tile_names)
|
|
return quad_data.tile_names
|
|
end
|
|
|
|
local function load_quad_data (filename)
|
|
if type(filename) ~= 'string' then error('Bad argument #1, expected string, got %s (%s)', filename, type(filename)) end
|
|
|
|
-- Attempt to load file
|
|
for filetype in pairs(SPRITESHEET_DATA_FILETYPES) do
|
|
local chunk, error_msg = love.filesystem.load(filename..'.lua')
|
|
if chunk then
|
|
local data = setfenv(chunk, SPRITESHEET_ENV)()
|
|
-- Error check
|
|
if type(data) ~= 'table' then error('Bad spritesheet "%s". Must return a table, but returned %s (%s)', filename, data, type(data)) end
|
|
if type(data.tile_width) ~= 'number' then error('Bad spritesheet "%s". The root table must contain key "tile_width", with integer value, but it was %s (%s)', filename, data.tile_width, type(data.tile_width)) end
|
|
if data.tile_width ~= math.floor(data.tile_width) then error('Bad spritesheet "%s". The root table must contain key "tile_width", with integer value, but it was %f (float)', filename, data.tile_width) end
|
|
if type(data.tile_height) ~= 'number' then error('Bad spritesheet "%s". The root table must contain key "tile_height", with integer value, but it was %s (%s)', filename, data.tile_height, type(data.tile_height)) end
|
|
if data.tile_height ~= math.floor(data.tile_height) then error('Bad spritesheet "%s". The root table must contain key "tile_height", with integer value, but it was %f (float)', filename, data.tile_height) end
|
|
if not (type(data.tile_names) == 'table' or type(data.tile_names) == 'number') then error('Bad spritesheet "%s". The root table must contain key "tile_names", with either a table or a number value, but it was %s (%s)', filename, data.tile_names, type(data.tile_names)) end
|
|
if data.tile_origin and type(data.tile_origin) ~= 'table' then error('Bad spritesheet "%s". If the root table contains key "tile_origin", it must be a table value, but it was %s (%s)', filename, data.tile_origin, type(data.tile_origin)) end
|
|
--
|
|
return data
|
|
end
|
|
print(error_msg)
|
|
end
|
|
|
|
-- Else, give up.
|
|
error('Could not find file "%s.lua" or "%s.raw".', filename, filename)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
local SpriteSheet = {}
|
|
SpriteSheet.__index = SpriteSheet
|
|
SpriteSheet.is_spritesheet = true
|
|
|
|
function SpriteSheet.new (filename)
|
|
local quad_data = load_quad_data(filename)
|
|
|
|
-- Set info
|
|
local self = setmetatable({}, SpriteSheet)
|
|
self.filename = filename
|
|
self.image = love.graphics.newImage(filename..'.png')
|
|
self.origin_x = 0
|
|
self.origin_y = 0
|
|
self.tile_width = quad_data.tile_width
|
|
self.tile_height = quad_data.tile_height
|
|
|
|
-- Error checking
|
|
if self.image:getWidth() % self.tile_width ~= 0 then error('Bad spritesheet "%s". Image size (%i, %i) must be dividable by tile size (%i, %i), but width leaves a remainder of %i.', filename, self.image:getWidth(), self.image:getHeight(), self.tile_width, self.tile_height, self.image:getWidth() % self.tile_width) end
|
|
if self.image:getHeight() % self.tile_height ~= 0 then error('Bad spritesheet "%s". Image size (%i, %i) must be dividable by tile size (%i, %i), but height leaves a remainder of %i.', filename, self.image:getWidth(), self.image:getHeight(), self.tile_width, self.tile_height, self.image:getHeight() % self.tile_height) end
|
|
|
|
-- Set origin
|
|
if quad_data.tile_origin then
|
|
self:setOrigin(unpack(quad_data.tile_origin))
|
|
end
|
|
|
|
-- Import quads into SpriteSheet
|
|
self.quads = load_quads(self.image, quad_data, self)
|
|
if rawget(self.quads, 'is_sprite') or rawget(self.quads, 'is_animation') then
|
|
self.only_quads = self.quads
|
|
else
|
|
for key, value in pairs(self.quads) do
|
|
assert(not self[key])
|
|
self[key] = value
|
|
end
|
|
end
|
|
|
|
-- Return
|
|
return self
|
|
end
|
|
|
|
function SpriteSheet:setOrigin (ox, oy, mode)
|
|
assert(type(ox) == 'number')
|
|
assert(type(oy) == 'number')
|
|
|
|
if mode == 'absolute' then
|
|
self.origin_x = ox
|
|
self.origin_y = oy
|
|
elseif mode == 'relative' or mode == nil then
|
|
self.origin_x = self.tile_width * ox
|
|
self.origin_y = self.tile_height * oy
|
|
else
|
|
error('Unknown origin mode "%s"', mode)
|
|
end
|
|
end
|
|
|
|
function SpriteSheet:getQuadKeys ()
|
|
local keys = {}
|
|
for key in pairs(self.quads) do keys[#keys+1] = key end
|
|
return keys
|
|
end
|
|
|
|
local function assert_self (self_v, is_field)
|
|
if type(self_v) ~= 'table' then error('Bad self value, must be indexable, but was %s (%s).', self_v, type(self_v)) end
|
|
if is_field and not self_v[is_field] then error('Bad self value, is indexable, but does not possess "%s" field.', is_field) end
|
|
return true
|
|
end
|
|
|
|
-- Create redirections
|
|
for _, method_name in pairs{ 'getImage', 'getQuad', 'draw', 'getDuration' } do
|
|
SpriteSheet[method_name] = function (self, ...)
|
|
assert_self (self, 'is_spritesheet')
|
|
if not self.only_quads then error('Attempting to call method "%s" on a SpriteSheet ("%s"). This is only allowed when the Spritesheet contains a single sprite or animation.', method_name, self.filename) end
|
|
return self.only_quads[method_name](self.only_quads, ...)
|
|
end
|
|
end
|
|
|
|
setmetatable(SpriteSheet, {__call = function(_, ...) return SpriteSheet.new(...) end})
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Return
|
|
|
|
return SpriteSheet
|
|
|