Skip to content

Commit

Permalink
Simplify attacks of opportunity (elliptic)
Browse files Browse the repository at this point in the history
Using new pursuit tech, shift the time that enemies launch opportunity
attacks from when the *player* moves to when an *enemy* follows the
player. This has several advantages:

- It means that monsters who don't actually pursue players won't get
  attacks of opportunity. That includes monsters which cast a spell,
  fired a ranged weapon, or were just blocked by other monsters
  instead of moving.
- Removes special cases involving actions that were previously
  disabled for attacks of opportunity, e.g. broodmothers' spider
  spawning effect.
- Removes the need to memorize monster vs player speeds to figure out
  whether attacks of opportunity could trigger. They'll now trigger
  whenever the monster manages to pursue.
- Removes the counterintuitive behavior by which monsters with
  reaching weapons (i.e. polearms) couldn't launch attacks from afar.
  They'll now do so if and only if they move to pursue - that is,
  on the turn the player moves away, they'll normally stand pat
  and launch a normal attack instead.
- In general, reduces complexity both for players and developers.

As always, this is an experiment. Let's try it out. :)
  • Loading branch information
PleasingFungus committed Aug 19, 2023
1 parent 01f9145 commit 4209ae1
Show file tree
Hide file tree
Showing 9 changed files with 62 additions and 114 deletions.
8 changes: 2 additions & 6 deletions crawl-ref/source/attack.cc
Expand Up @@ -479,14 +479,10 @@ bool attack::distortion_affects_defender()
NONE
};

// Don't banish or blink the player during aoops, for sanity.
const int banish_weight = crawl_state.player_moving ? 0 : 5;
const int blink_weight = crawl_state.player_moving ? 0 : 20;

const disto_effect choice = random_choose_weighted(35, SMALL_DMG,
25, BIG_DMG,
banish_weight, BANISH,
blink_weight, BLINK,
5, BANISH,
20, BLINK,
15, NONE);

if (simu && !(choice == SMALL_DMG || choice == BIG_DMG))
Expand Down
1 change: 0 additions & 1 deletion crawl-ref/source/hints.cc
Expand Up @@ -1050,7 +1050,6 @@ static bool _tutorial_interesting(hints_event_type event)
case HINT_SPELL_MISCAST:
case HINT_CLOUD_WARNING:
case HINT_SKILL_RAISE:
case HINT_OPPORTUNITY_ATTACK:
return true;
default:
return false;
Expand Down
20 changes: 5 additions & 15 deletions crawl-ref/source/melee-attack.cc
Expand Up @@ -2704,12 +2704,8 @@ bool melee_attack::mons_attack_effects()
&& !player_stair_delay() // feet otherwise occupied
&& player_equip_unrand(UNRAND_SLICK_SLIPPERS);
// Don't trample while player is moving - either mean or nonsensical
if (attacker != defender
&& (attk_flavour == AF_TRAMPLE || slippery)
&& !crawl_state.player_moving)
{
if (attacker != defender && (attk_flavour == AF_TRAMPLE || slippery))
do_knockback(slippery);
}

special_damage = 0;
special_damage_message.clear();
Expand Down Expand Up @@ -2872,12 +2868,12 @@ void melee_attack::mons_apply_attack_flavour()

case AF_BLINK:
// blinking can kill, delay the call
if (one_chance_in(3) && !crawl_state.player_moving)
if (one_chance_in(3))
blink_fineff::schedule(attacker);
break;

case AF_BLINK_WITH:
if (coinflip() && !crawl_state.player_moving)
if (coinflip())
blink_fineff::schedule(attacker, defender);
break;

Expand Down Expand Up @@ -3091,13 +3087,11 @@ void melee_attack::mons_apply_attack_flavour()
break;

case AF_ENSNARE:
if (!crawl_state.player_moving && one_chance_in(3))
if (one_chance_in(3))
ensnare(defender);
break;

case AF_CRUSH:
if (crawl_state.player_moving)
break; // Won't work while player is moving
if (needs_message)
{
mprf("%s %s %s.",
Expand All @@ -3112,9 +3106,7 @@ void melee_attack::mons_apply_attack_flavour()
break;

case AF_ENGULF:
if (!crawl_state.player_moving // Won't work while player is moving
&& x_chance_in_y(2, 3)
&& attacker->can_engulf(*defender))
if (x_chance_in_y(2, 3) && attacker->can_engulf(*defender))
{
const bool watery = attacker->type != MONS_QUICKSILVER_OOZE;
if (defender->is_player() && !you.duration[DUR_WATER_HOLD])
Expand Down Expand Up @@ -3297,8 +3289,6 @@ void melee_attack::mons_apply_attack_flavour()
break;
}
case AF_SLEEP:
if (crawl_state.player_moving)
break; // looks too weird to fall asleep while still in motion
if (!coinflip())
break;
if (attk_type == AT_SPORE)
Expand Down
52 changes: 50 additions & 2 deletions crawl-ref/source/mon-act.cc
Expand Up @@ -3052,7 +3052,7 @@ static void _maybe_randomize_energy(monster &mons, coord_def orig_pos)
mons.speed_increment += random2(3) - 1;
}

void launch_opportunity_attack(monster& mons)
static void _launch_opportunity_attack(monster& mons)
{
monster *ru_target = nullptr;
if (_handle_ru_melee_redirection(mons, &ru_target))
Expand All @@ -3061,6 +3061,54 @@ void launch_opportunity_attack(monster& mons)
learned_something_new(HINT_OPPORTUNITY_ATTACK);
}

static void _maybe_launch_opportunity_attack(monster &mon, coord_def orig_pos)
{
if (!crawl_state.potential_pursuers.count(&mon))
return;

const int new_dist = grid_distance(you.pos(), mon.pos());
// Some of these duplicate checks when marking potential
// pursuers. This is to avoid state changes after your turn
// and before the monster's.
// No, there is no logic to this ordering (pf):
if (!one_chance_in(3)
|| mon.wont_attack()
|| !mons_has_attacks(mon)
|| mon.confused()
|| mon.incapacitated()
|| mons_is_fleeing(mon)
|| !mon.can_see(you)
// the monster must actually be approaching you.
|| new_dist >= grid_distance(you.pos(), orig_pos)
// make sure they can actually reach you.
|| new_dist > mon.reach_range()
|| !cell_see_cell(mon.pos(), you.pos(), LOS_NO_TRANS)
// Zin protects!
|| is_sanctuary(mon.pos()))
{
return;
}

actor* foe = mon.get_foe();
if (!foe || !foe->is_player())
return;

// No random energy and no double opportunity attacks in a turn
// that they already launched an attack.
crawl_state.potential_pursuers.erase(&mon);

const string msg = make_stringf(" attacks as %s pursues you!",
mon.pronoun(PRONOUN_SUBJECTIVE).c_str());
simple_monster_message(mon, msg.c_str());
const int old_energy = mon.speed_increment;
_launch_opportunity_attack(mon);
// Refund most of the energy from the attack - for normal attack
// speed monsters, it will cost 0 energy 1/2 of the time and
// 1 energy 1/2 of the time.
// Only slow-attacking monsters will spend more than 1 energy.
mon.speed_increment = min(mon.speed_increment + 10, old_energy - random2(2));
}

static bool _do_move_monster(monster& mons, const coord_def& delta)
{
const coord_def orig_pos = mons.pos();
Expand Down Expand Up @@ -3189,7 +3237,7 @@ static bool _do_move_monster(monster& mons, const coord_def& delta)

_swim_or_move_energy(mons);

// Randomize move energy for monsters pursuing the player.
_maybe_launch_opportunity_attack(mons, orig_pos);
_maybe_randomize_energy(mons, orig_pos);

return true;
Expand Down
2 changes: 0 additions & 2 deletions crawl-ref/source/mon-act.h
Expand Up @@ -36,6 +36,4 @@ void handle_monster_move(monster* mon);

void queue_monster_for_action(monster* mons);

void launch_opportunity_attack(monster &mon);

#define ENERGY_SUBMERGE(entry) (max(entry->energy_usage.swim / 2, 1))
3 changes: 0 additions & 3 deletions crawl-ref/source/mon-place.cc
Expand Up @@ -2863,9 +2863,6 @@ monster* create_monster(mgen_data mg, bool fail_msg)
{
ASSERT(in_bounds(mg.pos)); // otherwise it's a guaranteed fail

if (crawl_state.player_moving)
return nullptr; // monster might end up on player's tile - too scary

const monster_type montype = fixup_zombie_type(mg.cls, mg.base_type);

monster *summd = 0;
Expand Down
87 changes: 5 additions & 82 deletions crawl-ref/source/movement.cc
Expand Up @@ -242,72 +242,6 @@ static void _mark_potential_pursuers(coord_def new_pos)
}
}

static void _trigger_opportunity_attacks(coord_def new_pos)
{
if (you.attribute[ATTR_SERPENTS_LASH] // too fast!
|| wu_jian_move_triggers_attacks(new_pos) // too cool!
|| is_sanctuary(you.pos()) // Zin protects!
|| is_sanctuary(new_pos)) // .. very generously.
{
return;
}

unwind_bool moving(crawl_state.player_moving, true);

const coord_def orig_pos = you.pos();
for (adjacent_iterator ai(orig_pos); ai; ++ai)
{
if (adjacent(*ai, new_pos))
continue;
monster* mon = monster_at(*ai);
// No, there is no logic to this ordering (pf):
if (!mon
|| mon->wont_attack()
|| !mons_has_attacks(*mon)
|| mon->confused()
|| mon->incapacitated()
|| mons_is_fleeing(*mon)
|| mon->is_constricted() && (mon->constricted_by != MID_PLAYER
|| mon->get_constrict_type() != CONSTRICT_MELEE)
|| !mon->can_see(you)
// only let monsters attack if they might follow you
|| !mon->may_have_action_energy() || mon->is_stationary()
// if you're swapping with a pal or moving off a fedhas plant,
// you can't be followed, so no aoops
|| monster_at(you.pos())
// Zin protects!
|| is_sanctuary(mon->pos())
// creates some weird bugs
|| mons_self_destructs(*mon)
// monsters that are slower than you mayn't attack
|| mon->outpaced_by_player()
|| !one_chance_in(3))
{
continue;
}

actor* foe = mon->get_foe();
if (!foe || !foe->is_player())
continue;

// Don't randomize movement energy for monsters that got an
// opportunity attack this turn.
crawl_state.potential_pursuers.erase(mon);

simple_monster_message(*mon, " attacks as you move away!");
const int old_energy = mon->speed_increment;
launch_opportunity_attack(*mon);
// Refund most of the energy from the attack - for normal attack
// speed monsters, it will cost 0 energy 1/2 of the time and
// 1 energy 1/2 of the time.
// Only slow-attacking monsters will spend more than 1 energy.
mon->speed_increment = min(mon->speed_increment + 10, old_energy - random2(2));

if (you.pending_revival || you.pos() != orig_pos)
return;
}
}

bool apply_cloud_trail(const coord_def old_pos)
{
if (you.duration[DUR_CLOUD_TRAIL])
Expand Down Expand Up @@ -1142,9 +1076,6 @@ void move_player_action(coord_def move)
else if (!running)
clear_travel_trail();

// Calculate time_taken before checking opportunity attacks so that
// we can guess whether monsters will be able to follow you (& hence
// trigger opp attacks).
_apply_move_time_taken();

coord_def old_pos = you.pos();
Expand All @@ -1153,20 +1084,12 @@ void move_player_action(coord_def move)
if (you.pos() != targ && targ_pass)
{
_clear_constriction_data();
// Make a list of pursuers before triggering opportunity attacks
// so that we can remove any monster that did one.
_mark_potential_pursuers(targ);
_trigger_opportunity_attacks(targ);
// Check nothing weird happened during opportunity attacks.
if (!you.pending_revival)
{
_mark_potential_pursuers(targ);
move_player_to_grid(targ, true);
apply_barbs_damage();
remove_ice_movement();
you.clear_far_engulf(false, true);
apply_cloud_trail(old_pos);
}
move_player_to_grid(targ, true);
apply_barbs_damage();
remove_ice_movement();
you.clear_far_engulf(false, true);
apply_cloud_trail(old_pos);
}

// Now it is safe to apply the swappee's location effects and add
Expand Down
1 change: 0 additions & 1 deletion crawl-ref/source/state.cc
Expand Up @@ -63,7 +63,6 @@ game_state::game_state()
tiles_disabled(false),
title_screen(true),
invisible_targeting(false),
player_moving(false),
darken_range(nullptr), unsaved_macros(false), disables(),
minor_version(-1), save_rcs_version(),
nonempty_buffer_flush_errors(false),
Expand Down
2 changes: 0 additions & 2 deletions crawl-ref/source/state.h
Expand Up @@ -133,8 +133,6 @@ struct game_state

bool invisible_targeting;

bool player_moving;

// Area beyond which view should be darkened, 0 = disabled.
targeter *darken_range;

Expand Down

0 comments on commit 4209ae1

Please sign in to comment.