diff --git a/mods/character-select-coop/a-utils.lua b/mods/character-select-coop/a-utils.lua new file mode 100644 index 000000000..625b2197c --- /dev/null +++ b/mods/character-select-coop/a-utils.lua @@ -0,0 +1,90 @@ +---------------------- +-- Global Functions -- +---------------------- + +local saveableCharacters = { + ["1"] = true, + ["2"] = true, + ["3"] = true, + ["4"] = true, + ["5"] = true, + ["6"] = true, + ["7"] = true, + ["8"] = true, + ["9"] = true, + ["0"] = true, + ["a"] = true, + ["b"] = true, + ["c"] = true, + ["d"] = true, + ["e"] = true, + ["f"] = true, + ["g"] = true, + ["h"] = true, + ["i"] = true, + ["j"] = true, + ["k"] = true, + ["l"] = true, + ["m"] = true, + ["n"] = true, + ["o"] = true, + ["p"] = true, + ["q"] = true, + ["r"] = true, + ["s"] = true, + ["t"] = true, + ["u"] = true, + ["v"] = true, + ["w"] = true, + ["x"] = true, + ["y"] = true, + ["z"] = true, + ["_"] = true, + ["-"] = true, +} + +function string_underscore_to_space(string) + local s = '' + for i = 1, #string do + local c = string:sub(i,i) + if c ~= '_' then + s = s .. c + else + s = s .. " " + end + end + return s +end + +function string_space_to_underscore(string) + local s = '' + for i = 1, #string do + local c = string:sub(i,i) + if saveableCharacters[string.lower(c)] then + s = s .. c + else + s = s .. "_" + end + end + return s +end + +---------------------- +-- Global Variables -- +---------------------- + +modVersion = "1.5" + +allowMenu = {} + +menuColorTable = { + {r = 255, g = 50, b = 50 }, + {r = 255, g = 100, b = 50 }, + {r = 255, g = 255, b = 50 }, + {r = 50, g = 255, b = 50 }, + {r = 100, g = 100, b = 255}, + {r = 251, g = 148, b = 220}, + {r = 130, g = 25, b = 130}, + {r = 255, g = 255, b = 255}, + {r = 50, g = 50, b = 50 }, +} diff --git a/mods/character-select-coop/actors/armature_geo.bin b/mods/character-select-coop/actors/armature_geo.bin new file mode 100644 index 000000000..70835e7ed Binary files /dev/null and b/mods/character-select-coop/actors/armature_geo.bin differ diff --git a/mods/character-select-coop/main.lua b/mods/character-select-coop/main.lua new file mode 100644 index 000000000..c83092e5d --- /dev/null +++ b/mods/character-select-coop/main.lua @@ -0,0 +1,788 @@ +-- name: Character Select +-- description: A Library / API made to make adding and\nusing Custom Characters as simple as possible!\n\nCreated by:\\#008800\\ Squishy6094\n\\#dcdcdc\\Concepts by:\\#4496f5\\ AngelicMiracles\\#AAAAFF\\\n\nGithub:\nSquishy6094/character-select-coop + + +menu = false +options = false + +currChar = 1 +local currOption = 1 + + +local TEX_HEADER = get_texture_info("char-select-text") + +local TEXT_PREF_LOAD = "Default" + +local ommActive = _G.OmmApi ~= nil + +--[[ + Note: Do NOT add characters via the characterTable below, + We highly reccomend you create your own mod and use the + API to add characters, this ensures your pack is easy + to use for anyone and low on file space! +]] +characterTable = { + [1] = { + name = "Default", + description = {"The vanilla cast for sm64ex-coop!", "", "These Characters are swappable", "via the default Options Menu"}, + credit = "Nintendo / Coop Team", + color = {r = 255, g = 50, b = 50}, + model = nil, + forceChar = nil, + lifeIcon = gTextures.mario_head, + }, +} + +local optionTableRef = { + openInputs = 1, + menuColor = 2, + anims = 3, + inputLatency = 4, + localModels = 5, + prefToDefault = 6, +} + +optionTable = { + [optionTableRef.openInputs] = { + name = "Open Binds", + toggle = tonumber(mod_storage_load("MenuInput")), + toggleSaveName = "MenuInput", + toggleDefault = 0, + toggleMax = 2, + toggleNames = {"None", ommActive and "D-pad Down + R" or "D-pad Down", "Z (Pause Menu)"}, + }, + [optionTableRef.menuColor] = { + name = "Menu Color", + toggle = tonumber(mod_storage_load("MenuColor")), + toggleSaveName = "MenuColor", + toggleDefault = 0, + toggleMax = 9, + toggleNames = {"Auto", "Red", "Orange", "Yellow", "Green", "Blue", "Pink", "Purple", "White", "Black"}, + }, + [optionTableRef.anims] = { + name = "Animations", + toggle = tonumber(mod_storage_load("Anims")), + toggleSaveName = "Anims", + toggleDefault = 1, + toggleMax = 1, + }, + [optionTableRef.inputLatency] = { + name = "Scroll Speed", + toggle = tonumber(mod_storage_load("Latency")), + toggleSaveName = "Latency", + toggleDefault = 1, + toggleMax = 2, + toggleNames = {"Slow", "Normal", "Fast"}, + }, + [optionTableRef.localModels] = { + name = "Locally Display Models", + toggle = tonumber(mod_storage_load("localModels")), + toggleSaveName = "localModels", + toggleDefault = 1, + toggleMax = 1, + }, + [optionTableRef.prefToDefault] = { + name = "Set Preference to Default", + toggle = 0, + toggleDefault = 0, + toggleMax = 1, + toggleNames = {"", ""}, + }, +} + +local defaultPlayerColors = { + [CT_MARIO] = {r = 255, g = 50, b = 50}, + [CT_LUIGI] = {r = 50, g = 255, b = 50}, + [CT_TOAD] = {r = 100, g = 100, b = 255}, + [CT_WALUIGI] = {r = 130, g = 25, b = 130}, + [CT_WARIO] = {r = 255, g = 255, b = 50}, +} + +local defaultIcons = { + [CT_MARIO] = gTextures.mario_head, + [CT_LUIGI] = gTextures.luigi_head, + [CT_TOAD] = gTextures.toad_head, + [CT_WALUIGI] = gTextures.waluigi_head, + [CT_WARIO] = gTextures.wario_head, +} + +local latencyValueTable = {15, 10, 5} + +local camera_freeze = camera_freeze +local camera_unfreeze = camera_unfreeze +local network_local_index_from_global = network_local_index_from_global +local obj_set_model_extended = obj_set_model_extended +local hud_hide = hud_hide +local hud_show = hud_show +local djui_chat_message_create = djui_chat_message_create +local djui_hud_set_resolution = djui_hud_set_resolution +local djui_hud_set_font = djui_hud_set_font +local djui_hud_set_color = djui_hud_set_color +local djui_hud_get_screen_width = djui_hud_get_screen_width +local djui_hud_render_rect = djui_hud_render_rect +local djui_hud_print_text = djui_hud_print_text +local djui_hud_render_texture = djui_hud_render_texture +local math_max = math.max +local math_min = math.min +local math_sin = math.sin +local math_random = math.random +local play_sound = play_sound +local mod_storage_save = mod_storage_save +local mod_storage_load = mod_storage_load +local string_lower = string.lower + +---@param m MarioState +local function nullify_inputs(m) + local c = m.controller + _G.charSelect.controller = { + buttonDown = c.buttonDown, + buttonPressed = c.buttonPressed & ~_G.charSelect.controller.buttonDown, + extStickX = c.extStickX, + extStickY = c.extStickY, + rawStickX = c.rawStickX, + rawStickY = c.rawStickY, + stickMag = c.stickMag, + stickX = c.stickX, + stickY = c.stickY + } + c.buttonDown = 0 + c.buttonPressed = 0 + c.extStickX = 0 + c.extStickY = 0 + c.rawStickX = 0 + c.rawStickY = 0 + c.stickMag = 0 + c.stickX = 0 + c.stickY = 0 +end + +local function load_preferred_char() + if mod_storage_load("PrefChar") ~= nil and mod_storage_load("PrefChar") ~= "Default" then + for i = 2, #characterTable do + if characterTable[i].name == mod_storage_load("PrefChar") then + currChar = i + djui_popup_create('Character Select:\nYour Preferred Character\n"'..string_underscore_to_space(characterTable[i].name)..'"\nwas applied successfully!', 4) + break + end + end + elseif mod_storage_load("PrefChar") == nil then + mod_storage_save("PrefChar", "Default") + end + if #characterTable == 1 then + djui_popup_create("Character Select:\nNo Characters were Found", 2) + end + TEXT_PREF_LOAD = mod_storage_load("PrefChar") +end + +local function failsafe_options() + for i = 1, #optionTable do + if optionTable[i].toggle == nil then + optionTable[i].toggle = optionTable[i].toggleDefault + if optionTable[i].toggleSaveName ~= nil then + mod_storage_save(optionTable[i].toggleSaveName, tostring(optionTable[i].toggle)) + end + end + if optionTable[i].toggleNames == nil then + optionTable[i].toggleNames = {"Off", "On"} + end + end + if optionTable[optionTableRef.openInputs].toggle == 1 and ommActive then + djui_popup_create('Character Select:\nYour Open bind has changed to:\nD-pad Down + R\nDue to OMM Rebirth being active!', 4) + end +end + +local hasOpenedMenu = false + +local function idiot_proof_note() + if mod_storage_load("openedmenu") == nil then + if #characterTable > 1 then + djui_chat_message_create("Character Select is active and has "..(#characterTable - 1).." characters available!\nYou can use \\#ffff00\\/char-select \\#ffffff\\to open the menu!") + else + djui_chat_message_create("Character Select is active!\nYou can use \\#ffff00\\/char-select \\#ffffff\\to open the menu!") + end + else + hasOpenedMenu = true + end +end + +------------------- +-- Model Handler -- +------------------- + +local stallFrame = 0 +local noLoop = false + +-- Respecfully, GO FUCK YOURSELVES. I hate EVERY SINGLE ONE OF YOU. Your lives are NOTHING. You serve ZERO PURPOSE. You should kill yourselves, NOW! +local ignored_surfaces = { + SURFACE_BURNING, SURFACE_QUICKSAND, SURFACE_INSTANT_QUICKSAND, SURFACE_INSTANT_MOVING_QUICKSAND, SURFACE_DEEP_MOVING_QUICKSAND, SURFACE_INSTANT_QUICKSAND, SURFACE_DEEP_QUICKSAND, SURFACE_SHALLOW_MOVING_QUICKSAND, + SURFACE_SHALLOW_QUICKSAND, SURFACE_WARP, SURFACE_LOOK_UP_WARP, SURFACE_WOBBLING_WARP, SURFACE_INSTANT_WARP_1B, SURFACE_INSTANT_WARP_1C, SURFACE_INSTANT_WARP_1D, SURFACE_INSTANT_WARP_1E +} +-- Yes, floral gave me permission to use this table full of USELESS PIECES OF SHITS + +--- @param m MarioState +local function mario_update(m) + if stallFrame == 1 then + load_preferred_char() + failsafe_options() + idiot_proof_note() + end + + if stallFrame < 2 then + stallFrame = stallFrame + 1 + end + + if m.playerIndex == 0 and stallFrame > 1 then + characterTable[1].forceChar = gNetworkPlayers[m.playerIndex].modelIndex + if currChar == 1 then + characterTable[1].color = defaultPlayerColors[gNetworkPlayers[m.playerIndex].modelIndex] + characterTable[1].lifeIcon = defaultIcons[gNetworkPlayers[m.playerIndex].modelIndex] + end + if optionTable[optionTableRef.localModels].toggle > 0 then + gPlayerSyncTable[0].modelId = characterTable[currChar].model + gPlayerSyncTable[0].capModels = characterTable[currChar].capModels + if characterTable[currChar].forceChar ~= nil then + gNetworkPlayers[m.playerIndex].overrideModelIndex = characterTable[currChar].forceChar + end + else + gPlayerSyncTable[0].modelId = nil + gPlayerSyncTable[0].capModels = nil + gNetworkPlayers[m.playerIndex].overrideModelIndex = characterTable[1].forceChar + end + + if menu then + for _, func in pairs(allowMenu) do + if not func() then + menu = false + return + end + end + camera_freeze() + hud_hide() + if _G.PersonalStarCounter then + _G.PersonalStarCounter.hide_star_counters(true) + end + local focusPos = { + x = m.pos.x, + y = m.pos.y + 120, + z = m.pos.z, + } + vec3f_copy(gLakituState.focus, focusPos) + gLakituState.pos.x = m.pos.x + sins(m.faceAngle.y) * 500 + gLakituState.pos.y = m.pos.y + 100 + gLakituState.pos.z = m.pos.z + coss(m.faceAngle.y) * 500 + + if m.forwardVel == 0 and m.pos.y == m.floorHeight and not ignored_surfaces[m.floor.type] and m.health > 255 then + set_mario_action(m, ACT_IDLE, 0) + set_mario_animation(m, MARIO_ANIM_STAR_DANCE) + end + noLoop = false + elseif not noLoop then + camera_unfreeze() + hud_show() + if _G.PersonalStarCounter then + _G.PersonalStarCounter.hide_star_counters(false) + end + noLoop = true + end + end + + --Set Pref to Default Check + if optionTable[optionTableRef.prefToDefault].toggle > 0 then + mod_storage_save("PrefChar", "Default") + TEXT_PREF_LOAD = "Default" + optionTable[optionTableRef.prefToDefault].toggle = 0 + end +end + +function set_model(o, model) + if obj_has_behavior_id(o, id_bhvMario) ~= 0 then + local i = network_local_index_from_global(o.globalPlayerIndex) + if gPlayerSyncTable[i].modelId ~= nil and obj_has_model_extended(o, gPlayerSyncTable[i].modelId) == 0 then + obj_set_model_extended(o, gPlayerSyncTable[i].modelId) + return + end + end + if get_object_list_from_behavior(o.behavior) == OBJ_LIST_LEVEL then + local i = network_local_index_from_global(o.globalPlayerIndex) + local capModels = gPlayerSyncTable[i].capModels + local capModel = nil + if capModels ~= nil then + if model == gMarioStates[i].character.capModelId then + capModel = capModels.normal + elseif model == gMarioStates[i].character.capWingModelId then + capModel = capModels.wing + elseif model == gMarioStates[i].character.capMetalModelId then + capModel = capModels.metal + elseif model == gMarioStates[i].character.capMetalWingModelId then + capModel = capModels.metalWing + end + if capModel ~= nil and obj_has_model_extended(o, capModel) == 0 then + obj_set_model_extended(o, capModel) + return + end + end + end +end + +hook_event(HOOK_MARIO_UPDATE, mario_update) +hook_event(HOOK_OBJECT_SET_MODEL, set_model) + +------------------ +-- Menu Handler -- +------------------ + +local buttonAnimTimer = 0 +local buttonScroll = 0 +local buttonScrollCap = 30 + +local optionAnimTimer = -200 +local optionAnimTimerCap = optionAnimTimer + +local inputStallTimer = 0 +local inputStallTo = 15 + +local TEXT_OPTIONS_HEADER = "Menu Options" +local TEXT_RATIO_UNSUPPORTED = "Your Current Aspect-Ratio isn't Supported!" +local TEXT_DESCRIPTION = "Character Description:" +local TEXT_PREF_SAVE = "Press A to Set as Preferred Character" +local TEXT_PAUSE_Z_OPEN = "Z Button - Character Select" +local TEXT_PAUSE_CURR_CHAR = "Current Character: " +if math_random(100) == 64 then -- Easter Egg if you get lucky loading the mod >v< + TEXT_PAUSE_Z_OPEN = "Z - DynOS" -- Referencing the original sm64ex DynOS options + TEXT_PAUSE_CURR_CHAR = "Model: " +end +local TEXT_OPTIONS_OPEN = "Press START to open Options" +local TEXT_MENU_CLOSE = "Press B to Exit Menu" +local TEXT_OPTIONS_SELECT = "A - Select | B - Exit " +local TEXT_LOCAL_MODEL_OFF = "Locally Display Models is Off" +local TEXT_LOCAL_MODEL_OFF_OPTIONS = "You can turn it back on in the Options Menu" + +local menuColor = characterTable[currChar].color + +local function on_hud_render() + djui_hud_set_resolution(RESOLUTION_N64) + djui_hud_set_font(FONT_NORMAL) + + local width = djui_hud_get_screen_width() + 1.4 + local height = 240 + local widthHalf = width*0.5 + local heightHalf = height*0.5 + local widthScale = math_max(width, 321.4)*0.00311332503 + + if menu then + if optionTable[optionTableRef.menuColor].toggle ~= 0 then + menuColor = menuColorTable[optionTable[optionTableRef.menuColor].toggle] + else + menuColor = characterTable[currChar].color + end + + if optionTable[optionTableRef.localModels].toggle == 0 then + djui_hud_set_color(0, 0, 0, 200) + djui_hud_render_rect(0, 0, width, height) + djui_hud_set_color(255, 255, 255, 255) + djui_hud_print_text(TEXT_LOCAL_MODEL_OFF, widthHalf - djui_hud_measure_text(TEXT_LOCAL_MODEL_OFF)*0.15*widthScale, heightHalf, 0.3 * widthScale) + djui_hud_print_text(TEXT_LOCAL_MODEL_OFF_OPTIONS, widthHalf - djui_hud_measure_text(TEXT_LOCAL_MODEL_OFF_OPTIONS)*0.1*widthScale, heightHalf + 10*widthScale, 0.2*widthScale) + end + + --Character Buttons + local x = 135 * widthScale * 0.8 + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + djui_hud_render_rect(0, 0, x, height) + djui_hud_set_color(0, 0, 0, 255) + djui_hud_render_rect(2, 2, x - 4, height - 4) + + if optionTable[optionTableRef.anims].toggle > 0 then + buttonAnimTimer = buttonAnimTimer + 1 + end + if optionTable[optionTableRef.anims].toggle == 0 then + buttonScroll = 0 + elseif buttonScroll > 0.1 or buttonScroll < -0.1 then + buttonScroll = buttonScroll*0.03*inputStallTo + end + + local buttonColor = {} + for i = -1, 4 do + if characterTable[i + currChar] ~= nil then + buttonColor = characterTable[i + currChar].color + djui_hud_set_color(buttonColor.r, buttonColor.g, buttonColor.b, 255) + local buttonX = 20 * widthScale + if optionTable[optionTableRef.anims].toggle > 0 then + if i == 0 then buttonX = buttonX + math_sin(buttonAnimTimer*0.05)*2.5 + 5 end + else + if i == 0 then buttonX = buttonX + 10 end + end + local y = (i + 3) * 30 + buttonScroll + djui_hud_render_rect(buttonX, y, 70, 20) + djui_hud_set_color(0, 0, 0, 255) + djui_hud_render_rect(buttonX + 1, y + 1, 68, 18) + djui_hud_set_font(FONT_TINY) + djui_hud_set_color(buttonColor.r, buttonColor.g, buttonColor.b, 255) + djui_hud_print_text(string_underscore_to_space(characterTable[currChar + i].name), buttonX + 5, y + 5, 0.6) + end + end + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + djui_hud_render_rect(0, height-2, x, 2) + + -- Scroll Bar + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + djui_hud_render_rect(7 * widthScale, 55, 7, 180) + djui_hud_set_color(0, 0, 0, 255) + djui_hud_render_rect(7 * widthScale + 1, 56, 5, 178) + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + djui_hud_render_rect(7 * widthScale + 2, 57 + 176 * ((currChar - 1) / #characterTable) - (buttonScroll*0.03333333333)*(176/#characterTable), 3, 176/#characterTable) + + + --Character Description + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + djui_hud_render_rect(width - x, 0, x, height) + djui_hud_set_color(0, 0, 0, 255) + djui_hud_render_rect(width - x + 2, 2, x - 4, height - 4) + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + djui_hud_set_font(FONT_NORMAL) + + local TEXT_NAME = string_underscore_to_space(characterTable[currChar].name) + local TEXT_CREDIT = "By: "..characterTable[currChar].credit + local TEXT_DESCRIPTION_TABLE = characterTable[currChar].description + local TEXT_PREF = 'Preferred Character: "'..string_underscore_to_space(TEXT_PREF_LOAD)..'"' + + local textX = x * 0.5 + djui_hud_print_text(TEXT_NAME, width - textX - djui_hud_measure_text(TEXT_NAME)*0.3, 55, 0.6) + djui_hud_set_font(FONT_TINY) + djui_hud_print_text(TEXT_CREDIT, width - textX - djui_hud_measure_text(TEXT_CREDIT)*0.3, 72, 0.6) + djui_hud_set_font(FONT_NORMAL) + djui_hud_print_text(TEXT_DESCRIPTION, width - textX - djui_hud_measure_text(TEXT_DESCRIPTION)*0.2, 85, 0.4) + for i = 1, #TEXT_DESCRIPTION_TABLE do + djui_hud_print_text(TEXT_DESCRIPTION_TABLE[i], width - textX - djui_hud_measure_text(TEXT_DESCRIPTION_TABLE[i])*0.15, 90 + i*9, 0.3) + end + djui_hud_print_text(TEXT_PREF, width - textX - djui_hud_measure_text(TEXT_PREF)*0.15, height - 20, 0.3) + djui_hud_print_text(TEXT_PREF_SAVE, width - textX - djui_hud_measure_text(TEXT_PREF_SAVE)*0.15, height - 30, 0.3) + + --Character Select Header + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + djui_hud_render_rect(0, 0, width, 50) + djui_hud_set_color(0, 0, 0, 255) + djui_hud_render_rect(2, 2, width - 4, 46) + djui_hud_set_color(menuColor.r * 0.5 + 127, menuColor.g * 0.5 + 127, menuColor.b * 0.5 + 127, 255) + djui_hud_render_texture(TEX_HEADER, widthHalf - 128, 10, 1, 1) + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + djui_hud_set_font(FONT_TINY) + djui_hud_print_text("Version: "..modVersion, 5, 3, 0.5) + --Unsupported Res Warning + if width < 321.2 or width > 575 then + djui_hud_print_text(TEXT_RATIO_UNSUPPORTED, 5, 39, 0.5) + end + + --Options display + if options or optionAnimTimer > optionAnimTimerCap then + djui_hud_set_color(menuColor.r * 0.25, menuColor.g * 0.25, menuColor.b * 0.25, 205 + math_max(-200, optionAnimTimer)) + djui_hud_render_rect(0, 0, width, height) + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + djui_hud_render_rect(width*0.5 - 50 * widthScale, math.min(55 - optionAnimTimer, height - 25 * widthScale), 100 * widthScale, 200) + djui_hud_set_color(0, 0, 0, 255) + djui_hud_render_rect(width*0.5 - 50 * widthScale + 2, math.min(55 - optionAnimTimer + 2, height - 25 * widthScale + 2), 100 * widthScale - 4, 196) + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + djui_hud_render_rect(width*0.5 - 50 * widthScale, height - 2, 100 * widthScale, 2) + djui_hud_set_font(FONT_NORMAL) + djui_hud_set_color(menuColor.r * 0.5 + 127, menuColor.g * 0.5 + 127, menuColor.b * 0.5 + 127, 255) + djui_hud_print_text(TEXT_OPTIONS_HEADER, widthHalf - djui_hud_measure_text(TEXT_OPTIONS_HEADER)*0.3*widthScale, 65 + optionAnimTimer * -1, 0.6*widthScale) + + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + for i = 1, #optionTable do + local toggleName = optionTable[i].name + local scale = 0.5 + local yOffset = 70 + 10 * math_min(widthScale, 1.8) + i * 9 * math_min(widthScale, 1.8) - optionAnimTimer + if i == currOption then + djui_hud_set_font(FONT_NORMAL) + scale = 0.3 + yOffset = yOffset - 1 + if optionTable[i].toggleNames[optionTable[i].toggle + 1] ~= "" then + toggleName = "> " .. toggleName .. " - " .. optionTable[i].toggleNames[optionTable[i].toggle + 1] + else + toggleName = "> " .. toggleName + end + else + djui_hud_set_font(FONT_TINY) + end + scale = scale * math_min(widthScale, 1.8) + djui_hud_print_text(toggleName, widthHalf - djui_hud_measure_text(toggleName)*scale*0.5, yOffset, scale) + end + + djui_hud_set_font(FONT_TINY) + djui_hud_print_text(TEXT_OPTIONS_SELECT, widthHalf - djui_hud_measure_text(TEXT_OPTIONS_SELECT)*0.3, height - 20 - optionAnimTimer, 0.6) + else + -- How to open options display + local widthScaleUnlimited = widthScale + local widthScale = math_min(widthScale, 1.42) + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + djui_hud_render_rect(widthHalf - 50 * widthScaleUnlimited, height - 25 * widthScale, 100 * widthScaleUnlimited, 26 * widthScale) + djui_hud_set_color(0, 0, 0, 255) + djui_hud_render_rect(widthHalf - 50 * widthScaleUnlimited + 2, height - 25 * widthScale + 2, 100 * widthScaleUnlimited - 4, 22 * widthScale) + djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255) + djui_hud_render_rect(widthHalf - 50 * widthScaleUnlimited, height - 2, 100 * widthScaleUnlimited, 2) + djui_hud_set_font(FONT_NORMAL) + djui_hud_print_text(TEXT_OPTIONS_OPEN, widthHalf - djui_hud_measure_text(TEXT_OPTIONS_OPEN)*0.175 * widthScale, height - 23 * widthScale + optionAnimTimer + 202, 0.35 * widthScale) + djui_hud_set_font(FONT_TINY) + djui_hud_print_text(TEXT_MENU_CLOSE, widthHalf - djui_hud_measure_text(TEXT_MENU_CLOSE)*0.25 * widthScale, height - 13 * widthScale + optionAnimTimer + 202, 0.5 * widthScale) + end + + -- Anim logic + if options then + if optionTable[optionTableRef.anims].toggle > 0 then + if optionAnimTimer < -1 then + optionAnimTimer = optionAnimTimer/1.1 + end + else + optionAnimTimer = -1 + end + else + if optionTable[optionTableRef.anims].toggle > 0 then + if optionAnimTimer > optionAnimTimerCap then + optionAnimTimer = optionAnimTimer*1.2 + end + else + optionAnimTimer = optionAnimTimerCap + end + end + optionAnimTimer = math_max(optionAnimTimer, -200) + else + options = false + optionAnimTimer = optionAnimTimerCap + end + + -- Info / Z Open Bind on Pause Menu + if is_game_paused() and not djui_hud_is_pause_menu_created() and gMarioStates[0].action ~= ACT_EXIT_LAND_SAVE_DIALOG then + local currCharY = 0 + djui_hud_set_resolution(RESOLUTION_DJUI) + if optionTable[optionTableRef.openInputs].toggle == 2 then + currCharY = 27 + local width = djui_hud_get_screen_width() - djui_hud_measure_text(TEXT_PAUSE_Z_OPEN) + djui_hud_set_font(FONT_NORMAL) + djui_hud_set_color(0, 0, 0, 255) + djui_hud_print_text(TEXT_PAUSE_Z_OPEN, width - 19, 17, 1) + djui_hud_set_color(255, 255, 255, 255) + djui_hud_print_text(TEXT_PAUSE_Z_OPEN, width - 20, 16, 1) + end + + if optionTable[optionTableRef.localModels].toggle == 1 then + local charName = string_underscore_to_space(characterTable[currChar].name) + local TEXT_PAUSE_CURR_CHAR_WITH_NAME = TEXT_PAUSE_CURR_CHAR..charName + local width = djui_hud_get_screen_width() - djui_hud_measure_text(TEXT_PAUSE_CURR_CHAR_WITH_NAME) + local charColor = characterTable[currChar].color + djui_hud_set_font(FONT_NORMAL) + djui_hud_set_color(0, 0, 0, 255) + djui_hud_print_text(TEXT_PAUSE_CURR_CHAR_WITH_NAME, width - 19, 17 + currCharY, 1) + djui_hud_set_color(255, 255, 255, 255) + djui_hud_print_text(TEXT_PAUSE_CURR_CHAR, width - 20, 16 + currCharY, 1) + djui_hud_set_color(charColor.r, charColor.g, charColor.b, 255) + djui_hud_print_text(charName, djui_hud_get_screen_width() - djui_hud_measure_text(charName) - 20, 16 + currCharY, 1) + else + local width = djui_hud_get_screen_width() - djui_hud_measure_text(TEXT_LOCAL_MODEL_OFF) + djui_hud_set_font(FONT_NORMAL) + djui_hud_set_color(0, 0, 0, 255) + djui_hud_print_text(TEXT_LOCAL_MODEL_OFF, width - 19, 17 + currCharY, 1) + djui_hud_set_color(255, 255, 255, 255) + djui_hud_print_text(TEXT_LOCAL_MODEL_OFF, width - 20, 16 + currCharY, 1) + end + end +end + +-- Custom life icon rendering (Thanks LuigiGamer) +function on_life_counter_render() + if obj_get_first_with_behavior_id(id_bhvActSelector) ~= nil then return end + -- Rendering settings -- + djui_hud_set_font(FONT_HUD) + djui_hud_set_resolution(RESOLUTION_N64) + djui_hud_set_color(255, 255, 255, 255); + + -- Texture scale -- + + -- Texture position -- + local x = 22 + local y = 15 + + -- Texture Rendering -- + if gNetworkPlayers[0].currActNum == 99 then return end + if not hud_is_hidden() then + local icon = characterTable[currChar].lifeIcon + djui_hud_render_texture(icon, x, y, 1/(icon.width/16), 1/(icon.height/16)) + djui_hud_print_text("@", x + 16, y, 1) + djui_hud_print_text(tostring(gMarioStates[0].numLives), x + 32, y, 1) + hud_set_value(HUD_DISPLAY_FLAGS, hud_get_value(HUD_DISPLAY_FLAGS) &~ HUD_DISPLAY_FLAG_LIVES) -- Hides the lives counter + else + hud_set_value(HUD_DISPLAY_FLAGS, hud_get_value(HUD_DISPLAY_FLAGS) | HUD_DISPLAY_FLAG_LIVES) -- Shows the lives counter, use this when you're no longer using a custom character + end +end + +local function before_mario_update(m) + if m.playerIndex ~= 0 then return end + if inputStallTimer > 0 then inputStallTimer = inputStallTimer - 1 end + + if menu and inputStallTo ~= latencyValueTable[optionTable[optionTableRef.inputLatency].toggle + 1] then + inputStallTo = latencyValueTable[optionTable[optionTableRef.inputLatency].toggle + 1] + end + + -- Menu Inputs + if not menu and (m.controller.buttonDown & D_JPAD) ~= 0 and m.action ~= ACT_EXIT_LAND_SAVE_DIALOG and optionTable[optionTableRef.openInputs].toggle == 1 then + if ommActive then + if (m.controller.buttonDown & R_TRIG) ~= 0 then + menu = true + end + else + menu = true + end + inputStallTimer = inputStallTo + end + if is_game_paused() and m.action ~= ACT_EXIT_LAND_SAVE_DIALOG and (m.controller.buttonPressed & Z_TRIG) ~= 0 and optionTable[optionTableRef.openInputs].toggle == 2 then + menu = true + end + + local cameraToObject = gMarioStates[0].marioObj.header.gfx.cameraToObject + + -- C-up Failsafe (Camera Softlocks) + if m.action == ACT_FIRST_PERSON or (m.prevAction == ACT_FIRST_PERSON and is_game_paused()) then + menu = false + elseif m.prevAction == ACT_FIRST_PERSON and not is_game_paused() then + m.prevAction = ACT_WALKING + end + + if gNetworkPlayers[0].currActNum == 99 then + menu = false + end + + if menu and not options then + if inputStallTimer == 0 then + if (m.controller.buttonPressed & D_JPAD) ~= 0 then + currChar = currChar + 1 + inputStallTimer = inputStallTo + buttonScroll = buttonScrollCap + play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject) + end + if (m.controller.buttonPressed & U_JPAD) ~= 0 then + currChar = currChar - 1 + inputStallTimer = inputStallTo + buttonScroll = -buttonScrollCap + play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject) + end + if (m.controller.buttonPressed & D_CBUTTONS) ~= 0 then + currChar = currChar + 1 + inputStallTimer = inputStallTo*0.6 + buttonScroll = buttonScrollCap + play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject) + end + if (m.controller.buttonPressed & U_CBUTTONS) ~= 0 then + currChar = currChar - 1 + inputStallTimer = inputStallTo*0.6 + buttonScroll = -buttonScrollCap + play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject) + end + if m.controller.stickY < -60 then + currChar = currChar + 1 + inputStallTimer = inputStallTo + buttonScroll = buttonScrollCap + play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject) + end + if m.controller.stickY > 60 then + currChar = currChar - 1 + inputStallTimer = inputStallTo + buttonScroll = -buttonScrollCap + play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject) + end + if (m.controller.buttonPressed & A_BUTTON) ~= 0 then + if characterTable[currChar] ~= nil then + TEXT_PREF_LOAD = characterTable[currChar].name + mod_storage_save("PrefChar", TEXT_PREF_LOAD) + inputStallTimer = inputStallTo + play_sound(SOUND_MENU_CLICK_FILE_SELECT, cameraToObject) + else + play_sound(SOUND_MENU_CAMERA_BUZZ, cameraToObject) + end + end + if (m.controller.buttonPressed & B_BUTTON) ~= 0 then + menu = false + end + if (m.controller.buttonPressed & START_BUTTON) ~= 0 then + options = true + end + end + if currChar > #characterTable then currChar = 1 end + if currChar < 1 then currChar = #characterTable end + nullify_inputs(m) + if is_game_paused() then + m.controller.buttonPressed = START_BUTTON + end + + -- Idiot Proof Check + if not hasOpenedMenu then + mod_storage_save("openedmenu", "youdidit") + hasOpenedMenu = true + end + end + + if options then + if inputStallTimer == 0 then + if (m.controller.buttonPressed & D_JPAD) ~= 0 then + currOption = currOption + 1 + inputStallTimer = inputStallTo + play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject) + end + if (m.controller.buttonPressed & U_JPAD) ~= 0 then + currOption = currOption - 1 + inputStallTimer = inputStallTo + play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject) + end + if m.controller.stickY < -60 then + currOption = currOption + 1 + inputStallTimer = inputStallTo + play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject) + end + if m.controller.stickY > 60 then + currOption = currOption - 1 + inputStallTimer = inputStallTo + play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject) + end + if (m.controller.buttonPressed & A_BUTTON) ~= 0 then + optionTable[currOption].toggle = optionTable[currOption].toggle + 1 + if optionTable[currOption].toggle > optionTable[currOption].toggleMax then optionTable[currOption].toggle = 0 end + if optionTable[currOption].toggleSaveName ~= nil then + mod_storage_save(optionTable[currOption].toggleSaveName, tostring(optionTable[currOption].toggle)) + end + inputStallTimer = inputStallTo + play_sound(SOUND_MENU_CHANGE_SELECT, cameraToObject) + end + if (m.controller.buttonPressed & B_BUTTON) ~= 0 then + options = false + inputStallTimer = inputStallTo + end + end + if currOption > #optionTable then currOption = 1 end + if currOption < 1 then currOption = #optionTable end + nullify_inputs(m) + end +end + +hook_event(HOOK_BEFORE_MARIO_UPDATE, before_mario_update) +hook_event(HOOK_ON_HUD_RENDER, on_hud_render) +hook_event(HOOK_ON_HUD_RENDER_BEHIND, on_life_counter_render) + +-------------- +-- Commands -- +-------------- + +local function chat_command(msg) + if msg ~= "" then + msg = string_lower(msg) + for i = 1, #characterTable do + if msg == string_underscore_to_space(string_lower(characterTable[i].name)) then + currChar = i + return true + end + end + djui_chat_message_create("Character Not Found") + return true + else + menu = not menu + return true + end +end + +hook_chat_command("char-select", "- Opens the Character Select Menu", chat_command) \ No newline at end of file diff --git a/mods/character-select-coop/o-api.lua b/mods/character-select-coop/o-api.lua new file mode 100644 index 000000000..46a3e98c5 --- /dev/null +++ b/mods/character-select-coop/o-api.lua @@ -0,0 +1,147 @@ + + +local characterVoices = {} + +local TEX_UNKNOWN_CHAR = get_texture_info("unknown-icon") + +local E_MODEL_ARMATURE = smlua_model_util_get_id("armature_geo") + +local table_insert = table.insert +local type = type + +--------- +-- API -- +--------- + +---@param name string|nil Underscores turn into Spaces +---@param description table|nil {"string"} +---@param credit string|nil +---@param color Color|nil {r, g, b} +---@param modelInfo ModelExtendedId|table|nil Use smlua_model_util_get_id() +---@param forceChar CharacterType|nil CT_MARIO, CT_LUIGI, CT_TOAD, CT_WALUIGI, CT_WARIO +---@param lifeIcon TextureInfo|nil Use get_texture_info() +---@return integer +local character_add = function(name, description, credit, color, modelInfo, forceChar, lifeIcon) + table_insert(characterTable, { + name = name and string_space_to_underscore(name) or "Untitled", + description = description and description or {"No description has been provided"}, + credit = credit and credit or "Unknown", + color = color and color or menuColorTable[8], + model = modelInfo and (type(modelInfo) == "table" and modelInfo[1] or modelInfo) or E_MODEL_ARMATURE, + capModels = type(modelInfo) == "table" and modelInfo[2] or nil, + forceChar = forceChar and forceChar or CT_MARIO, + lifeIcon = lifeIcon and lifeIcon or TEX_UNKNOWN_CHAR, + }) + return #characterTable +end + +---@param charNum integer Use _G.charSelect.character_get_number_from_string() or _G.charSelect.character_add()'s return value +---@param name string|nil Underscores turn into Spaces +---@param description table|nil {"string"} +---@param credit string|nil +---@param color Color|nil {r, g, b} +---@param modelInfo ModelExtendedId|integer|table|nil Use smlua_model_util_get_id() +---@param forceChar CharacterType|nil CT_MARIO, CT_LUIGI, CT_TOAD, CT_WALUIGI, CT_WARIO +---@param lifeIcon TextureInfo|nil Use get_texture_info() +local character_edit = function(charNum, name, description, credit, color, modelInfo, forceChar, lifeIcon) + characterTable[charNum] = characterTable[charNum] and { + name = name and string_space_to_underscore(name) or characterTable[charNum].name, + description = description and description or characterTable[charNum].description, + credit = credit and credit or characterTable[charNum].credit, + color = color and color or characterTable[charNum].color, + model = modelInfo and (type(modelInfo) == "table" and modelInfo[1] or modelInfo) or characterTable[charNum].model, + capModels = type(modelInfo) == "table" and modelInfo[2] or characterTable[charNum].capModels, + forceChar = forceChar and forceChar or characterTable[charNum].forceChar, + lifeIcon = lifeIcon and lifeIcon or characterTable[charNum].lifeIcon, + } or nil +end + +---@param modelInfo ModelExtendedId|integer +---@param clips table +local character_add_voice = function(modelInfo, clips) + characterVoices[modelInfo] = clips +end + +---@return table +local character_get_current_table = function () + return characterTable[currChar] +end + +local character_get_current_model_number = function () + return currChar +end + +---@param name string +local character_get_number_from_string = function (name) + for i = 2, #characterTable do + if characterTable[i].name == name or characterTable[i].name == string_space_to_underscore(name) then + return i + end + end + return false +end + +---@param m MarioState +local character_get_voice = function (m) + return characterVoices[gPlayerSyncTable[m.playerIndex].modelId] +end + +local version_get = function () + return modVersion +end + +local is_menu_open = function () + return menu +end + +local hook_allow_menu_open = function (func) + table.insert(allowMenu, func) +end + +local is_options_open = function () + return options +end + +local optionTableRef = { + openInputs = 1, + menuColor = 2, + anims = 3, + inputLatency = 4, + localModels = 5, + prefToDefault = 6, +} + +local controller = { + buttonDown = 0, + buttonPressed = 0, + extStickX = 0, + extStickY = 0, + rawStickX = 0, + rawStickY = 0, + stickMag = 0, + stickX = 0, + stickY = 0 +} + +---@param tableNum integer +local get_status = function (tableNum) + return optionTable[tableNum].toggle +end + +_G.charSelectExists = true -- Ace +_G.charSelect = { + character_add = character_add, + character_edit = character_edit, + character_add_voice = character_add_voice, + character_get_current_table = character_get_current_table, + character_get_current_model_number = character_get_current_model_number, + character_get_number_from_string = character_get_number_from_string, + character_get_voice = character_get_voice, + version_get = version_get, + is_menu_open = is_menu_open, + hook_allow_menu_open = hook_allow_menu_open, + is_options_open = is_options_open, + get_status = get_status, + optionTableRef = optionTableRef, + controller = controller, +} diff --git a/mods/character-select-coop/textures/char-select-text.png b/mods/character-select-coop/textures/char-select-text.png new file mode 100644 index 000000000..2f56001a8 Binary files /dev/null and b/mods/character-select-coop/textures/char-select-text.png differ diff --git a/mods/character-select-coop/textures/char-select-text.tex b/mods/character-select-coop/textures/char-select-text.tex new file mode 100644 index 000000000..7e4099310 Binary files /dev/null and b/mods/character-select-coop/textures/char-select-text.tex differ diff --git a/mods/character-select-coop/textures/unknown-icon.png b/mods/character-select-coop/textures/unknown-icon.png new file mode 100644 index 000000000..627624b05 Binary files /dev/null and b/mods/character-select-coop/textures/unknown-icon.png differ diff --git a/mods/character-select-coop/textures/unknown-icon.tex b/mods/character-select-coop/textures/unknown-icon.tex new file mode 100644 index 000000000..4a2c1483a Binary files /dev/null and b/mods/character-select-coop/textures/unknown-icon.tex differ diff --git a/mods/character-select-coop/voice.lua b/mods/character-select-coop/voice.lua new file mode 100644 index 000000000..558605d87 --- /dev/null +++ b/mods/character-select-coop/voice.lua @@ -0,0 +1,151 @@ +for i = 0, MAX_PLAYERS -1, 1 do + gPlayerSyncTable[i].customVoice = 0 +end + +local voicecount = 0 + +local SLEEP_TALK_SNORES = 8 + +gCustomVoiceSamples = {} +gCustomVoiceSamplesBackup = {} +gCustomVoiceStream = nil + +--- @param m MarioState +function stop_custom_character_sound(m, sound) + local voice_sample = gCustomVoiceSamples[m.playerIndex] + if voice_sample == false and gCustomVoiceSamplesBackup[m.playerIndex].loaded then + audio_sample_stop(gCustomVoiceSamplesBackup[m.playerIndex]) + audio_sample_destroy(gCustomVoiceSamplesBackup[m.playerIndex]) + voicecount = voicecount - 1 + gCustomVoiceSamplesBackup[m.playerIndex] = nil + return + end + if voice_sample == nil or type(voice_sample) == "boolean" then + return + end + if not voice_sample.loaded then + gCustomVoiceSamplesBackup[m.playerIndex] = true + return + end + + audio_sample_stop(voice_sample) + if voice_sample.file.relativePath:match('^.+/(.+)$') == sound then + return voice_sample + end + audio_sample_destroy(voice_sample) + voicecount = voicecount - 1 +end + +--- @param m MarioState +function play_custom_character_sound(m, voice) + local sound + if type(voice) == "table" then + sound = voice[math.random(#voice)] + else + sound = voice + end + if sound == nil then return 0 end + + --Get current sample and stop it + local voice_sample = stop_custom_character_sound(m, sound) + + if type(sound) ~= "string" then + return sound + end + + if (m.area == nil or m.area.camera == nil) and m.playerIndex == 0 then + if gCustomVoiceStream ~= nil then + audio_stream_stop(gCustomVoiceStream) + audio_stream_destroy(gCustomVoiceStream) + end + gCustomVoiceStream = audio_stream_load(sound) + audio_stream_play(gCustomVoiceStream, true, 1) + else + if voice_sample == nil then + voice_sample = audio_sample_load(sound) + while not voice_sample.loaded do end + voicecount = voicecount + 1 + end + audio_sample_play(voice_sample, m.pos, 1) + + if gCustomVoiceSamplesBackup[m.playerIndex] ~= nil and not(gCustomVoiceSamples[m.playerIndex] == false) then + gCustomVoiceSamplesBackup[m.playerIndex] = voice_sample + else + gCustomVoiceSamples[m.playerIndex] = voice_sample + end + end + return 0 +end + +--- @param m MarioState +local function custom_character_sound(m, characterSound) + if is_game_paused() then return end + if characterSound == CHAR_SOUND_SNORING3 then return 0 end + if characterSound == CHAR_SOUND_HAHA and m.hurtCounter > 0 then return 0 end + + local voice = _G.charSelect.character_get_voice(m)[characterSound] + if voice ~= nil then + return play_custom_character_sound(m, voice) + end + return 0 +end + +local STARTING_SNORE = 46 +local SLEEP_TALK_START = STARTING_SNORE + 49 +local SLEEP_TALK_END = SLEEP_TALK_START + SLEEP_TALK_SNORES + +--- @param m MarioState +local function custom_character_snore(m) + if is_game_paused() then return end + if gCustomVoiceSamplesBackup[m.playerIndex] ~= nil and not (gCustomVoiceSamples[m.playerIndex] == false) then + if gCustomVoiceSamples[m.playerIndex].loaded then + audio_sample_destroy(gCustomVoiceSamples[m.playerIndex]) + voicecount = voicecount - 1 + gCustomVoiceSamples[m.playerIndex] = false + end + end + local SNORE3_TABLE = _G.charSelect.character_get_voice(m)[CHAR_SOUND_SNORING3] + + if m.action ~= ACT_SLEEPING then + if m.isSnoring > 0 then + stop_custom_character_sound(m) + end + return + + elseif not (m.actionState == 2 and (m.flags & MARIO_MARIO_SOUND_PLAYED) ~= 0) then + return + end + + local animFrame = m.marioObj.header.gfx.animInfo.animFrame + + if SNORE3_TABLE ~= nil and #SNORE3_TABLE >= 2 then + if animFrame == 2 and m.actionTimer < SLEEP_TALK_START then + play_custom_character_sound(m, SNORE3_TABLE[2]) + elseif animFrame == 25 then + if #SNORE3_TABLE >= 3 then + m.actionTimer = m.actionTimer + 1 + if m.actionTimer >= SLEEP_TALK_END then + m.actionTimer = STARTING_SNORE + end + if m.actionTimer == SLEEP_TALK_START then + play_custom_character_sound(m, SNORE3_TABLE[3]) + elseif m.actionTimer < SLEEP_TALK_START then + play_custom_character_sound(m, SNORE3_TABLE[1]) + end + else + play_custom_character_sound(m, SNORE3_TABLE[1]) + end + end + elseif animFrame == 2 then + play_character_sound(m, CHAR_SOUND_SNORING2) + + elseif animFrame == 25 then + play_character_sound(m, CHAR_SOUND_SNORING1) + end +end + +_G.charSelect.voice = { + sound = custom_character_sound, + snore = custom_character_snore, +} +gPlayerSyncTable[0].customVoice = true \ No newline at end of file