Skip to content

Commit e559771

Browse files
committed
Changing GMCP output to support module level messages
When sending the full Char message it overwrites anything else added to the Char as a submodule. - Split out all submodules in Char - Added CombatStatus - Fixed decimals in Balance and Round output - Added enemy info to CombatStatus
1 parent c934104 commit e559771

File tree

7 files changed

+471
-124
lines changed

7 files changed

+471
-124
lines changed

modules/combat-twitch/combat.go

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/GoMudEngine/GoMud/internal/combat"
77
"github.com/GoMudEngine/GoMud/internal/events"
8+
"github.com/GoMudEngine/GoMud/internal/mobs"
89
"github.com/GoMudEngine/GoMud/internal/mudlog"
910
"github.com/GoMudEngine/GoMud/internal/plugins"
1011
"github.com/GoMudEngine/GoMud/internal/users"
@@ -18,14 +19,19 @@ type TwitchCombat struct {
1819
timer *CooldownTimer
1920
active bool
2021
mutex sync.RWMutex
22+
23+
// Track last target name per user for persistent targeting
24+
userTargets map[int]string // userId -> target name
25+
userTargetMutex sync.RWMutex
2126
}
2227

2328
// NewTwitchCombat creates a new twitch-based combat system
2429
func NewTwitchCombat(plug *plugins.Plugin) *TwitchCombat {
2530
tc := &TwitchCombat{
26-
plug: plug,
27-
calculator: NewTwitchCalculator(),
28-
active: false,
31+
plug: plug,
32+
calculator: NewTwitchCalculator(),
33+
active: false,
34+
userTargets: make(map[int]string),
2935
}
3036
tc.timer = NewCooldownTimer(tc)
3137
return tc
@@ -103,10 +109,25 @@ func (tc *TwitchCombat) SendBalanceNotification(actorId int, actorType combat.So
103109
func (tc *TwitchCombat) SendGMCPBalanceUpdate(userId int, remainingSeconds float64, maxSeconds float64) {
104110
// Check if user is actually in combat
105111
inCombat := false
106-
if user := users.GetByUserId(userId); user != nil {
112+
targetHpCurrent := 0
113+
targetHpMax := 0
114+
115+
user := users.GetByUserId(userId)
116+
if user != nil {
107117
inCombat = user.Character.Aggro != nil
118+
119+
// If in combat, get target HP
120+
if inCombat && user.Character.Aggro.MobInstanceId > 0 {
121+
if targetMob := mobs.GetInstance(user.Character.Aggro.MobInstanceId); targetMob != nil {
122+
targetHpCurrent = targetMob.Character.Health
123+
targetHpMax = targetMob.Character.HealthMax.ValueAdj
124+
}
125+
}
108126
}
109127

128+
// Get the user's current target
129+
target := tc.GetUserTarget(userId)
130+
110131
// Send GMCP combat status update
111132
events.AddToQueue(gmcp.GMCPCombatStatusUpdate{
112133
UserId: userId,
@@ -117,5 +138,68 @@ func (tc *TwitchCombat) SendGMCPBalanceUpdate(userId int, remainingSeconds float
117138
InCombat: inCombat,
118139
CombatStyle: combat.GetActiveCombatSystemName(),
119140
RoundNumber: 0, // Not applicable for twitch combat
141+
Target: target,
142+
TargetHpCurrent: targetHpCurrent,
143+
TargetHpMax: targetHpMax,
120144
})
121145
}
146+
147+
// SetUserTarget stores the last target name for a user
148+
func (tc *TwitchCombat) SetUserTarget(userId int, targetName string) {
149+
tc.userTargetMutex.Lock()
150+
if targetName == "" {
151+
delete(tc.userTargets, userId)
152+
} else {
153+
tc.userTargets[userId] = targetName
154+
}
155+
tc.userTargetMutex.Unlock()
156+
157+
// Send GMCP update with new target
158+
// Get current cooldown to include in update
159+
remaining := tc.timer.GetRemainingCooldown(userId, combat.User).Seconds()
160+
maxDuration := float64(0)
161+
if remaining > 0 {
162+
// If on cooldown, get max duration
163+
// For simplicity, we'll just send the update with current remaining time
164+
maxDuration = remaining // This isn't perfect but avoids accessing internal timer state
165+
}
166+
167+
tc.SendGMCPBalanceUpdate(userId, remaining, maxDuration)
168+
}
169+
170+
// GetUserTarget retrieves the last target name for a user
171+
func (tc *TwitchCombat) GetUserTarget(userId int) string {
172+
tc.userTargetMutex.RLock()
173+
defer tc.userTargetMutex.RUnlock()
174+
175+
return tc.userTargets[userId]
176+
}
177+
178+
// ClearUserTarget removes the stored target for a user
179+
func (tc *TwitchCombat) ClearUserTarget(userId int) {
180+
tc.userTargetMutex.Lock()
181+
delete(tc.userTargets, userId)
182+
tc.userTargetMutex.Unlock()
183+
184+
// Send GMCP update with cleared target
185+
remaining := tc.timer.GetRemainingCooldown(userId, combat.User).Seconds()
186+
maxDuration := float64(0)
187+
if remaining > 0 {
188+
maxDuration = remaining
189+
}
190+
191+
tc.SendGMCPBalanceUpdate(userId, remaining, maxDuration)
192+
}
193+
194+
// SendCombatUpdate sends GMCP updates for a player involved in combat
195+
// This should be called after any combat action that might change HP
196+
func (tc *TwitchCombat) SendCombatUpdate(userId int) {
197+
// Get current cooldown for the user
198+
remaining := tc.timer.GetRemainingCooldown(userId, combat.User).Seconds()
199+
maxDuration := float64(0)
200+
if remaining > 0 {
201+
maxDuration = remaining
202+
}
203+
204+
tc.SendGMCPBalanceUpdate(userId, remaining, maxDuration)
205+
}

modules/combat-twitch/commands.go

Lines changed: 185 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)