diff --git a/mods/shell-rush/actions.lua b/mods/shell-rush/actions.lua new file mode 100644 index 000000000..eb08d04d9 --- /dev/null +++ b/mods/shell-rush/actions.lua @@ -0,0 +1,278 @@ +gExtraMarioState = { } + +for i = 0, (MAX_PLAYERS - 1) do + gExtraMarioState[i] = { } + gExtraMarioState[i].lastY = 0 +end + +function race_get_slope_physics(m) + local friction = 0.96 + local force = 3 + + if mario_floor_is_slope(m) ~= 0 then + local slopeClass = 0 + + if m.action ~= ACT_SOFT_BACKWARD_GROUND_KB and m.action ~= ACT_SOFT_FORWARD_GROUND_KB then + slopeClass = mario_get_floor_class(m) + end + + if slopeClass == SURFACE_CLASS_VERY_SLIPPERY then + friction = 0.98 + force = 3.3 + elseif slopeClass == SURFACE_CLASS_SLIPPERY then + friction = 0.97 + force = 3.2 + end + end + + return { + force = force, + friction = friction, + } +end + +function race_apply_slope_accel(m) + local physics = race_get_slope_physics(m) + + local floor = m.floor + local floorNormal = m.floor.normal + + local mTheta = m.faceAngle.y + local mSpeed = m.forwardVel * 1.5 * gGlobalSyncTable.speed + if mSpeed > 135 * gGlobalSyncTable.speed then mSpeed = 135 * gGlobalSyncTable.speed end + + local mDir = { + x = sins(mTheta), + y = 0, + z = coss(mTheta) + } + + m.slideYaw = m.faceAngle.y + m.slideVelX = 0 + m.slideVelZ = 0 + + -- apply direction + local angle = vec3f_angle_between(m.vel, mDir) + + local parallel = vec3f_project(m.vel, mDir) + local perpendicular = { x = m.vel.x - parallel.x, y = m.vel.y - parallel.y, z = m.vel.z - parallel.z } + local parallelMag = vec3f_length(parallel) + local perpendicularMag = vec3f_length(perpendicular) + local originalPerpendicularMag = perpendicularMag + + if angle >= math.pi / 2 then + parallelMag = -1 + elseif parallelMag < mSpeed then + local lastMag = parallelMag + parallelMag = parallelMag * 0.85 + mSpeed * 0.15 + perpendicularMag = perpendicularMag - (parallelMag - lastMag) * 0.12 + if perpendicularMag < 0 then perpendicularMag = 0 end + end + + vec3f_normalize(parallel) + vec3f_normalize(perpendicular) + vec3f_non_nan(parallel) + vec3f_non_nan(perpendicular) + + local combined = { + x = parallel.x * parallelMag + perpendicular.x * perpendicularMag, + y = parallel.y * parallelMag + perpendicular.y * perpendicularMag, + z = parallel.z * parallelMag + perpendicular.z * perpendicularMag, + } + m.vel.x = combined.x + m.vel.z = combined.z + + -- apply friction + m.vel.x = m.vel.x * physics.friction + m.vel.z = m.vel.z * physics.friction + m.vel.y = 0.0 + + -- apply slope + m.vel.x = m.vel.x + physics.force * floorNormal.x + m.vel.z = m.vel.z + physics.force * floorNormal.z + + -- apply vanilla forces + local velBeforeVanilla = { x = m.vel.x, y = m.vel.y, z = m.vel.z } + mario_update_moving_sand(m) + mario_update_windy_ground(m) + m.vel.x = m.vel.x * 0.2 + velBeforeVanilla.x * 0.8 + m.vel.y = m.vel.y * 0.2 + velBeforeVanilla.y * 0.8 + m.vel.z = m.vel.z * 0.2 + velBeforeVanilla.z * 0.8 +end + +function update_race_shell_speed(m) + local maxTargetSpeed = 0 + local targetSpeed = 0 + local startForwardVel = m.forwardVel + + -- brake + if (m.controller.buttonDown & B_BUTTON) ~= 0 then + m.forwardVel = m.forwardVel * 0.9 + end + + -- set water level + if m.floorHeight < m.waterLevel then + m.floorHeight = m.waterLevel + m.floor = get_water_surface_pseudo_floor() + m.floor.originOffset = m.waterLevel -- Negative origin offset + end + + -- set max target speed + if m.floor ~= nil and m.floor.type == SURFACE_SLOW then + maxTargetSpeed = 48.0 + else + maxTargetSpeed = 64.0 + end + + -- set target speed + targetSpeed = m.intendedMag * 2.0 + if targetSpeed > maxTargetSpeed then + targetSpeed = maxTargetSpeed + end + if targetSpeed < 18.0 then + targetSpeed = 18.0 + end + + -- set speed + if m.forwardVel <= 0.0 then + m.forwardVel = 1.1 + + elseif m.forwardVel <= targetSpeed + 1.1 then + m.forwardVel = m.forwardVel + 1.1 + + elseif m.forwardVel > targetSpeed - 1.5 then + m.forwardVel = m.forwardVel - 1.5 + + elseif m.floor ~= nil and m.floor.normal.y >= 0.95 then + m.forwardVel = m.forwardVel - 1.1 + end + + if m.forwardVel > 64.0 then + if m.forwardVel > startForwardVel - 3.0 then + m.forwardVel = startForwardVel - 3.0 + end + end + + local turnSpeed = 0x800 + if (m.controller.buttonDown & B_BUTTON) ~= 0 then turnSpeed = 0x650 end + m.faceAngle.y = m.intendedYaw - approach_s32(convert_s16(m.intendedYaw - m.faceAngle.y), 0, turnSpeed, turnSpeed) + + race_apply_slope_accel(m) +end + +function act_race_shell_ground(m) + if m.actionTimer < 5 then m.actionTimer = m.actionTimer + 1 end + + local startYaw = m.faceAngle.y + + -- enforce min velocities + if m.forwardVel == 0 then m.forwardVel = 1 end + if vec3f_length(m.vel) == 0 then m.vel.x = 1 end + + -- jump + if (m.input & INPUT_A_PRESSED) ~= 0 then + m.vel.x = m.vel.x * 0.9 + m.vel.z = m.vel.z * 0.9 + return set_mario_action(m, ACT_RIDING_SHELL_JUMP, 0) + end + + -- update physics + update_race_shell_speed(m) + + -- set animation + if m.actionArg == 0 then + set_mario_animation(m, MARIO_ANIM_START_RIDING_SHELL) + else + set_mario_animation(m, MARIO_ANIM_RIDING_SHELL) + end + + local gs = perform_ground_step(m) + if gs == GROUND_STEP_LEFT_GROUND then + m.vel.y = (m.pos.y - gExtraMarioState[m.playerIndex].lastY) + return set_mario_action(m, ACT_RIDING_SHELL_FALL, 0) + + elseif gs == GROUND_STEP_HIT_WALL then + -- check if the wall is in the facing direction + local castDir = { + x = sins(m.faceAngle.y) * 200, + y = 0, + z = coss(m.faceAngle.y) * 200 + } + local info = collision_find_surface_on_ray( + m.pos.x, m.pos.y + 100, m.pos.z, + castDir.x, castDir.y, castDir.z) + if info.surface ~= nil then + mario_stop_riding_object(m) + play_sound(SOUND_ACTION_BONK, m.marioObj.header.gfx.cameraToObject) + m.particleFlags = m.particleFlags | PARTICLE_VERTICAL_STAR + m.forwardVel = 0 + set_mario_action(m, ACT_BACKWARD_GROUND_KB, 0) + end + end + + tilt_body_ground_shell(m, startYaw) + + if m.floor.type == SURFACE_BURNING then + play_sound(SOUND_MOVING_RIDING_SHELL_LAVA, m.marioObj.header.gfx.cameraToObject) + else + play_sound(SOUND_MOVING_TERRAIN_RIDING_SHELL, m.marioObj.header.gfx.cameraToObject) + end + + adjust_sound_for_speed(m) + + reset_rumble_timers(m) + gExtraMarioState[m.playerIndex].lastY = m.pos.y + return 0 +end + +function act_race_shell_air(m) + if m.actionTimer < 5 then m.actionTimer = m.actionTimer + 1 end + + play_mario_sound(m, SOUND_ACTION_TERRAIN_JUMP, 0) + set_mario_animation(m, MARIO_ANIM_JUMP_RIDING_SHELL) + + if m.vel.y > 65 then m.vel.y = 65 end + + local mSpeed = m.forwardVel / 128.0 * gGlobalSyncTable.speed + if mSpeed > 100 * gGlobalSyncTable.speed then mSpeed = 100 * gGlobalSyncTable.speed end + local mDir = { + x = sins(m.intendedYaw), + y = 0, + z = coss(m.intendedYaw) + } + + -- apply direction + local parallel = vec3f_project(mDir, m.vel) + local perpendicular = { x = mDir.x - parallel.x, y = mDir.y - parallel.y, z = mDir.z - parallel.z } + local parallelMag = vec3f_length(parallel) + if parallelMag < mSpeed then parallelMag = mSpeed / parallelMag end + + local combined = { + x = parallel.x * parallelMag + perpendicular.x * 0.95, + y = parallel.y * parallelMag + perpendicular.y * 0.95, + z = parallel.z * parallelMag + perpendicular.z * 0.95, + } + + m.vel.x = m.vel.x + mSpeed * mDir.x + m.vel.z = m.vel.z + mSpeed * mDir.z + + -- apply rotation + m.faceAngle.y = m.intendedYaw - approach_s32(convert_s16(m.intendedYaw - m.faceAngle.y), 0, 0x300, 0x300) + + local step = perform_air_step(m, 0) + if step == AIR_STEP_LANDED then + set_mario_action(m, ACT_RIDING_SHELL_GROUND, 1) + elseif step == AIR_STEP_HIT_WALL then + mario_set_forward_vel(m, 0.0) + elseif step == AIR_STEP_HIT_LAVA_WALL then + lava_boost_on_wall(m) + end + + m.marioObj.header.gfx.pos.y = m.marioObj.header.gfx.pos.y + 42.0 + gExtraMarioState[m.playerIndex].lastY = m.pos.y + return 0 +end + +hook_mario_action(ACT_RIDING_SHELL_GROUND, act_race_shell_ground) +hook_mario_action(ACT_RIDING_SHELL_JUMP, act_race_shell_air) +hook_mario_action(ACT_RIDING_SHELL_FALL, act_race_shell_air) diff --git a/mods/shell-rush/actors/banana_geo.bin b/mods/shell-rush/actors/banana_geo.bin new file mode 100644 index 000000000..ce937a7cd Binary files /dev/null and b/mods/shell-rush/actors/banana_geo.bin differ diff --git a/mods/shell-rush/actors/item_box_geo.bin b/mods/shell-rush/actors/item_box_geo.bin new file mode 100644 index 000000000..8bade9a1f Binary files /dev/null and b/mods/shell-rush/actors/item_box_geo.bin differ diff --git a/mods/shell-rush/actors/red_shell_geo.bin b/mods/shell-rush/actors/red_shell_geo.bin new file mode 100644 index 000000000..111bd90a9 Binary files /dev/null and b/mods/shell-rush/actors/red_shell_geo.bin differ diff --git a/mods/shell-rush/hud.lua b/mods/shell-rush/hud.lua new file mode 100644 index 000000000..3178e39ca --- /dev/null +++ b/mods/shell-rush/hud.lua @@ -0,0 +1,96 @@ + +function on_hud_render() + local s = gPlayerSyncTable[0] + hud_hide() + + djui_hud_set_resolution(RESOLUTION_N64) + djui_hud_set_font(FONT_NORMAL) + + local scale = 0.25 + local width = 0 + local height = 4 * scale + + for i in pairs(gRankings) do + local np = gNetworkPlayers[gRankings[i].playerIndex] + local w = (djui_hud_measure_text(tostring(i) .. '. ' .. np.name) + 8) * scale + if w > width then width = w end + height = height + 28 * scale + end + + djui_hud_set_color(0, 0, 0, 128) + djui_hud_render_rect(0, 0, width, height) + + local x = 4 * scale + local y = 0 + local rank = 0 + + -- draw rankings + for i in pairs(gRankings) do + local np = gNetworkPlayers[gRankings[i].playerIndex] + djui_hud_set_color(0, 0, 0, 255) + djui_hud_print_text(tostring(i) .. '. ' .. np.name, x + 2 * scale, y + 2 * scale, scale) + if gRankings[i].playerIndex == 0 then + rank = i + djui_hud_set_color(255, 240, 150, 255) + else + djui_hud_set_color(220, 220, 220, 255) + end + djui_hud_print_text(tostring(i) .. '. ' .. np.name, x + 0 * scale, y + 0 * scale, scale) + y = y + 28 * scale + end + + + if gGlobalSyncTable.gameState == GAME_STATE_RACE_COUNTDOWN then + -- draw countdown + scale = 0.6 + djui_hud_set_font(FONT_MENU) + djui_hud_set_color(64, 128, 255, 255) + local countdown = math.floor((gGlobalSyncTable.raceStartTime - get_network_area_timer()) / 30) + countdown = clamp(countdown + 1, 1, 5) + + local countdownText = tostring(countdown) + x = (djui_hud_get_screen_width() - djui_hud_measure_text(countdownText) * scale) / 2 + djui_hud_print_text(countdownText, x, 2 * scale, scale) + else + -- draw lap counter + scale = 0.3 + djui_hud_set_font(FONT_MENU) + djui_hud_set_color(64, 128, 255, 255) + local lapText = 'LAP ' .. tostring(s.lap) .. ' /' .. tostring(gGlobalSyncTable.maxLaps) + if s.finish ~= nil and s.finish > 0 then lapText = 'FINISHED' end + x = (djui_hud_get_screen_width() - djui_hud_measure_text(lapText) * scale) / 2 + djui_hud_print_text(lapText, x, 2 * scale, scale) + + -- draw player rank + if rank > 0 then + scale = 0.6 + djui_hud_set_color(255, clamp(255 - 255 * (rank / 8), 0, 255), 0, 255) + + local rankText = tostring(rank) .. 'th' + if rank == 1 then rankText = '1st' end + if rank == 2 then rankText = '2nd' end + if rank == 3 then rankText = '3rd' end + + x = (djui_hud_get_screen_width() - djui_hud_measure_text(rankText) * scale) / 2 + y = (djui_hud_get_screen_height() - 80 * scale) + djui_hud_print_text(rankText, x, y, scale) + end + end + + if gGlobalSyncTable.raceQuitTime > 0 then + -- draw ending countdown + scale = 0.6 + djui_hud_set_font(FONT_MENU) + djui_hud_set_color(64, 128, 255, 255) + local countdown = math.floor((gGlobalSyncTable.raceQuitTime - get_network_area_timer()) / 30) + countdown = clamp(countdown + 1, 1, 20) + + local countdownText = tostring(countdown) + x = (djui_hud_get_screen_width() - djui_hud_measure_text(countdownText) * scale) / 2 + y = 40 * scale + djui_hud_print_text(countdownText, x, y, scale) + end + +end + +hook_event(HOOK_ON_HUD_RENDER, on_hud_render) diff --git a/mods/shell-rush/item-box.lua b/mods/shell-rush/item-box.lua new file mode 100644 index 000000000..c4d1880ed --- /dev/null +++ b/mods/shell-rush/item-box.lua @@ -0,0 +1,74 @@ +local itemBoxTimeout = 30 * 4 -- 4 seocnds + +define_custom_obj_fields({ + oItemBoxTouched = 'u32', +}) + +function bhv_item_box_init(obj) + obj.oFlags = OBJ_FLAG_UPDATE_GFX_POS_AND_ANGLE + obj.oOpacity = 100 + obj_scale(obj, 1.0) + + obj.oPosY = obj.oPosY + 100 + local floor = cur_obj_update_floor_height_and_get_floor() + if floor ~= nil then + obj.oPosY = obj.oFloorHeight + 130 + end + + network_init_object(obj, false, { + 'oItemBoxTouched', + 'oTimer' + }) +end + +function bhv_item_box_collect(obj) + spawn_sparkles(obj) + spawn_mist(obj) + obj.oItemBoxTouched = 1 + obj.oTimer = 0 + network_send_object(obj, true) + select_powerup() + cur_obj_play_sound_2(SOUND_GENERAL_COLLECT_1UP) +end + +function bhv_item_box_loop(obj) + if obj.oItemBoxTouched == 1 then + if obj.oTimer >= itemBoxTimeout then + obj.oItemBoxTouched = 0 + if get_network_player_smallest_global() == gNetworkPlayers[0] then + network_send_object(obj, true) + end + elseif obj.oTimer < 5 then + obj_scale(obj, 1 - (obj.oTimer / 5)) + elseif obj.oTimer >= itemBoxTimeout - 10 then + obj_scale(obj, (obj.oTimer - (itemBoxTimeout - 10)) / 10) + cur_obj_unhide() + else + cur_obj_hide() + end + return + else + obj_scale(obj, 1.0) + cur_obj_unhide() + end + + obj.oFaceAngleYaw = obj.oFaceAngleYaw + 0x250 + obj.oFaceAngleRoll = 0 + obj.oFaceAnglePitch = 0 + + local m = nearest_mario_state_to_object(obj) + if m == gMarioStates[0] then + local s = gPlayerSyncTable[0] + if s.powerup[0] == POWERUP_NONE and s.powerup[1] == POWERUP_NONE and s.powerup[2] == POWERUP_NONE then + local player = m.marioObj + local yDist = math.abs(obj.oPosY - player.oPosY) + local xzDist = math.sqrt(math.pow(obj.oPosX - player.oPosX, 2) + math.pow(obj.oPosZ - player.oPosZ, 2)) + if xzDist < 120 and yDist < 200 then + bhv_item_box_collect(obj) + end + end + end +end + +id_bhvItemBox = hook_behavior(nil, OBJ_LIST_LEVEL, true, bhv_item_box_init, bhv_item_box_loop) +E_MODEL_ITEM_BOX = smlua_model_util_get_id("item_box_geo") diff --git a/mods/shell-rush/level-data.lua b/mods/shell-rush/level-data.lua new file mode 100644 index 000000000..3eef859ea --- /dev/null +++ b/mods/shell-rush/level-data.lua @@ -0,0 +1,147 @@ +gLevelDataTable = { + [-1] = { + waypoints = { }, + powerups = { }, + spawn = { }, + erase = { }, + platforms = { }, + }, + + [LEVEL_BOB] = { + waypoints = { + { x = -1953, y = 0, z = 1418 }, + { x = -2224, y = 73, z = 4738 }, + { x = 691, y = 744, z = 5584 }, + { x = 2607, y = 832, z = 6589 }, + { x = 6230, y = 981, z = 5311 }, + { x = 6818, y = 891, z = 2590 }, + { x = 4900, y = 1323, z = 989 }, + { x = 833, y = 768, z = 3215 }, + { x = -1641, y = 768, z = 1748 }, + { x = -3490, y = 1024, z = -275 }, + { x = -4786, y = 1293, z = -3216 }, + { x = -2399, y = 1015, z = -4957 }, + { x = 50, y = 1053, z = -2639 }, + { x = 2967, y = 1611, z = -1526 }, + { x = 5667, y = 1888, z = -3151 }, + { x = 5601, y = 2055, z = -6265 }, + { x = 2961, y = 2466, z = -7244 }, + { x = -341, y = 2603, z = -5406 }, + { x = 615, y = 2843, z = -2512 }, + }, + powerups = { + { pos = { x = 4223, y = 768, z = 6768 }, obj = nil }, + { pos = { x = 4267, y = 768, z = 6372 }, obj = nil }, + { pos = { x = 4097, y = 795, z = 5927 }, obj = nil }, + { pos = { x = -4197, y = 1022, z = -1507 }, obj = nil }, + { pos = { x = -3858, y = 1008, z = -1710 }, obj = nil }, + { pos = { x = -4483, y = 1088, z = -1298 }, obj = nil }, + { pos = { x = 5493, y = 1959, z = -4592 }, obj = nil }, + { pos = { x = 5883, y = 1963, z = -4603 }, obj = nil }, + { pos = { x = 6259, y = 2013, z = -4748 }, obj = nil }, + }, + spawn = { + { + a = { x = -993, y = 0, z = -869 }, + b = { x = -1264, y = 0, z = -2489 }, + }, + { + a = { x = -1658, y = 0, z = -864 }, + b = { x = -1900, y = 0, z = -2487 }, + }, + }, + erase = { }, + platforms = { + { pos = { x = 1100, y = 3000, z = -2800 }, rot = { x = 0x4000, y = 0x3604, z = 0 }, scale = { x = 2, y = 2, z = 2 } }, + }, + }, + + [LEVEL_SL] = { + waypoints = { + { x = -6715, y = 1979, z = -621 }, + { x = -2062, y = 1204, z = -4538 }, + { x = 3935, y = 790, z = -3129 }, + { x = 5457, y = 1024, z = 5326 }, + { x = 3614, y = 1024, z = 5615 }, + { x = 2617, y = 1426, z = -1412 }, + { x = -1056, y = 1536, z = -2493 }, + { x = -3857, y = 1024, z = 1497 }, + { x = -4666, y = 1382, z = 4190 }, + }, + powerups = { + { pos = { x = -6138, y = 2010, z = -977 }, obj = nil }, + { pos = { x = -6576, y = 2029, z = -1133 }, obj = nil }, + { pos = { x = -7000, y = 2043, z = -1239 }, obj = nil }, + { pos = { x = 232, y = 1352, z = -4544 }, obj = nil }, + { pos = { x = 3793, y = 1024, z = 3271 }, obj = nil }, + { pos = { x = 3232, y = 1024, z = 3317 }, obj = nil }, + { pos = { x = 2723, y = 1024, z = 3359 }, obj = nil }, + }, + spawn = { + { + a = { x = -6947, y = 1874, z = 291 }, + b = { x = -6961, y = 1683, z = 3040 }, + }, + { + a = { x = -6592, y = 1903, z = 291 }, + b = { x = -6488, y = 1640, z = 3040 }, + }, + }, + erase = { + [id_bhvMrBlizzard] = true, + [id_bhvBigChillBully] = true, + [id_bhvMoneybag] = true, + [id_bhvMoneybagHidden] = true, + }, + platforms = { + { pos = { x = 360, y = 2150, z = 1392 }, rot = { x = 0x4000, y = 0x49b2, z = 0 }, scale = { x = 1, y = 1, z = 1 } }, + }, + }, + + [LEVEL_CASTLE_GROUNDS] = { + waypoints = { + { x = -3122, y = 260, z = 4191 }, + { x = -3616, y = 415, z = 365 }, + { x = -5348, y = 492, z = -3201 }, + { x = -6273, y = 497, z = -2918 }, + { x = -6288, y = 336, z = -605 }, + { x = -3708, y = 412, z = 165 }, + { x = -331, y = 806, z = 511 }, + { x = 5171, y = 385, z = -1250 }, + { x = 4673, y = 544, z = -4888 }, + { x = 3930, y = -511, z = -2185 }, + { x = -265, y = -511, z = -1126 }, + { x = -3904, y = -511, z = -1674 }, + { x = -308, y = -511, z = -1189 }, + { x = 3891, y = -511, z = -1034 }, + { x = 4336, y = -800, z = 2988 }, + { x = 297, y = 632, z = 2089 }, + }, + powerups = { + { pos = { x = -3801, y = 399, z = 709 }, obj = nil }, + { pos = { x = -3604, y = 415, z = 363 }, obj = nil }, + { pos = { x = -3378, y = 431, z = -4 }, obj = nil }, + { pos = { x = -3302, y = 431, z = 599 }, obj = nil }, + { pos = { x = -3949, y = 396, z = 120 }, obj = nil }, + { pos = { x = -292, y = -511, z = -1156 }, obj = nil }, + { pos = { x = -292, y = -511, z = -1571 }, obj = nil }, + { pos = { x = -292, y = -511, z = -741 }, obj = nil }, + }, + spawn = { + { + a = { x = -2365, y = 260, z = 4673 }, + b = { x = -940, y = 260, z = 5294 }, + }, + { + a = { x = -2134, y = 260, z = 4143 }, + b = { x = -348, y = 260, z = 4922 }, + }, + }, + erase = { + [id_bhvDoorWarp] = true, + }, + platforms = { + { pos = { x = -3369, y = -540, z = -2025 }, rot = { x = 0, y = 0, z = 0 }, scale = { x = 1, y = 1, z = 1 } }, + }, + }, +} diff --git a/mods/shell-rush/level.lua b/mods/shell-rush/level.lua new file mode 100644 index 000000000..72644f9db --- /dev/null +++ b/mods/shell-rush/level.lua @@ -0,0 +1,155 @@ +gRaceShells = {} + +gLevelData = gLevelDataTable[-1] + +function erase_unwanted_entities(objList) + local obj = obj_get_first(objList) + while obj ~= nil do + local behaviorId = get_id_from_behavior(obj.behavior) + if gLevelData.erase[behaviorId] ~= nil then + obj.activeFlags = ACTIVE_FLAG_DEACTIVATED + end + + -- iterate + obj = obj_get_next(obj) + end +end + +function on_level_init() + -- set level data + local level = gNetworkPlayers[0].currLevelNum + if gLevelDataTable[level] ~= nil then + gLevelData = gLevelDataTable[level] + else + gLevelData = gLevelDataTable[-1] + end + + -- spawn all of the racing shells + for i = 0, (MAX_PLAYERS - 1) do + gRaceShells[i] = spawn_non_sync_object( + id_bhvRaceShell, + E_MODEL_KOOPA_SHELL, + 0, 0, 0, + function (obj) obj.heldByPlayerIndex = i end + ) + end + + -- spawn all of the waypoints + for i in pairs(gLevelData.waypoints) do + local waypoint = get_waypoint(i) + spawn_non_sync_object( + id_bhvRaceRing, + E_MODEL_WATER_RING, + waypoint.x, waypoint.y, waypoint.z, + function (obj) obj.oWaypointIndex = i end + ) + end + + -- spawn level-specific platforms + for i in pairs(gLevelData.platforms) do + local p = gLevelData.platforms[i] + spawn_non_sync_object( + id_bhvStaticCheckeredPlatform, + E_MODEL_CHECKERBOARD_PLATFORM, + p.pos.x, p.pos.y, p.pos.z, + function (obj) + obj.oOpacity = 255 + obj.oFaceAnglePitch = p.rot.x + obj.oFaceAngleYaw = p.rot.y + obj.oFaceAngleRoll = p.rot.z + obj_scale_xyz(obj, p.scale.x, p.scale.y, p.scale.z) + end) + end + -- reset the local player's data + local s = gPlayerSyncTable[0] + s.waypoint = 1 + s.lap = 0 + s.finish = 0 + for i = 0, 2 do + s.powerup[i] = POWERUP_NONE + end + + -- reset the custom level objects + for i in pairs(gLevelData.powerups) do + gLevelData.powerups[i].obj = nil + end + + for i = 0, (MAX_PLAYERS - 1) do + for j = 0, 2 do + gPowerups[i][j] = nil + end + end + + -- erase specified level entities + erase_unwanted_entities(OBJ_LIST_GENACTOR) + erase_unwanted_entities(OBJ_LIST_LEVEL) + erase_unwanted_entities(OBJ_LIST_SURFACE) + + -- reset rankings + race_clear_rankings() +end + +function spawn_custom_level_objects() + -- only handle powerups if we're the server + if not network_is_server() then + return + end + + -- look for existing powerups + local obj = obj_get_first(OBJ_LIST_LEVEL) + while obj ~= nil do + local behaviorId = get_id_from_behavior(obj.behavior) + + if behaviorId == id_bhvItemBox then + -- find closest position to put it in + local objPos = { x = obj.oPosX, y = obj.oPosY, z = obj.oPosZ } + for i in pairs(gLevelData.powerups) do + local powPos = gLevelData.powerups[i].pos + local tempPos = { x = powPos.x, y = objPos.y, z = powPos.z } + local dist = vec3f_dist(objPos, tempPos) + if dist < 5 then + gLevelData.powerups[i].obj = obj + end + end + end + + -- iterate + obj = obj_get_next(obj) + end + + -- spawn missing powerups + for i in pairs(gLevelData.powerups) do + if gLevelData.powerups[i].obj == nil then + local pos = gLevelData.powerups[i].pos + gLevelData.powerups[i].obj = spawn_sync_object( + id_bhvItemBox, + E_MODEL_ITEM_BOX, + pos.x, pos.y, pos.z, + function (obj) + --obj.oMoveAngleYaw = m.faceAngle.y + end + ) + end + end +end + +function on_object_unload(obj) + -- react to powerups getting unloaded + for i = 0, (MAX_PLAYERS - 1) do + for j = 0, 2 do + if obj == gPowerups[i][j] then + gPowerups[i][j] = nil + end + end + end + + -- react to level objects getting unloaded + for i in pairs(gLevelData.powerups) do + if gLevelData.powerups[i].obj == obj then + gLevelData.powerups[i].obj = nil + end + end +end + +hook_event(HOOK_ON_LEVEL_INIT, on_level_init) +hook_event(HOOK_ON_OBJECT_UNLOAD, on_object_unload) diff --git a/mods/shell-rush/main.lua b/mods/shell-rush/main.lua new file mode 100644 index 000000000..a14090ae6 --- /dev/null +++ b/mods/shell-rush/main.lua @@ -0,0 +1,141 @@ +-- name: Shell Rush +-- description: Race around SM64 levels on shells.\n\nCollect powerups such as red shells, green shells, bananas, and mushrooms.\n\nOnly use a save that has lowered the water in the moat. +-- incompatible: gamemode + +DEBUG = false +UNST22 = true -- gotta work around unst 22 bugs :( + +gPowerups = {} + +gGlobalSyncTable.speed = 0.8 + +for i = 0, (MAX_PLAYERS - 1) do + gPowerups[i] = { + [0] = nil, + [1] = nil, + [2] = nil, + } + local s = gPlayerSyncTable[i] + s.waypoint = 1 + s.lap = 0 + s.powerup = {} + s.powerup[0] = POWERUP_NONE + s.powerup[1] = POWERUP_NONE + s.powerup[2] = POWERUP_NONE + s.finish = 0 + s.random = 0 +end + +------------------------------------------------------------------------------- + +function mario_update_local(m) + local s = gPlayerSyncTable[m.playerIndex] + + -- crouch to use shell + local pressZ = (m.controller.buttonPressed & Z_TRIG) ~= 0 + local blockShell = (m.action & (ACT_FLAG_INVULNERABLE | ACT_GROUP_CUTSCENE | ACT_FLAG_INTANGIBLE)) ~= 0 + local allowShell = (m.action & (ACT_FLAG_STATIONARY | ACT_FLAG_MOVING | ACT_FLAG_SWIMMING)) ~= 0 + if pressZ and not is_riding(m) and not blockShell and allowShell then + set_mario_action(m, ACT_RIDING_SHELL_GROUND, 0) + m.actionTimer = 0 + -- fix vanilla camera + if m.area.camera.mode == CAMERA_MODE_WATER_SURFACE then + set_camera_mode(m.area.camera, CAMERA_MODE_FREE_ROAM, 1) + end + end + + -- use powerups + if (m.controller.buttonPressed & Z_TRIG) ~= 0 and ((is_riding(m) and m.actionTimer > 1) or DEBUG) then + for i = 0, 2 do + if s.powerup[i] ~= POWERUP_NONE then + use_powerup(m, s.powerup[i]) + s.powerup[i] = POWERUP_NONE + break + end + end + end + + -- debug + if DEBUG then + if (m.controller.buttonPressed & D_JPAD) ~= 0 then + warp_to_level(LEVEL_CASTLE_GROUNDS, 1, 16) + print(m.pos.x, m.pos.y, m.pos.z, ' --- ', m.faceAngle.y) + + for i = 0, 2 do + gPlayerSyncTable[0].powerup[i] = POWERUP_BANANA + end + end + + if (m.controller.buttonPressed & L_JPAD) ~= 0 then + for i = 0, 2 do + gPlayerSyncTable[0].powerup[i] = POWERUP_GREEN_SHELL + end + end + + if (m.controller.buttonPressed & R_JPAD) ~= 0 then + for i = 0, 2 do + gPlayerSyncTable[0].powerup[i] = POWERUP_RED_SHELL + end + end + + if (m.controller.buttonPressed & U_JPAD) ~= 0 then + for i = 0, 2 do + gPlayerSyncTable[0].powerup[i] = POWERUP_MUSHROOM + end + end + end +end + +function on_speed_command(msg) + if not network_is_server() then + djui_chat_message_create('Only the server can change this setting!') + return true + end + if tonumber(msg) > 0 then + gGlobalSyncTable.speed = tonumber(msg) + return true + end + return false +end + +function mario_update(m) + local s = gPlayerSyncTable[m.playerIndex] + if not active_player(m) then + return + end + + if m.playerIndex == 0 then + mario_update_local(m) + end + + -- max health + m.health = 0x880 + + -- spawn powerups + for i = 0, 2 do + if gPowerups[m.playerIndex][i] == nil and s.powerup[i] ~= POWERUP_NONE then + if s.powerup[i] ~= POWERUP_NONE then + gPowerups[m.playerIndex][i] = spawn_powerup(m, s.powerup[i], i) + end + end + end +end + +function allow_pvp_attack(m1, m2) + return false +end + +function on_pause_exit(exitToCastle) + return false +end + +function on_update() + spawn_custom_level_objects() + race_update() +end + +hook_event(HOOK_UPDATE, on_update) +hook_event(HOOK_MARIO_UPDATE, mario_update) +hook_event(HOOK_ALLOW_PVP_ATTACK, allow_pvp_attack) +hook_event(HOOK_ON_PAUSE_EXIT, on_pause_exit) +hook_chat_command('speed', "[decimal number, default: 0.8]", on_speed_command) diff --git a/mods/shell-rush/powerup.lua b/mods/shell-rush/powerup.lua new file mode 100644 index 000000000..5b77e81a9 --- /dev/null +++ b/mods/shell-rush/powerup.lua @@ -0,0 +1,273 @@ + +POWERUP_NONE = 0 +POWERUP_MUSHROOM = 1 +POWERUP_GREEN_SHELL = 2 +POWERUP_RED_SHELL = 3 +POWERUP_BANANA = 4 +POWERUP_MAX = 5 + +define_custom_obj_fields({ + oPowerupType = 'u32', + oPowerupIndex = 'u32', +}) + +function bhv_powerup_init(obj) + obj.oFlags = OBJ_FLAG_UPDATE_GFX_POS_AND_ANGLE + obj.oOpacity = 255 + + local m = gMarioStates[obj.heldByPlayerIndex] + local mTheta = m.faceAngle.y + local mMag = 0 + if obj.oPowerupType == POWERUP_BANANA then + mMag = 100 * (obj.oPowerupIndex + 1) + end + obj.oPosX = m.pos.x - sins(mTheta) * mMag + obj.oPosY = m.pos.y + obj.oPosZ = m.pos.z - coss(mTheta) * mMag + +end + +function bhv_powerup_stale(obj) + if obj.oPowerupType == POWERUP_MUSHROOM then + obj.oAction = 1 + obj.oTimer = 0 + else + obj.activeFlags = ACTIVE_FLAG_DEACTIVATED + end +end + +function bhv_powerup_spin(obj) + local m = gMarioStates[obj.heldByPlayerIndex] + local theta = get_network_area_timer() / 8.0 + theta = theta + (math.pi * 2) * obj.oPowerupIndex / 3.0 + local mag = 120 + if obj.oAction == 1 then + local scalar = (1 - (obj.oTimer / 5)) + scalar = scalar * scalar + mag = mag * scalar + end + + local vec = { + x = m.pos.x + math.sin(theta) * mag, + y = m.pos.y + mag, + z = m.pos.z + math.cos(theta) * mag + } + + vec.y = find_floor_height(vec.x, vec.y, vec.z) + 50 + if vec.y < m.pos.y + 50 then vec.y = m.pos.y + 50 end + + return vec +end + +function bhv_powerup_trail(obj) + local prevObj = gMarioStates[obj.heldByPlayerIndex].marioObj + local s = gPlayerSyncTable[obj.heldByPlayerIndex] + + for i = 0, 2 do + if i >= obj.oPowerupIndex then + break + end + if s.powerup[i] == POWERUP_BANANA then + prevObj = gPowerups[obj.heldByPlayerIndex][i] + end + end + + local theta = math.atan2(prevObj.oPosX - obj.oPosX, prevObj.oPosZ - obj.oPosZ) + if theta ~= theta then theta = 0 end + local mag = 150 + + local newPos = { + x = prevObj.oPosX - math.sin(theta) * mag, + y = prevObj.oPosY, + z = prevObj.oPosZ - math.cos(theta) * mag + } + + local vec = { + x = (newPos.x + obj.oPosX) / 2, + y = (newPos.y + obj.oPosY * 7) / 8, + z = (newPos.z + obj.oPosZ) / 2 + } + + local floor = find_floor_height(vec.x, vec.y, vec.z) + 25 + if vec.y < floor then vec.y = floor end + + return vec +end + +function bhv_powerup_loop(obj) + local m = gMarioStates[obj.heldByPlayerIndex] + local s = gPlayerSyncTable[obj.heldByPlayerIndex] + local p = gPowerups[obj.heldByPlayerIndex][obj.oPowerupIndex] + if obj.oAction == 0 then + if s.powerup[obj.oPowerupIndex] ~= obj.oPowerupType or p ~= obj then + bhv_powerup_stale(obj) + end + end + + local vec = nil + if obj.oPowerupType == POWERUP_BANANA then + vec = bhv_powerup_trail(obj) + else + vec = bhv_powerup_spin(obj) + end + + + local theta = get_network_area_timer() / 8.0 + theta = theta + (math.pi * 2) * obj.oPowerupIndex / 3.0 + + obj_set_vec3f(obj, vec) + obj.oFaceAngleYaw = theta * 0x6000 + obj.oFaceAngleRoll = 0 + obj.oFaceAnglePitch = 0 + + if obj.oAction == 1 and obj.oTimer > 5 then + obj.activeFlags = ACTIVE_FLAG_DEACTIVATED + end + +end + +id_bhvPowerup = hook_behavior(nil, OBJ_LIST_LEVEL, true, bhv_powerup_init, bhv_powerup_loop) + +----------------- + +function use_powerup(m, powerup) + local s = gPlayerSyncTable[m.playerIndex] + local theta = m.faceAngle.y + if powerup == POWERUP_BANANA then + theta = theta + 0x8000 + end + local spawnPosition = { + x = m.pos.x + sins(theta) * 120, + y = m.pos.y, + z = m.pos.z + coss(theta) * 120, + } + + if powerup == POWERUP_MUSHROOM then + m.forwardVel = 300 + play_character_sound(m, CHAR_SOUND_YAHOO) + elseif powerup == POWERUP_GREEN_SHELL then + spawn_sync_object( + id_bhvWeaponShell, + E_MODEL_KOOPA_SHELL, + spawnPosition.x, spawnPosition.y, spawnPosition.z, + function (obj) + if UNST22 then + obj.oFlyGuyIdleTimer = 0 + obj.oFlyGuyOscTimer = gNetworkPlayers[0].globalIndex + obj.oFlyGuyUnusedJitter = 0 + else + obj.oWeaponShellType = 0 + obj.oWeaponShellGlobalOwner = gNetworkPlayers[0].globalIndex + obj.oWeaponShellDeactivate = 0 + end + obj.oMoveAngleYaw = m.faceAngle.y + obj.oForwardVel = 85 + obj.oInteractStatus = 0 + end + ) + elseif powerup == POWERUP_RED_SHELL then + spawn_sync_object( + id_bhvWeaponShell, + E_MODEL_RED_SHELL, + spawnPosition.x, spawnPosition.y, spawnPosition.z, + function (obj) + if UNST22 then + obj.oFlyGuyIdleTimer = 1 + obj.oFlyGuyOscTimer = gNetworkPlayers[0].globalIndex + obj.oFlyGuyUnusedJitter = 0 + else + obj.oWeaponShellType = 1 + obj.oWeaponShellGlobalOwner = gNetworkPlayers[0].globalIndex + obj.oWeaponShellDeactivate = 0 + end + obj.oMoveAngleYaw = m.faceAngle.y + obj.oForwardVel = 85 + obj.oInteractStatus = 0 + end + ) + elseif powerup == POWERUP_BANANA then + spawn_sync_object( + id_bhvWeaponBanana, + E_MODEL_BANANA, + spawnPosition.x, spawnPosition.y, spawnPosition.z, + function (obj) + obj.oMoveAngleYaw = m.faceAngle.y + obj.oWeaponBananaGlobalOwner = gNetworkPlayers[0].globalIndex + end + ) + end +end + +function spawn_powerup(m, powerup, index) + if not is_in_local_area(m) then + return nil + end + + if powerup == POWERUP_MUSHROOM then + return spawn_non_sync_object( + id_bhvPowerup, + E_MODEL_1UP, + 0, 0, 0, + function(obj) + obj.heldByPlayerIndex = m.playerIndex + obj.oPowerupType = powerup + obj.oPowerupIndex = index + obj_set_billboard(obj) + obj_scale(obj, 1) + end + ) + elseif powerup == POWERUP_GREEN_SHELL then + return spawn_non_sync_object( + id_bhvPowerup, + E_MODEL_KOOPA_SHELL, + 0, 0, 0, + function(obj) + obj.heldByPlayerIndex = m.playerIndex + obj.oPowerupType = powerup + obj.oPowerupIndex = index + obj_scale(obj, 0.75) + end + ) + elseif powerup == POWERUP_RED_SHELL then + return spawn_non_sync_object( + id_bhvPowerup, + E_MODEL_RED_SHELL, + 0, 0, 0, + function(obj) + obj.heldByPlayerIndex = m.playerIndex + obj.oPowerupType = powerup + obj.oPowerupIndex = index + obj_scale(obj, 0.75) + end + ) + elseif powerup == POWERUP_BANANA then + return spawn_non_sync_object( + id_bhvPowerup, + E_MODEL_BANANA, + 0, 0, 0, + function(obj) + obj.heldByPlayerIndex = m.playerIndex + obj.oPowerupType = powerup + obj.oPowerupIndex = index + obj_scale(obj, 0.75) + end + ) + end + return nil +end + +function select_powerup() + local m = gMarioStates[0] + local s = gPlayerSyncTable[0] + local pick = math.random(1, POWERUP_MAX-1) + local luck = math.random() < 0.33 + if luck then + s.powerup[0] = pick + s.powerup[1] = pick + s.powerup[2] = pick + else + s.powerup[0] = pick + s.powerup[1] = POWERUP_NONE + s.powerup[2] = POWERUP_NONE + end +end \ No newline at end of file diff --git a/mods/shell-rush/race-ring.lua b/mods/shell-rush/race-ring.lua new file mode 100644 index 000000000..4faaddeff --- /dev/null +++ b/mods/shell-rush/race-ring.lua @@ -0,0 +1,94 @@ + +define_custom_obj_fields({ + oWaypointIndex = 'u32', +}) + +function bhv_race_ring_init(obj) + obj.oFlags = OBJ_FLAG_UPDATE_GFX_POS_AND_ANGLE + obj_scale(obj, 4) + obj_set_billboard(obj) + obj.oOpacity = 200 +end + +function bhv_race_ring_inactive(obj) + obj_scale(obj, 1) + obj.oOpacity = 64 + + local waypoint = get_waypoint(obj.oWaypointIndex) + obj_set_vec3f(obj, waypoint) + + local cur = gPlayerSyncTable[0].waypoint + local nex = get_waypoint_index(cur + 1) + + if cur == obj.oWaypointIndex then + obj.oAction = 1 + cur_obj_unhide() + elseif nex == obj.oWaypointIndex then + cur_obj_unhide() + else + cur_obj_hide() + end +end + +function bhv_race_ring_active(obj) + local player = gMarioStates[0].marioObj + local distanceToPlayer = dist_between_objects(obj, player) + + cur_obj_unhide() + obj_scale(obj, 4) + obj.oOpacity = 200 + + local waypoint = get_waypoint(obj.oWaypointIndex) + obj_set_vec3f(obj, waypoint) + + if distanceToPlayer < 573 then + obj.oAction = 2 + + local s = gPlayerSyncTable[0] + if s.waypoint == obj.oWaypointIndex then + if s.waypoint == 1 then + race_increment_lap() + end + s.waypoint = get_waypoint_index(obj.oWaypointIndex + 1) + end + cur_obj_play_sound_2(SOUND_GENERAL_COIN) + end +end + +function bhv_race_ring_shrinking(obj) + local scalar = 1 - (obj.oTimer / (30 * 3)) + scalar = scalar * scalar + scalar = scalar * scalar + scalar = scalar * scalar + + cur_obj_unhide() + obj_scale(obj, 4 * scalar) + obj.oOpacity = 200 * scalar + + local waypoint = get_waypoint(obj.oWaypointIndex) + local nextWaypoint = get_waypoint(obj.oWaypointIndex + 1) + local wpos = vec3f_tween(nextWaypoint, waypoint, scalar) + obj_set_vec3f(obj, wpos) + + spawn_non_sync_object(id_bhvTriangleParticleSpawner, E_MODEL_NONE, + obj.oPosX + 300 * (math.random() - 0.5) * scalar, + obj.oPosY + 300 * (math.random() - 0.5) * scalar, + obj.oPosZ + 300 * (math.random() - 0.5) * scalar, + nil) + + if scalar <= 0.05 then + obj.oAction = 0 + end +end + +function bhv_race_ring_loop(obj) + if obj.oAction == 0 then + bhv_race_ring_inactive(obj) + elseif obj.oAction == 1 then + bhv_race_ring_active(obj) + elseif obj.oAction == 2 then + bhv_race_ring_shrinking(obj) + end +end + +id_bhvRaceRing = hook_behavior(nil, OBJ_LIST_LEVEL, true, bhv_race_ring_init, bhv_race_ring_loop) diff --git a/mods/shell-rush/race-shell.lua b/mods/shell-rush/race-shell.lua new file mode 100644 index 000000000..efb501508 --- /dev/null +++ b/mods/shell-rush/race-shell.lua @@ -0,0 +1,90 @@ + +function bhv_race_shell_set_hitbox(obj) + local hitbox = get_temp_object_hitbox() + hitbox.interactType = INTERACT_KOOPA_SHELL + hitbox.downOffset = 0 + hitbox.damageOrCoinValue = 4 + hitbox.health = 1 + hitbox.numLootCoins = 1 + hitbox.radius = 50 + hitbox.height = 50 + hitbox.hurtboxRadius = 50 + hitbox.hurtboxHeight = 50 + obj_set_hitbox(obj, hitbox) +end + +function bhv_race_shell_water_drop(obj) + spawn_non_sync_object(id_bhvObjectWaveTrail, E_MODEL_WAVE_TRAIL, obj.oPosX, obj.oPosY, obj.oPosZ, nil) + if gMarioStates[0].forwardVel > 10.0 then + local drop = spawn_non_sync_object(id_bhvWaterDroplet, E_MODEL_WHITE_PARTICLE_SMALL, obj.oPosX, obj.oPosY, obj.oPosZ, nil) + if drop ~= nil then + obj_scale(drop, 1.5) + drop.oVelY = math.random() * 30.0 + obj_translate_xz_random(drop, 110.0) + end + end +end + +function bhv_race_shell_flame_spawn(obj) + for i = 0, 1 do + spawn_non_sync_object(id_bhvKoopaShellFlame, E_MODEL_RED_FLAME, obj.oPosX, obj.oPosY, obj.oPosZ, nil) + end +end + +function bhv_race_shell_spawn_sparkles(obj, offset) + spawn_non_sync_object(id_bhvSparkleSpawn, E_MODEL_NONE, obj.oPosX, obj.oPosY + offset, obj.oPosZ, nil) +end + +function bhv_race_shell_init(obj) + obj.oFlags = OBJ_FLAG_UPDATE_GFX_POS_AND_ANGLE + + -- set_obj_physics + obj.oWallHitboxRadius = 30 + obj.oGravity = -400 / 100.0 + obj.oBounciness = -50 / 100.0 + obj.oDragStrength = 1000 / 100.0 + obj.oFriction = 1000 / 100.0 + obj.oBuoyancy = 200 / 100.0 +end + +function bhv_race_shell_loop(obj) + local np = gNetworkPlayers[obj.heldByPlayerIndex] + local m = gMarioStates[obj.heldByPlayerIndex] + local player = m.marioObj + + local riding = is_riding(m) + + if active_player(m) and riding then + cur_obj_unhide() + else + cur_obj_hide() + return + end + + --bhv_race_shell_set_hitbox(obj) + cur_obj_scale(1.0) + + obj_copy_pos(obj, player) + obj.oFaceAngleYaw = player.oFaceAngleYaw + + local surface = cur_obj_update_floor_height_and_get_floor() + local waterLevel = find_water_level(obj.oPosX, obj.oPosZ) + + if math.abs(waterLevel - obj.oPosY) < 10.0 then + bhv_race_shell_water_drop(obj) + + elseif 5.0 > math.abs(obj.oPosY - obj.oFloorHeight) then + if surface ~= nil and surface.type == 1 then + bhv_race_shell_flame_spawn(obj) + elseif m.forwardVel > 70 then + bhv_race_shell_spawn_sparkles(obj, 10.0) + end + elseif m.forwardVel > 70 then + bhv_race_shell_spawn_sparkles(obj, 10.0) + end + + obj.oFaceAngleYaw = player.oMoveAngleYaw + +end + +id_bhvRaceShell = hook_behavior(nil, OBJ_LIST_LEVEL, true, bhv_race_shell_init, bhv_race_shell_loop) diff --git a/mods/shell-rush/race.lua b/mods/shell-rush/race.lua new file mode 100644 index 000000000..8e653cbf8 --- /dev/null +++ b/mods/shell-rush/race.lua @@ -0,0 +1,232 @@ +gRankings = {} + +GAME_STATE_INACTIVE = 0 +GAME_STATE_RACE_COUNTDOWN = 1 +GAME_STATE_RACE_ACTIVE = 2 +GAME_STATE_RACE_FINISH = 3 + +gGlobalSyncTable.maxLaps = 5 +gGlobalSyncTable.gameState = GAME_STATE_INACTIVE +gGlobalSyncTable.gotoLevel = -1 +gGlobalSyncTable.raceStartTime = 0 +gGlobalSyncTable.raceQuitTime = 0 + +function race_start(level) + gGlobalSyncTable.gotoLevel = level + gGlobalSyncTable.gameState = GAME_STATE_RACE_COUNTDOWN + gGlobalSyncTable.raceStartTime = 0 + gGlobalSyncTable.raceQuitTime = 0 + + for i = 0, (MAX_PLAYERS - 1) do + local s = gPlayerSyncTable[i] + s.random = math.random() + s.finish = 0 + end +end + +function race_clear_rankings() + for k,v in pairs(gRankings) do gRankings[k]=nil end +end + +function race_increment_lap() + local s = gPlayerSyncTable[0] + s.lap = s.lap + 1 + if s.lap > gGlobalSyncTable.maxLaps then + s.lap = gGlobalSyncTable.maxLaps + if s.finish == 0 then + s.finish = get_network_area_timer() + play_race_fanfare() + end + end +end + +function race_update_rankings() + -- order players by score + ordered = {} + for i = 0, (MAX_PLAYERS - 1) do + local m = gMarioStates[i] + local s = gPlayerSyncTable[i] + if active_player(m) then + local score = 0 + if s.finish > 0 then + score = (gGlobalSyncTable.maxLaps + 2) * 10000 + (10000 / s.finish) + else + -- figure out distance score + local maxDist = vec3f_dist(get_waypoint(s.waypoint - 1), get_waypoint(s.waypoint)) + if maxDist == 0 then maxDist = 1 end + local dist = vec3f_dist(m.pos, get_waypoint(s.waypoint)) + local distScore = clamp(1 - (dist/maxDist), 0, 1) + + -- figure out entire score + local lastWaypoint = get_waypoint_index(s.waypoint - 1) + score = s.lap * 10000 + lastWaypoint * 100 + distScore + if s.lap == 0 then score = 0 end + end + if score > 0 then + table.insert(ordered, { score = score, m = m }) + end + end + end + + table.sort(ordered, function (v1, v2) return v1.score > v2.score end) + + -- clear rankings + race_clear_rankings() + + -- set rankings + for i,v in ipairs(ordered) do + table.insert(gRankings, v.m) + end +end + +function race_start_line() + local index = 0 + for i = 0, (MAX_PLAYERS - 1) do + local s = gPlayerSyncTable[i] + if network_is_server() then + s.finish = 0 + end + if active_player(gMarioStates[i]) and s.random < gPlayerSyncTable[0].random then + index = index + 1 + end + end + + local lineIndex = (index % 2) + 1 + local lineBackIndex = index - (index % 2) + + local m = gMarioStates[0] + local spawnLine = gLevelData.spawn[lineIndex] + local point = vec3f_tween(spawnLine.a, spawnLine.b, lineBackIndex / MAX_PLAYERS) + local waypoint = get_waypoint(1) + + m.pos.x = point.x + m.pos.y = point.y + m.pos.z = point.z + + m.marioObj.oIntangibleTimer = 5 + set_mario_action(m, ACT_RIDING_SHELL_GROUND, 0) + m.vel.x = 0 + m.vel.y = 0 + m.vel.z = 0 + m.slideVelX = 0 + m.slideVelZ = 0 + m.forwardVel = 0 + m.faceAngle.x = 0 + m.faceAngle.y = atan2s(waypoint.z - m.pos.z, waypoint.x - m.pos.x) + m.faceAngle.z = 0 +end + +function race_update() + -- automatically start race + if gGlobalSyncTable.gameState == GAME_STATE_INACTIVE and network_player_connected_count() > 1 then + race_start(LEVEL_SL) + end + + local np = gNetworkPlayers[0] + if gGlobalSyncTable.gotoLevel ~= -1 then + if np.currLevelNum ~= gGlobalSyncTable.gotoLevel then + if gGlobalSyncTable.gotoLevel == LEVEL_CASTLE_GROUNDS then + warp_to_castle(LEVEL_VCUTM) + else + warp_to_level(gGlobalSyncTable.gotoLevel, 1, 16) + end + end + end + + -- make sure this is a valid level + if gLevelData == gLevelDataTable[-1] then + return + end + + if gGlobalSyncTable.gameState == GAME_STATE_RACE_COUNTDOWN then + race_start_line() + race_clear_rankings() + if network_is_server() then + if gGlobalSyncTable.raceStartTime == 0 then + if np.currAreaSyncValid then + gGlobalSyncTable.raceStartTime = get_network_area_timer() + 30 * 5 + gGlobalSyncTable.raceQuitTime = 0 + end + elseif gGlobalSyncTable.raceStartTime > get_network_area_timer() + 30 * 5 then + gGlobalSyncTable.raceStartTime = get_network_area_timer() + 30 * 5 + gGlobalSyncTable.raceQuitTime = 0 + elseif gGlobalSyncTable.raceStartTime > 0 and get_network_area_timer() >= gGlobalSyncTable.raceStartTime then + gGlobalSyncTable.gameState = GAME_STATE_RACE_ACTIVE + end + end + + elseif gGlobalSyncTable.gameState == GAME_STATE_RACE_ACTIVE then + race_update_rankings() + if network_is_server() then + if gGlobalSyncTable.raceQuitTime == 0 then + -- check for race finish + local foundFinisher = false + for i = 0, (MAX_PLAYERS - 1) do + local m = gMarioStates[i] + local s = gPlayerSyncTable[i] + if active_player(m) and s.finish > 0 then + foundFinisher = true + end + end + if foundFinisher then + -- set a timer until the race is finished + gGlobalSyncTable.raceQuitTime = get_network_area_timer() + 30 * 20 + end + elseif gGlobalSyncTable.raceQuitTime > 0 and get_network_area_timer() > gGlobalSyncTable.raceQuitTime then + -- race is finished, start a new one + if gLevelData == gLevelDataTable[LEVEL_CASTLE_GROUNDS] then + race_start(LEVEL_BOB) + elseif gLevelData == gLevelDataTable[LEVEL_SL] then + race_start(LEVEL_CASTLE_GROUNDS) + elseif gLevelData == gLevelDataTable[LEVEL_BOB] then + race_start(LEVEL_SL) + end + end + end + end +end + +function on_race_command(msg) + if not network_is_server() then + djui_chat_message_create('Only the server can change this setting!') + return true + end + if msg == 'BOB' then + race_start(LEVEL_BOB) + return true + end + if msg == 'SL' then + race_start(LEVEL_SL) + return true + end + if msg == 'CG' then + race_start(LEVEL_CASTLE_GROUNDS) + return true + end + return false +end + +function on_laps_command(msg) + if not network_is_server() then + djui_chat_message_create('Only the server can change this setting!') + return true + end + if tonumber(msg) > 0 then + gGlobalSyncTable.maxLaps = math.floor(tonumber(msg)) + return true + end + return false +end + +function on_game_state_changed(tag, oldVal, newVal) + local m = gMarioStates[0] + if oldVal ~= newVal then + if newVal == GAME_STATE_RACE_ACTIVE then + play_sound(SOUND_GENERAL_RACE_GUN_SHOT, m.marioObj.header.gfx.cameraToObject) + end + end +end + +hook_chat_command('race', "[CG|SL|BOB]", on_race_command) +hook_chat_command('laps', "[number]", on_laps_command) +hook_on_sync_table_change(gGlobalSyncTable, 'gameState', i, on_game_state_changed) diff --git a/mods/shell-rush/utils.lua b/mods/shell-rush/utils.lua new file mode 100644 index 000000000..e4ac3fdae --- /dev/null +++ b/mods/shell-rush/utils.lua @@ -0,0 +1,128 @@ +function clamp(v, min, max) + if v < min then return min end + if v > max then return max end + return v +end + +function convert_s16(num) + local min = -32768 + local max = 32767 + while (num < min) do + num = max + (num - min) + end + while (num > max) do + num = min + (num - max) + end + return num +end + +function active_player(m) + local np = gNetworkPlayers[m.playerIndex] + if m.playerIndex == 0 then + return true + end + if not np.connected then + return false + end + return is_player_active(m) +end + +function vec3f_tween(a, b, mult) + if mult < 0 then mult = 0 end + if mult > 1 then mult = 1 end + local amult = 1 - mult + return { + x = a.x * amult + b.x * mult, + y = a.y * amult + b.y * mult, + z = a.z * amult + b.z * mult, + } +end + +function obj_set_vec3f(obj, v) + if obj == nil or v == nil then return end + obj.oPosX = v.x + obj.oPosY = v.y + obj.oPosZ = v.z +end + +function get_last_waypoint_index() + local index = 1 + while gLevelData.waypoints[index + 1] ~= nil do + index = index + 1 + end + return index +end + +function get_waypoint(i) + return gLevelData.waypoints[get_waypoint_index(i)] +end + +function get_waypoint_index(i) + local lastIndex = get_last_waypoint_index() + i = ((i - 1) % lastIndex) + 1 + + if gLevelData.waypoints[i] == nil then + return 1 + end + + return i +end + +function vec3f_non_nan(v) + if v.x ~= v.x then v.x = 0 end + if v.y ~= v.y then v.y = 0 end + if v.z ~= v.z then v.z = 0 end +end + +function vec3f_angle_between(a, b) + return math.acos(vec3f_dot(a, b) / (vec3f_length(a) * vec3f_length(b))) +end + +function is_riding(m) + return (m.action == ACT_RIDING_SHELL_GROUND) or (m.action == ACT_RIDING_SHELL_FALL) or (m.action == ACT_RIDING_SHELL_JUMP) +end + +function is_in_local_area(m) + local np1 = gNetworkPlayers[0] + local np2 = gNetworkPlayers[m.playerIndex] + return (np1.currCourseNum == np2.currCourseNum) and (np1.currLevelNum == np2.currLevelNum) and (np1.currAreaIndex == np2.currAreaIndex) and (np1.currActNum == np2.currActNum) +end + +function spawn_mist(obj) + local spi = obj_get_temp_spawn_particles_info(E_MODEL_MIST) + if spi == nil then + return nil + end + + spi.behParam = 3 + spi.count = 5 + spi.offsetY = 25 + spi.forwardVelBase = 6 + spi.forwardVelRange = -6 + spi.velYBase = 6 + spi.velYRange = -6 + spi.gravity = 0 + spi.dragStrength = 5 + spi.sizeBase = 10 + spi.sizeRange = 16 + + cur_obj_spawn_particles(spi) +end + +function spawn_triangles(obj) + spawn_non_sync_object(id_bhvTriangleParticleSpawner, E_MODEL_NONE, + obj.oPosX, + obj.oPosY, + obj.oPosZ, + nil) +end + +function spawn_sparkles(obj) + for i = 0, 5 do + spawn_non_sync_object(id_bhvSparkleSpawn, E_MODEL_NONE, + obj.oPosX + math.random(-100, 100), + obj.oPosY + math.random(-100, 100), + obj.oPosZ + math.random(-100, 100), + nil) + end +end \ No newline at end of file diff --git a/mods/shell-rush/weapon-banana.lua b/mods/shell-rush/weapon-banana.lua new file mode 100644 index 000000000..d6687e508 --- /dev/null +++ b/mods/shell-rush/weapon-banana.lua @@ -0,0 +1,68 @@ +local bananaTimeout = 30 * 60 * 1 --- one minute + +define_custom_obj_fields({ + oWeaponBananaGlobalOwner = 'u32', +}) + +function bhv_weapon_banana_init(obj) + obj.oGraphYOffset = 0 + obj.oFlags = OBJ_FLAG_UPDATE_GFX_POS_AND_ANGLE + obj.oOpacity = 255 + obj.oVelY = 0 + obj_scale(obj, 0.9) + + obj.oPosY = obj.oPosY + 50 + + local hitbox = get_temp_object_hitbox() + hitbox.interactType = INTERACT_DAMAGE + hitbox.downOffset = 0 + hitbox.damageOrCoinValue = 4 + hitbox.health = 1 + hitbox.numLootCoins = 1 + hitbox.radius = 100 + hitbox.height = 70 + hitbox.hurtboxRadius = 100 + hitbox.hurtboxHeight = 70 + obj_set_hitbox(obj, hitbox) + + cur_obj_play_sound_2(SOUND_GENERAL_BOING1) + + network_init_object(obj, true, { + 'oWeaponBananaGlobalOwner' + }) +end + +function bhv_weapon_banana_destroy(obj) + obj.activeFlags = ACTIVE_FLAG_DEACTIVATED + spawn_triangles(obj) + cur_obj_play_sound_2(SOUND_GENERAL_FLAME_OUT) +end + +function bhv_weapon_banana_loop(obj) + local floor = cur_obj_update_floor_height_and_get_floor() + if floor ~= nil then + if obj.oPosY < obj.oFloorHeight + 10 then + obj.oVelY = 0 + obj.oPosY = obj.oFloorHeight + 5 + obj_orient_graph(obj, floor.normal.x, floor.normal.y, floor.normal.z) + else + obj.oVelY = obj.oVelY - 3 + obj.oPosY = obj.oPosY + obj.oVelY + if obj.oPosY < obj.oFloorHeight + 10 then + spawn_mist(obj) + end + end + end + + -- prevent interactions for the first 5 frames + if obj.oTimer < 5 then + obj.oInteractStatus = 0 + end + + if cur_obj_check_interacted() ~= 0 or obj.oTimer > bananaTimeout then + bhv_weapon_banana_destroy(obj) + end +end + +id_bhvWeaponBanana = hook_behavior(nil, OBJ_LIST_PUSHABLE, true, bhv_weapon_banana_init, bhv_weapon_banana_loop) +E_MODEL_BANANA = smlua_model_util_get_id("banana_geo") diff --git a/mods/shell-rush/weapon-shell.lua b/mods/shell-rush/weapon-shell.lua new file mode 100644 index 000000000..861b33ae3 --- /dev/null +++ b/mods/shell-rush/weapon-shell.lua @@ -0,0 +1,229 @@ +local shellTimeout = 30 * 30 --- 30 seconds + +define_custom_obj_fields({ + oWeaponShellType = 'u32', + oWeaponShellGlobalOwner = 'u32', + oWeaponShellDeactivate = 'u32', +}) + +function bhv_weapon_shell_init(obj) + obj.oFlags = OBJ_FLAG_UPDATE_GFX_POS_AND_ANGLE + obj.oOpacity = 255 + obj.oVelY = 0 + obj.oTimer = 0 + if UNST22 then + obj.oFlyGuyUnusedJitter = 0 + else + obj.oWeaponShellDeactivate = 0 + end + obj_scale(obj, 0.9) + + local hitbox = get_temp_object_hitbox() + hitbox.interactType = INTERACT_DAMAGE + hitbox.downOffset = 0 + hitbox.damageOrCoinValue = 4 + hitbox.health = 1 + hitbox.numLootCoins = 1 + hitbox.radius = 100 + hitbox.height = 70 + hitbox.hurtboxRadius = 100 + hitbox.hurtboxHeight = 70 + obj_set_hitbox(obj, hitbox) + + cur_obj_play_sound_2(SOUND_GENERAL_BIG_POUND) + + if UNST22 then + network_init_object(obj, false, { 'oFlyGuyUnusedJitter' }) + else + network_init_object(obj, false, { 'oWeaponShellDeactivate' }) + end +end + +function bhv_weapon_shell_destroy(obj) + obj.activeFlags = ACTIVE_FLAG_DEACTIVATED + spawn_triangles(obj) + cur_obj_play_sound_2(SOUND_GENERAL_BREAK_BOX) + + if UNST22 then + if obj.oFlyGuyUnusedJitter == 0 then + obj.oFlyGuyUnusedJitter = 1 + network_send_object(obj, true) + end + obj.oFlyGuyUnusedJitter = 1 + else + if obj.oWeaponShellDeactivate == 0 then + obj.oWeaponShellDeactivate = 1 + network_send_object(obj, true) + end + obj.oWeaponShellDeactivate = 1 + end +end + +function bhv_weapon_shell_move(obj) + local hit = false + local stepHeight = 100 + local savedX = obj.oPosX + local savedY = obj.oPosY + local savedZ = obj.oPosZ + + -- figure out direction + local v = { + x = sins(obj.oMoveAngleYaw) * obj.oForwardVel, + y = 0, + z = coss(obj.oMoveAngleYaw) * obj.oForwardVel, + } + + -- cast ray + local info = collision_find_surface_on_ray( + obj.oPosX, obj.oPosY + stepHeight, obj.oPosZ, + v.x, v.y, v.z) + + -- move the shell + obj.oPosX = info.hitPos.x + obj.oPosY = info.hitPos.y + obj.oPosZ = info.hitPos.z + + -- figure out how far from floor + local floorHeight = find_floor_height(obj.oPosX, obj.oPosY, obj.oPosZ) + + if floorHeight <= -10000.0 then + -- we're OOB + obj.oPosX = savedX + obj.oPosY = savedY + obj.oPosZ = savedZ + obj.oMoveAngleYaw = obj.oMoveAngleYaw + 0x4000 + obj.oFaceAngleYaw = obj.oMoveAngleYaw + return true + elseif math.abs(floorHeight - obj.oPosY) > stepHeight * 1.25 then + -- we're in the air + obj.oPosY = obj.oPosY - stepHeight + obj.oVelY = obj.oVelY - 5 + obj.oPosY = obj.oPosY + obj.oVelY + if obj.oPosY < floorHeight then + obj.oPosY = floorHeight + end + else + -- we're on the ground + obj.oPosY = floorHeight + obj.oVelY = 0 + end + + -- figure out if we hit wall + if info.surface ~= nil and math.abs(info.surface.normal.y) < 0.2 then + -- projection + local parallel = vec3f_project(v, info.surface.normal) + local perpendicular = { x = v.x - parallel.x, y = v.y - parallel.y, z = v.z - parallel.z } + + -- reflect velocity along normal + local reflect = { + x = perpendicular.x - parallel.x, + y = perpendicular.y - parallel.y, + z = perpendicular.z - parallel.z + } + + obj.oPosX = savedX + obj.oPosY = savedY + obj.oPosZ = savedZ + obj.oMoveAngleYaw = atan2s(reflect.z, reflect.x) + obj.oFaceAngleYaw = obj.oMoveAngleYaw + hit = true + spawn_mist(obj) + cur_obj_play_sound_2(SOUND_GENERAL_BOX_LANDING) + end + + -- orient to floor + info = collision_find_surface_on_ray( + obj.oPosX, obj.oPosY + stepHeight, obj.oPosZ, + 0, stepHeight * -1.5, 0) + if info.surface ~= nil then + obj_orient_graph(obj, info.surface.normal.x, info.surface.normal.y, info.surface.normal.z) + end + + -- attach to water + local waterLevel = find_water_level(obj.oPosX, obj.oPosZ) + if obj.oPosY < waterLevel then obj.oPosY = waterLevel end + + return hit +end + +function bhv_weapon_shell_red_loop(obj, hit) + if hit then + bhv_weapon_shell_destroy(obj) + return + end + + -- find target + local target = nil + local targetDist = 1000 + local v = { + x = sins(obj.oMoveAngleYaw) * obj.oForwardVel, + y = 0, + z = coss(obj.oMoveAngleYaw) * obj.oForwardVel, + } + for i = 0, (MAX_PLAYERS - 1) do + local m = gMarioStates[i] + local np = gNetworkPlayers[i] + local isntGlobalOwner = (np.globalIndex ~= obj.oWeaponShellGlobalOwner) + if UNST22 then isntGlobalOwner = (np.globalIndex ~= obj.oFlyGuyOscTimer) end + + if active_player(m) and isntGlobalOwner then + local dist = dist_between_objects(m.marioObj, obj) + local diff = { x = m.marioObj.oPosX - obj.oPosX, y = 0, z = m.marioObj.oPosZ - obj.oPosZ } + local angleBetween = vec3f_angle_between(diff, v) + if dist < targetDist and angleBetween < 100 then + target = m + targetDist = dist + end + end + end + + -- no target found :( + if target == nil then + return + end + + -- turn toward target + local turnSpeed = 0x300 + local targetYaw = atan2s(target.pos.z - obj.oPosZ, target.pos.x - obj.oPosX) + obj.oMoveAngleYaw = targetYaw - approach_s32(convert_s16(targetYaw - obj.oMoveAngleYaw), 0, turnSpeed, turnSpeed) + obj.oFaceAngleYaw = obj.oMoveAngleYaw +end + +function bhv_weapon_shell_loop(obj) + local hit = bhv_weapon_shell_move(obj) + if UNST22 then + if obj.oFlyGuyIdleTimer == 1 then + bhv_weapon_shell_red_loop(obj, hit) + end + if obj.oFlyGuyUnusedJitter ~= 0 then + bhv_weapon_shell_destroy(obj) + return + end + else + if obj.oWeaponShellType == 1 then + bhv_weapon_shell_red_loop(obj, hit) + end + if obj.oWeaponShellDeactivate ~= 0 then + bhv_weapon_shell_destroy(obj) + return + end + end + + -- prevent interactions for the first 5 frames + if obj.oTimer < 5 then + obj.oInteractStatus = 0 + end + + if cur_obj_check_interacted() ~= 0 then + bhv_weapon_shell_destroy(obj) + return + end + + if obj.oTimer > shellTimeout then + bhv_weapon_shell_destroy(obj) + return + end +end + +id_bhvWeaponShell = hook_behavior(nil, OBJ_LIST_PUSHABLE, true, bhv_weapon_shell_init, bhv_weapon_shell_loop) +E_MODEL_RED_SHELL = smlua_model_util_get_id("red_shell_geo")