type 0 turns out not to need a UDP transport. MessageManager::sendUdpMessage checks for a socket and a valid connection, and falls straight back to sendMessage when there is neither - we never send UdpConnectionInfoMessage, so the client has no socket and the sector traffic arrives on the tcp connection we already have. so SectorCommandMessage, 12904, is decoded now: a client tick, a client checksum and an optional command. a card played this way goes through the same summon path as before. the client stops sending EndClientTurnMessage in a battle - sendEndTurn asserts on isImmediateMessageExecution - so the checksum comparison moves onto the sector command, which carries the same two numbers. with the type at 0 the client no longer simulates. it renders what the snapshot says, which is why the snapshots start flowing again: the gate on them was the battle type all along. the bot's cards reach the player for the first time, because there is finally one simulation rather than two arguing.
326 lines
11 KiB
Rust
326 lines
11 KiB
Rust
use std::path::{Path, PathBuf};
|
|
use logic::battle::{
|
|
LogicBattle, LogicCharacter, LogicCharacterBuffComponent, LogicCombatComponent, LogicComponent,
|
|
LogicGameMode, LogicGameObject, LogicGameObjectEntry, LogicGameObjectManager,
|
|
LogicGameObjectRef, LogicHitpointComponent, LogicMovementComponent, LogicObjectBody,
|
|
LogicSummoner, LogicSummonerDeck,
|
|
LogicTilemap,
|
|
LogicTime, LogicVector2, BATTLE_TYPE_PVP, CHARACTER_OBJECT_TYPE, COMPONENT_PASSES,
|
|
DIRECTION_BOTTOM,
|
|
DIRECTION_TOP, SUBTILE_UNITS,
|
|
};
|
|
use logic::model::{LogicClientAvatar, LogicSpellDeck};
|
|
use logic::{table, LogicDataRef, LogicDataTables, LogicGlobals, LogicRandom};
|
|
pub const LOCATION_FILE_COLUMN: &str = "FileName";
|
|
pub const CHARACTER_KING_TOWER: &str = "KingTower";
|
|
pub const CHARACTER_PRINCESS_TOWER: &str = "PrincessTower";
|
|
pub const BUFF_ARRAY_TABLE: i32 = table::DAMAGE_TYPES;
|
|
pub const START_MANA_GLOBAL: &str = "START_MANA";
|
|
pub const CHARACTER_HITPOINTS_COLUMN: &str = "Hitpoints";
|
|
pub const CHARACTER_SPEED_COLUMN: &str = "Speed";
|
|
pub const SPELL_SUMMON_CHARACTER_COLUMN: &str = "SummonCharacter";
|
|
pub const SPELL_SUMMON_NUMBER_COLUMN: &str = "SummonNumber";
|
|
pub struct CharacterSpec {
|
|
pub data: LogicDataRef,
|
|
pub instance: i32,
|
|
pub position: LogicVector2,
|
|
pub owner: i32,
|
|
pub level_index: i32,
|
|
}
|
|
pub struct TowerSpec {
|
|
pub data: LogicDataRef,
|
|
pub instance: i32,
|
|
pub tile: (i32, i32),
|
|
pub owner: i32,
|
|
pub level_index: i32,
|
|
pub is_leader: bool,
|
|
pub summoner: bool,
|
|
pub deck_slots: usize,
|
|
}
|
|
pub struct BattleBuilder {
|
|
root: PathBuf,
|
|
buff_type_count: usize,
|
|
start_mana: i32,
|
|
}
|
|
fn tile_to_units(tile: i32) -> i32 {
|
|
tile.saturating_mul(SUBTILE_UNITS)
|
|
}
|
|
fn character_data(name: &str) -> LogicDataRef {
|
|
let combined = LogicDataRef::by_name(table::CHARACTERS_COMBINED, name);
|
|
if !combined.is_none() {
|
|
return combined;
|
|
}
|
|
let building = LogicDataRef::by_name(table::BUILDINGS, name);
|
|
if !building.is_none() {
|
|
return building;
|
|
}
|
|
LogicDataRef::by_name(table::CHARACTERS, name)
|
|
}
|
|
impl BattleBuilder {
|
|
pub fn new(root: impl AsRef<Path>) -> Self {
|
|
let buff_type_count = LogicDataTables::instance()
|
|
.table(BUFF_ARRAY_TABLE)
|
|
.map(|rows| rows.count())
|
|
.unwrap_or(0);
|
|
tracing::info!(
|
|
buff_type_count,
|
|
king_id = ?character_data(CHARACTER_KING_TOWER).global_id(),
|
|
princess_id = ?character_data(CHARACTER_PRINCESS_TOWER).global_id(),
|
|
"battle builder ready"
|
|
);
|
|
Self {
|
|
root: root.as_ref().to_path_buf(),
|
|
buff_type_count,
|
|
start_mana: LogicGlobals::number(START_MANA_GLOBAL),
|
|
}
|
|
}
|
|
pub fn buff_type_count(&self) -> usize {
|
|
self.buff_type_count
|
|
}
|
|
pub fn tilemap_for(&self, location: &LogicDataRef) -> std::io::Result<LogicTilemap> {
|
|
let file_name = location
|
|
.data()
|
|
.map(|data| data.string(LOCATION_FILE_COLUMN).to_owned())
|
|
.unwrap_or_default();
|
|
if file_name.is_empty() {
|
|
return Err(std::io::Error::other("the location names no tilemap"));
|
|
}
|
|
LogicTilemap::load(&self.root, &file_name)
|
|
}
|
|
fn hitpoints_of(data: &LogicDataRef, level_index: i32) -> i32 {
|
|
data.data()
|
|
.map(|row| row.int_at(CHARACTER_HITPOINTS_COLUMN, level_index.max(0) as usize))
|
|
.filter(|value| *value > 0)
|
|
.unwrap_or(1)
|
|
}
|
|
fn components_for(&self, hitpoints: i32, moves: bool) -> [Option<LogicComponent>; COMPONENT_PASSES] {
|
|
[
|
|
Some(LogicComponent::Combat(LogicCombatComponent::default())),
|
|
moves.then(|| LogicComponent::Movement(LogicMovementComponent::default())),
|
|
Some(LogicComponent::Hitpoint(LogicHitpointComponent::healthy(
|
|
hitpoints,
|
|
))),
|
|
Some(LogicComponent::Buff(LogicCharacterBuffComponent::empty(
|
|
self.buff_type_count,
|
|
))),
|
|
]
|
|
}
|
|
pub fn character(&self, spec: CharacterSpec) -> LogicGameObjectEntry {
|
|
let CharacterSpec {
|
|
data,
|
|
instance,
|
|
position,
|
|
owner,
|
|
level_index,
|
|
} = spec;
|
|
let hitpoints = Self::hitpoints_of(&data, level_index);
|
|
let moves = data
|
|
.data()
|
|
.map(|row| row.int(CHARACTER_SPEED_COLUMN) > 0)
|
|
.unwrap_or(false);
|
|
let character = LogicCharacter {
|
|
level_index,
|
|
base: LogicGameObject {
|
|
position,
|
|
owner_index: owner,
|
|
..LogicGameObject::default()
|
|
},
|
|
direction: LogicVector2::new(
|
|
0,
|
|
if owner == 0 {
|
|
DIRECTION_TOP
|
|
} else {
|
|
DIRECTION_BOTTOM
|
|
},
|
|
),
|
|
..LogicCharacter::default()
|
|
};
|
|
let components = self.components_for(hitpoints, moves);
|
|
LogicGameObjectEntry::new(
|
|
data,
|
|
LogicGameObjectRef::of(CHARACTER_OBJECT_TYPE + 1, instance),
|
|
LogicObjectBody::Character(Box::new(character)),
|
|
components,
|
|
)
|
|
}
|
|
pub fn summon(
|
|
&self,
|
|
spell: &LogicDataRef,
|
|
position: LogicVector2,
|
|
owner: i32,
|
|
level_index: i32,
|
|
first_instance: i32,
|
|
) -> Vec<LogicGameObjectEntry> {
|
|
let Some(row) = spell.data() else {
|
|
return Vec::new();
|
|
};
|
|
let name = row.string(SPELL_SUMMON_CHARACTER_COLUMN).to_owned();
|
|
if name.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
let data = character_data(&name);
|
|
if data.is_none() {
|
|
tracing::warn!(spell = %spell, character = name, "the spell summons a character the tables do not have");
|
|
return Vec::new();
|
|
}
|
|
let count = row.int(SPELL_SUMMON_NUMBER_COLUMN).max(1);
|
|
(0..count)
|
|
.map(|index| {
|
|
self.character(CharacterSpec {
|
|
data: data.clone(),
|
|
instance: first_instance + index,
|
|
position,
|
|
owner,
|
|
level_index,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
fn tower(&self, spec: TowerSpec) -> LogicGameObjectEntry {
|
|
let TowerSpec {
|
|
data,
|
|
instance,
|
|
tile,
|
|
owner,
|
|
level_index,
|
|
is_leader,
|
|
summoner,
|
|
deck_slots,
|
|
} = spec;
|
|
let hitpoints = Self::hitpoints_of(&data, level_index);
|
|
let character = LogicCharacter {
|
|
level_index,
|
|
is_leader,
|
|
base: LogicGameObject {
|
|
position: LogicVector2::new(tile_to_units(tile.0), tile_to_units(tile.1)),
|
|
owner_index: owner,
|
|
..LogicGameObject::default()
|
|
},
|
|
direction: LogicVector2::new(
|
|
0,
|
|
if owner == 0 {
|
|
DIRECTION_TOP
|
|
} else {
|
|
DIRECTION_BOTTOM
|
|
},
|
|
),
|
|
..LogicCharacter::default()
|
|
};
|
|
let body = if summoner {
|
|
LogicObjectBody::Summoner(Box::new(LogicSummoner {
|
|
character,
|
|
deck: Some(LogicSummonerDeck::starting(deck_slots)),
|
|
mana: self.start_mana,
|
|
..LogicSummoner::default()
|
|
}))
|
|
} else {
|
|
LogicObjectBody::Character(Box::new(character))
|
|
};
|
|
LogicGameObjectEntry::new(
|
|
data,
|
|
LogicGameObjectRef::of(CHARACTER_OBJECT_TYPE + 1, instance),
|
|
body,
|
|
self.components_for(hitpoints, false),
|
|
)
|
|
}
|
|
pub fn build(
|
|
&self,
|
|
location: LogicDataRef,
|
|
npc: LogicDataRef,
|
|
arena: LogicDataRef,
|
|
avatars: Vec<LogicClientAvatar>,
|
|
decks: [Option<LogicSpellDeck>; 2],
|
|
random_seed: i32,
|
|
) -> std::io::Result<LogicGameMode> {
|
|
let tilemap = self.tilemap_for(&location)?;
|
|
let height = tilemap.height();
|
|
let middle = tile_to_units(height) / 2;
|
|
let king = character_data(CHARACTER_KING_TOWER);
|
|
let princess = character_data(CHARACTER_PRINCESS_TOWER);
|
|
if king.is_none() {
|
|
return Err(std::io::Error::other(
|
|
"no KingTower row in the character tables",
|
|
));
|
|
}
|
|
let deck_slots = [
|
|
decks[0].as_ref().map(LogicSpellDeck::filled_slot_count).unwrap_or(0),
|
|
decks[1].as_ref().map(LogicSpellDeck::filled_slot_count).unwrap_or(0),
|
|
];
|
|
let mut objects = LogicGameObjectManager::default();
|
|
let mut leaders = [LogicGameObjectRef::NONE; 2];
|
|
let mut towers: [Vec<LogicGameObjectRef>; 2] = [Vec::new(), Vec::new()];
|
|
let mut instance = 0;
|
|
for tile in tilemap.king_towers() {
|
|
let owner = usize::from(tile_to_units(tile.1) >= middle);
|
|
let entry = self.tower(TowerSpec {
|
|
data: king.clone(),
|
|
instance,
|
|
tile: *tile,
|
|
owner: owner as i32,
|
|
level_index: 0,
|
|
is_leader: true,
|
|
summoner: true,
|
|
deck_slots: deck_slots[owner],
|
|
});
|
|
leaders[owner] = entry.global_id;
|
|
objects.push(entry);
|
|
instance += 1;
|
|
}
|
|
if !princess.is_none() {
|
|
for tile in tilemap.princess_towers() {
|
|
let owner = usize::from(tile_to_units(tile.1) >= middle);
|
|
let entry = self.tower(TowerSpec {
|
|
data: princess.clone(),
|
|
instance,
|
|
tile: *tile,
|
|
owner: owner as i32,
|
|
level_index: 0,
|
|
is_leader: false,
|
|
summoner: false,
|
|
deck_slots: 0,
|
|
});
|
|
towers[owner].push(entry.global_id);
|
|
objects.push(entry);
|
|
instance += 1;
|
|
}
|
|
}
|
|
if leaders.iter().any(LogicGameObjectRef::is_none) {
|
|
return Err(std::io::Error::other(format!(
|
|
"{} has {} king towers, the client needs exactly two",
|
|
location,
|
|
tilemap.king_towers().len()
|
|
)));
|
|
}
|
|
let account_ids = [
|
|
avatars
|
|
.first()
|
|
.map(|avatar| avatar.account_id)
|
|
.unwrap_or_default(),
|
|
avatars
|
|
.get(1)
|
|
.map(|avatar| avatar.account_id)
|
|
.unwrap_or_default(),
|
|
];
|
|
let battle = LogicBattle {
|
|
tilemap: Some(tilemap),
|
|
location,
|
|
npc,
|
|
arena,
|
|
account_ids,
|
|
battle_type: BATTLE_TYPE_PVP,
|
|
decks,
|
|
objects,
|
|
leaders,
|
|
leader_towers: towers,
|
|
..LogicBattle::default()
|
|
};
|
|
Ok(LogicGameMode {
|
|
random: LogicRandom::new(random_seed),
|
|
random_seed,
|
|
time: LogicTime::default(),
|
|
battle,
|
|
avatars,
|
|
..LogicGameMode::default()
|
|
})
|
|
}
|
|
}
|