-- name: Progress Painting Overlays -- description: Shows stars, keys, and cap switches in a course. Also (mostly) compatible with rom hacks!\n\nMod by EmilyEmmi. -- pausable: true -- this reduces lag apparently local djui_hud_set_color, get_texture_info, djui_hud_render_texture_interpolated, djui_hud_set_resolution, clamp, network_get_player_text_color_string, obj_has_model_extended = djui_hud_set_color, get_texture_info, djui_hud_render_texture_interpolated, djui_hud_set_resolution, clamp, network_get_player_text_color_string, obj_has_model_extended -- get rom hack + minimap mod local romhack_file = "vanilla" local showMiniMap = false for i, mod in pairs(gActiveMods) do if mod.enabled and mod.relativePath then if mod.incompatible and string.find(mod.incompatible, "romhack") then romhack_file = mod.relativePath:gsub("ROMHACK - ", "") elseif mod.relativePath == "Minimap" then showMiniMap = true end end end -- translation (MarioHunt version is used if it exists) function trans(thing) local lang = smlua_text_utils_get_language() if lang == "English" or (thing ~= "player" and thing ~= "players") then thing = thing:sub(1,1):upper() .. thing:sub(2) return thing end local trans_table = { ["Czech"] = { player = "Hráč", players = "Hráči", }, ["Dutch"] = { player = "Speler", players = "Spelers", }, ["French"] = { player = "Joueur", players = "Joueurs", }, ["German"] = { player = "Spieler", players = "Spieler", }, ["Italian"] = { player = "Giocatore", players = "Giocatori", }, ["Polish"] = { player = "Gracz", players = "Gracze", }, ["Portuguese"] = { player = "Jogador", players = "Jogadores", }, ["Russian"] = { player = "Игрок", players = "Игроки", }, ["Spanish"] = { player = "Jugador", players = "Jugadores", }, } if trans_table[lang][thing] then return trans_table[lang][thing] else thing = thing:sub(1,1):upper() .. thing:sub(2) return thing end end -- mh support if mhExists then if mhVersionNum then -- already implemented, don't load this mod return else trans = _G.mhApi.trans showMiniMap = true end end -- minimap TEX_HUD_BOX = get_texture_info('exclamation_box_seg8_texture_08017628') TEX_HUD_KEY_LEFT = get_texture_info('bowser_key_left_texture') TEX_HUD_KEY_RIGHT = get_texture_info('bowser_key_right_texture') local warpObjs = { id_bhvWarp, id_bhvWarpPipe, id_bhvDoorWarp, id_bhvFadingWarp, id_bhvWarp, id_bhvBooCage } local progressRadar = {} local progressMinimap = {} local frameCounter = 0 local overlayDisabled = false function painting_overlays() if overlayDisabled then return end if mhExists and mhApi.isMenuOpen() then return end if is_game_paused() then return end djui_hud_set_resolution(RESOLUTION_N64) djui_hud_set_font(FONT_NORMAL) djui_hud_set_color(255, 255, 255, 255) local m = gMarioStates[0] local np = gNetworkPlayers[0] local doneCourses = { [np.currCourseNum] = 1 } local warpList = {} frameCounter = frameCounter + 1 if frameCounter >= 60 then frameCounter = 0 end -- we know that vanilla (and lm64) only have warps in the castle and HMC if (romhack_file == "vanilla" or romhack_file == "luigis-mansion-64") and np.currCourseNum ~= 0 and np.currCourseNum ~= COURSE_HMC then return end -- To get the paintings, we use this get_painting_warp_node() -- Since it requires that mario be above the painting, we set mario's floor type and floorheight temporarily if (romhack_file == "vanilla" or romhack_file == "luigis-mansion-64" or romhack_file == "B3313") and np.currLevelNum == LEVEL_CASTLE then local paintingValueTable = { gPaintingValues.bob_painting, gPaintingValues.ccm_painting, gPaintingValues.wf_painting, gPaintingValues.jrb_painting, gPaintingValues.lll_painting, gPaintingValues.ssl_painting, gPaintingValues.ttm_slide_painting, gPaintingValues.ddd_painting, gPaintingValues.wdw_painting, gPaintingValues.thi_tiny_painting, gPaintingValues.ttm_painting, gPaintingValues.ttc_painting, gPaintingValues.sl_painting, gPaintingValues.thi_huge_painting, gPaintingValues.hmc_painting, } local donePaintings = {} local oldFloorType = m.floor.type local oldFloorHeight = m.floorHeight m.floorHeight = m.pos.y for i = 0, 14 do m.floor.type = i * 3 + SURFACE_PAINTING_WARP_D3 local warpNode = get_painting_warp_node() if warpNode and warpNode.destLevel ~= 0 and warpNode.destLevel ~= 164 then -- for some reason, ttm mountain slide uses 164 for the level? wtf local course = level_to_course[warpNode.destLevel] or 0 if not (doneCourses[course] and donePaintings[i]) then doneCourses[course] = 1 donePaintings[i] = 1 -- so thi huge is seperate local level = warpNode.destLevel local area = warpNode.destArea if i == 14 and np.currAreaIndex ~= 3 then -- rainbow ride HAD to be different (it uses hmc painting even though it's nowhere near the warp. Use hardcoded position) local pos = { -3400, 3116, 5886 } table.insert(warpList, { course, level, area, pos }) else local painting = paintingValueTable[i + 1] if painting then local pos = { painting.posX, painting.posY, painting.posZ } local yawRadians = painting.yaw * math.pi / 180 pos[1] = math.floor(pos[1] + math.cos(yawRadians) * painting.size * 0.5) pos[2] = math.floor(pos[2] + painting.size) pos[3] = math.floor(pos[3] - math.sin(yawRadians) * painting.size * 0.5) if painting.pitch ~= 0 then -- applies only to hmc local pitchRadians = painting.pitch * math.pi / 180 pos[1] = pos[1] + math.cos(pitchRadians) * painting.size * 0.5 pos[2] = pos[2] + painting.size * (math.cos(pitchRadians) - 1) + 100 pos[3] = pos[3] + math.sin(pitchRadians) * painting.size * 0.5 end table.insert(warpList, { course, level, area, pos }) end end end end end m.floorHeight = oldFloorHeight m.floor.type = oldFloorType end -- check for wing cap warp if m.numStars >= gLevelValues.wingCapLookUpReq then local pos = { -1024, 150, 717 } local valid = true if romhack_file == "vanilla" or romhack_file == "luigis-mansion-64" then valid = (np.currLevelNum == LEVEL_CASTLE and np.currAreaIndex == 1) -- hard-coded pos else -- we don't know where the wing cap warp is; only display if mario is on the surface if m.floor.type == SURFACE_LOOK_UP_WARP then pos = { m.pos.x, m.floorHeight + 300, m.pos.z } else valid = false end end if valid then local objWarpNode = area_get_warp_node(0xF2) local warpNode = objWarpNode and objWarpNode.node if warpNode and warpNode.destLevel ~= 0 and warpNode.destLevel ~= 164 then -- for some reason, ttm mountain slide uses 164 for the level? wtf local course = level_to_course[warpNode.destLevel] or 0 if not doneCourses[course] then doneCourses[course] = 1 local level = warpNode.destLevel local area = warpNode.destArea table.insert(warpList, { course, level, area, pos }) end end end end -- iterate through all warps now for i, id in ipairs(warpObjs) do local o = obj_get_first_with_behavior_id(id) while o do local nodeID = (o.oBehParams & 0x00FF0000) >> 16 local objWarpNode = area_get_warp_node(nodeID) local warpNode = objWarpNode and objWarpNode.node if warpNode and warpNode.destLevel ~= 0 and warpNode.destLevel ~= 164 then -- for some reason, ttm mountain slide uses 164 for the level? wtf local course = level_to_course[warpNode.destLevel] or 0 if not doneCourses[course] then doneCourses[course] = 1 local level = warpNode.destLevel local area = warpNode.destArea local pos = { math.floor(o.oPosX), math.floor(o.oPosY + 300), math.floor(o.oPosZ) } table.insert(warpList, { course, level, area, pos }) end end o = obj_get_next_with_same_behavior_id(o) end end --djui_chat_message_create("!!!!!") for i, warpData in ipairs(warpList) do local course = warpData[1] local level = warpData[2] local pos = { x = warpData[4][1], y = warpData[4][2], z = warpData[4][3] } local totalStars = 7 local file = 0 local starFlags = 0 local exThingy = 0 local haveExThingy = false local playerCountHere, isHunter = get_player_count(course, level, warpData[3]) if level == LEVEL_BOWSER_1 or level == LEVEL_BOWSER_2 or level == LEVEL_BOWSER_3 then totalStars = 0 elseif course == 0 then totalStars = 5 elseif course == COURSE_PSS then totalStars = 2 elseif course > 15 then totalStars = 1 end file = get_current_save_file_num() - 1 starFlags = save_file_get_star_flags(file, course - 1) while totalStars < 7 and starFlags >= (1 << totalStars) do totalStars = totalStars + 1 end if (romhack_file == "moonshine" or romhack_file == "B3313") and level ~= LEVEL_BOWSER_1 and level ~= LEVEL_BOWSER_2 and level ~= LEVEL_BOWSER_3 then -- nothing elseif course == COURSE_BITDW then exThingy = 1 if save_file_get_flags() & (SAVE_FLAG_HAVE_KEY_1 | SAVE_FLAG_UNLOCKED_BASEMENT_DOOR) ~= 0 then haveExThingy = true end elseif course == COURSE_BITFS then exThingy = 1 if save_file_get_flags() & (SAVE_FLAG_HAVE_KEY_2 | SAVE_FLAG_UNLOCKED_UPSTAIRS_DOOR) ~= 0 then haveExThingy = true end elseif course == COURSE_BITS and mhExists then exThingy = 1 -- not a key technically but whatever if mhApi.getGlobalField("bowserBeaten") then haveExThingy = true end elseif course == COURSE_TOTWC then exThingy = 2 if save_file_get_flags() & (SAVE_FLAG_HAVE_WING_CAP) ~= 0 then haveExThingy = true end elseif course == COURSE_VCUTM then exThingy = 3 if save_file_get_flags() & (SAVE_FLAG_HAVE_VANISH_CAP) ~= 0 then haveExThingy = true end elseif course == COURSE_COTMC then exThingy = 4 if save_file_get_flags() & (SAVE_FLAG_HAVE_METAL_CAP) ~= 0 then haveExThingy = true end end -- minimap render if showMiniMap then local levelSize = 8192 local renderSize = 80 local screenWidth = djui_hud_get_screen_width() local screenHeight = djui_hud_get_screen_height() local x = screenWidth - 90 local y = screenHeight - 120 local tex = gTextures.star local isStar = true if isStar and exThingy ~= 0 and (not haveExThingy) and frameCounter % 60 >= 30 then isStar = false end if pos.x > levelSize + 5 or pos.x < -levelSize - 5 or pos.z > levelSize + 5 or pos.z < -levelSize - 5 then -- adjust size if oob levelSize = levelSize * 2 end local xScaled = (pos.x / (levelSize * 2) + 0.5) local yScaled = (pos.z / (levelSize * 2) + 0.5) local scale = 0.5 local renderX = x + xScaled * renderSize - 8 * scale local renderY = y + yScaled * renderSize - 8 * scale local starsLeft = 0 local mapData = progressMinimap[i] if not mapData then progressMinimap[i] = { prevX = x, prevY = y } mapData = progressMinimap[i] end if isStar then local collectedStars = 0 for i = 1, totalStars do if (starFlags & (1 << (i - 1)) ~= 0) then collectedStars = collectedStars + 1 if collectedStars == totalStars then break end end end starsLeft = totalStars - collectedStars if starsLeft == 0 then isStar = false tex = nil end end if exThingy ~= 0 and not (isStar or haveExThingy) then if exThingy == 1 then tex = TEX_HUD_KEY_LEFT scale = scale / 4 elseif exThingy == 2 then tex = get_texture_info("exclamation_box_seg8_texture_08015E28") scale = scale / 2 elseif exThingy == 3 then tex = get_texture_info("exclamation_box_seg8_texture_08012E28") scale = scale / 2 else tex = get_texture_info("exclamation_box_seg8_texture_08014628") scale = scale / 2 end end if tex then local adjustScale = scale if isStar and OmmEnabled then local starColor = 0 if omm_star_colors[course] and OmmApi.omm_get_setting(gMarioStates[0], "color") ~= 0 then starColor = course end tex = get_texture_info("omm_tex_hud_star_" .. starColor) adjustScale = adjustScale / tex.width * 16 djui_hud_set_color(255, 255, 255, 255) elseif isStar and gGlobalSyncTable.ee then djui_hud_set_color(255, 0, 0, 255) else djui_hud_set_color(255, 255, 255, 255) end djui_hud_render_texture_interpolated(tex, mapData.prevX, mapData.prevY, adjustScale, adjustScale, renderX, renderY, adjustScale, adjustScale) if tex == TEX_HUD_KEY_LEFT then djui_hud_render_texture_interpolated(TEX_HUD_KEY_RIGHT, mapData.prevX + adjustScale * 32, mapData.prevY, adjustScale, adjustScale, renderX + adjustScale * 32, renderY, adjustScale, adjustScale) end if isStar then djui_hud_set_font(FONT_HUD) djui_hud_set_color(255, 255, 255, 255) djui_hud_print_text_interpolated(tostring(starsLeft), mapData.prevX + 6 * scale, mapData.prevY + 6 * scale, scale * 0.75, renderX + 6 * scale, renderY + 6 * scale, scale * 0.75) end end playerCountHere = 1 if playerCountHere ~= 0 then scale = 0.5 djui_hud_set_font(FONT_RECOLOR_HUD or FONT_HUD) if isHunter then djui_hud_set_color(0, 255, 255, 255) else djui_hud_set_color(169, 169, 169, 255) end local prevX = mapData.prevX local prevY = mapData.prevY - 12 * scale local x = renderX local y = renderY - 12 * scale if not FONT_RECOLOR_HUD then y = y + 8 * scale prevY = prevY + 8 * scale end prevX = prevX - 4 * scale x = x - 4 * scale scale = scale * 0.75 djui_hud_print_text_interpolated(tostring(playerCountHere), prevX, prevY, scale, x, y, scale) end mapData.prevX = renderX mapData.prevY = renderY end if totalStars ~= 0 or exThingy ~= 0 or (romhack_file == "B3313") then djui_hud_set_resolution(RESOLUTION_N64) local out = { x = 0, y = 0, z = 0 } local scale = 0.68 local dist = vec3f_dist(gLakituState.pos, pos) if dist > 2000 then scale = 0.5 scale = scale + dist / 4000 scale = clampf(1.68 - scale, 0, 0.68) end if scale ~= 0 and djui_hud_world_pos_to_screen_pos(pos, out) then djui_hud_set_font(FONT_NORMAL) local text = get_level_name(warpData[1], warpData[2], warpData[3]) local alpha = 255 if scale ~= 0.68 then alpha = scale * alpha / 0.68 end local radar = progressRadar[i] if not radar then progressRadar[i] = { prevX = out.x, prevY = out.y, prevScale = scale } radar = progressRadar[i] end local prevScale = radar.prevScale local width = djui_hud_measure_text(text) * scale local prevWidth = width * prevScale / scale local x = out.x - width * 0.5 local y = out.y - 35 * scale local prevX = radar.prevX - prevWidth * 0.5 local prevY = radar.prevY - 35 * prevScale local color = { r = 255, g = 255, b = 92 } -- yellow local texWidth = 16 local adjustScale = 1 if omm_star_colors[course] then color = omm_star_colors[course] end djui_hud_print_outlined_text_interpolated(text, prevX, prevY, prevScale, x, y, scale, color.r, color.g, color.b, alpha, 0.25) if OmmEnabled then texWidth = get_texture_info("omm_tex_hud_star_empty").width adjustScale = adjustScale / texWidth * 16 end width = (texWidth + 4) * totalStars - 4 if exThingy ~= 0 then width = width + 20 end width = width * scale * adjustScale prevWidth = width * prevScale / scale x = out.x - width * 0.5 y = out.y prevX = radar.prevX - prevWidth * 0.5 prevY = radar.prevY for i = 1, totalStars do local tex = gTextures.star if (starFlags & (1 << (i - 1)) == 0) then if OmmEnabled then tex = get_texture_info("omm_tex_hud_star_empty") djui_hud_set_color(255, 255, 255, alpha) else djui_hud_set_color(0, 0, 0, alpha // 2) end elseif OmmEnabled then -- do colored radar local starColor = 0 if omm_star_colors[course] and OmmApi.omm_get_setting(gMarioStates[0], "color") ~= 0 then starColor = course end tex = get_texture_info("omm_tex_hud_star_" .. starColor) r, g, b = 255, 255, 255 djui_hud_set_color(255, 255, 255, alpha) -- color is already loadeed elseif romhack_file == "sm74" and gNetworkPlayers[0].currAreaIndex == 2 then djui_hud_set_color(255, 0, 0, alpha) else djui_hud_set_color(255, 255, 255, alpha) end if tex then djui_hud_render_texture_interpolated(tex, prevX, prevY, prevScale * adjustScale, prevScale * adjustScale, x, y, scale * adjustScale, scale * adjustScale) end x = x + (texWidth + 4) * scale * adjustScale prevX = prevX + (texWidth + 4) * prevScale * adjustScale end if exThingy ~= 0 then local adjustScale = 1 if not haveExThingy then djui_hud_set_color(0, 0, 0, alpha // 2) else djui_hud_set_color(255, 255, 255, alpha) end if exThingy == 1 then tex = TEX_HUD_KEY_LEFT adjustScale = 0.25 elseif exThingy == 2 then tex = get_texture_info("exclamation_box_seg8_texture_08015E28") adjustScale = 0.5 elseif exThingy == 3 then tex = get_texture_info("exclamation_box_seg8_texture_08012E28") adjustScale = 0.5 else tex = get_texture_info("exclamation_box_seg8_texture_08014628") adjustScale = 0.5 end djui_hud_render_texture_interpolated(tex, prevX, prevY, prevScale * adjustScale, prevScale * adjustScale, x, y, scale * adjustScale, scale * adjustScale) if tex == TEX_HUD_KEY_LEFT then djui_hud_render_texture_interpolated(TEX_HUD_KEY_RIGHT, prevX + prevScale * adjustScale * 32, prevY, prevScale * adjustScale, prevScale * adjustScale, x + prevScale * adjustScale * 32, y, scale * adjustScale, scale * adjustScale) end end if playerCountHere ~= 0 then local color = {} -- for some reason, not doing this makes the level name also this color? doesn't make sense to me local adjustScale = 0.5 local runners = "" if isHunter then runners = trans("runners") if playerCountHere == 1 then runners = trans("runner") end color.r, color.g, color.b = 0, 255, 255 else runners = trans("players") if playerCountHere == 1 then runners = trans("player") end color.r, color.g, color.b = 169, 169, 169 end text = tostring(playerCountHere) .. " " .. runners width = djui_hud_measure_text(text) * scale * adjustScale prevWidth = width * prevScale / scale x = out.x - width / 2 y = out.y + 35 * scale * adjustScale prevX = radar.prevX - width / 2 prevY = radar.prevY + 35 * prevScale * adjustScale djui_hud_print_outlined_text_interpolated(text, prevX, prevY, prevScale * adjustScale, x, y, scale * adjustScale, color.r, color.g, color .b, alpha, 0.25) end radar.prevX = out.x radar.prevY = out.y radar.prevScale = scale end end end end -- hook last so MH doesn't render over this local didHook = false function update() if didHook then return end didHook = true hook_event(HOOK_ON_HUD_RENDER, painting_overlays) end hook_event(HOOK_UPDATE, update) function disable_command(msg) msg = msg and msg:lower() if msg == "on" then overlayDisabled = false elseif msg == "off" then overlayDisabled = true else overlayDisabled = not overlayDisabled end if overlayDisabled then djui_chat_message_create("Overlay is now \\#ff5a5a\\OFF!") else djui_chat_message_create("Overlay is now \\#5aff5a\\ON!") end return true end hook_chat_command("painting-overlay", "[ON|OFF] - Toggle the painting overlay that shows progress", disable_command) function djui_hud_print_outlined_text_interpolated(text, prevX, prevY, prevScale, x, y, scale, r, g, b, a, outlineDarkness) local offset = 1 * (scale * 2) local prevOffset = 1 * (prevScale * 2) -- render outline djui_hud_set_color(r * outlineDarkness, g * outlineDarkness, b * outlineDarkness, a) djui_hud_print_text_interpolated(text, prevX - prevOffset, prevY, prevScale, x - offset, y, scale) djui_hud_print_text_interpolated(text, prevX + prevOffset, prevY, prevScale, x + offset, y, scale) djui_hud_print_text_interpolated(text, prevX, prevY - prevOffset, prevScale, x, y - offset, scale) djui_hud_print_text_interpolated(text, prevX, prevY + prevOffset, prevScale, x, y + offset, scale) -- render text djui_hud_set_color(r, g, b, a) djui_hud_print_text_interpolated(text, prevX, prevY, prevScale, x, y, scale) djui_hud_set_color(255, 255, 255, 255) end -- shows the number of players in a course (not including self) -- level and area only apply in b3313 (level also applies in some other scenarios) -- act, if unset, allows for any act (unused in this mod) -- used with radar function get_player_count(course, level, area, act) local count = 0 local isB3313 = (romhack_file == "B3313") local isHunter = mhExists -- if true, only show alive runners here for i = 1, MAX_PLAYERS - 1 do local np = gNetworkPlayers[i] if np.connected and (not (mhExists and mhApi.isSpectator(i))) and np.currCourseNum == course and (act == nil or np.currActNum == act) then local valid = true if isB3313 and (level ~= np.currLevelNum or area ~= np.currAreaIndex) then valid = false elseif (level == LEVEL_BOWSER_1 or level == LEVEL_BOWSER_2 or level == LEVEL_BOWSER_3) and level ~= np.currLevelNum then valid = false elseif isHunter and mhApi.getTeam(i) ~= 1 then valid = false end if valid then count = count + 1 end end end return count, isHunter end -- star color table for OMM, based on course number omm_star_colors = { [COURSE_BOB] = { r = 71, g = 192, b = 71 }, [COURSE_WF] = { r = 190, g = 190, b = 190 }, [COURSE_JRB] = { r = 237, g = 176, b = 204 }, [COURSE_CCM] = { r = 30, g = 255, b = 255 }, [COURSE_BBH] = { r = 189, g = 148, b = 203 }, [COURSE_HMC] = { r = 127, g = 127, b = 127 }, [COURSE_LLL] = { r = 255, g = 25, b = 25 }, [COURSE_SSL] = { r = 192, g = 246, b = 74 }, [COURSE_DDD] = { r = 28, g = 169, b = 240 }, [COURSE_SL] = { r = 255, g = 255, b = 255 }, [COURSE_WDW] = { r = 168, g = 189, b = 208 }, [COURSE_TTM] = { r = 198, g = 161, b = 124 }, [COURSE_THI] = { r = 255, g = 200, b = 64 }, [COURSE_TTC] = { r = 250, g = 182, b = 146 }, [COURSE_RR] = { r = 241, g = 127, b = 237 }, } -- I still don't think there's an easier way to do this, unfortunately level_to_course = { [LEVEL_CASTLE_GROUNDS] = COURSE_NONE, -- Course 0 [LEVEL_CASTLE] = COURSE_NONE, -- Course 0 [LEVEL_CASTLE_COURTYARD] = COURSE_NONE, -- Course 0 [LEVEL_BOB] = COURSE_BOB, -- Course 1 [LEVEL_WF] = COURSE_WF, -- Course 2 [LEVEL_JRB] = COURSE_JRB, -- Course 3 [LEVEL_CCM] = COURSE_CCM, -- Course 4 [LEVEL_BBH] = COURSE_BBH, -- Course 5 [LEVEL_HMC] = COURSE_HMC, -- Course 6 [LEVEL_LLL] = COURSE_LLL, -- Course 7 [LEVEL_SSL] = COURSE_SSL, -- Course 8 [LEVEL_DDD] = COURSE_DDD, -- Course 9 [LEVEL_SL] = COURSE_SL, -- Course 10 [LEVEL_WDW] = COURSE_WDW, -- Course 11 [LEVEL_TTM] = COURSE_TTM, -- Course 12 [LEVEL_THI] = COURSE_THI, -- Course 13 [LEVEL_TTC] = COURSE_TTC, -- Course 14 [LEVEL_RR] = COURSE_RR, -- Course 15 [LEVEL_BITDW] = COURSE_BITDW, -- Course 16 [LEVEL_BOWSER_1] = COURSE_BITDW, -- Course 16 [LEVEL_BITFS] = COURSE_BITFS, -- Course 17 [LEVEL_BOWSER_2] = COURSE_BITFS, -- Course 17 [LEVEL_BITS] = COURSE_BITS, -- Course 18 [LEVEL_BOWSER_3] = COURSE_BITS, -- Course 18 [LEVEL_PSS] = COURSE_PSS, -- Course 19 [LEVEL_COTMC] = COURSE_COTMC, -- Course 20 [LEVEL_TOTWC] = COURSE_TOTWC, -- Course 21 [LEVEL_VCUTM] = COURSE_VCUTM, -- Course 22 [LEVEL_WMOTR] = COURSE_WMOTR, -- Course 23 [LEVEL_SA] = COURSE_SA, -- Course 24 [LEVEL_ENDING] = COURSE_CAKE_END, -- Course 25 }