-- The following code slightly simplifies the render/update code, making it easier to explain in the comments -- It replaces a few of the functions built into USC and changes behaviour slightly -- Ideally, this should be in the common.lua file, but the rest of the skin does not support it -- I'll be further refactoring and documenting the default skin and making it more easy to -- modify for those who either don't know how to skin well or just want to change a few images -- or behaviours of the default to better suit them. -- Skinning should be easy and fun! -- Animation functions begin function clamp(x, min, max) if x < min then x = min end if x > max then x = max end return x end function smootherstep(edge0, edge1, x) -- Scale, and clamp x to 0..1 range x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0) -- Evaluate polynomial return x * x * x * (x * (x * 6 - 15) + 10) end function to_range(val, start, stop) return start + (stop - start) * val end Animation = { start = 0, stop = 0, progress = 0, duration = 1, smoothStart = false } function Animation:new(o) o = o or {} setmetatable(o, self) self.__index = self return o end function Animation:restart(start, stop, duration) self.progress = 0 self.start = start self.stop = stop self.duration = duration end function Animation:tick(deltaTime) self.progress = math.min(1, self.progress + deltaTime / self.duration) if self.progress == 1 then return self.stop end if self.smoothStart then return to_range(smootherstep(0, 1, self.progress), self.start, self.stop) else return to_range(smootherstep(-1, 1, self.progress) * 2 - 1, self.start, self.stop) end end --- Animation Functions end local RECT_FILL = "fill" local RECT_STROKE = "stroke" local RECT_FILL_STROKE = RECT_FILL .. RECT_STROKE gfx._ImageAlpha = 1 if gfx._FillColor == nil then gfx._FillColor = gfx.FillColor gfx._StrokeColor = gfx.StrokeColor gfx._SetImageTint = gfx.SetImageTint end -- we aren't even gonna overwrite it here, it's just dead to us gfx.SetImageTint = nil function gfx.FillColor(r, g, b, a) r = math.floor(r or 255) g = math.floor(g or 255) b = math.floor(b or 255) a = math.floor(a or 255) gfx._ImageAlpha = a / 255 gfx._FillColor(r, g, b, a) gfx._SetImageTint(r, g, b) end function gfx.StrokeColor(r, g, b) r = math.floor(r or 255) g = math.floor(g or 255) b = math.floor(b or 255) gfx._StrokeColor(r, g, b) end function gfx.DrawRect(kind, x, y, w, h) local doFill = kind == RECT_FILL or kind == RECT_FILL_STROKE local doStroke = kind == RECT_STROKE or kind == RECT_FILL_STROKE local doImage = not (doFill or doStroke) gfx.BeginPath() if doImage then gfx.ImageRect(x, y, w, h, kind, gfx._ImageAlpha, 0) else gfx.Rect(x, y, w, h) if doFill then gfx.Fill() end if doStroke then gfx.Stroke() end end end local buttonStates = { } local buttonsInOrder = { game.BUTTON_BTA, game.BUTTON_BTB, game.BUTTON_BTC, game.BUTTON_BTD, game.BUTTON_FXL, game.BUTTON_FXR, game.BUTTON_STA, } function UpdateButtonStatesAfterProcessed() for i = 1, 6 do local button = buttonsInOrder[i] buttonStates[button] = game.GetButton(button) end end function game.GetButtonPressed(button) return game.GetButton(button) and not buttonStates[button] end -- -------------------------------------------------------------------------- -- -- game.IsUserInputActive: -- -- Used to determine if (valid) controller input is happening. -- -- Valid meaning that laser motion will not return true unless the laser is -- -- active in gameplay as well. -- -- This restriction is not applied to buttons. -- -- The player may press their buttons whenever and the function returns true. -- -- Lane starts at 1 and ends with 8. -- function game.IsUserInputActive(lane) if lane < 7 then return game.GetButton(buttonsInOrder[lane]) end return gameplay.IsLaserHeld(lane - 7) end -- -------------------------------------------------------------------------- -- -- gfx.FillLaserColor: -- -- Sets the current fill color to the laser color of the given index. -- -- An optional alpha value may be given as well. -- -- Index may be 1 or 2. -- function gfx.FillLaserColor(index, alpha) alpha = math.floor(alpha or 255) local r, g, b = game.GetLaserColor(index - 1) gfx.FillColor(r, g, b, alpha) end -- -------------------------------------------------------------------------- -- -- load_number_image: -- -- Loads numbers from files to allow usage of multi-colored numbers, custom -- -- fonts, and many other things -- function load_number_image(path) local images = {} for i = 0, 9 do images[i + 1] = gfx.CreateSkinImage(string.format("%s/%d.png", path, i), 0) end return images end -- -------------------------------------------------------------------------- -- -- draw_number: -- -- Draws numbers from images in the skin. -- -- Additional values control scaling and spacing. -- function draw_number(x, y, alpha, num, digits, images, is_dim, scale, kern) scale = scale or 1; kern = kern or 1; local tw, th = gfx.ImageSize(images[1]) tw = tw * scale; th = th * scale; x = x + (tw * (digits - 1)) / 2 y = y - th / 2 for i = 1, digits do local mul = 10 ^ (i - 1) local digit = math.floor(num / mul) % 10 local a = alpha if is_dim and num < mul then a = 0.4 end gfx.BeginPath() gfx.ImageRect(x, y, tw, th, images[digit + 1], a, 0) x = x - (tw * kern) end end -- -------------------------------------------------------------------------- -- -- -------------------------------------------------------------------------- -- -- -------------------------------------------------------------------------- -- -- The actual gameplay script starts here! -- -- -------------------------------------------------------------------------- -- -- -------------------------------------------------------------------------- -- -- -------------------------------------------------------------------------- -- -- Global data used by many things: -- local resx, resy -- The resolution of the window local portrait -- whether the window is in portrait orientation local desw, desh -- The resolution of the deisign local scale -- the scale to get from design to actual units -- -------------------------------------------------------------------------- -- -- Global data used by many things: -- local resx, resy -- The resolution of the window local resx_old = 0 local resy_old = 0 local portrait -- whether the window is in portrait orientation local desw, desh -- The resolution of the deisign local scale -- the scale to get from design to actual units -- -------------------------------------------------------------------------- -- -- All images used by the script: -- local jacketFallback = gfx.CreateSkinImage("song_select/loading.png", 0) local bottomFill = gfx.CreateSkinImage("console/console.png", 0) local topFill = gfx.CreateSkinImage("fill_top.png", 0) local critAnimImg = gfx.CreateSkinImage("crit_anim.png", gfx.IMAGE_REPEATX) local critAnim = gfx.ImagePattern(0,-50,100,100,0,critAnimImg,1) local critBar = gfx.CreateSkinImage("crit_bar.png", 0) local critConsole = gfx.CreateSkinImage("console/crit_console.png", 0) local critCap = gfx.CreateSkinImage("crit_cap.png", 0) local critCapBack = gfx.CreateSkinImage("crit_cap_back.png", 0) local laserTail = gfx.CreateSkinImage("laser_tail.png", 0) local laserCursor = gfx.CreateSkinImage("pointer.png", 0) local laserCursorText = gfx.CreateSkinImage("pointer_bottom.png", 0) local laserCursorOverlay = gfx.CreateSkinImage("pointer_overlay.png", 0) local laserCursorGlow = gfx.CreateSkinImage("pointer_glow.png", 0) local laserCursorShine = gfx.CreateSkinImage("pointer_shine.png", 0) local laserTopWave = gfx.CreateSkinImage("laser_top_wave.png", 0) local scoreEarly = gfx.CreateSkinImage("score_early.png", 0) local scoreLate = gfx.CreateSkinImage("score_late.png", 0) local numberImages = load_number_image("number") local prevGaugeType = nil local gaugeTransition = nil --Skin Settings info local username = game.GetSkinSetting('username') or ''; local ioConsoleDetails = { gfx.CreateSkinImage("console/detail_left.png", 0), gfx.CreateSkinImage("console/detail_right.png", 0), } local consoleAnimImages = { gfx.CreateSkinImage("console/glow_bta.png", 0), gfx.CreateSkinImage("console/glow_btb.png", 0), gfx.CreateSkinImage("console/glow_btc.png", 0), gfx.CreateSkinImage("console/glow_btd.png", 0), gfx.CreateSkinImage("console/glow_fxl.png", 0), gfx.CreateSkinImage("console/glow_fxr.png", 0), gfx.CreateSkinImage("console/glow_voll.png", 0), gfx.CreateSkinImage("console/glow_volr.png", 0), } -- -------------------------------------------------------------------------- -- local resx, resy -- The resolution of the window local portrait -- whether the window is in portrait orientation local desw, desh -- The resolution of the deisign local scale -- the scale to get from design to actual units -- -------------------------------------------------------------------------- -- -- Timers, used for animations: -- if introTimer == nil then introTimer = 2 outroTimer = 0 end local alertTimers = {-2,-2} local earlateTimer = 0 local critAnimTimer = 0 local consoleAnimSpeed = 10 local consoleAnimTimers = { 0, 0, 0, 0, 0, 0, 0, 0 } -- -------------------------------------------------------------------------- -- -- Miscelaneous, currently unsorted: -- local score = 0 local combo = 0 local jacket = nil local critLinePos = { 0.95, 0.75 }; local comboScale = 1.0 local late = false local diffNames = {"NOV", "ADV", "EXH", "MXM", "INF", "GRV", "HVN", "VVD"} local clearTexts = {"TRACK FAILED", "TRACK COMPLETE", "TRACK COMPLETE", "FULL COMBO", "PERFECT" } -- -------------------------------------------------------------------------- -- -- Cached calculations -- local song_info = {} local gauge_info = {} local crit_base_info = {} local combo_info = {} local practice_info = {} function LoadGauge(type) local name = type == 0 and "normal" or "hard" local gauge_verts = { {{gauge_info.posx, gauge_info.posy}, {0,1}}, {{gauge_info.posx + gauge_info.width, gauge_info.posy}, {1,1}}, {{gauge_info.posx + gauge_info.width, gauge_info.posy + gauge_info.height}, {1,0}}, {{gauge_info.posx, gauge_info.posy + gauge_info.height}, {0,0}}, } local meshes = {} meshes.front = gfx.CreateShadedMesh() meshes.front:SetPrimitiveType(meshes.front.PRIM_TRIFAN) meshes.front:SetData(gauge_verts) meshes.front:AddSkinTexture("mainTex", "gauges/" .. name .. "/gauge_front.png") meshes.back = gfx.CreateShadedMesh() meshes.back:SetPrimitiveType(meshes.back.PRIM_TRIFAN) meshes.back:SetData(gauge_verts) meshes.back:AddSkinTexture("mainTex", "gauges/" .. name .. "/gauge_back.png") meshes.fill = gfx.CreateShadedMesh("gauge") meshes.fill:SetPrimitiveType(meshes.fill.PRIM_TRIFAN) meshes.fill:SetData(gauge_verts) meshes.fill:AddSkinTexture("mainTex", "gauges/" .. name .. "/gauge_fill.png") meshes.fill:AddSkinTexture("maskTex", "gauges/" .. name .. "/gauge_mask.png") return meshes end -- -------------------------------------------------------------------------- -- -- ResetLayoutInformation: -- -- Resets the layout values used by the skin. -- function ResetLayoutInformation() portrait = resy > resx desw = portrait and 720 or 1280 desh = desw * (resy / resx) scale = resx / desw do --update song_info local songInfoWidth = 400 local jacketWidth = 100 song_info.songInfoWidth = songInfoWidth song_info.jacketWidth = jacketWidth gfx.LoadSkinFont("NotoSans-Regular.ttf") gfx.FontSize(30) song_info.textX = jacketWidth + 10 local titleWidth = songInfoWidth - jacketWidth - 20 local x1, y1, x2, y2 = gfx.TextBounds(0, 0, gameplay.title) song_info.title_textscale = math.min(titleWidth / x2, 1) x1,y1,x2,y2 = gfx.TextBounds(0,0,gameplay.artist) song_info.artist_textscale = math.min(titleWidth / x2, 1) end do --update gauge_info gauge_info.height = 1024 * 0.35 gauge_info.width = 512 * 0.35 gauge_info.posy = desh / 2 - gauge_info.height / 2 gauge_info.posx = desw - gauge_info.width if portrait then gauge_info.width = gauge_info.width * 0.8 gauge_info.height = gauge_info.height * 0.8 gauge_info.posy = gauge_info.posy - 30 gauge_info.posx = desw - gauge_info.width end gauge_info.label_posx = gauge_info.posx + (100 * 0.35) gauge_info.label_height = 880 * 0.35 if portrait then gauge_info.label_height = gauge_info.label_height * 0.8; end gauge_info.label_posy = gauge_info.posy + (70 * 0.35) + gauge_info.label_height gauge_info.meshes = {} gauge_info.meshes[0] = LoadGauge(0) gauge_info.meshes[1] = LoadGauge(1) end do --update crit_base_info -- The absolute width of the crit line itself -- we check to see if we're playing in portrait mode and -- change the width accordingly crit_base_info.critWidth = resx * (portrait and 1 or 0.8) crit_base_info.half_critWidth = crit_base_info.critWidth / 2 -- get the scaled dimensions of the crit line pieces local clw, clh = gfx.ImageSize(critAnimImg) crit_base_info.critAnimHeight = 15 * scale crit_base_info.critAnimWidth = crit_base_info.critAnimHeight * (clw / clh) local ccw, cch = gfx.ImageSize(critCap) crit_base_info.critCapHeight = crit_base_info.critAnimHeight * (cch / clh) crit_base_info.critCapWidth = crit_base_info.critCapHeight * (ccw / cch) crit_base_info.half_critAnimHeight = crit_base_info.critAnimHeight / 2 crit_base_info.half_critAnimWidth = crit_base_info.critAnimWidth / 2 crit_base_info.half_critCapHeight = crit_base_info.critCapHeight / 2 crit_base_info.half_critCapWidth = crit_base_info.critCapWidth / 2 end do --update combo_info combo_info.posx = desw / 2 combo_info.posy = desh * critLinePos[1] - 100 if portrait then combo_info.posy = desh * critLinePos[2] - 150 end end do-- update practice_info practice_info.posy = 120 practice_info.posx = 10 end end -- -------------------------------------------------------------------------- -- -- render: -- -- The primary & final render call. -- -- Use this to render basically anything that isn't the crit line or the -- -- intro/outro transitions. -- function render(deltaTime) -- make sure that our transform is cleared, clean working space -- TODO: this shouldn't be necessary!!! gfx.ResetTransform() gfx.Scale(scale, scale) local yshift = 0 -- In portrait, we draw a banner across the top -- The rest of the UI needs to be drawn below that banner -- TODO: this isn't how it'll work in the long run, I don't think if portrait then yshift = draw_banner(deltaTime) end gfx.Translate(0, yshift - 150 * math.max(introTimer - 1, 0)) draw_song_info(deltaTime) draw_score(deltaTime) if prevGaugeType ~= nil then if gameplay.gauge.type ~= prevGaugeType and gaugeTransition == nil then gaugeTransition = Animation:new() gaugeTransition.smoothStart = true gaugeTransition:restart(0, 1, 1 / 3) end end gfx.Translate(0, -yshift + 150 * math.max(introTimer - 1, 0)) gfx.Save() if gaugeTransition ~= nil then local v = gaugeTransition:tick(deltaTime) if v < 1 then local awayGauge = {} awayGauge.type = prevGaugeType awayGauge.value = 0.0 gfx.Save() gfx.Translate(v * gauge_info.width, 0) draw_gauge(awayGauge) gfx.Restore() gfx.Translate((1-v) * gauge_info.width, 0) else prevGaugeType = gameplay.gauge.type gaugeTransition = nil end else prevGaugeType = gameplay.gauge.type end draw_gauge(gameplay.gauge) gfx.Restore() if earlatePos ~= "off" then draw_earlate(deltaTime) end draw_combo(deltaTime) draw_alerts(deltaTime) if gameplay.practice_setup ~= nil then draw_practice(deltaTime); end local play_mode = "" if gameplay.practice_setup then play_mode = "Practice Setup" elseif gameplay.autoplay then play_mode = "Autoplay" elseif gameplay.playbackSpeed ~= nil and gameplay.playbackSpeed < 1 then play_mode = string.format("Speed: x%.2f", gameplay.playbackSpeed) elseif gameplay.hitWindow ~= nil and gameplay.hitWindow.type == 0 then play_mode = "Expand Judge" end if play_mode ~= "" then gfx.LoadSkinFont("NotoSans-Regular.ttf") gfx.FontSize(30) gfx.TextAlign(gfx.TEXT_ALIGN_TOP + gfx.TEXT_ALIGN_CENTER) gfx.FillColor(255,255,255) gfx.Text(play_mode, desw/2, yshift) end end -- -------------------------------------------------------------------------- -- -- SetUpCritTransform: -- -- Utility function which aligns the graphics transform to the center of the -- -- crit line on screen, rotation include. -- -- This function resets the graphics transform, it's up to the caller to -- -- save the transform if needed. -- function SetUpCritTransform() -- start us with a clean empty transform gfx.ResetTransform() -- translate and rotate accordingly gfx.Translate(gameplay.critLine.x, gameplay.critLine.y) gfx.Rotate(-gameplay.critLine.rotation) end -- -------------------------------------------------------------------------- -- -- GetCritLineCenteringOffset: -- -- Utility function which returns the magnitude of an offset to center the -- -- crit line on the screen based on its rotation. -- function GetCritLineCenteringOffset() return gameplay.critLine.xOffset * 10 end -- -------------------------------------------------------------------------- -- -- GetConsoleCenteringOffset: -- -- Utility function which returns the magnitude of an offset to center the -- -- console on the screen based on its position and rotation. -- function GetConsoleCenteringOffset() return (resx / 2 - gameplay.critLine.x) * (5 / 6) end -- -------------------------------------------------------------------------- -- -- render_crit_base: -- -- Called after rendering the highway and playable objects, but before -- -- the built-in hit effects. -- -- This is the first render function to be called each frame. -- -- This call resets the graphics transform, it's up to the caller to -- -- save the transform if needed. -- function render_crit_base(deltaTime) -- Kind of a hack, but here (since this is the first render function -- that gets called per frame) we update the layout information. -- This means that the player can resize their window and -- not break everything resx, resy = game.GetResolution() if resx ~= resx_old or resy ~= resy_old then ResetLayoutInformation() resx_old = resx resy_old = resy end critAnimTimer = critAnimTimer + deltaTime SetUpCritTransform() -- Figure out how to offset the center of the crit line to remain -- centered on the players screen local xOffset = GetCritLineCenteringOffset() gfx.Translate(xOffset, 0) -- Draw a transparent black overlay below the crit line -- This darkens the play area as it passes gfx.FillColor(0, 0, 0, 200) gfx.DrawRect(RECT_FILL, -resx, 0, resx * 2, resy) -- draw the back half of the caps at each end do gfx.FillColor(255, 255, 255) -- left side gfx.DrawRect(critCapBack, -crit_base_info.half_critWidth -crit_base_info.half_critCapWidth, -crit_base_info.half_critCapHeight, crit_base_info.critCapWidth, crit_base_info.critCapHeight) gfx.Scale(-1, 1) -- scale to flip horizontally -- right side gfx.DrawRect(critCapBack, -crit_base_info.half_critWidth - crit_base_info.half_critCapWidth, -crit_base_info.half_critCapHeight, crit_base_info.critCapWidth, crit_base_info.critCapHeight) gfx.Scale(-1, 1) -- unflip horizontally end -- render the core of the crit line do -- The crit line is made up of two rects with a pattern that scrolls in opposite directions on each rect local startOffset = crit_base_info.critAnimWidth * ((critAnimTimer * 1.5) % 1) -- left side -- Use a scissor to limit the drawable area to only what should be visible gfx.UpdateImagePattern(critAnim, -startOffset, -crit_base_info.half_critAnimHeight, crit_base_info.critAnimWidth, crit_base_info.critAnimHeight, 0, 1) gfx.Scissor(-crit_base_info.half_critWidth, -crit_base_info.half_critAnimHeight, crit_base_info.half_critWidth, crit_base_info.critAnimHeight) gfx.BeginPath() gfx.Rect(-crit_base_info.half_critWidth, -crit_base_info.half_critAnimHeight, crit_base_info.half_critWidth, crit_base_info.critAnimHeight) gfx.FillPaint(critAnim) gfx.Fill() gfx.ResetScissor() -- right side -- exactly the same, but in reverse gfx.UpdateImagePattern(critAnim, startOffset, -crit_base_info.half_critAnimHeight, crit_base_info.critAnimWidth, crit_base_info.critAnimHeight, 0, 1) gfx.Scissor(0, -crit_base_info.half_critAnimHeight, crit_base_info.half_critWidth, crit_base_info.critAnimHeight) gfx.BeginPath() gfx.Rect(0, -crit_base_info.half_critAnimHeight, crit_base_info.half_critWidth, crit_base_info.critAnimHeight) gfx.FillPaint(critAnim) gfx.Fill() gfx.ResetScissor() end -- Draw the front half of the caps at each end do gfx.FillColor(255, 255, 255) -- left side gfx.DrawRect(critCap, -crit_base_info.half_critWidth - crit_base_info.half_critCapWidth, -crit_base_info.half_critCapHeight, crit_base_info.critCapWidth, crit_base_info.critCapHeight) gfx.Scale(-1, 1) -- scale to flip horizontally -- right side gfx.DrawRect(critCap, -crit_base_info.half_critWidth - crit_base_info.half_critCapWidth, -crit_base_info.half_critCapHeight, crit_base_info.critCapWidth, crit_base_info.critCapHeight) gfx.Scale(-1, 1) -- unflip horizontally end -- we're done, reset graphics stuffs gfx.FillColor(255, 255, 255) gfx.ResetTransform() end -- -------------------------------------------------------------------------- -- -- render_crit_overlay: -- -- Called after rendering built-int crit line effects. -- -- Use this to render laser cursors or an IO Console in portrait mode! -- -- This call resets the graphics transform, it's up to the caller to -- -- save the transform if needed. -- function render_crit_overlay(deltaTime) SetUpCritTransform() -- Figure out how to offset the center of the crit line to remain -- centered on the players screen. local xOffset = GetConsoleCenteringOffset() -- When in portrait, we can draw the console at the bottom if portrait then -- We're going to make temporary modifications to the transform gfx.Save() gfx.Translate(xOffset, 0) local bfw, bfh = gfx.ImageSize(bottomFill) local distBetweenKnobs = 0.446 local distCritVertical = 0.098 local ioFillTx = bfw / 2 local ioFillTy = bfh * distCritVertical -- 0.098 -- The total dimensions for the console image local io_x, io_y, io_w, io_h = -ioFillTx, -ioFillTy, bfw, bfh -- Adjust the transform accordingly first local consoleFillScale = (resx * 0.775) / (bfw * distBetweenKnobs) gfx.Scale(consoleFillScale, consoleFillScale); -- Actually draw the fill gfx.FillColor(255, 255, 255) gfx.DrawRect(bottomFill, io_x, io_y, io_w, io_h) -- Then draw the details which need to be colored to match the lasers for i = 1, 2 do gfx.FillLaserColor(i) gfx.DrawRect(ioConsoleDetails[i], io_x, io_y, io_w, io_h) end -- Draw the button press animations by overlaying transparent images gfx.GlobalCompositeOperation(gfx.BLEND_OP_LIGHTER) for i = 1, 6 do -- While a button is held, increment a timer -- If not held, that timer is set back to 0 if game.GetButton(buttonsInOrder[i]) then consoleAnimTimers[i] = consoleAnimTimers[i] + deltaTime * consoleAnimSpeed * 3.14 * 2 else consoleAnimTimers[i] = 0 end -- If the timer is active, flash based on a sin wave local timer = consoleAnimTimers[i] if timer ~= 0 then local image = consoleAnimImages[i] local alpha = (math.sin(timer) * 0.5 + 0.5) * 0.5 + 0.25 gfx.FillColor(255, 255, 255, alpha * 255); gfx.DrawRect(image, io_x, io_y, io_w, io_h) end end gfx.GlobalCompositeOperation(gfx.BLEND_OP_SOURCE_OVER) -- Undo those modifications gfx.Restore(); end local cw, ch = gfx.ImageSize(laserCursor) local cursorWidth = 40 * scale local cursorHeight = cursorWidth * (ch / cw) -- draw each laser cursor for i = 1, 2 do local cursor = gameplay.critLine.cursors[i - 1] local pos, skew = cursor.pos, cursor.skew -- Add a kinda-perspective effect with a horizontal skew gfx.SkewX(skew) --Add the tail, only active in critical zone if (gameplay.laserActive[i]) then gfx.FillLaserColor(i, cursor.alpha * 255) gfx.DrawRect(laserTail, pos - cursorWidth / 2 - 64, -cursorHeight / 2 - 5, cursorWidth * 5, cursorHeight * 5) end -- Draw the SDVX Icon eye and tails below the overlay gfx.FillColor(255, 255, 255, cursor.alpha * 255) gfx.DrawRect(laserCursorText, pos - cursorWidth / 2 - 18, -cursorHeight / 2 - 18, cursorWidth * 2, cursorHeight * 2) -- Draw the colored background with the appropriate laser color gfx.FillLaserColor(i, cursor.alpha * 130) gfx.DrawRect(laserCursor, pos - cursorWidth / 2 - 18, -cursorHeight / 2 - 18, cursorWidth * 2, cursorHeight * 2) --Add the top wave effect, only active in critical zone if (gameplay.laserActive[i]) then gfx.FillLaserColor(i, cursor.alpha * 180) gfx.DrawRect(laserTopWave, pos - cursorWidth / 2 - 80, -cursorHeight / 2 - 24, cursorWidth * 6, cursorHeight * 6) end -- Draw the uncolored overlay on top of the color gfx.FillColor(255, 255, 255, cursor.alpha * 255) gfx.DrawRect(laserCursorOverlay, pos - cursorWidth / 2 - 18, -cursorHeight / 2 - 18, cursorWidth * 2, cursorHeight * 2) -- Draw the colored glow on top of the pointer gfx.FillLaserColor(i, cursor.alpha * 160) gfx.DrawRect(laserCursorGlow, pos - cursorWidth / 2 - 18, -cursorHeight / 2 - 20, cursorWidth * 2, cursorHeight * 2) -- Draw the uncolored overlay on top of the color gfx.FillColor(255, 255, 255, cursor.alpha * 150) gfx.DrawRect(laserCursorShine, pos - cursorWidth / 2 - 18, -cursorHeight / 2 - 20, cursorWidth * 2, cursorHeight * 2) -- Un-skew gfx.SkewX(-skew) end -- We're done, reset graphics stuffs gfx.FillColor(255, 255, 255) gfx.ResetTransform() end -- -------------------------------------------------------------------------- -- -- draw_banner: -- -- Renders the banner across the top of the screen in portrait. -- -- This function expects no graphics transform except the design scale. -- function draw_banner(deltaTime) local bannerWidth, bannerHeight = gfx.ImageSize(topFill) local actualHeight = desw * (bannerHeight / bannerWidth) gfx.FillColor(255, 255, 255) gfx.DrawRect(topFill, 0, 0, desw, actualHeight) return actualHeight end -- -------------------------------------------------------------------------- -- -- draw_stat: -- -- Draws a formatted name + value combination at x, y over w, h area. -- function draw_stat(x, y, w, h, name, value, format, r, g, b) gfx.Save() -- Translate from the parent transform, wherever that may be gfx.Translate(x, y) -- Draw the `name` top-left aligned at `h` size gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP) gfx.FontSize(h) gfx.Text(name .. ":", 0, 0) -- 0, 0, is x, y after translation -- Realign the text and draw the value, formatted gfx.TextAlign(gfx.TEXT_ALIGN_RIGHT + gfx.TEXT_ALIGN_TOP) gfx.Text(string.format(format, value), w, 0) -- This draws an underline beneath the text -- The line goes from 0, h to w, h gfx.BeginPath() gfx.MoveTo(0, h) gfx.LineTo(w, h) -- only defines the line, does NOT draw it yet -- If a color is provided, set it if r then gfx.StrokeColor(r, g, b) -- otherwise, default to a light grey else gfx.StrokeColor(200, 200, 200) end -- Stroke out the line gfx.StrokeWidth(1) gfx.Stroke() -- Undo our transform changes gfx.Restore() -- Return the next `y` position, for easier vertical stacking return y + h + 5 end -- -------------------------------------------------------------------------- -- -- draw_song_info: -- -- Draws current song information at the top left of the screen. -- -- This function expects no graphics transform except the design scale. -- local songBack = gfx.CreateSkinImage("song_back.png", 0) local numberDot = gfx.CreateSkinImage("number/dot.png", 0) local diffImages = { gfx.CreateSkinImage("diff/1 novice.png", 0), gfx.CreateSkinImage("diff/2 advanced.png", 0), gfx.CreateSkinImage("diff/3 exhaust.png", 0), gfx.CreateSkinImage("diff/4 maximum.png", 0), gfx.CreateSkinImage("diff/5 infinite.png", 0), gfx.CreateSkinImage("diff/6 gravity.png", 0), gfx.CreateSkinImage("diff/7 heavenly.png", 0), gfx.CreateSkinImage("diff/8 vivid.png", 0) } local memo = Memo.new() function draw_song_info(deltaTime) local jacketWidth = 105 -- Check to see if there's a jacket to draw, and attempt to load one if not if jacket == nil or jacket == jacketFallback then jacket = gfx.LoadImageJob(gameplay.jacketPath, jacketFallback) end gfx.Save() -- Add a small margin at the edge gfx.Translate(5,5) -- There's less screen space in portrait, the playable area is effectively a square -- We scale down to take up less space if portrait then gfx.Scale(0.7, 0.7) end -- Ensure the font has been loaded gfx.LoadSkinFont("segoeui.ttf") -- Draw the background local tw, th = gfx.ImageSize(songBack) gfx.FillColor(255,255,255) gfx.BeginPath() gfx.ImageRect(-2, -71, tw * 0.855, th * 0.855, songBack, 1, 0) -- Draw the jacket gfx.FillColor(255, 255, 255) gfx.DrawRect(jacket, 31, -39, song_info.jacketWidth, song_info.jacketWidth) -- Draw a background for the following level stat gfx.FillColor(0, 0, 0, 200) gfx.DrawRect(RECT_FILL, 0, 85, 60, 15) -- Level Name : Level Number gfx.FillColor(255, 255, 255) draw_stat(0, 85, 55, 15, diffNames[gameplay.difficulty + 1], gameplay.level, "%02d") -- Reset some text related stuff that was changed in draw_state gfx.TextAlign(gfx.TEXT_ALIGN_LEFT) gfx.FontSize(30) gfx.FillColor(255, 255, 255) local textscale = song_info.title_textscale local textX = song_info.textX gfx.Save() do -- Draw the song title, scaled to fit as best as possible gfx.Translate(textX, 30) gfx.Scale(textscale, textscale) gfx.Text(gameplay.title, 0, 0) end gfx.Restore() textscale = song_info.artist_textscale gfx.Save() do -- Draw the song artist, scaled to fit as best as possible gfx.Translate(textX, 60) gfx.Scale(textscale, textscale) gfx.Text(gameplay.artist, 0, 0) end gfx.Restore() -- Draw the BPM gfx.FontSize(20) gfx.Text(string.format("BPM: %.1f", gameplay.bpm), textX, 85) -- Fill the progress bar gfx.FillColor(0, 150, 255) gfx.DrawRect(RECT_FILL, song_info.jacketWidth, song_info.jacketWidth - 10, (song_info.songInfoWidth - song_info.jacketWidth) * gameplay.progress, 10) -- When the player is holding Start, the hispeed can be changed -- Shows the current hispeed values if game.GetButton(game.BUTTON_STA) then gfx.FillColor(20, 20, 20, 200); gfx.DrawRect(RECT_FILL, 100, 100, song_info.songInfoWidth - 100, 20) gfx.FillColor(255, 255, 255) if game.GetButton(game.BUTTON_BTB) then gfx.Text(string.format("Hid/Sud Cutoff: %.1f%% / %.1f%%", gameplay.hiddenCutoff * 100, gameplay.suddenCutoff * 100), textX, 115) elseif game.GetButton(game.BUTTON_BTC) then gfx.Text(string.format("Hid/Sud Fade: %.1f%% / %.1f%%", gameplay.hiddenFade * 100, gameplay.suddenFade * 100), textX, 115) else gfx.Text(string.format("HiSpeed: %.0f x %.1f = %.0f", gameplay.bpm, gameplay.hispeed, gameplay.bpm * gameplay.hispeed), textX, 115) end end -- aaaand, scene! gfx.Restore() end -- -------------------------------------------------------------------------- -- -- draw_best_diff: -- -- If there are other saved scores, this displays the difference between -- -- the current play and your best. -- function draw_best_diff(deltaTime, x, y) -- Don't do anything if there's nothing to do if not gameplay.scoreReplays[1] then return end -- Calculate the difference between current and best play local difference = score - gameplay.scoreReplays[1].currentScore local prefix = "" -- used to properly display negative values gfx.BeginPath() gfx.FontSize(26) gfx.FillColor(255, 255, 255) if difference < 0 then -- If we're behind the best score, separate the minus sign and change the color gfx.FillColor(255, 90, 70) difference = math.abs(difference) prefix = "- " elseif difference > 0 then -- If we're behind the best score, separate the minus sign and change the color gfx.FillColor(170, 160, 255) difference = math.abs(difference) prefix = "+ " end -- %08d formats a number to 8 characters -- This includes the minus sign, so we do that separately gfx.LoadSkinFont("Digital-Serial-Bold.ttf") gfx.FontSize(26) gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP) gfx.Text(string.format("%s%08d", prefix, difference), x, y) end function draw_username(deltaTime, x, y) gfx.BeginPath() gfx.FillColor(255, 255, 255) gfx.LoadSkinFont("Digital-Serial-Bold.ttf") gfx.FontSize(26) gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP) gfx.Text(string.sub(username, 1, 8), x, y) end function draw_username(deltaTime, x, y) gfx.BeginPath() gfx.FillColor(255, 255, 255) gfx.LoadSkinFont("Digital-Serial-Bold.ttf") gfx.FontSize(26) gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP) gfx.Text(string.sub(username, 1, 8), x, y) end local score_animation = Animation:new() -- -------------------------------------------------------------------------- -- -- draw_score: -- local scoreBack = gfx.CreateSkinImage("score_back.png", 0) local scoreNumber = load_number_image("score_num") local maxCombo = 0 function draw_score(deltaTime) local tw, th = gfx.ImageSize(scoreBack) gfx.FillColor(255, 255, 255) gfx.BeginPath() tw = tw * 0.61; th = th * 0.61; gfx.ImageRect(desw - tw + 12, portrait and 50 or 0, tw, th, scoreBack, 1, 0) gfx.FillColor(255, 255, 255) draw_number(desw - 305, portrait and 132 or 64, 1.0, math.floor(score / 10000), 4, scoreNumber, true, 0.40, 1.12) draw_number(desw - 110, portrait and 137 or 68, 1.0, score, 4, scoreNumber, true, 0.3, 1.12) -- Draw max combo gfx.FillColor(255, 255, 255) draw_number(desw - 300, portrait and 207 or 110, 1.0, maxCombo, 4, numberImages, true) end -- -------------------------------------------------------------------------- -- -- draw_gauge: -- function draw_gauge(gauge) local c = nil if gauge.type == 0 then if gauge.value > 0.7 then c = {r = 1, g = 0, b = 1} else c = {r = 0, g = 0.5, b = 1} end else c = {r = 1, g = 0.5, b = 0} end gauge_info.meshes[gauge.type].fill:SetParamVec4("barColor", c.r, c.g, c.b, 1) gauge_info.meshes[gauge.type].fill:SetParam("rate", gauge.value) gauge_info.meshes[gauge.type].back:Draw() gauge_info.meshes[gauge.type].fill:Draw() gauge_info.meshes[gauge.type].front:Draw() --draw gauge % label local posy = gauge_info.label_posy - gauge_info.label_height * gauge.value gfx.BeginPath() gfx.Rect(gauge_info.label_posx-35, posy-10, 40, 20) gfx.FillColor(0,0,0,200) gfx.Fill() gfx.FillColor(255,255,255) gfx.TextAlign(gfx.TEXT_ALIGN_RIGHT + gfx.TEXT_ALIGN_MIDDLE) gfx.FontSize(20) gfx.Text(string.format("%d%%", math.floor(gauge.value * 100)), gauge_info.label_posx, posy ) end -- -------------------------------------------------------------------------- -- -- draw_combo: -- function draw_combo(deltaTime) if combo == 0 then return end gfx.BeginPath() gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE) if gameplay.comboState == 2 then gfx.FillColor(100,255,0) --puc elseif gameplay.comboState == 1 then gfx.FillColor(255,200,0) --uc else gfx.FillColor(255,255,255) --regular end gfx.LoadSkinFont("NovaMono.ttf") gfx.FontSize(70 * math.max(comboScale, 1)) comboScale = comboScale - deltaTime * 3 gfx.Text(tostring(combo), combo_info.posx, combo_info.posy) end -- -------------------------------------------------------------------------- -- -- draw_earlate: -- function draw_earlate(deltaTime) earlateTimer = math.max(earlateTimer - deltaTime,0) if earlateTimer == 0 then return nil end local alpha = math.floor(earlateTimer * 20) % 2 alpha = alpha * 200 + 55 gfx.BeginPath() gfx.FontSize(35) gfx.TextAlign(gfx.TEXT_ALIGN_CENTER, gfx.TEXT_ALIGN_MIDDLE) local ypos = desh * critLinePos[1] - 150 if portrait then ypos = desh * critLinePos[2] - 200 end if earlatePos == "middle" then ypos = ypos - 200 elseif earlatePos == "top" then ypos = ypos - 400 end if late then gfx.FillColor(0,255,255, alpha) gfx.Text("LATE", desw / 2, ypos) else gfx.FillColor(255,0,255, alpha) gfx.Text("EARLY", desw / 2, ypos) end end -- -------------------------------------------------------------------------- -- -- draw_alerts: -- function draw_alerts(deltaTime) alertTimers[1] = math.max(alertTimers[1] - deltaTime,-2) alertTimers[2] = math.max(alertTimers[2] - deltaTime,-2) if alertTimers[1] > 0 then --draw left alert gfx.Save() local posx = desw / 2 - 350 local posy = desh * critLinePos[1] - 135 if portrait then posy = desh * critLinePos[2] - 135 posx = 65 end gfx.Translate(posx,posy) r,g,b = game.GetLaserColor(0) local alertScale = (-(alertTimers[1] ^ 2.0) + (1.5 * alertTimers[1])) * 5.0 alertScale = math.min(alertScale, 1) gfx.Scale(1, alertScale) gfx.BeginPath() gfx.RoundedRectVarying(-50,-50,100,100,20,0,20,0) gfx.StrokeColor(r,g,b) gfx.FillColor(20,20,20) gfx.StrokeWidth(2) gfx.Fill() gfx.Stroke() gfx.BeginPath() gfx.FillColor(r,g,b) gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE) gfx.FontSize(90) gfx.Text("L",0,0) gfx.Restore() end if alertTimers[2] > 0 then --draw right alert gfx.Save() local posx = desw / 2 + 350 local posy = desh * critLinePos[1] - 135 if portrait then posy = desh * critLinePos[2] - 135 posx = desw - 65 end gfx.Translate(posx,posy) r,g,b = game.GetLaserColor(1) local alertScale = (-(alertTimers[2] ^ 2.0) + (1.5 * alertTimers[2])) * 5.0 alertScale = math.min(alertScale, 1) gfx.Scale(1, alertScale) gfx.BeginPath() gfx.RoundedRectVarying(-50,-50,100,100,0,20,0,20) gfx.StrokeColor(r,g,b) gfx.FillColor(20,20,20) gfx.StrokeWidth(2) gfx.Fill() gfx.Stroke() gfx.BeginPath() gfx.FillColor(r,g,b) gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE) gfx.FontSize(90) gfx.Text("R",0,0) gfx.Restore() end end function change_earlatepos() if earlatePos == "top" then earlatePos = "off" elseif earlatePos == "off" then earlatePos = "bottom" elseif earlatePos == "bottom" then earlatePos = "middle" elseif earlatePos == "middle" then earlatePos = "top" end game.SetSkinSetting("earlate_position", earlatePos) end -- -------------------------------------------------------------------------- -- -- draw_status: -- local statusBack = Image.skin("status_back.png") local apealCard = Image.skin("appeal_card.png") local dan = Image.skin("dan.png") local volforce = Image.skin ("volforce.png") function draw_status(deltaTime) -- Draw the background gfx.FillColor(255, 255, 255) gfx.BeginPath() statusBack:draw({ x = 0, y = desh / 2 - 195, w = statusBack.w * 0.85, h = statusBack.h * 0.85, anchor_h = Image.ANCHOR_LEFT }) gfx.Fill() -- Draw the apeal card apealCard:draw({ x = 12, y = desh / 2 - 220, w = apealCard.w * 0.62, h = apealCard.h * 0.62, anchor_h = Image.ANCHOR_LEFT, anchor_v = Image.ANCHOR_TOP }) -- Draw the dan dan:draw({ x = 164, y = desh / 2 - 117, w = dan.w * 0.32, h = dan.h * 0.32 }) -- Draw the Volforce volforce:draw({ x = 240, y = desh / 2 - 119, w = volforce.w * 0.12, h = volforce.h * 0.12 }) -- Draw the best difference draw_best_diff(deltaTime, 145, desh / 2 - 175) -- Draw the username draw_username(deltatime, 145, desh / 2 - 198) end -- -------------------------------------------------------------------------- -- -- render_intro: -- local bta_last = false function render_intro(deltaTime) if gameplay.demoMode then introTimer = 0 return true end if not game.GetButton(game.BUTTON_STA) then introTimer = introTimer - deltaTime earlateTimer = 0 else earlateTimer = 1 if (not bta_last) and game.GetButton(game.BUTTON_BTA) then change_earlatepos() end end bta_last = game.GetButton(game.BUTTON_BTA) introTimer = math.max(introTimer, 0) return introTimer <= 0 end -- -------------------------------------------------------------------------- -- -- render_outro: -- function render_outro(deltaTime, clearState) if clearState == 0 then return true end if not gameplay.demoMode then gfx.ResetTransform() gfx.BeginPath() gfx.Rect(0,0,resx,resy) gfx.FillColor(0,0,0, math.floor(127 * math.min(outroTimer, 1))) gfx.Fill() gfx.Scale(scale,scale) gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE) gfx.FillColor(255,255,255, math.floor(255 * math.min(outroTimer, 1))) gfx.LoadSkinFont("NovaMono.ttf") gfx.FontSize(70) gfx.Text(clearTexts[clearState], desw / 2, desh / 2) outroTimer = outroTimer + deltaTime return outroTimer > 2, 1 - outroTimer else outroTimer = outroTimer + deltaTime return outroTimer > 2, 1 end end -- -------------------------------------------------------------------------- -- -- update_score: -- function update_score(newScore) if newScore ~= score then score_animation:restart(score_animation:tick(0), newScore, 0.33) score = newScore end end -- -------------------------------------------------------------------------- -- -- update_combo: -- function update_combo(newCombo) combo = newCombo comboScale = 1.5 end -- -------------------------------------------------------------------------- -- -- near_hit: -- function near_hit(wasLate) --for updating early/late display late = wasLate earlateTimer = 0.75 end -- -------------------------------------------------------------------------- -- -- laser_alert: -- function laser_alert(isRight) --for starting laser alert animations if isRight and alertTimers[2] < -1.5 then alertTimers[2] = 1.5 elseif alertTimers[1] < -1.5 then alertTimers[1] = 1.5 end end -- ======================== Start mutliplayer ======================== json = require "json" local normal_font = game.GetSkinSetting('multi.normal_font') if normal_font == nil then normal_font = 'NotoSans-Regular.ttf' end local mono_font = game.GetSkinSetting('multi.mono_font') if mono_font == nil then mono_font = 'NovaMono.ttf' end local users = nil function init_tcp() Tcp.SetTopicHandler("game.scoreboard", function(data) users = {} for i, u in ipairs(data.users) do table.insert(users, u) end end) end -- Hook the render function and draw the scoreboard local real_render = render render = function(deltaTime) real_render(deltaTime) draw_users(deltaTime) end -- Update the users in the scoreboard function score_callback(response) if response.status ~= 200 then error() return end local jsondata = json.decode(response.text) users = {} for i, u in ipairs(jsondata.users) do table.insert(users, u) end end -- Render scoreboard function draw_users(detaTime) if (users == nil) then return end local yshift = 0 -- In portrait, we draw a banner across the top -- The rest of the UI needs to be drawn below that banner if portrait then local bannerWidth, bannerHeight = gfx.ImageSize(topFill) yshift = desw * (bannerHeight / bannerWidth) gfx.Scale(0.7, 0.7) end gfx.Save() -- Add a small margin at the edge gfx.Translate(5,yshift+200) -- Reset some text related stuff that was changed in draw_state gfx.TextAlign(gfx.TEXT_ALIGN_LEFT) gfx.FontSize(35) gfx.FillColor(255, 255, 255) local yoff = 0 if portrait then yoff = 75; end local rank = 0 for i, u in ipairs(users) do gfx.FillColor(255, 255, 255) local score_big = string.format("%04d",math.floor(u.score/1000)); local score_small = string.format("%03d",u.score%1000); local user_text = '('..u.name..')'; local size_big = 40; local size_small = 28; local size_name = 30; if u.id == gameplay.user_id then size_big = 48 size_small = 32 size_name = 40 rank = i; end gfx.LoadSkinFont(mono_font) gfx.FontSize(size_big) gfx.Text(score_big, 0, yoff); local xmin,ymin,xmax,ymax_big = gfx.TextBounds(0, yoff, score_big); xmax = xmax + 7 gfx.FontSize(size_small) gfx.Text(score_small, xmax, yoff); xmin,ymin,xmax,ymax = gfx.TextBounds(xmax, yoff, score_small); xmax = xmax + 7 if u.id == gameplay.user_id then gfx.FillColor(237, 240, 144) end gfx.LoadSkinFont(normal_font) gfx.FontSize(size_name) gfx.Text(user_text, xmax, yoff) yoff = ymax_big + 15 end gfx.Restore() end -- ======================== Start practice mode ======================== local is_playing_practice = false local mission_str = "" local count_play = 0 local count_success = 0 local last_run = "" local last_timing = "" -- Called when the practice starts function practice_start(mission_type, mission_threshold, mission_description) is_playing_practice = true mission_str = string.format("Mission: %s", mission_description) end -- Called when a run for the practice is finished function practice_end_run(playCount, successCount, isSuccessful, scoring) count_play = playCount count_success = successCount last_run = string.format("Last run: %d (%d-%d)", scoring.score, scoring.goods, scoring.misses) last_timing = string.format("Hit delta: %d±%dms", scoring.meanHitDelta, scoring.meanHitDeltaAbs) end -- Called when user enters the setup again function practice_end(playCount, successCount) is_playing_practice = false count_play = playCount count_success = successCount end function draw_practice(deltaTime) if not is_playing_practice then return end gfx.Save() gfx.Translate(practice_info.posx, practice_info.posy) gfx.LoadSkinFont("NotoSans-Regular.ttf") gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP) gfx.FillColor(255, 255, 255) gfx.FontSize(25) gfx.Text(mission_str, 0, 0) if count_play > 0 then local play_stat = string.format("Clear rate: %d/%d (%.1f%%)", count_success, count_play, (100.0 * count_success / count_play)) gfx.Text(play_stat, 0, 30) gfx.FontSize(20) gfx.Text(last_run, 0, 55) gfx.Text(last_timing, 0, 75) end gfx.Restore() end