diff --git a/_datafiles/guides/building/scripting/FUNCTIONS_ACTORS.md b/_datafiles/guides/building/scripting/FUNCTIONS_ACTORS.md index a07ba2b1..d69188c8 100644 --- a/_datafiles/guides/building/scripting/FUNCTIONS_ACTORS.md +++ b/_datafiles/guides/building/scripting/FUNCTIONS_ACTORS.md @@ -16,6 +16,7 @@ ActorObjects are the basic object that represents Users and NPCs - [ActorObject.GetLevel() int](#actorobjectgetlevel-int) - [ActorObject.GetStat(statName string) int](#actorobjectgetstatstatname-string-int) - [ActorObject.SetTempData(key string, value any)](#actorobjectsettempdatakey-string-value-any) + - [ActorObject.SetResetRoomId(roomId int)](#actorobjectsetresetroomidroomid-int) - [ActorObject.GetTempData(key string) any](#actorobjectgettempdatakey-string-any) - [ActorObject.SetMiscCharacterData(key string, value any)](#actorobjectsetmisccharacterdatakey-string-value-any) - [ActorObject.GetMiscCharacterData(key string) any](#actorobjectgetmisccharacterdatakey-string-any) @@ -25,16 +26,19 @@ ActorObjects are the basic object that represents Users and NPCs - [ActorObject.GetRoomId() int](#actorobjectgetroomid-int) - [ActorObject.HasQuest(questId string) bool](#actorobjecthasquestquestid-string-bool) - [ActorObject.GiveQuest(questId string)](#actorobjectgivequestquestid-string) - - [ActorObject.GetPartyMembers() \[\]Actor](#actorobjectgetpartymembers-actor) + - [ActorObject.GetParty(\[excludeSelf bool\])](#actorobjectgetpartyexcludeself-bool) + - [ActorObject.GetPartyPresent(\[excludeSelf bool\])](#actorobjectgetpartypresentexcludeself-bool) + - [ActorObject.GetPartyMissing()](#actorobjectgetpartymissing) - [ActorObject.AddGold(amt int \[, bankAmt int\])](#actorobjectaddgoldamt-int--bankamt-int) - [ActorObject.AddHealth(amt int) int](#actorobjectaddhealthamt-int-int) + - [ActorObject.AddMana(amt int) int](#actorobjectaddmanaamt-int-int) - [ActorObject.Sleep(seconds int)](#actorobjectsleepseconds-int) - [ActorObject.Command(cmd string \[, waitTurns int\])](#actorobjectcommandcmd-string--waitturns-int) - [ActorObject.CommandFlagged(cmd string, flag int \[, waitTurns int\])](#actorobjectcommandflaggedcmd-string-flag-int--waitturns-int) - [ActorObject.IsTameable() bool](#actorobjectistameable-bool) - [ActorObject.TrainSkill(skillName string, skillLevel int)](#actorobjecttrainskillskillname-string-skilllevel-int) - [ActorObject.GetSkillLevel(skillName string)](#actorobjectgetskilllevelskillname-string) - - [ActorObject.MoveRoom(destRoomId int \[, leaveCharmedMobsBehind bool\] )](#actorobjectmoveroomdestroomid-int--leavecharmedmobsbehind-bool-) + - [ActorObject.MoveRoom(destRoomId int )](#actorobjectmoveroomdestroomid-int-) - [ActorObject.UpdateItem(itemId ItemObject)](#actorobjectupdateitemitemid-itemobject) - [ActorObject.GiveItem(itemId ItemObject)](#actorobjectgiveitemitemid-itemobject) - [ActorObject.TakeItem(itemId ItemObject)](#actorobjecttakeitemitemid-itemobject) @@ -118,7 +122,7 @@ Retrieves a ActorObject for a given mobInstanceId. | createIfMissing | If true and mob isn't found, the mob will be created and returned. | ## [ActorObject.UserId() int](/internal/scripting/actor_func.go) -Returns the userId of the ActorObject.˚ +Returns the userId of the ActorObject. _Note: Only useful for User ActorObjects - Returns zero otherwise._ @@ -173,6 +177,16 @@ _Note: This is useful for saving/retrieving data that an ActorObject can carry a | key | A unique identifier for the data. | | value | What you will be saving. If null, frees from memory. | +## [ActorObject.SetResetRoomId(roomId int)](/internal/scripting/actor_func.go) +Sets the "Reset Room Id" a player will be sent to if they log out. + +_Note: This is only useful if the player is being sent to an ephemeral chunk._ + +| Argument | Explanation | +| --- | --- | +| roomId | The RoomId the player should be sent to. | + + ## [ActorObject.GetTempData(key string) any](/internal/scripting/actor_func.go) Gets temporary data for the ActorObject. @@ -235,14 +249,28 @@ Get whether a ActorObject has a quest/progress. | questId | The quest identifier string to check, such as `3-start`. | ## [ActorObject.GiveQuest(questId string)](/internal/scripting/actor_func.go) -Grants a quest or progress on a quest to a ActorObject. If they are in a party, grants to the party members as well. +Grants a quest or progress on a quest to a ActorObject. | Argument | Explanation | | --- | --- | | questId | The quest identifier string to give, such as `3-start`. | -## [ActorObject.GetPartyMembers() []Actor](/internal/scripting/actor_func.go) -Returns a list of actors in the party, both players and mobs. +## [ActorObject.GetParty([excludeSelf bool])](/internal/scripting/actor_func.go) +Returns a Party object that operates on the entire party and charmed mobs. + +| Argument | Explanation | +| --- | --- | +| excludeSelf | If true, excludes the actor from the operating list. | + +## [ActorObject.GetPartyPresent([excludeSelf bool])](/internal/scripting/actor_func.go) +Returns a Party object that operates on the party members and charmed mobs in the same room. + +| Argument | Explanation | +| --- | --- | +| excludeSelf | If true, excludes the actor from the operating list. | + +## [ActorObject.GetPartyMissing()](/internal/scripting/actor_func.go) +Returns a Party object that operates on the party members and charmed mobs missing from the room. ## [ActorObject.AddGold(amt int [, bankAmt int])](/internal/scripting/actor_func.go) Update how much gold an ActorObject has @@ -259,6 +287,13 @@ Update how much health an ActorObject has, and returns the actual amount their h | --- | --- | | amt | A positive or negative amount of health to alter the actors health by. | +## [ActorObject.AddMana(amt int) int](/internal/scripting/actor_func.go) +Update how much mana an ActorObject has, and returns the actual amount their mana changed. + +| Argument | Explanation | +| --- | --- | +| amt | A positive or negative amount of mana to alter the actors mana by. | + ## [ActorObject.Sleep(seconds int)](/internal/scripting/actor_func.go) Force a mob to wait this many seconds before executing any additional behaviors @@ -309,13 +344,14 @@ Returns the current skil level for the skillName, or zero if none. | skillName | The name of the skill to train, such as `map` or `backstab`. | -## [ActorObject.MoveRoom(destRoomId int [, leaveCharmedMobsBehind bool] )](/internal/scripting/actor_func.go) +## [ActorObject.MoveRoom(destRoomId int )](/internal/scripting/actor_func.go) Quietly moves an ActorObject to a new room | Argument | Explanation | | --- | --- | | destRoomId | The room id to move them to. | -| leaveCharmedMobsBehind | If true, does not also move charmed mobs with the user. | + + ## [ActorObject.UpdateItem(itemId ItemObject)](/internal/scripting/actor_func.go) Accepts an ItemObject to update in the players backpack. If the item does not already exist in the players backpack, it is ignored. diff --git a/_datafiles/guides/building/scripting/FUNCTIONS_ITEMS.md b/_datafiles/guides/building/scripting/FUNCTIONS_ITEMS.md index e5eda869..c02f0800 100644 --- a/_datafiles/guides/building/scripting/FUNCTIONS_ITEMS.md +++ b/_datafiles/guides/building/scripting/FUNCTIONS_ITEMS.md @@ -16,6 +16,7 @@ ActorObjects are the basic object that represents Users and NPCs - [ItemObject.SetTempData(key string, value any)](#itemobjectsettempdatakey-string-value-any) - [ItemObject.GetTempData(key string) any](#itemobjectgettempdatakey-string-any) - [ItemObject.Rename(newName string \[, displayNameOrStyle string\])](#itemobjectrenamenewname-string--displaynameorstyle-string) + - [ItemObject.ShorthandId() string](#itemobjectshorthandid-string) - [ItemObject.Redescribe(newDescription string)](#itemobjectredescribenewdescription-string) ## [CreateItem(itemId int) ItemObject ](/internal/scripting/item_func.go) @@ -74,10 +75,10 @@ Sets temporary data of any sort on the item. This data is not saved/loaded when | Argument | Explanation | | --- | --- | | key | The name to store the data under. Also used to retrieve the data later. | -| vaue | The data to store. | +| value | The data to store. | ## [ItemObject.GetTempData(key string) any](/internal/scripting/item_func.go) -Sets temporary data of any sort on the item. This data is not saved/loaded when despawning. +Gets temporary data of any sort on the item. This data is not saved/loaded when despawning. | Argument | Explanation | | --- | --- | @@ -92,6 +93,9 @@ Renames the item, also optionally provide a fancy name or colorpattern | newName | The plaintext name. | | displayNameOrStyle | A fancy name in ansi tags, color short tags, or a pattern like :flame | +## [ItemObject.ShorthandId() string](/internal/scripting/item_func.go) +Returns the shorthand identifier for the item. + ## [ItemObject.Redescribe(newDescription string)](/internal/scripting/item_func.go) Change the description for an item diff --git a/_datafiles/guides/building/scripting/FUNCTIONS_PARTY.md b/_datafiles/guides/building/scripting/FUNCTIONS_PARTY.md new file mode 100644 index 00000000..696f285a --- /dev/null +++ b/_datafiles/guides/building/scripting/FUNCTIONS_PARTY.md @@ -0,0 +1,198 @@ +# PartyObject + +PartyObjects represent collections of actors (users and NPCs) that are grouped together. They provide convenient methods to perform operations on multiple actors at once. + +- [PartyObject](#partyobject) + - [PartyObject.GetMembers() \[\]ActorObject](#partyobjectgetmembers-actorobject) + - [PartyObject.SendText(msg string)](#partyobjectsendtextmsg-string) + - [PartyObject.SetResetRoomId(roomId int)](#partyobjectsetresetroomidroomid-int) + - [PartyObject.GiveQuest(questId string)](#partyobjectgivequestquestid-string) + - [PartyObject.AddGold(amt int \[, bankAmt int\])](#partyobjectaddgoldamt-int--bankamt-int) + - [PartyObject.AddHealth(amt int)](#partyobjectaddhealthamt-int) + - [PartyObject.AddMana(amt int)](#partyobjectaddmanaamt-int) + - [PartyObject.Command(cmd string \[, waitSeconds float\])](#partyobjectcommandcmd-string--waitseconds-float) + - [PartyObject.TrainSkill(skillName string, skillLevel int)](#partyobjecttrainskillskillname-string-skilllevel-int) + - [PartyObject.MoveRoom(destRoomId int)](#partyobjectmoveroomdestroomid-int) + - [PartyObject.AddEventLog(category string, message string)](#partyobjectaddeventlogcategory-string-message-string) + - [PartyObject.GiveBuff(buffId int, source string)](#partyobjectgivebuffbuffid-int-source-string) + - [PartyObject.CancelBuffWithFlag(buffFlag string)](#partyobjectcancelbuffwithflagbuffflag-string) + - [PartyObject.RemoveBuff(buffId int)](#partyobjectremovebuffbuffid-int) + - [PartyObject.ChangeAlignment(alignmentChange int)](#partyobjectchangealignmentalignmentchange-int) + - [PartyObject.LearnSpell(spellId string)](#partyobjectlearnspellspellid-string) + - [PartyObject.SetHealth(amt int)](#partyobjectsethealthamt-int) + - [PartyObject.SetAdjective(adj string, addIt bool)](#partyobjectsetadjectiveadj-string-addit-bool) + - [PartyObject.GiveTrainingPoints(ct int)](#partyobjectgivetrainingpointsct-int) + - [PartyObject.GiveStatPoints(ct int)](#partyobjectgivestatpointsct-int) + - [PartyObject.GiveExtraLife()](#partyobjectgiveextralife) + - [PartyObject.GrantXP(xpAmt int, reason string)](#partyobjectgrantxpxpamt-int-reason-string) + - [PartyObject.TimerSet(name string, period string)](#partyobjecttimersetname-string-period-string) + + + + +## [PartyObject.GetMembers() []ActorObject](/internal/scripting/party_func.go) +Returns an array of all ActorObjects in the party based on the party configuration. + +_Note: This includes both party members and their charmed creatures, filtered by presence/absence based on how the party object was created._ + +## [PartyObject.SendText(msg string)](/internal/scripting/party_func.go) +Sends a message to all members of the party. + +| Argument | Explanation | +| --- | --- | +| msg | the message to send to all party members | + +## [PartyObject.SetResetRoomId(roomId int)](/internal/scripting/party_func.go) +Sets the "Reset Room Id" for all user party members (where they will be sent if they log out). + +_Note: Only affects user party members, not NPCs. This is only useful if players are being sent to ephemeral chunks._ + +| Argument | Explanation | +| --- | --- | +| roomId | The RoomId all party members should be sent to | + +## [PartyObject.GiveQuest(questId string)](/internal/scripting/party_func.go) +Grants a quest or progress on a quest to all party members. + +| Argument | Explanation | +| --- | --- | +| questId | The quest identifier string to give, such as `3-start` | + +## [PartyObject.AddGold(amt int [, bankAmt int])](/internal/scripting/party_func.go) +Updates how much gold all party members have. + +| Argument | Explanation | +| --- | --- | +| amt | A positive or negative amount of gold to alter each party member's gold by | +| bankAmt (optional) | A positive or negative amount of gold to alter each party member's bank gold by | + +## [PartyObject.AddHealth(amt int)](/internal/scripting/party_func.go) +Updates how much health all party members have. + +| Argument | Explanation | +| --- | --- | +| amt | A positive or negative amount of health to alter each party member's health by | + +## [PartyObject.AddMana(amt int)](/internal/scripting/party_func.go) +Updates how much mana all party members have. + +| Argument | Explanation | +| --- | --- | +| amt | A positive or negative amount of mana to alter each party member's mana by | + +## [PartyObject.Command(cmd string [, waitSeconds float])](/internal/scripting/party_func.go) +Forces all party members to execute a command as if they entered it. + +_Note: Don't underestimate the power of this function! Complex and interesting behaviors or interactions can emerge from using it._ + +| Argument | Explanation | +| --- | --- | +| cmd | The command to execute such as `look west` or `say goodbye` | +| waitSeconds (optional) | The number of seconds to wait before executing the command | + +## [PartyObject.TrainSkill(skillName string, skillLevel int)](/internal/scripting/party_func.go) +Sets a skill level for all party members, if it's greater than what they already have. + +| Argument | Explanation | +| --- | --- | +| skillName | The name of the skill to train, such as `map` or `backstab` | +| skillLevel | The skill level to train to | + +## [PartyObject.MoveRoom(destRoomId int)](/internal/scripting/party_func.go) +Quietly moves all party members to a new room. + +| Argument | Explanation | +| --- | --- | +| destRoomId | The room id to move them all to | + +## [PartyObject.AddEventLog(category string, message string)](/internal/scripting/party_func.go) +Adds a line to all party members' Event Log (`history`). + +| Argument | Explanation | +| --- | --- | +| category | A short single word category | +| message | A single line describing the event | + +## [PartyObject.GiveBuff(buffId int, source string)](/internal/scripting/party_func.go) +Grants all party members a Buff. + +| Argument | Explanation | +| --- | --- | +| buffId | The ID of the buff to give them | +| source | The source of the buff, "item", "spell", "trap", "curse", etc. or empty | + +## [PartyObject.CancelBuffWithFlag(buffFlag string)](/internal/scripting/party_func.go) +Cancels any buffs that have the flag provided for all party members. + +| Argument | Explanation | +| --- | --- | +| buffFlag | The buff flag to check [see buffspec.go](../buffs/buffspec.go) | + +## [PartyObject.RemoveBuff(buffId int)](/internal/scripting/party_func.go) +Remove a buff from all party members without triggering onEnd. + +| Argument | Explanation | +| --- | --- | +| buffId | The ID of the buff to remove | + +## [PartyObject.ChangeAlignment(alignmentChange int)](/internal/scripting/party_func.go) +Updates the alignment of all party members by a relative amount. Caps result at -100 to 100. + +| Argument | Explanation | +| --- | --- | +| alignmentChange | The alignment adjustment, from -200 to 200 | + +## [PartyObject.LearnSpell(spellId string)](/internal/scripting/party_func.go) +Adds the spell to all party members' spellbooks. + +| Argument | Explanation | +| --- | --- | +| spellId | The ID of the spell | + +## [PartyObject.SetHealth(amt int)](/internal/scripting/party_func.go) +Sets all party members' 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 | + +## [PartyObject.SetAdjective(adj string, addIt bool)](/internal/scripting/party_func.go) +Adds or removes a specific text adjective to all party members' names. + +| Argument | Explanation | +| --- | --- | +| adj | Adjective such as "sleeping", "crying" or "busy" | +| addIt | `true` to add it. `false` to remove it | + +## [PartyObject.GiveTrainingPoints(ct int)](/internal/scripting/party_func.go) +Increases training points for all party members. + +| Argument | Explanation | +| --- | --- | +| ct | How many training points to give | + +## [PartyObject.GiveStatPoints(ct int)](/internal/scripting/party_func.go) +Increases stat points for all party members. + +| Argument | Explanation | +| --- | --- | +| ct | How many stat points to give | + +## [PartyObject.GiveExtraLife()](/internal/scripting/party_func.go) +Increases extra lives by 1 for all party members. + +## [PartyObject.GrantXP(xpAmt int, reason string)](/internal/scripting/party_func.go) +Gives experience points to all party members. + +| Argument | Explanation | +| --- | --- | +| xpAmt | How much experience to grant | +| reason | Short reasons such as "combat", "trash cleanup" | + +## [PartyObject.TimerSet(name string, period string)](/internal/scripting/party_func.go) +Starts a new Round timer for all party members. + +| Argument | Explanation | +| --- | --- | +| name | A string identifier. Reusing names will overwrite previously assigned names | +| period | How long until the timer expires. `1 real hour`, `1 hour`, etc | \ No newline at end of file diff --git a/_datafiles/guides/building/scripting/FUNCTIONS_ROOMS.md b/_datafiles/guides/building/scripting/FUNCTIONS_ROOMS.md index a871255c..78d619c3 100644 --- a/_datafiles/guides/building/scripting/FUNCTIONS_ROOMS.md +++ b/_datafiles/guides/building/scripting/FUNCTIONS_ROOMS.md @@ -1,11 +1,13 @@ # Room Specific Functions - [Room Specific Functions](#room-specific-functions) + - [CreateEmptyRoomInstances(quantity int) \[\]int](#createemptyroominstancesquantity-int-int) - [CreateInstancesFromRoomIds(RoomIds \[int, int...\]) Object ](#createinstancesfromroomidsroomids-int-int-object-) - [CreateInstancesFromZone(zoneName string) Object ](#createinstancesfromzonezonename-string-object-) - [GetRoom(roomId int) RoomObject ](#getroomroomid-int-roomobject-) - [RoomObject.RoomId() int](#roomobjectroomid-int) - [RoomObject.SendText(msg string\[, excludeUserIds int\])](#roomobjectsendtextmsg-string-excludeuserids-int) + - [RoomObject.SendTextToExits(msg string, isQuiet bool\[, excludeUserIds int\])](#roomobjectsendtexttoexitsmsg-string-isquiet-bool-excludeuserids-int) - [RoomObject.SetTempData(key string, value any)](#roomobjectsettempdatakey-string-value-any) - [RoomObject.GetTempData(key string) any](#roomobjectgettempdatakey-string-any) - [RoomObject.SetPermData(key string, value any)](#roomobjectsetpermdatakey-string-value-any) @@ -13,7 +15,7 @@ - [RoomObject.GetItems() \[\]ItemObject](#roomobjectgetitems-itemobject) - [RoomObject.DestroyItem(itm ScriptItem) ](#roomobjectdestroyitemitm-scriptitem-) - [RoomObject.SpawnItem(itemId int, inStash bool) \[\]ItemObject](#roomobjectspawnitemitemid-int-instash-bool-itemobject) - - [RoomObject.GetMob(mobId int) Actor](#roomobjectgetmobmobid-int-actor) + - [RoomObject.GetMob(mobId int \[, createIfMissing bool\]) Actor](#roomobjectgetmobmobid-int--createifmissing-bool-actor) - [RoomObject.GetMobs(\[mobId int\]) \[\]Actor](#roomobjectgetmobsmobid-int-actor) - [RoomObject.GetPlayers() \[\]Actor](#roomobjectgetplayers-actor) - [RoomObject.GetAllActors() \[\]Actor](#roomobjectgetallactors-actor) @@ -23,17 +25,24 @@ - [RoomObject.HasQuest(questId string \[,partyUserId int\]) \[\]int](#roomobjecthasquestquestid-string-partyuserid-int-int) - [RoomObject.MissingQuest(questId string \[,partyUserId int\]) \[\]int](#roomobjectmissingquestquestid-string-partyuserid-int-int) - [RoomObject.SpawnMob(mobId int) Actor](#roomobjectspawnmobmobid-int-actor) - - [RoomObject.AddTemporaryExit(exitNameSimple string, exitNameFancy string, exitRoomId int, expiresTimeString string](#roomobjectaddtemporaryexitexitnamesimple-string-exitnamefancy-string-exitroomid-int-expirestimestring-string) - - [RoomObject.RemoveTemporaryExit(exitNameSimple string, exitNameFancy string, exitRoomId int](#roomobjectremovetemporaryexitexitnamesimple-string-exitnamefancy-string-exitroomid-int) + - [RoomObject.AddTemporaryExit(exitNameSimple string, exitNameFancy string, exitRoomId int, expiresTimeString string) bool](#roomobjectaddtemporaryexitexitnamesimple-string-exitnamefancy-string-exitroomid-int-expirestimestring-string-bool) + - [RoomObject.RemoveTemporaryExit(exitNameSimple string, exitNameFancy string, exitRoomId int) bool](#roomobjectremovetemporaryexitexitnamesimple-string-exitnamefancy-string-exitroomid-int-bool) - [RoomObject.HasMutator(mutName string) bool](#roomobjecthasmutatormutname-string-bool) - [RoomObject.AddMutator(mutName string)](#roomobjectaddmutatormutname-string) - [RoomObject.RemoveMutator(mutName string)](#roomobjectremovemutatormutname-string) - [RoomObject.IsEphemeral() bool](#roomobjectisephemeral-bool) - [RoomObject.RoomIdSource() int](#roomobjectroomidsource-int) - - [RoomObject.RepeatSpawnItem(itemId int, roundInterval int \[, containerName\]](#roomobjectrepeatspawnitemitemid-int-roundinterval-int--containername) + - [RoomObject.RepeatSpawnItem(itemId int, roundInterval int \[, containerName\]) bool](#roomobjectrepeatspawnitemitemid-int-roundinterval-int--containername-bool) - [RoomObject.SetLocked(exitName string, lockIt bool)](#roomobjectsetlockedexitname-string-lockit-bool) - [RoomObject.IsLocked(exitName string) bool](#roomobjectislockedexitname-string-bool) +## [CreateEmptyRoomInstances(quantity int) []int](/internal/scripting/room_func.go) +Creates a specified number of empty ephemeral room instances. + +| Argument | Explanation | +| --- | --- | +| quantity | Number of empty rooms to create | + ## [CreateInstancesFromRoomIds(RoomIds [int, int...]) Object ](/internal/scripting/room_func.go) Returns an Object with key/value pairs of `ProvidedRoomId`=>`NewRoomId` Creates ephemeral instances of the RoomId's provided. @@ -66,6 +75,15 @@ Sends a message to everyone in the room. | msg | the message to send | | excludeUserIds | One or more comma separated userIds to exclude from receiving the message. | +## [RoomObject.SendTextToExits(msg string, isQuiet bool[, excludeUserIds int])](/internal/scripting/room_func.go) +Sends a message to all rooms with an exit leading to this room. + +| Argument | Explanation | +| --- | --- | +| msg | the message to send | +| isQuiet | If true, only those with superior "hearing" will see it. | +| excludeUserIds | One or more comma separated userIds to exclude from receiving the message. | + ## [RoomObject.SetTempData(key string, value any)](/internal/scripting/room_func.go) Sets temporary data for the room (Lasts until the room is unloaded from memory). @@ -112,7 +130,7 @@ _Note: See [/scripting/docs/FUNCTIONS_ITEMS.md](FUNCTIONS_ITEMS.md) for details ## [RoomObject.DestroyItem(itm ScriptItem) ](/internal/scripting/room_func.go) Destroy an item from the ground. -## [RoomObject.SpawnItem(itemId int, inStash bool) []ItemObject](/internal/scripting/room_func.go) +## [RoomObject.SpawnItem(itemId int, inStash bool)](/internal/scripting/room_func.go) Spawns an item in the room. | Argument | Explanation | @@ -121,12 +139,13 @@ Spawns an item in the room. | inStash | If true, spawns stashed instead of visible. | -## [RoomObject.GetMob(mobId int) Actor](/internal/scripting/room_func.go) +## [RoomObject.GetMob(mobId int [, createIfMissing bool]) Actor](/internal/scripting/room_func.go) Returns the first mob that matches the provided MobId type (Note: NOT MOB INSTANCE ID!) | Argument | Explanation | | --- | --- | | mobId | MobId to match. | +| createIfMissing (optional) | If true, will spawn the mob if not found. | ## [RoomObject.GetMobs([mobId int]) []Actor](/internal/scripting/room_func.go) Returns an array of mob `Actor`s in the room. @@ -186,7 +205,7 @@ _Note: This could be useful for situations where you want to allow a whole party | partyUserId (optional) | Only check the specified user and their party | ## [RoomObject.MissingQuest(questId string [,partyUserId int]) []int](/internal/scripting/room_func.go) -Returns an array of userId's in the romo who DON'T have the questId. If partyyUserId is supplied, only checks the user and their party specified. +Returns an array of userId's in the room who DON'T have the questId. If partyUserId is supplied, only checks the user and their party specified. _Note: This could be useful for situations where you want to disallow a whole party access to an area even if only one of them is missing the quest._ @@ -202,7 +221,7 @@ Creates a new instance of MobId,and returns the `Actor` of the mob. | --- | --- | | mobId | The ID if the mob type to spawn. NOT THE INSTANCE ID. | -## [RoomObject.AddTemporaryExit(exitNameSimple string, exitNameFancy string, exitRoomId int, expiresTimeString string](/internal/scripting/room_func.go) +## [RoomObject.AddTemporaryExit(exitNameSimple string, exitNameFancy string, exitRoomId int, expiresTimeString string) bool](/internal/scripting/room_func.go) Adds a temporary exit to the room for the specified amount of time. | Argument | Explanation | @@ -212,7 +231,7 @@ Adds a temporary exit to the room for the specified amount of time. | exitRoomId | The roomId the exit should lead to. | | expiresTimeString | Time string (1 day, 1 real day, 4 hours, etc) before it vanishes. | -## [RoomObject.RemoveTemporaryExit(exitNameSimple string, exitNameFancy string, exitRoomId int](/internal/scripting/room_func.go) +## [RoomObject.RemoveTemporaryExit(exitNameSimple string, exitNameFancy string, exitRoomId int) bool](/internal/scripting/room_func.go) Removes a temporary exit _Note: all 3 parameters much match an existing temporary exit for it to be removed._ @@ -243,7 +262,7 @@ _Note: If the mutator already exists this is ignored._ ## [RoomObject.RemoveMutator(mutName string)](/internal/scripting/room_func.go) Removes a mutator from a room. -_Note: This only expires it. It may be a mutator that respawns, in which case this doens't really completely remove it._ +_Note: This only expires it. It may be a mutator that respawns, in which case this doesn't really completely remove it._ | Argument | Explanation | | --- | --- | @@ -252,20 +271,14 @@ _Note: This only expires it. It may be a mutator that respawns, in which case th ## [RoomObject.IsEphemeral() bool](/internal/scripting/room_func.go) Returns true if the room is an Ephemeral Copy of a room. -_Note: This only expires it. It may be a mutator that respawns, in which case this doens't really completely remove it._ - -| Argument | Explanation | -| --- | --- | -| mutName | the MutatorId of the mutator. | - ## [RoomObject.RoomIdSource() int](/internal/scripting/room_func.go) Returns the source RoomId if this room is an ephemeral copy, otherwise just the normal RoomId -## [RoomObject.RepeatSpawnItem(itemId int, roundInterval int [, containerName]](/internal/scripting/room_func.go) -Removes a temporary exit +## [RoomObject.RepeatSpawnItem(itemId int, roundInterval int [, containerName]) bool](/internal/scripting/room_func.go) +Sets up automatic item respawning in the room. -_Note: all 3 parameters much match an existing temporary exit for it to be removed._ +_Note: The item will respawn after the specified interval when removed from the room._ | Argument | Explanation | | --- | --- | diff --git a/_datafiles/guides/building/scripting/README.md b/_datafiles/guides/building/scripting/README.md index 81b3ff54..c3dcd2b0 100644 --- a/_datafiles/guides/building/scripting/README.md +++ b/_datafiles/guides/building/scripting/README.md @@ -21,6 +21,8 @@ See [Spell Scripting](SCRIPTING_SPELLS.md) [ActorObject Functions](FUNCTIONS_ACTORS.md) - Functions that query or alter user/mob data. +[ActorObject Functions](FUNCTIONS_PARTY.md) - Functions that operate on the whole party. + [RoomObject Functions](FUNCTIONS_ROOMS.md) - Functions that query or alter room data. [ItemObject Functions](FUNCTIONS_ITEMS.md) - Functions that query or alter item data. diff --git a/_datafiles/sample-scripts/mobs/item-gold-quest.js b/_datafiles/sample-scripts/mobs/item-gold-quest.js index cc0b5ab2..171ddfdb 100644 --- a/_datafiles/sample-scripts/mobs/item-gold-quest.js +++ b/_datafiles/sample-scripts/mobs/item-gold-quest.js @@ -46,7 +46,7 @@ function onAsk(mob, room, eventDetails) { // // Search the text they inputted for the "ask" command for one of the questStartSubjects // - match = UtilFindMatchIn(eventDetails.askText, questStartSubjects); + var match = UtilFindMatchIn(eventDetails.askText, questStartSubjects); if ( match.found ) { @@ -55,7 +55,7 @@ function onAsk(mob, room, eventDetails) { // // Give them the start quest id // - user.GiveQuest(QUEST_START_ID); + user.GetParty().GiveQuest(QUEST_START_ID); } @@ -66,7 +66,7 @@ function onAsk(mob, room, eventDetails) { // By this point in the script we know they've at least started the quest // Lets see if they are asking any follow up questions for more info. // - match = UtilFindMatchIn(eventDetails.askText, questInfoSubjects); + var match = UtilFindMatchIn(eventDetails.askText, questInfoSubjects); if ( match.found ) { mob.Command("emote thinks hard for a moment."); mob.Command("say You can get sharp sticks from a shop, and gold from selling objects, or possibly killing bad guys and looting them."); @@ -129,7 +129,7 @@ function onGive(mob, room, eventDetails) { // // Give them the next step of the quest // - user.GiveQuest(QUEST_NEXT_STEP_ID); + user.GetParty().GiveQuest(QUEST_NEXT_STEP_ID); return true; } @@ -170,7 +170,7 @@ function onGive(mob, room, eventDetails) { // // If they gave too much gold, lets give them back the change. // - excessGold = eventDetails.gold - REQUIRED_GOLD_AMOUNT; + var excessGold = eventDetails.gold - REQUIRED_GOLD_AMOUNT; if ( excessGold > 0 ) { mob.Command("say Here's your change."); mob.Command("give "+String(excessGold)+" gold " + user.ShorthandId()); // Give it to the player using shorthand @@ -179,7 +179,7 @@ function onGive(mob, room, eventDetails) { // // They have now completed the entire quest, all steps are complete. // - user.GiveQuest(QUEST_END_ID); + user.GetParty().GiveQuest(QUEST_END_ID); return true; } diff --git a/_datafiles/world/default/mobs/frostfang/scripts/2-guard-hungry.js b/_datafiles/world/default/mobs/frostfang/scripts/2-guard-hungry.js index eab32f0e..aed40db2 100644 --- a/_datafiles/world/default/mobs/frostfang/scripts/2-guard-hungry.js +++ b/_datafiles/world/default/mobs/frostfang/scripts/2-guard-hungry.js @@ -23,7 +23,7 @@ function onAsk(mob, room, eventDetails) { mob.Command("say I forgot my lunch today, and I'm so hungry."); mob.Command("say Do you think you could find a cheese sandwich for me?"); - user.GiveQuest("4-start"); + user.GetParty().GiveQuest("4-start"); } else if ( user.HasQuest("4-end") ) { mob.Command("sayto @" + String(user.UserId()) + " Thanks, but you've done enough. Too much, really."); @@ -70,7 +70,7 @@ function onGive(mob, room, eventDetails) { if ( user.HasQuest("4-start") ) { - user.GiveQuest("4-end"); + user.GetParty().GiveQuest("4-end"); mob.Command("say Thanks! I can get on with my day now."); mob.Command("eat !"+String(eventDetails.item.ItemId) ); @@ -137,10 +137,10 @@ function onIdle(mob, room) { if ( sizeAfter != sizeBefore ) { if ( sizeAfter == 0 ) { + mob.SetTempData('playersTold', null); + } else { mob.SetTempData('playersTold', playersTold); } - } else { - mob.SetTempData('playersTold', null); } action = round % 3; diff --git a/_datafiles/world/default/mobs/frostfang/scripts/26-frostfang_citizen-locketsadness.js b/_datafiles/world/default/mobs/frostfang/scripts/26-frostfang_citizen-locketsadness.js index fc92bcf5..bf613af0 100644 --- a/_datafiles/world/default/mobs/frostfang/scripts/26-frostfang_citizen-locketsadness.js +++ b/_datafiles/world/default/mobs/frostfang/scripts/26-frostfang_citizen-locketsadness.js @@ -30,7 +30,7 @@ function onAsk(mob, room, eventDetails) { mob.Command("emote sighs deeply."); mob.Command("say I lost my locket. I think it was when I was gardening."); - user.GiveQuest("1-start"); + user.GetParty().GiveQuest("1-start"); return true; } @@ -78,7 +78,7 @@ function onGive(mob, room, eventDetails) { mob.Command("give !20033 @" + String(eventDetails.sourceId)); // Give it to the player using shorthand } - user.GiveQuest("1-end"); + user.GetParty().GiveQuest("1-end"); return true; diff --git a/_datafiles/world/default/mobs/frostfang/scripts/26-frostfang_citizen-rattrap.js b/_datafiles/world/default/mobs/frostfang/scripts/26-frostfang_citizen-rattrap.js index 54e12cbe..d6727f47 100644 --- a/_datafiles/world/default/mobs/frostfang/scripts/26-frostfang_citizen-rattrap.js +++ b/_datafiles/world/default/mobs/frostfang/scripts/26-frostfang_citizen-rattrap.js @@ -19,7 +19,7 @@ function onAsk(mob, room, eventDetails) { mob.Command("say Thanks for picking it up!"); - user.GiveQuest("7-gettrap"); + user.GetParty().GiveQuest("7-tradetrap"); return true; } diff --git a/_datafiles/world/default/mobs/frostfang/scripts/39-elara.js b/_datafiles/world/default/mobs/frostfang/scripts/39-elara.js index c7537891..591a08c7 100644 --- a/_datafiles/world/default/mobs/frostfang/scripts/39-elara.js +++ b/_datafiles/world/default/mobs/frostfang/scripts/39-elara.js @@ -19,7 +19,7 @@ function onAsk(mob, room, eventDetails) { mob.Command("say I took a book called The History of Frostfang out with me weeks ago and can't remember where I left it."); mob.Command("say If you can find it for me, I'll teach you a useful spell."); - user.GiveQuest("6-start"); + user.GetParty().GiveQuest("6-start"); return true; } @@ -61,18 +61,14 @@ function onGive(mob, room, eventDetails) { if ( user.HasQuest("6-start") ) { - user.GiveQuest("6-end"); + user.GetParty().GiveQuest("6-end"); mob.Command("say Thank you! It is such an interesting history. For example, Frostfang used to be called DragonsFang!"); mob.Command("say I'll teach you the Illuminate spell. It's useful in dark places."); mob.Command("emote Shows you some useful gestures."); mob.Command("say Check your spellbook."); - partyMembers = user.GetPartyMembers(); - for( i = 0; i < partyMembers.length; i++ ) { - a = partyMembers[i]; - a.LearnSpell("illum"); - } + user.GetParty().LearnSpell("illum"); return true; } diff --git a/_datafiles/world/default/mobs/frostfang/scripts/4-clergyman-sanctuary.js b/_datafiles/world/default/mobs/frostfang/scripts/4-clergyman-sanctuary.js index e7eb94a8..297b00d9 100644 --- a/_datafiles/world/default/mobs/frostfang/scripts/4-clergyman-sanctuary.js +++ b/_datafiles/world/default/mobs/frostfang/scripts/4-clergyman-sanctuary.js @@ -15,7 +15,7 @@ function onAsk(mob, room, eventDetails) { mob.Command("say I often see some priests snooping around in the alley behind the Sanctuary."); mob.Command("say I used to think they were just taking care of the rat problem, but now I'm not so sure."); - user.GiveQuest("2-catacombs"); + user.GetParty().GiveQuest("2-catacombs"); return true; } diff --git a/_datafiles/world/default/mobs/frostfang/scripts/40-rodric.js b/_datafiles/world/default/mobs/frostfang/scripts/40-rodric.js index 83e1508e..d760b677 100644 --- a/_datafiles/world/default/mobs/frostfang/scripts/40-rodric.js +++ b/_datafiles/world/default/mobs/frostfang/scripts/40-rodric.js @@ -19,7 +19,7 @@ function onAsk(mob, room, eventDetails) { mob.Command("say I'm running out of traps and don't seem to be making a dent in the rat numbers."); mob.Command("say If you can kill 25 of them, come back and see me. I'll pay you for your trouble."); - user.GiveQuest("7-start"); + user.GetParty().GiveQuest("7-start"); return true; } return false; @@ -33,7 +33,7 @@ function onAsk(mob, room, eventDetails) { mob.Command("say Thank you for killing those rats! I can finally get a little rest."); mob.Command("say While you're feeling helpful, if you could recover a rat trap from a frostfang citizen I was working for, I would be very grateful."); - user.GiveQuest("7-gettrap"); + user.GetParty().GiveQuest("7-gettrap"); return true; } @@ -104,7 +104,7 @@ function onGive(mob, room, eventDetails) { mob.Command("say Thank you so much! I can finally get back to catching some rats, and maybe earn a little coin."); mob.Command("say The thieves guild used to employ me to eliminate rats around their hideout, but for some reason they don't seem to need my help anymore, and didn't pay me for my last job I did for them."); - user.GiveQuest("7-end"); + user.GetParty().GiveQuest("7-end"); } diff --git a/_datafiles/world/default/mobs/frostfang/scripts/8-king.js b/_datafiles/world/default/mobs/frostfang/scripts/8-king.js index 9868edd1..62734e46 100644 --- a/_datafiles/world/default/mobs/frostfang/scripts/8-king.js +++ b/_datafiles/world/default/mobs/frostfang/scripts/8-king.js @@ -20,7 +20,7 @@ function onAsk(mob, room, eventDetails) { mob.Command("say My spies haven't been able to discover anything suspicious about their behavior, which is the first clue something is up."); mob.Command("say Maybe you could snoop around there a bit and see if you can discover anything. They are just to the south of Town Square."); - user.GiveQuest("2-start"); + user.GetParty().GiveQuest("2-start"); return true; } @@ -51,7 +51,7 @@ function onGive(mob, room, eventDetails) { mob.AddGold(1250); mob.Command("give 1250 gold @" + String(eventDetails.sourceId)); - user.GiveQuest("2-end"); + user.GetParty().GiveQuest("2-end"); return true; } else { @@ -74,7 +74,7 @@ function onShow(mob, room, eventDetails) { mob.Command("say Thank you for taking care of that problem. The kingdom is indebted to you."); - user.GiveQuest("2-end"); + user.GetParty().GiveQuest("2-end"); return true; diff --git a/_datafiles/world/default/mobs/whispering_wastes/scripts/27-hermit-winterfire.js b/_datafiles/world/default/mobs/whispering_wastes/scripts/27-hermit-winterfire.js index a8dcb8c2..64517a74 100644 --- a/_datafiles/world/default/mobs/whispering_wastes/scripts/27-hermit-winterfire.js +++ b/_datafiles/world/default/mobs/whispering_wastes/scripts/27-hermit-winterfire.js @@ -14,7 +14,7 @@ function onAsk(mob, room, eventDetails) { mob.Command("say I've been waiting for a shipment of winterfire crystals. They should have been here months ago."); mob.Command("say I'll never abandon my post! Can you find out what happened to my crystals?"); - user.GiveQuest("5-start"); + user.GetParty().GiveQuest("5-start"); return true; } @@ -23,7 +23,7 @@ function onAsk(mob, room, eventDetails) { if ( match.found ) { mob.Command("say The shipment was supposed to come from the far east city of Mystarion. I'm not sure what happened to it."); - user.GiveQuest("5-lookeast"); + user.GetParty().GiveQuest("5-lookeast"); return true; } @@ -47,9 +47,9 @@ function onGive(mob, room, eventDetails) { } if (eventDetails.item) { - if (eventDetails.item.ItemId != 4) { + if (eventDetails.item.ItemId == 4) { mob.Command("say Finally! My winterfire crystal! Thank you so much!"); - user.GiveQuest("5-end"); + user.GetParty().GiveQuest("5-end"); return true; } } diff --git a/_datafiles/world/default/rooms/dark_forest/565.js b/_datafiles/world/default/rooms/dark_forest/565.js index f7e58363..aa762c20 100644 --- a/_datafiles/world/default/rooms/dark_forest/565.js +++ b/_datafiles/world/default/rooms/dark_forest/565.js @@ -75,13 +75,12 @@ function onCommand(cmd, rest, user, room) { if ( climbDown ) { - partyMembers = user.GetPartyMembers(); - + partyMembers = user.GetPartyPresent().GetMembers(); for( i = 0; i < partyMembers.length; i++ ) { a = partyMembers[i]; - if ( a.UserId() == user.UserId() ) { + if ( a.UserId() == user.UserId() ) { continue; } diff --git a/_datafiles/world/default/rooms/dark_forest/568.js b/_datafiles/world/default/rooms/dark_forest/568.js index 54a915ae..41c18c38 100644 --- a/_datafiles/world/default/rooms/dark_forest/568.js +++ b/_datafiles/world/default/rooms/dark_forest/568.js @@ -74,9 +74,8 @@ function onCommand(cmd, rest, user, room) { if ( climbDown ) { - - partyMembers = user.GetPartyMembers(); - + + partyMembers = user.GetPartyPresent().GetMembers(); for( i = 0; i < partyMembers.length; i++ ) { a = partyMembers[i]; diff --git a/_datafiles/world/default/rooms/frostfang/26.js b/_datafiles/world/default/rooms/frostfang/26.js index 19f591df..9c309ec1 100644 --- a/_datafiles/world/default/rooms/frostfang/26.js +++ b/_datafiles/world/default/rooms/frostfang/26.js @@ -17,7 +17,7 @@ function onCommand(cmd, rest, user, room) { SendUserMessage(user.UserId(), "You press the eyes of the raven, and follow a secret entrance to the west!"); SendRoomMessage(room.RoomId(), user.GetCharacterName(true)+" presses in the eyes of the raven, and falls through into a room to the west!", user.UserId()); - user.GiveQuest("2-investigate"); + user.GetParty().GiveQuest("2-investigate"); user.MoveRoom(31); return true; diff --git a/_datafiles/world/default/rooms/frostfang/276.js b/_datafiles/world/default/rooms/frostfang/276.js index 41f37e75..3e2055b5 100644 --- a/_datafiles/world/default/rooms/frostfang/276.js +++ b/_datafiles/world/default/rooms/frostfang/276.js @@ -57,7 +57,7 @@ function onCommand(cmd, rest, user, room) { SendRoomMessage(room.RoomId(), user.GetCharacterName(true)+" takes a golden locket from the pile of leaves.", user.UserId()); user.GiveItem(20025); - user.GiveQuest("1-return"); + user.GetParty().GiveQuest("1-return"); locketAvailableRound = roundNow + UtilGetMinutesToRounds(15); diff --git a/_datafiles/world/default/rooms/frostfang/35.js b/_datafiles/world/default/rooms/frostfang/35.js index 10ca4bb4..21ae873d 100644 --- a/_datafiles/world/default/rooms/frostfang/35.js +++ b/_datafiles/world/default/rooms/frostfang/35.js @@ -2,6 +2,12 @@ const magic_phrase = "zyphrial lumara vorthos"; +// EventFlags constants +const EventFlags = { + CmdSkipScripts: 1, + CmdBlockInputUntilComplete: 2 +}; + function onCommand_west(rest, user, room) { if ( !user.HasQuest("3-end") ) { @@ -49,7 +55,7 @@ function onCommand_say(rest, user, room) { return true; } - user.GiveQuest("3-end"); + user.GetParty().GiveQuest("3-end"); user.GiveBuff(3, "enchantment"); diff --git a/_datafiles/world/default/spells/sparks.js b/_datafiles/world/default/spells/sparks.js index 188510a0..d31f44b3 100644 --- a/_datafiles/world/default/spells/sparks.js +++ b/_datafiles/world/default/spells/sparks.js @@ -42,12 +42,12 @@ function onMagic(sourceActor, targetActors) { SendRoomMessage(roomId, sourceName+' stops chanting and lets loose a shower of sparks, hitting '+targetName+'.', sourceUserId, targetUserId); // Tell the target about the dmg - SendUserMessage(targetUserId, sourceName+' stops chanting fires a shower of sparks at you, hitting for '+dmgAmtStr+' damage.'); + SendUserMessage(targetUserId, sourceName+' stops chanting and fires a shower of sparks at you, hitting for '+dmgAmtStr+' damage.'); } else { // Tell the cast they did it to themselves - SendUserMessage(sourceUserId, 'You stop chanting and fires a shower of sparks at yourself, doing '+dmgAmtStr+' damage.'); + SendUserMessage(sourceUserId, 'You stop chanting and fire a shower of sparks at yourself, doing '+dmgAmtStr+' damage.'); // Tell the room about the dmg, except the source and target SendRoomMessage(roomId, sourceName+' stops chanting and fires a shower of sparks at themselves, hurting themselves.', sourceUserId, targetUserId); diff --git a/_datafiles/world/default/spells/tameskill.js b/_datafiles/world/default/spells/tameskill.js index 3218ece6..18d89bcb 100644 --- a/_datafiles/world/default/spells/tameskill.js +++ b/_datafiles/world/default/spells/tameskill.js @@ -91,7 +91,7 @@ function onWait(sourceActor, targetActor) { break; default: SendUserMessage(sourceActor.UserId(), 'You whistle several times, changing your pitch ever so slightly.'); - SendRoomMessage(sourceActor.GetRoomId(), ``+sourceActor.GetCharacterName(true)+' whistles several times, changing your pitch ever so slightly.', sourceActor.UserId()); + SendRoomMessage(sourceActor.GetRoomId(), ``+sourceActor.GetCharacterName(true)+' whistles several times, changing their pitch ever so slightly.', sourceActor.UserId()); } } diff --git a/internal/characters/character.go b/internal/characters/character.go index 081439df..21613a23 100644 --- a/internal/characters/character.go +++ b/internal/characters/character.go @@ -50,6 +50,7 @@ type Character struct { Description string // A description of the character. Adjectives []string `yaml:"adjectives,omitempty"` // Decorative text for the name of the character (e.g. "sleeping", "dead", "wounded") RoomId int // The room id the character is in. + RoomIdOnReset int // The room they are sent to if their RoomId isn't found. Zone string // The zone the character is in. The folder the room can be located in too. RaceId int // Character race Stats stats.Statistics // Character stats diff --git a/internal/hooks/PlayerSpawn_HandleJoin.go b/internal/hooks/PlayerSpawn_HandleJoin.go index 1072b778..127b6756 100644 --- a/internal/hooks/PlayerSpawn_HandleJoin.go +++ b/internal/hooks/PlayerSpawn_HandleJoin.go @@ -34,15 +34,34 @@ func HandleJoin(e events.Event) events.ListenerReturn { users.RemoveZombieUser(evt.UserId) room := rooms.LoadRoom(user.Character.RoomId) - if room == nil { + sendResetMessage := false + + // If an onreset room is set, send to it, then clear it. + // This way, if they were in an ephemeral chunk and it has been cleared, we + // handle sending them to whatever "reset" room was defined (if any) + if room == nil && user.Character.RoomIdOnReset != 0 { + mudlog.Warn("HandleJoin", "msg", fmt.Sprintf("room %d not found, trying RoomIdOnReset %d", user.Character.RoomId, user.Character.RoomIdOnReset)) + room = rooms.LoadRoom(user.Character.RoomIdOnReset) + + if room != nil { + sendResetMessage = true + user.Character.RoomId = user.Character.RoomIdOnReset + } + + user.Character.RoomIdOnReset = 0 + } + // If still no room was found, send to the zero room (default world start room) + if room == nil { mudlog.Error("EnterWorld", "error", fmt.Sprintf(`room %d not found`, user.Character.RoomId)) if err := rooms.MoveToRoom(user.UserId, 0); err != nil { mudlog.Error("EnterWorld", "msg", "could not move to room 0", "error", err) } + // Load whatever default room they have been assigned room = rooms.LoadRoom(user.Character.RoomId) + sendResetMessage = true } // TODO HERE @@ -62,6 +81,11 @@ func HandleJoin(e events.Event) events.ListenerReturn { } if room != nil { + + if sendResetMessage { + user.SendText("A portal opens before you and you feel an intense pulling... you can't escape it... You are transported elsewhere!") + } + if doLook, err := scripting.TryRoomScriptEvent(`onEnter`, user.UserId, user.Character.RoomId); err != nil || doLook { user.CommandFlagged(`look`, events.CmdSecretly) // Do a secret look. } diff --git a/internal/mapper/mapper.go b/internal/mapper/mapper.go index 044d5d50..a6c15492 100644 --- a/internal/mapper/mapper.go +++ b/internal/mapper/mapper.go @@ -998,15 +998,15 @@ func PreCacheMaps() { func validateRoomBiomes() { missingBiomeCount := 0 invalidBiomeCount := 0 - + for _, roomId := range rooms.GetAllRoomIds() { room := rooms.LoadRoom(roomId) if room == nil { continue } - + originalBiome := room.Biome - + // Check if room has no biome if originalBiome == "" { zoneBiome := rooms.GetZoneBiome(room.Zone) @@ -1022,7 +1022,7 @@ func validateRoomBiomes() { } } } - + if missingBiomeCount > 0 || invalidBiomeCount > 0 { mudlog.Info("Biome validation complete", "missing", missingBiomeCount, "invalid", invalidBiomeCount) } diff --git a/internal/procedural/gridmaze.go b/internal/procedural/gridmaze.go new file mode 100644 index 00000000..a72a583a --- /dev/null +++ b/internal/procedural/gridmaze.go @@ -0,0 +1,539 @@ +package procedural + +import ( + "hash/fnv" + "math/rand" + "time" +) + +// GridRoom implements the MazeRoom interface +type GridRoom struct { + x, y, z int + connections []*GridRoom + step int + distanceFromStart int + isStart bool + isEnd bool +} + +func (r *GridRoom) GetPosition() (int, int, int) { + return r.x, r.y, r.z +} + +func (r *GridRoom) GetConnections() []MazeRoom { + connections := make([]MazeRoom, len(r.connections)) + for i, conn := range r.connections { + connections[i] = conn + } + return connections +} + +func (r *GridRoom) IsConnectedTo(room MazeRoom) bool { + if room == nil { + return false + } + rx, ry, rz := room.GetPosition() + for _, conn := range r.connections { + if conn.x == rx && conn.y == ry && conn.z == rz { + return true + } + } + return false +} + +func (r *GridRoom) GetStep() int { + return r.step +} + +func (r *GridRoom) IsStart() bool { + return r.isStart +} + +func (r *GridRoom) IsEnd() bool { + return r.isEnd +} + +func (r *GridRoom) IsDeadEnd() bool { + return len(r.connections) == 1 +} + +func (r *GridRoom) GetDistanceFromStart() int { + return r.distanceFromStart +} + +func (r *GridRoom) addConnection(other *GridRoom) { + if r.IsConnectedTo(other) { + return + } + r.connections = append(r.connections, other) + other.connections = append(other.connections, r) +} + +// GridMaze implements the Maze interface +type GridMaze struct { + rooms Maze2D + startX, startY int + endX, endY int + criticalPath []*GridRoom + rand *rand.Rand +} + +// NewGridMaze creates a new grid maze generator +func NewGridMaze() *GridMaze { + return &GridMaze{ + rand: rand.New(rand.NewSource(time.Now().UnixNano())), + } +} + +func (m *GridMaze) Generate2D(xMax, yMax int, seed ...string) [][]*GridRoom { + // Set up random generator with seed if provided + if len(seed) > 0 && seed[0] != "" { + h := fnv.New64a() + h.Write([]byte(seed[0])) + m.rand = rand.New(rand.NewSource(int64(h.Sum64()))) + } + m.rooms = make(Maze2D, yMax) + for y := range m.rooms { + m.rooms[y] = make([]*GridRoom, xMax) + } + + // Create all rooms initially + m.createAllRooms(xMax, yMax) + + // Generate maze using recursive backtracking + m.generateMazeRecursiveBacktracking(xMax, yMax) + + // Set start and end points + m.setStartAndEnd(xMax, yMax) + + // Calculate distances from start + m.calculateDistancesFromStart() + + // Find critical path and set steps + m.findCriticalPath() + + // Add additional connections to create more branching and dead ends + m.addAdditionalConnections(xMax, yMax) + + // Remove some rooms randomly, but protect critical path and preserve dead ends + m.removeRoomsProtectingPath(xMax, yMax) + + // Remove any orphaned rooms (not reachable from start) + m.removeOrphanedRooms(xMax, yMax) + + // Recalculate after room removal and connection addition + m.calculateDistancesFromStart() + m.findCriticalPath() + + // Convert to interface slice + result := make([][]*GridRoom, yMax) + for y := range result { + result[y] = make([]*GridRoom, xMax) + for x := range result[y] { + if m.rooms[y][x] != nil { + result[y][x] = m.rooms[y][x] + } + } + } + + return result +} + +func (m *GridMaze) createAllRooms(xMax, yMax int) { + // Create all rooms initially - maze algorithm will determine connectivity + for x := 0; x < xMax; x++ { + for y := 0; y < yMax; y++ { + m.rooms[y][x] = &GridRoom{ + x: x, y: y, + connections: make([]*GridRoom, 0), + } + } + } +} + +func (m *GridMaze) setStartAndEnd(xMax, yMax int) { + // Collect all available rooms + availableRooms := make([][2]int, 0) + for x := 0; x < xMax; x++ { + for y := 0; y < yMax; y++ { + if m.rooms[y][x] != nil { + availableRooms = append(availableRooms, [2]int{x, y}) + } + } + } + + if len(availableRooms) == 0 { + return + } + + // Randomly select start position + startIdx := m.rand.Intn(len(availableRooms)) + m.startX, m.startY = availableRooms[startIdx][0], availableRooms[startIdx][1] + m.rooms[m.startY][m.startX].isStart = true + + if len(availableRooms) == 1 { + return + } + + // For end position, use a weighted random selection that favors distant rooms + // but still allows for some variety + weightedCandidates := make([]weightedCandidate, 0) + for _, pos := range availableRooms { + x, y := pos[0], pos[1] + if x == m.startX && y == m.startY { + continue // Skip start position + } + + // Calculate Manhattan distance from start + dist := abs(x-m.startX) + abs(y-m.startY) + // Weight increases with distance, but add base weight for variety + weight := dist + 1 // Base weight of 1 ensures all rooms have some chance + weightedCandidates = append(weightedCandidates, weightedCandidate{x: x, y: y, weight: weight}) + } + + if len(weightedCandidates) > 0 { + selected := m.selectWeightedRandom(weightedCandidates) + m.endX, m.endY = selected.x, selected.y + m.rooms[m.endY][m.endX].isEnd = true + } +} + +// weightedCandidate represents a position with an associated weight for selection +type weightedCandidate struct { + x, y int + weight int +} + +// selectWeightedRandom selects a candidate based on weighted probability +func (m *GridMaze) selectWeightedRandom(candidates []weightedCandidate) weightedCandidate { + // Calculate total weight + totalWeight := 0 + for _, candidate := range candidates { + totalWeight += candidate.weight + } + + // Select random value within total weight + randomValue := m.rand.Intn(totalWeight) + + // Find the candidate that corresponds to this random value + currentWeight := 0 + for _, candidate := range candidates { + currentWeight += candidate.weight + if randomValue < currentWeight { + return candidate + } + } + + // Fallback (should never reach here) + return candidates[len(candidates)-1] +} + +func (m *GridMaze) generateMazeRecursiveBacktracking(xMax, yMax int) { + // Recursive backtracking algorithm for maze generation + visited := make([][]bool, yMax) + for y := range visited { + visited[y] = make([]bool, xMax) + } + + // Start from a random position + startX := m.rand.Intn(xMax) + startY := m.rand.Intn(yMax) + + m.backtrack(startX, startY, visited, xMax, yMax) +} + +func (m *GridMaze) backtrack(x, y int, visited [][]bool, xMax, yMax int) { + visited[y][x] = true + + // Get all valid neighbors in random order + directions := [][2]int{{0, 1}, {1, 0}, {0, -1}, {-1, 0}} + m.shuffleDirections(directions) + + for _, dir := range directions { + nx, ny := x+dir[0], y+dir[1] + + // Check if neighbor is valid and unvisited + if nx >= 0 && nx < xMax && ny >= 0 && ny < yMax && !visited[ny][nx] { + // Connect current room to neighbor + m.rooms[y][x].addConnection(m.rooms[ny][nx]) + + // Recursively visit neighbor + m.backtrack(nx, ny, visited, xMax, yMax) + } + } +} + +func (m *GridMaze) shuffleDirections(directions [][2]int) { + for i := len(directions) - 1; i > 0; i-- { + j := m.rand.Intn(i + 1) + directions[i], directions[j] = directions[j], directions[i] + } +} + +func (m *GridMaze) removeRoom(x, y int) { + if m.rooms[y][x] == nil { + return + } + + // Remove all connections to this room + for _, conn := range m.rooms[y][x].connections { + for i, backConn := range conn.connections { + if backConn == m.rooms[y][x] { + conn.connections = append(conn.connections[:i], conn.connections[i+1:]...) + break + } + } + } + + // Remove the room + m.rooms[y][x] = nil +} + +func (m *GridMaze) removeRoomsProtectingPath(xMax, yMax int) { + // Create a set of critical path rooms to protect + criticalPathSet := make(map[*GridRoom]bool) + for _, room := range m.criticalPath { + criticalPathSet[room] = true + } + + // Remove some rooms + for x := 0; x < xMax; x++ { + for y := 0; y < yMax; y++ { + room := m.rooms[y][x] + if room == nil { + continue + } + + if criticalPathSet[room] { + continue + } + + if m.rand.Float32() < 0.5 { + // Only remove rooms that are not adjacent to critical path + if m.canRemoveWithoutDisconnectingPath(x, y) { + m.removeRoom(x, y) + } + } + + } + } +} + +func (m *GridMaze) canRemoveWithoutDisconnectingPath(x, y int) bool { + room := m.rooms[y][x] + if room == nil { + return false + } + + // Don't remove rooms that are directly on the critical path + if room.GetStep() > 0 { + return false + } + + // Don't remove rooms that would isolate other rooms + // Check if removing this room would create orphaned areas + if len(room.connections) > 1 { + // Temporarily remove connections and check connectivity + originalConnections := make([]*GridRoom, len(room.connections)) + copy(originalConnections, room.connections) + + // If this room bridges different areas, keep it + for i, conn1 := range room.connections { + for j, conn2 := range room.connections { + if i != j && !m.areConnectedWithoutRoom(conn1, conn2, room) { + return false + } + } + } + } + + return true +} + +func (m *GridMaze) removeOrphanedRooms(xMax, yMax int) { + if m.rooms[m.startY][m.startX] == nil { + return + } + + // Use BFS to find all rooms reachable from the start room + reachable := make(map[*GridRoom]bool) + queue := []*GridRoom{m.rooms[m.startY][m.startX]} + reachable[m.rooms[m.startY][m.startX]] = true + + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + + for _, conn := range current.connections { + if !reachable[conn] { + reachable[conn] = true + queue = append(queue, conn) + } + } + } + + // Remove any rooms that are not reachable from the start + for x := 0; x < xMax; x++ { + for y := 0; y < yMax; y++ { + room := m.rooms[y][x] + if room != nil && !reachable[room] { + m.removeRoom(x, y) + } + } + } +} + +func (m *GridMaze) calculateDistancesFromStart() { + if m.rooms[m.startY][m.startX] == nil { + return + } + + // Use BFS to calculate distances from start + queue := []*GridRoom{m.rooms[m.startY][m.startX]} + visited := make(map[*GridRoom]bool) + + m.rooms[m.startY][m.startX].distanceFromStart = 0 + visited[m.rooms[m.startY][m.startX]] = true + + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + + for _, conn := range current.connections { + if !visited[conn] { + visited[conn] = true + conn.distanceFromStart = current.distanceFromStart + 1 + queue = append(queue, conn) + } + } + } +} + +func (m *GridMaze) findCriticalPath() { + if m.rooms[m.startY][m.startX] == nil || m.rooms[m.endY][m.endX] == nil { + return + } + + // Use BFS to find shortest path + queue := []*GridRoom{m.rooms[m.startY][m.startX]} + visited := make(map[*GridRoom]bool) + parent := make(map[*GridRoom]*GridRoom) + + visited[m.rooms[m.startY][m.startX]] = true + + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + + if current == m.rooms[m.endY][m.endX] { + // Reconstruct path + path := make([]*GridRoom, 0) + for room := current; room != nil; room = parent[room] { + path = append([]*GridRoom{room}, path...) + } + + // Set steps + for i, room := range path { + room.step = i + 1 + } + + m.criticalPath = path + return + } + + for _, conn := range current.connections { + if !visited[conn] { + visited[conn] = true + parent[conn] = current + queue = append(queue, conn) + } + } + } +} + +func (m *GridMaze) GetStart() (int, int) { + return m.startX, m.startY +} + +func (m *GridMaze) GetEnd() (int, int) { + return m.endX, m.endY +} + +func (m *GridMaze) GetCriticalPath() []MazeRoom { + path := make([]MazeRoom, len(m.criticalPath)) + for i, room := range m.criticalPath { + path[i] = room + } + return path +} + +// areConnectedWithoutRoom checks if two rooms are connected without going through a specific room +func (m *GridMaze) areConnectedWithoutRoom(room1, room2, excludeRoom *GridRoom) bool { + if room1 == room2 { + return true + } + + visited := make(map[*GridRoom]bool) + queue := []*GridRoom{room1} + visited[room1] = true + visited[excludeRoom] = true // Exclude the room we're testing + + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + + if current == room2 { + return true + } + + for _, conn := range current.connections { + if !visited[conn] { + visited[conn] = true + queue = append(queue, conn) + } + } + } + + return false +} + +// addAdditionalConnections creates more branching paths to increase dead ends +func (m *GridMaze) addAdditionalConnections(xMax, yMax int) { + // Add some additional connections to create more complex paths + // This increases the number of potential dead ends + connectionAttempts := (xMax * yMax) / 10 // Attempt connections for 10% of grid size + + for i := 0; i < connectionAttempts; i++ { + x := m.rand.Intn(xMax) + y := m.rand.Intn(yMax) + + if m.rooms[y][x] == nil { + continue + } + + // Try to connect to a nearby room that's not already connected + directions := [][2]int{{0, 1}, {1, 0}, {0, -1}, {-1, 0}} + m.shuffleDirections(directions) + + for _, dir := range directions { + nx, ny := x+dir[0], y+dir[1] + + if nx >= 0 && nx < xMax && ny >= 0 && ny < yMax && + m.rooms[ny][nx] != nil && !m.rooms[y][x].IsConnectedTo(m.rooms[ny][nx]) { + + // Only add connection if it doesn't create too dense connectivity + if len(m.rooms[y][x].connections) < 3 && len(m.rooms[ny][nx].connections) < 3 { + m.rooms[y][x].addConnection(m.rooms[ny][nx]) + break + } + } + } + } +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} diff --git a/internal/procedural/gridmaze_test.go b/internal/procedural/gridmaze_test.go new file mode 100644 index 00000000..35bb7976 --- /dev/null +++ b/internal/procedural/gridmaze_test.go @@ -0,0 +1,326 @@ +package procedural + +import ( + "fmt" + "strings" + "testing" +) + +func TestGridMaze_Generate(t *testing.T) { + tests := []struct { + name string + xMax int + yMax int + seeds []string // Test with specific seeds for consistent results + }{ + { + name: "Very Small maze 5x5", + xMax: 5, + yMax: 5, + seeds: []string{"abcdef"}, + }, + { + name: "Small maze 8x8", + xMax: 8, + yMax: 8, + seeds: []string{"one"}, + }, + { + name: "Medium maze 12x12", + xMax: 12, + yMax: 12, + seeds: []string{"four"}, + }, + { + name: "Large maze 25x5", + xMax: 25, + yMax: 5, + seeds: []string{"five"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for i, seed := range tt.seeds { + t.Run(fmt.Sprintf("seed_%s", seed), func(t *testing.T) { + // Create maze with deterministic seed + maze := NewGridMaze() + + // Generate the maze + rooms := maze.Generate2D(tt.xMax, tt.yMax, seed) + + // Verify basic properties + if len(rooms) != tt.yMax { + t.Errorf("Expected %d rows, got %d", tt.yMax, len(rooms)) + } + + for y := range rooms { + if len(rooms[y]) != tt.xMax { + t.Errorf("Expected %d columns in row %d, got %d", tt.xMax, y, len(rooms[y])) + } + } + + // Verify start and end points exist + startX, startY := maze.GetStart() + endX, endY := maze.GetEnd() + + if rooms[startY][startX] == nil { + t.Error("Start room is nil") + } + if rooms[endY][endX] == nil { + t.Error("End room is nil") + } + + if !rooms[startY][startX].IsStart() { + t.Error("Start room doesn't report as start") + } + if !rooms[endY][endX].IsEnd() { + t.Error("End room doesn't report as end") + } + + // Verify critical path exists + criticalPath := maze.GetCriticalPath() + if len(criticalPath) == 0 { + t.Error("Critical path is empty") + } + + // Verify critical path starts at start and ends at end + if len(criticalPath) > 0 { + firstRoom := criticalPath[0] + lastRoom := criticalPath[len(criticalPath)-1] + + fx, fy, _ := firstRoom.GetPosition() + lx, ly, _ := lastRoom.GetPosition() + + if fx != startX || fy != startY { + t.Errorf("Critical path doesn't start at start room: expected (%d,%d), got (%d,%d)", startX, startY, fx, fy) + } + if lx != endX || ly != endY { + t.Errorf("Critical path doesn't end at end room: expected (%d,%d), got (%d,%d)", endX, endY, lx, ly) + } + } + + // Render the maze beautifully + rendered := renderMaze(rooms, maze, tt.xMax, tt.yMax) + + // Print the maze (this will show in test output with -v flag) + t.Logf("\n=== Maze %dx%d (seed: %s) ===\n%s\n", tt.xMax, tt.yMax, seed, rendered) + + // Verify maze has some complexity (not all rooms should exist due to removal) + roomCount := 0 + for x := 0; x < tt.xMax; x++ { + for y := 0; y < tt.yMax; y++ { + if rooms[y][x] != nil { + roomCount++ + } + } + } + + expectedMinRooms := (tt.xMax * tt.yMax) / 2 // At least half should remain + if roomCount < expectedMinRooms { + t.Logf("Warning: Maze seems sparse with only %d/%d rooms", roomCount, tt.xMax*tt.yMax) + } + + // Test connectivity of critical path + for i := 0; i < len(criticalPath)-1; i++ { + current := criticalPath[i] + next := criticalPath[i+1] + + if !current.IsConnectedTo(next) { + t.Errorf("Critical path broken: room at step %d not connected to step %d", i+1, i+2) + } + } + + // Only run first seed for large mazes to avoid too much output + if tt.xMax >= 16 && i > 0 { + return + } + }) + } + }) + } +} + +func TestGridMaze_Properties(t *testing.T) { + maze := NewGridMaze() + maze.rand.Seed(12345) // Deterministic for testing + + rooms := maze.Generate2D(10, 8) + + // Test room properties + roomsFound := 0 + deadEnds := 0 + criticalPathRooms := 0 + + var z = 0 + for x := 0; x < 10; x++ { + for y := 0; y < 8; y++ { + room := rooms[y][x] + if room == nil { + continue + } + + roomsFound++ + + // Test position consistency + rx, ry, rz := room.GetPosition() + if rx != x || ry != y || rz != z { + t.Errorf("Room position mismatch: expected (%d,%d,%d), got (%d,%d,%d)", x, y, z, rx, ry, rz) + } + + // Test connections are bidirectional + connections := room.GetConnections() + for _, conn := range connections { + if !conn.IsConnectedTo(room) { + t.Error("Connection is not bidirectional") + } + } + + // Count dead ends + if room.IsDeadEnd() { + deadEnds++ + } + + // Count critical path rooms + if room.GetStep() > 0 { + criticalPathRooms++ + } + + // Test distance from start is reasonable + dist := room.GetDistanceFromStart() + if dist < 0 { + t.Errorf("Negative distance from start: %d", dist) + } + } + } + + t.Logf("Maze statistics: %d rooms, %d dead ends, %d critical path rooms", roomsFound, deadEnds, criticalPathRooms) + + // Verify we have some rooms + if roomsFound == 0 { + t.Error("No rooms found in maze") + } + + // Verify critical path length matches what we found + criticalPath := maze.GetCriticalPath() + if len(criticalPath) != criticalPathRooms { + t.Errorf("Critical path length mismatch: expected %d, got %d", criticalPathRooms, len(criticalPath)) + } +} + +// renderMaze creates a beautiful ASCII representation of the maze +func renderMaze(rooms [][]*GridRoom, maze *GridMaze, xMax, yMax int) string { + var result strings.Builder + + // Create a larger grid to show walls and passages + displayWidth := xMax*4 + 2 + displayHeight := yMax*2 + 2 + display := make([][]rune, displayHeight) + for i := range display { + display[i] = make([]rune, displayWidth) + for j := range display[i] { + display[i][j] = ' ' + } + } + + // Fill in walls initially + for y := 0; y < displayHeight; y++ { + for x := 0; x < displayWidth; x++ { + display[y][x] = '█' + } + } + + // Get critical path for highlighting + criticalPath := maze.GetCriticalPath() + criticalPathSet := make(map[MazeRoom]bool) + for _, room := range criticalPath { + criticalPathSet[room] = true + } + + roundCount := 0 + // Process each room + for x := 0; x < xMax; x++ { + for y := 0; y < yMax; y++ { + room := rooms[y][x] + if room == nil { + continue + } + + roundCount++ + + // Calculate display position + displayX := x*4 + 2 + displayY := y*2 + 1 + + // Choose room symbol + var roomSymbol rune + if room.IsStart() { + roomSymbol = 'S' + } else if room.IsEnd() { + roomSymbol = 'E' + } else if criticalPathSet[room] { + roomSymbol = '.' // Highlight critical path + } else if room.IsDeadEnd() { + roomSymbol = '☠' + } else { + roomSymbol = ' ' + } + + // Place room symbol + display[displayY][displayX] = roomSymbol + display[displayY][displayX+1] = roomSymbol + + // Draw connections + connections := room.GetConnections() + for _, conn := range connections { + connX, connY, _ := conn.GetPosition() + + // Only draw if connection is to adjacent room to avoid duplicates + if connX == x && connY == y+1 { // Vert-Down + display[displayY+1][displayX] = ' ' + display[displayY+1][displayX+1] = ' ' + } else if connX == x+1 && connY == y { // Right + display[displayY][displayX+2] = ' ' + display[displayY][displayX+3] = ' ' + } + } + } + } + + // Convert display to string with nice borders + result.WriteString("┌") + for x := 0; x < displayWidth; x++ { + result.WriteString("─") + } + result.WriteString("┐\n") + + for y := 0; y < displayHeight; y++ { + result.WriteString("│") + for x := 0; x < displayWidth; x++ { + char := display[y][x] + if char == '█' { + result.WriteString("█") + } else { + result.WriteRune(char) + } + } + result.WriteString("│\n") + } + + result.WriteString("└") + for x := 0; x < displayWidth; x++ { + result.WriteString("─") + } + result.WriteString("┘\n") + + // Add legend + result.WriteString("\nLegend: SS=Start, EE=End, ..=Critical Path, ☠☠=Dead End, ██=Wall\n") + + // Add statistics + startX, startY := maze.GetStart() + endX, endY := maze.GetEnd() + result.WriteString(fmt.Sprintf("Start: (%d,%d), End: (%d,%d), Room Count: %d, Critical Path Length: %d\n", + startX, startY, endX, endY, roundCount, len(criticalPath))) + + return result.String() +} diff --git a/internal/procedural/procedural.go b/internal/procedural/procedural.go new file mode 100644 index 00000000..28a80eb7 --- /dev/null +++ b/internal/procedural/procedural.go @@ -0,0 +1,51 @@ +package procedural + +// [y][x] +type Maze2D [][]*GridRoom + +// [z][y][x] +type Maze3D [][][]*GridRoom + +// MazeRoom represents a single room/space in the maze +type MazeRoom interface { + // GetPosition returns the x, y, z coordinates of this room + GetPosition() (int, int, int) + + // GetConnections returns all rooms this room connects to + GetConnections() []MazeRoom + + // IsConnectedTo checks if this room is connected to another room + IsConnectedTo(room MazeRoom) bool + + // GetStep returns the step number on the critical path (0 if not on critical path) + GetStep() int + + // IsStart returns true if this is the start room + IsStart() bool + + // IsEnd returns true if this is the end room + IsEnd() bool + + // IsDeadEnd returns true if this room is a dead end (only one connection) + IsDeadEnd() bool + + // GetDistanceFromStart returns the number of steps from the start room + GetDistanceFromStart() int +} + +// Maze represents a 2D maze generator +type Maze interface { + // Generate creates a new maze with the specified dimensions + // Optional seed string can be provided for deterministic generation + // Returns a 2D slice where [x][y] contains a MazeRoom or nil + Generate2D(xMax, yMax int, seed ...string) Maze2D + + // GetStart returns the starting room position + GetStart() (int, int) + + // GetEnd returns the ending room position + GetEnd() (int, int) + + // GetCriticalPath returns the rooms on the critical path from start to end + GetCriticalPath() []MazeRoom +} diff --git a/internal/procedural/rooms.go b/internal/procedural/rooms.go new file mode 100644 index 00000000..218edf43 --- /dev/null +++ b/internal/procedural/rooms.go @@ -0,0 +1,150 @@ +package procedural + +import ( + "github.com/GoMudEngine/GoMud/internal/exit" + "github.com/GoMudEngine/GoMud/internal/rooms" +) + +func CreateEphemeralMaze2D(mazeRooms [][]*GridRoom) (allRoomIds []int, startRoomId int, endRoomId int) { + + startRoomId = 0 + endRoomId = 0 + + mazeH := len(mazeRooms) + mazeW := len(mazeRooms[0]) + + roomCt := 0 + mazeRoomToTmpRoomId := map[MazeRoom]int{} + + for x := 0; x < mazeW; x++ { + for y := 0; y < mazeH; y++ { + + mazeRoomNow := mazeRooms[y][x] + + if mazeRoomNow == nil { + continue + } + + roomCt++ + } + } + + allRoomIds, _ = rooms.CreateEmptyEphemeralRooms(roomCt) + + nextTmpRoomId := 0 + + for y := 0; y < mazeH; y++ { + for x := 0; x < mazeW; x++ { + + if mazeRooms[y][x] == nil { + continue + } + + tmpId1, ok := mazeRoomToTmpRoomId[mazeRooms[y][x]] + if !ok { + tmpId1 = allRoomIds[nextTmpRoomId] + mazeRoomToTmpRoomId[mazeRooms[y][x]] = tmpId1 + nextTmpRoomId++ + } + + r1 := rooms.LoadRoom(tmpId1) + + if mazeRooms[y][x].IsStart() { + startRoomId = tmpId1 + } else if mazeRooms[y][x].IsEnd() { + endRoomId = tmpId1 + } + + if y > 0 && mazeRooms[y-1][x] != nil { + if mazeRooms[y][x].IsConnectedTo(mazeRooms[y-1][x]) { + // Connect the rooms + tmpId2, ok := mazeRoomToTmpRoomId[mazeRooms[y-1][x]] + if !ok { + tmpId2 = allRoomIds[nextTmpRoomId] + mazeRoomToTmpRoomId[mazeRooms[y-1][x]] = tmpId2 + nextTmpRoomId++ + } + r2 := rooms.LoadRoom(tmpId2) + + r1.Exits["north"] = exit.RoomExit{ + RoomId: r2.RoomId, + MapDirection: "north", + } + r2.Exits["south"] = exit.RoomExit{ + RoomId: r1.RoomId, + MapDirection: "south", + } + } + } + + if x > 0 && mazeRooms[y][x-1] != nil { + if mazeRooms[y][x].IsConnectedTo(mazeRooms[y][x-1]) { + // Connect the rooms + tmpId2, ok := mazeRoomToTmpRoomId[mazeRooms[y][x-1]] + if !ok { + tmpId2 = allRoomIds[nextTmpRoomId] + mazeRoomToTmpRoomId[mazeRooms[y][x-1]] = tmpId2 + nextTmpRoomId++ + } + r2 := rooms.LoadRoom(tmpId2) + + r1.Exits["west"] = exit.RoomExit{ + RoomId: r2.RoomId, + MapDirection: "west", + } + r2.Exits["east"] = exit.RoomExit{ + RoomId: r1.RoomId, + MapDirection: "east", + } + } + } + + if y < mazeH-1 && mazeRooms[y+1][x] != nil { + + if mazeRooms[y][x].IsConnectedTo(mazeRooms[y+1][x]) { + // Connect the rooms + tmpId2, ok := mazeRoomToTmpRoomId[mazeRooms[y+1][x]] + if !ok { + tmpId2 = allRoomIds[nextTmpRoomId] + mazeRoomToTmpRoomId[mazeRooms[y+1][x]] = tmpId2 + nextTmpRoomId++ + } + r2 := rooms.LoadRoom(tmpId2) + + r1.Exits["south"] = exit.RoomExit{ + RoomId: r2.RoomId, + MapDirection: "south", + } + r2.Exits["north"] = exit.RoomExit{ + RoomId: r1.RoomId, + MapDirection: "north", + } + } + } + if x < mazeW-1 && mazeRooms[y][x+1] != nil { + if mazeRooms[y][x].IsConnectedTo(mazeRooms[y][x+1]) { + // Connect the rooms + tmpId2, ok := mazeRoomToTmpRoomId[mazeRooms[y][x+1]] + if !ok { + tmpId2 = allRoomIds[nextTmpRoomId] + mazeRoomToTmpRoomId[mazeRooms[y][x+1]] = tmpId2 + nextTmpRoomId++ + } + r2 := rooms.LoadRoom(tmpId2) + + r1.Exits["east"] = exit.RoomExit{ + RoomId: r2.RoomId, + MapDirection: "east", + } + r2.Exits["west"] = exit.RoomExit{ + RoomId: r1.RoomId, + MapDirection: "west", + } + } + } + + } + } + + return allRoomIds, startRoomId, endRoomId +} diff --git a/internal/rooms/ephemeral.go b/internal/rooms/ephemeral.go index f12121f5..57beaf3e 100644 --- a/internal/rooms/ephemeral.go +++ b/internal/rooms/ephemeral.go @@ -12,7 +12,7 @@ import ( const ( ephemeralChunksLimit = 100 // The maximum number of ephemeral chunks that can be created - ephemeralChunkSize = 250 // The maximum quantity of ephemeral room's that can be copied/created in a given chunk. + ephemeralChunkSize = 1000 // The maximum quantity of ephemeral room's that can be copied/created in a given chunk. roomIdMin32Bit = 1000000 // 1,000,000 - Safe for 32-bit systems (was 1,000,000,000 which overflows when multiplied by 1000) ) @@ -22,7 +22,9 @@ var ( originalRoomIdLookups = map[int]int{} // a map of ephemeralId's to their original RoomId's, for special purposes // errors errNoRoomIdsProvided = errors.New(`no RoomId's were provided`) + errInvalidRoomQuantity = errors.New(`one or more empty rooms must be requested.`) errRoomNotFound = errors.New(`the requested RoomId wasn't found`) + errZoneNotFound = errors.New(`the requested zone wasn't found`) errEphemeralChunkLimit = fmt.Errorf(`the ephemeral chunk limit of %d has been reached.`, ephemeralChunksLimit) errEphemeralRoomLimit = fmt.Errorf(`the ephemeral room request limit of %d is exceeded.`, ephemeralChunkSize) errNonUniqueRoomId = errors.New(`a RoomId has been provided more than once. they must all be unique`) @@ -38,6 +40,16 @@ func GetChunkCount() int { return result } +// Looks at chunk array and returns the first unused/empty index, or error if none found. +func getNextAvailableChunk() (int, error) { + for i := 0; i < ephemeralChunksLimit; i++ { + if len(ephemeralRoomChunks[i]) == 0 { + return i, nil + } + } + return -1, errEphemeralChunkLimit +} + // Looks for any ephemeralRoomId's that exits for the given roomId. // Returns a slice containing all found ephemeralIds func FindEphemeralRoomIds(roomId int) []int { @@ -52,19 +64,64 @@ func FindEphemeralRoomIds(roomId int) []int { return allEphemeralRoomIds } -// accepts RoomId's as arguments, and creates ephemeral copies of them, returning the new ID's of the copies. -func CreateEphemeralRoomIds(roomIds ...int) (map[int]int, error) { +// Accepts a quantity and returns roomId's for a chunk of empty rooms +// This is a special use function for dynamically building ephemeral rooms in code +func CreateEmptyEphemeralRooms(qty int) ([]int, error) { + + if qty > ephemeralChunkSize { + return []int{}, errEphemeralRoomLimit + } - ephemeralRooms := map[int]int{} + if qty < 1 { + return []int{}, errInvalidRoomQuantity + } + + ephemeralRoomIds := make([]int, 0, qty) + + // First find a chunk ID + chunkId, err := getNextAvailableChunk() + if err != nil { + return []int{}, err + } + + for i := 0; i < qty; i++ { + room := NewEmptyRoom() + room.RoomId = ephemeralRoomIdMinimum + (chunkId * ephemeralChunkSize) + i + + // Save the original room ID in case we need it at some point + originalRoomIdLookups[room.RoomId] = 0 + + addRoomToMemory(room) + ephemeralRoomIds = append(ephemeralRoomIds, room.RoomId) + } + + trackedRoomIds := make([]int, len(ephemeralRoomIds)) + copy(trackedRoomIds, ephemeralRoomIds) + + ephemeralRoomChunks[chunkId] = trackedRoomIds + + mudlog.Info("CreateEmptyEphemeral...()", + "created", len(trackedRoomIds), + "chunkId", chunkId, + "Ephemeral RoomIds", fmt.Sprintf("%d - %d", ephemeralRoomIds[0], ephemeralRoomIds[len(ephemeralRoomIds)-1]), + "Chunks Remaining", ephemeralChunksLimit-GetChunkCount()) + + return ephemeralRoomIds, nil +} + +// accepts RoomId's as arguments, and creates ephemeral copies of them, returning the new ID's of the copies. +func CreateEphemeralRoomIds(roomIds ...int) (map[int]int, error) { if len(roomIds) == 0 { - return ephemeralRooms, errNoRoomIdsProvided + return map[int]int{}, errNoRoomIdsProvided } if len(roomIds) > ephemeralChunkSize { - return ephemeralRooms, errEphemeralRoomLimit + return map[int]int{}, errEphemeralRoomLimit } + ephemeralRooms := make(map[int]int, len(roomIds)) + // Make sure that all values in the roomIds slice are unique. roomIdReplacements := map[int]int{} // original=>ephemeral replacements for _, roomId := range roomIds { @@ -74,17 +131,14 @@ func CreateEphemeralRoomIds(roomIds ...int) (map[int]int, error) { roomIdReplacements[roomId] = 0 } - // First reserve the chunk - chunkId := -1 - for i := 0; i < ephemeralChunksLimit; i++ { - if len(ephemeralRoomChunks[i]) == 0 { - chunkId = i - break - } + // First find a chunk ID + chunkId, err := getNextAvailableChunk() + if err != nil { + return map[int]int{}, err } ephemeralRoomIds := []int{} - for idx, roomId := range roomIds { + for i, roomId := range roomIds { // Load only data from the template if roomId == 0 { @@ -96,7 +150,7 @@ func CreateEphemeralRoomIds(roomIds ...int) (map[int]int, error) { continue } - room.RoomId = ephemeralRoomIdMinimum + (chunkId * ephemeralChunkSize) + idx + room.RoomId = ephemeralRoomIdMinimum + (chunkId * ephemeralChunkSize) + i // Save the original room ID in case we need it at some point originalRoomIdLookups[room.RoomId] = roomId @@ -132,7 +186,7 @@ func CreateEphemeralRoomIds(roomIds ...int) (map[int]int, error) { "created", len(ephemeralRoomIds), "chunkId", chunkId, "Ephemeral RoomIds", fmt.Sprintf("%d - %d", ephemeralRoomIds[0], ephemeralRoomIds[len(ephemeralRoomIds)-1]), - "Chunks Remaining", GetChunkCount()) + "Chunks Remaining", ephemeralChunksLimit-GetChunkCount()) return ephemeralRooms, nil } @@ -140,6 +194,10 @@ func CreateEphemeralRoomIds(roomIds ...int) (map[int]int, error) { // accepts RoomId's as arguments, and creates ephemeral copies of them, returning the new ID's of the copies. func CreateEphemeralZone(zoneName string) (map[int]int, error) { + if _, exists := roomManager.zones[zoneName]; !exists { + return nil, errZoneNotFound + } + roomIds := make([]int, len(roomManager.zones[zoneName].RoomIds)) idx := 0 diff --git a/internal/rooms/roommanager.go b/internal/rooms/roommanager.go index eb2afd65..d76a1f2c 100644 --- a/internal/rooms/roommanager.go +++ b/internal/rooms/roommanager.go @@ -264,6 +264,8 @@ func MoveToRoom(userId int, toRoomId int, isSpawn ...bool) error { // Put them in their own instance of it. deathRecoveryRoomId := int(cfg.DeathRecoveryRoom) if toRoomId == deathRecoveryRoomId { + // Set their reset room id to death recovery. + user.Character.RoomIdOnReset = deathRecoveryRoomId if newRooms, err := CreateEphemeralRoomIds(deathRecoveryRoomId); err == nil { toRoomId = newRooms[deathRecoveryRoomId] } @@ -325,6 +327,32 @@ func MoveToRoom(userId int, toRoomId int, isSpawn ...bool) error { } } } + + // + // If they are moving into an ephemeral room + // + if IsEphemeralRoomId(newRoom.RoomId) { + + if user.Character.RoomIdOnReset == 0 { + // + // If their previous room was non ephemeral, set it to their reset room by default + // This can be overridden manually by scripts/coding if/when needed. + // + if !IsEphemeralRoomId(user.Character.RoomId) { + user.Character.RoomIdOnReset = user.Character.RoomId + } + } + + } else { + // + // If their RoomIdOnReset is non zero and they are moving to a non ephemeral room + // Clear their RoomIdOnReset. + // + if user.Character.RoomIdOnReset != 0 { + user.Character.RoomIdOnReset = 0 + } + } + // // Done adding mutator buffs // diff --git a/internal/rooms/rooms.go b/internal/rooms/rooms.go index 49906960..b167e361 100644 --- a/internal/rooms/rooms.go +++ b/internal/rooms/rooms.go @@ -122,6 +122,19 @@ func NewRoom(zone string) *Room { return r } +func NewEmptyRoom() *Room { + r := &Room{ + Title: "An empty room.", + Description: "This is an empty room that was never given a description.", + MapSymbol: ``, + Exits: make(map[string]exit.RoomExit), + players: []int{}, + visitors: make(map[VisitorType]map[int]uint64), + tempDataStore: make(map[string]any), + } + return r +} + func (r *Room) IsEphemeral() bool { return r.RoomId >= ephemeralRoomIdMinimum } @@ -2198,7 +2211,6 @@ func (r *Room) Validate() error { } } - // Make sure all items are validated (and have uids) for i := range r.Items { r.Items[i].Validate() diff --git a/internal/scripting/actor_func.go b/internal/scripting/actor_func.go index f1260099..93b478c5 100644 --- a/internal/scripting/actor_func.go +++ b/internal/scripting/actor_func.go @@ -9,7 +9,6 @@ import ( "github.com/GoMudEngine/GoMud/internal/configs" "github.com/GoMudEngine/GoMud/internal/events" "github.com/GoMudEngine/GoMud/internal/mobs" - "github.com/GoMudEngine/GoMud/internal/parties" "github.com/GoMudEngine/GoMud/internal/pets" "github.com/GoMudEngine/GoMud/internal/races" "github.com/GoMudEngine/GoMud/internal/rooms" @@ -104,6 +103,13 @@ func (a ScriptActor) GetStat(statName string) int { return 0 } +func (a ScriptActor) SetResetRoomId(roomId int) { + if a.userRecord == nil { + return + } + a.userRecord.Character.RoomIdOnReset = roomId +} + func (a ScriptActor) SetTempData(key string, value any) { if a.userRecord != nil { @@ -203,72 +209,41 @@ func (a ScriptActor) HasQuest(questId string) bool { func (a ScriptActor) GiveQuest(questId string) { - if a.userRecord != nil { - // If in a party, give to all party members. - if party := parties.Get(a.userId); party != nil { - for _, userId := range party.GetMembers() { - - events.AddToQueue(events.Quest{ - UserId: userId, - QuestToken: questId, - }) - - } - return - } else { - - events.AddToQueue(events.Quest{ - UserId: a.userId, - QuestToken: questId, - }) - - } - } - //a.characterRecord.GiveQuestToken(questId) + events.AddToQueue(events.Quest{ + UserId: a.UserId(), + QuestToken: questId, + }) } -func (a ScriptActor) GetPartyMembers() []ScriptActor { - - partyMembers := []ScriptActor{} - partyUserId := 0 - - if a.userRecord == nil { - if a.mobRecord.Character.Charmed == nil { - return partyMembers - } - - partyUserId = a.mobRecord.Character.Charmed.UserId - } else { - partyUserId = a.userId +// Gets a party object that indexes ALL members of the party +func (a ScriptActor) GetParty(excludeSelf ...bool) ScriptParty { + return ScriptParty{ + actor: &a, + includePresent: true, + includeMissing: true, + includeSelf: len(excludeSelf) == 0 || !excludeSelf[0], } +} - if partyUserId < 1 { - return partyMembers - } - - // If in a party, give to all party members. - if party := parties.Get(partyUserId); party != nil { - for _, userId := range party.GetMembers() { - - if a := GetActor(userId, 0); a != nil { - partyMembers = append(partyMembers, *a) - } - - } +// Gets a party object that indexes only party members in the same room +func (a ScriptActor) GetPartyPresent(excludeSelf ...bool) ScriptParty { + return ScriptParty{ + actor: &a, + includePresent: true, + includeMissing: false, + includeSelf: len(excludeSelf) == 0 || !excludeSelf[0], } +} - mobPartyMembers := []ScriptActor{} - - for _, char := range partyMembers { - for _, mobInstId := range char.characterRecord.GetCharmIds() { - if a := GetActor(0, mobInstId); a != nil { - mobPartyMembers = append(mobPartyMembers, *a) - } - } +// Gets a party object that indexes only members NOT in the same room. +func (a ScriptActor) GetPartyMissing() ScriptParty { + return ScriptParty{ + actor: &a, + includePresent: false, + includeMissing: true, + includeSelf: false, } - - return append(partyMembers, mobPartyMembers...) } func (a ScriptActor) AddGold(amt int, bankAmt ...int) { @@ -364,7 +339,7 @@ func (a ScriptActor) GetSkillLevel(skillName string) int { return a.characterRecord.GetSkillLevel(skills.SkillTag(skillName)) } -func (a ScriptActor) MoveRoom(destRoomId int, leaveCharmedMobs ...bool) { +func (a ScriptActor) MoveRoom(destRoomId int) { if a.userRecord != nil { @@ -374,11 +349,9 @@ func (a ScriptActor) MoveRoom(destRoomId int, leaveCharmedMobs ...bool) { rooms.MoveToRoom(a.userId, destRoomId) - if len(leaveCharmedMobs) < 1 || !leaveCharmedMobs[0] { - for _, mobInstId := range a.characterRecord.GetCharmIds() { - rmNow.RemoveMob(mobInstId) - rmNext.AddMob(mobInstId) - } + for _, mobInstId := range a.characterRecord.GetCharmIds() { + rmNow.RemoveMob(mobInstId) + rmNext.AddMob(mobInstId) } if doLook, err := TryRoomScriptEvent(`onEnter`, a.userRecord.UserId, destRoomId); err != nil || doLook { diff --git a/internal/scripting/party_func.go b/internal/scripting/party_func.go new file mode 100644 index 00000000..b66dc795 --- /dev/null +++ b/internal/scripting/party_func.go @@ -0,0 +1,240 @@ +package scripting + +import ( + "github.com/GoMudEngine/GoMud/internal/parties" +) + +type ScriptParty struct { + actor *ScriptActor + includeSelf bool + includePresent bool + includeMissing bool +} + +// GetMembers() is the core feature of this, everything works off of it. +func (p ScriptParty) GetMembers() []ScriptActor { + + partyMembers := []ScriptActor{} + addedUsers := map[int]struct{}{} // Track so we never accidentally add someone twice + addedMobs := map[int]struct{}{} // Track so we never accidentally add someone twice + + // Usually we would include self, but just in case... + if p.includeSelf { + + partyMembers = append(partyMembers, *p.actor) + + if p.actor.characterRecord != nil { + addedUsers[p.actor.userId] = struct{}{} + } else if p.actor.mobRecord != nil { + addedMobs[p.actor.mobInstanceId] = struct{}{} + } + } + + partyUserId := p.actor.UserId() + sourceRoomId := p.actor.GetRoomId() + + if p.actor.userRecord == nil { + if p.actor.mobRecord.Character.Charmed == nil { + return partyMembers + } + + partyUserId = p.actor.mobRecord.Character.Charmed.UserId + } + + if partyUserId < 1 { + return partyMembers + } + + // If in a party, give to all party members. + if party := parties.Get(partyUserId); party != nil { + for _, userId := range party.GetMembers() { + + if _, ok := addedUsers[userId]; ok { + continue + } + + if a := GetActor(userId, 0); a != nil { + + if a.GetRoomId() == sourceRoomId { + if !p.includePresent { + continue + } + } else { + if !p.includeMissing { + continue + } + } + + partyMembers = append(partyMembers, *a) + addedUsers[userId] = struct{}{} + + } + + } + } + + mobPartyMembers := []ScriptActor{} + + // Add all charmed mobs for all party members, too. + for _, char := range partyMembers { + for _, mobInstId := range char.characterRecord.GetCharmIds() { + + if _, ok := addedMobs[mobInstId]; ok { + continue + } + + if a := GetActor(0, mobInstId); a != nil { + + if a.GetRoomId() == sourceRoomId { + if !p.includePresent { + continue + } + } else { + if !p.includeMissing { + continue + } + } + + mobPartyMembers = append(mobPartyMembers, *a) + addedMobs[mobInstId] = struct{}{} + + } + } + } + + return append(partyMembers, mobPartyMembers...) +} + +// +// Simple helper to loop through all members and apply a function +// + +func (p ScriptParty) each(fn func(ScriptActor)) { + allParty := p.GetMembers() + for _, a := range allParty { + fn(a) + } +} + +// +// What follows are many functions graduated from the actor level to the party level +// + +func (p ScriptParty) SendText(msg string) { + p.each(func(a ScriptActor) { + a.SendText(msg) + }) +} + +func (p ScriptParty) SetResetRoomId(roomId int) { + p.each(func(a ScriptActor) { + if a.userRecord == nil { + return + } + a.userRecord.Character.RoomIdOnReset = roomId + }) +} + +func (p ScriptParty) GiveQuest(questId string) { + p.each(func(a ScriptActor) { + a.GiveQuest(questId) + }) +} + +func (p ScriptParty) AddGold(amt int, bankAmt ...int) { + p.each(func(a ScriptActor) { + a.AddGold(amt, bankAmt...) + }) +} +func (p ScriptParty) AddHealth(amt int) { + p.each(func(a ScriptActor) { + a.AddHealth(amt) + }) +} +func (p ScriptParty) AddMana(amt int) { + p.each(func(a ScriptActor) { + a.AddMana(amt) + }) +} +func (p ScriptParty) Command(cmd string, waitSeconds ...float64) { + p.each(func(a ScriptActor) { + a.Command(cmd, waitSeconds...) + }) +} +func (p ScriptParty) TrainSkill(skillName string, skillLevel int) { + p.each(func(a ScriptActor) { + a.TrainSkill(skillName, skillLevel) + }) +} +func (p ScriptParty) MoveRoom(destRoomId int) { + p.each(func(a ScriptActor) { + a.MoveRoom(destRoomId) + }) +} +func (p ScriptParty) AddEventLog(category string, message string) { + p.each(func(a ScriptActor) { + a.AddEventLog(category, message) + }) +} + +func (p ScriptParty) GiveBuff(buffId int, source string) { + p.each(func(a ScriptActor) { + a.GiveBuff(buffId, source) + }) +} +func (p ScriptParty) CancelBuffWithFlag(buffFlag string) { + p.each(func(a ScriptActor) { + a.CancelBuffWithFlag(buffFlag) + }) +} +func (p ScriptParty) RemoveBuff(buffId int) { + p.each(func(a ScriptActor) { + a.RemoveBuff(buffId) + }) +} +func (p ScriptParty) ChangeAlignment(alignmentChange int) { + p.each(func(a ScriptActor) { + a.ChangeAlignment(alignmentChange) + }) +} +func (p ScriptParty) LearnSpell(spellId string) { + p.each(func(a ScriptActor) { + a.LearnSpell(spellId) + }) +} +func (p ScriptParty) SetHealth(amt int) { + p.each(func(a ScriptActor) { + a.SetHealth(amt) + }) +} +func (p ScriptParty) SetAdjective(adj string, addIt bool) { + p.each(func(a ScriptActor) { + a.SetAdjective(adj, addIt) + }) +} +func (p ScriptParty) GiveTrainingPoints(ct int) { + p.each(func(a ScriptActor) { + a.GiveTrainingPoints(ct) + }) +} +func (p ScriptParty) GiveStatPoints(ct int) { + p.each(func(a ScriptActor) { + a.GiveStatPoints(ct) + }) +} +func (p ScriptParty) GiveExtraLife() { + p.each(func(a ScriptActor) { + a.GiveExtraLife() + }) +} + +func (p ScriptParty) GrantXP(xpAmt int, reason string) { + p.each(func(a ScriptActor) { + a.GrantXP(xpAmt, reason) + }) +} +func (p ScriptParty) TimerSet(name string, period string) { + p.each(func(a ScriptActor) { + a.TimerSet(name, period) + }) +} diff --git a/internal/scripting/room_func.go b/internal/scripting/room_func.go index 86321c88..183bd472 100644 --- a/internal/scripting/room_func.go +++ b/internal/scripting/room_func.go @@ -25,6 +25,7 @@ import ( func setRoomFunctions(vm *goja.Runtime) { vm.Set(`GetRoom`, GetRoom) vm.Set(`GetMap`, GetMap) + vm.Set(`CreateEmptyRoomInstances`, CreateEmptyRoomInstances) vm.Set(`CreateInstancesFromRoomIds`, CreateInstancesFromRoomIds) vm.Set(`CreateInstancesFromZone`, CreateInstancesFromZone) } @@ -386,7 +387,12 @@ func (r ScriptRoom) IsEphemeral() bool { // # These functions get exported to the scripting engine // // //////////////////////////////////////////////////////// -func CreateInstancesFromRoomIds(roomList []int) map[int]int { +func CreateEmptyRoomInstances(quantity int) []int { + ret, _ := rooms.CreateEmptyEphemeralRooms(quantity) + return ret +} + +func CreateInstancesFromRoomIds(roomList ...int) map[int]int { ret, _ := rooms.CreateEphemeralRoomIds(roomList...) return ret }