Get basic ht anims working

This commit is contained in:
Local Atticus 2022-04-26 18:12:14 -04:00 committed by nashiora
parent 789038d24f
commit 5268ac47a5
31 changed files with 565 additions and 66 deletions

202
scripts/api/animation.lua Normal file
View File

@ -0,0 +1,202 @@
require "common.class"
require "api.graphics"
local Image = require "api.image"
---@class AnimationParams
---@field fps number?
---@field loop boolean?
---@field loopPoint integer?
---@field width number?
---@field height number?
---@field x number?
---@field y number?
---@field scaleX number?
---@field scaleY number?
---@field centered boolean?
---@field blendOp integer?
---@field color number[]?
---@field alpha number?
---@field stroke StrokeParams?
---@class Animation
---@field frames Image[]
---@field frameCount integer
---@field frameTime number
---@field loop boolean
---@field loopPoint integer
---@field width number?
---@field height number?
---@field x number?
---@field y number?
---@field scaleX number?
---@field scaleY number?
---@field centered boolean?
---@field blendOp integer?
---@field color number[]?
---@field alpha number?
---@field stroke StrokeParams?
local Animation = { };
---@class AnimationState
---@field animation Animation # The animation data this state is playing through
---@field frameIndex integer # Current frame in the animation
---@field timer number # Timer used to determine when to change to the next frame
---@field running boolean # Is the animation currently running and accepting updates?
---@field callback function? # Called when the animation completes
local AnimationState = { };
local function loadSequentialAnimationFrames(animPath)
local frames = { };
local count = 0;
local detectedFormat = nil;
while (true) do
local frame = nil;
if (detectedFormat) then
frame = Image.new(detectedFormat:format(animPath, count + 1), true);
else
for i = 1, 4 do
local format = '%s/%0' .. i .. 'd.png';
frame = Image.new(format:format(animPath, count + 1), true);
if (frame) then
detectedFormat = format;
break;
end
end
end
if (not frame) then
break;
end
count = count + 1;
frames[count] = frame;
end
return frames, count;
end
---Animation constructor
---@param animPath string
---@param params AnimationParams
---@return Animation
function Animation.new(animPath, params)
local frames, frameCount = loadSequentialAnimationFrames(animPath);
local instance = {
frames = frames,
frameCount = frameCount,
frameTime = 1 / (params.fps or 30),
loop = params.loop or false,
loopPoint = params.loopPoint or 1,
};
if (params.width ~= nil) then instance.width = params.width; end
if (params.height ~= nil) then instance.height = params.height; end
if (params.x ~= nil) then instance.x = params.x; end
if (params.y ~= nil) then instance.y = params.y; end
if (params.scaleX ~= nil) then instance.scaleX = params.scaleX; end
if (params.scaleY ~= nil) then instance.scaleY = params.scaleY; end
if (params.centered ~= nil) then instance.centered = params.centered; end
if (params.blendOp ~= nil) then instance.blendOp = params.blendOp; end
if (params.color ~= nil) then instance.color = params.color; end
if (params.alpha ~= nil) then instance.alpha = params.alpha; end
if (params.stroke ~= nil) then instance.stroke = params.stroke; end
return Base(Animation, instance);
end
---Create an AnimationState to play this animation.
---The AnimationState is not started.
---@param callback function?
---@return AnimationState
function Animation:createState(callback)
---@type AnimationState
local state = { animation = self, callback = callback, frameIndex = 1, timer = 0, running = false };
return Base(AnimationState, state);
end
---Create an AnimationState to play this animation and start it.
---@param callback function?
---@return AnimationState
function Animation:start(callback)
local state = self:createState(callback);
state:start();
return state;
end
---Start this AnimationState.
---Does nothing if it's already running.
function AnimationState:start()
self.running = true;
end
---Restart this AnimationState.
---The frame index is reset to 1.
function AnimationState:restart()
self.running = true;
self.frameIndex = 1;
self.timer = 0;
end
---Stop this AnimationState.
function AnimationState:stop()
self.running = false;
end
---Updates this AnimationState and then rendersit, passing on the given ImageParams to each frame.
---@param deltaTime number
---@param params? ImageParams
function AnimationState:render(deltaTime, params)
if (not self.running) then return; end;
self.timer = self.timer + deltaTime;
while (self.timer > self.animation.frameTime) do
self.timer = self.timer - self.animation.frameTime;
self.frameIndex = self.frameIndex + 1;
if (self.frameIndex > self.animation.frameCount) then
if (self.animation.loop) then
self.frameIndex = self.animation.loopPoint;
else
self.running = false;
if (self.callback) then
self.callback();
end
return;
end
end
end
if (params) then
if (params.width == nil) then params.width = self.animation.width; end
if (params.height == nil) then params.height = self.animation.height; end
if (params.x == nil) then params.x = self.animation.x; end
if (params.y == nil) then params.y = self.animation.y; end
if (params.scaleX == nil) then params.scaleX = self.animation.scaleX; end
if (params.scaleY == nil) then params.scaleY = self.animation.scaleY; end
if (params.centered == nil) then params.centered = self.animation.centered; end
if (params.blendOp == nil) then params.blendOp = self.animation.blendOp; end
if (params.alpha == nil) then params.alpha = self.animation.alpha; end
if (params.stroke == nil) then params.stroke = self.animation.stroke; end
end
local frame = self.animation.frames[self.frameIndex];
if (not frame) then
-- TODO(local): what do
else
frame:render(params);
end
end
return Animation;

6
scripts/api/graphics.lua Normal file
View File

@ -0,0 +1,6 @@
-- TODO(local): put these class types somewhere more common
---@class StrokeParams
---@field color number[]?
---@field alpha number?
---@field size number?

171
scripts/api/image.lua Normal file
View File

@ -0,0 +1,171 @@
require "common.class"
require "api.graphics"
---@class ImageParams
---@field width number
---@field height number
---@field x number?
---@field y number?
---@field scaleX number?
---@field scaleY number?
---@field centered boolean?
---@field blendOp integer?
---@field color number[]?
---@field alpha number?
---@field stroke StrokeParams?
---@class Image
---@field handle integer
---@field width number
---@field height number
---@field x number?
---@field y number?
---@field scaleX number?
---@field scaleY number?
---@field centered boolean?
---@field blendOp integer?
---@field color number[]?
---@field alpha number?
---@field stroke StrokeParams?
local Image = { };
---Image constructor
---@param imagePath string # The path to the skin image to load
---@return Image
function Image.new(imagePath, noFallback)
local handle = gfx.CreateSkinImage(imagePath or '', 0);
if (not handle) then
game.Log('Failed to load image "' .. imagePath .. '"', game.LOGGER_ERROR);
if (noFallback) then return nil; end
handle = gfx.CreateSkinImage('missing.png', 0);
if (not handle) then
game.Log('Failed to load fallback image "missing.png"', game.LOGGER_ERROR);
end
end
local width, height = 64, 64;
if (handle) then
width, height = gfx.ImageSize(handle);
end
local instance = {
handle = handle,
width = width,
height = height,
};
return Base(Image, instance);
end
---Set the width and height of this Image.
---@param width number
---@param height number
---@return Image # Returns self for method chaining
function Image:setSize(width, height)
if (type(width) ~= "number") then width = 0; end
if (type(height) ~= "number") then height = 0; end
self.width = width;
self.height = height;
return self;
end
---Set the stored position for this Image.
---If the position of this Image will not change frequently,
---using this method allows you to cache the render position
---instead of passing it to the render method on each invocation.
---@param x number
---@param y number
---@return Image # Returns self for method chaining
function Image:setPosition(x, y)
if (type(x) ~= "number") then x = 0; end
if (type(y) ~= "number") then y = 0; end
self.x = x;
self.y = y;
return self;
end
---Renders this Image, applying any of the given ImageParams,
---then any of the cached Image fields, then any default values.
---@param params? ImageParams
function Image:render(params)
params = params or { };
local sx = params.scaleX or self.scaleX or 1;
local sy = params.scaleY or self.scaleY or 1;
local x = params.x or self.x or 0;
local y = params.y or self.y or 0;
local w = (params.width or self.width ) * sx;
local h = (params.height or self.height) * sy;
if (params.centered or self.centered) then
x = x - w / 2;
y = y - h / 2;
end
local blendOp = params.blendOp or self.blendOp or gfx.BLEND_OP_SOURCE_OVER;
local r = 255;
local g = 255;
local b = 255;
if (params.color) then
r = params.color[1];
g = params.color[2];
b = params.color[3];
elseif (self.color) then
r = self.color[1];
g = self.color[2];
b = self.color[3];
end
local a = params.alpha or self.alpha or 1;
gfx.BeginPath();
gfx.GlobalCompositeOperation(blendOp);
if (not self.handle) then
gfx.FillColor(r, g, b, a);
gfx.Rect(x, y, w, h);
gfx.FillColor(255, 255, 255, 255);
else
gfx.SetImageTint(r, g, b);
gfx.ImageRect(x, y, w, h, self.handle, a, 0);
gfx.SetImageTint(255, 255, 255);
end
if (params.stroke or self.stroke) then
r = 255;
g = 255;
b = 255;
if (params.stroke.color) then
r = params.stroke.color[1];
g = params.stroke.color[2];
b = params.stroke.color[3];
elseif (self.stroke and self.stroke.color) then
r = self.stroke.color[1];
g = self.stroke.color[2];
b = self.stroke.color[3];
end
a = params.stroke.alpha or (self.stroke and self.stroke.alpha) or 255;
local size = params.stroke.size or (self.stroke and self.stroke.size) or 1;
gfx.StrokeColor(r, g, b, a);
gfx.StrokeWidth(size);
gfx.Stroke();
end
end
return Image;

View File

@ -20,6 +20,7 @@ Memo.memoize = function(this, key, generator)
return value
end
--[[
-- Image class
--------------
@ -130,6 +131,8 @@ ImageFont.draw = function(this, text, x, y, alpha, hFlag, vFlag)
end
end
]]
function GetDisplayDifficulty(jacketPath, difficulty)
if jacketPath == nil then
return difficulty
@ -137,18 +140,20 @@ function GetDisplayDifficulty(jacketPath, difficulty)
local strippedPath = string.match(jacketPath:lower(), "[/\\][^\\/]+$")
if difficulty == 3 and strippedPath then
if string.find(strippedPath, "inf") ~= nil then
return 5
if string.find(strippedPath, "inf") ~= nil then
return 5
elseif string.find(strippedPath, "grv") ~= nil then
return 6
return 6
elseif string.find(strippedPath, "hvn") ~= nil then
return 7
elseif string.find(strippedPath, "vvd") ~= nil then
return 8
end
return 7
elseif string.find(strippedPath, "vvd") ~= nil then
return 8
elseif string.find(strippedPath, "xcd") ~= nil then
return 9
end
end
return difficulty+1
return difficulty + 1
end
function split(s, delimiter)
@ -157,4 +162,42 @@ function split(s, delimiter)
table.insert(result, match);
end
return result;
end
function firstAlphaNum(s)
for i = 1, string.len(s) do
local byte = string.byte(s, i);
if ((byte >= 65 and byte <= 90) or (byte >= 97 and byte <= 122) or (byte >= 48 and byte <= 57)) then
return string.sub(s, i, i);
end
end
return '';
end
---Set's up scaled transforms based on the current resolution.
---@param x number
---@param y number
---@param rotation number
---@return number, boolean # The scale applied to the transform and the current landscape state
function setUpTransforms(x, y, rotation)
local resx, resy = game.GetResolution();
isLandscape = resx > resy;
local desw, desh;
if (isLandscape) then
desw = 1920;
desh = 1080;
else
desw = 1080;
desh = 1920;
end
scale = resx / desw;
gfx.Translate(x, y);
gfx.Rotate(rotation);
gfx.Scale(scale, scale);
return scale, isLandscape;
end

View File

@ -11,6 +11,8 @@ local Gauge = require('gameplay.gauge')
local Chain = require('gameplay.chain')
local LaserAlert = require('gameplay.laser_alert')
local HitFX = require 'gameplay.hitfx'
local TrackEnd = require('gameplay.track_end')
local json = require("common.json")
@ -28,6 +30,8 @@ local fullX, fullY
local desw = 1080
local desh = 1920
local showHitAnims = true;
local resolutionChange = function(x, y)
resX = x
resY = y
@ -78,11 +82,11 @@ end
function render_crit_base(deltaTime)
CritLine.renderBase(deltaTime, gameplay.critLine.x, gameplay.critLine.y, -gameplay.critLine.rotation, gameplay.critLine.cursors);
HitFX.render(deltaTime, gameplay.critLine.x, gameplay.critLine.y, -gameplay.critLine.rotation, gameplay.critLine.cursors);
Console.render(deltaTime, gameplay.critLine.x, gameplay.critLine.y, -gameplay.critLine.rotation);
end
function render_crit_overlay(deltaTime)
end
function render_intro(deltaTime)
@ -121,11 +125,14 @@ function near_hit(wasLate)
end
function button_hit(button, rating, delta)
if (showHitAnims) then
HitFX.TriggerAnimation("Crit", button + 1)
end
end
function laser_slam_hit(slamLength, startPos, endPost, index)
if (showHitAnims) then
end
end
function laser_alert(isRight)
@ -156,9 +163,9 @@ end
-- Update the users in the scoreboard
function score_callback(response)
if response.status ~= 200 then
error()
return
if response.status ~= 200 then
error()
return
end
local jsondata = json.decode(response.text)
users = {}

View File

@ -4,20 +4,6 @@ local consoleBaseImage = gfx.CreateSkinImage("gameplay/console/base.png", 0)
local CONSOLE_W = 1352;
local CONSOLE_H = 712;
-- Similar to crit line transforms, since the console needs to follow the lane rotation
local setUpTransforms = function (x,y,rotation)
local resx, resy = game.GetResolution()
local desw = 1080
local desh = 1920
local scale = resx / desw
gfx.Translate(x, y)
gfx.Rotate(rotation)
gfx.Scale(scale,scale)
end
local render = function (deltaTime, critLineCenterX, critLineCenterY, critLineRotation)
local resx, resy = game.GetResolution();
if (resx > resy) then

View File

@ -1,4 +1,6 @@
local blackGradientImage = gfx.CreateSkinImage('gameplay/crit_line/black_gradient.png', 0)
local baseImage = gfx.CreateSkinImage("gameplay/crit_line/base.png", 0)
local baseImageLandscape = gfx.CreateSkinImage("gameplay/crit_line/base_landscape.png", 0)
local textImage = gfx.CreateSkinImage("gameplay/crit_line/text.png", 0)
@ -17,42 +19,23 @@ local cursorGlowTopImages = {
local CRITBAR_W = 1496
local CRITBAR_H = 348
local scale;
local scale = 1;
local isLandscape = false;
local setUpTransforms = function (x,y,rotation)
local resx, resy = game.GetResolution();
isLandscape = resx > resy;
local desw, desh;
if (isLandscape) then
desw = 1920;
desh = 1080;
else
desw = 1080;
desh = 1920;
end
scale = resx / desw
gfx.Translate(x, y)
gfx.Rotate(rotation)
gfx.Scale(scale,scale)
end
local drawCursors = function (centerX, centerY,cursors)
local cursorW = 598*0.2;
local cursorH = 673*0.2;
local cursorW = 598 * 0.165;
local cursorH = 673 * 0.14;
for i = 0, 1, 1 do
gfx.Save();
local cursor = cursors[i];
gfx.BeginPath();
gfx.SkewX(cursor.skew)
local cursorX = (cursor.pos *(1/scale) - cursorW/2);
local cursorY = (-cursorH/2);
gfx.Save();
gfx.BeginPath();
local skew = cursor.pos * 0.001;
gfx.SkewX(skew);
local cursorX = cursor.pos * (1 / scale) - cursorW / 2;
local cursorY = -cursorH / 2;
gfx.ImageRect(
cursorX,
@ -99,27 +82,30 @@ local drawCursors = function (centerX, centerY,cursors)
end
local renderBase = function (deltaTime, centerX, centerY, rotation, cursors)
setUpTransforms(centerX, centerY, rotation)
scale, isLandscape = setUpTransforms(centerX, centerY, rotation)
gfx.BeginPath()
gfx.FillColor(0, 0, 0, 192)
gfx.Rect(-1080/2, 0, 1080, 1080)
gfx.Rect(-9999, 0, 9999 * 2, 1080)
gfx.Fill()
gfx.BeginPath();
if (isLandscape) then
gfx.ImageRect(-CRITBAR_W/2, -CRITBAR_H/2, CRITBAR_W, CRITBAR_H, baseImageLandscape, 1, 0);
--gfx.BeginPath()
--gfx.ImageRect(-9999, -8, 9999 * 2, 24, blackGradientImage, 192.0 / 255, 0)
gfx.BeginPath();
gfx.ImageRect(-9999, -CRITBAR_H/2, 9999 * 2, CRITBAR_H, baseImageLandscape, 1, 0);
else
gfx.BeginPath();
gfx.ImageRect(-CRITBAR_W/2, -CRITBAR_H/2, CRITBAR_W, CRITBAR_H, baseImage, 1, 0);
end
drawCursors(centerX, centerY, cursors)
gfx.ResetTransform()
end
local renderOverlay = function (deltaTime)
end
return {

View File

@ -0,0 +1,97 @@
require 'common.globals'
local Animation = require 'api.animation'
local Animations = {
Crit = Animation.new('gameplay/hit_animation_frames/critical_taps', {
centered = true,
}),
Near = Animation.new('gameplay/hit_animation_frames/near_taps', {
centered = true,
}),
};
local animationStates = {
---@type AnimationState[]
Hold = { },
---@type AnimationState[]
Tap = { }
};
for i = 1, 6 do
--animationStates.Hold[i] =
--animationStates.Tap[i] = Animations
end
local HitFX = { };
local LanePositions = {
1.5 / 6,
2.5 / 6,
3.5 / 6,
4.5 / 6,
1 / 3,
2 / 3
};
local function setupLaneTransform(lanePosition, critCenterX, critCenterY)
local critLine = gameplay.critLine;
local x = critCenterX + (critLine.line.x2 - critLine.line.x1) * lanePosition;
local y = critCenterY + (critLine.line.y2 - critLine.line.y1) * lanePosition;
setUpTransforms(x, y, -critLine.rotation);
end
function HitFX.render(deltaTime, critCenterX, critCenterY, critRotation, cursors)
local baseHitSize = 325;
for i = 1, 6 do
local hitSize = baseHitSize;
if (i > 4) then
hitSize = hitSize * 1.5;
end
local laneWidth = (track.GetCurrentLaneXPos(2) - track.GetCurrentLaneXPos(1)) * (i <= 4 and 1 or 2);
local lanePosition = track.GetCurrentLaneXPos(i) + laneWidth / 2
if (i == 5) then
lanePosition = -track.GetCurrentLaneXPos(6) - laneWidth / 2
end
local holdState = animationStates.Hold[i];
local tapState = animationStates.Tap[i];
local isHeld = gameplay.noteHeld[i];
if (isHeld) then
if (holdState) then
if (not holdState.running) then
holdState:restart();
end
setupLaneTransform(lanePosition, critCenterX, critCenterY);
holdState:render(deltaTime);
gfx.ResetTransform();
end
else
if (holdState and holdState.running) then
holdState:restart();
end
if (tapState and tapState.running) then
setupLaneTransform(lanePosition, critCenterX, critCenterY);
tapState:render(deltaTime, {
centered = true,
width = hitSize,
height = hitSize,
});
gfx.ResetTransform();
end
end
end
end
function HitFX.TriggerAnimation(name, lane)
animationStates.Tap[lane] = Animations[name]:start();
end
return HitFX;

View File

@ -785,8 +785,9 @@ function drawScrollbar()
gfx.FillColor(255,255,255)
gfx.TextAlign(gfx.TEXT_ALIGN_MIDDLE + gfx.TEXT_ALIGN_CENTER)
if (songwheel.songs[selectedIndex] ~= nil) then
local letter = string.upper(common.firstLetter(songwheel.songs[selectedIndex].title))
gfx.Text(letter, fillXPos-10, scrollbarYPos + 5)
local title = songwheel.songs[selectedIndex].title;
local letter = string.upper(firstAlphaNum(title))
gfx.Text(letter, fillXPos-10, scrollbarYPos + 5)
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 B

After

Width:  |  Height:  |  Size: 133 B

BIN
textures/missing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 194 B