converge combat timers, projectile references, pending damage, and the spawn ring
- replay the shooter's combat update a second time when it launches a projectile, matching the client's own component-pass re-entry - carry the shooter's damage effect on every projectile and null a shot's target/source when either leaves the board - derive pending physical damage from the shots actually in flight instead of an incremental total, so it can never go negative - split the spawn ring's two angle registers so three- and five-unit cards land where the client puts them - attribute a combat target left at NONE to the exact site that did it, gated to report each object once - number and annotate every checksum field so a client-reported mismatch resolves straight to a name
This commit is contained in:
parent
c18ce37a42
commit
07f0ee5427
28 changed files with 3100 additions and 535 deletions
|
|
@ -1,9 +1,10 @@
|
|||
use titan::{ByteStreamReader, ByteStreamWriter, LogicLong, Payload, Result};
|
||||
use crate::battle::logic_game_object::LogicGameObjectEntry;
|
||||
use crate::battle::logic_game_object::{LogicGameObjectEntry, LogicObjectBody, LogicVector2};
|
||||
use crate::battle::logic_game_object_manager::LogicGameObjectManager;
|
||||
use crate::battle::logic_game_object_ref::LogicGameObjectRef;
|
||||
use crate::battle::logic_summoner::{check_spell_position, find_position_for_spell, DeployBlocker};
|
||||
use crate::data::LogicDataRef;
|
||||
use crate::model::LogicSpellDeck;
|
||||
use titan::{ByteStreamReader, ByteStreamWriter, LogicLong, Payload, Result};
|
||||
pub const BATTLE_TYPE_PVP: i32 = 0;
|
||||
pub const BATTLE_TYPE_NPC: i32 = 1;
|
||||
pub const BATTLE_TYPE_REPLAY: i32 = 3;
|
||||
|
|
@ -11,8 +12,6 @@ pub const BATTLE_INT_ARRAY: usize = 8;
|
|||
pub const BATTLE_TICKS_PER_SECOND: i32 = 20;
|
||||
pub const LEADER_TOWER_COUNT: i32 = 2;
|
||||
pub const CROWNS_FOR_LEADER: i32 = 3;
|
||||
pub const LOCATION_MATCH_LENGTH_COLUMN: &str = "MatchLength";
|
||||
pub const LOCATION_OVERTIME_COLUMN: &str = "OvertimeSeconds";
|
||||
pub const BATTLE_TRAILING_INTS: usize = 6;
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LogicBattle {
|
||||
|
|
@ -41,6 +40,59 @@ pub struct LogicBattle {
|
|||
pub trailing: [i32; BATTLE_TRAILING_INTS],
|
||||
pub tilemap: Option<crate::battle::LogicTilemap>,
|
||||
}
|
||||
impl LogicBattle {
|
||||
pub fn deploy_blockers(&self, caster_is_owner_zero: bool) -> Vec<DeployBlocker> {
|
||||
let mut blockers = Vec::new();
|
||||
for entry in self.objects.objects.iter() {
|
||||
if !entry.is_alive() || entry.is_projectile() {
|
||||
continue;
|
||||
}
|
||||
let Some(data) = entry.data.as_character() else {
|
||||
continue;
|
||||
};
|
||||
let is_summoner = matches!(entry.body, LogicObjectBody::Summoner(_));
|
||||
if !data.is_building() && !is_summoner {
|
||||
continue;
|
||||
}
|
||||
let no_deploy_w = data.no_deploy_size_w();
|
||||
let friendly = no_deploy_w < 1 || (entry.owner_index() == 0) == caster_is_owner_zero;
|
||||
let (size_w, size_h) = if friendly {
|
||||
let size = data.size_in_tiles();
|
||||
(size, size)
|
||||
} else {
|
||||
(no_deploy_w, data.no_deploy_size_h())
|
||||
};
|
||||
let position = entry.position();
|
||||
blockers.push(DeployBlocker {
|
||||
x: position.0,
|
||||
y: position.1,
|
||||
size_w,
|
||||
size_h,
|
||||
});
|
||||
}
|
||||
blockers
|
||||
}
|
||||
pub fn resolve_spell_position(
|
||||
&self,
|
||||
spell: &LogicDataRef,
|
||||
raw: LogicVector2,
|
||||
owner: i32,
|
||||
) -> Option<LogicVector2> {
|
||||
let tilemap = self.tilemap.as_ref()?;
|
||||
let summon_name = spell
|
||||
.as_spell()
|
||||
.map(|spell| spell.summon_character().to_owned());
|
||||
let summon = summon_name
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(|name| LogicDataRef::by_name(crate::data::table::CHARACTERS_COMBINED, &name));
|
||||
let summon_data = summon.as_ref().and_then(|data| data.as_character());
|
||||
if check_spell_position(tilemap, raw.x, raw.y, summon_data.is_some()) != 0 {
|
||||
return None;
|
||||
}
|
||||
let blockers = self.deploy_blockers(owner == 0);
|
||||
find_position_for_spell(summon_data.as_ref(), raw, tilemap, &blockers, false)
|
||||
}
|
||||
}
|
||||
impl Default for LogicBattle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
|
@ -77,14 +129,14 @@ impl LogicBattle {
|
|||
}
|
||||
pub fn match_length_seconds(&self) -> i32 {
|
||||
self.location
|
||||
.data()
|
||||
.map(|row| row.int(LOCATION_MATCH_LENGTH_COLUMN))
|
||||
.as_location()
|
||||
.map(|location| location.match_length())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
pub fn overtime_length_seconds(&self) -> i32 {
|
||||
self.location
|
||||
.data()
|
||||
.map(|row| row.int(LOCATION_OVERTIME_COLUMN))
|
||||
.as_location()
|
||||
.map(|location| location.overtime_seconds())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
pub fn leader(&self, index: usize) -> Option<&LogicGameObjectEntry> {
|
||||
|
|
@ -95,7 +147,9 @@ impl LogicBattle {
|
|||
.find(|entry| entry.global_id == *id)
|
||||
}
|
||||
pub fn is_leader_alive(&self, index: usize) -> bool {
|
||||
self.leader(index).map(|entry| entry.is_alive()).unwrap_or(false)
|
||||
self.leader(index)
|
||||
.map(|entry| entry.is_alive())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
pub fn stars(&self, index: usize) -> i32 {
|
||||
let other = 1 - index.min(1);
|
||||
|
|
@ -130,9 +184,30 @@ impl LogicBattle {
|
|||
}
|
||||
self.stars(0) != self.stars(1)
|
||||
}
|
||||
pub fn resolve_winner(&mut self, tick: i32) {
|
||||
if self.battle_ended_called {
|
||||
return;
|
||||
}
|
||||
self.battle_ended_called = true;
|
||||
self.end_counter = 1;
|
||||
let mut end = self.match_length_seconds();
|
||||
if self.is_on_overtime {
|
||||
end += self.overtime_length_seconds();
|
||||
}
|
||||
self.battle_ended_with_timeout = tick / BATTLE_TICKS_PER_SECOND >= end;
|
||||
let (s0, s1) = (self.stars(0), self.stars(1));
|
||||
self.winner_index = if s0 < s1 {
|
||||
1
|
||||
} else if s0 > s1 {
|
||||
0
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
}
|
||||
}
|
||||
impl Payload for LogicBattle {
|
||||
fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||
titan::checksum::checksum_trace_note("battle.header");
|
||||
self.location.encode(writer)?;
|
||||
self.npc.encode(writer)?;
|
||||
self.arena.encode(writer)?;
|
||||
|
|
@ -154,6 +229,7 @@ impl Payload for LogicBattle {
|
|||
writer.write_boolean(self.show_start_hud);
|
||||
writer.write_boolean(self.is_on_overtime);
|
||||
self.objects.encode(writer)?;
|
||||
titan::checksum::checksum_trace_note("battle.decks");
|
||||
for deck in &self.decks {
|
||||
match deck {
|
||||
None => writer.write_boolean(false),
|
||||
|
|
@ -163,9 +239,11 @@ impl Payload for LogicBattle {
|
|||
}
|
||||
}
|
||||
}
|
||||
titan::checksum::checksum_trace_note("battle.leaders");
|
||||
for leader in &self.leaders {
|
||||
leader.encode(writer)?;
|
||||
}
|
||||
titan::checksum::checksum_trace_note("battle.leader_towers");
|
||||
for towers in &self.leader_towers {
|
||||
writer.write_vint(towers.len() as i32);
|
||||
for tower in towers {
|
||||
|
|
@ -176,6 +254,7 @@ impl Payload for LogicBattle {
|
|||
writer.write_vint(self.winner_score_change);
|
||||
writer.write_vint(self.loser_score_change);
|
||||
}
|
||||
titan::checksum::checksum_trace_note("battle.trailing");
|
||||
for value in self.trailing {
|
||||
writer.write_vint(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use titan::{ByteStreamWriter, Result};
|
||||
use crate::battle::logic_game_object::{LogicGameObject, LogicVector2};
|
||||
use titan::{ByteStreamWriter, Result};
|
||||
pub const DIRECTION_TOP: i32 = 256;
|
||||
pub const DIRECTION_BOTTOM: i32 = -256;
|
||||
pub const DEFAULT_SIZE: i32 = 100;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
use titan::{ByteStreamWriter, Payload, Result};
|
||||
use crate::battle::logic_game_object::LogicVector2;
|
||||
use crate::battle::logic_game_object_ref::LogicGameObjectRef;
|
||||
use titan::{ByteStreamWriter, Payload, Result};
|
||||
pub const COMPONENT_COMBAT: usize = 0;
|
||||
pub const COMPONENT_MOVEMENT: usize = 1;
|
||||
pub const COMPONENT_HITPOINT: usize = 2;
|
||||
pub const COMPONENT_BUFF: usize = 3;
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct LogicCombatComponent {
|
||||
pub flag_72: bool,
|
||||
pub flag_73: bool,
|
||||
pub is_healing: bool,
|
||||
pub charge_ready: bool,
|
||||
pub field_48: i32,
|
||||
pub field_52: i32,
|
||||
pub field_60: i32,
|
||||
pub field_64: i32,
|
||||
pub field_68: i32,
|
||||
pub load_timer: i32,
|
||||
pub dash_cooldown: i32,
|
||||
pub special_attack_counter: i32,
|
||||
pub attack_finish_timer: i32,
|
||||
pub target: LogicGameObjectRef,
|
||||
pub attackers: Vec<(LogicGameObjectRef, i32)>,
|
||||
pub observers: Vec<LogicGameObjectRef>,
|
||||
|
|
@ -21,14 +21,33 @@ pub struct LogicCombatComponent {
|
|||
pub hit_timer: i32,
|
||||
}
|
||||
impl LogicCombatComponent {
|
||||
pub fn note_attacker(&mut self, attacker: LogicGameObjectRef, timer: i32) {
|
||||
if let Some(entry) = self
|
||||
.attackers
|
||||
.iter_mut()
|
||||
.find(|(reference, _)| *reference == attacker)
|
||||
{
|
||||
entry.1 = timer;
|
||||
} else {
|
||||
self.attackers.push((attacker, timer));
|
||||
}
|
||||
}
|
||||
pub fn age_attackers(&mut self, dt: i32) {
|
||||
for i in (0..self.attackers.len()).rev() {
|
||||
self.attackers[i].1 -= dt;
|
||||
if self.attackers[i].1 <= 0 {
|
||||
self.attackers.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||
writer.write_boolean(self.flag_72);
|
||||
writer.write_boolean(self.flag_73);
|
||||
writer.write_boolean(self.is_healing);
|
||||
writer.write_boolean(self.charge_ready);
|
||||
writer.write_vint(self.hit_timer);
|
||||
writer.write_vint(self.field_52);
|
||||
writer.write_vint(self.field_60);
|
||||
writer.write_vint(self.field_64);
|
||||
writer.write_vint(self.field_68);
|
||||
writer.write_vint(self.load_timer);
|
||||
writer.write_vint(self.dash_cooldown);
|
||||
writer.write_vint(self.special_attack_counter);
|
||||
writer.write_vint(self.attack_finish_timer);
|
||||
writer.write_vint(self.attackers.len() as i32);
|
||||
writer.write_vint(self.observers.len() as i32);
|
||||
self.target.encode(writer)?;
|
||||
|
|
@ -66,7 +85,6 @@ pub struct LogicMovementComponent {
|
|||
pub move_timer: i32,
|
||||
pub jump_distance: i32,
|
||||
pub goal: Option<(i32, i32)>,
|
||||
pub route: Vec<(i32, i32)>,
|
||||
}
|
||||
impl Default for LogicMovementComponent {
|
||||
fn default() -> Self {
|
||||
|
|
@ -92,7 +110,6 @@ impl Default for LogicMovementComponent {
|
|||
move_timer: 0,
|
||||
jump_distance: 0,
|
||||
goal: None,
|
||||
route: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,6 +159,19 @@ impl LogicHitpointComponent {
|
|||
..Self::default()
|
||||
}
|
||||
}
|
||||
pub fn with_lifetime(hitpoints: i32, life_time: i32) -> Self {
|
||||
let lifetime_damage = if life_time >= 1 {
|
||||
(100_000i64 * hitpoints as i64 / life_time as i64 / 20) as i32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
Self {
|
||||
hitpoints,
|
||||
base_hitpoints: hitpoints,
|
||||
lifetime_damage,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||
writer.write_vint(self.hitpoints);
|
||||
writer.write_vint(self.base_hitpoints);
|
||||
|
|
@ -161,17 +191,19 @@ pub struct LogicCharacterBuffComponent {
|
|||
pub field_36: i32,
|
||||
pub field_40: i32,
|
||||
pub buff_type_count: usize,
|
||||
pub field_52: i32,
|
||||
pub damage_multiplier: i32,
|
||||
pub field_56: i32,
|
||||
pub field_60: i32,
|
||||
pub field_64: i32,
|
||||
pub field_68: i32,
|
||||
pub size_multiplier: i32,
|
||||
pub field_72: i32,
|
||||
}
|
||||
impl LogicCharacterBuffComponent {
|
||||
pub fn empty(buff_type_count: usize) -> Self {
|
||||
Self {
|
||||
buff_type_count,
|
||||
damage_multiplier: 100,
|
||||
size_multiplier: 100,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -187,15 +219,25 @@ impl LogicCharacterBuffComponent {
|
|||
for _ in 0..self.buff_type_count {
|
||||
writer.write_boolean(false);
|
||||
}
|
||||
writer.write_vint(self.field_52);
|
||||
writer.write_vint(self.damage_multiplier);
|
||||
writer.write_vint(self.field_56);
|
||||
writer.write_vint(self.field_60);
|
||||
writer.write_vint(self.field_64);
|
||||
writer.write_vint(self.field_68);
|
||||
writer.write_vint(self.size_multiplier);
|
||||
writer.write_vint(self.field_72);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod buff_component_tests {
|
||||
use super::LogicCharacterBuffComponent;
|
||||
#[test]
|
||||
fn an_unbuffed_component_reports_full_size_and_damage() {
|
||||
let component = LogicCharacterBuffComponent::empty(5);
|
||||
assert_eq!(component.size_multiplier, 100);
|
||||
assert_eq!(component.damage_multiplier, 100);
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LogicComponent {
|
||||
Combat(LogicCombatComponent),
|
||||
|
|
@ -204,6 +246,14 @@ pub enum LogicComponent {
|
|||
Buff(LogicCharacterBuffComponent),
|
||||
}
|
||||
impl LogicComponent {
|
||||
pub fn kind(&self) -> &'static str {
|
||||
match self {
|
||||
LogicComponent::Combat(_) => "combat",
|
||||
LogicComponent::Movement(_) => "movement",
|
||||
LogicComponent::Hitpoint(_) => "hitpoint",
|
||||
LogicComponent::Buff(_) => "buff",
|
||||
}
|
||||
}
|
||||
pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||
match self {
|
||||
LogicComponent::Combat(component) => component.encode(writer),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use titan::{ByteStreamWriter, Payload, Result};
|
||||
use crate::battle::logic_battle::LogicBattle;
|
||||
use crate::battle::logic_time::LogicTime;
|
||||
use crate::battle::logic_tutorial_manager::LogicTutorialManager;
|
||||
use crate::logic_random::LogicRandom;
|
||||
use crate::model::LogicClientAvatar;
|
||||
use titan::{ByteStreamWriter, Payload, Result};
|
||||
pub const SECTION_BATTLE: i32 = 11;
|
||||
pub const SECTION_TUTORIAL: i32 = 12;
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
|
|
@ -16,40 +16,57 @@ pub struct LogicGameMode {
|
|||
pub tutorial_manager: LogicTutorialManager,
|
||||
}
|
||||
impl LogicGameMode {
|
||||
pub fn snapshot(&self) -> Result<Vec<u8>> {
|
||||
pub fn snapshot(&self, commands: &[Vec<u8>]) -> Result<Vec<u8>> {
|
||||
let mut writer = ByteStreamWriter::new();
|
||||
self.encode(&mut writer)?;
|
||||
self.write(&mut writer, Some(commands))?;
|
||||
Ok(writer.into_inner())
|
||||
}
|
||||
}
|
||||
impl LogicGameMode {
|
||||
pub fn write(&self, writer: &mut ByteStreamWriter, with_commands: bool) -> Result<i32> {
|
||||
pub fn write(
|
||||
&self,
|
||||
writer: &mut ByteStreamWriter,
|
||||
commands: Option<&[Vec<u8>]>,
|
||||
) -> Result<i32> {
|
||||
titan::checksum::checksum_trace_note("gamemode.tick, checkpoint, section");
|
||||
writer.write_vint(self.time.tick);
|
||||
writer.write_checksum_checkpoint();
|
||||
writer.write_vint(SECTION_BATTLE);
|
||||
titan::checksum::checksum_trace_note("time");
|
||||
self.time.encode(writer)?;
|
||||
titan::checksum::checksum_trace_note("random, seed");
|
||||
self.random.encode(writer)?;
|
||||
writer.write_vint(self.random_seed);
|
||||
self.battle.encode(writer)?;
|
||||
for avatar in &self.avatars {
|
||||
for (index, avatar) in self.avatars.iter().enumerate() {
|
||||
titan::checksum::checksum_trace_note(format!("avatar[{index}]"));
|
||||
avatar.encode(writer)?;
|
||||
}
|
||||
titan::checksum::checksum_trace_note("tutorial");
|
||||
writer.write_vint(SECTION_TUTORIAL);
|
||||
self.tutorial_manager.encode(writer)?;
|
||||
let checksum = writer.write_checksum_checkpoint();
|
||||
if with_commands {
|
||||
writer.write_vint(0);
|
||||
if let Some(commands) = commands {
|
||||
writer.write_vint(commands.len() as i32);
|
||||
for command in commands {
|
||||
writer.write_raw(command);
|
||||
}
|
||||
}
|
||||
Ok(checksum)
|
||||
}
|
||||
pub fn calculate_checksum(&self) -> Result<i32> {
|
||||
let mut writer = ByteStreamWriter::new();
|
||||
self.write(&mut writer, false)
|
||||
self.write(&mut writer, None)
|
||||
}
|
||||
pub fn checksum_stream(&self) -> Result<(i32, Vec<u8>)> {
|
||||
let mut writer = ByteStreamWriter::new();
|
||||
let checksum = self.write(&mut writer, None)?;
|
||||
Ok((checksum, writer.into_inner()))
|
||||
}
|
||||
}
|
||||
impl Payload for LogicGameMode {
|
||||
fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||
self.write(writer, true)?;
|
||||
self.write(writer, Some(&[]))?;
|
||||
Ok(())
|
||||
}
|
||||
fn decode(_reader: &mut titan::ByteStreamReader<'_>) -> Result<Self> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use titan::Payload;
|
||||
use crate::battle::logic_game_object_ref::LogicGameObjectRef;
|
||||
use crate::data::LogicDataRef;
|
||||
use titan::Payload;
|
||||
pub const COMPONENT_PASSES: usize = 4;
|
||||
pub const OBJECT_TYPE_COUNT: usize = 6;
|
||||
pub const CHARACTER_OBJECT_TYPE: i32 = 5;
|
||||
|
|
@ -35,30 +35,83 @@ impl LogicGameObject {
|
|||
pub enum LogicObjectBody {
|
||||
Character(Box<crate::battle::logic_character::LogicCharacter>),
|
||||
Summoner(Box<crate::battle::logic_character::LogicSummoner>),
|
||||
Projectile(Box<crate::battle::logic_projectile::LogicProjectile>),
|
||||
}
|
||||
impl LogicObjectBody {
|
||||
pub fn base(&self) -> &LogicGameObject {
|
||||
match self {
|
||||
LogicObjectBody::Character(character) => &character.base,
|
||||
LogicObjectBody::Summoner(summoner) => &summoner.character.base,
|
||||
LogicObjectBody::Projectile(projectile) => &projectile.base,
|
||||
}
|
||||
}
|
||||
pub fn level_index(&self) -> i32 {
|
||||
match self {
|
||||
LogicObjectBody::Character(character) => character.level_index,
|
||||
LogicObjectBody::Summoner(summoner) => summoner.character.level_index,
|
||||
LogicObjectBody::Projectile(projectile) => projectile.level_index,
|
||||
}
|
||||
}
|
||||
pub fn base_mut(&mut self) -> &mut LogicGameObject {
|
||||
match self {
|
||||
LogicObjectBody::Character(character) => &mut character.base,
|
||||
LogicObjectBody::Summoner(summoner) => &mut summoner.character.base,
|
||||
LogicObjectBody::Projectile(projectile) => &mut projectile.base,
|
||||
}
|
||||
}
|
||||
pub fn lane_id(&self) -> i32 {
|
||||
match self {
|
||||
LogicObjectBody::Character(character) => character.lane_id,
|
||||
LogicObjectBody::Summoner(summoner) => summoner.character.lane_id,
|
||||
LogicObjectBody::Projectile(_) => 0,
|
||||
}
|
||||
}
|
||||
pub fn set_lane_id(&mut self, lane: i32) {
|
||||
match self {
|
||||
LogicObjectBody::Character(character) => character.lane_id = lane,
|
||||
LogicObjectBody::Summoner(summoner) => summoner.character.lane_id = lane,
|
||||
LogicObjectBody::Projectile(_) => {}
|
||||
}
|
||||
}
|
||||
pub fn state(&self) -> i32 {
|
||||
match self {
|
||||
LogicObjectBody::Character(character) => character.state,
|
||||
LogicObjectBody::Summoner(summoner) => summoner.character.state,
|
||||
LogicObjectBody::Projectile(_) => 0,
|
||||
}
|
||||
}
|
||||
pub fn set_state(&mut self, state: i32) {
|
||||
match self {
|
||||
LogicObjectBody::Character(character) => character.state = state,
|
||||
LogicObjectBody::Summoner(summoner) => summoner.character.state = state,
|
||||
LogicObjectBody::Projectile(_) => {}
|
||||
}
|
||||
}
|
||||
pub fn set_direction(&mut self, direction: LogicVector2) {
|
||||
match self {
|
||||
LogicObjectBody::Character(character) => character.direction = direction,
|
||||
LogicObjectBody::Summoner(summoner) => summoner.character.direction = direction,
|
||||
LogicObjectBody::Projectile(_) => {}
|
||||
}
|
||||
}
|
||||
pub fn add_pending_physical_damage(&mut self, amount: i32) {
|
||||
match self {
|
||||
LogicObjectBody::Character(character) => {
|
||||
character.pending_physical_damage =
|
||||
(character.pending_physical_damage + amount).max(0)
|
||||
}
|
||||
LogicObjectBody::Summoner(summoner) => {
|
||||
summoner.character.pending_physical_damage =
|
||||
(summoner.character.pending_physical_damage + amount).max(0)
|
||||
}
|
||||
LogicObjectBody::Projectile(_) => {}
|
||||
}
|
||||
}
|
||||
pub fn encode(&self, writer: &mut titan::ByteStreamWriter) -> titan::Result<()> {
|
||||
match self {
|
||||
LogicObjectBody::Character(character) => character.encode(writer),
|
||||
LogicObjectBody::Summoner(summoner) => summoner.encode(writer),
|
||||
LogicObjectBody::Projectile(projectile) => projectile.encode(writer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -83,8 +136,14 @@ impl LogicGameObjectEntry {
|
|||
}
|
||||
}
|
||||
pub fn is_alive(&self) -> bool {
|
||||
if let LogicObjectBody::Projectile(projectile) = &self.body {
|
||||
return !projectile.destroyed;
|
||||
}
|
||||
self.hitpoints().map(|value| value > 0).unwrap_or(false)
|
||||
}
|
||||
pub fn is_projectile(&self) -> bool {
|
||||
matches!(self.body, LogicObjectBody::Projectile(_))
|
||||
}
|
||||
pub fn new(
|
||||
data: LogicDataRef,
|
||||
global_id: LogicGameObjectRef,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use titan::{ByteStreamReader, ByteStreamWriter, Payload, Result};
|
||||
use crate::battle::logic_game_object::{LogicGameObjectEntry, COMPONENT_PASSES, OBJECT_TYPE_COUNT};
|
||||
use titan::{ByteStreamReader, ByteStreamWriter, Payload, Result};
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LogicGameObjectManager {
|
||||
pub instance_counters: [i32; OBJECT_TYPE_COUNT],
|
||||
|
|
@ -14,6 +14,30 @@ impl Default for LogicGameObjectManager {
|
|||
}
|
||||
}
|
||||
impl LogicGameObjectManager {
|
||||
pub fn reserve_instance(&mut self, object_type: i32) -> i32 {
|
||||
let Ok(index) = usize::try_from(object_type) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(counter) = self.instance_counters.get_mut(index) else {
|
||||
return 0;
|
||||
};
|
||||
let instance = *counter;
|
||||
*counter = instance + 1;
|
||||
instance
|
||||
}
|
||||
fn note(&self, index: usize, entry: &LogicGameObjectEntry, section: &str) {
|
||||
if !titan::checksum::checksum_trace_active() {
|
||||
return;
|
||||
}
|
||||
let id = match entry.global_id.0 {
|
||||
Some(id) => format!("{}:{}", id.class_id, id.instance_id),
|
||||
None => "-".to_string(),
|
||||
};
|
||||
titan::checksum::checksum_trace_note(format!(
|
||||
"objects[{index}].{section} {id} {}",
|
||||
entry.data.name()
|
||||
));
|
||||
}
|
||||
pub fn push(&mut self, entry: LogicGameObjectEntry) {
|
||||
let object_type = entry.object_type();
|
||||
if let Ok(index) = usize::try_from(object_type) {
|
||||
|
|
@ -29,22 +53,28 @@ impl LogicGameObjectManager {
|
|||
}
|
||||
impl Payload for LogicGameObjectManager {
|
||||
fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||
titan::checksum::checksum_trace_note("objects.instance_counters");
|
||||
for counter in self.instance_counters {
|
||||
writer.write_vint(counter);
|
||||
}
|
||||
titan::checksum::checksum_trace_note("objects.count");
|
||||
writer.write_vint(self.objects.len() as i32);
|
||||
for entry in &self.objects {
|
||||
for (index, entry) in self.objects.iter().enumerate() {
|
||||
self.note(index, entry, "data");
|
||||
entry.data.encode(writer)?;
|
||||
}
|
||||
for entry in &self.objects {
|
||||
for (index, entry) in self.objects.iter().enumerate() {
|
||||
self.note(index, entry, "global_id");
|
||||
entry.global_id.encode(writer)?;
|
||||
}
|
||||
for entry in &self.objects {
|
||||
for (index, entry) in self.objects.iter().enumerate() {
|
||||
self.note(index, entry, "body");
|
||||
entry.body.encode(writer)?;
|
||||
}
|
||||
for pass in 0..COMPONENT_PASSES {
|
||||
for entry in &self.objects {
|
||||
for (index, entry) in self.objects.iter().enumerate() {
|
||||
if let Some(component) = &entry.components[pass] {
|
||||
self.note(index, entry, component.kind());
|
||||
component.encode(writer)?;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,119 +1,340 @@
|
|||
use std::collections::BinaryHeap;
|
||||
use crate::battle::logic_tilemap::{
|
||||
LogicTilemap, GROUND_COST, SUBTILE_UNITS, WATER_COST, WATER_COST_JUMPING,
|
||||
};
|
||||
pub const PATH_STEP_LIMIT: usize = 4096;
|
||||
fn tile_of(units: i32) -> i32 {
|
||||
units.div_euclid(SUBTILE_UNITS)
|
||||
use crate::battle::logic_simulation::distance_squared;
|
||||
use crate::battle::logic_tilemap::{LogicTilemap, SUBTILE_UNITS};
|
||||
pub const COST_UNREACHABLE: i32 = 0xFFF_FFFF;
|
||||
pub const COST_MAX_TRAVERSABLE: i32 = 268_435_454;
|
||||
#[inline]
|
||||
pub fn tile_of(units: i32) -> i32 {
|
||||
units / SUBTILE_UNITS
|
||||
}
|
||||
fn centre_of(tile: i32) -> i32 {
|
||||
#[inline]
|
||||
pub fn centre_of(tile: i32) -> i32 {
|
||||
tile * SUBTILE_UNITS + SUBTILE_UNITS / 2
|
||||
}
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct Step {
|
||||
estimate: i32,
|
||||
cost: i32,
|
||||
index: usize,
|
||||
#[inline]
|
||||
pub fn path_finder_cost(tm: &LogicTilemap, x: i32, y: i32, lane_id: i32, jump: bool) -> i32 {
|
||||
if (x | y) < 0 || tm.width() <= x || tm.height() <= y {
|
||||
return COST_UNREACHABLE;
|
||||
}
|
||||
impl Ord for Step {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
other
|
||||
.estimate
|
||||
.cmp(&self.estimate)
|
||||
.then_with(|| other.index.cmp(&self.index))
|
||||
let v = tm.tile_at(x, y);
|
||||
if (v >> 5) & 1 == 1 {
|
||||
if jump {
|
||||
20
|
||||
} else {
|
||||
800
|
||||
}
|
||||
} else {
|
||||
let lane = v & 3;
|
||||
if lane != 0 {
|
||||
if lane_id == lane {
|
||||
1
|
||||
} else {
|
||||
5
|
||||
}
|
||||
} else {
|
||||
20
|
||||
}
|
||||
}
|
||||
impl PartialOrd for Step {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
#[inline]
|
||||
pub fn is_passable_path_finder(tm: &LogicTilemap, x: i32, y: i32) -> bool {
|
||||
(x | y) >= 0 && tm.width() > x && tm.height() > y
|
||||
}
|
||||
struct AStar<'a> {
|
||||
tm: &'a LogicTilemap,
|
||||
width: i32,
|
||||
height: i32,
|
||||
goal_x: i32,
|
||||
goal_y: i32,
|
||||
lane_id: i32,
|
||||
jump: bool,
|
||||
state: Vec<u8>,
|
||||
came_from: Vec<i32>,
|
||||
score: Vec<i32>,
|
||||
heap: Vec<i32>,
|
||||
heap_size: usize,
|
||||
}
|
||||
impl AStar<'_> {
|
||||
#[inline]
|
||||
fn heuristic(&self, x: i32, y: i32) -> i32 {
|
||||
10 * (self.goal_x - x).abs().max((self.goal_y - y).abs())
|
||||
}
|
||||
fn heap_add(&mut self, node: i32) {
|
||||
let pos = self.heap_size;
|
||||
self.heap[pos] = node;
|
||||
self.heap_size += 1;
|
||||
if pos >= 1 {
|
||||
let key = self.score[node as usize];
|
||||
let mut v2 = pos as i32;
|
||||
let mut v4 = pos as i32 - 1;
|
||||
loop {
|
||||
let parent_idx = v4 >> 1;
|
||||
let parent = self.heap[parent_idx as usize];
|
||||
if key >= self.score[parent as usize] {
|
||||
break;
|
||||
}
|
||||
self.heap[v2 as usize] = parent;
|
||||
v2 = parent_idx;
|
||||
self.heap[parent_idx as usize] = node;
|
||||
v4 = parent_idx - 1;
|
||||
if parent_idx < 1 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn remove_smallest(&mut self) -> i32 {
|
||||
let v1 = self.heap_size;
|
||||
if v1 == 0 {
|
||||
return -1;
|
||||
}
|
||||
let root = self.heap[0];
|
||||
let last = self.heap[v1 - 1];
|
||||
self.heap_size = v1 - 1;
|
||||
self.heap[0] = last;
|
||||
let mut pos = 0i32;
|
||||
loop {
|
||||
let left = (2 * pos) | 1;
|
||||
let right = 2 * pos + 2;
|
||||
let size = self.heap_size as i32;
|
||||
let mut best = pos;
|
||||
if right < size {
|
||||
if self.score[last as usize] <= self.score[self.heap[right as usize] as usize] {
|
||||
best = pos;
|
||||
} else {
|
||||
best = right;
|
||||
}
|
||||
}
|
||||
if left < size
|
||||
&& self.score[self.heap[best as usize] as usize]
|
||||
> self.score[self.heap[left as usize] as usize]
|
||||
{
|
||||
best = left;
|
||||
}
|
||||
if best == pos {
|
||||
break;
|
||||
}
|
||||
self.heap[pos as usize] = self.heap[best as usize];
|
||||
self.heap[best as usize] = last;
|
||||
pos = best;
|
||||
}
|
||||
root
|
||||
}
|
||||
fn relax(&mut self, cur: i32, nx: i32, ny: i32, nidx: i32, weight: i32) {
|
||||
if ny < 0 {
|
||||
return;
|
||||
}
|
||||
if nx < 0 || self.height <= ny {
|
||||
return;
|
||||
}
|
||||
if self.width <= nx {
|
||||
return;
|
||||
}
|
||||
let cost = path_finder_cost(self.tm, nx, ny, self.lane_id, self.jump);
|
||||
if cost <= COST_MAX_TRAVERSABLE && self.state[nidx as usize] != 2 {
|
||||
let g = self.score[cur as usize] + cost * weight;
|
||||
let f = g + self.heuristic(nx, ny);
|
||||
if self.state[nidx as usize] == 0 {
|
||||
self.state[nidx as usize] = 1;
|
||||
self.came_from[nidx as usize] = cur;
|
||||
self.score[nidx as usize] = f;
|
||||
self.heap_add(nidx);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn expand(&mut self, node: i32) {
|
||||
let w = self.width;
|
||||
let y = node / w;
|
||||
let x = node % w;
|
||||
self.relax(node, x, y - 1, node - w, 10);
|
||||
self.relax(node, x, y + 1, node + w, 10);
|
||||
self.relax(node, x - 1, y, node - 1, 10);
|
||||
self.relax(node, x + 1, y, node + 1, 10);
|
||||
self.relax(node, x - 1, y - 1, node - 1 - w, 14);
|
||||
self.relax(node, x - 1, y + 1, node - 1 + w, 14);
|
||||
self.relax(node, x + 1, y + 1, node + 1 + w, 14);
|
||||
self.relax(node, x + 1, y - 1, node + 1 - w, 14);
|
||||
}
|
||||
fn run(&mut self, start: i32, goal: i32) -> Vec<i32> {
|
||||
self.goal_x = goal % self.width;
|
||||
self.goal_y = goal / self.width;
|
||||
self.came_from[start as usize] = -1;
|
||||
self.came_from[goal as usize] = -1;
|
||||
self.expand(start);
|
||||
self.state[start as usize] = 2;
|
||||
if self.heap_size > 0 {
|
||||
loop {
|
||||
let v = self.remove_smallest();
|
||||
self.state[v as usize] = 2;
|
||||
self.expand(v);
|
||||
if self.state[goal as usize] == 2 || self.heap_size == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut path = Vec::new();
|
||||
let mut cur = goal;
|
||||
let mut i = self.came_from[goal as usize];
|
||||
while i != -1 {
|
||||
path.push(cur);
|
||||
cur = i;
|
||||
i = self.came_from[i as usize];
|
||||
}
|
||||
path
|
||||
}
|
||||
}
|
||||
pub fn find_path(
|
||||
tilemap: &LogicTilemap,
|
||||
from: (i32, i32),
|
||||
to: (i32, i32),
|
||||
jumps: bool,
|
||||
) -> Vec<(i32, i32)> {
|
||||
let start = (tile_of(from.0), tile_of(from.1));
|
||||
let goal = (tile_of(to.0), tile_of(to.1));
|
||||
if start == goal
|
||||
|| !tilemap.is_inside(start.0, start.1)
|
||||
|| !tilemap.is_inside(goal.0, goal.1)
|
||||
{
|
||||
tm: &LogicTilemap,
|
||||
start_tile: (i32, i32),
|
||||
goal_tile: (i32, i32),
|
||||
lane_id: i32,
|
||||
jump: bool,
|
||||
) -> Vec<i32> {
|
||||
let width = tm.width();
|
||||
let height = tm.height();
|
||||
if width < 1 || height < 1 {
|
||||
return Vec::new();
|
||||
}
|
||||
let width = tilemap.width().max(1) as usize;
|
||||
let height = tilemap.height().max(1) as usize;
|
||||
let index_of = |x: i32, y: i32| y as usize * width + x as usize;
|
||||
let mut cost = vec![i32::MAX; width * height];
|
||||
let mut came_from = vec![usize::MAX; width * height];
|
||||
let mut open = BinaryHeap::new();
|
||||
let start_index = index_of(start.0, start.1);
|
||||
cost[start_index] = 0;
|
||||
open.push(Step {
|
||||
estimate: 0,
|
||||
cost: 0,
|
||||
index: start_index,
|
||||
});
|
||||
let goal_index = index_of(goal.0, goal.1);
|
||||
let mut visited = 0;
|
||||
while let Some(step) = open.pop() {
|
||||
if step.index == goal_index {
|
||||
break;
|
||||
if !is_passable_path_finder(tm, start_tile.0, start_tile.1) {
|
||||
return Vec::new();
|
||||
}
|
||||
if step.cost > cost[step.index] {
|
||||
continue;
|
||||
if path_finder_cost(tm, goal_tile.0, goal_tile.1, lane_id, jump) >= COST_UNREACHABLE {
|
||||
return Vec::new();
|
||||
}
|
||||
visited += 1;
|
||||
if visited > PATH_STEP_LIMIT {
|
||||
break;
|
||||
}
|
||||
let x = (step.index % width) as i32;
|
||||
let y = (step.index / width) as i32;
|
||||
for (dx, dy) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
|
||||
let (nx, ny) = (x + dx, y + dy);
|
||||
if !tilemap.is_inside(nx, ny) {
|
||||
continue;
|
||||
}
|
||||
let water = tilemap.is_water(nx, ny);
|
||||
let step_cost = if water {
|
||||
if jumps {
|
||||
WATER_COST_JUMPING
|
||||
} else {
|
||||
WATER_COST
|
||||
}
|
||||
} else {
|
||||
GROUND_COST
|
||||
let start = start_tile.0 + width * start_tile.1;
|
||||
let goal = goal_tile.0 + width * goal_tile.1;
|
||||
let n = (width * height) as usize;
|
||||
let mut a = AStar {
|
||||
tm,
|
||||
width,
|
||||
height,
|
||||
goal_x: 0,
|
||||
goal_y: 0,
|
||||
lane_id,
|
||||
jump,
|
||||
state: vec![0u8; n],
|
||||
came_from: vec![-1i32; n],
|
||||
score: vec![0i32; n],
|
||||
heap: vec![0i32; n],
|
||||
heap_size: 0,
|
||||
};
|
||||
let next = index_of(nx, ny);
|
||||
let total = step.cost.saturating_add(step_cost);
|
||||
if total >= cost[next] {
|
||||
continue;
|
||||
a.run(start, goal)
|
||||
}
|
||||
cost[next] = total;
|
||||
came_from[next] = step.index;
|
||||
let heuristic = (goal.0 - nx).abs() + (goal.1 - ny).abs();
|
||||
open.push(Step {
|
||||
estimate: total.saturating_add(heuristic),
|
||||
cost: total,
|
||||
index: next,
|
||||
});
|
||||
pub fn get_lane_id(tm: &LogicTilemap, deploy_x: i32, deploy_y: i32) -> i32 {
|
||||
let (mw, mh) = (tm.width(), tm.height());
|
||||
if mw < 1 {
|
||||
return 0;
|
||||
}
|
||||
let dx = deploy_x / SUBTILE_UNITS;
|
||||
let ndy = deploy_y / -SUBTILE_UNITS;
|
||||
let mut best_lane = 0i32;
|
||||
let mut best_dist = i32::MAX;
|
||||
let mut x = 0;
|
||||
while x < mw {
|
||||
let mut y = 0;
|
||||
while y < mh {
|
||||
let lane = tm.tile_at(x, y) & 3;
|
||||
let d = (x - dx) * (x - dx) + (ndy + y) * (ndy + y);
|
||||
if lane >= 1 && d < best_dist {
|
||||
best_dist = d;
|
||||
best_lane = lane;
|
||||
}
|
||||
y += 1;
|
||||
}
|
||||
x += 1;
|
||||
}
|
||||
best_lane
|
||||
}
|
||||
pub fn spawn_lane_of(tm: &LogicTilemap, x: i32, y: i32) -> i32 {
|
||||
let x = x.clamp(250, SUBTILE_UNITS * tm.width() - 250);
|
||||
let y = y.clamp(250, SUBTILE_UNITS * tm.height() - 250);
|
||||
get_lane_id(tm, x, y)
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn closest_tile_position_to_target(
|
||||
tm: &LogicTilemap,
|
||||
unit_x: i32,
|
||||
unit_y: i32,
|
||||
target_x: i32,
|
||||
target_y: i32,
|
||||
target_tile_x: i32,
|
||||
target_tile_y: i32,
|
||||
range: i32,
|
||||
healing_power: i32,
|
||||
owner_top: bool,
|
||||
) -> Option<(i32, i32)> {
|
||||
let (width, height) = (tm.width(), tm.height());
|
||||
let (ref_x, ref_y) = if healing_power < 1 {
|
||||
(unit_x, unit_y)
|
||||
} else {
|
||||
(target_x, if owner_top { -range } else { range } + target_y)
|
||||
};
|
||||
let r = range / SUBTILE_UNITS + 1;
|
||||
let x_min = (target_tile_x - r).max(0);
|
||||
let x_max = (r + target_tile_x).min(width - 1);
|
||||
let y_min = (target_tile_y - r).max(0);
|
||||
let y_max = (r + target_tile_y).min(height - 1);
|
||||
if y_min > y_max {
|
||||
return None;
|
||||
}
|
||||
let range_sq = range.wrapping_mul(range);
|
||||
let mut best = i32::MAX;
|
||||
let mut best_tile: Option<(i32, i32)> = None;
|
||||
let mut ty = y_min;
|
||||
while ty <= y_max {
|
||||
let cy = SUBTILE_UNITS * ty + SUBTILE_UNITS / 2;
|
||||
let dy_sq = (cy - ref_y).wrapping_mul(cy - ref_y);
|
||||
let mut tx = x_min;
|
||||
while tx <= x_max {
|
||||
let cx = SUBTILE_UNITS * tx + SUBTILE_UNITS / 2;
|
||||
if is_passable_path_finder(tm, tx, ty)
|
||||
&& distance_squared((target_x, target_y), (cx, cy)) <= range_sq
|
||||
{
|
||||
let d = dy_sq.wrapping_add((cx - ref_x).wrapping_mul(cx - ref_x));
|
||||
if d < best {
|
||||
best = d;
|
||||
best_tile = Some((tx, ty));
|
||||
}
|
||||
}
|
||||
if came_from[goal_index] == usize::MAX {
|
||||
return Vec::new();
|
||||
tx += 1;
|
||||
}
|
||||
let mut path = Vec::new();
|
||||
let mut cursor = goal_index;
|
||||
while cursor != start_index {
|
||||
let x = (cursor % width) as i32;
|
||||
let y = (cursor / width) as i32;
|
||||
path.push((centre_of(x), centre_of(y)));
|
||||
cursor = came_from[cursor];
|
||||
if cursor == usize::MAX {
|
||||
return Vec::new();
|
||||
ty += 1;
|
||||
}
|
||||
best_tile
|
||||
}
|
||||
#[inline]
|
||||
pub fn vec_length(x: i32, y: i32) -> i32 {
|
||||
crate::logic_sqrt(distance_squared((0, 0), (x, y)))
|
||||
}
|
||||
#[inline]
|
||||
pub fn normalize_to(x: &mut i32, y: &mut i32, target_len: i32) {
|
||||
let len = vec_length(*x, *y);
|
||||
if len != 0 {
|
||||
*x = x.wrapping_mul(target_len) / len;
|
||||
*y = y.wrapping_mul(target_len) / len;
|
||||
}
|
||||
}
|
||||
path.reverse();
|
||||
path
|
||||
pub fn path_target_normal(path: &[i32], unit_x: i32, unit_y: i32, width: i32) -> (i32, i32) {
|
||||
if path.is_empty() {
|
||||
return (0, 0);
|
||||
}
|
||||
let node = path[path.len() - 1];
|
||||
let mut nx = centre_of(node % width) - unit_x;
|
||||
let mut ny = centre_of(node / width) - unit_y;
|
||||
normalize_to(&mut nx, &mut ny, 256);
|
||||
(nx, ny)
|
||||
}
|
||||
pub fn target_position_where_going_now(
|
||||
path: &[i32],
|
||||
width: i32,
|
||||
target_pos: Option<(i32, i32)>,
|
||||
unit_tile_x: i32,
|
||||
unit_tile_y: i32,
|
||||
) -> (i32, i32) {
|
||||
if let Some(&node) = path.last() {
|
||||
(centre_of(node % width), centre_of(node / width))
|
||||
} else if let Some(p) = target_pos {
|
||||
p
|
||||
} else {
|
||||
(centre_of(unit_tile_x), centre_of(unit_tile_y))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
crates/logic/src/battle/logic_projectile.rs
Normal file
43
crates/logic/src/battle/logic_projectile.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use crate::battle::logic_game_object::{LogicGameObject, LogicVector2};
|
||||
use crate::battle::logic_game_object_ref::LogicGameObjectRef;
|
||||
use crate::data::LogicDataRef;
|
||||
use titan::{ByteStreamWriter, GlobalId, Payload, Result};
|
||||
pub const PROJECTILE_OBJECT_TYPE: i32 = 3;
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct LogicProjectile {
|
||||
pub destroyed: bool,
|
||||
pub base: LogicGameObject,
|
||||
pub target_position: LogicVector2,
|
||||
pub start_position: LogicVector2,
|
||||
pub offset: LogicVector2,
|
||||
pub target: LogicGameObjectRef,
|
||||
pub source: LogicGameObjectRef,
|
||||
pub aux_data: LogicDataRef,
|
||||
pub effect: LogicDataRef,
|
||||
pub level_index: i32,
|
||||
pub target_z: i32,
|
||||
pub start_z: i32,
|
||||
pub hit_objects: Vec<GlobalId>,
|
||||
}
|
||||
impl LogicProjectile {
|
||||
pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||
writer.write_boolean(self.destroyed);
|
||||
self.base.encode_base(writer)?;
|
||||
self.target_position.encode(writer)?;
|
||||
self.start_position.encode(writer)?;
|
||||
self.offset.encode(writer)?;
|
||||
self.target.encode(writer)?;
|
||||
self.source.encode(writer)?;
|
||||
self.aux_data.encode(writer)?;
|
||||
self.effect.encode(writer)?;
|
||||
writer.write_vint(self.level_index);
|
||||
writer.write_vint(self.target_z);
|
||||
writer.write_vint(self.start_z);
|
||||
writer.write_vint(self.hit_objects.len() as i32);
|
||||
for id in &self.hit_objects {
|
||||
writer.write_vint(id.class_id);
|
||||
writer.write_vint(id.instance_id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
210
crates/logic/src/battle/logic_summoner.rs
Normal file
210
crates/logic/src/battle/logic_summoner.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
use crate::battle::logic_game_object::LogicVector2;
|
||||
use crate::battle::logic_simulation::distance_squared;
|
||||
use crate::battle::logic_tilemap::{LogicTilemap, SUBTILE_UNITS};
|
||||
use crate::data::LogicCharacterData;
|
||||
pub const DEPLOY_CELL_UNITS: i32 = 1000;
|
||||
pub const FIND_POSITION_MAX_RING: i32 = 30;
|
||||
pub const DEPLOY_BORDER_TILES: i32 = 0;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DeployBlocker {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub size_w: i32,
|
||||
pub size_h: i32,
|
||||
}
|
||||
pub fn snap_coordinate(character: &LogicCharacterData, value: i32) -> i32 {
|
||||
let base = DEPLOY_CELL_UNITS * (value / DEPLOY_CELL_UNITS);
|
||||
let centred = !character.is_building() || character.size_in_tiles() & 1 == 1;
|
||||
if centred {
|
||||
base + 500
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
pub fn build_free_tile_map(blockers: &[DeployBlocker], cell_w: i32, cell_h: i32) -> Vec<bool> {
|
||||
let mut free = vec![true; (cell_w.max(0) * cell_h.max(0)) as usize];
|
||||
for blocker in blockers {
|
||||
if blocker.size_w == 0 {
|
||||
continue;
|
||||
}
|
||||
let y_from = (blocker.y - 500 * blocker.size_h) / DEPLOY_CELL_UNITS;
|
||||
let y_to = (blocker.y + 500 * blocker.size_h) / DEPLOY_CELL_UNITS;
|
||||
let x_from = (blocker.x - 500 * blocker.size_w) / DEPLOY_CELL_UNITS;
|
||||
let x_to = (blocker.x + 500 * blocker.size_w) / DEPLOY_CELL_UNITS;
|
||||
let mut y = y_from;
|
||||
while y < y_to {
|
||||
if y >= 0 && y < cell_h {
|
||||
let mut x = x_from;
|
||||
while x < x_to {
|
||||
if x >= 0 && x < cell_w {
|
||||
free[(y * cell_w + x) as usize] = false;
|
||||
}
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
free
|
||||
}
|
||||
pub fn position_free(
|
||||
free: &[bool],
|
||||
cell_w: i32,
|
||||
cell_h: i32,
|
||||
pos: (i32, i32),
|
||||
size_in_tiles: i32,
|
||||
tilemap: &LogicTilemap,
|
||||
) -> bool {
|
||||
let x0 = (pos.0 - 500 * size_in_tiles) / DEPLOY_CELL_UNITS;
|
||||
let x1 = (pos.0 + 500 * size_in_tiles) / DEPLOY_CELL_UNITS;
|
||||
if x0 >= x1 {
|
||||
return true;
|
||||
}
|
||||
let y0 = (pos.1 - 500 * size_in_tiles) / DEPLOY_CELL_UNITS;
|
||||
let y1 = (pos.1 + 500 * size_in_tiles) / DEPLOY_CELL_UNITS;
|
||||
let mut x = x0;
|
||||
while x < x1 {
|
||||
let mut y = y0;
|
||||
while y < y1 {
|
||||
if x < 0 || y < 0 || x >= cell_w || y >= cell_h {
|
||||
return false;
|
||||
}
|
||||
if !free[(y * cell_w + x) as usize] {
|
||||
return false;
|
||||
}
|
||||
let mut quadrant = 0;
|
||||
while quadrant < 4 {
|
||||
if !tilemap.can_place_egg(2 * x + (quadrant & 1), 2 * y + (quadrant >> 1)) {
|
||||
return false;
|
||||
}
|
||||
quadrant += 1;
|
||||
}
|
||||
y += 1;
|
||||
}
|
||||
x += 1;
|
||||
}
|
||||
true
|
||||
}
|
||||
pub fn roads_beneath(tilemap: &LogicTilemap, pos: (i32, i32), size_in_tiles: i32) -> bool {
|
||||
let from_x = (pos.0 - 500 * size_in_tiles) / SUBTILE_UNITS;
|
||||
let to_x = (pos.0 + 500 * size_in_tiles) / SUBTILE_UNITS;
|
||||
let from_y = (pos.1 - 500 * size_in_tiles) / SUBTILE_UNITS;
|
||||
let to_y = (pos.1 + 500 * size_in_tiles) / SUBTILE_UNITS;
|
||||
let mut x = from_x;
|
||||
while x < to_x {
|
||||
let mut y = from_y;
|
||||
while y < to_y {
|
||||
if tilemap.lane_bits(x, y) >= 1 {
|
||||
return true;
|
||||
}
|
||||
y += 1;
|
||||
}
|
||||
x += 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
pub fn check_spell_position(
|
||||
tilemap: &LogicTilemap,
|
||||
x: i32,
|
||||
y: i32,
|
||||
summons_a_character: bool,
|
||||
) -> i32 {
|
||||
if !summons_a_character {
|
||||
return 0;
|
||||
}
|
||||
if x < -499 {
|
||||
return 1;
|
||||
}
|
||||
let tile_x = x / SUBTILE_UNITS;
|
||||
let tile_y = y / SUBTILE_UNITS;
|
||||
if tile_y < DEPLOY_BORDER_TILES {
|
||||
return 2;
|
||||
}
|
||||
if tile_x >= tilemap.width() {
|
||||
return 3;
|
||||
}
|
||||
if tile_y >= tilemap.height() - DEPLOY_BORDER_TILES {
|
||||
return 4;
|
||||
}
|
||||
if !tilemap.can_place_egg(tile_x, tile_y) {
|
||||
return 5;
|
||||
}
|
||||
0
|
||||
}
|
||||
pub fn find_position_for_spell(
|
||||
summon: Option<&LogicCharacterData>,
|
||||
input: LogicVector2,
|
||||
tilemap: &LogicTilemap,
|
||||
blockers: &[DeployBlocker],
|
||||
avoid_roads: bool,
|
||||
) -> Option<LogicVector2> {
|
||||
let x = input.x.clamp(0, SUBTILE_UNITS * tilemap.width());
|
||||
let y = input.y.clamp(0, SUBTILE_UNITS * tilemap.height());
|
||||
let Some(summon) = summon else {
|
||||
return Some(LogicVector2::new(
|
||||
x + 500 - x % DEPLOY_CELL_UNITS,
|
||||
y + 500 - y % DEPLOY_CELL_UNITS,
|
||||
));
|
||||
};
|
||||
let cell_w = tilemap.width() >> 1;
|
||||
let cell_h = tilemap.height() >> 1;
|
||||
let free = build_free_tile_map(blockers, cell_w, cell_h);
|
||||
let footprint = if summon.is_building() {
|
||||
summon.size_in_tiles()
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let origin_x = snap_coordinate(summon, x);
|
||||
let origin_y = snap_coordinate(summon, y);
|
||||
let mut best: Option<(i32, i32)> = None;
|
||||
let mut best_distance = i32::MAX;
|
||||
let mut last_ring = FIND_POSITION_MAX_RING;
|
||||
let mut ring = 0;
|
||||
loop {
|
||||
let steps = if ring <= 0 { 1 } else { 2 * ring };
|
||||
let mut step = 0;
|
||||
while step < steps {
|
||||
let low = -ring;
|
||||
let rising = step - ring;
|
||||
let falling = ring - step;
|
||||
let corners = if ring <= 0 { 1 } else { 4 };
|
||||
let mut corner = 0;
|
||||
while corner < corners {
|
||||
let (mut dx, mut dy) = if corner & 1 == 1 {
|
||||
(low, falling)
|
||||
} else {
|
||||
(rising, low)
|
||||
};
|
||||
if corner >= 2 {
|
||||
dx = -dx;
|
||||
dy = -dy;
|
||||
}
|
||||
let candidate = (
|
||||
origin_x + DEPLOY_CELL_UNITS * dx,
|
||||
origin_y + DEPLOY_CELL_UNITS * dy,
|
||||
);
|
||||
if position_free(&free, cell_w, cell_h, candidate, footprint, tilemap) {
|
||||
let mut distance = distance_squared(candidate, (x, y));
|
||||
if avoid_roads && roads_beneath(tilemap, candidate, summon.size_in_tiles()) {
|
||||
distance = i32::MAX;
|
||||
}
|
||||
if distance < best_distance {
|
||||
best = Some(candidate);
|
||||
best_distance = distance;
|
||||
last_ring = 0;
|
||||
}
|
||||
}
|
||||
corner += 1;
|
||||
}
|
||||
step += 1;
|
||||
}
|
||||
if ring >= last_ring {
|
||||
break;
|
||||
}
|
||||
ring += 1;
|
||||
}
|
||||
if best_distance == i32::MAX {
|
||||
return None;
|
||||
}
|
||||
best.map(|(bx, by)| LogicVector2::new(bx, by))
|
||||
}
|
||||
|
|
@ -2,9 +2,7 @@ use std::collections::HashMap;
|
|||
use std::path::Path;
|
||||
pub const SUBTILE_UNITS: i32 = 500;
|
||||
pub const WATER_BIT: i32 = 5;
|
||||
pub const WATER_COST: i32 = 800;
|
||||
pub const WATER_COST_JUMPING: i32 = 20;
|
||||
pub const GROUND_COST: i32 = 1;
|
||||
pub const NO_DEPLOY_BIT: i32 = 4;
|
||||
pub const SECTION_OBJECTS: &str = "Objects";
|
||||
pub const SECTION_MAP: &str = "Map";
|
||||
pub const OBJECT_KING_TOWER: &str = "KingTower";
|
||||
|
|
@ -66,15 +64,21 @@ impl LogicTilemap {
|
|||
}
|
||||
}
|
||||
SECTION_MAP => {
|
||||
let values: Vec<i32> = row[1..]
|
||||
let cells: Vec<&str> = row
|
||||
.get(1..)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|value| value.trim())
|
||||
.take_while(|value| !value.is_empty())
|
||||
.filter_map(number)
|
||||
.collect();
|
||||
if !values.is_empty() {
|
||||
tilemap.tiles.push(values);
|
||||
if cells.iter().all(|value| number(value).is_none()) {
|
||||
continue;
|
||||
}
|
||||
tilemap.tiles.push(
|
||||
cells
|
||||
.iter()
|
||||
.map(|value| number(value).unwrap_or(0))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -99,6 +103,13 @@ impl LogicTilemap {
|
|||
pub fn is_water(&self, x: i32, y: i32) -> bool {
|
||||
(self.tile_at(x, y) >> WATER_BIT) & 1 == 1
|
||||
}
|
||||
pub fn lane_bits(&self, x: i32, y: i32) -> i32 {
|
||||
self.tile_at(x, y) & 3
|
||||
}
|
||||
pub fn can_place_egg(&self, x: i32, y: i32) -> bool {
|
||||
let tile = self.tile_at(x, y);
|
||||
(tile >> WATER_BIT) & 1 == 0 && (tile >> NO_DEPLOY_BIT) & 1 == 0
|
||||
}
|
||||
pub fn is_inside(&self, x: i32, y: i32) -> bool {
|
||||
x >= 0 && y >= 0 && x < self.width() && y < self.height()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use titan::Payload;
|
||||
use crate::battle::logic_game_object_ref::LogicGameObjectRef;
|
||||
use crate::data::LogicDataRef;
|
||||
use titan::Payload;
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Payload)]
|
||||
pub struct LogicTutorialManager {
|
||||
pub tutorial: LogicDataRef,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ mod logic_game_object;
|
|||
mod logic_game_object_manager;
|
||||
mod logic_game_object_ref;
|
||||
mod logic_pathfinder;
|
||||
mod logic_projectile;
|
||||
mod logic_simulation;
|
||||
mod logic_summoner;
|
||||
mod logic_tilemap;
|
||||
mod logic_time;
|
||||
mod logic_tutorial_manager;
|
||||
|
|
@ -16,15 +18,15 @@ pub use logic_battle::{
|
|||
LogicBattle, BATTLE_INT_ARRAY, BATTLE_TICKS_PER_SECOND, BATTLE_TRAILING_INTS, BATTLE_TYPE_NPC,
|
||||
BATTLE_TYPE_PVP, BATTLE_TYPE_REPLAY, CROWNS_FOR_LEADER, LEADER_TOWER_COUNT,
|
||||
};
|
||||
pub use logic_battle_event::LogicBattleEvent;
|
||||
pub use logic_character::{
|
||||
LogicCharacter, LogicSummoner, LogicSummonerDeck, DEFAULT_SIZE, DIRECTION_BOTTOM, DIRECTION_TOP,
|
||||
};
|
||||
pub use logic_component::{
|
||||
LogicMovementComponent,
|
||||
LogicCharacterBuffComponent, LogicCombatComponent, LogicComponent, LogicHitpointComponent,
|
||||
COMPONENT_BUFF, COMPONENT_COMBAT, COMPONENT_HITPOINT, COMPONENT_MOVEMENT,
|
||||
LogicMovementComponent, COMPONENT_BUFF, COMPONENT_COMBAT, COMPONENT_HITPOINT,
|
||||
COMPONENT_MOVEMENT,
|
||||
};
|
||||
pub use logic_battle_event::LogicBattleEvent;
|
||||
pub use logic_game_mode::{LogicGameMode, SECTION_BATTLE, SECTION_TUTORIAL};
|
||||
pub use logic_game_object::{
|
||||
LogicGameObject, LogicGameObjectEntry, LogicObjectBody, LogicVector2, CHARACTER_OBJECT_TYPE,
|
||||
|
|
@ -32,12 +34,48 @@ pub use logic_game_object::{
|
|||
};
|
||||
pub use logic_game_object_manager::LogicGameObjectManager;
|
||||
pub use logic_game_object_ref::LogicGameObjectRef;
|
||||
pub use logic_pathfinder::find_path;
|
||||
pub use logic_pathfinder::{
|
||||
centre_of, closest_tile_position_to_target, find_path, get_lane_id, path_target_normal,
|
||||
spawn_lane_of, target_position_where_going_now, tile_of,
|
||||
};
|
||||
pub use logic_projectile::{LogicProjectile, PROJECTILE_OBJECT_TYPE};
|
||||
pub use logic_simulation::{
|
||||
LogicCharacterStats, CHARACTER_DEPLOY_TIME_COLUMN, CHARACTER_STATE_DEPLOY, CHARACTER_STATE_MOVING,
|
||||
TICK_MILLISECONDS,
|
||||
LogicCharacterStats, CHARACTER_STATE_DEPLOY, CHARACTER_STATE_MOVING, TICK_MILLISECONDS,
|
||||
};
|
||||
pub use logic_summoner::{
|
||||
build_free_tile_map, check_spell_position, find_position_for_spell, position_free,
|
||||
roads_beneath, snap_coordinate, DeployBlocker, DEPLOY_CELL_UNITS,
|
||||
};
|
||||
pub use logic_tilemap::{LogicTilemap, OBJECT_KING_TOWER, OBJECT_PRINCESS_TOWER, SUBTILE_UNITS};
|
||||
pub use logic_time::LogicTime;
|
||||
pub use logic_tutorial_manager::LogicTutorialManager;
|
||||
pub use verify::{verify_snapshot, SnapshotReport};
|
||||
mod target_attribution {
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::OnceLock;
|
||||
thread_local! {
|
||||
static TICK: Cell<i32> = const { Cell::new(0) };
|
||||
}
|
||||
pub fn set_tick(tick: i32) {
|
||||
TICK.with(|t| t.set(tick));
|
||||
}
|
||||
pub fn tick() -> i32 {
|
||||
TICK.with(|t| t.get())
|
||||
}
|
||||
pub fn enabled() -> bool {
|
||||
static ON: OnceLock<bool> = OnceLock::new();
|
||||
*ON.get_or_init(|| std::env::var("SCROLL_TRACE_TICK").is_ok())
|
||||
}
|
||||
thread_local! {
|
||||
static REPORTED: RefCell<HashSet<(i32, i32, &'static str)>> =
|
||||
RefCell::new(HashSet::new());
|
||||
}
|
||||
pub fn first_time(object: (i32, i32), site: &'static str) -> bool {
|
||||
REPORTED.with(|seen| seen.borrow_mut().insert((object.0, object.1, site)))
|
||||
}
|
||||
}
|
||||
pub use target_attribution::{
|
||||
enabled as target_attribution_enabled, first_time as attribution_first_time,
|
||||
set_tick as set_attribution_tick, tick as attribution_tick,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
use titan::{ByteStreamReader, Result};
|
||||
use crate::battle::logic_game_mode::{SECTION_BATTLE, SECTION_TUTORIAL};
|
||||
use crate::data::{table, LogicDataRef, LogicDataTables};
|
||||
use crate::model::DECK_SLOT_COUNT;
|
||||
use titan::{ByteStreamReader, Result};
|
||||
pub const BUFF_ARRAY_TABLE: i32 = table::DAMAGE_TYPES;
|
||||
pub const MOVEMENT_SPEED_COLUMN: &str = "Speed";
|
||||
pub const MOVEMENT_TAIL_VINTS: usize = 17;
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SnapshotReport {
|
||||
|
|
@ -68,12 +67,18 @@ impl<'a, 'b> Verifier<'a, 'b> {
|
|||
self.reader.read_vint()?;
|
||||
self.vints(2)?;
|
||||
self.vints(12)?;
|
||||
let row = data.data();
|
||||
let mana_limit = row.map(|r| r.int("ManaGenerateLimit")).unwrap_or(0);
|
||||
let character = data.as_character();
|
||||
let mana_limit = character
|
||||
.as_ref()
|
||||
.map(|character| character.mana_generate_limit())
|
||||
.unwrap_or(0);
|
||||
if mana_limit >= 1 {
|
||||
self.reader.read_vint()?;
|
||||
}
|
||||
let reload = row.map(|r| r.int("ReloadAfterHits")).unwrap_or(0);
|
||||
let reload = character
|
||||
.as_ref()
|
||||
.map(|character| character.reload_after_hits())
|
||||
.unwrap_or(0);
|
||||
if reload >= 1 {
|
||||
self.vints(2)?;
|
||||
}
|
||||
|
|
@ -93,6 +98,23 @@ impl<'a, 'b> Verifier<'a, 'b> {
|
|||
self.vints(4)?;
|
||||
Ok(())
|
||||
}
|
||||
fn projectile(&mut self) -> Result<()> {
|
||||
self.reader.read_boolean()?;
|
||||
self.vints(2)?;
|
||||
self.vints(2)?;
|
||||
self.reader.read_vint()?;
|
||||
self.vints(2)?;
|
||||
self.vints(2)?;
|
||||
self.vints(2)?;
|
||||
self.global_id()?;
|
||||
self.global_id()?;
|
||||
self.data_ref()?;
|
||||
self.data_ref()?;
|
||||
self.vints(3)?;
|
||||
let hits = self.reader.read_vint()?.max(0) as usize;
|
||||
self.vints(hits * 2)?;
|
||||
Ok(())
|
||||
}
|
||||
fn movement(&mut self) -> Result<()> {
|
||||
for _ in 0..4 {
|
||||
self.reader.read_boolean()?;
|
||||
|
|
@ -219,7 +241,9 @@ impl<'a, 'b> Verifier<'a, 'b> {
|
|||
for entry in &data {
|
||||
let is_summoner = entry.global_id().is_some() && entry.global_id() == summoner;
|
||||
let entry = entry.clone();
|
||||
if is_summoner {
|
||||
if entry.as_projectile().is_some() {
|
||||
self.projectile()?;
|
||||
} else if is_summoner {
|
||||
self.summoner(&entry)?;
|
||||
} else {
|
||||
self.character(&entry)?;
|
||||
|
|
@ -232,9 +256,12 @@ impl<'a, 'b> Verifier<'a, 'b> {
|
|||
self.mark("components");
|
||||
for pass in 0..4 {
|
||||
for entry in &data {
|
||||
if entry.as_projectile().is_some() {
|
||||
continue;
|
||||
}
|
||||
let moves = entry
|
||||
.data()
|
||||
.map(|row| row.int(MOVEMENT_SPEED_COLUMN) > 0)
|
||||
.as_character()
|
||||
.map(|character| character.speed() > 0)
|
||||
.unwrap_or(false);
|
||||
match pass {
|
||||
0 => self.combat()?,
|
||||
|
|
@ -281,8 +308,11 @@ impl<'a, 'b> Verifier<'a, 'b> {
|
|||
self.reader.read_vint()?;
|
||||
self.mark("commands");
|
||||
let commands = self.reader.read_vint()?;
|
||||
if commands != 0 {
|
||||
return Err(titan::Error::Unsupported("commands are not expected yet"));
|
||||
if !(0..=32).contains(&commands) {
|
||||
return Err(titan::Error::Unsupported("implausible command count"));
|
||||
}
|
||||
for _ in 0..commands {
|
||||
crate::commands::LogicCommandManager::decode_command(self.reader)?;
|
||||
}
|
||||
self.mark("end");
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use titan::{ByteStreamReader, ByteStreamWriter, GlobalId, Payload, Result};
|
||||
use crate::data::logic_data::LogicData;
|
||||
use crate::data::logic_data_tables::LogicDataTables;
|
||||
use crate::data::tables::table;
|
||||
use crate::data::typed::{
|
||||
LogicArenaData, LogicRarityData, LogicResourceData, LogicResourcePackData, LogicSpellData,
|
||||
LogicArenaData, LogicCharacterData, LogicLocationData, LogicNpcData, LogicProjectileData,
|
||||
LogicRarityData, LogicResourceData, LogicResourcePackData, LogicSpellData,
|
||||
LogicTreasureChestData,
|
||||
};
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use titan::{ByteStreamReader, ByteStreamWriter, GlobalId, Payload, Result};
|
||||
#[derive(Clone, Default)]
|
||||
pub enum LogicDataRef {
|
||||
#[default]
|
||||
|
|
@ -121,6 +122,18 @@ impl LogicDataRef {
|
|||
pub fn as_resource_pack(&self) -> Option<LogicResourcePackData> {
|
||||
self.typed(table::RESOURCE_PACKS, LogicResourcePackData::new)
|
||||
}
|
||||
pub fn as_character(&self) -> Option<LogicCharacterData> {
|
||||
self.typed(table::CHARACTERS_COMBINED, LogicCharacterData::new)
|
||||
}
|
||||
pub fn as_location(&self) -> Option<LogicLocationData> {
|
||||
self.typed(table::LOCATIONS, LogicLocationData::new)
|
||||
}
|
||||
pub fn as_npc(&self) -> Option<LogicNpcData> {
|
||||
self.typed(table::NPCS, LogicNpcData::new)
|
||||
}
|
||||
pub fn as_projectile(&self) -> Option<LogicProjectileData> {
|
||||
self.typed(table::PROJECTILES, LogicProjectileData::new)
|
||||
}
|
||||
}
|
||||
impl PartialEq for LogicDataRef {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::data::scaled::{level_scale, ScaleKind};
|
||||
use std::sync::Arc;
|
||||
use titan::{CsvRow, CsvTable, GlobalId};
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -59,6 +60,30 @@ impl LogicData {
|
|||
_ => 0,
|
||||
}
|
||||
}
|
||||
pub fn scaled_value(&self, column: &str, level: i32, kind: ScaleKind) -> i32 {
|
||||
let (Some(row), Some(index)) = (self.row(), self.column(column)) else {
|
||||
return 0;
|
||||
};
|
||||
let count = row.value_count(index);
|
||||
if count == 0 {
|
||||
return 0;
|
||||
}
|
||||
if count == 1 {
|
||||
return row
|
||||
.int(index)
|
||||
.wrapping_mul(level_scale(kind.percent(), level))
|
||||
/ 100;
|
||||
}
|
||||
let index_at = if count < 8 {
|
||||
level.min(count as i32 - 1)
|
||||
} else {
|
||||
level
|
||||
};
|
||||
if index_at < 0 {
|
||||
return 0;
|
||||
}
|
||||
row.int_at(index, index_at as usize)
|
||||
}
|
||||
pub fn string_at(&self, column: &str, index: usize) -> &str {
|
||||
match (self.row(), self.column(column)) {
|
||||
(Some(row), Some(column)) => row.string_at(column, index),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::data::logic_data::LogicData;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use titan::CsvTable;
|
||||
use crate::data::logic_data::LogicData;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LogicDataTable {
|
||||
table_index: i32,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use crate::data::logic_data::LogicData;
|
||||
use crate::data::logic_data_table::LogicDataTable;
|
||||
use crate::data::tables::table;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, OnceLock, PoisonError, RwLock};
|
||||
use titan::{CsvError, CsvReader, GlobalId};
|
||||
use crate::data::logic_data::LogicData;
|
||||
use crate::data::logic_data_table::LogicDataTable;
|
||||
use crate::data::tables::table;
|
||||
pub const TABLE_COUNT: usize = 62;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct LogicDataTableResource {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ mod globals;
|
|||
mod logic_data;
|
||||
mod logic_data_table;
|
||||
mod logic_data_tables;
|
||||
mod scaled;
|
||||
mod tables;
|
||||
mod typed;
|
||||
pub use data_ref::LogicDataRef;
|
||||
|
|
@ -12,8 +13,10 @@ pub use logic_data_table::LogicDataTable;
|
|||
pub use logic_data_tables::{
|
||||
DataError, LogicDataTableResource, LogicDataTables, DATA_TABLE_RESOURCES, TABLE_COUNT,
|
||||
};
|
||||
pub use scaled::{level_scale, ScaleKind};
|
||||
pub use tables::{table, DataId, RESOURCE_DIAMONDS, RESOURCE_FREE_GOLD, RESOURCE_GOLD};
|
||||
pub use typed::{
|
||||
arena_by_index, LogicArenaData, LogicRarityData, LogicResourceData, LogicResourcePackData,
|
||||
LogicSpellData, LogicTreasureChestData,
|
||||
arena_by_index, LogicArenaData, LogicCharacterData, LogicLocationData, LogicNpcData,
|
||||
LogicProjectileData, LogicRarityData, LogicResourceData, LogicResourcePackData, LogicSpellData,
|
||||
LogicTreasureChestData,
|
||||
};
|
||||
|
|
|
|||
63
crates/logic/src/data/scaled.rs
Normal file
63
crates/logic/src/data/scaled.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
use crate::data::globals::LogicGlobals;
|
||||
pub const DAMAGE_PER_SPELL_LEVEL: &str = "DAMAGE_INCREASE_PERCENT_PER_SPELL_LEVEL";
|
||||
pub const HITPOINT_PER_SPELL_LEVEL: &str = "HITPOINT_INCREASE_PERCENT_PER_SPELL_LEVEL";
|
||||
pub const DAMAGE_PER_KING_LEVEL: &str = "DAMAGE_INCREASE_PERCENT_PER_KING_LEVEL";
|
||||
pub const HITPOINT_PER_KING_LEVEL: &str = "HITPOINT_INCREASE_PERCENT_PER_KING_LEVEL";
|
||||
pub const DAMAGE_PER_TOWER_LEVEL: &str = "DAMAGE_INCREASE_PERCENT_PER_TOWER_LEVEL";
|
||||
pub const HITPOINT_PER_TOWER_LEVEL: &str = "HITPOINT_INCREASE_PERCENT_PER_TOWER_LEVEL";
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ScaleKind {
|
||||
None,
|
||||
SpellDamage,
|
||||
SpellHitpoints,
|
||||
KingDamage,
|
||||
KingHitpoints,
|
||||
TowerDamage,
|
||||
TowerHitpoints,
|
||||
}
|
||||
impl ScaleKind {
|
||||
pub fn percent(self) -> i32 {
|
||||
match self {
|
||||
ScaleKind::None => 0,
|
||||
ScaleKind::SpellDamage => LogicGlobals::number(DAMAGE_PER_SPELL_LEVEL),
|
||||
ScaleKind::SpellHitpoints => LogicGlobals::number(HITPOINT_PER_SPELL_LEVEL),
|
||||
ScaleKind::KingDamage => LogicGlobals::number(DAMAGE_PER_KING_LEVEL),
|
||||
ScaleKind::KingHitpoints => LogicGlobals::number(HITPOINT_PER_KING_LEVEL),
|
||||
ScaleKind::TowerDamage => LogicGlobals::number(DAMAGE_PER_TOWER_LEVEL),
|
||||
ScaleKind::TowerHitpoints => LogicGlobals::number(HITPOINT_PER_TOWER_LEVEL),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn level_scale(percent: i32, level: i32) -> i32 {
|
||||
let mut scale: i32 = 100;
|
||||
if level < 1 {
|
||||
return scale;
|
||||
}
|
||||
let step = percent.wrapping_add(100);
|
||||
for _ in 0..level {
|
||||
scale = if scale >= 100_000 {
|
||||
(scale / 100).wrapping_mul(step)
|
||||
} else {
|
||||
scale.wrapping_mul(step) / 100
|
||||
};
|
||||
}
|
||||
scale
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::level_scale;
|
||||
#[test]
|
||||
fn the_ten_percent_ladder_matches_the_measured_damage() {
|
||||
assert_eq!(level_scale(10, 0), 100);
|
||||
assert_eq!(level_scale(10, 1), 110);
|
||||
assert_eq!(level_scale(10, 2), 121);
|
||||
assert_eq!(level_scale(10, 6), 176);
|
||||
assert_eq!(24 * level_scale(10, 6) / 100, 42);
|
||||
assert_eq!(24 * level_scale(10, 2) / 100, 29);
|
||||
}
|
||||
#[test]
|
||||
fn a_level_below_one_never_enters_the_loop() {
|
||||
assert_eq!(level_scale(10, -3), 100);
|
||||
assert_eq!(level_scale(9, 0), 100);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
use crate::data::data_ref::LogicDataRef;
|
||||
use crate::data::logic_data::LogicData;
|
||||
use crate::data::tables::table;
|
||||
use std::sync::Arc;
|
||||
macro_rules! typed_data {
|
||||
($name:ident) => {
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -27,6 +28,9 @@ macro_rules! typed_data {
|
|||
pub fn int_at(&self, column: &str, level: usize) -> i32 {
|
||||
self.0.int_at(column, level)
|
||||
}
|
||||
pub fn scaled(&self, column: &str, level: i32, kind: crate::data::ScaleKind) -> i32 {
|
||||
self.0.scaled_value(column, level, kind)
|
||||
}
|
||||
pub fn string(&self, column: &str) -> &str {
|
||||
self.0.string(column)
|
||||
}
|
||||
|
|
@ -42,6 +46,242 @@ typed_data!(LogicResourceData);
|
|||
typed_data!(LogicTreasureChestData);
|
||||
typed_data!(LogicRarityData);
|
||||
typed_data!(LogicResourcePackData);
|
||||
typed_data!(LogicCharacterData);
|
||||
typed_data!(LogicLocationData);
|
||||
typed_data!(LogicNpcData);
|
||||
fn tower_projectile(tower: &str) -> String {
|
||||
LogicDataRef::by_name(crate::data::tables::table::BUILDINGS, tower)
|
||||
.as_character()
|
||||
.map(|data| data.projectile().to_owned())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
typed_data!(LogicProjectileData);
|
||||
impl LogicProjectileData {
|
||||
pub fn speed(&self) -> i32 {
|
||||
self.int("Speed")
|
||||
}
|
||||
pub fn damage(&self, level: usize) -> i32 {
|
||||
self.scaled("Damage", level as i32, self.damage_kind())
|
||||
}
|
||||
fn damage_kind(&self) -> crate::data::ScaleKind {
|
||||
let name = self.name();
|
||||
if name == tower_projectile("KingTower") {
|
||||
crate::data::ScaleKind::KingDamage
|
||||
} else if name == tower_projectile("PrincessTower") {
|
||||
crate::data::ScaleKind::TowerDamage
|
||||
} else {
|
||||
crate::data::ScaleKind::SpellDamage
|
||||
}
|
||||
}
|
||||
pub fn radius(&self) -> i32 {
|
||||
self.int("Radius")
|
||||
}
|
||||
pub fn is_homing(&self) -> bool {
|
||||
self.boolean("Homing")
|
||||
}
|
||||
pub fn pushback(&self) -> i32 {
|
||||
self.int("PushBack")
|
||||
}
|
||||
pub fn damage_type(&self) -> i32 {
|
||||
self.int("DamageType")
|
||||
}
|
||||
pub fn has_reduced_tower_damage(&self) -> bool {
|
||||
self.boolean("ReducedTowerDamage")
|
||||
}
|
||||
pub fn uses_pending_physical_damage(&self) -> bool {
|
||||
self.is_homing() && self.radius() < 1 && self.damage_type() == 0
|
||||
}
|
||||
pub fn damage_effect(&self) -> LogicDataRef {
|
||||
let named = self.string("DamageType");
|
||||
let mut damage_type = LogicDataRef::by_name(table::DAMAGE_TYPES, named);
|
||||
if damage_type.is_none() {
|
||||
damage_type = LogicDataRef::by_name(table::DAMAGE_TYPES, "Physical");
|
||||
}
|
||||
let effect = damage_type
|
||||
.data()
|
||||
.map(|row| row.string("DamageEffect").to_string())
|
||||
.unwrap_or_default();
|
||||
LogicDataRef::by_name(table::EFFECTS, &effect)
|
||||
}
|
||||
}
|
||||
impl LogicCharacterData {
|
||||
pub fn damage_effect(&self) -> LogicDataRef {
|
||||
LogicDataRef::by_name(table::EFFECTS, self.string("DamageEffect"))
|
||||
}
|
||||
pub fn speed(&self) -> i32 {
|
||||
self.int("Speed")
|
||||
}
|
||||
pub fn sight_range(&self) -> i32 {
|
||||
self.int("SightRange")
|
||||
}
|
||||
pub fn range(&self) -> i32 {
|
||||
self.int("Range")
|
||||
}
|
||||
pub fn collision_radius(&self) -> i32 {
|
||||
self.int("CollisionRadius")
|
||||
}
|
||||
pub fn is_building(&self) -> bool {
|
||||
self.speed() == 0
|
||||
}
|
||||
pub fn tile_size_override(&self) -> i32 {
|
||||
self.int("TileSizeOverride")
|
||||
}
|
||||
pub fn size_in_tiles(&self) -> i32 {
|
||||
let override_size = self.tile_size_override();
|
||||
if override_size > 0 {
|
||||
return override_size;
|
||||
}
|
||||
(self.collision_radius() + 499) / 500 + 1
|
||||
}
|
||||
pub fn no_deploy_size_w(&self) -> i32 {
|
||||
self.int("NoDeploySizeW")
|
||||
}
|
||||
pub fn no_deploy_size_h(&self) -> i32 {
|
||||
self.int("NoDeploySizeH")
|
||||
}
|
||||
pub fn hit_speed(&self) -> i32 {
|
||||
self.int("HitSpeed")
|
||||
}
|
||||
pub fn load_time(&self) -> i32 {
|
||||
self.int("LoadTime")
|
||||
}
|
||||
pub fn force_attack_animation_to_end(&self) -> bool {
|
||||
self.boolean("ForceAttackAnimationToEnd")
|
||||
}
|
||||
pub fn mass(&self) -> i32 {
|
||||
self.int("Mass")
|
||||
}
|
||||
pub fn stop_movement_after_ms(&self) -> i32 {
|
||||
self.int("StopMovementAfterMS")
|
||||
}
|
||||
pub fn wait_ms(&self) -> i32 {
|
||||
self.int("WaitMS")
|
||||
}
|
||||
pub fn flying_height(&self) -> i32 {
|
||||
self.int("FlyingHeight")
|
||||
}
|
||||
pub fn is_flying(&self) -> bool {
|
||||
self.flying_height() > 0
|
||||
}
|
||||
pub fn jump_enabled(&self) -> bool {
|
||||
self.boolean("JumpEnabled")
|
||||
}
|
||||
pub fn deploy_time(&self) -> i32 {
|
||||
self.int("DeployTime")
|
||||
}
|
||||
pub fn special_attack_interval(&self) -> i32 {
|
||||
self.int("SpecialAttackInterval")
|
||||
}
|
||||
pub fn mana_generate_limit(&self) -> i32 {
|
||||
self.int("ManaGenerateLimit")
|
||||
}
|
||||
pub fn reload_after_hits(&self) -> i32 {
|
||||
self.int("ReloadAfterHits")
|
||||
}
|
||||
pub fn attacks_air(&self) -> bool {
|
||||
self.boolean("AttacksAir")
|
||||
}
|
||||
pub fn attacks_ground(&self) -> bool {
|
||||
self.boolean("AttacksGround")
|
||||
}
|
||||
pub fn target_only_buildings(&self) -> bool {
|
||||
self.boolean("TargetOnlyBuildings")
|
||||
}
|
||||
pub fn projectile(&self) -> &str {
|
||||
self.string("Projectile")
|
||||
}
|
||||
pub fn spawn_character(&self) -> &str {
|
||||
self.string("SpawnCharacter")
|
||||
}
|
||||
pub fn spawn_character_level_index(&self) -> i32 {
|
||||
self.int("SpawnCharacterLevelIndex")
|
||||
}
|
||||
pub fn spawn_number(&self) -> i32 {
|
||||
self.int("SpawnNumber")
|
||||
}
|
||||
pub fn spawn_interval(&self) -> i32 {
|
||||
self.int("SpawnInterval")
|
||||
}
|
||||
pub fn spawn_pause_time(&self) -> i32 {
|
||||
self.int("SpawnPauseTime")
|
||||
}
|
||||
pub fn spawn_start_time(&self) -> i32 {
|
||||
self.int("SpawnStartTime")
|
||||
}
|
||||
pub fn spawn_limit(&self) -> i32 {
|
||||
self.int("SpawnLimit")
|
||||
}
|
||||
pub fn life_time(&self) -> i32 {
|
||||
self.int("LifeTime")
|
||||
}
|
||||
pub fn projectile_start_radius(&self) -> i32 {
|
||||
self.int("ProjectileStartRadius")
|
||||
}
|
||||
pub fn projectile_y_offset(&self) -> i32 {
|
||||
self.int("ProjectileYOffset")
|
||||
}
|
||||
pub fn projectile_start_z(&self) -> i32 {
|
||||
self.int("ProjectileStartZ")
|
||||
}
|
||||
pub fn hitpoints(&self, level: usize) -> i32 {
|
||||
self.scaled("Hitpoints", level as i32, self.scale_kind(false))
|
||||
}
|
||||
fn scale_kind(&self, damage: bool) -> crate::data::ScaleKind {
|
||||
use crate::data::ScaleKind;
|
||||
if self.name() == "KingTower" {
|
||||
if damage {
|
||||
ScaleKind::KingDamage
|
||||
} else {
|
||||
ScaleKind::KingHitpoints
|
||||
}
|
||||
} else if self.boolean("IsSummonerTower") {
|
||||
if damage {
|
||||
ScaleKind::TowerDamage
|
||||
} else {
|
||||
ScaleKind::TowerHitpoints
|
||||
}
|
||||
} else if damage {
|
||||
ScaleKind::SpellDamage
|
||||
} else {
|
||||
ScaleKind::SpellHitpoints
|
||||
}
|
||||
}
|
||||
pub fn damage(&self, level: usize) -> i32 {
|
||||
let projectile = self.projectile();
|
||||
if projectile.is_empty() {
|
||||
return self.scaled("Damage", level as i32, self.scale_kind(true));
|
||||
}
|
||||
LogicDataRef::by_name(crate::data::tables::table::PROJECTILES, projectile)
|
||||
.data()
|
||||
.map(|shot| shot.int_at("Damage", level))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
impl LogicLocationData {
|
||||
pub fn match_length(&self) -> i32 {
|
||||
self.int("MatchLength")
|
||||
}
|
||||
pub fn overtime_seconds(&self) -> i32 {
|
||||
self.int("OvertimeSeconds")
|
||||
}
|
||||
pub fn file_name(&self) -> &str {
|
||||
self.string("FileName")
|
||||
}
|
||||
}
|
||||
impl LogicNpcData {
|
||||
pub fn location(&self) -> &str {
|
||||
self.string("Location")
|
||||
}
|
||||
pub fn mana_regen_ms(&self) -> i32 {
|
||||
self.int("ManaRegenMs")
|
||||
}
|
||||
pub fn mana_regen_ms_end(&self) -> i32 {
|
||||
self.int("ManaRegenMsEnd")
|
||||
}
|
||||
pub fn mana_regen_ms_overtime(&self) -> i32 {
|
||||
self.int("ManaRegenMsOvertime")
|
||||
}
|
||||
}
|
||||
impl LogicArenaData {
|
||||
pub fn index(&self) -> i32 {
|
||||
self.int("Arena")
|
||||
|
|
@ -243,6 +483,12 @@ impl LogicSpellData {
|
|||
pub fn mana_cost(&self) -> i32 {
|
||||
self.int("ManaCost")
|
||||
}
|
||||
pub fn summon_character(&self) -> &str {
|
||||
self.string("SummonCharacter")
|
||||
}
|
||||
pub fn summon_number(&self) -> i32 {
|
||||
self.int("SummonNumber")
|
||||
}
|
||||
pub fn unlock_arena(&self) -> &str {
|
||||
self.string("UnlockArena")
|
||||
}
|
||||
|
|
@ -272,6 +518,9 @@ impl LogicArenaData {
|
|||
pub fn request_size(&self) -> i32 {
|
||||
self.int("RequestSize")
|
||||
}
|
||||
pub fn pvp_location(&self) -> &str {
|
||||
self.string("PvpLocation")
|
||||
}
|
||||
}
|
||||
impl LogicResourceData {
|
||||
pub fn cap(&self) -> i32 {
|
||||
|
|
|
|||
|
|
@ -4,30 +4,31 @@ pub mod commands;
|
|||
pub mod data;
|
||||
pub mod factory;
|
||||
pub mod home;
|
||||
pub mod logic_math;
|
||||
pub mod logic_random;
|
||||
pub mod messages;
|
||||
pub mod logic_math;
|
||||
pub mod model;
|
||||
pub use logic_math::{logic_cos_scaled, logic_sin, logic_sin_scaled, logic_sqrt, spawn_offset, SIN_TABLE, SQRT_TABLE};
|
||||
pub use commands::{
|
||||
chest_source, command_type, CommandMeta, CommandOutcome, Execute, LogicBuyCardCommand,
|
||||
LogicBuyChestCommand, LogicBuyResourcePackCommand, LogicClaimAchievementRewardCommand,
|
||||
LogicClaimRewardCommand, LogicCollectFreeChestCommand, LogicCollectMultiWinChestCommand,
|
||||
LogicCommand, LogicCommandHeader, LogicCommandManager, LogicCompleteTutorialBattleCommand,
|
||||
LogicDoSpellCommand, LogicFuseSpellsCommand,
|
||||
LogicHelpOpenedCommand, LogicMoveSpellCommand, LogicPageOpenedCommand,
|
||||
LogicRefreshAchievementsCommand, LogicReward, LogicShopOpenedCommand,
|
||||
LogicDoSpellCommand, LogicFuseSpellsCommand, LogicHelpOpenedCommand, LogicMoveSpellCommand,
|
||||
LogicPageOpenedCommand, LogicRefreshAchievementsCommand, LogicReward, LogicShopOpenedCommand,
|
||||
LogicShopSeedChangedCommand, LogicSortCollectionCommand, LogicStartMatchmakeCommand,
|
||||
LogicStartRewardClaimCommand, LogicSwapSpellsCommand, LogicUpdateLastShownLevelUpCommand,
|
||||
};
|
||||
pub use data::{
|
||||
table, DataError, LogicArenaData, LogicData, LogicDataRef, LogicDataTable,
|
||||
LogicDataTableResource, LogicDataTables, LogicGlobals, LogicRarityData, LogicResourceData,
|
||||
LogicResourcePackData, LogicSpellData, LogicTreasureChestData, DATA_TABLE_RESOURCES,
|
||||
TABLE_COUNT,
|
||||
table, DataError, LogicArenaData, LogicCharacterData, LogicData, LogicDataRef, LogicDataTable,
|
||||
LogicDataTableResource, LogicDataTables, LogicGlobals, LogicLocationData, LogicNpcData,
|
||||
LogicProjectileData, LogicRarityData, LogicResourceData, LogicResourcePackData, LogicSpellData,
|
||||
LogicTreasureChestData, DATA_TABLE_RESOURCES, TABLE_COUNT,
|
||||
};
|
||||
pub use factory::{scroll_message_registry, LogicScrollMessageFactory};
|
||||
pub use home::{randomize_shop_items, LogicHomeMode};
|
||||
pub use logic_math::{
|
||||
logic_cos_scaled, logic_sin, logic_sin_scaled, logic_sqrt, spawn_offset, SIN_TABLE, SQRT_TABLE,
|
||||
};
|
||||
pub use logic_random::LogicRandom;
|
||||
pub use messages::*;
|
||||
pub use model::*;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
pub const SQRT_TABLE: [i32; 256] = [
|
||||
0, 16, 22, 27, 32, 35, 39, 42, 45, 48, 50, 53, 55, 57, 59, 61,
|
||||
64, 65, 67, 69, 71, 73, 75, 76, 78, 80, 81, 83, 84, 86, 87, 89,
|
||||
90, 91, 93, 94, 96, 97, 98, 99, 101, 102, 103, 104, 106, 107, 108, 109,
|
||||
110, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126,
|
||||
128, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142,
|
||||
143, 144, 144, 145, 146, 147, 148, 149, 150, 150, 151, 152, 153, 154, 155, 155,
|
||||
156, 157, 158, 159, 160, 160, 161, 162, 163, 163, 164, 165, 166, 167, 167, 168,
|
||||
169, 170, 170, 171, 172, 173, 173, 174, 175, 176, 176, 177, 178, 178, 179, 180,
|
||||
181, 181, 182, 183, 183, 184, 185, 185, 186, 187, 187, 188, 189, 189, 190, 191,
|
||||
192, 192, 193, 193, 194, 195, 195, 196, 197, 197, 198, 199, 199, 200, 201, 201,
|
||||
202, 203, 203, 204, 204, 205, 206, 206, 207, 208, 208, 209, 209, 210, 211, 211,
|
||||
212, 212, 213, 214, 214, 215, 215, 216, 217, 217, 218, 218, 219, 219, 220, 221,
|
||||
221, 222, 222, 223, 224, 224, 225, 225, 226, 226, 227, 227, 228, 229, 229, 230,
|
||||
230, 231, 231, 232, 232, 233, 234, 234, 235, 235, 236, 236, 237, 237, 238, 238,
|
||||
239, 240, 240, 241, 241, 242, 242, 243, 243, 244, 244, 245, 245, 246, 246, 247,
|
||||
247, 248, 248, 249, 249, 250, 250, 251, 251, 252, 252, 253, 253, 254, 254, 255,
|
||||
0, 16, 22, 27, 32, 35, 39, 42, 45, 48, 50, 53, 55, 57, 59, 61, 64, 65, 67, 69, 71, 73, 75, 76,
|
||||
78, 80, 81, 83, 84, 86, 87, 89, 90, 91, 93, 94, 96, 97, 98, 99, 101, 102, 103, 104, 106, 107,
|
||||
108, 109, 110, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 128,
|
||||
128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 144, 145,
|
||||
146, 147, 148, 149, 150, 150, 151, 152, 153, 154, 155, 155, 156, 157, 158, 159, 160, 160, 161,
|
||||
162, 163, 163, 164, 165, 166, 167, 167, 168, 169, 170, 170, 171, 172, 173, 173, 174, 175, 176,
|
||||
176, 177, 178, 178, 179, 180, 181, 181, 182, 183, 183, 184, 185, 185, 186, 187, 187, 188, 189,
|
||||
189, 190, 191, 192, 192, 193, 193, 194, 195, 195, 196, 197, 197, 198, 199, 199, 200, 201, 201,
|
||||
202, 203, 203, 204, 204, 205, 206, 206, 207, 208, 208, 209, 209, 210, 211, 211, 212, 212, 213,
|
||||
214, 214, 215, 215, 216, 217, 217, 218, 218, 219, 219, 220, 221, 221, 222, 222, 223, 224, 224,
|
||||
225, 225, 226, 226, 227, 227, 228, 229, 229, 230, 230, 231, 231, 232, 232, 233, 234, 234, 235,
|
||||
235, 236, 236, 237, 237, 238, 238, 239, 240, 240, 241, 241, 242, 242, 243, 243, 244, 244, 245,
|
||||
245, 246, 246, 247, 247, 248, 248, 249, 249, 250, 250, 251, 251, 252, 252, 253, 253, 254, 254,
|
||||
255,
|
||||
];
|
||||
pub fn logic_sqrt(value: i32) -> i32 {
|
||||
if value < 0x10000 {
|
||||
|
|
@ -36,7 +34,11 @@ pub fn logic_sqrt(value: i32) -> i32 {
|
|||
SQRT_TABLE[(value >> 8) as usize]
|
||||
};
|
||||
let next = seed + 1;
|
||||
return if next.wrapping_mul(next) > value { seed } else { next };
|
||||
return if next.wrapping_mul(next) > value {
|
||||
seed
|
||||
} else {
|
||||
next
|
||||
};
|
||||
}
|
||||
let refined = if value < 0x1000000 {
|
||||
let seed = if value < 0x100000 {
|
||||
|
|
@ -73,16 +75,11 @@ pub fn logic_sqrt(value: i32) -> i32 {
|
|||
root - i32::from(root.wrapping_mul(root) > value)
|
||||
}
|
||||
pub const SIN_TABLE: [i32; 91] = [
|
||||
0, 18, 36, 54, 71, 89, 107, 125, 143, 160,
|
||||
178, 195, 213, 230, 248, 265, 282, 299, 316, 333,
|
||||
350, 367, 384, 400, 416, 433, 449, 465, 481, 496,
|
||||
512, 527, 543, 558, 573, 587, 602, 616, 630, 644,
|
||||
658, 672, 685, 698, 711, 724, 737, 749, 761, 773,
|
||||
784, 796, 807, 818, 828, 839, 849, 859, 868, 878,
|
||||
887, 896, 904, 912, 920, 928, 935, 943, 949, 956,
|
||||
962, 968, 974, 979, 984, 989, 994, 998, 1002, 1005,
|
||||
1008, 1011, 1014, 1016, 1018, 1020, 1022, 1023, 1023, 1024,
|
||||
1024,
|
||||
0, 18, 36, 54, 71, 89, 107, 125, 143, 160, 178, 195, 213, 230, 248, 265, 282, 299, 316, 333,
|
||||
350, 367, 384, 400, 416, 433, 449, 465, 481, 496, 512, 527, 543, 558, 573, 587, 602, 616, 630,
|
||||
644, 658, 672, 685, 698, 711, 724, 737, 749, 761, 773, 784, 796, 807, 818, 828, 839, 849, 859,
|
||||
868, 878, 887, 896, 904, 912, 920, 928, 935, 943, 949, 956, 962, 968, 974, 979, 984, 989, 994,
|
||||
998, 1002, 1005, 1008, 1011, 1014, 1016, 1018, 1020, 1022, 1023, 1023, 1024, 1024,
|
||||
];
|
||||
pub fn logic_sin(deg: i32) -> i32 {
|
||||
let mut v = deg % 360;
|
||||
|
|
@ -104,13 +101,64 @@ pub fn logic_sin_scaled(deg: i32, scale: i32) -> i32 {
|
|||
pub fn logic_cos_scaled(deg: i32, scale: i32) -> i32 {
|
||||
logic_sin_scaled(deg + 90, scale)
|
||||
}
|
||||
pub const ATAN_TABLE: [i32; 129] = [
|
||||
0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 8, 9, 9, 10, 10, 11, 11, 11, 12, 12,
|
||||
13, 13, 14, 14, 14, 15, 15, 16, 16, 17, 17, 17, 18, 18, 19, 19, 19, 20, 20, 21, 21, 21, 22, 22,
|
||||
22, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 27, 27, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, 31,
|
||||
31, 31, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 35, 36, 36, 36, 37, 37, 37, 37, 38, 38,
|
||||
38, 39, 39, 39, 39, 40, 40, 40, 40, 41, 41, 41, 41, 42, 42, 42, 42, 43, 43, 43, 43, 44, 44, 44,
|
||||
44, 45, 45, 45,
|
||||
];
|
||||
pub fn get_angle(x: i32, y: i32) -> i32 {
|
||||
if x == 0 && y == 0 {
|
||||
return 0;
|
||||
}
|
||||
if x >= 1 && y >= 0 {
|
||||
return if y >= x {
|
||||
90 - ATAN_TABLE[((x << 7) / y) as usize]
|
||||
} else {
|
||||
ATAN_TABLE[((y << 7) / x) as usize]
|
||||
};
|
||||
}
|
||||
let v3 = x.abs();
|
||||
if x <= 0 && y >= 1 {
|
||||
return if v3 >= y {
|
||||
180 - ATAN_TABLE[((y << 7) / v3) as usize]
|
||||
} else {
|
||||
ATAN_TABLE[((v3 << 7) / y) as usize] + 90
|
||||
};
|
||||
}
|
||||
let v4 = y.abs();
|
||||
if x < 0 && y <= 0 {
|
||||
if v4 < v3 {
|
||||
return ATAN_TABLE[((v4 << 7) / v3) as usize] + 180;
|
||||
}
|
||||
if v4 != 0 {
|
||||
return 270 - ATAN_TABLE[((v3 << 7) / v4) as usize];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if v3 < v4 {
|
||||
return ATAN_TABLE[((v3 << 7) / v4) as usize] + 270;
|
||||
}
|
||||
if v3 == 0 {
|
||||
return 0;
|
||||
}
|
||||
let v5 = (360 - ATAN_TABLE[((v4 << 7) / v3) as usize]) % 360;
|
||||
if v5 >= 0 {
|
||||
v5
|
||||
} else {
|
||||
v5 + 360
|
||||
}
|
||||
}
|
||||
pub fn spawn_offset(index: i32, count: i32, radius: i32, mirror: bool) -> (i32, i32) {
|
||||
if count <= 1 {
|
||||
return (0, 0);
|
||||
}
|
||||
let mut idx = index;
|
||||
let mut n = count;
|
||||
let mut base_angle = 90;
|
||||
let mut ring_base = 180;
|
||||
let mut angle_base = 90;
|
||||
let mut divisor = 2;
|
||||
let mut r = radius;
|
||||
let use_ring = match count {
|
||||
|
|
@ -118,12 +166,12 @@ pub fn spawn_offset(index: i32, count: i32, radius: i32, mirror: bool) -> (i32,
|
|||
3 | 5 => true,
|
||||
4 => {
|
||||
n = 4;
|
||||
base_angle = 45;
|
||||
ring_base = 45;
|
||||
true
|
||||
}
|
||||
7 => {
|
||||
if index != 0 {
|
||||
base_angle = 0;
|
||||
ring_base = 0;
|
||||
idx -= 1;
|
||||
n = 6;
|
||||
true
|
||||
|
|
@ -132,7 +180,7 @@ pub fn spawn_offset(index: i32, count: i32, radius: i32, mirror: bool) -> (i32,
|
|||
}
|
||||
}
|
||||
_ => {
|
||||
base_angle = 0;
|
||||
ring_base = 0;
|
||||
count >= 3
|
||||
}
|
||||
};
|
||||
|
|
@ -142,10 +190,12 @@ pub fn spawn_offset(index: i32, count: i32, radius: i32, mirror: bool) -> (i32,
|
|||
r = (3 * idx % 7).wrapping_mul(r) / 6;
|
||||
}
|
||||
divisor = n;
|
||||
angle_base = ring_base;
|
||||
} else if count != 2 {
|
||||
divisor = count;
|
||||
angle_base = ring_base;
|
||||
}
|
||||
let angle = base_angle + 360 * idx / divisor.max(1) + 90;
|
||||
let angle = angle_base + 360 * idx / divisor.max(1) + 90;
|
||||
let dx = logic_cos_scaled(angle, r);
|
||||
let dy = logic_sin_scaled(angle, r);
|
||||
(dx, if mirror { -dy } else { dy })
|
||||
|
|
@ -165,6 +215,20 @@ mod trig_tests {
|
|||
assert_eq!(logic_sin_scaled(90, 1000), 1000);
|
||||
}
|
||||
#[test]
|
||||
fn get_angle_matches_the_client_cardinals() {
|
||||
assert_eq!(get_angle(0, 0), 0);
|
||||
assert_eq!(get_angle(1, 0), 0);
|
||||
assert_eq!(get_angle(0, 1), 90);
|
||||
assert_eq!(get_angle(-1, 0), 180);
|
||||
assert_eq!(get_angle(0, -1), 270);
|
||||
assert_eq!(get_angle(1, 1), 45);
|
||||
assert_eq!(get_angle(-1, 1), 135);
|
||||
assert_eq!(get_angle(-1, -1), 225);
|
||||
assert_eq!(get_angle(1, -1), 315);
|
||||
assert_eq!(get_angle(700, 700), 45);
|
||||
assert_eq!(get_angle(0, 5000), 90);
|
||||
}
|
||||
#[test]
|
||||
fn a_single_unit_has_no_offset_and_pairs_split() {
|
||||
assert_eq!(spawn_offset(0, 1, 300, false), (0, 0));
|
||||
let a = spawn_offset(0, 2, 300, false);
|
||||
|
|
@ -174,4 +238,26 @@ mod trig_tests {
|
|||
assert_eq!(a.0, -b.0);
|
||||
assert_ne!(a.0, 0);
|
||||
}
|
||||
#[test]
|
||||
fn a_three_unit_card_starts_its_ring_on_the_y_axis() {
|
||||
let ring: Vec<_> = (0..3).map(|i| spawn_offset(i, 3, 500, true)).collect();
|
||||
assert_eq!(
|
||||
ring[0].0, 0,
|
||||
"the first of three is not on the y axis: {ring:?}"
|
||||
);
|
||||
assert!(
|
||||
ring[0].1 > 0,
|
||||
"the first of three points the wrong way: {ring:?}"
|
||||
);
|
||||
assert_eq!(ring[1].0, -ring[2].0, "the pair is not symmetric: {ring:?}");
|
||||
assert_eq!(ring[1].1, ring[2].1, "the pair is not level: {ring:?}");
|
||||
assert!(ring[1].1 < 0, "the pair is on the wrong side: {ring:?}");
|
||||
for (dx, dy) in &ring {
|
||||
let r = ((dx * dx + dy * dy) as f64).sqrt();
|
||||
assert!((r - 577.0).abs() <= 1.0, "radius drifted: {ring:?}");
|
||||
}
|
||||
let pair = [spawn_offset(0, 2, 500, true), spawn_offset(1, 2, 500, true)];
|
||||
assert_eq!(pair[0].1, 0);
|
||||
assert_eq!(pair[0].0, -pair[1].0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use titan::Message;
|
||||
use crate::battle::LogicGameObjectRef;
|
||||
use titan::Message;
|
||||
pub const BATTLE_RESULT_WIN: i32 = 1;
|
||||
pub const BATTLE_RESULT_LOSE: i32 = 2;
|
||||
pub const BATTLE_RESULT_DRAW: i32 = 3;
|
||||
|
|
@ -46,4 +46,25 @@ impl BattleResultMessage {
|
|||
..Self::default()
|
||||
}
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn result(
|
||||
result: i32,
|
||||
own_stars: i32,
|
||||
opponent_stars: i32,
|
||||
score_change: i32,
|
||||
gold_reward: i32,
|
||||
exp_reward: i32,
|
||||
full_update: Option<Vec<u8>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
result,
|
||||
score_change,
|
||||
gold_reward,
|
||||
exp_reward,
|
||||
own_stars,
|
||||
opponent_stars,
|
||||
full_update,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,65 @@ pub struct SectorHeartbeatMessage {
|
|||
}
|
||||
impl SectorHeartbeatMessage {
|
||||
pub fn new(server_turn: i32, checksum: i32) -> Self {
|
||||
Self::build(server_turn, checksum, &[], None)
|
||||
}
|
||||
pub fn with_commands(server_turn: i32, checksum: i32, commands: &[Vec<u8>]) -> Self {
|
||||
Self::build(server_turn, checksum, commands, None)
|
||||
}
|
||||
pub fn with_tick_data(
|
||||
server_turn: i32,
|
||||
checksum: i32,
|
||||
commands: &[Vec<u8>],
|
||||
tick_data: &[u8],
|
||||
) -> Self {
|
||||
Self::build(server_turn, checksum, commands, Some(tick_data))
|
||||
}
|
||||
fn build(
|
||||
server_turn: i32,
|
||||
checksum: i32,
|
||||
commands: &[Vec<u8>],
|
||||
tick_data: Option<&[u8]>,
|
||||
) -> Self {
|
||||
let mut writer = ByteStreamWriter::new();
|
||||
writer.write_vint(server_turn);
|
||||
writer.write_vint(checksum);
|
||||
if !commands.is_empty() || tick_data.is_some() {
|
||||
writer.write_vint(commands.len() as i32);
|
||||
for command in commands {
|
||||
writer.write_raw(command);
|
||||
}
|
||||
}
|
||||
if let Some(tick_data) = tick_data {
|
||||
writer.write_bytes(Some(tick_data));
|
||||
}
|
||||
Self {
|
||||
body: writer.into_inner(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use titan::ByteStreamReader;
|
||||
#[test]
|
||||
fn an_empty_turn_keeps_the_two_vint_form() {
|
||||
let message = SectorHeartbeatMessage::new(19, -1234);
|
||||
let mut reader = ByteStreamReader::new(&message.body);
|
||||
assert_eq!(reader.read_vint().unwrap(), 19);
|
||||
assert_eq!(reader.read_vint().unwrap(), -1234);
|
||||
assert!(reader.is_at_end());
|
||||
}
|
||||
#[test]
|
||||
fn commands_follow_the_checksum_behind_a_count() {
|
||||
let first = vec![7u8, 1, 2];
|
||||
let second = vec![9u8, 3];
|
||||
let message =
|
||||
SectorHeartbeatMessage::with_commands(19, -1234, &[first.clone(), second.clone()]);
|
||||
let mut reader = ByteStreamReader::new(&message.body);
|
||||
assert_eq!(reader.read_vint().unwrap(), 19);
|
||||
assert_eq!(reader.read_vint().unwrap(), -1234);
|
||||
assert_eq!(reader.read_vint().unwrap(), 2);
|
||||
let rest = &message.body[message.body.len() - first.len() - second.len()..];
|
||||
assert_eq!(rest, [first, second].concat());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
42
crates/logic/tests/client_path_replay.rs
Normal file
42
crates/logic/tests/client_path_replay.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
use logic::battle::{find_path, LogicTilemap};
|
||||
fn client_path() -> Vec<i32> {
|
||||
let mut path = vec![1755];
|
||||
let mut node = 1720;
|
||||
while node >= 1144 {
|
||||
path.push(node);
|
||||
node -= 36;
|
||||
}
|
||||
path.push(1107);
|
||||
path
|
||||
}
|
||||
fn arena() -> LogicTilemap {
|
||||
LogicTilemap::load(
|
||||
std::path::Path::new("../../assets"),
|
||||
"locations/goblin_arena.csv",
|
||||
)
|
||||
.expect("the goblin arena tilemap")
|
||||
}
|
||||
#[test]
|
||||
fn we_reproduce_the_clients_route_node_for_node() {
|
||||
let tm = arena();
|
||||
let expected = client_path();
|
||||
assert_eq!(expected.len(), 19);
|
||||
for start in [(26, 29), (27, 29)] {
|
||||
assert_eq!(
|
||||
find_path(&tm, start, (27, 48), 2, false),
|
||||
expected,
|
||||
"start {start:?} should walk the client's route"
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn the_route_bulges_because_the_lane_pinches_not_because_of_a_tie() {
|
||||
let tm = arena();
|
||||
assert_eq!(tm.lane_bits(27, 31), 0);
|
||||
assert_eq!(tm.lane_bits(28, 31), 2);
|
||||
let path = find_path(&tm, (27, 29), (27, 48), 2, false);
|
||||
assert!(
|
||||
!path.contains(&(31 * 36 + 27)),
|
||||
"the route must not step on the off-lane tile (27,31)"
|
||||
);
|
||||
}
|
||||
88
crates/logic/tests/lane_assignment.rs
Normal file
88
crates/logic/tests/lane_assignment.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
use logic::battle::{get_lane_id, spawn_lane_of, LogicTilemap};
|
||||
fn arena() -> LogicTilemap {
|
||||
LogicTilemap::load(
|
||||
std::path::Path::new("../../assets"),
|
||||
"locations/goblin_arena.csv",
|
||||
)
|
||||
.expect("the goblin arena tilemap")
|
||||
}
|
||||
#[test]
|
||||
fn the_arena_is_36_by_64_subtiles() {
|
||||
let tm = arena();
|
||||
assert_eq!((tm.width(), tm.height()), (36, 64));
|
||||
}
|
||||
#[test]
|
||||
fn the_map_rows_are_all_full_width() {
|
||||
let tm = arena();
|
||||
assert_eq!(tm.tiles.len(), 64);
|
||||
for (y, row) in tm.tiles.iter().enumerate() {
|
||||
assert_eq!(row.len(), 36, "map row {y} is ragged");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn the_arena_has_its_river_and_both_lanes() {
|
||||
let tm = arena();
|
||||
let tiles = || (0..tm.height()).flat_map(|y| (0..tm.width()).map(move |x| (x, y)));
|
||||
assert_eq!(
|
||||
tiles().filter(|(x, y)| tm.lane_bits(*x, *y) >= 1).count(),
|
||||
692
|
||||
);
|
||||
assert_eq!(tiles().filter(|(x, y)| tm.is_water(*x, *y)).count(), 112);
|
||||
}
|
||||
#[test]
|
||||
fn the_right_lane_pinches_to_two_columns_at_the_bridge() {
|
||||
let tm = arena();
|
||||
for y in [31, 32] {
|
||||
assert_eq!(
|
||||
tm.lane_bits(27, y),
|
||||
0,
|
||||
"({},{y}) is off-lane at the pinch",
|
||||
27
|
||||
);
|
||||
assert_eq!(
|
||||
tm.lane_bits(30, y),
|
||||
0,
|
||||
"({},{y}) is off-lane at the pinch",
|
||||
30
|
||||
);
|
||||
assert_eq!(tm.lane_bits(28, y), 2);
|
||||
assert_eq!(tm.lane_bits(29, y), 2);
|
||||
}
|
||||
for y in [30, 33, 34, 40, 47] {
|
||||
for x in 27..31 {
|
||||
assert_eq!(tm.lane_bits(x, y), 2, "({x},{y}) is the right lane");
|
||||
}
|
||||
}
|
||||
for y in 30..34 {
|
||||
for x in 24..27 {
|
||||
assert!(tm.is_water(x, y), "({x},{y}) should be river");
|
||||
}
|
||||
assert!(tm.is_water(31, y), "(31,{y}) should be river");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn a_drop_by_the_right_bridge_takes_the_right_lane() {
|
||||
let tm = arena();
|
||||
assert_eq!(get_lane_id(&tm, 12500, 14500), 2);
|
||||
assert_eq!(get_lane_id(&tm, 10000, 14500), 2);
|
||||
assert_eq!(get_lane_id(&tm, 11000, 14500), 2);
|
||||
}
|
||||
#[test]
|
||||
fn a_drop_by_the_left_bridge_takes_the_left_lane() {
|
||||
let tm = arena();
|
||||
for (x, y) in [(6923, 8500), (7788, 8999), (7788, 8001)] {
|
||||
assert_eq!(get_lane_id(&tm, x, y), 1, "({x},{y}) is on the left");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn the_single_spawn_path_clamps_before_it_reads_the_lane() {
|
||||
let tm = arena();
|
||||
assert_eq!(
|
||||
spawn_lane_of(&tm, -5000, 14500),
|
||||
get_lane_id(&tm, 250, 14500)
|
||||
);
|
||||
assert_eq!(
|
||||
spawn_lane_of(&tm, 999_999, 14500),
|
||||
get_lane_id(&tm, 36 * 500 - 250, 14500)
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +1,37 @@
|
|||
use std::cell::RefCell;
|
||||
thread_local! {
|
||||
static TRACE: RefCell<Option<Vec<String>>> = const { RefCell::new(None) };
|
||||
static SKIP_ONE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
}
|
||||
pub fn checksum_trace_skip_one() {
|
||||
SKIP_ONE.with(|s| s.set(true));
|
||||
}
|
||||
pub fn checksum_trace_start() {
|
||||
TRACE.with(|t| *t.borrow_mut() = Some(Vec::new()));
|
||||
}
|
||||
pub fn checksum_trace_take() -> Vec<String> {
|
||||
TRACE.with(|t| t.borrow_mut().take().unwrap_or_default())
|
||||
}
|
||||
pub fn checksum_trace_note(note: impl std::fmt::Display) {
|
||||
TRACE.with(|t| {
|
||||
if let Some(list) = t.borrow_mut().as_mut() {
|
||||
list.push(format!("# {note}"));
|
||||
}
|
||||
});
|
||||
}
|
||||
pub fn checksum_trace_active() -> bool {
|
||||
TRACE.with(|t| t.borrow().is_some())
|
||||
}
|
||||
fn trace_field(kind: &str, value: i32) {
|
||||
if SKIP_ONE.with(|s| s.replace(false)) {
|
||||
return;
|
||||
}
|
||||
TRACE.with(|t| {
|
||||
if let Some(list) = t.borrow_mut().as_mut() {
|
||||
list.push(format!("{kind}:{value}"));
|
||||
}
|
||||
});
|
||||
}
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ChecksumEncoder {
|
||||
state: u32,
|
||||
|
|
@ -16,18 +50,23 @@ impl ChecksumEncoder {
|
|||
self.state = self.state.rotate_right(31).wrapping_add(delta as u32);
|
||||
}
|
||||
pub fn write_boolean(&mut self, value: bool) {
|
||||
trace_field("b", value as i32);
|
||||
self.mix(if value { 13 } else { 7 });
|
||||
}
|
||||
pub fn write_byte(&mut self, value: i8) {
|
||||
trace_field("y", value as i32);
|
||||
self.mix(value as u8 as i32 + 11);
|
||||
}
|
||||
pub fn write_short(&mut self, value: i16) {
|
||||
trace_field("s", value as i32);
|
||||
self.mix(value as u16 as i32 + 19);
|
||||
}
|
||||
pub fn write_int(&mut self, value: i32) {
|
||||
trace_field("I", value);
|
||||
self.mix(value.wrapping_add(9));
|
||||
}
|
||||
pub fn write_vint(&mut self, value: i32) {
|
||||
trace_field("i", value);
|
||||
self.mix(value.wrapping_add(33));
|
||||
}
|
||||
pub fn write_long(&mut self, high: i32, low: i32) {
|
||||
|
|
@ -36,11 +75,18 @@ impl ChecksumEncoder {
|
|||
}
|
||||
pub fn write_string(&mut self, length: Option<usize>) {
|
||||
match length {
|
||||
None => self.mix(27),
|
||||
Some(length) => self.mix(length as i32 + 28),
|
||||
None => {
|
||||
trace_field("S", -1);
|
||||
self.mix(27);
|
||||
}
|
||||
Some(length) => {
|
||||
trace_field("S", length as i32);
|
||||
self.mix(length as i32 + 28);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn write_string_reference(&mut self, length: usize) {
|
||||
trace_field("R", length as i32);
|
||||
self.mix(length as i32 + 38);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue