feat: port Freeze weapon to pure Rust
Browse filesPort FireWeapon__Freeze (0x51E920) β plays freeze sound at worm position,
creates landscape freeze overlay via PCLandscape vtable[5], and increments
WormEntry counters (turn_action +3, effect_counter +10).
Sound positioning uses the DDGame sound queue entry directly after
PlaySoundGlobal queues the sound effect.
Add freeze_nuke_armageddon replay test (2Γ Freeze, plus Nuke/Armageddon).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-wormkit/src/replacements/weapon.rs
CHANGED
|
@@ -391,8 +391,8 @@ unsafe fn fire_weapon_special(
|
|
| 391 |
(*worm).set_state(WormState::TeleportCancelled_Maybe);
|
| 392 |
}
|
| 393 |
}
|
| 394 |
-
// Freeze (
|
| 395 |
-
17 =>
|
| 396 |
// Suicide Bomber
|
| 397 |
18 => (*worm).set_state(WormState::SuicideBomber),
|
| 398 |
// Skip Go (pure Rust)
|
|
@@ -555,6 +555,72 @@ unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
|
|
| 555 |
entry.turn_action_counter_Maybe += 7;
|
| 556 |
}
|
| 557 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
// ββ SpecialImpact wrapper βββββββββββββββββββββββββββββββββ
|
| 559 |
|
| 560 |
/// Typed wrapper for SpecialImpact (0x5193D0).
|
|
|
|
| 391 |
(*worm).set_state(WormState::TeleportCancelled_Maybe);
|
| 392 |
}
|
| 393 |
}
|
| 394 |
+
// Freeze (pure Rust)
|
| 395 |
+
17 => fire_freeze(worm),
|
| 396 |
// Suicide Bomber
|
| 397 |
18 => (*worm).set_state(WormState::SuicideBomber),
|
| 398 |
// Skip Go (pure Rust)
|
|
|
|
| 555 |
entry.turn_action_counter_Maybe += 7;
|
| 556 |
}
|
| 557 |
|
| 558 |
+
/// Freeze (subtype 17) β pure Rust port of 0x51E920.
|
| 559 |
+
///
|
| 560 |
+
/// Plays a freeze sound, optionally creates a freeze visual overlay on the
|
| 561 |
+
/// landscape and increments worm counters. The visual/counter path only runs
|
| 562 |
+
/// when CTaskWorm+0x2EC (freeze_target_team) is nonzero.
|
| 563 |
+
///
|
| 564 |
+
/// Convention: usercall(EAX=worm, ESI=worm, EDI=worm), plain RET.
|
| 565 |
+
unsafe fn fire_freeze(worm: *mut CTaskWorm) {
|
| 566 |
+
use openwa_core::rebase::rb;
|
| 567 |
+
|
| 568 |
+
let ddgame = (*worm).ddgame();
|
| 569 |
+
let game_version = (*(*ddgame).game_info).game_version;
|
| 570 |
+
|
| 571 |
+
// Read freeze position and target from CTaskWorm fields
|
| 572 |
+
let worm_raw = worm as *const u8;
|
| 573 |
+
let freeze_x = *(worm_raw.add(0x2E0) as *const i32);
|
| 574 |
+
let freeze_y = *(worm_raw.add(0x2E4) as *const i32);
|
| 575 |
+
let freeze_target = *(worm_raw.add(0x2EC) as *const i32);
|
| 576 |
+
|
| 577 |
+
// Choose sound: 0x70 if freeze has target or old version, else 0x73
|
| 578 |
+
let sound_id: u32 = if freeze_target != 0 || game_version < 0x21 {
|
| 579 |
+
0x70
|
| 580 |
+
} else {
|
| 581 |
+
0x73
|
| 582 |
+
};
|
| 583 |
+
|
| 584 |
+
// Call PlaySoundGlobal β thiscall(ECX=worm, sound_id, 3, 0x10000, 0x10000)
|
| 585 |
+
type PlaySoundGlobalFn = unsafe extern "thiscall" fn(*mut CTaskWorm, u32, i32, i32, i32) -> i32;
|
| 586 |
+
let play_sound: PlaySoundGlobalFn = core::mem::transmute(rb(va::PLAY_SOUND_GLOBAL));
|
| 587 |
+
let result = play_sound(worm, sound_id, 3, 0x10000, 0x10000);
|
| 588 |
+
|
| 589 |
+
// If sound was queued, set its position to the freeze location
|
| 590 |
+
if result != 0 {
|
| 591 |
+
let queue_count = (*ddgame).sound_queue_count;
|
| 592 |
+
if queue_count > 0 {
|
| 593 |
+
let entry = &mut (*ddgame).sound_queue[(queue_count - 1) as usize];
|
| 594 |
+
entry.is_local = 1;
|
| 595 |
+
entry.secondary_vtable = 0;
|
| 596 |
+
entry.pos_x = freeze_x as u32;
|
| 597 |
+
entry.pos_y = freeze_y as u32;
|
| 598 |
+
}
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
// If freeze has a target, apply the visual effect and update counters
|
| 602 |
+
if freeze_target != 0 {
|
| 603 |
+
// Call PCLandscape vtable[5] to create freeze overlay
|
| 604 |
+
let landscape = (*ddgame).landscape as *mut u8;
|
| 605 |
+
let landscape_vt = *(landscape as *const *const usize);
|
| 606 |
+
let freeze_visual: unsafe extern "thiscall" fn(*mut u8, i32, i32, *mut u8, *mut u8) =
|
| 607 |
+
core::mem::transmute(*landscape_vt.add(5));
|
| 608 |
+
let sprite1 = (*ddgame).sprite_cache_2[freeze_target as usize];
|
| 609 |
+
let sprite2 = (*ddgame).sprite_cache_2[19 + freeze_target as usize];
|
| 610 |
+
freeze_visual(landscape, freeze_x >> 16, freeze_y >> 16, sprite1, sprite2);
|
| 611 |
+
|
| 612 |
+
// Increment WormEntry counters
|
| 613 |
+
let arena = openwa_core::engine::ddgame::TeamArenaRef::from_ptr(
|
| 614 |
+
&raw mut (*ddgame).team_arena,
|
| 615 |
+
);
|
| 616 |
+
let team_index = (*worm).team_index as usize;
|
| 617 |
+
let worm_index = (*worm).worm_index as usize;
|
| 618 |
+
let entry = arena.team_worm_mut(team_index, worm_index);
|
| 619 |
+
entry.turn_action_counter_Maybe += 3;
|
| 620 |
+
entry.effect_counter_04_Maybe += 10;
|
| 621 |
+
}
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
// ββ SpecialImpact wrapper βββββββββββββββββββββββββββββββββ
|
| 625 |
|
| 626 |
/// Typed wrapper for SpecialImpact (0x5193D0).
|
testdata/replays/freeze_nuke_armageddon.WAgame
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0c40f3581f382865d4b8e7f29970084e3bba185d594be18638d529db2a3d4a3b
|
| 3 |
+
size 14457
|
testdata/replays/freeze_nuke_armageddon_expected.log
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e238485ecb1c48d34e8d48aecae2f9dca02532b521339870f6e8e8fc63dc4135
|
| 3 |
+
size 2051
|