match against a bot player instead of an npc
the decks were the visible half: LogicBattle carries one LogicSpellDeck per player and we wrote both as absent, so the client cleared them and the card bar came up empty. both sides get a real deck now - the player from their profile, the bot from the fullest row of predefined_decks. matchmaking no longer picks a row out of npcs.csv. it takes the location from the arena's PvpLocation column and builds an opponent avatar with its own name, arena and trophies, so the battle reads as a player match rather than a trainer one. StartMissionMessage still goes through the npc path unchanged. the battle type stays 1 on purpose. LogicGameMode::isImmediateMessageExecution is (type - 1) < 3, so 1, 2 and 3 let the client simulate locally while 0 makes it wait for the server to drive the sector - which needs the real tick loop we do not have yet.
This commit is contained in:
parent
4477b12f0a
commit
27f99ee898
6 changed files with 104 additions and 26 deletions
|
|
@ -6,7 +6,7 @@ use logic::battle::{
|
|||
LogicTime, LogicVector2, BATTLE_TYPE_NPC, CHARACTER_OBJECT_TYPE, DIRECTION_BOTTOM,
|
||||
DIRECTION_TOP, SUBTILE_UNITS,
|
||||
};
|
||||
use logic::model::LogicClientAvatar;
|
||||
use logic::model::{LogicClientAvatar, LogicSpellDeck};
|
||||
use logic::{table, LogicDataRef, LogicDataTables, LogicRandom};
|
||||
pub const LOCATION_FILE_COLUMN: &str = "FileName";
|
||||
pub const CHARACTER_KING_TOWER: &str = "KingTower";
|
||||
|
|
@ -132,6 +132,7 @@ impl BattleBuilder {
|
|||
npc: LogicDataRef,
|
||||
arena: LogicDataRef,
|
||||
avatars: Vec<LogicClientAvatar>,
|
||||
decks: [Option<LogicSpellDeck>; 2],
|
||||
random_seed: i32,
|
||||
) -> std::io::Result<LogicGameMode> {
|
||||
let tilemap = self.tilemap_for(&location)?;
|
||||
|
|
@ -203,6 +204,7 @@ impl BattleBuilder {
|
|||
arena,
|
||||
account_ids,
|
||||
battle_type: BATTLE_TYPE_NPC,
|
||||
decks,
|
||||
objects,
|
||||
leaders,
|
||||
leader_towers: towers,
|
||||
|
|
|
|||
54
crates/game-service/src/bot.rs
Normal file
54
crates/game-service/src/bot.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use logic::model::{LogicClientAvatar, LogicSpell, LogicSpellDeck, DECK_SLOT_COUNT};
|
||||
use logic::{table, LogicDataRef, LogicDataTables};
|
||||
use titan::LogicLong;
|
||||
pub const BOT_NAME: &str = "Bot";
|
||||
pub const DECK_SPELLS_COLUMN: &str = "Spells";
|
||||
pub const DECK_LEVEL_COLUMN: &str = "SpellLevel";
|
||||
pub struct BotPlayer {
|
||||
pub avatar: LogicClientAvatar,
|
||||
pub deck: LogicSpellDeck,
|
||||
}
|
||||
fn predefined_deck() -> Option<LogicSpellDeck> {
|
||||
let row = LogicDataTables::instance()
|
||||
.table(table::PREDEFINED_DECKS)
|
||||
.and_then(|rows| {
|
||||
rows.iter()
|
||||
.filter(|row| row.value_count(DECK_SPELLS_COLUMN) >= DECK_SLOT_COUNT)
|
||||
.last()
|
||||
.cloned()
|
||||
})?;
|
||||
let cards = (0..DECK_SLOT_COUNT).filter_map(|slot| {
|
||||
let name = row.string_at(DECK_SPELLS_COLUMN, slot);
|
||||
let data = LogicDataRef::by_name(table::SPELLS, name);
|
||||
if data.is_none() {
|
||||
tracing::warn!(card = name, "predefined deck names a card the tables do not have");
|
||||
return None;
|
||||
}
|
||||
let level = row.int_at(DECK_LEVEL_COLUMN, slot).max(1);
|
||||
Some(LogicSpell::card(data, level - 1, 1))
|
||||
});
|
||||
Some(LogicSpellDeck::from_cards(cards))
|
||||
}
|
||||
impl BotPlayer {
|
||||
pub fn matching(arena: LogicDataRef, score: i32, deck: LogicSpellDeck) -> Self {
|
||||
let account = LogicLong::new(0, 0);
|
||||
let mut avatar = LogicClientAvatar {
|
||||
name: BOT_NAME.to_owned(),
|
||||
name_set_by_user: true,
|
||||
arena,
|
||||
score,
|
||||
..LogicClientAvatar::default()
|
||||
};
|
||||
avatar.avatar_id = account;
|
||||
avatar.account_id = account;
|
||||
avatar.home_id = account;
|
||||
avatar.name_change_state = -1;
|
||||
Self { avatar, deck }
|
||||
}
|
||||
pub fn against(player: &LogicClientAvatar, fallback: &LogicSpellDeck) -> Self {
|
||||
let deck = predefined_deck()
|
||||
.filter(|deck| deck.filled_slot_count() == DECK_SLOT_COUNT)
|
||||
.unwrap_or_else(|| fallback.clone());
|
||||
Self::matching(player.arena.clone(), player.score, deck)
|
||||
}
|
||||
}
|
||||
|
|
@ -48,8 +48,11 @@ fn chest_slots(profile: &PlayerProfile) -> Vec<Option<LogicChest>> {
|
|||
}
|
||||
slots
|
||||
}
|
||||
pub fn build_deck(profile: &PlayerProfile) -> LogicSpellDeck {
|
||||
LogicSpellDeck::from_cards(profile.deck.iter().map(spell_of))
|
||||
}
|
||||
pub fn build_home(profile: &PlayerProfile) -> LogicClientHome {
|
||||
let deck = LogicSpellDeck::from_cards(profile.deck.iter().map(spell_of));
|
||||
let deck = build_deck(profile);
|
||||
let collection =
|
||||
LogicSpellCollection::from_spells(profile.collection.iter().map(spell_of).collect());
|
||||
let mut home = LogicClientHome {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod battle;
|
||||
pub mod bot;
|
||||
pub mod catalog;
|
||||
pub mod config;
|
||||
pub mod home;
|
||||
|
|
@ -9,9 +10,10 @@ pub mod shop;
|
|||
pub mod store;
|
||||
pub mod time;
|
||||
pub use battle::BattleBuilder;
|
||||
pub use bot::BotPlayer;
|
||||
pub use catalog::{Catalog, ARENA_FALLBACK_INSTANCE, GOLD_RESOURCE_FALLBACK_INSTANCE};
|
||||
pub use config::{CardRef, DataSelector, GameConfig, StarterProfile};
|
||||
pub use home::{build_avatar, build_home};
|
||||
pub use home::{build_avatar, build_deck, build_home};
|
||||
pub use home_mode::{HomeMode, HomeModeRegistry, TurnResult};
|
||||
pub use rewards::RewardRoller;
|
||||
pub use service::GameService;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::sync::Arc;
|
||||
use logic::{table, LogicDataTables};
|
||||
use logic::table;
|
||||
use logic::{
|
||||
AvailableServerCommandMessage, EndClientTurnMessage, LogicCommandManager, LogicDataRef,
|
||||
LogicShopSeedChangedCommand, OutOfSyncMessage, OwnHomeDataMessage, SectorStateMessage,
|
||||
|
|
@ -13,12 +13,20 @@ use titan::{Message, MessageMeta, Payload};
|
|||
use crate::battle::BattleBuilder;
|
||||
use crate::catalog::Catalog;
|
||||
use crate::config::GameConfig;
|
||||
use crate::home::build_avatar;
|
||||
use crate::bot::BotPlayer;
|
||||
use crate::home::{build_avatar, build_deck};
|
||||
use crate::home_mode::{available_server_command, HomeModeRegistry};
|
||||
use crate::rewards::RewardRoller;
|
||||
use crate::shop::{ShopCatalog, ShopCycle};
|
||||
use crate::store::{connect_failed, unavailable, PlayerProfile, ProfileStore};
|
||||
use crate::time::unix_seconds;
|
||||
pub const ARENA_PVP_LOCATION_COLUMN: &str = "PvpLocation";
|
||||
pub const NPC_LOCATION_COLUMN: &str = "Location";
|
||||
fn location_of(data: &LogicDataRef, column: &str) -> LogicDataRef {
|
||||
data.data()
|
||||
.map(|row| LogicDataRef::by_name(table::LOCATIONS, row.string(column)))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
pub struct GameService {
|
||||
config: GameConfig,
|
||||
profiles: ProfileStore,
|
||||
|
|
@ -90,17 +98,27 @@ impl GameService {
|
|||
.lock()
|
||||
.await
|
||||
.stop_home_logic();
|
||||
let location = npc
|
||||
.data()
|
||||
.map(|row| LogicDataRef::by_name(table::LOCATIONS, row.string("Location")))
|
||||
.unwrap_or_default();
|
||||
let location = if npc.is_none() {
|
||||
location_of(&profile.arena, ARENA_PVP_LOCATION_COLUMN)
|
||||
} else {
|
||||
location_of(&npc, NPC_LOCATION_COLUMN)
|
||||
};
|
||||
let deck = build_deck(&profile);
|
||||
let (opponent, opponent_deck) = if npc.is_none() {
|
||||
let bot = BotPlayer::against(&avatar, &deck);
|
||||
(bot.avatar, bot.deck)
|
||||
} else {
|
||||
(avatar.clone(), deck.clone())
|
||||
};
|
||||
let opponent_name = opponent.name.clone();
|
||||
let battle = self
|
||||
.battles
|
||||
.build(
|
||||
location.clone(),
|
||||
npc.clone(),
|
||||
profile.arena.clone(),
|
||||
vec![avatar.clone(), avatar],
|
||||
vec![avatar, opponent],
|
||||
[Some(deck), Some(opponent_deck)],
|
||||
self.config.random_seed,
|
||||
)
|
||||
.map_err(|error| RpcError::Rejected(error.to_string()))?;
|
||||
|
|
@ -113,7 +131,7 @@ impl GameService {
|
|||
tracing::debug!(steps = ?report.steps, "snapshot sections");
|
||||
tracing::info!(
|
||||
%account,
|
||||
npc = %npc,
|
||||
opponent = %opponent_name,
|
||||
location = %location,
|
||||
objects = battle.battle.objects.objects.len(),
|
||||
bytes = snapshot.len(),
|
||||
|
|
@ -134,13 +152,6 @@ impl GameService {
|
|||
}
|
||||
Ok(vec![encode(&SectorStateMessage::new(snapshot))?])
|
||||
}
|
||||
fn bot_opponent(&self) -> LogicDataRef {
|
||||
LogicDataTables::instance()
|
||||
.table(table::NPCS)
|
||||
.and_then(|rows| rows.get_at(0).cloned())
|
||||
.map(LogicDataRef::from)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
async fn roller(&self, account: AccountRef) -> RpcResult<RewardRoller> {
|
||||
let profile = self.profile(account).await?;
|
||||
let fallback: Vec<LogicDataRef> = profile
|
||||
|
|
@ -249,14 +260,8 @@ impl GameApi for GameService {
|
|||
account: AccountRef,
|
||||
_payload: Vec<u8>,
|
||||
) -> RpcResult<Vec<WireMessage>> {
|
||||
let npc = self.bot_opponent();
|
||||
if npc.is_none() {
|
||||
return Err(RpcError::Rejected(
|
||||
"npcs.csv has no bot to match against".into(),
|
||||
));
|
||||
}
|
||||
tracing::info!(%account, bot = %npc, "matchmaking against a bot");
|
||||
self.sector_state_for(account, npc).await
|
||||
tracing::info!(%account, "matchmaking against a bot player");
|
||||
self.sector_state_for(account, LogicDataRef::None).await
|
||||
}
|
||||
async fn end_client_turn(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,18 @@ impl LogicData {
|
|||
_ => 0,
|
||||
}
|
||||
}
|
||||
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),
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
pub fn value_count(&self, column: &str) -> usize {
|
||||
match (self.row(), self.column(column)) {
|
||||
(Some(row), Some(column)) => row.value_count(column),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
pub fn string(&self, column: &str) -> &str {
|
||||
match (self.row(), self.column(column)) {
|
||||
(Some(row), Some(index)) => row.string(index),
|
||||
|
|
|
|||
Loading…
Reference in a new issue