diff --git a/config-definitions.json b/config-definitions.json index ac1c731..a60dc15 100644 --- a/config-definitions.json +++ b/config-definitions.json @@ -69,6 +69,27 @@ "default": true }, + "gameplay_earlyLateFor": { + "label": "Show Early/Late display for", + "type": "selection", + "default": "NEAR (or worse)", + "values": ["CRITICAL (or worse)", "NEAR (or worse)", "OFF"] + }, + + "gameplay_earlyLatePosition": { + "label": "Early/Late display position", + "type": "selection", + "default": "STANDARD", + "values": ["UPPER+", "UPPER", "STANDARD", "LOWER"] + }, + + "gameplay_msFor": { + "label": "Show millisecond display for", + "type": "selection", + "default": "NEAR (or worse)", + "values": ["ALL", "CRITICAL (or worse)", "NEAR (or worse)", "NONE"] + }, + "separator_f": {}, "Debug": { "type": "label" }, diff --git a/fonts/Digital-Serial-ExtraBold.ttf b/fonts/Digital-Serial-ExtraBold.ttf new file mode 100644 index 0000000..52d9fd6 Binary files /dev/null and b/fonts/Digital-Serial-ExtraBold.ttf differ diff --git a/scripts/gameplay.lua b/scripts/gameplay.lua index ab78608..5673702 100644 --- a/scripts/gameplay.lua +++ b/scripts/gameplay.lua @@ -18,6 +18,7 @@ local Chain = require('gameplay.chain') local LaserAlert = require('gameplay.laser_alert') local HitFX = require 'gameplay.hitfx' +local EarlyLate = require 'gameplay.earlylate' local TrackEnd = require('gameplay.track_end') @@ -59,15 +60,26 @@ function render(deltaTime) Chain.render(deltaTime, gameplay.comboState, chain, gameplay.critLine.x, gameplay.critLine.y); LaserAlert.render(deltaTime); + + EarlyLate.render(deltaTime) 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); + local cl = gameplay.critLine + + CritLine.renderBase(deltaTime, cl.x, cl.y, -cl.rotation); + Console.render(deltaTime, cl.x, cl.y, -cl.rotation); end function render_crit_overlay(deltaTime) + local cl = gameplay.critLine + local centerX = cl.x + local centerY = cl.y + local rot = -cl.rotation + + HitFX.renderButtons(deltaTime, centerX, centerY, rot); + HitFX.renderLasers(deltaTime, centerX, centerY, rot, cl.cursors); + CritLine.renderOverlay(deltaTime, centerX, centerY, rot, cl.cursors, gameplay.laserActive) end function render_intro(deltaTime) @@ -109,10 +121,14 @@ function button_hit(button, rating, delta) if (showHitAnims) then if (rating == 1) then HitFX.TriggerAnimation("Near", button + 1) - else + elseif (rating == 2) then HitFX.TriggerAnimation("Crit", button + 1) end end + + if 0 < rating and rating < 3 then + EarlyLate.TriggerAnimation(rating, delta) + end end function laser_slam_hit(slamLength, startPos, endPost, index) diff --git a/scripts/gameplay/crit_line.lua b/scripts/gameplay/crit_line.lua index 0541641..916f7a7 100644 --- a/scripts/gameplay/crit_line.lua +++ b/scripts/gameplay/crit_line.lua @@ -17,6 +17,10 @@ local cursorGlowTopImages = { gfx.CreateSkinImage("gameplay/crit_line/cursor_glow_top_left.png", 0), gfx.CreateSkinImage("gameplay/crit_line/cursor_glow_top_right.png", 0), } +local cursorTailImages = { + gfx.CreateSkinImage("gameplay/crit_line/cursor_tail_l.png", 0), + gfx.CreateSkinImage("gameplay/crit_line/cursor_tail_r.png", 0), +} local CRITBAR_W = 1080 local CRITBAR_H = 251 @@ -24,10 +28,15 @@ local CRITBAR_H = 251 local scale = 1; local isLandscape = false; -local drawCursors = function (centerX, centerY,cursors) +local drawCursors = function (centerX, centerY,cursors, laserActive) local cursorW = 598 * 0.165; local cursorH = 673 * 0.14; + + local tailW = cursorW * 9 + local tailH = cursorH * 9 + for i = 0, 1, 1 do + local luaIndex = i + 1 local cursor = cursors[i]; gfx.Save(); @@ -39,6 +48,18 @@ local drawCursors = function (centerX, centerY,cursors) local cursorX = cursor.pos * (1 / scale) - cursorW / 2; local cursorY = -cursorH / 2; + if laserActive[luaIndex] then + gfx.ImageRect( + cursor.pos - tailW / 2, + - tailH / 2, + tailW, + tailH, + cursorTailImages[luaIndex], + cursor.alpha / 2, + 0 + ) + end + gfx.ImageRect( cursorX, cursorY, @@ -54,7 +75,7 @@ local drawCursors = function (centerX, centerY,cursors) cursorY, cursorW, cursorH, - cursorGlowBottomImages[i+1], + cursorGlowBottomImages[luaIndex], cursor.alpha, 0 ); @@ -74,7 +95,7 @@ local drawCursors = function (centerX, centerY,cursors) cursorY, cursorW, cursorH, - cursorGlowTopImages[i+1], + cursorGlowTopImages[luaIndex], cursor.alpha, 0 ); @@ -83,7 +104,7 @@ local drawCursors = function (centerX, centerY,cursors) end end -local renderBase = function (deltaTime, centerX, centerY, rotation, cursors) +local renderBase = function (deltaTime, centerX, centerY, rotation) scale, isLandscape = Dimensions.setUpTransforms(centerX, centerY, rotation) gfx.BeginPath() @@ -99,13 +120,15 @@ local renderBase = function (deltaTime, centerX, centerY, rotation, cursors) 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) +local renderOverlay = function (deltaTime, centerX, centerY, rotation, cursors, laserActive) + scale, isLandscape = Dimensions.setUpTransforms(centerX, centerY, rotation) + drawCursors(centerX, centerY, cursors, laserActive) + + gfx.ResetTransform() end return { diff --git a/scripts/gameplay/earlylate.lua b/scripts/gameplay/earlylate.lua index ce6aadf..78909bb 100644 --- a/scripts/gameplay/earlylate.lua +++ b/scripts/gameplay/earlylate.lua @@ -1,30 +1,109 @@ +local Dimensions = require "common.dimensions" -local desw = 1080; -local desh = 1920; +-- Used for comparing button_hit()'s delta parameter with the +-- gameplay_earlyLateFor/gameplay_msDisplay skin settings values. +-- If the number is <= delta then the EarlyLate/ms should be shown +local compare = { + ["ALL"] = 2, + ["CRITICAL (or worse)"] = 2, + ["NEAR (or worse)"] = 1, + ["NONE"] = -1, + ["OFF"] = -1 +} -local transitionExistScale = 0; +local portraitHeightFractions = { + ["UPPER+"] = 2.4, + ["UPPER"] = 3, + ["STANDARD"] = 4.2, + ["LOWER"] = 5.3, +} -local tickTransitions = function (deltaTime) - - if transitionExistScale < 1 then - transitionExistScale = transitionExistScale + deltaTime / 2 -- transition should last for that time in seconds - end -end +local landscapeHeightFractions = { + ["UPPER+"] = 1.5, + ["UPPER"] = 2.7, + ["STANDARD"] = 4.1, + ["LOWER"] = 6.7, +} -local render = function (deltaTime, comboState, combo, critLineCenterX, critLineCenterY) - tickTransitions(deltaTime) +local earlyLateFor = compare[game.GetSkinSetting("gameplay_earlyLateFor")] +local msFor = compare[game.GetSkinSetting("gameplay_msFor")] +local earlyLatePosition = game.GetSkinSetting("gameplay_earlyLatePosition") - if (transitionExistScale >= 1) then - return; +local EarlyLate = { + timer = 0, + color = {}, + earlyLateText = "", + millisecText = "" +} + +function EarlyLate.render(deltaTime) + if EarlyLate.timer <= 0 then + return end + EarlyLate.timer = EarlyLate.timer - deltaTime * 100 -end + local screenW, screenH = Dimensions.screen.width, Dimensions.screen.height + local screenCenterX = screenW / 2 -local trigger = function () + local desh, fractionTable + + if screenH > screenW then + desh = 1600 + fractionTable = portraitHeightFractions + else + desh = 1080 + fractionTable = landscapeHeightFractions + end + + local scale = screenH / desh + local y = screenH / 8 * fractionTable[earlyLatePosition] + + gfx.BeginPath() + gfx.LoadSkinFont("Digital-Serial-ExtraBold.ttf") + gfx.FontSize(20 * scale) + + local color = EarlyLate.color + gfx.FillColor(color[1], color[2], color[3]) + gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_BASELINE) + + gfx.FastText(EarlyLate.earlyLateText, screenCenterX - 100 * scale, y) + gfx.FastText(EarlyLate.millisecText, screenCenterX + 100 * scale, y) end -return { - render=render -} \ No newline at end of file +function EarlyLate.TriggerAnimation(rating, millisec) + local showEarlyLate = rating <= earlyLateFor + local showMillisec = rating <= msFor + local isEarly = millisec < 0 + + if millisec == 0 then return end + if not showEarlyLate and not showMillisec then return end + + if showEarlyLate then + EarlyLate.earlyLateText = isEarly and "EARLY" or "LATE" + else + EarlyLate.earlyLateText = "" + end + + if showMillisec then + local millisecText = string.format("%dms", millisec) + + -- prepend + sign for lates + millisecText = isEarly and millisecText or "+"..millisecText + + EarlyLate.millisecText = millisecText + else + EarlyLate.millisecText = "" + end + + if isEarly then + EarlyLate.color = {206, 94, 135} + else + EarlyLate.color = {53, 102, 197} + end + + EarlyLate.timer = 120 +end + +return EarlyLate \ No newline at end of file diff --git a/scripts/gameplay/hitfx.lua b/scripts/gameplay/hitfx.lua index ec9cd1e..71acc74 100644 --- a/scripts/gameplay/hitfx.lua +++ b/scripts/gameplay/hitfx.lua @@ -13,41 +13,166 @@ local Animations = { Near = Animation.new('gameplay/hit_animation_frames/near_taps', { centered = true, }), + + HoldCrit = Animation.new('gameplay/hit_animation_frames/hold_critical', { + centered = true, + loop = true, + }), + + HoldDome = Animation.new('gameplay/hit_animation_frames/hold_dome', { + centered = true, + loop = true, + loopPoint = 10 + }), + + HoldEnd = Animation.new('gameplay/hit_animation_frames/hold_end', { + centered = true, + }), + + HoldInner = Animation.new('gameplay/hit_animation_frames/hold_inner', { + centered = true, + loop = true, + }), + + LaserCrit = Animation.new('gameplay/hit_animation_frames/laser_critical', { + loop = true, + }), + + LaserDome = Animation.new('gameplay/hit_animation_frames/laser_dome', { + loop = true, + }), + + LaserEndOuter = Animation.new('gameplay/hit_animation_frames/laser_end_outer', {}), + + LaserEndLeft = Animation.new('gameplay/hit_animation_frames/laser_end_l_inner', {}), + + LaserEndRight = Animation.new('gameplay/hit_animation_frames/laser_end_r_inner', {}), }; -local animationStates = { - ---@type AnimationState[] - Hold = { }, - ---@type AnimationState[] - Tap = { } -}; +---@class LaserStateTable +---@field Crit AnimationState +---@field Dome AnimationState +---@field EndInner AnimationState +---@field EndOuter AnimationState + +---@type LaserStateTable[] +local laserStateTables = { + { + Crit = Animations.LaserCrit:createState(), + Dome = Animations.LaserDome:createState(), + EndInner = Animations.LaserEndLeft:createState(), + EndOuter = Animations.LaserEndOuter:createState() + }, + { + Crit = Animations.LaserCrit:createState(), + Dome = Animations.LaserDome:createState(), + EndInner = Animations.LaserEndRight:createState(), + EndOuter = Animations.LaserEndOuter:createState() + } +} + +---@class HoldStateTable +---@field Crit AnimationState +---@field Dome AnimationState +---@field End AnimationState +---@field Inner AnimationState + +---@type HoldStateTable[] +local holdStateTables = {} + +for i = 1, 6 do + holdStateTables[i] = { + Crit = Animations.HoldCrit:createState(), + Dome = Animations.HoldDome:createState(), + End = Animations.HoldEnd:createState(), + Inner = Animations.HoldInner:createState() + } +end + +---@type AnimationState[] +local tapStates = {} local HitFX = { }; -local LanePositions = { - 1.5 / 6, - 2.5 / 6, - 3.5 / 6, - 4.5 / 6, - 1 / 3, - 2 / 3 -}; +local function setUpTransform(critCenterX, critCenterY, critRotation, xScalar) + local critLine = gameplay.critLine + local x = critCenterX + (critLine.line.x2 - critLine.line.x1) * xScalar + local y = critCenterY + (critLine.line.y2 - critLine.line.y1) * xScalar -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; - Dimensions.setUpTransforms(x, y, -critLine.rotation); + Dimensions.setUpTransforms(x, y, critRotation) end -function HitFX.render(deltaTime, critCenterX, critCenterY, critRotation, cursors) - local baseHitSize = 325; +function HitFX.renderLasers(deltaTime, critCenterX, critCenterY, critRotation, cursors) + local hitSize = 406 + -- Lasers + for laser = 1, 2 do + -- Update + local isActive = gameplay.laserActive[laser] + local laserState = laserStateTables[laser] + local isAnimationPlaying = laserState.Dome.running + + if isActive and not isAnimationPlaying then + laserState.Crit:restart() + laserState.Dome:restart() + end + + if not isActive and isAnimationPlaying then + laserState.Crit:stop() + laserState.Dome:stop() + + laserState.EndInner:restart() + laserState.EndOuter:restart() + end + + -- Render + local laserColor = {game.GetLaserColor(laser - 1)} + local x = cursors[laser - 1].pos + + Dimensions.setUpTransforms(critCenterX, critCenterY, critRotation) + + laserState.Dome:render(deltaTime, { + centered = true, + width = hitSize, + height = hitSize, + color = laserColor, + x = x, + }) + + laserState.Crit:render(deltaTime, { + centered = true, + width = hitSize, + height = hitSize, + x = x, + }) + laserState.EndInner:render(deltaTime, { + centered = true, + width = hitSize, + height = hitSize, + x = x, + }) + laserState.EndOuter:render(deltaTime, { + centered = true, + width = hitSize, + height = hitSize, + color = laserColor, + x = x, + }) + end +end + +function HitFX.renderButtons(deltaTime, critCenterX, critCenterY, critRotation) + --local baseHitSize = 325; + local hitSize = 406 + + -- BT + FX 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 @@ -55,40 +180,66 @@ function HitFX.render(deltaTime, critCenterX, critCenterY, critRotation, cursors lanePosition = -track.GetCurrentLaneXPos(6) - laneWidth / 2 end - local holdState = animationStates.Hold[i]; - local tapState = animationStates.Tap[i]; + -- Update Holds + local isHeld = gameplay.noteHeld[i] + local holdStates = holdStateTables[i] + local isAnimationPlaying = holdStates.Dome.running - local isHeld = gameplay.noteHeld[i]; - if (isHeld) then - if (holdState) then - if (not holdState.running) then - holdState:restart(); - end + if isHeld and not isAnimationPlaying then + holdStates.Crit:restart() + holdStates.Dome:restart() + holdStates.Inner:restart() + end - setupLaneTransform(lanePosition, critCenterX, critCenterY); - holdState:render(deltaTime); - gfx.ResetTransform(); - end - else - if (holdState and holdState.running) then - holdState:restart(); - end + if not isHeld and isAnimationPlaying then + holdStates.Crit:stop() + holdStates.Dome:stop() + holdStates.Inner:stop() - if (tapState and tapState.running) then - setupLaneTransform(lanePosition, critCenterX, critCenterY); - tapState:render(deltaTime, { - centered = true, - width = hitSize, - height = hitSize, - }); - gfx.ResetTransform(); - end + holdStates.End:restart() + end + + -- Render holds + setUpTransform(critCenterX, critCenterY, critRotation, lanePosition) + + holdStates.Inner:render(deltaTime, { + centered = true, + width = hitSize, + height = hitSize + }) + holdStates.Dome:render(deltaTime, { + centered = true, + width = hitSize, + height = hitSize + }) + holdStates.Crit:render(deltaTime, { + centered = true, + width = hitSize, + height = hitSize + }) + holdStates.End:render(deltaTime, { + centered = true, + width = hitSize, + height = hitSize + }) + + -- Render Taps + local tapState = tapStates[i] + + if tapState then + tapState:render(deltaTime, { + centered = true, + width = hitSize, + height = hitSize, + }); end end + + gfx.ResetTransform() end function HitFX.TriggerAnimation(name, lane) - animationStates.Tap[lane] = Animations[name]:start(); + tapStates[lane] = Animations[name]:start(); end return HitFX; diff --git a/textures/gameplay/crit_line/cursor_tail_l.png b/textures/gameplay/crit_line/cursor_tail_l.png new file mode 100644 index 0000000..f94892e Binary files /dev/null and b/textures/gameplay/crit_line/cursor_tail_l.png differ diff --git a/textures/gameplay/crit_line/cursor_tail_r.png b/textures/gameplay/crit_line/cursor_tail_r.png new file mode 100644 index 0000000..e77f1dc Binary files /dev/null and b/textures/gameplay/crit_line/cursor_tail_r.png differ