@@ -204,7 +204,8 @@ void StartAttack(Player &player, Direction d, bool includesFirstFrame)
204204 // Combining Fastest Attack with any other attack speed modifier skips over the fourth frame, reducing the effectiveness of Fastest Attack.
205205 // Faster Attack makes up for this by also skipping the sixth frame so this case only applies when using Quick or Fast Attack modifiers.
206206 skippedAnimationFrames = 3 ;
207- } else if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FastestAttack)) {
207+ } else if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FastestAttack)
208+ || player.isOnArenaLevel () && player._pClass == HeroClass::Warrior) {
208209 skippedAnimationFrames = 4 ;
209210 } else if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FasterAttack)) {
210211 skippedAnimationFrames = 3 ;
@@ -219,7 +220,8 @@ void StartAttack(Player &player, Direction d, bool includesFirstFrame)
219220 skippedAnimationFrames = 2 ;
220221 } else if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FastAttack)) {
221222 skippedAnimationFrames = 1 ;
222- } else if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FastestAttack)) {
223+ } else if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FastestAttack)
224+ || player.isOnArenaLevel () && player._pClass == HeroClass::Warrior) {
223225 // Fastest Attack is skipped if Fast or Faster Attack is also specified, because both skip the frame that triggers Fastest Attack skipping.
224226 skippedAnimationFrames = 2 ;
225227 }
@@ -246,7 +248,8 @@ void StartRangeAttack(Player &player, Direction d, WorldTileCoord cx, WorldTileC
246248 if (includesFirstFrame && HasAnyOf (player._pIFlags , ItemSpecialEffect::QuickAttack | ItemSpecialEffect::FastAttack)) {
247249 skippedAnimationFrames += 1 ;
248250 }
249- if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FastAttack)) {
251+ if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FastAttack)
252+ || player.isOnArenaLevel () && player._pClass == HeroClass::Rogue) {
250253 skippedAnimationFrames += 1 ;
251254 }
252255 }
@@ -301,10 +304,18 @@ void StartSpell(Player &player, Direction d, WorldTileCoord cx, WorldTileCoord c
301304 if (!isValid)
302305 return ;
303306
307+ int8_t skippedAnimationFrames = 0 ;
308+ // Arena Fast Cast
309+ if (player.isOnArenaLevel () && (player._pClass == HeroClass::Rogue)) {
310+ skippedAnimationFrames = 4 ;
311+ } else if (player.isOnArenaLevel () && player._pClass == HeroClass::Warrior) {
312+ skippedAnimationFrames = 6 ;
313+ }
314+
304315 auto animationFlags = AnimationDistributionFlags::ProcessAnimationPending;
305316 if (player._pmode == PM_SPELL)
306317 animationFlags = static_cast <AnimationDistributionFlags>(animationFlags | AnimationDistributionFlags::RepeatedAction);
307- NewPlrAnim (player, GetPlayerGraphicForSpell (player.queuedSpell .spellId ), d, animationFlags, 0 , player._pSFNum );
318+ NewPlrAnim (player, GetPlayerGraphicForSpell (player.queuedSpell .spellId ), d, animationFlags, skippedAnimationFrames , player._pSFNum );
308319
309320 PlaySfxLoc (GetSpellData (player.queuedSpell .spellId ).sSFX , player.position .tile );
310321
@@ -401,6 +412,8 @@ void InitLevelChange(Player &player)
401412 RemovePlrMissiles (player);
402413 player.pManaShield = false ;
403414 player.wReflections = 0 ;
415+ player.arenaLastStunTime = 0 ;
416+ player.arenaStunHitCount = 0 ;
404417 if (&player != MyPlayer) {
405418 // share info about your manashield when another player joins the level
406419 if (myPlayer.pManaShield )
@@ -763,6 +776,11 @@ bool PlrHitPlr(Player &attacker, Player &target)
763776 int blkper = target.GetBlockChance () - (attacker._pLevel * 2 );
764777 blkper = clamp (blkper, 0 , 100 );
765778
779+ // Arena balance: cap block chance at 75%
780+ if (target.isOnArenaLevel () && blkper > 75 ) {
781+ blkper = 75 ;
782+ }
783+
766784 if (hit >= hper) {
767785 return false ;
768786 }
@@ -797,11 +815,70 @@ bool PlrHitPlr(Player &attacker, Player &target)
797815 }
798816 RedrawComponent (PanelDrawComponent::Health);
799817 }
818+
819+ // Arena steal effects: enable all mana/life steal against players in arena
820+ if (target.isOnArenaLevel ()) {
821+ int stealAmount = 0 ;
822+
823+ // Mana steal effects
824+ if (HasAnyOf (attacker._pIFlags , ItemSpecialEffect::StealMana3 | ItemSpecialEffect::StealMana5) && HasNoneOf (attacker._pIFlags , ItemSpecialEffect::NoMana)) {
825+ if (HasAnyOf (attacker._pIFlags , ItemSpecialEffect::StealMana3)) {
826+ stealAmount = 3 * dam / 100 ;
827+ }
828+ if (HasAnyOf (attacker._pIFlags , ItemSpecialEffect::StealMana5)) {
829+ stealAmount = 5 * dam / 100 ;
830+ }
831+ attacker._pMana += stealAmount;
832+ if (attacker._pMana > attacker._pMaxMana ) {
833+ attacker._pMana = attacker._pMaxMana ;
834+ }
835+ attacker._pManaBase += stealAmount;
836+ if (attacker._pManaBase > attacker._pMaxManaBase ) {
837+ attacker._pManaBase = attacker._pMaxManaBase ;
838+ }
839+ RedrawComponent (PanelDrawComponent::Mana);
840+ }
841+
842+ // Life steal effects
843+ if (HasAnyOf (attacker._pIFlags , ItemSpecialEffect::StealLife3 | ItemSpecialEffect::StealLife5)) {
844+ if (HasAnyOf (attacker._pIFlags , ItemSpecialEffect::StealLife3)) {
845+ stealAmount = 3 * dam / 100 ;
846+ }
847+ if (HasAnyOf (attacker._pIFlags , ItemSpecialEffect::StealLife5)) {
848+ stealAmount = 5 * dam / 100 ;
849+ }
850+ attacker._pHitPoints += stealAmount;
851+ if (attacker._pHitPoints > attacker._pMaxHP ) {
852+ attacker._pHitPoints = attacker._pMaxHP ;
853+ }
854+ attacker._pHPBase += stealAmount;
855+ if (attacker._pHPBase > attacker._pMaxHPBase ) {
856+ attacker._pHPBase = attacker._pMaxHPBase ;
857+ }
858+ RedrawComponent (PanelDrawComponent::Health);
859+ }
860+ }
800861 if (&attacker == MyPlayer) {
801862 NetSendCmdDamage (true , target.getId (), skdam, DamageType::Physical);
802863 }
803864 StartPlrHit (target, skdam, false );
804865
866+ // Arena knockback: enable knockback against players in arena
867+ if (target.isOnArenaLevel () && HasAnyOf (attacker._pIFlags , ItemSpecialEffect::Knockback)) {
868+ if (target._pmode != PM_GOTHIT)
869+ StartPlrHit (target, 0 , true );
870+
871+ Direction knockbackDir = GetDirection (attacker.position .tile , target.position .tile );
872+ Point newPosition = target.position .tile + knockbackDir;
873+ if (PosOkPlayer (target, newPosition)) {
874+ target.position .tile = newPosition;
875+ FixPlayerLocation (target, target._pdir );
876+ FixPlrWalkTags (target);
877+ dPlayer[newPosition.x ][newPosition.y ] = target.getId () + 1 ;
878+ SetPlayerOld (target);
879+ }
880+ }
881+
805882 return true ;
806883}
807884
@@ -2376,6 +2453,8 @@ void CreatePlayer(Player &player, HeroClass c)
23762453 player.pManaShield = false ;
23772454 player.pDamAcFlags = ItemSpecialEffectHf::None;
23782455 player.wReflections = 0 ;
2456+ player.arenaLastStunTime = 0 ;
2457+ player.arenaStunHitCount = 0 ;
23792458
23802459 InitDungMsgs (player);
23812460 CreatePlrItems (player);
@@ -2510,6 +2589,8 @@ void InitPlayer(Player &player, bool firstTime)
25102589 player.queuedSpell .spellType = player._pRSplType ;
25112590 player.pManaShield = false ;
25122591 player.wReflections = 0 ;
2592+ player.arenaLastStunTime = 0 ;
2593+ player.arenaStunHitCount = 0 ;
25132594 }
25142595
25152596 if (player.isOnActiveLevel ()) {
@@ -2632,7 +2713,8 @@ void StartPlrBlock(Player &player, Direction dir)
26322713 PlaySfxLoc (IS_ISWORD, player.position .tile );
26332714
26342715 int8_t skippedAnimationFrames = 0 ;
2635- if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FastBlock)) {
2716+ if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FastBlock)
2717+ || player.isOnArenaLevel ()) {
26362718 skippedAnimationFrames = (player._pBFrames - 2 ); // ISPL_FASTBLOCK means we cancel the animation if frame 2 was shown
26372719 }
26382720
@@ -2656,6 +2738,32 @@ void FixPlrWalkTags(const Player &player)
26562738 }
26572739}
26582740
2741+ bool CanPlayerBeStunnedInArena (Player &player)
2742+ {
2743+ if (!player.isOnArenaLevel ())
2744+ return true ; // Normal stun rules apply outside arena
2745+
2746+ const uint32_t currentTime = SDL_GetTicks ();
2747+ const uint32_t stunCooldown = 1000 ; // 1 second cooldown
2748+ const uint8_t maxHitsPerCooldown = 1 ; // can only be stunned once per second
2749+
2750+ // Check if enough time has passed since last stun
2751+ if (currentTime - player.arenaLastStunTime >= stunCooldown) {
2752+ // Reset hit counter after cooldown period
2753+ player.arenaStunHitCount = 0 ;
2754+ }
2755+
2756+ // Check if player has been hit too many times recently
2757+ if (player.arenaStunHitCount >= maxHitsPerCooldown) {
2758+ return false ; // Player is immune to stun
2759+ }
2760+
2761+ // Allow stun and increment hit counter
2762+ player.arenaStunHitCount ++;
2763+ player.arenaLastStunTime = currentTime;
2764+ return true ;
2765+ }
2766+
26592767void StartPlrHit (Player &player, int dam, bool forcehit)
26602768{
26612769 if (player._pInvincible && player._pHitPoints == 0 && &player == MyPlayer) {
@@ -2674,10 +2782,16 @@ void StartPlrHit(Player &player, int dam, bool forcehit)
26742782 return ;
26752783 }
26762784
2785+ // Arena stun resistance - check if player can be stunned
2786+ if (!CanPlayerBeStunnedInArena (player)) {
2787+ return ; // Player is immune to stun in arena
2788+ }
2789+
26772790 Direction pd = player._pdir ;
26782791
26792792 int8_t skippedAnimationFrames = 0 ;
2680- if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FastestHitRecovery)) {
2793+ if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FastestHitRecovery)
2794+ || player.isOnArenaLevel ()) {
26812795 skippedAnimationFrames = 3 ;
26822796 } else if (HasAnyOf (player._pIFlags , ItemSpecialEffect::FasterHitRecovery)) {
26832797 skippedAnimationFrames = 2 ;
@@ -2690,7 +2804,21 @@ void StartPlrHit(Player &player, int dam, bool forcehit)
26902804 NewPlrAnim (player, player_graphic::Hit, pd, AnimationDistributionFlags::None, skippedAnimationFrames);
26912805
26922806 player._pmode = PM_GOTHIT;
2693- FixPlayerLocation (player, pd);
2807+
2808+ // Fix southward walking escape bug: use position.old to return to the original tile
2809+ if (player.isWalking ()) {
2810+ player.position .tile = player.position .old ;
2811+ player.position .future = player.position .old ;
2812+ if (&player == MyPlayer) {
2813+ ViewPosition = player.position .tile ;
2814+ }
2815+ ChangeLightXY (player.lightId , player.position .tile );
2816+ ChangeVisionXY (player.getId (), player.position .tile );
2817+ player._pdir = pd;
2818+ } else {
2819+ FixPlayerLocation (player, pd);
2820+ }
2821+
26942822 FixPlrWalkTags (player);
26952823 dPlayer[player.position .tile .x ][player.position .tile .y ] = player.getId () + 1 ;
26962824 SetPlayerOld (player);
0 commit comments