scroll.server/crates/game-service/src/battle_session.rs
WiseDev dbbd162bed regenerate mana on the server
at type 0 the client stopped working the elixir bar out for itself and
started reading it from the summoner in our snapshot - which never
moved, so the bar sat where the opening state left it and no card could
be afforded.

the rule from LogicSummoner::tick: an accumulator gains five thousand a
tick and one mana is granted for every MANA_REGEN_MS * 100 / MAX_MANA it
holds, the remainder carried rather than dropped. that works out at
2.8 seconds a mana with the shipped globals, and halves in the last
sixty seconds through MANA_REGEN_MS_END, which is the speed-up the game
has always had.
2026-08-23 13:53:41 +03:00

364 lines
13 KiB
Rust

use std::collections::HashMap;
use std::sync::Arc;
pub const SNAPSHOT_INTERVAL_TICKS: i32 = 20;
use std::time::Instant;
use logic::battle::{
LogicGameMode, LogicGameObjectEntry, LogicVector2, BATTLE_TICKS_PER_SECOND, BATTLE_TYPE_PVP,
};
use logic::battle::{verify_snapshot, LogicBattleEvent};
use logic::SectorStateMessage;
use logic::{BattleEventMessage, LogicDataRef, LogicRandom};
use titan::LogicLong;
use service_rpc::{AccountRef, WireMessage};
use tokio::sync::Mutex;
pub struct BattleSession {
mode: LogicGameMode,
tick: i32,
queued: Vec<(i32, Vec<LogicGameObjectEntry>)>,
outbound: Vec<WireMessage>,
started_at: Instant,
random: LogicRandom,
taunts: Vec<LogicDataRef>,
next_bot_emote: i32,
next_snapshot: i32,
next_bot_play: i32,
pending_bot_play: Option<(LogicDataRef, LogicVector2, i32)>,
}
impl BattleSession {
pub fn new(mode: LogicGameMode, taunts: Vec<LogicDataRef>) -> Self {
let mut random = LogicRandom::new(mode.random_seed);
let next_bot_emote = Self::roll_emote_tick(&mut random, 0);
Self {
mode,
tick: 0,
queued: Vec::new(),
outbound: Vec::new(),
started_at: Instant::now(),
random,
taunts,
next_bot_emote,
next_snapshot: SNAPSHOT_INTERVAL_TICKS,
next_bot_play: 0,
pending_bot_play: None,
}
}
pub fn take_bot_play(&mut self) -> Option<(LogicDataRef, LogicVector2, i32)> {
self.pending_bot_play.take()
}
pub fn bot_play(&mut self) -> Option<(LogicDataRef, LogicVector2, i32)> {
let deck = self.mode.battle.decks.get(1)?.as_ref()?;
let filled: Vec<LogicDataRef> = deck
.slots
.iter()
.flatten()
.map(|spell| spell.data.clone())
.collect();
if filled.is_empty() {
return None;
}
let card = filled
.get(self.random.next(filled.len() as i32).max(0) as usize)?
.clone();
let towers: Vec<(i32, i32)> = self
.mode
.battle
.objects
.objects
.iter()
.filter(|entry| entry.owner_index() == crate::bot::BOT_OWNER_INDEX)
.map(|entry| entry.position())
.collect();
let leader = self.mode.battle.leader(crate::bot::BOT_OWNER_INDEX as usize)?;
let (king_x, king_y) = leader.position();
let ahead = towers
.iter()
.map(|(_, y)| *y)
.fold(king_y, |best, y| if (y - king_y).abs() > (best - king_y).abs() { y } else { best });
let lane_x = towers
.iter()
.filter(|(_, y)| *y == ahead)
.map(|(x, _)| *x)
.collect::<Vec<_>>();
let x = lane_x
.get(self.random.next(lane_x.len().max(1) as i32).max(0) as usize)
.copied()
.unwrap_or(king_x);
Some((card, LogicVector2::new(x, ahead), self.next_instance()))
}
pub fn pushes_snapshots(&self) -> bool {
self.mode.battle.battle_type == BATTLE_TYPE_PVP
}
fn snapshot_message(&mut self) -> Option<WireMessage> {
let snapshot = self.mode.snapshot().ok()?;
let report = verify_snapshot(&snapshot);
if !report.is_ok() {
tracing::error!(
bytes = snapshot.len(),
trailing = report.trailing,
error = report.error.as_deref().unwrap_or("-"),
"the battle snapshot does not read back, not pushing it"
);
return None;
}
crate::wire::encode(&SectorStateMessage::new(snapshot)).ok()
}
fn roll_emote_tick(random: &mut LogicRandom, from: i32) -> i32 {
let span = crate::bot::BOT_EMOTE_MAX_SECONDS - crate::bot::BOT_EMOTE_MIN_SECONDS;
let seconds = crate::bot::BOT_EMOTE_MIN_SECONDS + random.next(span.max(1));
from + seconds * BATTLE_TICKS_PER_SECOND
}
pub fn opponent_account(&self) -> LogicLong {
self.mode
.battle
.account_ids
.get(1)
.copied()
.unwrap_or_default()
}
pub fn bot_emote(&mut self) -> Option<LogicBattleEvent> {
if self.taunts.is_empty() {
return None;
}
let index = self.random.next(self.taunts.len() as i32).max(0) as usize;
let taunt = self.taunts.get(index)?;
let instance = taunt.global_id().map(|id| id.instance_id).unwrap_or(0);
Some(LogicBattleEvent {
event_type: 1,
account_id: self.opponent_account(),
ticks: vec![self.tick],
coords: Vec::new(),
params: vec![instance],
})
}
pub fn elapsed_tick(&self) -> i32 {
let millis = self.started_at.elapsed().as_millis() as i64;
(millis * BATTLE_TICKS_PER_SECOND as i64 / 1000) as i32
}
pub fn advance_to_now(&mut self) {
let tick = self.elapsed_tick();
self.advance_to(tick);
}
pub fn push_outbound(&mut self, message: WireMessage) {
self.outbound.push(message);
}
pub fn take_outbound(&mut self) -> Vec<WireMessage> {
std::mem::take(&mut self.outbound)
}
pub fn mode(&self) -> &LogicGameMode {
&self.mode
}
pub fn tick(&self) -> i32 {
self.tick
}
pub fn seconds(&self) -> i32 {
self.tick / BATTLE_TICKS_PER_SECOND
}
pub fn queue(&mut self, at_tick: i32, entries: Vec<LogicGameObjectEntry>) {
if entries.is_empty() {
return;
}
self.queued.push((at_tick.max(self.tick + 1), entries));
}
pub fn advance_to(&mut self, tick: i32) {
while self.tick < tick {
self.tick += 1;
self.release_queued(self.tick);
self.mode.battle.tick(self.tick);
if self.pushes_snapshots() && self.tick >= self.next_snapshot {
self.next_snapshot = self.tick + SNAPSHOT_INTERVAL_TICKS;
if let Some(message) = self.snapshot_message() {
self.outbound.push(message);
}
}
if self.tick >= self.next_bot_play {
let span = crate::bot::BOT_PLAY_MAX_SECONDS - crate::bot::BOT_PLAY_MIN_SECONDS;
let wait = crate::bot::BOT_PLAY_MIN_SECONDS + self.random.next(span.max(1));
self.next_bot_play = self.tick + wait * BATTLE_TICKS_PER_SECOND;
if let Some((card, position, instance)) = self.bot_play() {
self.pending_bot_play = Some((card, position, instance));
}
}
if self.tick >= self.next_bot_emote {
self.next_bot_emote = Self::roll_emote_tick(&mut self.random, self.tick);
if let Some(event) = self.bot_emote() {
if let Ok(message) = crate::wire::encode(&BattleEventMessage::new(event)) {
self.outbound.push(message);
}
}
}
}
self.mode.time.tick = self.tick;
}
fn release_queued(&mut self, tick: i32) {
let mut due = Vec::new();
self.queued.retain(|(at, entries)| {
if *at <= tick {
due.extend(entries.iter().cloned());
false
} else {
true
}
});
for entry in due {
self.mode.battle.objects.push(entry);
}
}
pub fn is_finished(&self) -> bool {
self.mode.battle.is_end_condition_matched(self.tick)
}
pub fn stars(&self) -> (i32, i32) {
(self.mode.battle.stars(0), self.mode.battle.stars(1))
}
pub fn checksum(&self) -> Option<i32> {
self.mode.calculate_checksum().ok()
}
pub fn towers_standing(&self) -> (usize, usize) {
(
self.mode.battle.leader_towers[0].len(),
self.mode.battle.leader_towers[1].len(),
)
}
pub fn owner_of(&self, account: LogicLong) -> Option<i32> {
self.mode
.battle
.account_ids
.iter()
.position(|id| *id == account)
.map(|index| index as i32)
}
pub fn next_instance(&self) -> i32 {
self.mode
.battle
.objects
.objects
.iter()
.filter_map(|entry| entry.global_id.0.map(|id| id.instance_id))
.max()
.map(|last| last + 1)
.unwrap_or(0)
}
pub fn deck_card(&self, owner: i32, slot: i32) -> Option<LogicDataRef> {
let deck = self.mode.battle.decks.get(owner.max(0) as usize)?.as_ref()?;
let spell = deck.slots.get(slot.max(0) as usize)?.as_ref()?;
Some(spell.data.clone())
}
pub fn push(&mut self, entry: LogicGameObjectEntry) {
self.mode.battle.objects.push(entry);
}
pub fn object_count(&self) -> usize {
self.mode.battle.objects.objects.len()
}
}
#[derive(Default)]
pub struct BattleRegistry {
sessions: Mutex<HashMap<AccountRef, Arc<Mutex<BattleSession>>>>,
}
impl BattleRegistry {
pub async fn start(
&self,
players: &[AccountRef],
mode: LogicGameMode,
taunts: Vec<LogicDataRef>,
) {
let session = Arc::new(Mutex::new(BattleSession::new(mode, taunts)));
let mut sessions = self.sessions.lock().await;
for account in players {
sessions.insert(*account, Arc::clone(&session));
}
}
async fn session(&self, account: AccountRef) -> Option<Arc<Mutex<BattleSession>>> {
self.sessions.lock().await.get(&account).cloned()
}
pub async fn is_running(&self, account: AccountRef) -> bool {
self.sessions.lock().await.contains_key(&account)
}
pub async fn finish(&self, account: AccountRef) -> Option<Arc<Mutex<BattleSession>>> {
self.sessions.lock().await.remove(&account)
}
pub async fn tick<F>(&self, account: AccountRef, summon: F) -> Vec<WireMessage>
where
F: Fn(&LogicDataRef, LogicVector2, i32, i32) -> Vec<LogicGameObjectEntry>,
{
let Some(handle) = self.session(account).await else {
return Vec::new();
};
let mut session = handle.lock().await;
session.advance_to_now();
if let Some((card, position, instance)) = session.take_bot_play() {
let entries = summon(&card, position, crate::bot::BOT_OWNER_INDEX, instance);
if !entries.is_empty() {
tracing::debug!(card = %card, count = entries.len(), "the bot played a card");
for entry in entries {
session.push(entry);
}
}
}
session.take_outbound()
}
pub async fn answer_emote(&self, account: AccountRef) {
let Some(handle) = self.session(account).await else {
return;
};
let mut session = handle.lock().await;
let Some(event) = session.bot_emote() else {
return;
};
if let Ok(message) = crate::wire::encode(&BattleEventMessage::new(event)) {
session.push_outbound(message);
}
}
pub async fn send(&self, account: AccountRef, message: WireMessage) {
if let Some(handle) = self.session(account).await {
handle.lock().await.push_outbound(message);
}
}
pub async fn advance(&self, account: AccountRef, tick: i32) -> Option<BattleProgress> {
let handle = self.session(account).await?;
let mut session = handle.lock().await;
session.advance_to(tick);
Some(BattleProgress {
seconds: session.seconds(),
finished: session.is_finished(),
stars: session.stars(),
objects: session.object_count(),
towers: session.towers_standing(),
checksum: session.checksum(),
})
}
pub async fn play<F>(
&self,
account: AccountRef,
executor: LogicLong,
slot: i32,
position: logic::battle::LogicVector2,
at_tick: i32,
summon: F,
) -> usize
where
F: Fn(&LogicDataRef, logic::battle::LogicVector2, i32, i32) -> Vec<LogicGameObjectEntry>,
{
let Some(handle) = self.session(account).await else {
return 0;
};
let mut session = handle.lock().await;
let Some(owner) = session.owner_of(executor) else {
return 0;
};
let Some(card) = session.deck_card(owner, slot) else {
return 0;
};
let entries = summon(&card, position, owner, session.next_instance());
let spawned = entries.len();
session.queue(at_tick, entries);
spawned
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BattleProgress {
pub seconds: i32,
pub finished: bool,
pub stars: (i32, i32),
pub objects: usize,
pub towers: (usize, usize),
pub checksum: Option<i32>,
}