feat(jump-hop): redesign sling platform gameplay
This commit is contained in:
@@ -5,61 +5,18 @@ use crate::{
|
||||
JumpHopPlatform, JumpHopRunSnapshot, JumpHopRunStatus, JumpHopScoring, JumpHopTileType,
|
||||
};
|
||||
|
||||
const JUMP_HOP_PLATFORM_SIZE_MULTIPLIER: f32 = 2.0;
|
||||
const JUMP_HOP_CHARGE_TO_DISTANCE_RATIO: f32 = 0.008;
|
||||
|
||||
pub fn generate_jump_hop_path(seed: &str, difficulty: JumpHopDifficulty) -> JumpHopPath {
|
||||
let config = difficulty_config(difficulty);
|
||||
let mut rng = DeterministicRng::new(seed, difficulty.as_str());
|
||||
let platform_count = rng.range_u32(config.min_platforms, config.max_platforms) as usize;
|
||||
let mut platforms = Vec::with_capacity(platform_count);
|
||||
let mut x = 0.0f32;
|
||||
let mut y = 0.0f32;
|
||||
|
||||
for index in 0..platform_count {
|
||||
let tile_type = if index == 0 {
|
||||
JumpHopTileType::Start
|
||||
} else if index + 1 == platform_count {
|
||||
JumpHopTileType::Finish
|
||||
} else if index % 7 == 0 {
|
||||
JumpHopTileType::Bonus
|
||||
} else if index % 5 == 0 {
|
||||
JumpHopTileType::Target
|
||||
} else if index % 4 == 0 {
|
||||
JumpHopTileType::Accent
|
||||
} else {
|
||||
JumpHopTileType::Normal
|
||||
};
|
||||
let width = rng.range_f32(config.min_width, config.max_width);
|
||||
let height = width * rng.range_f32(0.86, 1.04);
|
||||
let landing_radius = width * config.landing_radius_factor;
|
||||
let perfect_radius = landing_radius * config.perfect_radius_factor;
|
||||
|
||||
platforms.push(JumpHopPlatform {
|
||||
platform_id: format!("jump-hop-platform-{index:03}"),
|
||||
tile_type,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
landing_radius,
|
||||
perfect_radius,
|
||||
score_value: if tile_type == JumpHopTileType::Bonus {
|
||||
180
|
||||
} else {
|
||||
100
|
||||
},
|
||||
});
|
||||
|
||||
if index + 1 < platform_count {
|
||||
let distance = rng.range_f32(config.min_gap, config.max_gap);
|
||||
let direction = if rng.next_u32() % 2 == 0 { 1.0 } else { -1.0 };
|
||||
x += distance * 0.62 * direction;
|
||||
y += distance;
|
||||
}
|
||||
}
|
||||
let platform_count = 8usize;
|
||||
let platforms = build_platforms_until(seed, difficulty, platform_count);
|
||||
|
||||
JumpHopPath {
|
||||
seed: seed.trim().to_string(),
|
||||
difficulty,
|
||||
finish_index: platform_count.saturating_sub(1) as u32,
|
||||
finish_index: u32::MAX,
|
||||
platforms,
|
||||
camera_preset: "portrait-isometric-9x16".to_string(),
|
||||
scoring: JumpHopScoring {
|
||||
@@ -85,6 +42,7 @@ pub fn start_run(
|
||||
if path.platforms.is_empty() {
|
||||
return Err(JumpHopError::EmptyPath);
|
||||
}
|
||||
let path = normalize_jump_hop_path_platform_size(path);
|
||||
|
||||
Ok(JumpHopRunSnapshot {
|
||||
run_id,
|
||||
@@ -103,7 +61,9 @@ pub fn start_run(
|
||||
|
||||
pub fn apply_jump(
|
||||
run: &JumpHopRunSnapshot,
|
||||
charge_ms: u32,
|
||||
drag_distance: f32,
|
||||
drag_vector_x: Option<f32>,
|
||||
drag_vector_y: Option<f32>,
|
||||
jumped_at_ms: u64,
|
||||
) -> Result<JumpHopRunSnapshot, JumpHopError> {
|
||||
if run.status != JumpHopRunStatus::Playing {
|
||||
@@ -111,46 +71,42 @@ pub fn apply_jump(
|
||||
}
|
||||
let current_index = run.current_platform_index as usize;
|
||||
let next_index = current_index + 1;
|
||||
let path = extend_jump_hop_path(run.path.clone(), next_index + 3);
|
||||
let current = run
|
||||
.path
|
||||
.platforms
|
||||
.get(current_index)
|
||||
.ok_or(JumpHopError::EmptyPath)?;
|
||||
let target = run
|
||||
.path
|
||||
let target = path
|
||||
.platforms
|
||||
.get(next_index)
|
||||
.ok_or(JumpHopError::NoNextPlatform)?;
|
||||
let capped_charge = charge_ms.min(run.path.scoring.max_charge_ms);
|
||||
let jump_distance = capped_charge as f32 * run.path.scoring.charge_to_distance_ratio;
|
||||
let capped_drag_distance = drag_distance.clamp(0.0, run.path.scoring.max_charge_ms as f32);
|
||||
let jump_distance = capped_drag_distance * run.path.scoring.charge_to_distance_ratio;
|
||||
let vector_x = target.x - current.x;
|
||||
let vector_y = target.y - current.y;
|
||||
let target_distance = vector_x.hypot(vector_y).max(0.0001);
|
||||
let unit_x = vector_x / target_distance;
|
||||
let unit_y = vector_y / target_distance;
|
||||
let (unit_x, unit_y) = normalize_jump_direction(
|
||||
drag_vector_x,
|
||||
drag_vector_y,
|
||||
vector_x / target_distance,
|
||||
vector_y / target_distance,
|
||||
);
|
||||
let landed_x = current.x + unit_x * jump_distance;
|
||||
let landed_y = current.y + unit_y * jump_distance;
|
||||
let landing_error = (landed_x - target.x).hypot(landed_y - target.y);
|
||||
let target_landing_radius = target.landing_radius;
|
||||
|
||||
let mut next = run.clone();
|
||||
let result = if landing_error <= target.perfect_radius {
|
||||
if next_index as u32 == run.path.finish_index {
|
||||
JumpHopJumpResultKind::Finish
|
||||
} else {
|
||||
JumpHopJumpResultKind::Perfect
|
||||
}
|
||||
} else if landing_error <= target.landing_radius {
|
||||
if next_index as u32 == run.path.finish_index {
|
||||
JumpHopJumpResultKind::Finish
|
||||
} else {
|
||||
JumpHopJumpResultKind::Hit
|
||||
}
|
||||
next.path = path;
|
||||
let result = if landing_error <= target_landing_radius {
|
||||
JumpHopJumpResultKind::Hit
|
||||
} else {
|
||||
JumpHopJumpResultKind::Miss
|
||||
};
|
||||
|
||||
next.last_jump = Some(JumpHopLastJump {
|
||||
charge_ms: capped_charge,
|
||||
charge_ms: capped_drag_distance.round() as u32,
|
||||
jump_distance,
|
||||
target_platform_index: next_index as u32,
|
||||
landed_x,
|
||||
@@ -166,23 +122,8 @@ pub fn apply_jump(
|
||||
}
|
||||
|
||||
next.current_platform_index = next_index as u32;
|
||||
next.combo = next.combo.saturating_add(1);
|
||||
next.score = next.score.saturating_add(target.score_value);
|
||||
if matches!(
|
||||
result,
|
||||
JumpHopJumpResultKind::Perfect | JumpHopJumpResultKind::Finish
|
||||
) {
|
||||
next.score = next
|
||||
.score
|
||||
.saturating_add(run.path.scoring.perfect_bonus)
|
||||
.saturating_add(next.combo.saturating_mul(run.path.scoring.hit_bonus));
|
||||
} else {
|
||||
next.score = next.score.saturating_add(run.path.scoring.hit_bonus);
|
||||
}
|
||||
if result == JumpHopJumpResultKind::Finish {
|
||||
next.status = JumpHopRunStatus::Cleared;
|
||||
next.finished_at_ms = Some(jumped_at_ms);
|
||||
}
|
||||
next.combo = 0;
|
||||
next.score = next.current_platform_index;
|
||||
|
||||
Ok(next)
|
||||
}
|
||||
@@ -201,9 +142,31 @@ pub fn restart_run(
|
||||
)
|
||||
}
|
||||
|
||||
fn normalize_jump_hop_path_platform_size(mut path: JumpHopPath) -> JumpHopPath {
|
||||
let should_scale_legacy_path = path
|
||||
.platforms
|
||||
.iter()
|
||||
.any(|platform| platform.width < 1.2 && platform.landing_radius < 0.75);
|
||||
if !should_scale_legacy_path {
|
||||
if (path.scoring.charge_to_distance_ratio - JUMP_HOP_CHARGE_TO_DISTANCE_RATIO).abs()
|
||||
> f32::EPSILON
|
||||
{
|
||||
path.scoring.charge_to_distance_ratio = JUMP_HOP_CHARGE_TO_DISTANCE_RATIO;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
for platform in &mut path.platforms {
|
||||
platform.width *= JUMP_HOP_PLATFORM_SIZE_MULTIPLIER;
|
||||
platform.height *= JUMP_HOP_PLATFORM_SIZE_MULTIPLIER;
|
||||
platform.landing_radius *= JUMP_HOP_PLATFORM_SIZE_MULTIPLIER;
|
||||
platform.perfect_radius *= JUMP_HOP_PLATFORM_SIZE_MULTIPLIER;
|
||||
}
|
||||
path.scoring.charge_to_distance_ratio = JUMP_HOP_CHARGE_TO_DISTANCE_RATIO;
|
||||
path
|
||||
}
|
||||
|
||||
struct DifficultyConfig {
|
||||
min_platforms: u32,
|
||||
max_platforms: u32,
|
||||
min_gap: f32,
|
||||
max_gap: f32,
|
||||
min_width: f32,
|
||||
@@ -214,54 +177,143 @@ struct DifficultyConfig {
|
||||
max_charge_ms: u32,
|
||||
}
|
||||
|
||||
fn build_platforms_until(
|
||||
seed: &str,
|
||||
difficulty: JumpHopDifficulty,
|
||||
required_count: usize,
|
||||
) -> Vec<JumpHopPlatform> {
|
||||
let config = difficulty_config(difficulty);
|
||||
let mut platforms = Vec::with_capacity(required_count);
|
||||
let mut x = 0.0f32;
|
||||
let mut y = 0.0f32;
|
||||
|
||||
for index in 0..required_count {
|
||||
platforms.push(build_platform(seed, difficulty, index, x, y, &config));
|
||||
if index + 1 < required_count {
|
||||
let mut rng = DeterministicRng::new(seed, &format!("{}:{index}", difficulty.as_str()));
|
||||
let distance = rng.range_f32(config.min_gap, config.max_gap);
|
||||
let lane = rng.range_f32(0.42, 0.86);
|
||||
let direction = if rng.next_u32() % 2 == 0 { 1.0 } else { -1.0 };
|
||||
x += distance * lane * direction;
|
||||
y += distance;
|
||||
}
|
||||
}
|
||||
|
||||
platforms
|
||||
}
|
||||
|
||||
fn build_platform(
|
||||
seed: &str,
|
||||
difficulty: JumpHopDifficulty,
|
||||
index: usize,
|
||||
x: f32,
|
||||
y: f32,
|
||||
config: &DifficultyConfig,
|
||||
) -> JumpHopPlatform {
|
||||
let mut rng = DeterministicRng::new(seed, &format!("platform:{}:{index}", difficulty.as_str()));
|
||||
let tile_type = if index == 0 {
|
||||
JumpHopTileType::Start
|
||||
} else if index % 11 == 0 {
|
||||
JumpHopTileType::Bonus
|
||||
} else if index % 7 == 0 {
|
||||
JumpHopTileType::Accent
|
||||
} else if index % 3 == 0 {
|
||||
JumpHopTileType::Target
|
||||
} else {
|
||||
JumpHopTileType::Normal
|
||||
};
|
||||
let width = rng.range_f32(config.min_width, config.max_width);
|
||||
let height = width * rng.range_f32(0.88, 1.06);
|
||||
let landing_radius = width * config.landing_radius_factor;
|
||||
|
||||
JumpHopPlatform {
|
||||
platform_id: format!("jump-hop-platform-{index:05}"),
|
||||
tile_type,
|
||||
x,
|
||||
y,
|
||||
width: width * JUMP_HOP_PLATFORM_SIZE_MULTIPLIER,
|
||||
height: height * JUMP_HOP_PLATFORM_SIZE_MULTIPLIER,
|
||||
landing_radius: landing_radius * JUMP_HOP_PLATFORM_SIZE_MULTIPLIER,
|
||||
perfect_radius: landing_radius
|
||||
* config.perfect_radius_factor
|
||||
* JUMP_HOP_PLATFORM_SIZE_MULTIPLIER,
|
||||
score_value: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_jump_hop_path(mut path: JumpHopPath, required_count: usize) -> JumpHopPath {
|
||||
if path.platforms.len() >= required_count {
|
||||
return path;
|
||||
}
|
||||
path.platforms = build_platforms_until(&path.seed, path.difficulty, required_count);
|
||||
path.finish_index = u32::MAX;
|
||||
path
|
||||
}
|
||||
|
||||
fn normalize_jump_direction(
|
||||
drag_vector_x: Option<f32>,
|
||||
drag_vector_y: Option<f32>,
|
||||
fallback_x: f32,
|
||||
fallback_y: f32,
|
||||
) -> (f32, f32) {
|
||||
let Some(drag_x) = drag_vector_x.filter(|value| value.is_finite()) else {
|
||||
return (fallback_x, fallback_y);
|
||||
};
|
||||
let Some(drag_y) = drag_vector_y.filter(|value| value.is_finite()) else {
|
||||
return (fallback_x, fallback_y);
|
||||
};
|
||||
// 前端提交的是屏幕拖拽向量:x 轴同向,y 轴向下为正。
|
||||
// 真实起跳需要“反向弹出”,同时把屏幕 y 翻回世界坐标的向上为正。
|
||||
let jump_x = -drag_x;
|
||||
let jump_y = drag_y;
|
||||
let length = jump_x.hypot(jump_y);
|
||||
if length < 0.0001 {
|
||||
(fallback_x, fallback_y)
|
||||
} else {
|
||||
(jump_x / length, jump_y / length)
|
||||
}
|
||||
}
|
||||
|
||||
fn difficulty_config(difficulty: JumpHopDifficulty) -> DifficultyConfig {
|
||||
match difficulty {
|
||||
JumpHopDifficulty::Easy => DifficultyConfig {
|
||||
min_platforms: 12,
|
||||
max_platforms: 14,
|
||||
min_gap: 1.0,
|
||||
max_gap: 1.45,
|
||||
min_width: 0.9,
|
||||
max_width: 1.08,
|
||||
landing_radius_factor: 0.62,
|
||||
perfect_radius_factor: 0.32,
|
||||
charge_to_distance_ratio: 0.004,
|
||||
charge_to_distance_ratio: JUMP_HOP_CHARGE_TO_DISTANCE_RATIO,
|
||||
max_charge_ms: 700,
|
||||
},
|
||||
JumpHopDifficulty::Standard => DifficultyConfig {
|
||||
min_platforms: 16,
|
||||
max_platforms: 18,
|
||||
min_gap: 1.22,
|
||||
max_gap: 1.78,
|
||||
min_width: 0.82,
|
||||
max_width: 1.0,
|
||||
landing_radius_factor: 0.54,
|
||||
perfect_radius_factor: 0.26,
|
||||
charge_to_distance_ratio: 0.004,
|
||||
charge_to_distance_ratio: JUMP_HOP_CHARGE_TO_DISTANCE_RATIO,
|
||||
max_charge_ms: 780,
|
||||
},
|
||||
JumpHopDifficulty::Advanced => DifficultyConfig {
|
||||
min_platforms: 20,
|
||||
max_platforms: 24,
|
||||
min_gap: 1.45,
|
||||
max_gap: 2.05,
|
||||
min_width: 0.72,
|
||||
max_width: 0.94,
|
||||
landing_radius_factor: 0.48,
|
||||
perfect_radius_factor: 0.22,
|
||||
charge_to_distance_ratio: 0.004,
|
||||
charge_to_distance_ratio: JUMP_HOP_CHARGE_TO_DISTANCE_RATIO,
|
||||
max_charge_ms: 860,
|
||||
},
|
||||
JumpHopDifficulty::Challenge => DifficultyConfig {
|
||||
min_platforms: 26,
|
||||
max_platforms: 32,
|
||||
min_gap: 1.7,
|
||||
max_gap: 2.35,
|
||||
min_width: 0.66,
|
||||
max_width: 0.88,
|
||||
landing_radius_factor: 0.42,
|
||||
perfect_radius_factor: 0.18,
|
||||
charge_to_distance_ratio: 0.004,
|
||||
charge_to_distance_ratio: JUMP_HOP_CHARGE_TO_DISTANCE_RATIO,
|
||||
max_charge_ms: 950,
|
||||
},
|
||||
}
|
||||
@@ -289,13 +341,6 @@ impl DeterministicRng {
|
||||
(self.state >> 32) as u32
|
||||
}
|
||||
|
||||
fn range_u32(&mut self, min: u32, max: u32) -> u32 {
|
||||
if max <= min {
|
||||
return min;
|
||||
}
|
||||
min + self.next_u32() % (max - min + 1)
|
||||
}
|
||||
|
||||
fn range_f32(&mut self, min: f32, max: f32) -> f32 {
|
||||
if max <= min {
|
||||
return min;
|
||||
@@ -319,14 +364,67 @@ mod tests {
|
||||
let challenge = generate_jump_hop_path("seed-a", JumpHopDifficulty::Challenge);
|
||||
|
||||
assert_eq!(first, second);
|
||||
assert!((16..=18).contains(&first.platforms.len()));
|
||||
assert!((26..=32).contains(&challenge.platforms.len()));
|
||||
assert_eq!(first.platforms.len(), 8);
|
||||
assert_eq!(challenge.platforms.len(), 8);
|
||||
assert_eq!(first.platforms.first().unwrap().tile_type.as_str(), "start");
|
||||
assert_eq!(first.platforms.last().unwrap().tile_type.as_str(), "finish");
|
||||
assert_eq!(first.finish_index, u32::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_resolution_distinguishes_perfect_hit_and_miss() {
|
||||
fn difficulty_charge_to_distance_ratio_is_doubled() {
|
||||
let easy = generate_jump_hop_path("seed-ratio-easy", JumpHopDifficulty::Easy);
|
||||
let standard = generate_jump_hop_path("seed-ratio-standard", JumpHopDifficulty::Standard);
|
||||
let advanced = generate_jump_hop_path("seed-ratio-advanced", JumpHopDifficulty::Advanced);
|
||||
let challenge = generate_jump_hop_path("seed-ratio-challenge", JumpHopDifficulty::Challenge);
|
||||
|
||||
assert_eq!(easy.scoring.charge_to_distance_ratio, 0.008);
|
||||
assert_eq!(standard.scoring.charge_to_distance_ratio, 0.008);
|
||||
assert_eq!(advanced.scoring.charge_to_distance_ratio, 0.008);
|
||||
assert_eq!(challenge.scoring.charge_to_distance_ratio, 0.008);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_platforms_use_double_size_and_landing_radius() {
|
||||
let path = generate_jump_hop_path("seed-size", JumpHopDifficulty::Standard);
|
||||
let first_platform = path.platforms.first().expect("platform should exist");
|
||||
|
||||
assert!(first_platform.width >= 1.64);
|
||||
assert!(first_platform.width <= 2.0);
|
||||
assert!(first_platform.height >= 1.44);
|
||||
assert!(first_platform.height <= 2.12);
|
||||
assert!(first_platform.landing_radius >= 0.88);
|
||||
assert!(first_platform.landing_radius <= 1.08);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_run_normalizes_legacy_single_size_platforms() {
|
||||
let mut path = generate_jump_hop_path("seed-legacy", JumpHopDifficulty::Standard);
|
||||
for platform in &mut path.platforms {
|
||||
platform.width /= 2.0;
|
||||
platform.height /= 2.0;
|
||||
platform.landing_radius /= 2.0;
|
||||
platform.perfect_radius /= 2.0;
|
||||
}
|
||||
let legacy_width = path.platforms[0].width;
|
||||
let legacy_landing_radius = path.platforms[0].landing_radius;
|
||||
|
||||
let run = start_run(
|
||||
"run-legacy".to_string(),
|
||||
"user-legacy".to_string(),
|
||||
"profile-legacy".to_string(),
|
||||
path,
|
||||
100,
|
||||
)
|
||||
.expect("run should start");
|
||||
|
||||
assert!((run.path.platforms[0].width - legacy_width * 2.0).abs() < 0.0001);
|
||||
assert!(
|
||||
(run.path.platforms[0].landing_radius - legacy_landing_radius * 2.0).abs() < 0.0001
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_resolution_distinguishes_hit_and_miss() {
|
||||
let path = generate_jump_hop_path("seed-b", JumpHopDifficulty::Easy);
|
||||
let run = start_run(
|
||||
"run-1".to_string(),
|
||||
@@ -338,25 +436,25 @@ mod tests {
|
||||
.expect("run should start");
|
||||
let target = &run.path.platforms[1];
|
||||
let distance = target.x.hypot(target.y);
|
||||
let perfect_charge = (distance / run.path.scoring.charge_to_distance_ratio) as u32;
|
||||
|
||||
let perfect = apply_jump(&run, perfect_charge, 200).expect("jump should resolve");
|
||||
assert_eq!(
|
||||
perfect.last_jump.as_ref().unwrap().result,
|
||||
JumpHopJumpResultKind::Perfect
|
||||
);
|
||||
assert_eq!(perfect.status, JumpHopRunStatus::Playing);
|
||||
assert_eq!(perfect.current_platform_index, 1);
|
||||
let target_charge = (distance / run.path.scoring.charge_to_distance_ratio) as u32;
|
||||
|
||||
let hit =
|
||||
apply_jump(&run, perfect_charge.saturating_add(80), 200).expect("jump should resolve");
|
||||
apply_jump(&run, target_charge as f32, None, None, 200).expect("jump should resolve");
|
||||
assert_eq!(
|
||||
hit.last_jump.as_ref().unwrap().result,
|
||||
JumpHopJumpResultKind::Hit
|
||||
);
|
||||
assert_eq!(hit.status, JumpHopRunStatus::Playing);
|
||||
assert_eq!(hit.current_platform_index, 1);
|
||||
|
||||
let miss =
|
||||
apply_jump(&run, perfect_charge.saturating_add(900), 200).expect("jump should resolve");
|
||||
let miss = apply_jump(
|
||||
&run,
|
||||
target_charge.saturating_add(900) as f32,
|
||||
None,
|
||||
None,
|
||||
200,
|
||||
)
|
||||
.expect("jump should resolve");
|
||||
assert_eq!(miss.status, JumpHopRunStatus::Failed);
|
||||
assert_eq!(
|
||||
miss.last_jump.as_ref().unwrap().result,
|
||||
@@ -364,6 +462,39 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_resolution_uses_screen_drag_y_axis_for_forward_jump_direction() {
|
||||
let path = generate_jump_hop_path("seed-screen-axis", JumpHopDifficulty::Easy);
|
||||
let run = start_run(
|
||||
"run-screen-axis".to_string(),
|
||||
"user-screen-axis".to_string(),
|
||||
"profile-screen-axis".to_string(),
|
||||
path,
|
||||
100,
|
||||
)
|
||||
.expect("run should start");
|
||||
let current = &run.path.platforms[0];
|
||||
let target = &run.path.platforms[1];
|
||||
let target_distance = (target.x - current.x).hypot(target.y - current.y);
|
||||
let charge = (target_distance / run.path.scoring.charge_to_distance_ratio).round() as u32;
|
||||
|
||||
let result = apply_jump(
|
||||
&run,
|
||||
charge as f32,
|
||||
Some(-(target.x - current.x)),
|
||||
Some(target.y - current.y),
|
||||
200,
|
||||
)
|
||||
.expect("jump should resolve");
|
||||
|
||||
assert_eq!(result.status, JumpHopRunStatus::Playing);
|
||||
assert_eq!(
|
||||
result.last_jump.as_ref().unwrap().result,
|
||||
JumpHopJumpResultKind::Hit
|
||||
);
|
||||
assert_eq!(result.current_platform_index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_returns_to_first_platform_and_playing_state() {
|
||||
let path = generate_jump_hop_path("seed-c", JumpHopDifficulty::Easy);
|
||||
@@ -392,4 +523,32 @@ mod tests {
|
||||
assert_eq!(restarted.started_at_ms, 300);
|
||||
assert!(restarted.finished_at_ms.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_jump_extends_infinite_path_buffer_and_counts_jumps() {
|
||||
let path = generate_jump_hop_path("seed-d", JumpHopDifficulty::Easy);
|
||||
let mut run = start_run(
|
||||
"run-1".to_string(),
|
||||
"user-1".to_string(),
|
||||
"profile-1".to_string(),
|
||||
path,
|
||||
100,
|
||||
)
|
||||
.expect("run should start");
|
||||
|
||||
for step in 0..9 {
|
||||
let current = &run.path.platforms[run.current_platform_index as usize];
|
||||
let target = &run.path.platforms[run.current_platform_index as usize + 1];
|
||||
let distance = (target.x - current.x).hypot(target.y - current.y);
|
||||
let charge = (distance / run.path.scoring.charge_to_distance_ratio).round() as u32;
|
||||
run = apply_jump(&run, charge as f32, None, None, 200 + step)
|
||||
.expect("jump should resolve");
|
||||
}
|
||||
|
||||
assert_eq!(run.status, JumpHopRunStatus::Playing);
|
||||
assert_eq!(run.current_platform_index, 9);
|
||||
assert_eq!(run.score, 9);
|
||||
assert!(run.path.platforms.len() >= 12);
|
||||
assert!(run.finished_at_ms.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user