@@ -14,6 +14,7 @@ import (
1414 "github.com/GoMudEngine/GoMud/internal/mudlog"
1515 "github.com/GoMudEngine/GoMud/internal/rooms"
1616 "github.com/GoMudEngine/GoMud/internal/users"
17+ "github.com/GoMudEngine/GoMud/internal/util"
1718)
1819
1920// registerCommands registers all combat-related commands
@@ -22,6 +23,9 @@ func (tc *TwitchCombat) registerCommands() {
2223 tc .plug .AddUserCommand ("attack" , tc .attackCommand , false , false )
2324 tc .plug .AddUserCommand ("kill" , tc .attackCommand , false , false ) // Alias for attack
2425 tc .plug .AddUserCommand ("balance" , tc .balanceCommand , false , false )
26+ tc .plug .AddUserCommand ("cleartarget" , tc .clearTargetCommand , false , false )
27+ tc .plug .AddUserCommand ("target" , tc .targetCommand , false , false )
28+ tc .plug .AddUserCommand ("flee" , tc .fleeCommand , false , false )
2529 tc .plug .AddUserCommand ("combatinfo" , tc .combatInfoCommand , true , true ) // Admin only
2630 tc .plug .AddUserCommand ("config" , tc .configCommand , true , true ) // Admin only
2731
@@ -41,23 +45,55 @@ func (tc *TwitchCombat) attackCommand(rest string, user *users.UserRecord, room
4145 return true , nil
4246 }
4347
44- // Basic attack targeting logic (simplified for demo)
45- if rest == "" {
46- user .SendText ("Attack whom?" )
47- return true , nil
48- }
49-
50- // Find target using the same logic as round-based combat
51- attackPlayerId , attackMobInstanceId := room .FindByName (rest )
48+ var attackPlayerId , attackMobInstanceId int
49+ var targetName string
5250
53- // Can't attack self
54- if attackPlayerId == user .UserId {
55- attackPlayerId = 0
51+ // If no target specified, check if we have a persistent target
52+ if rest == "" {
53+ // First check if we're in active combat with a specific mob
54+ if user .Character .Aggro != nil && user .Character .Aggro .MobInstanceId > 0 {
55+ // Reuse existing mob target
56+ attackMobInstanceId = user .Character .Aggro .MobInstanceId
57+ // Verify the mob is still in the room
58+ targetMob := mobs .GetInstance (attackMobInstanceId )
59+ if targetMob == nil || targetMob .Character .RoomId != user .Character .RoomId {
60+ user .SendText ("Your target is no longer here." )
61+ user .Character .Aggro = nil
62+ // Try to use stored target name
63+ targetName = tc .GetUserTarget (user .UserId )
64+ if targetName != "" {
65+ attackPlayerId , attackMobInstanceId = room .FindByName (targetName )
66+ }
67+ }
68+ } else {
69+ // Not in active combat, check for stored target name
70+ targetName = tc .GetUserTarget (user .UserId )
71+ if targetName != "" {
72+ attackPlayerId , attackMobInstanceId = room .FindByName (targetName )
73+ } else {
74+ user .SendText ("Attack whom?" )
75+ return true , nil
76+ }
77+ }
78+ } else {
79+ // New target specified
80+ targetName = rest
81+ // Find target using the same logic as round-based combat
82+ attackPlayerId , attackMobInstanceId = room .FindByName (rest )
83+
84+ // Can't attack self
85+ if attackPlayerId == user .UserId {
86+ attackPlayerId = 0
87+ }
5688 }
5789
5890 // Check if we found a target
5991 if attackMobInstanceId == 0 && attackPlayerId == 0 {
6092 user .SendText ("They aren't here." )
93+ // Clear stored target if it's no longer valid
94+ if targetName != "" {
95+ tc .ClearUserTarget (user .UserId )
96+ }
6197 return true , nil
6298 }
6399
@@ -73,6 +109,10 @@ func (tc *TwitchCombat) attackCommand(rest string, user *users.UserRecord, room
73109 return true , nil
74110 }
75111
112+ // Store the actual mob name for persistent targeting
113+ // Always use the mob's actual name, not what the user typed
114+ tc .SetUserTarget (user .UserId , targetMob .Character .Name )
115+
76116 // Register the player with the timer if not already registered
77117 tc .timer .RegisterActor (user .UserId , combat .User )
78118
@@ -191,6 +231,9 @@ func (tc *TwitchCombat) mobAttackCommand(rest string, mob *mobs.Mob, room *rooms
191231 cooldown := tc .calculateWeaponCooldown (& mob .Character , true ) // true = mob
192232 tc .timer .SetActorCooldown (mob .InstanceId , combat .Mob , cooldown )
193233
234+ // Send GMCP update to the player showing updated HP
235+ tc .SendCombatUpdate (attackPlayerId )
236+
194237 // Check if player died
195238 if targetUser .Character .Health <= 0 {
196239 mob .Character .EndAggro ()
@@ -640,3 +683,134 @@ func (tc *TwitchCombat) configCommand(rest string, user *users.UserRecord, room
640683 return true , nil
641684 }
642685}
686+
687+ // clearTargetCommand clears the stored combat target
688+ func (tc * TwitchCombat ) clearTargetCommand (rest string , user * users.UserRecord , room * rooms.Room , flags events.EventFlag ) (bool , error ) {
689+ tc .ClearUserTarget (user .UserId )
690+ user .SendText (`<ansi fg="yellow">Combat target cleared.</ansi>` )
691+ return true , nil
692+ }
693+
694+ // targetCommand shows or sets the combat target
695+ func (tc * TwitchCombat ) targetCommand (rest string , user * users.UserRecord , room * rooms.Room , flags events.EventFlag ) (bool , error ) {
696+ if rest == "" {
697+ // Show current target
698+ currentTarget := tc .GetUserTarget (user .UserId )
699+ if currentTarget == "" {
700+ user .SendText (`<ansi fg="yellow">No target set.</ansi>` )
701+ } else {
702+ user .SendText (fmt .Sprintf (`<ansi fg="yellow">Current target: %s</ansi>` , currentTarget ))
703+ }
704+ } else {
705+ // Set new target
706+ // Verify the target exists in the room
707+ _ , mobId := room .FindByName (rest )
708+ if mobId == 0 {
709+ user .SendText ("They aren't here." )
710+ return true , nil
711+ }
712+
713+ // Get the actual mob to use its real name
714+ targetMob := mobs .GetInstance (mobId )
715+ if targetMob == nil {
716+ user .SendText ("They aren't here." )
717+ return true , nil
718+ }
719+
720+ tc .SetUserTarget (user .UserId , targetMob .Character .Name )
721+ user .SendText (fmt .Sprintf (`<ansi fg="yellow">Target set to: %s</ansi>` , targetMob .Character .Name ))
722+ }
723+ return true , nil
724+ }
725+
726+ // fleeCommand handles flee attempts in twitch combat
727+ func (tc * TwitchCombat ) fleeCommand (rest string , user * users.UserRecord , room * rooms.Room , flags events.EventFlag ) (bool , error ) {
728+ // Check if user is in combat
729+ if user .Character .Aggro == nil {
730+ user .SendText (`You aren't in combat!` )
731+ return true , nil
732+ }
733+
734+ // Check if still on cooldown from last action
735+ if ! tc .timer .CanPerformAction (user .UserId , combat .User ) {
736+ nextAction := tc .timer .GetNextActionTime (user .UserId , combat .User )
737+ remaining := time .Until (nextAction )
738+ user .SendText (fmt .Sprintf (`<ansi fg="red">You are unbalanced! Can't flee for %.1f seconds.</ansi>` , remaining .Seconds ()))
739+ return true , nil
740+ }
741+
742+ user .SendText (`You attempt to flee...` )
743+
744+ // Immediate flee check based on speed stats
745+ blockedByMob := ""
746+
747+ // Check each mob in the room that has aggro on the player
748+ for _ , mobId := range room .GetMobs () {
749+ mob := mobs .GetInstance (mobId )
750+ if mob == nil || mob .Character .Aggro == nil || mob .Character .Aggro .UserId != user .UserId {
751+ continue
752+ }
753+
754+ // Speed-based flee chance calculation (same as combat-rounds)
755+ chanceIn100 := int (float64 (user .Character .Stats .Speed .ValueAdj ) /
756+ (float64 (user .Character .Stats .Speed .ValueAdj ) + float64 (mob .Character .Stats .Speed .ValueAdj )) * 70 )
757+ chanceIn100 += 30
758+
759+ roll := util .Rand (100 )
760+ util .LogRoll (`Flee` , roll , chanceIn100 )
761+
762+ if roll >= chanceIn100 {
763+ blockedByMob = mob .Character .Name
764+ break
765+ }
766+ }
767+
768+ // Handle flee result
769+ if blockedByMob != "" {
770+ user .SendText (fmt .Sprintf (`<ansi fg="red">%s blocks your escape!</ansi>` , blockedByMob ))
771+ room .SendText (
772+ fmt .Sprintf (`<ansi fg="username">%s</ansi> <ansi fg="red">tries to flee, but is blocked by %s!</ansi>` ,
773+ user .Character .Name , blockedByMob ),
774+ user .UserId )
775+
776+ // Set a short cooldown for failed flee
777+ tc .timer .SetActorCooldown (user .UserId , combat .User , 2 * time .Second )
778+ tc .SendCombatUpdate (user .UserId )
779+ } else {
780+ // Successful flee
781+ exitName , _ := room .GetRandomExit ()
782+ if exitName == "" {
783+ user .SendText (`<ansi fg="red">There's nowhere to run!</ansi>` )
784+ // Set a short cooldown
785+ tc .timer .SetActorCooldown (user .UserId , combat .User , 2 * time .Second )
786+ tc .SendCombatUpdate (user .UserId )
787+ return true , nil
788+ }
789+
790+ // Execute flee
791+ events .AddToQueue (events.Input {
792+ UserId : user .UserId ,
793+ InputText : exitName ,
794+ })
795+
796+ user .SendText (`<ansi fg="yellow">You flee in panic!</ansi>` )
797+ room .SendText (
798+ fmt .Sprintf (`<ansi fg="username">%s</ansi> <ansi fg="yellow">flees in panic!</ansi>` , user .Character .Name ),
799+ user .UserId )
800+
801+ // Clear combat state
802+ user .Character .EndAggro ()
803+ tc .ClearUserTarget (user .UserId )
804+ tc .timer .ClearActorCooldown (user .UserId , combat .User )
805+
806+ // Notify all mobs that were fighting this player
807+ for _ , mobId := range room .GetMobs () {
808+ mob := mobs .GetInstance (mobId )
809+ if mob != nil && mob .Character .Aggro != nil && mob .Character .Aggro .UserId == user .UserId {
810+ mob .Character .EndAggro ()
811+ }
812+ }
813+ }
814+
815+ return true , nil
816+ }
0 commit comments