Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/battle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ use crate::{
battle_phase::{
PhaseMessage, StartOfPhaseEffectsMessage, TurnStartMessage,
advance_after_start_of_phase_effects, check_for_active_effect_damage_on_turn_start,
check_should_advance_phase, decrement_turn_count_effects_on_turn_start, init_phase_system,
is_enemy_phase, is_running_enemy_phase, is_running_player_phase,
check_for_stun_on_turn_start, check_should_advance_phase,
decrement_turn_count_effects_on_turn_start, init_phase_system, is_enemy_phase,
is_running_enemy_phase, is_running_player_phase,
phase_ui::{
BattlePhaseMessageComplete, ShowBattleBannerMessage, banner_animation_system,
spawn_banner_system,
Expand Down Expand Up @@ -216,6 +217,8 @@ pub fn battle_plugin(app: &mut App) {
decrement_turn_count_effects_on_turn_start::<Enemy>,
check_for_active_effect_damage_on_turn_start::<Player>,
check_for_active_effect_damage_on_turn_start::<Enemy>,
check_for_stun_on_turn_start::<Player>,
check_for_stun_on_turn_start::<Enemy>,
advance_after_start_of_phase_effects,
spawn_banner_system,
banner_animation_system,
Expand Down
19 changes: 19 additions & 0 deletions src/battle_phase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,25 @@ pub fn check_for_active_effect_damage_on_turn_start<T: PhaseSystem<PlayerEnemyPh
}
}

pub fn check_for_stun_on_turn_start<T: PhaseSystem<PlayerEnemyPhase>>(
mut message_reader: MessageReader<StartOfPhaseEffectsMessage>,
mut query: Query<(&ActiveEffects, &mut UnitPhaseResources), With<T::Marker>>,
) {
for message in message_reader.read() {
if message.phase != T::OWNED_PHASE {
continue;
}

for (active_effects, mut phase_resources) in query.iter_mut() {
if active_effects.statuses().contains(&StatusTag::Stunned) {
phase_resources.action_points_left_in_phase = 0;
phase_resources.movement_points_left_in_phase = 0;
phase_resources.waited = true;
}
}
}
}

#[derive(Component)]
struct PoisonDamageEntity;

Expand Down
5 changes: 4 additions & 1 deletion src/combat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1685,7 +1685,10 @@ pub mod skills {
advancing_event: SkillEvent::ProjectileImpact(SkillAnimationId(1)),
},
SkillStage {
stage: SkillStageAction::Impact(vec![SkillActionIndex(0)]),
stage: SkillStageAction::Impact(vec![
SkillActionIndex(0),
SkillActionIndex(1),
]),
advancing_event: SkillEvent::ProjectileImpact(SkillAnimationId(1)),
},
],
Expand Down
13 changes: 11 additions & 2 deletions src/enemy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{
skills::{ATTACK_SKILL_ID, Targeting},
},
enemy::behaviors::EnemyAiBehavior,
gameplay_effects::ActiveEffects,
grid::{
GridManager, GridManagerResource, GridPosition, GridPositionChangeResult,
manhattan_distance,
Expand Down Expand Up @@ -42,12 +43,12 @@ pub fn begin_enemy_phase(
mut commands: Commands,
mut message_reader: MessageReader<PhaseMessage>,
mut conductor: ResMut<EnemyTurnConductorResource>,
enemy_units: Query<(Entity, &Unit, &UnitDerivedStats), With<Enemy>>,
enemy_units: Query<(Entity, &Unit, &UnitDerivedStats, Option<&ActiveEffects>), With<Enemy>>,
) {
for message in message_reader.read() {
let PhaseMessageType::PhaseBegin(phase) = message.0;
if phase == PlayerEnemyPhase::Enemy {
for (e, unit, stats) in enemy_units.iter() {
for (e, unit, stats, active_effects) in enemy_units.iter() {
// Clean up any potential stale references to Enemy Behaviors
commands
.entity(e)
Expand All @@ -57,6 +58,14 @@ pub fn begin_enemy_phase(
continue;
}

// Skip stunned enemies — they lose their turn
if active_effects
.map(|effects| effects.prevent_action() && effects.prevent_move())
.unwrap_or(false)
{
continue;
}

info!("Adding {:?} to Enemy Turn List", unit.name);
conductor.0.queue.push_front(e);
}
Expand Down
130 changes: 125 additions & 5 deletions src/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -724,18 +724,43 @@ pub fn execute_unit_actions(
mut commands: Commands,
mut reader: MessageReader<UnitExecuteActionMessage>,
mut command_completed_writer: MessageWriter<UnitActionCompletedMessage>,
active_effects_query: Query<&ActiveEffects>,
// I don't love that I do this here since I do the other things out of band, but I don't
// really need to wait for anything else to wait so :shrug:
mut unit_phase_resources: Query<&mut UnitPhaseResources>,
) {
for message in reader.read() {
let active_effects = active_effects_query.get(message.entity).ok();

match &message.action {
UnitExecuteAction::Move(valid_move) => {
if active_effects
.map(|active_effects| active_effects.prevent_move())
.unwrap_or_default()
{
command_completed_writer.write(UnitActionCompletedMessage {
unit: message.entity,
action: UnitAction::Move,
});
continue;
}

Comment on lines +737 to +747
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably would want to find a way to prevent the UnitExecuteAction itself as opposed to processing the action, but skipping the completion I think, but pretty impressive that the model came up with this imo

commands
.entity(message.entity)
.insert(GridMovement::new(valid_move.path.clone(), 0.4));
}
UnitExecuteAction::Attack(attack_intent) => {
if active_effects
.map(|active_effects| active_effects.prevent_action())
.unwrap_or_default()
{
command_completed_writer.write(UnitActionCompletedMessage {
unit: message.entity,
action: UnitAction::Attack,
});
continue;
}

commands.spawn((CombatActionMarker, attack_intent.clone()));
}
UnitExecuteAction::Wait => {
Expand Down Expand Up @@ -1402,18 +1427,25 @@ mod tests {
PhaseMessage, UnitPhaseResources, check_should_advance_phase, init_phase_system,
phase_ui::ShowBattleBannerMessage, prepare_for_phase,
},
combat::skills::setup_skill_system,
combat::{
AttackIntent,
skills::{ATTACK_SKILL_ID, setup_skill_system},
},
gameplay_effects::{
ActiveEffects, Effect, EffectData, EffectDuration, EffectMetadata, EffectType,
StatusTag,
},
grid::{
self, GridManager, GridManagerResource, GridMovement, GridPosition,
sync_grid_positions_to_manager,
},
grid_cursor,
player::{self, Player, PlayerGameStates, PlayerInputAction, PlayerState},
unit::{
PLAYER_TEAM, StatContainer, StatType, StatValue, Unit, UnitActionCompletedMessage,
UnitBaseStats, UnitDerivedStats, UnitExecuteActionMessage, execute_unit_actions,
handle_unit_cursor_actions, handle_unit_ui_command, overlay::OverlaysMessage,
unlock_cursor_after_unit_ui_command,
CombatActionMarker, PLAYER_TEAM, StatContainer, StatType, StatValue, Unit, UnitAction,
UnitActionCompletedMessage, UnitBaseStats, UnitDerivedStats, UnitExecuteAction,
UnitExecuteActionMessage, ValidMove, execute_unit_actions, handle_unit_cursor_actions,
handle_unit_ui_command, overlay::OverlaysMessage, unlock_cursor_after_unit_ui_command,
},
};
use bevy::{
Expand All @@ -1423,6 +1455,7 @@ mod tests {
audio::AudioPlugin,
ecs::system::RunSystemOnce,
input::InputPlugin,
prelude::{Entity, MessageReader, Query},
time::Time,
transform::components::Transform,
};
Expand Down Expand Up @@ -1600,4 +1633,91 @@ mod tests {

Ok(())
}

fn stunned_effect(turn_count: u8) -> ActiveEffects {
ActiveEffects {
effects: vec![Effect {
metadata: EffectMetadata {
target: Entity::PLACEHOLDER,
source: None,
},
data: EffectData {
effect_type: EffectType::StatusInfliction(StatusTag::Stunned),
duration: EffectDuration::TurnCount(turn_count),
},
}],
}
}

#[test]
fn test_execute_unit_actions_blocks_stunned_move_and_attack() -> anyhow::Result<()> {
let mut app = App::new();
app.add_message::<UnitExecuteActionMessage>();
app.add_message::<UnitActionCompletedMessage>();

let stunned_unit = app
.world_mut()
.spawn((
stunned_effect(2),
UnitPhaseResources {
movement_points_left_in_phase: 3,
action_points_left_in_phase: 1,
waited: false,
},
GridPosition { x: 2, y: 2 },
))
.id();

let defender = app.world_mut().spawn_empty().id();

app.world_mut().write_message(UnitExecuteActionMessage {
entity: stunned_unit,
action: UnitExecuteAction::Move(ValidMove {
target: GridPosition { x: 3, y: 2 },
path: vec![GridPosition { x: 2, y: 2 }, GridPosition { x: 3, y: 2 }],
movement_used: 1,
}),
});

app.world_mut().write_message(UnitExecuteActionMessage {
entity: stunned_unit,
action: UnitExecuteAction::Attack(AttackIntent {
attacker: stunned_unit,
defender,
skill: ATTACK_SKILL_ID,
}),
});

app.world_mut()
.run_system_once(execute_unit_actions)
.map_err(|e| anyhow::anyhow!("Failed to run execute_unit_actions: {:?}", e))?;

assert!(app.world().get::<GridMovement>(stunned_unit).is_none());

let combat_marker_count = app
.world_mut()
.run_system_once(|query: Query<&CombatActionMarker>| query.iter().count())
.map_err(|e| anyhow::anyhow!("Failed to inspect combat markers: {:?}", e))?;
assert_eq!(combat_marker_count, 0);

let completed_actions = app
.world_mut()
.run_system_once(|mut reader: MessageReader<UnitActionCompletedMessage>| {
reader
.read()
.map(|m| (m.unit, m.action))
.collect::<Vec<_>>()
})
.map_err(|e| anyhow::anyhow!("Failed to read action completion messages: {:?}", e))?;

assert_eq!(
completed_actions,
vec![
(stunned_unit, UnitAction::Move),
(stunned_unit, UnitAction::Attack),
]
);

Ok(())
}
}