diff --git a/_datafiles/guides/building/scripting/FUNCTIONS_ACTORS.md b/_datafiles/guides/building/scripting/FUNCTIONS_ACTORS.md index 4ab09cc1..a07ba2b1 100644 --- a/_datafiles/guides/building/scripting/FUNCTIONS_ACTORS.md +++ b/_datafiles/guides/building/scripting/FUNCTIONS_ACTORS.md @@ -54,6 +54,7 @@ ActorObjects are the basic object that represents Users and NPCs - [ActorObject.GetMobKills(mobId int) int](#actorobjectgetmobkillsmobid-int-int) - [ActorObject.GetRaceKills(raceName string) int](#actorobjectgetracekillsracename-string-int) - [ActorObject.GetHealth() int](#actorobjectgethealth-int) + - [ActorObject.SetHealth(amt int)](#actorobjectsethealthamt-int) - [ActorObject.GetHealthMax() int](#actorobjectgethealthmax-int) - [ActorObject.GetHealthPct() float](#actorobjectgethealthpct-float) - [ActorObject.GetMana() int](#actorobjectgetmana-int) @@ -87,6 +88,7 @@ ActorObjects are the basic object that represents Users and NPCs - [ActorObject.TimerSet(name string, period string)](#actorobjecttimersetname-string-period-string) - [ActorObject.TimerExpired(name string) bool](#actorobjecttimerexpiredname-string-bool) - [ActorObject.TimerExists(name string) bool](#actorobjecttimerexistsname-string-bool) + - [ActorObject.AddEventLog(category string, message string)](#actorobjectaddeventlogcategory-string-message-string) @@ -439,6 +441,14 @@ Returns the number of times the actor has killed a certain race of mob ## [ActorObject.GetHealth() int](/internal/scripting/actor_func.go) Returns current actor health +## [ActorObject.SetHealth(amt int)](/internal/scripting/actor_func.go) +Sets actor health to a specific amount. If this exceeds their maximum health, sets to their maximum health. + +| Argument | Explanation | +| --- | --- | +| amt | number of hitpoints to set them to | + + ## [ActorObject.GetHealthMax() int](/internal/scripting/actor_func.go) Returns current actor max health @@ -586,3 +596,10 @@ Returns true if the specified timer has expired or doesn't exist. Returns true if the specified timer exists. Set timers always exist until they are checked for expiration with `TimerExpired(name string)` +## [ActorObject.AddEventLog(category string, message string)](/internal/scripting/actor_func.go) +Adds a line to the users Event Log (`history`) + +| Argument | Explanation | +| --- | --- | +| category | A short single word category | +| message | A single line describing the event | \ No newline at end of file diff --git a/_datafiles/guides/building/scripting/SCRIPTING_MOBS.md b/_datafiles/guides/building/scripting/SCRIPTING_MOBS.md index 68de1242..0b3006f0 100644 --- a/_datafiles/guides/building/scripting/SCRIPTING_MOBS.md +++ b/_datafiles/guides/building/scripting/SCRIPTING_MOBS.md @@ -203,3 +203,13 @@ NOTE: You can safely start a new path with `mob.Command('pathto 123')` before re | mob | [ActorObject](FUNCTIONS_ACTORS.md) | | room | [RoomObject](FUNCTIONS_ROOMS.md) | | eventDetails.status | `start`, `waypoint`, or `end` | + +`onPlayerDowned()` is called when a player is downed (not killed), and this mob aggro'd on them. +NOTE: If `true` is returned, will ensure this script only runs once for this type of MobId - for example, only one of the two guards in the room. + +| Argument | Explanation | +| --- | --- | +| mob | [ActorObject](FUNCTIONS_ACTORS.md) | +| user | [ActorObject](FUNCTIONS_ACTORS.md) | +| room | [RoomObject](FUNCTIONS_ROOMS.md) | + diff --git a/_datafiles/world/default/mobs/frostfang/10-ivar_froststeel.yaml b/_datafiles/world/default/mobs/frostfang/10-ivar_froststeel.yaml index d22f3d7f..9f9d0805 100644 --- a/_datafiles/world/default/mobs/frostfang/10-ivar_froststeel.yaml +++ b/_datafiles/world/default/mobs/frostfang/10-ivar_froststeel.yaml @@ -4,6 +4,8 @@ itemdropchance: 2 hostile: false groups: - frostfang-npc +combatcommands: + - 'callforhelp 7:guard:calls for the guards.' idlecommands: - 'say type list to see my wares' - 'say If you''re looking to sell something, I may be interested... as long as it''s not too special or unique' diff --git a/_datafiles/world/default/mobs/frostfang/11-brynja_snowdeal.yaml b/_datafiles/world/default/mobs/frostfang/11-brynja_snowdeal.yaml index b09d1cbc..62cbeb2a 100644 --- a/_datafiles/world/default/mobs/frostfang/11-brynja_snowdeal.yaml +++ b/_datafiles/world/default/mobs/frostfang/11-brynja_snowdeal.yaml @@ -4,6 +4,8 @@ itemdropchance: 2 hostile: false groups: - frostfang-npc +combatcommands: + - 'callforhelp 7:guard:calls for the guards.' idlecommands: - 'say type list to see my wares' - 'say If you''re looking to sell something, I may be interested... as long as it''s not too special or unique' diff --git a/_datafiles/world/default/mobs/frostfang/2-guard.yaml b/_datafiles/world/default/mobs/frostfang/2-guard.yaml index 7c859b81..b3b97be3 100644 --- a/_datafiles/world/default/mobs/frostfang/2-guard.yaml +++ b/_datafiles/world/default/mobs/frostfang/2-guard.yaml @@ -4,7 +4,9 @@ itemdropchance: 2 hostile: false maxwander: 20 groups: - - frostfang-npc + - frostfang-law +combatcommands: + - 'callforhelp 5:puts their fingers to their mouth and whistle loudly.' activitylevel: 20 character: name: guard diff --git a/_datafiles/world/default/mobs/frostfang/26-frostfang_citizen.yaml b/_datafiles/world/default/mobs/frostfang/26-frostfang_citizen.yaml index daa37844..c1d42a30 100644 --- a/_datafiles/world/default/mobs/frostfang/26-frostfang_citizen.yaml +++ b/_datafiles/world/default/mobs/frostfang/26-frostfang_citizen.yaml @@ -4,6 +4,8 @@ itemdropchance: 2 hostile: false groups: - frostfang-npc +combatcommands: + - 'callforhelp 7:guard:calls for the guards.' activitylevel: 30 maxwander: 5 idlecommands: diff --git a/_datafiles/world/default/mobs/frostfang/3-captain_of_the_guard.yaml b/_datafiles/world/default/mobs/frostfang/3-captain_of_the_guard.yaml index dfc62d7a..505c2b12 100644 --- a/_datafiles/world/default/mobs/frostfang/3-captain_of_the_guard.yaml +++ b/_datafiles/world/default/mobs/frostfang/3-captain_of_the_guard.yaml @@ -4,7 +4,9 @@ itemdropchance: 2 hostile: false maxwander: 20 groups: - - frostfang-npc + - frostfang-law +combatcommands: + - 'callforhelp 5:puts their fingers to their mouth and whistle loudly.' idlecommands: - 'emote mumbles something about the weather' - 'wander' diff --git a/_datafiles/world/default/mobs/frostfang/39-elara.yaml b/_datafiles/world/default/mobs/frostfang/39-elara.yaml index b5d03329..dcf40b50 100644 --- a/_datafiles/world/default/mobs/frostfang/39-elara.yaml +++ b/_datafiles/world/default/mobs/frostfang/39-elara.yaml @@ -5,6 +5,8 @@ hostile: false maxwander: 0 groups: - frostfang-npc +combatcommands: + - 'callforhelp 7:guard:calls for the guards.' activitylevel: 20 character: name: elara diff --git a/_datafiles/world/default/mobs/frostfang/40-rodric.yaml b/_datafiles/world/default/mobs/frostfang/40-rodric.yaml index 51df8bf6..1a0b2c1b 100644 --- a/_datafiles/world/default/mobs/frostfang/40-rodric.yaml +++ b/_datafiles/world/default/mobs/frostfang/40-rodric.yaml @@ -7,6 +7,8 @@ idlecommands: - 'wander' groups: - frostfang-npc +combatcommands: + - 'callforhelp 7:guard:calls for the guards.' activitylevel: 20 character: name: Rodric diff --git a/_datafiles/world/default/mobs/frostfang/42-master_at_arms.yaml b/_datafiles/world/default/mobs/frostfang/42-master_at_arms.yaml index cef6d638..fe81499c 100644 --- a/_datafiles/world/default/mobs/frostfang/42-master_at_arms.yaml +++ b/_datafiles/world/default/mobs/frostfang/42-master_at_arms.yaml @@ -5,6 +5,8 @@ hostile: false maxwander: 0 groups: - frostfang-npc +combatcommands: + - 'callforhelp 7:guard:calls for the guards.' idlecommands: - 'emote checks the edge of their blade.' - 'emote inspects the training equipment' diff --git a/_datafiles/world/default/mobs/frostfang/5-armorer.yaml b/_datafiles/world/default/mobs/frostfang/5-armorer.yaml index d27f6f10..58b32246 100644 --- a/_datafiles/world/default/mobs/frostfang/5-armorer.yaml +++ b/_datafiles/world/default/mobs/frostfang/5-armorer.yaml @@ -4,6 +4,8 @@ itemdropchance: 2 hostile: false groups: - frostfang-npc +combatcommands: + - 'callforhelp 7:guard:calls for the guards.' idlecommands: - 'say type list to see my wares' - 'say If you''re looking to sell something, I may be interested... as long as it''s not too special or unique' diff --git a/_datafiles/world/default/mobs/frostfang/50-moilyn_the_wizard.yaml b/_datafiles/world/default/mobs/frostfang/50-moilyn_the_wizard.yaml index 7dbb60f5..b220b33b 100644 --- a/_datafiles/world/default/mobs/frostfang/50-moilyn_the_wizard.yaml +++ b/_datafiles/world/default/mobs/frostfang/50-moilyn_the_wizard.yaml @@ -4,6 +4,8 @@ itemdropchance: 2 hostile: false groups: - frostfang-npc +combatcommands: + - 'callforhelp 7:guard:calls for the guards.' idlecommands: - 'say type list to see what magical objects I have for sale' activitylevel: 10 diff --git a/_datafiles/world/default/mobs/frostfang/6-trainer.yaml b/_datafiles/world/default/mobs/frostfang/6-trainer.yaml index fd515cdf..11b2c9b0 100644 --- a/_datafiles/world/default/mobs/frostfang/6-trainer.yaml +++ b/_datafiles/world/default/mobs/frostfang/6-trainer.yaml @@ -4,6 +4,8 @@ itemdropchance: 2 hostile: false groups: - frostfang-npc +combatcommands: + - 'callforhelp 7:guard:calls for the guards.' idlecommands: - 'say I can help you train new skills!' - 'say There are other trainers to be found in the world, with other skills to teach you.' diff --git a/_datafiles/world/default/mobs/frostfang/7-wench.yaml b/_datafiles/world/default/mobs/frostfang/7-wench.yaml index f692738d..fc960d1b 100644 --- a/_datafiles/world/default/mobs/frostfang/7-wench.yaml +++ b/_datafiles/world/default/mobs/frostfang/7-wench.yaml @@ -4,6 +4,8 @@ itemdropchance: 2 hostile: false groups: - frostfang-npc +combatcommands: + - 'callforhelp 7:guard:calls for the guards.' idlecommands: - 'say You can sleep here for a bit here to refresh.' - 'say If you''re hungry check the list of food for sale.' diff --git a/_datafiles/world/default/mobs/frostfang/8-king.yaml b/_datafiles/world/default/mobs/frostfang/8-king.yaml index bc674510..7deb1558 100644 --- a/_datafiles/world/default/mobs/frostfang/8-king.yaml +++ b/_datafiles/world/default/mobs/frostfang/8-king.yaml @@ -4,6 +4,8 @@ itemdropchance: 2 hostile: false groups: - frostfang-npc +combatcommands: + - 'callforhelp 7:guard:calls for the guards.' character: name: king description: 'Seated upon his regal throne, the King of Frostfang carries the weight of his kingdom with a quiet dignity. His attire is a rich tapestry of royal blues and silvers, adorned with intricate embroidery that tells the story of his lineage and the battles fought to protect his realm. A heavy, ermine-trimmed cloak drapes over his shoulders, signifying his status and the responsibilities that come with it. His crown, a delicate circlet of silver and sapphires, rests atop his brow, glinting softly in the dim light of the throne room.' diff --git a/_datafiles/world/default/mobs/frostfang/9-kings_guard.yaml b/_datafiles/world/default/mobs/frostfang/9-kings_guard.yaml index ffb47f63..74dece93 100644 --- a/_datafiles/world/default/mobs/frostfang/9-kings_guard.yaml +++ b/_datafiles/world/default/mobs/frostfang/9-kings_guard.yaml @@ -4,7 +4,9 @@ itemdropchance: 2 hostile: false maxwander: 2 groups: - - frostfang-npc + - frostfang-law +combatcommands: + - 'callforhelp 5:puts their fingers to their mouth and whistle loudly.' character: name: king's guard description: 'The King''s Guard stands resolute, a formidable perception in the royal throne room. Clad in meticulously maintained armor that gleams under the soft lighting, they exude an air of unwavering loyalty and strength. The guard''s helmet obscures their face, adding an element of mystery and intimidation, while their posture remains erect and vigilant, ready to spring into action at a moment''s notice. A well-polished broadsword hangs at their side, its perception a silent promise to defend the king and the kingdom with their life.' diff --git a/_datafiles/world/default/mobs/frostfang/scripts/2-guard.js b/_datafiles/world/default/mobs/frostfang/scripts/2-guard.js index 69c51c0d..24f7472f 100644 --- a/_datafiles/world/default/mobs/frostfang/scripts/2-guard.js +++ b/_datafiles/world/default/mobs/frostfang/scripts/2-guard.js @@ -64,4 +64,18 @@ function onPath(mob, room, eventDetails) { } } +} + + +function onPlayerDowned(mob, user, room) { + + user.SendText(mob.GetCharacterName(true) + " approaches you from behind, striking you with the pommel of their sword."); + user.SendText("The last thing you remember hearing is the jingling of chains as you black out."); + + room.SendText(mob.GetCharacterName(true) + " approaches " + user.GetCharacterName(true) + " from behind, striking them with the pommel of their sword.", user.UserId()); + room.SendText(mob.GetCharacterName(true) + " places " + user.GetCharacterName(true) + " in chains, and sends them away with a deputy to put them in jail.", user.UserId()); + + user.MoveRoom(1003); + + return true; } \ No newline at end of file diff --git a/_datafiles/world/default/rooms/frostfang/1003.js b/_datafiles/world/default/rooms/frostfang/1003.js index 9e4dc06c..e692d04f 100644 --- a/_datafiles/world/default/rooms/frostfang/1003.js +++ b/_datafiles/world/default/rooms/frostfang/1003.js @@ -3,6 +3,8 @@ const JAIL_TIME = "1 hour"; function onEnter(user, room) { + user.SetHealth(1); + if ( !room.IsEphemeral() ) { var newRoomIds = CreateInstancesFromRoomIds( [room.RoomId()] ); @@ -14,17 +16,23 @@ function onEnter(user, room) { } - user.TimerSet("jail", JAIL_TIME); - - room.SendText(""); - room.SendText("********************************************************************************"); - room.SendText("You hear a loud !!!CLANK!!!, and can immediately tell..."); - room.SendText("The cell door is LOCKED from the other side!"); - room.SendText('You hear someone shout, "Maybe an hour in a cell will cool you off!"'); - room.SendText("********************************************************************************"); - room.SendText(""); - - user.Command("look", 1); + if ( !user.TimerExists("jail") ) { + + user.AddEventLog(`jail`, `Thrown in jail`); + + user.TimerSet("jail", JAIL_TIME); + + room.SendText(""); + room.SendText("********************************************************************************"); + room.SendText("You hear a loud !!!CLANK!!!, and can immediately tell..."); + room.SendText("The cell door is LOCKED from the other side!"); + room.SendText('You hear someone shout, "Maybe an hour in a cell will cool you off!"'); + room.SendText("********************************************************************************"); + room.SendText(""); + + user.Command("look", 1); + + } return false; } @@ -35,8 +43,9 @@ function onIdle(room) { var playersInRoom = room.GetPlayers(); for( var i in playersInRoom ) { if ( playersInRoom[i].TimerExpired("jail") ) { - room.SendText("You hear a loud CLANK, and the cell door is UNLOCKED from the other side."); + room.SendText("You hear a loud !!!KA-LUNK!!!, and the cell door is UNLOCKED from the other side."); room.SetLocked("cell door", false); + playersInRoom[i].AddEventLog(`jail`, `Released from jail`); } } } diff --git a/internal/characters/character.go b/internal/characters/character.go index 14fc7ab1..3306509a 100644 --- a/internal/characters/character.go +++ b/internal/characters/character.go @@ -1242,7 +1242,12 @@ func (c *Character) ApplyHealthChange(healthChange int) int { newHealth := c.Health + healthChange if newHealth < 0 { c.CancelBuffsWithFlag(buffs.CancelIfCombat) - if newHealth < -10 { + + // If they haven't dropped yet, require a drop before going straight to death. + // Don't allow players to drop under -5 in a single hit. + if newHealth < -5 && oldHealth > 0 { + newHealth = -5 + } else if newHealth <= -10 { newHealth = -10 } } else if newHealth > c.HealthMax.Value { diff --git a/internal/events/eventtypes.go b/internal/events/eventtypes.go index 999995af..c8ec8427 100644 --- a/internal/events/eventtypes.go +++ b/internal/events/eventtypes.go @@ -244,6 +244,13 @@ type LevelUp struct { func (l LevelUp) Type() string { return `LevelUp` } +type PlayerDrop struct { + UserId int + RoomId int +} + +func (l PlayerDrop) Type() string { return `PlayerDrop` } + type PlayerDeath struct { UserId int RoomId int diff --git a/internal/hooks/Message_SendMessages.go b/internal/hooks/Message_SendMessages.go index 69762710..40113abf 100644 --- a/internal/hooks/Message_SendMessages.go +++ b/internal/hooks/Message_SendMessages.go @@ -21,8 +21,6 @@ func Message_SendMessage(e events.Event) events.ListenerReturn { return events.Continue } - //mudlog.Debug("Message{}", "userId", message.UserId, "roomId", message.RoomId, "length", len(message.Text), "IsCommunication", message.IsCommunication) - if message.UserId > 0 { if user := users.GetByUserId(message.UserId); user != nil { diff --git a/internal/hooks/NewRound_DoCombat.go b/internal/hooks/NewRound_DoCombat.go index b6f5fdaa..33a40a62 100644 --- a/internal/hooks/NewRound_DoCombat.go +++ b/internal/hooks/NewRound_DoCombat.go @@ -1075,18 +1075,15 @@ func handleAffected(affectedPlayerIds []int, affectedMobInstanceIds []int) { if user := users.GetByUserId(userId); user != nil { if user.Character.Health <= -10 { + user.Command(`suicide`) // suicide drops all money/items and transports to land of the dead. + } else if user.Character.Health < 1 { - user.SendText(`you drop to the ground!`) - - if room := rooms.LoadRoom(user.Character.RoomId); room != nil { - room.SendText( - fmt.Sprintf(`%s drops to the ground!`, user.Character.Name), - user.UserId) - } + events.AddToQueue(events.PlayerDrop{UserId: user.UserId, RoomId: user.Character.RoomId}) } + } } diff --git a/internal/hooks/PlayerDrop_HandlePlayerDrop.go b/internal/hooks/PlayerDrop_HandlePlayerDrop.go new file mode 100644 index 00000000..991019a0 --- /dev/null +++ b/internal/hooks/PlayerDrop_HandlePlayerDrop.go @@ -0,0 +1,66 @@ +package hooks + +import ( + "fmt" + + "github.com/GoMudEngine/GoMud/internal/events" + "github.com/GoMudEngine/GoMud/internal/mobs" + "github.com/GoMudEngine/GoMud/internal/mudlog" + "github.com/GoMudEngine/GoMud/internal/rooms" + "github.com/GoMudEngine/GoMud/internal/scripting" + "github.com/GoMudEngine/GoMud/internal/users" +) + +// +// Some clean up +// + +func HandlePlayerDrop(e events.Event) events.ListenerReturn { + + evt, typeOk := e.(events.PlayerDrop) + if !typeOk { + mudlog.Error("Event", "Expected Type", "PlayerDrop", "Actual Type", e.Type()) + return events.Cancel + } + + user := users.GetByUserId(evt.UserId) + if user == nil { + mudlog.Error("HandlePlayerDrop", "error", fmt.Sprintf(`user %d not found`, evt.UserId)) + return events.Cancel + } + + user.SendText(`you drop to the ground!`) + + room := rooms.LoadRoom(evt.RoomId) + if room == nil { + return events.Continue + } + + room.SendText( + fmt.Sprintf(`%s drops to the ground!`, user.Character.Name), + user.UserId) + + // Loop through all mobs in the room. If any hate the player, try onPlayerDowned() + skipMobIds := map[int]struct{}{} + for _, mobInstanceId := range room.GetMobs() { + mob := mobs.GetInstance(mobInstanceId) + if mob == nil { + continue + } + + if _, ok := skipMobIds[int(mob.MobId)]; ok { + continue + } + + if !mob.HasAttackedPlayer(user.UserId) { + continue + } + + if isUnique, _ := scripting.TryPlayerDownedEvent(mobInstanceId, user.UserId); isUnique { + skipMobIds[int(mob.MobId)] = struct{}{} + } + + } + + return events.Continue +} diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go index 3061c397..a591b5cc 100644 --- a/internal/hooks/hooks.go +++ b/internal/hooks/hooks.go @@ -70,6 +70,7 @@ func RegisterListeners() { // User Settings change events.RegisterListener(events.UserSettingChanged{}, ClearSettingCaches) + events.RegisterListener(events.PlayerDrop{}, HandlePlayerDrop) events.RegisterListener(events.WebClientCommand{}, WebClientCommand_SendWebClientCommand) events.RegisterListener(events.CharacterCreated{}, BroadcastNewChar) diff --git a/internal/mapper/mapper.go b/internal/mapper/mapper.go index 45ed645c..067b6b83 100644 --- a/internal/mapper/mapper.go +++ b/internal/mapper/mapper.go @@ -376,6 +376,54 @@ func (r *mapper) GetCoordinates(roomId int) (x, y, z int, err error) { return node.Pos.x, node.Pos.y, node.Pos.z, nil } +// Returns all rooms of the map within a certain manhattan distance +func (r *mapper) FindRoomsInDistance(centerRoomId int, xyRadius int, zRadiusOpt ...int) []int { + foundRooms := []int{} + + startNode := r.crawledRooms[centerRoomId] + if startNode == nil { + return foundRooms + } + + xyRadius = int(math.Abs(float64(xyRadius))) + zRadius := 0 + if len(zRadiusOpt) == 0 { + zRadius = int(math.Abs(float64(zRadiusOpt[0]))) + } + + minX, maxX := startNode.Pos.x-xyRadius, startNode.Pos.x+xyRadius + minY, maxY := startNode.Pos.y-xyRadius, startNode.Pos.y+xyRadius + minZ, maxZ := startNode.Pos.z-zRadius, startNode.Pos.z+zRadius + + maxXYDist := float64(xyRadius) + maxZDist := float64(zRadius) + + for z := minZ; z <= maxZ; z++ { + for y := minY; y <= maxY; y++ { + for x := minX; x <= maxX; x++ { + + if math.Abs(float64((startNode.Pos.x-x)+(startNode.Pos.y-y))) > maxXYDist { + continue + } + + if math.Abs(float64(startNode.Pos.z-z)) > maxZDist { + continue + } + + // fmt.Println(x, y, z, `=`, ((startNode.Pos.x - x) + (startNode.Pos.y - y))) + if roomId, _ := r.GetRoomId(x, y, z); roomId != 0 { + if roomId == centerRoomId { + continue + } + foundRooms = append(foundRooms, roomId) + } + } + } + } + + return foundRooms +} + // Finds the first room in a given direction // Allowed directions: func (r *mapper) FindAdjacentRoom(centerRoomId int, direction string, limitDistance ...int) (roomId int, distance int) { diff --git a/internal/mobcommands/attack.go b/internal/mobcommands/attack.go index 771228cd..4004a532 100644 --- a/internal/mobcommands/attack.go +++ b/internal/mobcommands/attack.go @@ -42,6 +42,50 @@ func Attack(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) { } } } + } else if rest[0] == '*' { // choose a target at random. Friend or foe. + + if rest == `*` { // * ANYONE + + allMobs := []int{} + allPlayers := room.GetPlayers() + for _, mobInstanceId := range room.GetMobs() { + if mobInstanceId == mob.InstanceId { + continue + } + allMobs = append(allMobs, mobInstanceId) + } + + randomSelection := util.Rand(len(allMobs) + len(allPlayers)) + + if randomSelection < len(allMobs) { + attackMobInstanceId = allMobs[randomSelection] + } else { + randomSelection -= len(allMobs) + attackPlayerId = allPlayers[randomSelection] + } + + } else if rest == `*mob` { // *mob ANY MOB + + allMobs := []int{} + for _, mobInstanceId := range room.GetMobs() { + if mobInstanceId == mob.InstanceId { + continue + } + allMobs = append(allMobs, mobInstanceId) + } + + if len(allMobs) > 0 { + attackMobInstanceId = allMobs[util.Rand(len(allMobs))] + } + + } else { // *user etc. ANY PLAYER + + if allPlayers := room.GetPlayers(); len(allPlayers) > 0 { + attackPlayerId = allPlayers[util.Rand(len(allPlayers))] + } + + } + } else { attackPlayerId, attackMobInstanceId = room.FindByName(rest) } @@ -66,6 +110,9 @@ func Attack(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) { if u != nil { + // Track that they've attacked this player + mob.PlayerAttacked(attackPlayerId) + mob.Character.SetAggro(attackPlayerId, 0, characters.DefaultAttack) if !isSneaking { diff --git a/internal/mobcommands/callforhelp.go b/internal/mobcommands/callforhelp.go index 0929a0ff..c9ba2067 100644 --- a/internal/mobcommands/callforhelp.go +++ b/internal/mobcommands/callforhelp.go @@ -2,7 +2,10 @@ package mobcommands import ( "fmt" + "strconv" + "strings" + "github.com/GoMudEngine/GoMud/internal/mapper" "github.com/GoMudEngine/GoMud/internal/mobs" "github.com/GoMudEngine/GoMud/internal/rooms" ) @@ -11,34 +14,63 @@ import ( // Format should be: // callforhelp blows his horn // "blows his horn" will be emoted to the room +// Other valid formats: +// callforhelp 5:blows a horn, calling for help +// callforhelp 5:guard:blows a horn, calling for help +// callforhelp guard:blows a horn, calling for help func CallForHelp(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) { - if mob.Character.Aggro == nil || mob.Character.Aggro.UserId == 0 { - return false, fmt.Errorf(`mob %d has no aggro`, mob.InstanceId) + calledForHelp := false + + maxRange := 1 + // Can prefix callforhelp string with a number and : to force range. + // "callforhelp 5:blows a horn, calling for help" + if i := strings.Index(rest, ":"); i >= 0 { + newRangeStr := strings.TrimSpace(rest[:i]) + if newRange, err := strconv.Atoi(newRangeStr); err == nil { + maxRange = newRange + if i < len(rest)-1 { + rest = strings.TrimSpace(rest[i+1:]) + } else { + rest = `` + } + } } - calledForHelp := false + mobNameSearch := `` + // "callforhelp 5:guard:blows a horn, calling for help" + // "callforhelp guard:blows a horn, calling for help" + if i := strings.Index(rest, ":"); i >= 0 { + mobNameSearch = strings.ToLower(strings.TrimSpace(rest[:i])) - for _, roomInfo := range room.Exits { - adjRoom := rooms.LoadRoom(roomInfo.RoomId) - if adjRoom.MobCt() < 1 { - continue + if i < len(rest)-1 { + rest = strings.TrimSpace(rest[i+1:]) + } else { + rest = `` } + } - exitIntoRoom := adjRoom.FindExitTo(room.RoomId) - if exitIntoRoom == `` { + m := mapper.GetMapper(room.RoomId) + roomList := m.FindRoomsInDistance(room.RoomId, maxRange, 0) + + for _, roomId := range roomList { + testRoom := rooms.LoadRoom(roomId) + if testRoom == nil { continue } - for _, nearbyMobInstanceId := range adjRoom.GetMobs(rooms.FindNeutral, rooms.FindHostile) { - if mobInfo := mobs.GetInstance(nearbyMobInstanceId); mobInfo != nil { + for _, nearbyMobInstanceId := range testRoom.GetMobs(rooms.FindNeutral, rooms.FindHostile) { - //if mobInfo.MaxWander == 0 { // Mobs that do not wander at all won't heed the call - // continue - //} + if mobInfo := mobs.GetInstance(nearbyMobInstanceId); mobInfo != nil { - if !mobInfo.IsAlly(mob) { // Only help allies - continue + if mobNameSearch == `` { + if !mobInfo.ConsidersAnAlly(mob) { // Only help allies + continue + } + } else { + if mobNameSearch != strings.ToLower(mobInfo.Character.Name) { + continue + } } if !calledForHelp { @@ -51,10 +83,20 @@ func CallForHelp(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) { } } - mobInfo.Command(fmt.Sprintf(`go %s`, exitIntoRoom)) - mobInfo.Command(fmt.Sprintf(`attack @%d`, mob.Character.Aggro.UserId)) + _, randomRoomId := room.GetRandomExit() + + if randomRoomId > 0 { + mobInfo.Command(fmt.Sprintf(`go %d`, randomRoomId), 1.0) + } + + mobInfo.Command(fmt.Sprintf(`go %d`, room.RoomId), 1.0) + + if mob.Character.Aggro != nil && mob.Character.Aggro.UserId > 0 { + mobInfo.Command(fmt.Sprintf(`attack @%d`, mob.Character.Aggro.UserId), 0.25) + } } } + } return true, nil diff --git a/internal/mobcommands/go.go b/internal/mobcommands/go.go index 65ad9bb5..231b6ec0 100644 --- a/internal/mobcommands/go.go +++ b/internal/mobcommands/go.go @@ -2,6 +2,7 @@ package mobcommands import ( "fmt" + "strconv" "github.com/GoMudEngine/GoMud/internal/buffs" "github.com/GoMudEngine/GoMud/internal/configs" @@ -17,6 +18,50 @@ func Go(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) { return true, nil } + // Special behavior allowed for mobs to travel to specific rooms, even if disconnected. + if forceRoomId, err := strconv.Atoi(rest); err == nil { + + foundRoomExit := false + for exitName, exitInfo := range room.Exits { + if exitInfo.RoomId == forceRoomId { + rest = exitName + foundRoomExit = true + } + } + + if !foundRoomExit { + c := configs.GetTextFormatsConfig() + + if forceRoomId == room.RoomId { + return true, nil + } + + destRoom := rooms.LoadRoom(forceRoomId) + if destRoom == nil { + return true, nil + } + + room.RemoveMob(mob.InstanceId) + destRoom.AddMob(mob.InstanceId) + + // Tell the old room they are leaving + room.SendText( + fmt.Sprintf(string(c.ExitRoomMessageWrapper), + fmt.Sprintf(`%s runs off suddenly.`, mob.Character.Name), + )) + + // Tell the new room they have arrived + + destRoom.SendText( + fmt.Sprintf(string(c.EnterRoomMessageWrapper), + fmt.Sprintf(`%s enters from nearby.`, mob.Character.Name), + )) + + return true, nil + + } + } + exitName := `` goRoomId := 0 diff --git a/internal/mobcommands/shout.go b/internal/mobcommands/shout.go index 99fd49b9..2b1b2bad 100644 --- a/internal/mobcommands/shout.go +++ b/internal/mobcommands/shout.go @@ -2,7 +2,6 @@ package mobcommands import ( "fmt" - "strings" "github.com/GoMudEngine/GoMud/internal/buffs" "github.com/GoMudEngine/GoMud/internal/mobs" @@ -13,8 +12,6 @@ func Shout(rest string, mob *mobs.Mob, room *rooms.Room) (bool, error) { isSneaking := mob.Character.HasBuffFlag(buffs.Hidden) - rest = strings.ToUpper(rest) - if isSneaking { room.SendText(fmt.Sprintf(`someone shouts, "%s"`, rest)) } else { diff --git a/internal/mobs/mobs.go b/internal/mobs/mobs.go index 58cb04b1..24625fb6 100644 --- a/internal/mobs/mobs.go +++ b/internal/mobs/mobs.go @@ -75,9 +75,10 @@ type Mob struct { QuestFlags []string `yaml:"questflags,omitempty,flow"` // What quest flags are set on this mob? BuffIds []int `yaml:"buffids,omitempty"` // Buff Id's this mob always has upon spawn tempDataStore map[string]any - conversationId int // Identifier of conversation currently involved in. - Path PathQueue `yaml:"-"` // a pre-calculated path the mob is following. - lastCommandTurn uint64 // The last turn a command was scheduled for + conversationId int // Identifier of conversation currently involved in. + Path PathQueue `yaml:"-"` // a pre-calculated path the mob is following. + lastCommandTurn uint64 // The last turn a command was scheduled for + playersAttacked map[int]struct{} // all players this mob has attacked at some point } func MobInstanceExists(instanceId int) bool { @@ -102,6 +103,7 @@ func GetAllMobNames() []string { func TrackRecentDeath(instanceId int) { recentlyDied[instanceId] = int(util.GetRoundCount()) } + func RecentlyDied(instanceId int) bool { if len(recentlyDied) > 30 { @@ -258,6 +260,21 @@ func (m *Mob) AddBuff(buffId int, source string) { } +func (m *Mob) PlayerAttacked(userId int) { + if m.playersAttacked == nil { + m.playersAttacked = map[int]struct{}{} + } + m.playersAttacked[userId] = struct{}{} +} + +func (m *Mob) HasAttackedPlayer(userId int) bool { + if m.playersAttacked == nil { + return false + } + _, ok := m.playersAttacked[userId] + return ok +} + func (m *Mob) InConversation() bool { return m.conversationId > 0 } @@ -570,7 +587,7 @@ func (m *Mob) GetIdleCommand() string { return `` } -func (r *Mob) IsAlly(m *Mob) bool { +func (r *Mob) ConsidersAnAlly(m *Mob) bool { if m.MobId == r.MobId { return true // Auto ally with own kind @@ -581,10 +598,12 @@ func (r *Mob) IsAlly(m *Mob) bool { } // If they both belong to factions/groups, check for matches - if len(m.Groups) > 0 && len(r.Groups) > 0 { + // Could conver tthis to a look up map. + // Only a couple entries likely, so maybe not worth it. + if len(r.Groups) > 0 { // Look for a group match - for _, testGroup := range m.Groups { - for _, targetGroup := range r.Groups { + for _, targetGroup := range r.Groups { + for _, testGroup := range m.Groups { if testGroup == targetGroup { return true } @@ -608,6 +627,7 @@ func (r *Mob) Validate() error { } r.Character.Validate() + return nil } diff --git a/internal/scripting/actor_func.go b/internal/scripting/actor_func.go index fc385418..f1260099 100644 --- a/internal/scripting/actor_func.go +++ b/internal/scripting/actor_func.go @@ -402,6 +402,12 @@ func (a ScriptActor) UpdateItem(itm ScriptItem) { a.userRecord.Character.UpdateItem(itm.originalItem, *itm.itemRecord) } +func (a ScriptActor) AddEventLog(category string, message string) { + if a.userRecord != nil { + a.userRecord.EventLog.Add(category, message) + } +} + func (a ScriptActor) GiveItem(itm any) { var sItem *ScriptItem @@ -573,6 +579,13 @@ func (a ScriptActor) GetRaceKills(race string) int { return raceKills[race] } +func (a ScriptActor) SetHealth(amt int) { + a.characterRecord.Health = amt + if a.characterRecord.Health > a.characterRecord.HealthMax.Value { + a.characterRecord.Health = a.characterRecord.HealthMax.Value + } +} + func (a ScriptActor) GetHealth() int { return a.characterRecord.Health } diff --git a/internal/scripting/mob.go b/internal/scripting/mob.go index 4015f389..d6a3882f 100644 --- a/internal/scripting/mob.go +++ b/internal/scripting/mob.go @@ -22,6 +22,68 @@ func PruneMobVMs(instanceIds ...int) { } +func TryPlayerDownedEvent(mobInstanceId int, downedPlayerId int) (bool, error) { + sMob := GetActor(0, mobInstanceId) + if sMob == nil { + return false, errors.New("mob not found") + } + + vmw, err := getMobVM(sMob) + if err != nil { + return false, err + } + + tUser := GetActor(downedPlayerId, 0) + if tUser == nil { + return false, errors.New("player not found") + } + + timestart := time.Now() + defer func() { + mudlog.Debug("TryPlayerDownedEvent()", "mobInstanceId", mobInstanceId, "downedPlayerId", downedPlayerId, "time", time.Since(timestart)) + }() + + if onCommandFunc, ok := vmw.GetFunction(`onPlayerDowned`); ok { + + tmr := time.AfterFunc(scriptRoomTimeout, func() { + vmw.VM.Interrupt(errTimeout) + }) + + sRoom := GetRoom(sMob.GetRoomId()) + + res, err := onCommandFunc(goja.Undefined(), + vmw.VM.ToValue(sMob), + vmw.VM.ToValue(tUser), + vmw.VM.ToValue(sRoom), + ) + vmw.VM.ClearInterrupt() + tmr.Stop() + + if err != nil { + + // Wrap the error + finalErr := fmt.Errorf("%s(): %w", `onPlayerDowned`, err) + + if _, ok := finalErr.(*goja.Exception); ok { + mudlog.Error("JSVM", "exception", finalErr) + return false, finalErr + } else if errors.Is(finalErr, errTimeout) { + mudlog.Error("JSVM", "interrupted", finalErr) + return false, finalErr + } + + mudlog.Error("JSVM", "error", finalErr) + return false, finalErr + } + + if boolVal, ok := res.Export().(bool); ok { + return boolVal, nil + } + } + + return false, ErrEventNotFound +} + func TryMobScriptEvent(eventName string, mobInstanceId int, sourceId int, sourceType string, details map[string]any) (bool, error) { sMob := GetActor(0, mobInstanceId) diff --git a/internal/usercommands/attack.go b/internal/usercommands/attack.go index fdbdc35e..55e008e1 100644 --- a/internal/usercommands/attack.go +++ b/internal/usercommands/attack.go @@ -10,6 +10,7 @@ import ( "github.com/GoMudEngine/GoMud/internal/parties" "github.com/GoMudEngine/GoMud/internal/rooms" "github.com/GoMudEngine/GoMud/internal/users" + "github.com/GoMudEngine/GoMud/internal/util" ) func Attack(rest string, user *users.UserRecord, room *rooms.Room, flags events.EventFlag) (bool, error) { @@ -85,6 +86,50 @@ func Attack(rest string, user *users.UserRecord, room *rooms.Room, flags events. } } + } else if rest[0] == '*' { // choose a target at random. Friend or foe. + + if rest == `*` { // * ANYONE + + allMobs := room.GetMobs() + allPlayers := []int{} + for _, userId := range room.GetPlayers() { + if userId == user.UserId { + continue + } + allPlayers = append(allPlayers, userId) + } + + randomSelection := util.Rand(len(allMobs) + len(allPlayers)) + + if randomSelection < len(allMobs) { + attackMobInstanceId = allMobs[randomSelection] + } else { + randomSelection -= len(allMobs) + attackPlayerId = allPlayers[randomSelection] + } + + } else if rest == `*mob` { // *mob ANY MOB + + if allMobs := room.GetMobs(); len(allMobs) > 0 { + attackMobInstanceId = allMobs[util.Rand(len(allMobs))] + } + + } else { // *user etc. ANY PLAYER + + allPlayers := []int{} + for _, userId := range room.GetPlayers() { + if userId == user.UserId { + continue + } + allPlayers = append(allPlayers, userId) + } + + if len(allPlayers) > 0 { + attackPlayerId = allPlayers[util.Rand(len(allPlayers))] + } + + } + } else { attackPlayerId, attackMobInstanceId = room.FindByName(rest) } diff --git a/internal/usercommands/go.go b/internal/usercommands/go.go index 559c7e34..5a8c1834 100644 --- a/internal/usercommands/go.go +++ b/internal/usercommands/go.go @@ -67,7 +67,6 @@ func Go(rest string, user *users.UserRecord, room *rooms.Room, flags events.Even exitInfo, _ := room.GetExitInfo(exitName) - fmt.Println(exitInfo.Lock.IsLocked()) if exitInfo.Lock.IsLocked() { lockId := fmt.Sprintf(`%d-%s`, room.RoomId, exitName) diff --git a/world.go b/world.go index 2e037965..16842d08 100644 --- a/world.go +++ b/world.go @@ -1076,29 +1076,6 @@ func (w *World) Kick(userId int, reason string) { connections.Kick(user.ConnectionId(), reason) } -// Handle dropped players -func (w *World) HandleDroppedPlayers(droppedPlayers []int) { - - if len(droppedPlayers) == 0 { - return - } - - for _, userId := range droppedPlayers { - if user := users.GetByUserId(userId); user != nil { - - user.SendText(`you drop to the ground!`) - - if room := rooms.LoadRoom(user.Character.RoomId); room != nil { - room.SendText( - fmt.Sprintf(`%s drops to the ground!`, user.Character.Name), - user.UserId) - } - } - } - - return -} - // Should only handle sending messages out to users func (w *World) EventLoop() {