973 lines
42 KiB
Lua
973 lines
42 KiB
Lua
-- YataiDONNantoka gameplay background: taikonantoka's Play/Bg, Dancer, Chara, Chibi,
|
|
-- Drapery and Load layers (Files/Theme/Original/Play, 640x480, drawn 1:1).
|
|
-- Numbers and timings come from dispconf.ini [演奏], the *.ini files next to the art and
|
|
-- the decompiled play loop sub_40F6D0 / sub_422330 / sub_41CEB0 -- see
|
|
-- Graphics/background/MAPPING_bg.md for the source of every constant.
|
|
--
|
|
-- Time bases (taikonantoka):
|
|
-- ms : play-screen milliseconds (dancer-bg NORMAL set, upper-bg scroll, fades)
|
|
-- clk : beat clock, 3600 units per beat, follows BPM changes (dbl_162B918)
|
|
-- The engine hands the background no GOGO flag and no difficulty; see MAPPING_bg.md
|
|
-- ("engine gaps") for what that leaves out.
|
|
-- Round 2: balloon burst from the engine's "balloon_pop" sound, balloon fail after the
|
|
-- 1 s no-hit timeout, and the Play/Load/bg_end wipe when the song audio ("song") stops
|
|
-- (continued over the engine's result transition by Scripts/result/result_transition.lua
|
|
-- through the shared global NK_PLAY_WIPE_MS).
|
|
|
|
local SCR = "background"
|
|
local BEAT = 3600
|
|
|
|
-- Resolution independence: every screen-space number in this file (positions, distances,
|
|
-- amplitudes, px/s speeds) is in taikonantoka's 640x480 pixels and is multiplied by
|
|
-- K = screen width / 640 -- at the constant tables right below them, or inline as "* K".
|
|
-- A skin reusing this script at 960x720 ships the art 1.5x, so K = 1.5. Texture sizes
|
|
-- (t.w / t.h) are already screen pixels and are never scaled; times, alphas and frame math
|
|
-- are unchanged. K is the integer 1 at 640 wide, so the native skin keeps exactly the same
|
|
-- (integer) arithmetic.
|
|
local K = tex.get_screen_width() / 640
|
|
K = math.tointeger(K) or K
|
|
local SW, SH = 640 * K, 480 * K
|
|
|
|
-- dispconf.ini [演奏]
|
|
local CFG = {
|
|
bg_scroll_speed = 44,
|
|
character_x = 86, character_y = 62,
|
|
character_balloon_x = 125, character_balloon_y = 129,
|
|
balloon_x = 247, balloon_y = 150,
|
|
balloon_hit_x = 249, balloon_hit_y = 159,
|
|
bubble_x = 207.5, bubble_y = 64, bubble_num_y = 58.8, bubble_num_w = 22,
|
|
stage_y = 456,
|
|
note_y = 156,
|
|
}
|
|
for k, v in pairs(CFG) do CFG[k] = v * K end -- bg_scroll_speed is px/s: scales too
|
|
|
|
-- Play/Bg/dancer_1/dancer_bg.ini (the only dancer_N folder the game loads; dancer_2- is
|
|
-- skipped by its FindFirstFile("dancer_%d") probe).
|
|
-- move: {kind, ...} appear: {kind, t0,t1,t2,t3,h1,h2,h3}
|
|
local DBG_NORMAL = {
|
|
{ tex = "bg_1", x = 320, y = 323 },
|
|
{ tex = "bg_2", x = -256, y = 323, move = { "xloop", 512, -512, 180000 } },
|
|
{ tex = "bg_3", x = 256, y = 323, move = { "xloop", 512, -512, 180000 } },
|
|
{ tex = "bg_2", x = 768, y = 323, move = { "xloop", 512, -512, 180000 } },
|
|
{ tex = "bg_3", x = 1280, y = 323, move = { "xloop", 512, -512, 180000 } },
|
|
{ tex = "bg_4", x = -76, y = 358, move = { "xloop", -100, 800, 180000 } },
|
|
{ tex = "bg_5", x = -76, y = 358, add = true,
|
|
move = { "xloop_blink", -100, 800, 180000, 128, 255, 1000 } },
|
|
{ tex = "bg_6", x = 320, y = 370 },
|
|
{ tex = "bg_7", x = 320, y = 287, move = { "nodraw" },
|
|
appear = { "bound_up", 1300, 1700, 1700, 1700, 60, 0, 0 } },
|
|
{ tex = "bg_8", x = 320, y = 347 },
|
|
}
|
|
local DBG_CLEAR = {
|
|
{ tex = "bg_clear_1", x = 320, y = 347,
|
|
appear = { "bound_down_cos", 0, 500, 700, 900, 255, 60, 60 } },
|
|
{ tex = "bg_clear_2", x = -320, y = 347, offset = -900, move = { "xloop", 640, 0, 6400 },
|
|
appear = { "bound_down_cos", 0, 500, 700, 900, 255, 60, 60 } },
|
|
{ tex = "bg_clear_2", x = 320, y = 347, offset = -900, move = { "xloop", 640, 0, 6400 },
|
|
appear = { "bound_down_cos", 0, 500, 700, 900, 255, 60, 60 } },
|
|
}
|
|
-- scale x / y, the xloop start / end x (move[2], move[3]) and the appear heights
|
|
-- (appear[6..8]); periods, blink alphas, offsets and appear times are ms / alpha
|
|
for _, list in ipairs({ DBG_NORMAL, DBG_CLEAR }) do
|
|
for _, l in ipairs(list) do
|
|
l.x, l.y = l.x * K, l.y * K
|
|
local m = l.move
|
|
if m and (m[1] == "xloop" or m[1] == "xloop_blink") then m[2], m[3] = m[2] * K, m[3] * K end
|
|
local a = l.appear
|
|
if a then for i = 6, 8 do a[i] = a[i] * K end end
|
|
end
|
|
end
|
|
|
|
-- Play/Dancer/1/dancer.ini (Dancer/2 is byte-identical)
|
|
local DANCER = {
|
|
x = { 320, 190, 450, 60, 580 },
|
|
y = { 352, 342, 361, 353, 353 },
|
|
interval = 16,
|
|
order = { 1,1,2,2,3,3,2,2,1,1,2,2,3,4,5,6,7,7,8,8,9,9,8,8,7,7,8,8,9,9,6,6,10,11,12,11,10,11,12,11,
|
|
10,13,14,15,14,13,14,15,14,13,10,11,12,11,10,11,12,11,10,13,14,15,14,13,14,15,14,13 },
|
|
appear_pattern = { 1, 1, 1, 0, 0 },
|
|
appear_interval = 3.5,
|
|
appear_order = { 1,2,3,4,5,6,7,8,8,9,9,10,10,11,11,12,12,12,12,12 },
|
|
}
|
|
for i = 1, #DANCER.x do DANCER.x[i] = DANCER.x[i] * K end
|
|
for i = 1, #DANCER.y do DANCER.y[i] = DANCER.y[i] * K end
|
|
|
|
-- Play/Chibi/{1,2,Soul}/chibi.ini
|
|
local CHIBI_SETS = {
|
|
{ sub = "nk_chibi1", x = 125, y = 62, xp = { 1, 1, 2, 2 }, yp = { 2, 2, 1, 1 }, sort = 4,
|
|
order = { 1, 2, 3 }, interval = 0.5, appear = 1, appear_frame = 2 },
|
|
{ sub = "nk_chibi2", x = 125, y = 62, xp = { 1, 1, 1, 1 }, yp = { 1, 1, 1, 1 }, sort = 4,
|
|
order = { 1, 2, 3, 4, 5 }, interval = 1.0, appear = 1, appear_frame = 1 },
|
|
}
|
|
local CHIBI_SOUL = { sub = "nk_chibi_soul", x = 174, y = 82, xp = { 0 }, yp = { 0 }, sort = 1,
|
|
order = { 4, 5 }, interval = 1.0, appear = 0, appear_frame = 1 }
|
|
for _, set in ipairs({ CHIBI_SETS[1], CHIBI_SETS[2], CHIBI_SOUL }) do set.x, set.y = set.x * K, set.y * K end
|
|
|
|
-- full-combo drapery flower centres (hard-coded in sub_40F6D0)
|
|
local FLOWERS = { { 230, 190 }, { 260, 124 }, { 318, 185 }, { 358, 122 },
|
|
{ 400, 192 }, { 450, 120 }, { 492, 192 }, { 538, 130 } }
|
|
for _, p in ipairs(FLOWERS) do p[1], p[2] = p[1] * K, p[2] * K end
|
|
|
|
local P2_BALLOON_DY = 30
|
|
local P2_MIRROR = 475 -- 2P HUD: 1P y -> 475 - y (640 px; the 2P lane is the 1P lane mirrored)
|
|
local SUBSETS = { "nk_upper", "nk_dbg1", "nk_stage", "nk_fiver", "nk_dancer1", "nk_chibi1",
|
|
"nk_chibi2", "nk_chibi_soul", "nk_chara", "nk_drapery", "nk_load", "nk_dan", "nk_bubble",
|
|
"nk_symbol" } -- [YataiDON14] the difficulty symbol
|
|
|
|
local function rad(deg) return deg * math.pi / 180 end
|
|
local function trunc(v) if v >= 0 then return math.floor(v) else return math.ceil(v) end end
|
|
-- C remainder (sign of the dividend)
|
|
local function cmod(a, b) return math.fmod(a, b) end
|
|
|
|
local NEVER = 1e9
|
|
|
|
---------------------------------------------------------------------------------------------
|
|
-- parent collab scenes that have no taikonantoka counterpart keep the parent's look.
|
|
-- DAN and PRACTICE gameplay use the taikonantoka enso (Graphics/game/MAPPING_hud.md,
|
|
-- "Practice / Dan"): the engine hands them this same Background with scene_preset "DAN" /
|
|
-- "PRACTICE"; the practice overlay (large drums, pause buttons, progress bar) and the dan
|
|
-- panels / dan gauge are engine draws on top and keep working.
|
|
local PARENT_SCENES = {
|
|
TUTORIAL = { "background/bg_collabs/tutorial" },
|
|
}
|
|
|
|
Background = {}
|
|
Background.__index = Background
|
|
|
|
-- sub -> name -> texture (two levels: a hit builds no key string)
|
|
local TCACHE = {}
|
|
local MISSING = {}
|
|
local function T(sub, name)
|
|
local by_sub = TCACHE[sub]
|
|
if not by_sub then by_sub = {}; TCACHE[sub] = by_sub end
|
|
local c = by_sub[name]
|
|
if c then return (c ~= MISSING) and c or nil end
|
|
local id = tex.get_texture(SCR .. "/" .. sub .. "/" .. name)
|
|
local info = id and tex.get_texture_info(sub, name)
|
|
if not (id and info) then by_sub[name] = MISSING; return nil end
|
|
c = { id = id, w = info.width, h = info.height }
|
|
by_sub[name] = c
|
|
return c
|
|
end
|
|
|
|
-- top-left draw
|
|
local BLIT_P = {}
|
|
local function blit(t, x, y, alpha, blend)
|
|
if not t then return end
|
|
local p = BLIT_P
|
|
p.x, p.y = x, y
|
|
p.fade = (alpha and alpha < 255) and math.max(alpha, 0) / 255 or nil
|
|
p.blend = blend or nil
|
|
tex.draw_texture(t.id, p)
|
|
end
|
|
|
|
-- centred draw (taikonantoka sub_431B30 / sub_432490: top-left = centre - size/2)
|
|
local function blit_c(t, cx, cy, alpha, blend)
|
|
if not t then return end
|
|
blit(t, cx - t.w / 2, cy - t.h / 2, alpha, blend)
|
|
end
|
|
|
|
function Background.new(player_num, bpm, scene_preset)
|
|
local parent = scene_preset and PARENT_SCENES[scene_preset]
|
|
if parent then
|
|
local mod = require(parent[1])
|
|
local cls = (type(mod) == "table" and mod.new) and mod
|
|
or ({ PRACTICE = PracticeBackground, TUTORIAL = TutorialBackground })[scene_preset]
|
|
if cls then
|
|
if parent[2] then return cls.new(player_num, bpm, parent[2], parent[3]) end
|
|
return cls.new(player_num, bpm)
|
|
end
|
|
end
|
|
|
|
local self = setmetatable({}, Background)
|
|
NK_DAN_RED = nil -- set by handle_dan (nk_hud's norma line)
|
|
self.player_num = player_num
|
|
self.is_2p = (player_num == 3)
|
|
self.bpm = (bpm and bpm > 0) and bpm or 120
|
|
|
|
for _, s in ipairs(SUBSETS) do tex.load_folder(SCR, s) end
|
|
|
|
-- random picks, as the game does when config.ini forces none
|
|
local up = math.random(1, 4)
|
|
self.upper = { p1 = T("nk_upper", "bg_" .. up .. "_p1"), miss = T("nk_upper", "bg_" .. up .. "_miss"),
|
|
clear = T("nk_upper", "bg_" .. up .. "_clear") }
|
|
-- 2P: the same set recoloured blue (nk_upper/*_2p.png; taikonantoka has no 2P art)
|
|
self.upper2 = { p1 = T("nk_upper", "bg_" .. up .. "_p1_2p"), miss = T("nk_upper", "bg_" .. up .. "_miss_2p"),
|
|
clear = T("nk_upper", "bg_" .. up .. "_clear_2p") }
|
|
self.stage = T("nk_stage", "stage_" .. math.random(1, 3))
|
|
self.fiver = T("nk_fiver", "fiver_" .. math.random(1, 2) .. "_1")
|
|
self.chibi_set = CHIBI_SETS[math.random(1, 2)]
|
|
|
|
local function prep(list)
|
|
for _, l in ipairs(list) do l.t = T("nk_dbg1", l.tex) end
|
|
local maxend = -NEVER
|
|
for _, l in ipairs(list) do
|
|
l.appear_end = l.appear and trunc(l.appear[5]) or -NEVER
|
|
if l.appear_end > maxend then maxend = l.appear_end end
|
|
end
|
|
return maxend
|
|
end
|
|
prep(DBG_NORMAL)
|
|
self.clear_max_end = prep(DBG_CLEAR)
|
|
|
|
self.chara = {}
|
|
for _, n in ipairs({ "chara_p1_1", "chara_p1_2", "chara_p1_3", "chara_p1_4", "chara_p1_5",
|
|
"chara_p1_miss_1", "chara_p1_miss_2", "chara_p1_balloon_1",
|
|
"chara_p1_balloon_2", "chara_p1_balloon_3", "chara_p1_balloon_4",
|
|
"chara_p1_balloon_5", "balloon_1", "balloon_2", "balloon_3", "balloon_4",
|
|
"balloon_5", "balloon_6", "balloon_7", "balloon_8" }) do
|
|
self.chara[n] = T("nk_chara", n)
|
|
end
|
|
-- 2P Don: nk_chara/chara_p1_*_2p.png (blue recolour); the balloons are shared
|
|
self.chara2 = {}
|
|
for n, t in pairs(self.chara) do self.chara2[n] = T("nk_chara", n .. "_2p") or t end
|
|
-- 2P playing alone (top HUD): the blue upper background and the blue Don there too
|
|
if player_num == 2 then self.upper, self.chara = self.upper2, self.chara2 end
|
|
|
|
self.start_ms = nil
|
|
self.ms = 0 -- ms since the play screen started
|
|
self.now = 0 -- engine frame ms
|
|
self.clk = 0 -- beat clock
|
|
self.last_now = nil
|
|
|
|
-- gauge / states (sub_41CEB0)
|
|
self.gauge = 0 -- 0..100
|
|
self.gauge_int = 0
|
|
self.clear_line = 80 -- 60/70/80 by course; refined when the engine reports a clear
|
|
self.is_clear = false
|
|
self.is_full = false
|
|
self.state = 0 -- 0 normal 1 miss 2 clear 3 full
|
|
self.prev_state = 0
|
|
self.t_miss = NEVER
|
|
self.t_clear = NEVER
|
|
self.pending_hit = false
|
|
self.clear_since = nil -- CLEAR dancer-bg clock origin (ms)
|
|
self.level_t = {} -- beat-clock stamp per integer gauge level (dbl_162BFE8)
|
|
self.level_t[0] = 7200 - 2000
|
|
|
|
self.combo = 0
|
|
self.hop_ms = -NEVER
|
|
-- 2P (is_2p): the bottom HUD's own upper-bg / Don state, driven by player 2's events only.
|
|
-- The 1P state stays on self; both go through the same code via Background:ps(player_num).
|
|
self.p2 = { state = 0, prev_state = 0, t_miss = NEVER, t_clear = NEVER, pending_hit = false,
|
|
combo = 0, hop_ms = -NEVER, is_clear = false,
|
|
balloon = 0, balloon_ms = -NEVER, balloon_hit = false, pop_prev = false }
|
|
self.balloon = 0 -- dword_8F9C8C: 1..6 rolling, 9 burst, 10 fail
|
|
self.balloon_ms = -NEVER -- dbl_162B2F0
|
|
self.balloon_hit = false -- a balloon hit arrived since the last update
|
|
self.pop_prev = false -- "balloon_pop" was playing at the previous balloon hit
|
|
self.wipe_ms = nil -- Play/Load/bg_end start (engine frame ms)
|
|
self.end_bg = T("nk_load", "bg_end")
|
|
NK_PLAY_WIPE_MS = nil
|
|
self.chibis = {}
|
|
self.fiver_state = 0
|
|
self.song_end_ms = nil
|
|
return self
|
|
end
|
|
|
|
-- Quick restart (F1): the engine rebuilds the players but keeps this background object, so its
|
|
-- own play state (combo, Don / upper-bg states, balloons, gauge) would carry over. A restart plays
|
|
-- "restart"; a new play of it (audio.play_count, else the sound's rising edge) means start over.
|
|
function NK_RESTARTED(st)
|
|
if audio.play_count then
|
|
local n = audio.play_count("restart")
|
|
local hit = st.restart_n ~= nil and n > st.restart_n
|
|
st.restart_n = n
|
|
return hit
|
|
end
|
|
local on = audio.is_sound_playing("restart")
|
|
local hit = on and not st.restart_on
|
|
st.restart_on = on
|
|
return hit
|
|
end
|
|
|
|
-- the per-play state of Background.new (textures and random picks stay)
|
|
function Background:reset_play()
|
|
self.start_ms, self.ms, self.now, self.clk, self.last_now = nil, 0, 0, 0, nil
|
|
self.gauge, self.gauge_int, self.clear_line = 0, 0, 80
|
|
self.is_clear, self.is_full = false, false
|
|
self.state, self.prev_state, self.t_miss, self.t_clear = 0, 0, NEVER, NEVER
|
|
self.pending_hit, self.clear_since = false, nil
|
|
self.level_t = { [0] = 7200 - 2000 }
|
|
self.combo, self.hop_ms = 0, -NEVER
|
|
self.p2 = { state = 0, prev_state = 0, t_miss = NEVER, t_clear = NEVER, pending_hit = false,
|
|
combo = 0, hop_ms = -NEVER, is_clear = false,
|
|
balloon = 0, balloon_ms = -NEVER, balloon_hit = false, pop_prev = false }
|
|
self.balloon, self.balloon_ms, self.balloon_hit, self.pop_prev = 0, -NEVER, false, false
|
|
self.wipe_ms = nil
|
|
NK_PLAY_WIPE_MS = nil
|
|
self.chibis, self.fiver_state, self.song_end_ms = {}, 0, nil
|
|
end
|
|
|
|
---------------------------------------------------------------------------------------------
|
|
function Background:update(current_ms, bpm)
|
|
self.restart_st = self.restart_st or {}
|
|
if NK_RESTARTED(self.restart_st) then self:reset_play() end
|
|
if bpm and bpm > 0 then self.bpm = bpm end
|
|
if not self.start_ms then self.start_ms = current_ms end
|
|
if self.last_now then
|
|
local dt = current_ms - self.last_now
|
|
if dt > 0 then self.clk = self.clk + dt * self.bpm / 60000 * BEAT end
|
|
end
|
|
self.last_now = current_ms
|
|
self.now = current_ms
|
|
self.ms = current_ms - self.start_ms
|
|
|
|
-- chibi lifetime: 6 beats, or once off the right edge
|
|
local keep = {}
|
|
for _, c in ipairs(self.chibis) do
|
|
if self.clk - c.clk < 21600 then keep[#keep + 1] = c end
|
|
end
|
|
self.chibis = keep
|
|
|
|
self:update_balloon(self)
|
|
if self.is_2p then self:update_balloon(self.p2) end
|
|
|
|
-- Play/Load/bg_end: state 9 of the play loop starts when the BGM stops playing
|
|
-- (IDirectSoundBuffer::GetStatus, sub_40F6D0 ~L11774). Polled only after the last note.
|
|
if not self.wipe_ms and self.song_end_ms and not audio.is_sound_playing("song") then
|
|
self.wipe_ms = current_ms
|
|
if not NK_PLAY_WIPE_MS then NK_PLAY_WIPE_MS = current_ms end
|
|
end
|
|
end
|
|
|
|
-- per-player state: self (1P, and the only player outside 2P) or self.p2
|
|
function Background:ps(player_num)
|
|
if self.is_2p and player_num == 2 then return self.p2 end
|
|
return self
|
|
end
|
|
|
|
-- balloon burst: the engine plays "balloon_pop" in the frame of the bursting hit
|
|
-- (Player::check_balloon); a rising edge right after a balloon hit = burst (state 9).
|
|
-- s = the player's state (self or self.p2): each player has its own balloon.
|
|
function Background:update_balloon(s)
|
|
if s.balloon_hit then
|
|
s.balloon_hit = false
|
|
local pop = audio.is_sound_playing("balloon_pop")
|
|
if pop and not s.pop_prev and s.balloon >= 1 and s.balloon <= 6 then
|
|
s.balloon = 9 -- timed from the bursting hit (dbl_162B2F0)
|
|
end
|
|
s.pop_prev = pop
|
|
end
|
|
local e = self.now - s.balloon_ms
|
|
if s.balloon >= 1 and s.balloon <= 6 and e > 1000 then
|
|
-- fallback for an engine without handle_balloon_end: 1 s without a hit counts as the
|
|
-- balloon ending unburst
|
|
s.balloon = 10; s.balloon_ms = self.now
|
|
elseif s.balloon == 9 and e >= 340 then s.balloon = 0
|
|
elseif s.balloon == 10 and e > 300 then s.balloon = 0 end
|
|
end
|
|
|
|
local function hit(self, player_num)
|
|
local s = self:ps(player_num)
|
|
s.symbol_hit_ms = self.now -- [YataiDON14] the difficulty symbol's hit glow
|
|
s.pending_hit = true
|
|
s.combo = s.combo + 1
|
|
if s.combo % 10 == 0 then s.hop_ms = self.now end
|
|
if s == self then self:add_chibi(false) end -- chibis belong to the 1P HUD
|
|
end
|
|
|
|
function Background:handle_good(player_num) hit(self, player_num) end
|
|
function Background:handle_ok(player_num) hit(self, player_num) end
|
|
|
|
function Background:handle_bad(player_num)
|
|
local s = self:ps(player_num)
|
|
s.combo = 0
|
|
s.hop_ms = -NEVER
|
|
if s.state ~= 1 then
|
|
s.t_miss = self.ms
|
|
s.prev_state = s.state
|
|
s.state = 1
|
|
end
|
|
if s == self then self:add_chibi(true) end
|
|
end
|
|
|
|
function Background:handle_drumroll(player_num) end
|
|
|
|
function Background:handle_balloon(player_num, remaining)
|
|
-- dword_8F9C8C: first hit 1, +1 per hit, capped at 6
|
|
local s = self:ps(player_num)
|
|
s.balloon_left = remaining -- hits left (engine 24f8d9b; nil / -1 = not known: no bubble)
|
|
if s.balloon == 0 or s.balloon >= 9 then s.balloon = 1 else s.balloon = math.min(s.balloon + 1, 6) end
|
|
s.balloon_ms = self.now
|
|
s.balloon_hit = true
|
|
end
|
|
|
|
-- the balloon reached its end note (8) unburst: the Don lets go right away (fail, as the 1 s
|
|
-- timeout above), not up to a second later
|
|
function Background:handle_balloon_end(player_num)
|
|
local s = self:ps(player_num)
|
|
if s.balloon >= 1 and s.balloon <= 6 then s.balloon = 10; s.balloon_ms = self.now end
|
|
s.balloon_left = nil
|
|
end
|
|
|
|
-- kusudama hits (the engine's handle_kusudama; without it they came as handle_balloon): the chara
|
|
-- blows up a balloon only for balloon notes, never for a kusudama
|
|
function Background:handle_kusudama(player_num) end
|
|
|
|
function Background:handle_song_end(player_num, good, ok, bad, total)
|
|
self.song_end_ms = self.now
|
|
self.full_combo = (bad == 0 and total > 0)
|
|
end
|
|
|
|
-- DAN: the player has no gauge (game_dan.cpp resets it), so handle_gauge never comes; the
|
|
-- dan gauge arrives with handle_dan (DanGameScreen::push_dan_state, every frame) and drives
|
|
-- the dancers / upper-bg states the same way. Clear = the "gauge" exam's red value (80 when
|
|
-- the course has none). Called on the object, so nk_hud's handle_gauge (chained onto it) gets
|
|
-- the value too and draws taikonantoka's soul gauge at the normal-play position; the engine's
|
|
-- own dan gauge is moved off-screen in Graphics/game/gauge_dan.
|
|
function Background:handle_dan(player_num, st)
|
|
if type(st) ~= "table" then return end
|
|
local g = tonumber(st.gauge) or 0
|
|
local red = 80
|
|
if type(st.exams) == "table" then
|
|
for _, row in ipairs(st.exams) do
|
|
if type(row) == "table" and row.type == "gauge" and tonumber(row.red) and row.red > 0 then
|
|
red = row.red; break
|
|
end
|
|
end
|
|
end
|
|
NK_DAN_RED = red
|
|
self:handle_gauge(1, g / 100, g >= red, g >= 100)
|
|
-- exam rows the engine will draw (draw_dan_info: gauge exams skipped, at most 3)
|
|
local rows = 0
|
|
if type(st.exams) == "table" then
|
|
for _, row in ipairs(st.exams) do
|
|
if type(row) == "table" and row.type ~= "gauge" then rows = rows + 1 end
|
|
end
|
|
end
|
|
self.dan_rows = math.min(rows, 3)
|
|
end
|
|
|
|
-- Dan exam panel: the parent's (PyTaikoGreen) dan background under the whole lower half, drawn at
|
|
-- the end of draw_back, i.e. right before DanGameScreen::draw_dan_info:
|
|
-- nk_dan/wall = background/collab/dan/bg_normal/bg_0 with its notice board painted out (vertical
|
|
-- stripes only, so it is stretched from the song-title bar (y 255) to the bottom)
|
|
-- nk_dan/footer = background/collab/dan/footer/0 (blue band), y 448..480
|
|
-- nk_dan/remaining_box = that notice board (bg_0 15..282 x 103..304, halved), under "残り音符数".
|
|
-- Layout (Graphics/game/dan_info + skin_config dan_select_exam_info / dan_game_hori_name): the three exam
|
|
-- rows share y 255..448 evenly (tops 263.5 / 327.5 / 391.5, step 64); the rank plate + board group
|
|
-- (150 high) is centred in the same span.
|
|
local DAN_PANEL = { top = 255, footer_y = 448, box_x = 6.5, box_y = 323.5 }
|
|
function Background:draw_dan_panel()
|
|
local P = DAN_PANEL
|
|
local wall = T("nk_dan", "wall")
|
|
if wall then
|
|
local h = (480 - P.top) * K
|
|
tex.draw_texture(wall.id, { x = 0, y = P.top * K, x2 = SW - wall.w, y2 = h - wall.h })
|
|
end
|
|
blit(T("nk_dan", "footer"), 0, P.footer_y * K)
|
|
blit(T("nk_dan", "remaining_box"), P.box_x * K, P.box_y * K)
|
|
end
|
|
|
|
function Background:add_chibi(bad)
|
|
local set = bad and CHIBI_SOUL or self.chibi_set
|
|
local kind = trunc(self.now) % set.sort
|
|
if #self.chibis >= 100 then table.remove(self.chibis, 1) end
|
|
self.chibis[#self.chibis + 1] = { set = set, kind = kind, clk = self.clk }
|
|
end
|
|
|
|
-- upper-bg state machine (sub_41CEB0, Great/Good branch) on a per-player state s
|
|
local function upper_state(s, ms, is_clear, was_clear, is_rainbow)
|
|
if s.pending_hit then
|
|
s.pending_hit = false
|
|
if s.state == 1 then s.t_miss = ms end
|
|
if is_rainbow and s.state ~= 3 then
|
|
s.prev_state = s.state; s.state = 3
|
|
elseif is_clear and s.state < 2 then
|
|
s.prev_state = s.state; s.state = 2
|
|
if not was_clear then s.t_clear = ms end
|
|
elseif not is_clear and s.state == 1 then
|
|
s.prev_state = 1; s.state = 0; s.t_clear = NEVER
|
|
end
|
|
end
|
|
if is_clear and not was_clear and s.state >= 2 and s.t_clear == NEVER then
|
|
s.t_clear = ms
|
|
end
|
|
end
|
|
|
|
function Background:handle_gauge(player_num, progress, is_clear, is_rainbow)
|
|
if self.is_2p and player_num ~= 1 then
|
|
if player_num == 2 then
|
|
local s = self.p2
|
|
upper_state(s, self.ms, is_clear, s.is_clear, is_rainbow)
|
|
s.is_clear = is_clear
|
|
end
|
|
return
|
|
end
|
|
local g = math.max(0, math.min(100, progress * 100))
|
|
local gi = math.floor(g + 1e-6)
|
|
local was_clear = self.is_clear
|
|
|
|
if is_clear and not was_clear then
|
|
-- the engine's norma is 60/70/80 % like taikonantoka's dword_8FA188
|
|
if gi < 70 then self.clear_line = 60 elseif gi < 80 then self.clear_line = 70 else self.clear_line = 80 end
|
|
elseif not is_clear and gi >= self.clear_line - 10 and self.clear_line < 80 then
|
|
self.clear_line = math.min(80, math.floor(gi / 10) * 10 + 10)
|
|
end
|
|
|
|
-- per-level beat-clock stamps (thread code C 12610)
|
|
if gi > self.gauge_int then
|
|
for i = self.gauge_int + 1, gi do self.level_t[i] = self.clk end
|
|
elseif gi < self.gauge_int then
|
|
for i = gi, self.gauge_int - 1 do self.level_t[i] = self.clk end
|
|
end
|
|
|
|
-- CLEAR dancer-bg clock: ms since the gauge last reached the clear line
|
|
if is_clear and not was_clear then self.clear_since = self.ms end
|
|
if not is_clear then self.clear_since = nil end
|
|
|
|
upper_state(self, self.ms, is_clear, was_clear, is_rainbow)
|
|
|
|
self.gauge = g
|
|
self.gauge_int = gi
|
|
self.is_clear = is_clear
|
|
self.is_full = is_rainbow
|
|
end
|
|
|
|
---------------------------------------------------------------------------------------------
|
|
-- dancer_bg.ini layers (sub_422330)
|
|
local function draw_layer(l, t)
|
|
local tx = l.t
|
|
if not tx then return end
|
|
local blend = l.add and "additive" or nil
|
|
if l.appear and t < l.appear_end then
|
|
local a = l.appear
|
|
local t0, t1, t2, t3, h1, h2, h3 = a[2], a[3], a[4], a[5], a[6], a[7], a[8]
|
|
local S = h1 - h2 + h3
|
|
local y
|
|
local function f(ta, tb) return (tb > ta) and (t - ta) / (tb - ta) or 1 end
|
|
if a[1] == "bound_up" then
|
|
if t < t0 then y = l.y + S
|
|
elseif t < t1 then y = l.y + S - h1 * f(t0, t1)
|
|
elseif t < t2 then y = l.y - h2 + h3 + h2 * f(t1, t2)
|
|
else y = l.y + h3 * (1 - f(t2, t3)) end
|
|
else -- bound_down_cos
|
|
if t < t0 then y = l.y - S
|
|
elseif t < t1 then y = l.y - S + h1 - h1 * math.cos(rad(90 * f(t0, t1)))
|
|
elseif t < t2 then y = l.y - S + h1 - h2 * math.sin(rad(90 * f(t1, t2)))
|
|
else y = l.y - h3 * math.cos(rad(90 * f(t2, t3))) end
|
|
end
|
|
blit_c(tx, l.x, y, nil, blend)
|
|
return
|
|
end
|
|
local m = l.move
|
|
if not m then blit_c(tx, l.x, l.y, nil, blend); return end
|
|
if m[1] == "nodraw" then return end
|
|
local a, b, P = m[2], m[3], m[4]
|
|
local x = l.x + a - (a - b) * cmod(trunc(t + (l.offset or 0)), P) / P
|
|
local alpha = nil
|
|
if m[1] == "xloop_blink" then
|
|
local amin, amax, pb = m[5], m[6], m[7]
|
|
local r = cmod(trunc(t), pb)
|
|
alpha = trunc(amin + (amax - amin) / 2 * (1 + math.sin(2 * math.pi * (r - 180) / pb)))
|
|
end
|
|
blit_c(tx, x, l.y, alpha, blend)
|
|
end
|
|
|
|
function Background:draw_dancer_bg()
|
|
local tC = self.clear_since and (self.ms - self.clear_since) or -NEVER
|
|
if not (self.clear_max_end <= tC) then
|
|
for _, l in ipairs(DBG_NORMAL) do draw_layer(l, self.ms) end
|
|
end
|
|
if tC >= 0 then
|
|
for _, l in ipairs(DBG_CLEAR) do draw_layer(l, tC) end
|
|
end
|
|
end
|
|
|
|
-- Play/Bg/upper: tiled, scrolling bg_scroll_speed px/s; clear/miss cross-fade over 100 ms
|
|
function Background:draw_upper(y0, u, s)
|
|
y0 = y0 or 0
|
|
u = u or self.upper
|
|
s = s or self
|
|
if not u.p1 then return end
|
|
local sp = CFG.bg_scroll_speed
|
|
local t = self.ms
|
|
local function off_for(w)
|
|
if sp == 0 then return 0 end
|
|
return (trunc(t) % trunc(w * 1000 / sp)) * sp / 1000
|
|
end
|
|
local off = off_for(u.p1.w)
|
|
local i = 0
|
|
while (i - 1) * u.p1.w < SW do
|
|
local function tx(w) return i * w - off - i * K * (w - off) / w end
|
|
blit(u.p1, tx(u.p1.w), y0)
|
|
-- clear layer
|
|
local dc = t - s.t_clear
|
|
if u.clear and dc >= 0 then
|
|
if s.state >= 2 and dc < 100 then
|
|
blit(u.clear, tx(u.clear.w), y0, trunc(dc / 100 * 255))
|
|
elseif dc >= 100 then
|
|
blit(u.clear, tx(u.clear.w), y0)
|
|
end
|
|
end
|
|
-- miss layer
|
|
local dm = t - s.t_miss
|
|
if u.miss and dm >= 0 then
|
|
if s.state == 1 then
|
|
blit(u.miss, tx(u.miss.w), y0, dm < 100 and trunc(dm * 2.55) or nil)
|
|
elseif s.prev_state == 1 and dm < 100 then
|
|
blit(u.miss, tx(u.miss.w), y0, trunc(255 - dm * 2.55))
|
|
end
|
|
end
|
|
i = i + 1
|
|
end
|
|
end
|
|
|
|
-- Play/Chara (Don), centred at (character_x, character_y)
|
|
function Background:draw_chara(cy_override, c, s)
|
|
s = s or self
|
|
if s.balloon > 0 then return end
|
|
c = c or self.chara
|
|
local clk = trunc(self.clk)
|
|
local cx, cy = CFG.character_x, cy_override or CFG.character_y
|
|
if s.state == 1 then
|
|
blit_c((clk % 3600 < 1800) and c.chara_p1_miss_2 or c.chara_p1_miss_1, cx, cy)
|
|
return
|
|
end
|
|
local e = self.now - s.hop_ms
|
|
if e >= 100 and e <= 440 then
|
|
blit_c(c.chara_p1_5, cx, cy - 20 * K * math.sin(rad((e - 100) * 180 / 340)))
|
|
return
|
|
end
|
|
local n = clk % 14400
|
|
local t
|
|
if n >= 12600 then t = c.chara_p1_4
|
|
elseif n >= 10800 then t = c.chara_p1_3
|
|
elseif clk % 3600 < 1800 then t = c.chara_p1_1
|
|
else t = c.chara_p1_2 end
|
|
blit_c(t, cx, cy)
|
|
end
|
|
|
|
-- balloon-roll Don (dword_8F9C8C: 1..6 rolling, 9 burst, 10 fail; asm 0x41993B-0x419F39)
|
|
-- s / c: the player's state and art (self / self.chara, or self.p2 / self.chara2);
|
|
-- Y maps a 1P y to this player's (2P: mirrored about y 237.5 like the rest of the 2P HUD)
|
|
function Background:draw_balloon_chara(s, c, Y)
|
|
s = s or self
|
|
c = c or self.chara
|
|
Y = Y or function(y) return y end
|
|
if s.balloon <= 0 then return end
|
|
local e = self.now - s.balloon_ms
|
|
local st = s.balloon
|
|
-- the art is not flipped for 2P: the chara keeps its 1P offset from the (mirrored) balloon,
|
|
-- so its mouth stays on the balloon's neck
|
|
local bx, by = CFG.character_balloon_x, Y(CFG.balloon_y) - (CFG.balloon_y - CFG.character_balloon_y)
|
|
if st == 9 then
|
|
-- burst: chara_p1_balloon_5 rises 40 px over 120 ms, fades 240..340 ms;
|
|
-- balloon_8 at balloon_hit, opaque < 50 ms, fading 50..100 ms
|
|
local y = by - 40 * K * math.min(e, 120) / 120
|
|
local a = (e >= 240) and trunc(255 - 255 * (e - 240) / 100) or nil
|
|
blit_c(c.chara_p1_balloon_5, bx, y, a)
|
|
if e < 100 then
|
|
blit_c(c.balloon_8, CFG.balloon_hit_x, Y(CFG.balloon_hit_y),
|
|
(e >= 50) and trunc(255 - 255 * (e - 50) / 50) or nil)
|
|
end
|
|
return
|
|
elseif st == 10 then
|
|
-- fail: chara_p1_balloon_3 < 100 ms, chara_p1_balloon_4 after, fading 200..300 ms
|
|
local a = (e >= 200) and trunc(255 - 255 * (e - 200) / 100) or nil
|
|
blit_c((e < 100) and c.chara_p1_balloon_3 or c.chara_p1_balloon_4, bx, by, a)
|
|
return
|
|
end
|
|
if e >= 50 and st == 1 then st = 2; s.balloon = 2 end
|
|
if e < 50 or e >= 200 then
|
|
blit_c(c.chara_p1_balloon_1, bx, by)
|
|
blit_c(c["balloon_" .. st], CFG.balloon_x, Y(CFG.balloon_y))
|
|
else
|
|
blit_c(c.chara_p1_balloon_2, bx, by)
|
|
local b = (e < 140) and st or math.min(st + 1, 7)
|
|
blit_c(c["balloon_" .. b], CFG.balloon_hit_x, Y(CFG.balloon_hit_y))
|
|
end
|
|
self:draw_balloon_bubble(s, e, function(y) return Y(CFG.balloon_y) - (CFG.balloon_y - y) end)
|
|
end
|
|
|
|
-- hits left over the chara (taikonantoka draw_balloon_hitting): the bubble keeps its offset from the
|
|
-- balloon (not mirrored for 2P); the digits right to left, each hit popping them up from their
|
|
-- bottom edge (100 ms)
|
|
function Background:draw_balloon_bubble(s, e, Y)
|
|
local left = s.balloon_left
|
|
if not left or left < 0 then return end
|
|
blit_c(T("nk_bubble", "bubble"), CFG.bubble_x, Y(CFG.bubble_y))
|
|
local str = tostring(left)
|
|
local n = #str
|
|
local w = CFG.bubble_num_w
|
|
local sc = (e < 100) and (1 + 0.25 * (1 - e / 100)) or 1
|
|
for i = 1, n do
|
|
local t = T("nk_bubble", "digit_" .. str:sub(i, i))
|
|
if t then
|
|
local cx = CFG.bubble_x + (i - (n + 1) / 2) * w
|
|
local bottom = Y(CFG.bubble_num_y) + t.h / 2
|
|
local h = t.h * sc
|
|
tex.draw_texture(t.id, { x = cx - t.w / 2, y = bottom - h, y2 = h - t.h })
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Play/Dancer: 5 slots, slot s visible from gauge level trunc(s * clear_line / 4)
|
|
function Background:draw_dancers()
|
|
local clk = self.clk
|
|
local P = trunc(DANCER.interval * BEAT)
|
|
local AP = trunc(DANCER.appear_interval * BEAT)
|
|
for s = 0, 4 do
|
|
local lvl = trunc(s * self.clear_line * 0.25)
|
|
if lvl <= self.gauge_int then
|
|
local typ = s -- dance_pattern = 2 (fixed positions)
|
|
local dx, dy = DANCER.x[s + 1], DANCER.y[typ + 1]
|
|
local T0 = self.level_t[lvl] or 0
|
|
if clk < T0 + AP then
|
|
local p = (clk - T0) / AP
|
|
if p >= 0 then
|
|
local fr = DANCER.appear_order[math.min(trunc(p * 20), 19) + 1]
|
|
local t = T("nk_dancer1", "appear_" .. (typ + 1) .. "_" .. fr)
|
|
local y = dy
|
|
if DANCER.appear_pattern[typ + 1] == 1 then
|
|
local v
|
|
if p <= 0.4 then v = 520 - 250 * math.sin(rad(90 * p / 0.4))
|
|
elseif p < 0.7 then v = 270 + 70 * math.sin(rad(90 * (p - 0.4) / 0.3)) end
|
|
if v then y = dy + v * K - 340 * K end
|
|
end
|
|
blit_c(t, dx, y)
|
|
if s == 2 and DANCER.appear_pattern[typ + 1] == 1 and p >= 0.5 then
|
|
local v
|
|
if p < 0.7 then v = 110 * math.sin(rad(90 + 450 * (p - 0.5)))
|
|
elseif p < 0.73 then v = 0
|
|
elseif p < 0.95 then v = 80 * math.sin(rad(627.27 * (p - 0.73)))
|
|
else v = 54 end
|
|
local a = (p < 0.6) and trunc(255 * (p - 0.5) * 10) or nil
|
|
blit_c(T("nk_dancer1", "shakushi_1"), DANCER.x[3], DANCER.y[3] - v * K, a)
|
|
end
|
|
end
|
|
else
|
|
local m = trunc(clk) % P
|
|
local k = 68 * m / P
|
|
local idx = math.min(trunc(k), 67)
|
|
local fr = DANCER.order[idx + 1]
|
|
if s == 2 then
|
|
local f0 = fr - 1
|
|
local sn = math.sin(rad(180 + 90 * k))
|
|
local sx, sy, name = DANCER.x[3], dy, nil
|
|
if f0 <= 2 then name = "shakushi_2"; sy = dy - 24 * K * math.max(0, sn)
|
|
elseif f0 == 3 then name = "shakushi_2"; sx = sx + 5 * K; sy = dy - 5 * K
|
|
elseif f0 == 4 then name = "shakushi_3"; sy = dy - 6 * K
|
|
elseif f0 == 5 then name = "shakushi_4"; sx = sx + 5 * K; sy = dy - 5 * K
|
|
elseif f0 <= 8 then
|
|
name = "shakushi_4"
|
|
if sn >= 0 and k >= 16 and k <= 29 then sy = dy - 24 * K * sn end
|
|
end
|
|
if name then blit_c(T("nk_dancer1", name), sx, sy) end
|
|
end
|
|
blit_c(T("nk_dancer1", "dance_" .. (typ + 1) .. "_" .. fr), dx, dy)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Play/Bg/stage: centred on stage_y
|
|
function Background:draw_stage()
|
|
if self.stage then blit(self.stage, 0, CFG.stage_y - self.stage.h / 2) end
|
|
end
|
|
|
|
-- Play/Bg/fiver: rises with the full soul gauge, bobs each beat, falls when it drops
|
|
function Background:draw_fiver()
|
|
local f = self.fiver
|
|
if not f then return end
|
|
local p = (self.clk % 3600) / 20
|
|
local h = f.h
|
|
local function bob() return 526 * K - trunc(46 * K * math.sin(rad(p))) - h end
|
|
local st = self.fiver_state
|
|
if self.is_full then
|
|
if st == 0 then
|
|
if p > 110 and p <= 180 then self.fiver_state = 1 end
|
|
elseif st == 1 then
|
|
if p <= 90 then
|
|
blit(f, 0, SH - trunc(h * math.sin(rad(p))))
|
|
if p >= 70 then self.fiver_state = 2 end
|
|
elseif p <= 110 then
|
|
blit(f, 0, bob()); self.fiver_state = 2
|
|
end
|
|
else
|
|
blit(f, 0, bob())
|
|
end
|
|
else
|
|
if st == 2 then
|
|
blit(f, 0, bob())
|
|
if p <= 90 then self.fiver_state = 1 end
|
|
elseif st == 1 then
|
|
if p <= 90 then
|
|
blit(f, 0, bob())
|
|
else
|
|
local y = 530 * K - trunc((h + 50 * K) * math.sin(rad(p)))
|
|
blit(f, 0, y)
|
|
if p >= 175 or y >= SH then self.fiver_state = 0 end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- "chibi_<kind+1>_<frame>" strings, built once instead of per chibi per frame
|
|
local CHIBI_NAME = setmetatable({}, { __index = function(t, kind)
|
|
local names = setmetatable({}, { __index = function(n, frame)
|
|
local s = "chibi_" .. (kind + 1) .. "_" .. frame
|
|
n[frame] = s
|
|
return s
|
|
end })
|
|
t[kind] = names
|
|
return names
|
|
end })
|
|
|
|
-- Play/Chibi: one per Great/Good hit (Soul chibi on a miss), 6 beats of life
|
|
function Background:draw_chibis()
|
|
for _, c in ipairs(self.chibis) do
|
|
local set, kind = c.set, c.kind
|
|
local dt = self.clk - c.clk
|
|
local name, x, y
|
|
if dt < 1800 then
|
|
x = set.x + dt / 18 * K
|
|
if set.appear == 1 then
|
|
y = set.y - 35 * K * math.sin(rad(dt / 10))
|
|
name = CHIBI_NAME[kind][set.appear_frame]
|
|
else
|
|
y = set.y
|
|
name = CHIBI_NAME[kind][(dt < 600) and 1 or (dt < 1200) and 2 or 3]
|
|
end
|
|
else
|
|
x = set.x + 100 * K + (dt - 1800) / 27 * K
|
|
if set.xp[kind + 1] == 2 then x = x + 18 * K * math.sin(rad(dt / 20)) end
|
|
local yp = set.yp[kind + 1]
|
|
if yp == 0 then
|
|
if dt < 5900 then y = set.y - 16 * K * math.sin(rad((dt - 2300) / 20))
|
|
else y = set.y + 19 * K * math.sin(rad((dt - 5900) / 15)) end
|
|
elseif yp == 1 then
|
|
y = CHIBI_SETS[1].y
|
|
else
|
|
y = set.y + 9 * K * math.sin(rad(dt / 10))
|
|
end
|
|
local Pi = trunc(set.interval * BEAT)
|
|
local n = #set.order
|
|
local idx = math.floor(((trunc(dt) % Pi) * n) / Pi)
|
|
name = CHIBI_NAME[kind][set.order[idx + 1]]
|
|
end
|
|
local t = T(set.sub, name)
|
|
if t and x - t.w / 2 <= SW then blit_c(t, x, y) end
|
|
end
|
|
end
|
|
|
|
-- Play/Drapery: full-combo curtain, from last note + 1000 ms
|
|
function Background:draw_drapery()
|
|
if not (self.song_end_ms and self.full_combo) then return end
|
|
local d = self.now - self.song_end_ms
|
|
local d1, d2, d3 = T("nk_drapery", "drapery_1"), T("nk_drapery", "drapery_2"), T("nk_drapery", "drapery_3")
|
|
if not (d1 and d2 and d3) then return end
|
|
local E
|
|
if d < 800 then E = 150 + 490 * math.sin(rad(90 + 90 * d / 800))
|
|
elseif d < 950 then E = 150 + 20 * math.sin(rad(180 * (d - 800) / 150))
|
|
elseif d < 1050 then E = 150 + 10 * math.sin(rad(180 * (d - 950) / 100))
|
|
else E = 150 end
|
|
E = E * K
|
|
local x0 = d2.w - 28 * K
|
|
local F
|
|
if d < 1050 then F = x0
|
|
elseif d < 1400 then F = x0 * (1 - 0.8 * (d - 1050) / 350)
|
|
elseif d < 1500 then F = 0.2 * x0 * (1 - math.sin(rad(90 * (d - 1400) / 100)))
|
|
else F = 0 end
|
|
local S = CFG.note_y
|
|
local function cropped(t)
|
|
local l = trunc(F)
|
|
if l >= t.w then return end
|
|
tex.draw_texture(t.id, { x = d1.w + E - 2 * K, y = S - t.h / 2 + 0,
|
|
src = { x = l, y = 0, width = t.w - l, height = t.h }, x2 = -l })
|
|
end
|
|
cropped(d2)
|
|
blit(d1, E, S - d1.h / 2)
|
|
for i, pos in ipairs(FLOWERS) do
|
|
local tt = d - 1500 - 33.33 * (i - 1)
|
|
if tt >= 0 then
|
|
local fl = T("nk_drapery", "flower_" .. i)
|
|
if fl then
|
|
local sc = (tt < 80) and (1 + 0.3 * math.sin(rad(90 * tt / 80))) or 1
|
|
tex.draw_texture(fl.id, { x = pos[1] - fl.w / 2, y = pos[2] - fl.h / 2, center = true, scale = sc })
|
|
end
|
|
end
|
|
end
|
|
cropped(d3)
|
|
end
|
|
|
|
---------------------------------------------------------------------------------------------
|
|
-- 2P (skin_config game_p2_y 231): 1P HUD / 1P lane (116..220) / title bar (220..255) / 2P lane
|
|
-- (255..359) / 2P HUD (359..480) -- the 2P lane is the 1P lane mirrored about y 237.5 (y -> 475 - y).
|
|
-- The 2P HUD gets the blue upper background and the blue Don (mirrored position, not flipped); the
|
|
-- soul gauge / balloons are nk_hud's.
|
|
local P2_HUD_Y = 359
|
|
local P2_LANE_EDGE_H = 4
|
|
function Background:draw_p2_hud()
|
|
self:draw_upper(P2_HUD_Y * K, self.upper2, self.p2)
|
|
self:draw_symbol(2, self.p2, (P2_MIRROR - 64) * K) -- [YataiDON14] the 2P difficulty symbol
|
|
self:draw_chara((P2_MIRROR - 62) * K, self.chara2, self.p2) -- CFG.character_y is already x K: 62 = dispconf
|
|
tex.draw_rect(0, P2_HUD_Y * K, SW, P2_LANE_EDGE_H * K, 0, 0, 0, 255)
|
|
end
|
|
|
|
-- The black title band under the 1P lane (lane_background rows 109..145 in taikonantoka's art).
|
|
-- It is cut out of lane/lane_background.png and drawn here once, so the 2P lane (which reuses
|
|
-- lane_background) does not get a second band below it. 1P lane_background top = 92 + 18.
|
|
local TITLE_BAND_Y = 92 + 18 + 109
|
|
|
|
local SYMBOL_COURSE = { [0] = "easy", "normal", "hard", "oni", "edit" }
|
|
local SYMBOL_GLOW1 = { 0, 0, .5, 1, .918, .832, .75, .668, .582, .5, .418, .332, .25, .168, .082, 0 }
|
|
local SYMBOL_GLOW2 = { 0, 0, 0, 0, .5, 1, .801, .602, .398, .199, 0, 0, 0, 0, 0, 0 }
|
|
function Background:draw_symbol(pn, s, cy)
|
|
local c = type(NK_COURSE_BY_PLAYER) == "table" and NK_COURSE_BY_PLAYER[pn]
|
|
local name = SYMBOL_COURSE[c or 3] or "oni"
|
|
local base = T("nk_symbol", name .. "_1")
|
|
if not base then return end
|
|
local x, y = 98 * K, cy - base.h / 2
|
|
blit(base, x, y)
|
|
if s.symbol_hit_ms then
|
|
local f = trunc((self.now - s.symbol_hit_ms) * 60 / 1000) + 1
|
|
local a1, a2 = SYMBOL_GLOW1[f], SYMBOL_GLOW2[f]
|
|
if a1 and a1 > 0 then blit(T("nk_symbol", name .. "_2"), x, y, trunc(a1 * 255), "additive") end
|
|
if a2 and a2 > 0 then blit(T("nk_symbol", name .. "_3"), x, y, trunc(a2 * 255), "additive") end
|
|
end
|
|
end
|
|
|
|
function Background:draw_back()
|
|
self:draw_dancer_bg()
|
|
self:draw_upper()
|
|
self:draw_symbol(1, self, 64 * K) -- [YataiDON14] under the chara
|
|
self:draw_chara()
|
|
if not self.is_2p then self:draw_dancers() end
|
|
self:draw_stage()
|
|
if not self.is_2p then self:draw_fiver() end
|
|
if self.is_2p then self:draw_p2_hud() end
|
|
blit(T("nk_upper", "title_band"), 0, TITLE_BAND_Y * K)
|
|
if self.dan_rows then self:draw_dan_panel() end
|
|
end
|
|
|
|
function Background:draw_fore()
|
|
self:draw_chibis()
|
|
self:draw_balloon_chara()
|
|
if self.is_2p then
|
|
self:draw_balloon_chara(self.p2, self.chara2, function(y) return (P2_MIRROR + P2_BALLOON_DY) * K - y end)
|
|
end
|
|
self:draw_drapery()
|
|
-- The wipe is drawn by Scripts/result/result_transition.lua only: that layer is above the
|
|
-- engine's song title (GameScreen::draw_overlay), this one is below it (user: no title on
|
|
-- the transition).
|
|
end
|
|
|
|
-- Play/Load/bg_end.png (640x960): y = 480 - 0.96 d, d = ms since the BGM stopped,
|
|
-- -480 from 1000 ms on (asm 0x41C229); the bottom half then covers the screen until the
|
|
-- engine changes scene (result_transition.lua keeps drawing it above the HUD).
|
|
function Background:draw_end_wipe()
|
|
if not (self.wipe_ms and self.end_bg) then return end
|
|
local d = self.now - self.wipe_ms
|
|
blit(self.end_bg, 0, (d < 1000) and (SH - d * 0.96 * K) or -SH)
|
|
end
|
|
|
|
function Background:destroy()
|
|
TCACHE = {}
|
|
for _, s in ipairs(SUBSETS) do tex.unload_folder(SCR, s) end
|
|
end
|
|
|
|
-- HUD module (Graphics/game/MAPPING_hud.md, round 2)
|
|
do local ok, hud = pcall(require, "game.nk_hud"); if ok and type(hud) == "table" then hud.install(Background) end end
|