mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Add persistent native difficulty selection and refresh mode-dependent runtime settings
This commit is contained in:
1 parent
a21f78aa52
commit
c818285b4d
20 files changed
+339
-30
No files matched your search
@@ -6,6 +6,7 @@ pub(crate) struct Config {
|
||||
pub asset_root: PathBuf,
|
||||
pub verification_capture: Option<PathBuf>,
|
||||
pub map: Option<skate_data::skate_map::SkateMap>,
|
||||
pub difficulty: crate::difficulty::Difficulty,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -14,7 +15,9 @@ impl Config {
|
||||
asset_root: PathBuf::from("assets"),
|
||||
verification_capture: None,
|
||||
map: None,
|
||||
difficulty: crate::difficulty::Difficulty::default(),
|
||||
};
|
||||
let mut difficulty_override = None;
|
||||
let mut args = std::env::args_os().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.to_str() {
|
||||
@@ -25,6 +28,10 @@ impl Config {
|
||||
let path = PathBuf::from(args.next().ok_or("--map requires a .skate file")?);
|
||||
config.map = Some(skate_data::skate_map::SkateMap::load(&path)?);
|
||||
}
|
||||
Some("--difficulty") => {
|
||||
let value = args.next().ok_or("--difficulty requires easy, normal or hardcore")?;
|
||||
difficulty_override = Some(crate::difficulty::Difficulty::parse(&value.to_string_lossy())?);
|
||||
}
|
||||
Some("--verify") => {
|
||||
config.verification_capture = Some(
|
||||
args.next()
|
||||
@@ -34,7 +41,7 @@ impl Config {
|
||||
}
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Unknown argument {arg:?}. Usage: skate-game [--assets DIRECTORY] [--map MAP.skate] [--verify CAPTURE.png]"
|
||||
"Unknown argument {arg:?}. Usage: skate-game [--assets DIRECTORY] [--map MAP.skate] [--difficulty easy|normal|hardcore] [--verify CAPTURE.png]"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -43,6 +50,10 @@ impl Config {
|
||||
.asset_root
|
||||
.canonicalize()
|
||||
.map_err(|e| format!("Asset root {}: {e}", config.asset_root.display()))?;
|
||||
config.difficulty = match difficulty_override {
|
||||
Some(mode) => mode,
|
||||
None => crate::difficulty::Difficulty::load(&config.asset_root)?,
|
||||
};
|
||||
if let Some(path) = &mut config.verification_capture {
|
||||
if path.extension().and_then(|x| x.to_str()) != Some("png") {
|
||||
return Err("--verify output must be a PNG file".into());
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Host preference selects the native physics_mode collection, never a multiplier.
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub(crate) const NATIVE_MODES: [&str; 5] = ["easy", "normal", "hardcore", "motorized", "test"];
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[repr(u32)]
|
||||
pub(crate) enum Difficulty {
|
||||
#[default]
|
||||
Easy = 0,
|
||||
Normal = 1,
|
||||
Hardcore = 2,
|
||||
}
|
||||
impl Difficulty {
|
||||
pub const ALL: [Self; 3] = [Self::Easy, Self::Normal, Self::Hardcore];
|
||||
pub fn key(self) -> &'static str { NATIVE_MODES[self as usize] }
|
||||
pub fn label(self) -> &'static str {
|
||||
match self { Self::Easy => "Easy", Self::Normal => "Normal", Self::Hardcore => "Hardcore" }
|
||||
}
|
||||
pub fn parse(value: &str) -> Result<Self, String> {
|
||||
Self::ALL.into_iter().find(|d| d.key().eq_ignore_ascii_case(value))
|
||||
.ok_or_else(|| format!("Unknown difficulty {value:?}; expected easy, normal or hardcore"))
|
||||
}
|
||||
pub fn path(root: &Path) -> PathBuf {
|
||||
root.parent().unwrap_or(root).join("settings/gameplay.json")
|
||||
}
|
||||
pub fn load(root: &Path) -> Result<Self, String> {
|
||||
match std::fs::read(Self::path(root)) {
|
||||
Ok(bytes) => serde_json::from_slice::<Saved>(&bytes).map(|s| s.difficulty)
|
||||
.map_err(|e| format!("Invalid saved gameplay settings: {e}")),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
|
||||
Err(e) => Err(format!("Cannot read gameplay settings: {e}")),
|
||||
}
|
||||
}
|
||||
pub fn save(self, root: &Path) -> Result<(), String> {
|
||||
let path = Self::path(root);
|
||||
std::fs::create_dir_all(path.parent().unwrap()).map_err(|e| e.to_string())?;
|
||||
std::fs::write(path, serde_json::to_vec_pretty(&Saved { difficulty: self }).map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Saved { difficulty: Difficulty }
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn retail_indices_and_saved_names_agree() {
|
||||
for (index, d) in Difficulty::ALL.into_iter().enumerate() {
|
||||
assert_eq!(d as usize, index);
|
||||
assert_eq!(Difficulty::parse(d.label()).unwrap(), d);
|
||||
let bytes = serde_json::to_vec(&Saved { difficulty: d }).unwrap();
|
||||
assert_eq!(serde_json::from_slice::<Saved>(&bytes).unwrap().difficulty, d);
|
||||
}
|
||||
assert!(Difficulty::parse("motorized").is_err());
|
||||
assert!(serde_json::from_str::<Saved>(r#"{"difficulty":"made-up"}"#).is_err());
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use bevy::{
|
||||
window::{PresentMode, PrimaryWindow},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::difficulty::Difficulty;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
@@ -79,6 +80,7 @@ pub(crate) struct Menu {
|
||||
settings: GraphicsSettings,
|
||||
path: PathBuf,
|
||||
supported_msaa: Vec<u32>,
|
||||
difficulty: Difficulty,
|
||||
status: String,
|
||||
}
|
||||
pub(crate) fn gameplay_active(menu: Option<Res<Menu>>) -> bool {
|
||||
@@ -190,14 +192,14 @@ fn setup(
|
||||
display: Display::None, width:percent(100), height:percent(100), align_items:AlignItems::Center,
|
||||
justify_content:JustifyContent::Center, position_type:PositionType::Absolute, ..default()
|
||||
}, BackgroundColor(Color::srgba(0.015,0.025,0.04,0.88)))).with_children(|root| {
|
||||
root.spawn((Node { width:px(560),max_width:percent(95),padding:UiRect::all(px(28)),flex_direction:FlexDirection::Column,row_gap:px(10),border_radius:BorderRadius::all(px(12)),..default() },
|
||||
root.spawn((Node { width:px(560),max_width:percent(95),padding:UiRect::all(px(18)),flex_direction:FlexDirection::Column,row_gap:px(6),border_radius:BorderRadius::all(px(12)),..default() },
|
||||
BackgroundColor(Color::srgb(0.035,0.055,0.08)))).with_children(|panel| {
|
||||
panel.spawn((Text::new("PAUSED"),TextFont {font_size:32.,..default()},TextColor(Color::WHITE)));
|
||||
panel.spawn((Text::new("GRAPHICS"),TextFont {font_size:16.,..default()},TextColor(Color::srgb(0.4,0.85,0.85))));
|
||||
for i in 0..7 {
|
||||
panel.spawn((Button, MenuRow(i), Node {width:percent(100),min_height:px(48),padding:UiRect::all(px(12)),align_items:AlignItems::Center,border_radius:BorderRadius::all(px(5)),..default()},
|
||||
panel.spawn((Text::new("GAMEPLAY & GRAPHICS"),TextFont {font_size:16.,..default()},TextColor(Color::srgb(0.4,0.85,0.85))));
|
||||
for i in 0..8 {
|
||||
panel.spawn((Button, MenuRow(i), Node {width:percent(100),min_height:px(36),padding:UiRect::all(px(8)),align_items:AlignItems::Center,border_radius:BorderRadius::all(px(5)),..default()},
|
||||
BackgroundColor(Color::srgb(0.08,0.11,0.15)))).with_children(|row| {
|
||||
row.spawn((MenuLabel(i),Text::new(""),TextFont {font_size:20.,..default()},TextColor(Color::WHITE)));
|
||||
row.spawn((MenuLabel(i),Text::new(""),TextFont {font_size:18.,..default()},TextColor(Color::WHITE)));
|
||||
});
|
||||
}
|
||||
panel.spawn((StatusLabel,Text::new(""),TextFont {font_size:15.,..default()},TextColor(Color::srgb(0.65,0.75,0.8))));
|
||||
@@ -211,6 +213,7 @@ fn setup(
|
||||
settings,
|
||||
path,
|
||||
supported_msaa,
|
||||
difficulty: config.difficulty,
|
||||
status: String::new(),
|
||||
});
|
||||
}
|
||||
@@ -227,6 +230,8 @@ fn cycle<T: PartialEq + Copy>(values: &[T], value: T, direction: i32) -> T {
|
||||
values[(index + direction).rem_euclid(values.len() as i32) as usize]
|
||||
}
|
||||
fn interact(
|
||||
config: Res<crate::config::Config>,
|
||||
mut physics: ResMut<crate::physics::GamePhysics>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
mut menu: ResMut<Menu>,
|
||||
mut time: ResMut<Time<Virtual>>,
|
||||
@@ -239,10 +244,10 @@ fn interact(
|
||||
let mut action = None;
|
||||
if menu.open {
|
||||
if keys.just_pressed(KeyCode::ArrowUp) {
|
||||
menu.selected = (menu.selected + 6) % 7;
|
||||
menu.selected = (menu.selected + 7) % 8;
|
||||
}
|
||||
if keys.just_pressed(KeyCode::ArrowDown) {
|
||||
menu.selected = (menu.selected + 1) % 7;
|
||||
menu.selected = (menu.selected + 1) % 8;
|
||||
}
|
||||
if keys.just_pressed(KeyCode::ArrowLeft) {
|
||||
action = Some((menu.selected, -1));
|
||||
@@ -274,8 +279,16 @@ fn interact(
|
||||
}
|
||||
3 => menu.settings.fps = cycle(LIMITS, menu.settings.fps, direction),
|
||||
4 => menu.settings.occlusion = !menu.settings.occlusion,
|
||||
5 => menu.open = false,
|
||||
6 => {
|
||||
5 => {
|
||||
menu.difficulty = cycle(&Difficulty::ALL, menu.difficulty, direction);
|
||||
physics.set_difficulty(menu.difficulty);
|
||||
menu.status = match menu.difficulty.save(&config.asset_root) {
|
||||
Ok(()) => "Difficulty saved".into(),
|
||||
Err(e) => format!("Applied, but could not save: {e}"),
|
||||
};
|
||||
}
|
||||
6 => menu.open = false,
|
||||
7 => {
|
||||
exit.write(AppExit::Success);
|
||||
}
|
||||
_ => {}
|
||||
@@ -390,7 +403,8 @@ fn labels(
|
||||
}
|
||||
),
|
||||
4 => format!("Occlusion culling {}", if s.occlusion { "On" } else { "Off" }),
|
||||
5 => "Resume".into(),
|
||||
5 => format!("Difficulty {}", menu.difficulty.label()),
|
||||
6 => "Resume".into(),
|
||||
_ => "Quit game".into(),
|
||||
};
|
||||
}
|
||||
@@ -428,7 +442,7 @@ mod tests {
|
||||
.insert_resource(images)
|
||||
.insert_resource(Menu {
|
||||
open: false, selected: 0, settings: GraphicsSettings::default(),
|
||||
path: PathBuf::new(), supported_msaa: vec![1, 2, 4, 8], status: String::new(),
|
||||
difficulty: Difficulty::Easy, path: PathBuf::new(), supported_msaa: vec![1, 2, 4, 8], status: String::new(),
|
||||
})
|
||||
.add_systems(Update, apply);
|
||||
app.world_mut().spawn((Window::default(), PrimaryWindow));
|
||||
|
||||
@@ -4,6 +4,7 @@ mod app;
|
||||
mod assets;
|
||||
mod camera;
|
||||
mod config;
|
||||
mod difficulty;
|
||||
mod graph_host;
|
||||
mod graph_runtime;
|
||||
mod input;
|
||||
@@ -48,14 +49,15 @@ fn main() -> bevy::app::AppExit {
|
||||
return bevy::app::AppExit::error();
|
||||
}
|
||||
}
|
||||
let physics = match physics::GamePhysics::load_with_map(&config.asset_root, config.map.as_ref()) {
|
||||
eprintln!("SKATE_DIFFICULTY mode={} native_index={}", config.difficulty.key(), config.difficulty as u32);
|
||||
let physics = match physics::GamePhysics::load_with_difficulty(&config.asset_root, config.map.as_ref(), config.difficulty) {
|
||||
Ok(physics) => physics,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
return bevy::app::AppExit::error();
|
||||
}
|
||||
};
|
||||
let skater = match physics::SkaterRuntime::load(&config.asset_root, &graphs, &physics, "easy") {
|
||||
let skater = match physics::SkaterRuntime::load(&config.asset_root, &graphs, &physics, config.difficulty.key()) {
|
||||
Ok(skater) => skater,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
|
||||
@@ -86,6 +86,14 @@ pub(crate) struct GamePhysics {
|
||||
}
|
||||
|
||||
impl GamePhysics {
|
||||
pub(crate) fn set_difficulty(&mut self, difficulty: crate::difficulty::Difficulty) {
|
||||
// Actor publication carries this selector into the next physical packet.
|
||||
// Keep the board, active trick, equipment preferences and controller history.
|
||||
self.animation_profile.physics_mode = difficulty as u32;
|
||||
}
|
||||
|
||||
pub(crate) fn difficulty_index(&self) -> u32 { self.animation_profile.physics_mode }
|
||||
|
||||
pub(crate) fn world(&self) -> &BoardWorld {
|
||||
&self.world
|
||||
}
|
||||
@@ -99,9 +107,13 @@ impl GamePhysics {
|
||||
}
|
||||
|
||||
pub fn load_with_map(asset_root: &std::path::Path, map: Option<&skate_data::skate_map::SkateMap>) -> Result<Self, String> {
|
||||
Self::load_with_difficulty(asset_root, map, crate::difficulty::Difficulty::Easy)
|
||||
}
|
||||
|
||||
pub fn load_with_difficulty(asset_root: &std::path::Path, map: Option<&skate_data::skate_map::SkateMap>, difficulty: crate::difficulty::Difficulty) -> Result<Self, String> {
|
||||
let data = Collections::load(asset_root)?;
|
||||
let settings = PhysicsSettings::load(&data)?;
|
||||
let animation_profile = animation_phase::AnimationProfile::load(&data, "easy")?;
|
||||
let animation_profile = animation_phase::AnimationProfile::load(&data, difficulty.key())?;
|
||||
let mut spawn = RetailAffineTransform {
|
||||
translation: Vector3::new(
|
||||
0.0,
|
||||
@@ -314,6 +326,10 @@ mod air_tests;
|
||||
#[path = "tests/wipeout_playback.rs"]
|
||||
mod wipeout_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/difficulty.rs"]
|
||||
mod difficulty_tests;
|
||||
|
||||
fn present(
|
||||
physics: Res<GamePhysics>,
|
||||
skater: Res<SkaterRuntime>,
|
||||
|
||||
@@ -29,6 +29,7 @@ pub(crate) struct AnimationInput {
|
||||
bone_names: Vec<AttributeName>,
|
||||
right_toe: usize,
|
||||
settings: FinalizationInput,
|
||||
height_overrides: [bool; 5],
|
||||
}
|
||||
|
||||
impl AnimationInput {
|
||||
@@ -39,6 +40,9 @@ impl AnimationInput {
|
||||
.position(|n| n.eq_ignore_ascii_case("RightToeBase"))
|
||||
.ok_or("Stock skeleton is missing RightToeBase")?;
|
||||
Ok(Self {
|
||||
height_overrides: crate::difficulty::NATIVE_MODES.map(|key|
|
||||
data.boolean("physics_mode", key, "JumpHeightOverrideEnabled"))
|
||||
.into_iter().collect::<Result<Vec<_>, _>>()?.try_into().unwrap(),
|
||||
fields: ScalarAttributeInputs::reset(0, 0),
|
||||
extra: ExtendedAttributes::reset(0.0),
|
||||
contacts: ContactEventState {
|
||||
@@ -84,6 +88,12 @@ impl AnimationInput {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_physics_mode(&mut self, mode: u32) -> Result<(), String> {
|
||||
self.settings.allow_height_override = *self.height_overrides.get(mode as usize)
|
||||
.ok_or_else(|| format!("Invalid animation physics mode {mode}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ProcessedPhysIn Reset precedes physical/packet flag publication. The
|
||||
/// caller then publishes those flags before dispatching the current pose.
|
||||
pub fn reset_processed(&mut self) {
|
||||
|
||||
@@ -60,6 +60,10 @@ pub(super) fn advance(
|
||||
actions,
|
||||
input_available,
|
||||
)?;
|
||||
skater.ground_settings = skater.ground_profiles.select(
|
||||
skater.player_input.processed.state_variant_index_2528,
|
||||
skater.player_input.processed.surface_mode_2540,
|
||||
)?;
|
||||
if teleported {
|
||||
skater.player_state.reset_for_teleport();
|
||||
skater.centre_of_mass_filter.reset();
|
||||
|
||||
@@ -22,6 +22,7 @@ mod settings;
|
||||
pub(crate) use launch::{GroundLaunchInfo, GroundLaunchPhysical};
|
||||
pub(crate) use services::{GroundControllers, GroundPhysicalFrame};
|
||||
pub(crate) use settings::GroundSettings;
|
||||
pub(crate) use settings::GroundProfiles;
|
||||
use skate_core::{
|
||||
math::Vector3,
|
||||
physics::{
|
||||
|
||||
@@ -20,6 +20,24 @@ use skate_core::{
|
||||
},
|
||||
};
|
||||
use skate_data::collections::Collections;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Immutable stock tables selected by the processed packet, including its
|
||||
/// difficulty-dependent SurfacePhysics normalization. No per-tick parsing.
|
||||
pub(crate) struct GroundProfiles(Vec<Vec<Arc<GroundSettings>>>);
|
||||
impl GroundProfiles {
|
||||
pub fn load(data: &Collections) -> Result<Self, String> {
|
||||
crate::difficulty::NATIVE_MODES.into_iter().map(|mode| {
|
||||
(1..=5).map(|surface| {
|
||||
GroundSettings::load(data, mode, super::surface_key(surface)?).map(Arc::new)
|
||||
}).collect::<Result<Vec<_>, String>>()
|
||||
}).collect::<Result<Vec<_>, String>>().map(Self)
|
||||
}
|
||||
pub fn select(&self, mode: u32, surface: u32) -> Result<Arc<GroundSettings>, String> {
|
||||
surface.checked_sub(1).and_then(|s| self.0.get(mode as usize)?.get(s as usize))
|
||||
.cloned().ok_or_else(|| format!("Invalid processed physics mode/surface {mode}/{surface}"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct GroundSettings {
|
||||
steering: SteeringSettings,
|
||||
|
||||
@@ -30,12 +30,12 @@ pub(crate) struct GroundState {
|
||||
pub heading_previous: f32,
|
||||
pub pumping_settings: GroundPumping,
|
||||
output_settings: GroundOutputSettings,
|
||||
auto_push_enabled: bool,
|
||||
auto_push_enabled: [bool; 5],
|
||||
pub entered: bool,
|
||||
pub(super) entry_settings: super::entry::EntrySettings,
|
||||
}
|
||||
impl GroundState {
|
||||
pub fn load(data: &Collections, mode: &str, human_player: bool) -> Result<Self, String> {
|
||||
pub fn load(data: &Collections, _mode: &str, human_player: bool) -> Result<Self, String> {
|
||||
let push = |field| data.float("physics_push", "default", field);
|
||||
Ok(Self {
|
||||
world_grab: super::world_grab::GroundWorldGrabState::default(),
|
||||
@@ -65,7 +65,9 @@ impl GroundState {
|
||||
mode_speed_threshold_0: push("Hash_501D5581043D7D3C")?,
|
||||
},
|
||||
entry_settings: super::entry::EntrySettings::load(data)?,
|
||||
auto_push_enabled: data.boolean("physics_mode", mode, "AutoPushEnabled")?,
|
||||
auto_push_enabled: crate::difficulty::NATIVE_MODES.map(|mode|
|
||||
data.boolean("physics_mode", mode, "AutoPushEnabled"))
|
||||
.into_iter().collect::<Result<Vec<_>, _>>()?.try_into().unwrap(),
|
||||
entered: false,
|
||||
})
|
||||
}
|
||||
@@ -86,7 +88,7 @@ impl GroundState {
|
||||
scalar_2720: animation.fields.balance,
|
||||
flags_2476: p.flags_2476,
|
||||
flags_2484: p.flags_2484,
|
||||
selected_mode_flag_109: self.auto_push_enabled,
|
||||
selected_mode_flag_109: self.auto_push_enabled[p.state_variant_index_2528 as usize],
|
||||
},
|
||||
self.output_settings,
|
||||
)
|
||||
|
||||
@@ -177,6 +177,7 @@ impl PlayerInputCallbacks for Callbacks<'_, '_> {
|
||||
physical: &mut PhysicalPlayerInput,
|
||||
processed: &mut ProcessedPhysicsInput,
|
||||
) -> Result<(), String> {
|
||||
self.animation_input.select_physics_mode(processed.state_variant_index_2528)?;
|
||||
self.skeleton_input.process_data(
|
||||
board,
|
||||
toolkit,
|
||||
|
||||
@@ -65,7 +65,8 @@ pub(crate) struct SkaterRuntime {
|
||||
pub skeleton_input: SkeletonInputRuntime,
|
||||
pub ground: GroundState,
|
||||
pub ground_runtime: GroundRuntime,
|
||||
pub ground_settings: GroundSettings,
|
||||
pub ground_settings: std::sync::Arc<GroundSettings>,
|
||||
pub ground_profiles: super::ground_runtime::GroundProfiles,
|
||||
pub ground_lifecycle: super::ground_phase::GroundLifecycle,
|
||||
pub animation_input: AnimationInput,
|
||||
pub animation_feedback: AnimationFeedback,
|
||||
@@ -84,6 +85,9 @@ impl SkaterRuntime {
|
||||
mode: &str,
|
||||
) -> Result<Self, String> {
|
||||
let data = Collections::load(asset_root)?;
|
||||
let ground_profiles = super::ground_runtime::GroundProfiles::load(&data)?;
|
||||
let mode_index = crate::difficulty::NATIVE_MODES.iter().position(|m| *m == mode)
|
||||
.ok_or_else(|| format!("Invalid skater mode {mode}"))? as u32;
|
||||
// The host's current character is a custom skater with no pro selector
|
||||
// or equipped physical hat. These are profile choices, not force values.
|
||||
let mut animation = SkaterAnimation::load(asset_root, &data, graphs, b"")?;
|
||||
@@ -231,7 +235,8 @@ impl SkaterRuntime {
|
||||
},
|
||||
ground: GroundState::load(&data, mode, true)?,
|
||||
ground_runtime: GroundRuntime::load(&data)?,
|
||||
ground_settings: GroundSettings::load(&data, mode, "smooth")?,
|
||||
ground_settings: ground_profiles.select(mode_index, 1)?,
|
||||
ground_profiles,
|
||||
ground_lifecycle: super::ground_phase::GroundLifecycle::new(),
|
||||
animation_input,
|
||||
animation_feedback: AnimationFeedback::load(&data)?,
|
||||
|
||||
@@ -21,7 +21,7 @@ fn roll_off(air_stick: Option<i16>) {
|
||||
let root = std::path::Path::new(&root);
|
||||
let assets = skate_data::GameAssets::load(root).unwrap();
|
||||
let graphs = crate::graph_runtime::StockGraphs::load(root, &assets).unwrap();
|
||||
let mut physics = GamePhysics::load(root).unwrap();
|
||||
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
|
||||
// Keep this air fixture's open edge now that the production level has a
|
||||
// downhill ramp. The dedicated ramp regression uses the unmodified world.
|
||||
physics.world = BoardWorld::new(physics.world.triangles().iter().copied()
|
||||
@@ -144,7 +144,7 @@ fn stock_starting_ramp_reaches_floor_without_air() {
|
||||
let root = std::path::Path::new(&root);
|
||||
let assets = skate_data::GameAssets::load(root).unwrap();
|
||||
let graphs = crate::graph_runtime::StockGraphs::load(root, &assets).unwrap();
|
||||
let mut physics = GamePhysics::load(root).unwrap();
|
||||
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
|
||||
let mut skater = SkaterRuntime::load(root, &graphs, &physics, "normal").unwrap();
|
||||
let mut controls = PlayerControls::default();
|
||||
let mut input = crate::input::ControllerInput::default();
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//! Integration with the extracted stock data and the real simulation pipeline.
|
||||
use super::*;
|
||||
use crate::difficulty::Difficulty;
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires private stock animation banks and collections"]
|
||||
fn stock_difficulties_switch_without_resetting_the_player() {
|
||||
let root = std::env::var_os("SKATE3_ASSET_ROOT").expect("SKATE3_ASSET_ROOT");
|
||||
let root = std::path::Path::new(&root);
|
||||
let data = Collections::load(root).unwrap();
|
||||
let assets = skate_data::GameAssets::load(root).unwrap();
|
||||
let graphs = crate::graph_runtime::StockGraphs::load(root, &assets).unwrap();
|
||||
let mut physics = GamePhysics::load_with_difficulty(root, None, Difficulty::Easy).unwrap();
|
||||
let mut skater = SkaterRuntime::load(root, &graphs, &physics, "easy").unwrap();
|
||||
let mut controls = PlayerControls::load(root).unwrap();
|
||||
let mut input = crate::input::ControllerInput::default();
|
||||
let mut camera = crate::camera::CameraRuntime::load(root).unwrap();
|
||||
|
||||
// Check the complete mode/surface product before exercising live switches.
|
||||
for mode in Difficulty::ALL {
|
||||
for surface in 1..=5 {
|
||||
let settings = skater.ground_profiles.select(mode as u32, surface).unwrap();
|
||||
let key = ground_runtime::surface_key(surface).unwrap();
|
||||
assert_eq!(settings.wheel_material.dynamic_friction.to_bits(),
|
||||
data.float("physics_surfaces", key, "WheelDynamicFriction").unwrap().to_bits());
|
||||
assert_eq!(settings.board().propulsion.mode_speed_changes.map(f32::to_bits),
|
||||
["MaxPushDVStart", "MaxPushDVEnd"].map(|f| data.float("physics_mode", mode.key(), f).unwrap().to_bits()));
|
||||
assert_eq!(settings.wobble_amplitude.to_bits(),
|
||||
data.float("physics_mode", mode.key(), "Hash_5B57F2CCCCEEF430").unwrap().to_bits());
|
||||
}
|
||||
}
|
||||
assert!(skater.ground_profiles.select(5, 1).is_err());
|
||||
assert!(skater.ground_profiles.select(0, 0).is_err());
|
||||
|
||||
for mode in [Difficulty::Easy, Difficulty::Normal, Difficulty::Hardcore, Difficulty::Easy] {
|
||||
let before = physics.board.bodies().map(|b| (b.rates.position, b.rates.linear_velocity));
|
||||
let ticks = physics.ticks;
|
||||
let generation = skater.pose_generation;
|
||||
physics.set_difficulty(mode);
|
||||
assert_eq!(physics.ticks, ticks);
|
||||
assert_eq!(skater.pose_generation, generation);
|
||||
assert_eq!(physics.board.bodies().map(|b| (b.rates.position, b.rates.linear_velocity)), before);
|
||||
for _ in 0..60 {
|
||||
input.sample_raw_for_test(skate_core::input::xbox::XboxState {
|
||||
buttons: 0, triggers: [0; 2], left: [0; 2], right: [0; 2],
|
||||
});
|
||||
let mut actions = input.player_actions();
|
||||
controls.update(&mut actions, physics.settings.step.simulation.time_step,
|
||||
physics.settings.input_magnitude_threshold, skater.player_input.physical.scoring.capabilities_204);
|
||||
controls.publish_gestures(physics.difficulty_index(), skater.player_input.physical.state.state_16);
|
||||
frame::advance(&mut physics, &mut skater, &mut controls, &graphs, &mut actions, true, &mut camera).unwrap();
|
||||
assert_eq!(skater.player_input.processed.state_variant_index_2528, mode as u32);
|
||||
let selected = skater.ground_profiles.select(mode as u32, skater.player_input.processed.surface_mode_2540).unwrap();
|
||||
assert!(std::sync::Arc::ptr_eq(&selected, &skater.ground_settings));
|
||||
}
|
||||
assert_eq!(physics.ticks, ticks + 60);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ fn stock_skater_startup_builds_the_graphs_and_physical_body_from_the_same_rig()
|
||||
let root = std::path::Path::new(&root);
|
||||
let manifest = skate_data::GameAssets::load(root).unwrap();
|
||||
let graphs = crate::graph_runtime::StockGraphs::load(root, &manifest).unwrap();
|
||||
let mut physics = GamePhysics::load(root).unwrap();
|
||||
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
|
||||
// This regression isolates grounded controls on the original flat fixture.
|
||||
// The production box is only six metres wide: sustained kickturning leaves
|
||||
// it at x=-3.32, correctly selecting KnownAir and ForcePhysics2. Separate
|
||||
@@ -363,7 +363,7 @@ fn stock_wheel_hardness_reaches_ground_force_and_torque() {
|
||||
let graphs = crate::graph_runtime::StockGraphs::load(root, &manifest).unwrap();
|
||||
let mut samples = Vec::new();
|
||||
for hardness in [0.0_f32, 0.7, 1.0] {
|
||||
let mut physics = GamePhysics::load(root).unwrap();
|
||||
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
|
||||
let mut skater = SkaterRuntime::load(root, &graphs, &physics, "normal").unwrap();
|
||||
let mut controls = PlayerControls::default();
|
||||
let mut camera = crate::camera::CameraRuntime::load(root).unwrap();
|
||||
|
||||
@@ -9,7 +9,7 @@ fn raw_controller_bail_runs_stock_wipeout_for_120_ticks() {
|
||||
let root = std::path::Path::new(&root);
|
||||
let assets = skate_data::GameAssets::load(root).unwrap();
|
||||
let graphs = crate::graph_runtime::StockGraphs::load(root, &assets).unwrap();
|
||||
let mut physics = GamePhysics::load(root).unwrap();
|
||||
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
|
||||
let mut skater = SkaterRuntime::load(root, &graphs, &physics, "normal").unwrap();
|
||||
let mut controls = PlayerControls::default();
|
||||
let mut input = crate::input::ControllerInput::default();
|
||||
@@ -173,7 +173,7 @@ fn authored_checkpoint_reply_runs_teleport_state_and_restores_riding() {
|
||||
let root = std::path::Path::new(&root);
|
||||
let assets = skate_data::GameAssets::load(root).unwrap();
|
||||
let graphs = crate::graph_runtime::StockGraphs::load(root, &assets).unwrap();
|
||||
let mut physics = GamePhysics::load(root).unwrap();
|
||||
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
|
||||
let mut skater = SkaterRuntime::load(root, &graphs, &physics, "normal").unwrap();
|
||||
let mut controls = PlayerControls::default();
|
||||
let mut input = crate::input::ControllerInput::default();
|
||||
|
||||
@@ -55,7 +55,7 @@ fn verify(
|
||||
}
|
||||
if state.captured && state.elapsed > 6. {
|
||||
let input_report = format!(
|
||||
"Game integration capture (not a Skate 3 parity verdict)\nExecutable: {}\nPolls: {}\nConsumed batches: {}\nStatus: {:?}\nPacket numbers: {:?}\nActions 64..81 by device: {:?}\nDerived ticks: {}\nIntents: {:?}\nPhysics ticks: {}\nContacts: {}\nBody positions: {:?}\nPhysical pose publications: {}\nGraph animation ticks: {}\nTruck targets: {:?}\nGround speed: {}\nGameplay camera shot: {}\nCamera frame: {:?}\n",
|
||||
"Game integration capture (not a Skate 3 parity verdict)\nExecutable: {}\nPolls: {}\nConsumed batches: {}\nStatus: {:?}\nPacket numbers: {:?}\nActions 64..81 by device: {:?}\nDerived ticks: {}\nIntents: {:?}\nDifficulty index: {}\nPhysics ticks: {}\nContacts: {}\nBody positions: {:?}\nPhysical pose publications: {}\nGraph animation ticks: {}\nTruck targets: {:?}\nGround speed: {}\nGameplay camera shot: {}\nCamera frame: {:?}\n",
|
||||
std::env::current_exe()
|
||||
.map_or_else(|_| "unavailable".into(), |p| p.display().to_string()),
|
||||
input.publications,
|
||||
@@ -65,6 +65,7 @@ fn verify(
|
||||
input.mapped_actions,
|
||||
controls.ticks,
|
||||
controls.intents,
|
||||
physics.difficulty_index(),
|
||||
physics.ticks,
|
||||
physics.contact_count,
|
||||
physics.board.bodies().map(|body| body.rates.position),
|
||||
|
||||
Reference in new issue
Block a user