Files
ReSkate/Extension/UI/Overlay/skate_menu_skater.cpp
T
Fourth Studio b999f37e7b Board Break: wear and tear, rail snaps, break and body sounds, settings
Boards wear: hard landings, hits onto rails and ledges, grinding and
slams wear the board of the deck in the game, kept per deck. From a fifth
worn it steers lazier and its trucks go soft (the game's own physics
tuning, outside multiplayer sessions only); a worn board breaks from less
and, worn out, can give way under the rider. A board that broke is a new
one once it is whole again, and the game's own board wear is cleared with
it.

A boardslide, lipslide or darkslide that comes down too hard onto a rail
can snap the board, and the rider goes down with it: No Bail can now be
asked for a wipeout, which goes in as a cause before the selector picks.

Sounds play through XAudio2: a crack by the size of the break, metal when
the board broke on metal (the material comes from the game's contact
effects; which are metal is marked in play or listed by a pack), and the
body's hits in a bail by how hard they land.

The settings are a card on the overlay's Skater page, two rows in Mod
Options and console verbs, kept in BoardBreak.settings. A deck may name
itself and set its wheels wider apart in a deck.ini; Old School and Pig
join the decks the feature knows.
2026-10-15 19:22:21 +02:00

313 lines
21 KiB
C++

#include "skate_menu_internal.h"
#include <algorithm>
#include <array>
#include <cmath>
// The SKATER page.
namespace dingosdk::overlay::menu {
namespace {
// A slider row with a trailing button (e.g. "Default") after a field() label.
float trailing_width(const char* label) {
return ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2;
}
}
void camera_controls(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& debug = model.debug;
begin_card(menu, "freecam", "FREECAM");
bool flight = debug.free_camera;
if (toggle_row(menu, "Freecam", "Detach the camera and explore.", flight,
debug.available && debug.camera_available && callbacks.queue_debug))
debug_request(menu, callbacks, {DebugAction::set_free_camera, flight});
bool freecam_controller = model.bindings.freecam_controller;
if (toggle_row(menu, "Block Input(Enable Controller Support)", "Block player input and use the controller for the Freecam.", freecam_controller,
model.bindings.available && callbacks.queue_debug)) {
std::array<char, 512> result{};
callbacks.queue_console_command(callbacks.user, freecam_controller ? "freecam_controller true" : "freecam_controller false", result.data(), result.size());
}
field(menu, "Teleport to Freecam");
ImGui::BeginDisabled(!debug.free_camera || !debug.camera_position_valid || !callbacks.queue_console_command);
if (ImGui::Button("Teleport", ImVec2(-1, 0))) {
std::array<char, 512> result{};
callbacks.queue_console_command(callbacks.user, "tp_to_freecam", result.data(), result.size());
}
ImGui::EndDisabled();
{
field(menu, "Field of view");
const bool custom = debug.free_camera_fov > 0;
int free_fov = static_cast<int>(std::lround(custom ? debug.free_camera_fov : debug.camera_fov));
const float reset = trailing_width("Default");
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - reset - ImGui::GetStyle().ItemSpacing.x);
ImGui::BeginDisabled(!callbacks.queue_debug);
if (ImGui::SliderInt("##free-camera-fov", &free_fov, 40, 120, custom ? "%d degrees" : "%d degrees (game)",
ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_free_camera_fov, false, static_cast<float>(free_fov)});
ImGui::SameLine();
ImGui::BeginDisabled(!custom);
if (ImGui::Button("Default", ImVec2(reset, 0)))
debug_request(menu, callbacks, {DebugAction::set_free_camera_fov, false, 0.0f});
ImGui::EndDisabled();
ImGui::EndDisabled();
}
field(menu, "Flight speed", "Also used by Noclip.");
ImGui::BeginDisabled((!debug.free_camera && !debug.noclip) || !debug.camera_available || !callbacks.queue_debug);
constexpr std::array<float, 7> speeds{0.6f, 3, 5, 15, 60, 300, 1500};
constexpr std::array<const char*, 7> labels{"Precise", "Slow", "Cruise", "Default", "Fast", "Travel", "Maximum"};
int selected = 3;
for (int i = 0; i < static_cast<int>(speeds.size()); ++i)
if (std::abs(debug.camera_speed - speeds[i]) < .01f) selected = i;
if (ImGui::SliderInt("##flight-speed", &selected, 0, 6, labels[selected], ImGuiSliderFlags_NoInput))
debug_request(menu, callbacks, {DebugAction::set_camera_speed, false, speeds[selected]});
ImGui::EndDisabled();
if (debug.free_camera) note("Close the menu to fly. WASD / Q E move; hold the right mouse button to look.");
if (!debug.camera_available) warn(debug.camera_unavailable.c_str());
end_card();
begin_card(menu, "first-person", "FIRST PERSON");
bool first_person = debug.first_person;
if (toggle_row(menu, "First person", "See the world through the skater's eyes.", first_person,
debug.available && debug.camera_available && callbacks.queue_debug))
debug_request(menu, callbacks, {DebugAction::set_first_person, first_person});
field(menu, "Field of view");
int fov = static_cast<int>(std::lround(debug.first_person_fov > 0 ? debug.first_person_fov : debug.camera_fov));
ImGui::BeginDisabled(!debug.first_person || !callbacks.queue_debug);
if (ImGui::SliderInt("##first-person-fov", &fov, 40, 120, "%d degrees", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_first_person_fov, false, static_cast<float>(fov)});
ImGui::EndDisabled();
{
auto settings = debug.first_person_arm;
const bool can_edit = debug.available && callbacks.queue_debug;
if (toggle_row(menu, "True first person",
"Keep the horizon level and the view steady; the head's heading is still followed.", settings.stabilize, can_edit))
debug_request(menu, callbacks, {DebugAction::set_first_person_stabilize, settings.stabilize});
ImGui::BeginDisabled(!settings.stabilize || !can_edit);
const auto percent = [&](const char* label, const char* id, float value, DebugAction action) {
field(menu, label);
if (ImGui::SliderFloat(id, &value, 0, dingosdk::first_person::strength_limit, "%.0f%%", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {action, false, value});
};
percent("Smoothing", "##fp-smoothing", settings.smoothing, DebugAction::set_first_person_smoothing);
percent("Head nod (up / down)", "##fp-head-pitch", settings.head_pitch, DebugAction::set_first_person_head_pitch);
percent("Head tilt (horizon roll)", "##fp-head-roll", settings.head_roll, DebugAction::set_first_person_head_roll);
percent("Head bob", "##fp-bob", settings.bob, DebugAction::set_first_person_bob);
if (toggle_row(menu, "Follow flips", "Turn the view with the skater during flips and bails.", settings.follow_flips,
can_edit && settings.stabilize))
debug_request(menu, callbacks, {DebugAction::set_first_person_follow_flips, settings.follow_flips});
ImGui::EndDisabled();
if (toggle_row(menu, "Third person on foot",
"Use the game's camera while walking; first person comes back when you get on the board.",
settings.board_only, can_edit))
debug_request(menu, callbacks, {DebugAction::set_first_person_board_only, settings.board_only});
if (toggle_row(menu, "First person in the camera cycle",
"Clicking the right stick (R3) on the board goes high camera, low camera, first person, then high again. "
"Needs Camera height control on in the game's settings (Gameplay > Advanced Camera).",
settings.camera_cycle, can_edit))
debug_request(menu, callbacks, {DebugAction::set_first_person_camera_cycle, settings.camera_cycle});
note("Nod, tilt and bob are the share of the head's own movement kept. Use Spring arm > Pitch to look "
"further down at the board.");
}
if (ImGui::TreeNode("Spring arm")) {
namespace fp = dingosdk::first_person;
auto settings = debug.first_person_arm;
if (toggle_row(menu, "Spring", "Let the camera lag behind and soften animated head movement.", settings.enabled,
debug.available && callbacks.queue_debug))
debug_request(menu, callbacks, {DebugAction::set_first_person_spring, settings.enabled});
ImGui::BeginDisabled(!debug.available || !callbacks.queue_debug);
const auto slider = [&](const char* label, float value, float low, float high, const char* format, DebugAction action) {
field(menu, label);
ImGui::PushID(label);
if (ImGui::SliderFloat("##arm-value", &value, low, high, format, ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {action, false, value});
ImGui::PopID();
};
note("Position offset, metres from the head");
slider("Right", settings.offset[0], -fp::offset_limit, fp::offset_limit, "%.3f", DebugAction::set_first_person_offset_x);
slider("Up", settings.offset[1], -fp::offset_limit, fp::offset_limit, "%.3f", DebugAction::set_first_person_offset_y);
slider("Forward", settings.offset[2], -fp::offset_limit, fp::offset_limit, "%.3f", DebugAction::set_first_person_offset_z);
note("Rotation offset, degrees");
slider("Pitch", settings.rotation[0], -fp::rotation_limit, fp::rotation_limit, "%.1f", DebugAction::set_first_person_pitch);
slider("Yaw", settings.rotation[1], -fp::rotation_limit, fp::rotation_limit, "%.1f", DebugAction::set_first_person_yaw);
slider("Roll", settings.rotation[2], -fp::rotation_limit, fp::rotation_limit, "%.1f", DebugAction::set_first_person_roll);
note("Smoothing strength");
slider("Moving up", settings.up, 0, fp::strength_limit, "%.0f%%", DebugAction::set_first_person_spring_up);
slider("Moving down", settings.down, 0, fp::strength_limit, "%.0f%%", DebugAction::set_first_person_spring_down);
slider("Moving left", settings.left, 0, fp::strength_limit, "%.0f%%", DebugAction::set_first_person_spring_left);
slider("Moving right", settings.right, 0, fp::strength_limit, "%.0f%%", DebugAction::set_first_person_spring_right);
if (ImGui::Button("Reset arm settings", ImVec2(-FLT_MIN, 0)))
debug_request(menu, callbacks, {DebugAction::reset_first_person_arm});
ImGui::EndDisabled();
note("Higher smoothing follows more slowly; 0% follows directly. Offsets work with Spring off. "
"Depth and roll use the average strength.");
ImGui::TreePop();
}
end_card();
}
// Board Break's settings (Extension/BoardBreak/board_break_feel.h): how a board breaks, wears and
// sounds. Each is a console verb, kept with the profile.
void board_break_settings_card(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& bb = model.board_break;
if (!bb.available) return;
const bool console = bb.enabled && callbacks.queue_console_command;
std::string subtitle = !bb.has_deck ? "No Board Break deck is in the game"
: "Your board: " + std::to_string(static_cast<int>(bb.worn * 100.0f + 0.5f)) + "% worn (" + bb.condition + ")";
begin_card(menu, "board-break-settings", "BOARD BREAK SETTINGS", subtitle.c_str());
if (!bb.enabled) note("Switch Board Break on to change these.");
const auto flag = [&](const char* label, const char* hint, bool on, const char* verb) {
bool value = on;
if (toggle_row(menu, label, hint, value, console)) send_console(menu, callbacks, std::string("boardbreak ") + verb + (value ? " 1" : " 0"));
};
flag("Always break", "Every slam breaks the board, and every slide that comes down onto a rail: for replay clips.", bb.always, "always");
flag("Rail snaps", "A boardslide or darkslide that comes down too hard onto a rail or ledge can snap the board, and you go down with it.",
bb.rails, "rails");
flag("Wear and tear", "Every hit wears your board. A worn board turns lazier, wobbles and breaks more easily; worn out, it can give way. "
"Turn on Board wear (Mod Options) to see the scratches.", bb.wear, "wear");
flag("Break sounds", "The crack of a break, and metal when it breaks on metal.", bb.crack_sounds, "cracks");
flag("Body hit sounds", "Each hit of your body in a bail, as hard as it hits.", bb.body_sounds, "bodysounds");
// Sent once a slider is let go; the dragged value shows until the setting comes back.
const auto percent = [&](const char* id, const char* label, const char* hint, int value, int low, int high, int step, const char* verb) {
static std::array<int, 3> edited{-1, -1, -1};
static std::array<double, 3> edited_at{};
const auto slot = std::string_view(id) == "bb-sensitivity" ? 0u : std::string_view(id) == "bb-joined" ? 1u : 2u;
int shown = edited[slot] >= 0 ? edited[slot] : value;
field(menu, label, hint);
ImGui::BeginDisabled(!console);
if (ImGui::SliderInt((std::string("##") + id).c_str(), &shown, low, high, "%d%%", ImGuiSliderFlags_AlwaysClamp)) {
edited[slot] = std::clamp((shown + step / 2) / step * step, low, high);
edited_at[slot] = ImGui::GetTime();
}
if (ImGui::IsItemDeactivatedAfterEdit() && edited[slot] >= 0) {
send_console(menu, callbacks, std::string("boardbreak ") + verb + " " + std::to_string(edited[slot]));
edited_at[slot] = ImGui::GetTime();
}
if (edited[slot] >= 0 && !ImGui::IsItemActive() && (value == edited[slot] || ImGui::GetTime() - edited_at[slot] > 2.0)) edited[slot] = -1;
ImGui::EndDisabled();
};
percent("bb-sensitivity", "Break sensitivity", "How easily a board breaks. 200% takes half the force, 50% twice as much.", bb.sensitivity, 10, 400, 10,
"sensitivity");
percent("bb-joined", "Grip tape holds", "How many breaks the grip tape holds together instead of a piece coming off.", bb.joined, 0, 100, 5, "joined");
percent("bb-wear-rate", "Wear speed", "How fast boards wear. 0% keeps a board as it is.", bb.wear_rate, 0, 500, 10, "wearrate");
ImGui::BeginDisabled(!console || !bb.has_deck);
if (ImGui::Button("New board", ImVec2(-FLT_MIN, 0))) send_console(menu, callbacks, "boardbreak worn 0");
ImGui::EndDisabled();
end_card();
}
void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& debug = model.debug;
begin_card(menu, "movement", "FLIGHT & BAILS");
bool noclip = debug.noclip;
if (toggle_row(menu, "Noclip", "Fly with the normal player camera. Includes No Bail; uses the Freecam flight speed.", noclip,
(debug.noclip_available || debug.noclip) && callbacks.queue_debug))
debug_request(menu, callbacks, {DebugAction::set_noclip, noclip});
if (!debug.noclip_available && !debug.noclip) note(debug.noclip_unavailable.c_str());
bool no_bail = debug.no_bail;
const char* bail_help = debug.noclip && debug.no_bail_active
? "Protection is automatic during noclip. Enable this to keep it when flight ends."
: debug.no_bail && !debug.no_bail_active
? "Enabled; waiting for an active local skater."
: "Prevent new wipeouts. Recover from any current bail before enabling.";
if (toggle_row(menu, "No Bail", bail_help, no_bail,
(debug.no_bail_available || debug.no_bail) && callbacks.queue_debug))
debug_request(menu, callbacks, {DebugAction::set_no_bail, no_bail});
bool hall_of_meat = model.hall_of_meat.enabled;
// No Bail (and noclip, which includes it) stops the bails Hall of Meat scores.
const bool bails_off = debug.no_bail || debug.no_bail_active;
const char* meat_help = hall_of_meat && bails_off
? "On, but No Bail is stopping your bails, so nothing shows. Turn No Bail (and noclip) off to use it."
: bails_off
? "Your bails show the bones they hurt and score a Meat card. Needs No Bail (and noclip) off."
: "Your bails show the bones they hurt and score a Meat card. A break slows the game in single player.";
if (toggle_row(menu, "Hall of Meat", meat_help, hall_of_meat, model.hall_of_meat.available && callbacks.queue_console_command))
send_console(menu, callbacks, hall_of_meat ? "hallofmeat 1" : "hallofmeat 0");
bool road_rash = model.road_rash.enabled;
const bool rash_console = model.road_rash.available && callbacks.queue_console_command;
if (toggle_row(menu, "Road Rash", "Bails leave marks that build up: dust, scrapes, bruises, wounds. Needs the Road Rash item pack.",
road_rash, rash_console))
send_console(menu, callbacks, road_rash ? "roadrash 1" : "roadrash 0");
bool rash_blood = model.road_rash.blood;
if (toggle_row(menu, "Road Rash blood", "Fresh blood on the worst wounds. Off, they are dry and scabbed.", rash_blood,
rash_console && model.road_rash.enabled))
send_console(menu, callbacks, rash_blood ? "roadrashblood 1" : "roadrashblood 0");
bool board_break = model.board_break.enabled;
if (toggle_row(menu, "Board Break", model.board_break.has_deck
? "A hard slam can snap your board: at the nose, in the middle or at the tail. Whole again once you pick it up."
: "A hard slam can snap your board. Needs one of the Board Break deck packs.",
board_break, model.board_break.available && callbacks.queue_console_command))
send_console(menu, callbacks, board_break ? "boardbreak 1" : "boardbreak 0");
end_card();
board_break_deck_card(menu, model, callbacks);
board_break_settings_card(menu, model, callbacks);
// Grind exit control (Extension/Skater/grind_exit_control.h).
const auto& grind = model.grind_exit;
const bool grind_console = grind.available && callbacks.queue_console_command;
begin_card(menu, "grind-exits", "GRIND EXITS", "Choose the side you pop out of a grind using LT/RT");
bool grind_on = grind.enabled;
if (toggle_row(menu, "Grind exit triggers", "Press LT or RT while grinding to pick the side you pop out to.",
grind_on, grind_console))
send_console(menu, callbacks, grind_on ? "grindexit exits on" : "grindexit exits off");
field(menu, "Input Mode");
int grind_sides = grind.toe_heel ? 0 : 1;
if (choice(menu, "grind-sides", grind_sides, {"Toe / Heel", "Left / Right"}, grind_console, 0.0f,
{"Direction is based on your skater's toe/heel side", "Direction is based on left or right of your direction of travel"}))
send_console(menu, callbacks, grind_sides == 0 ? "grindexit sides toeheel" : "grindexit sides travel");
if (grind.toe_heel) {
field(menu, "Your stance");
int grind_stance = grind.stance == 2 ? 1 : grind.stance == 1 ? 0 : -1;
if (choice(menu, "grind-stance", grind_stance, {"Regular", "Goofy"}, grind_console))
send_console(menu, callbacks, grind_stance == 1 ? "grindexit stance goofy" : "grindexit stance regular");
if (grind.stance == 0) warn("Pick your stance. Until then LT / RT are left / right of travel.");
}
{
// Sent once the slider is let go (not every frame of a drag); the dragged value shows
// until the setting comes back, in steps of 5.
static int edited = -1;
static double edited_at = 0;
int strength = edited >= 0 ? edited : grind.strength;
field(menu, "Pop-out strength", "How far to the side a trigger exit pops. 100% is the full exit; lower it for rails next to a wall.");
ImGui::BeginDisabled(!grind_console);
if (ImGui::SliderInt("##grind-strength", &strength, 10, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp)) {
edited = std::clamp((strength + 2) / 5 * 5, 10, 100);
edited_at = ImGui::GetTime();
}
if (ImGui::IsItemDeactivatedAfterEdit() && edited >= 0) {
send_console(menu, callbacks, "grindexit strength " + std::to_string(edited));
edited_at = ImGui::GetTime();
}
if (edited >= 0 && !ImGui::IsItemActive() && (grind.strength == edited || ImGui::GetTime() - edited_at > 2.0)) edited = -1;
ImGui::EndDisabled();
}
end_card();
begin_card(menu, "boosts", "BOOSTS", "Buttons are set in Settings > Controls");
ImGui::BeginDisabled(!callbacks.queue_debug);
field(menu, "Forward boost");
float forward_velocity = debug.forward_velocity_speed;
if (ImGui::SliderFloat("##forward-velocity", &forward_velocity, 1.0f, 300.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_forward_velocity_speed, false, forward_velocity});
field(menu, "Up boost");
float up_velocity = debug.up_velocity_speed;
if (ImGui::SliderFloat("##up-velocity", &up_velocity, 1.0f, 25.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_up_velocity_speed, false, up_velocity});
field(menu, "Off-board up boost");
float offboard_up_velocity = debug.offboard_up_velocity_speed;
if (ImGui::SliderFloat("##offboard-up-velocity", &offboard_up_velocity, 1.0f, 25.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_offboard_up_velocity_speed, false, offboard_up_velocity});
ImGui::EndDisabled();
note("Controller: left stick moves, right stick looks, RT / LT rise and fall, click the left stick to boost.");
note("Keyboard: WASD / Q E, Shift to boost. Close the menu to fly.");
end_card();
}
void skater_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
category_tabs(menu, menu.skater_tab, {"CAMERA", "GAMEPLAY"}, "skater-tabs");
ImGui::PushID(menu.skater_tab);
ImGui::BeginChild("skater-tab", ImVec2(0, page_body_height(menu)));
if (menu.skater_tab == 0) camera_controls(menu, model, callbacks);
else movement_controls(menu, model, callbacks);
ImGui::EndChild();
ImGui::PopID();
}
}