Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions _datafiles/guides/building/scripting/FUNCTIONS_PETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ take effect immediately and are persisted the next time the character is saved.
- [PetObject.GetCapacity() int](#petobjectgetcapacity-int)
- [PetObject.ItemCount() int](#petobjectitemcount-int)
- [PetObject.HasScript() bool](#petobjecthasscript-bool)
- [PetObject.IsMissing() bool](#petobjectismissing-bool)
- [PetObject.GoMissing(rounds int)](#petobjectgomissingrounds-int)

---

Expand Down Expand Up @@ -200,3 +202,54 @@ Returns `true` if this pet type has a script file on disk.

Useful in generic scripts that want to check whether a pet will respond to
events before attempting to trigger them.

---

## [PetObject.IsMissing() bool](/internal/scripting/pet_func.go)
Returns `true` when the pet is temporarily absent (`MissingCountdown > 0`).

While missing, the pet does not appear in room descriptions, does not
participate in combat, does not contribute stat or buff bonuses, and does not
respond to commands or `PetAct` ticks.

**Example:**
```javascript
var pet = actor.GetPet();
if (pet !== null && pet.IsMissing()) {
actor.SendText('Your pet has wandered off somewhere...');
}
```

---

## [PetObject.GoMissing(rounds int)](/internal/scripting/pet_func.go)
Causes the pet to go absent for the given number of rounds, or returns it
immediately when called with `0`.

- **Positive value**: sets the countdown, fires `PetLeave()`, and hides the
pet from all game systems until the countdown reaches zero.
- **Zero**: clears the countdown immediately and fires `PetReturn()`. Has no
effect if the pet is not currently missing.

| Argument | Explanation |
| --- | --- |
| rounds | Rounds to be absent, or `0` to return immediately. Negative values are treated as `0`. |

**Example:**
```javascript
function PetAct(pet, actor, room) {
// 1% chance per round for the pet to wander off for 10 rounds
if (RandInt(1, 100) === 1) {
pet.GoMissing(10);
}
}

// Return the pet early from a script command
function onCommand_recall(rest, pet, actor, room) {
if (pet.IsMissing()) {
pet.GoMissing(0);
actor.SendText('Your pet bounds back to your side!');
}
return true;
}
```
57 changes: 57 additions & 0 deletions _datafiles/guides/building/scripting/SCRIPTING_PETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,60 @@ function onCommand_pet(rest, pet, actor, room) {
return false;
}
```

---

```javascript
function PetLeave(pet, actor, room) {
}
```

`PetLeave()` is called immediately when
[PetObject.GoMissing()](FUNCTIONS_PETS.md#petobjectgomissingrounds-int) is
invoked. Use it to emit a message or trigger effects when the pet disappears.

The pet is already marked as missing when this fires, so `pet.IsMissing()`
returns `true` inside the handler.

There is no return value.

| Argument | Explanation |
| --- | --- |
| pet | [PetObject](FUNCTIONS_PETS.md) — the pet. |
| actor | [ActorObject](FUNCTIONS_ACTORS.md) — the player who owns the pet. |
| room | [RoomObject](FUNCTIONS_ROOMS.md) — the room both are in. |

**Example:**
```javascript
function PetLeave(pet, actor, room) {
room.SendText(pet.NameSimple() + ' darts into the shadows and disappears!');
}
```

---

```javascript
function PetReturn(pet, actor, room) {
}
```

`PetReturn()` is called the round the pet's `MissingCountdown` reaches zero.
Use it to announce the pet's return or apply any effects.

The countdown has already reached zero when this fires, so `pet.IsMissing()`
returns `false` inside the handler.

There is no return value.

| Argument | Explanation |
| --- | --- |
| pet | [PetObject](FUNCTIONS_PETS.md) — the pet. |
| actor | [ActorObject](FUNCTIONS_ACTORS.md) — the player who owns the pet. |
| room | [RoomObject](FUNCTIONS_ROOMS.md) — the room both are in. |

**Example:**
```javascript
function PetReturn(pet, actor, room) {
room.SendText(pet.NameSimple() + ' trots back to your side.');
}
```
8 changes: 6 additions & 2 deletions internal/characters/character.go
Original file line number Diff line number Diff line change
Expand Up @@ -1442,7 +1442,11 @@ func (c *Character) MovementCost() int {
}

func (c *Character) StatMod(statName string) int {
return c.Equipment.StatMod(statName) + c.Buffs.StatMod(statName) + c.Pet.StatMod(statName)
petMod := 0
if !c.Pet.IsMissing() {
petMod = c.Pet.StatMod(statName)
}
return c.Equipment.StatMod(statName) + c.Buffs.StatMod(statName) + petMod
}

// returns true if something has changed.
Expand Down Expand Up @@ -2262,7 +2266,7 @@ func (c *Character) reapplyPermabuffs(removedItems ...items.Item) {
}

// Apply any buffs from pet
if c.Pet.Exists() {
if c.Pet.Exists() && !c.Pet.IsMissing() {
for _, buffId := range c.Pet.GetBuffs() {
buffIdCount[buffId] = 100 // Don't allow pet buffs to be removed, keep this number high
}
Expand Down
60 changes: 31 additions & 29 deletions internal/combat/combat.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,52 +405,54 @@ func calculateCombat(sourceChar characters.Character, targetChar characters.Char
}

// Pet has a 20% chance per attack round to join the fight (once, regardless of weapon count)
chance, petDmg := sourceChar.Pet.GetEffectiveDamage()
if chance > 0 && util.RollDice(1, chance) <= chance {
if sourceChar.RoomId == targetChar.RoomId {
if sourceChar.Pet.Exists() && petDmg.DiceRoll != `` {
if sourceChar.Pet.Exists() && !sourceChar.Pet.IsMissing() {
chance, petDmg := sourceChar.Pet.GetEffectiveDamage()
if chance > 0 && util.RollDice(1, chance) <= chance {
if sourceChar.RoomId == targetChar.RoomId {
if petDmg.DiceRoll != `` {

pAttacks, pDCount, pDSides, pDBonus, critBuffs := sourceChar.Pet.GetDiceRoll()
combatMsgs := sourceChar.Pet.GetCombatMessages(string(targetType))
pAttacks, pDCount, pDSides, pDBonus, critBuffs := sourceChar.Pet.GetDiceRoll()
combatMsgs := sourceChar.Pet.GetCombatMessages(string(targetType))

for p := 0; p < pAttacks; p++ {
for p := 0; p < pAttacks; p++ {

if !Hits(0, targetChar.Stats.Speed.ValueAdj, 0) {
targetDisplayName := fmt.Sprintf(`<ansi fg="%sname">%s</ansi>`, string(targetType), targetChar.Name)
toAttackerMsg := combatMsgs.ApplyTokens(combatMsgs.Miss, sourceChar.Pet.DisplayName(), 0, targetDisplayName)
attackResult.SendToSource(toAttackerMsg)
continue
}
if !Hits(0, targetChar.Stats.Speed.ValueAdj, 0) {
targetDisplayName := fmt.Sprintf(`<ansi fg="%sname">%s</ansi>`, string(targetType), targetChar.Name)
toAttackerMsg := combatMsgs.ApplyTokens(combatMsgs.Miss, sourceChar.Pet.DisplayName(), 0, targetDisplayName)
attackResult.SendToSource(toAttackerMsg)
continue
}

attackTargetDamage := util.RollDice(pDCount, pDSides) + pDBonus
attackTargetDamage := util.RollDice(pDCount, pDSides) + pDBonus

attackTargetDamage, _ = applyDefenseReduction(attackTargetDamage, targetChar.GetDefense())
attackTargetDamage, _ = applyDefenseReduction(attackTargetDamage, targetChar.GetDefense())

attackResult.DamageToTarget += attackTargetDamage
attackResult.DamageToTarget += attackTargetDamage

targetDisplayName := fmt.Sprintf(`<ansi fg="%sname">%s</ansi>`, string(targetType), targetChar.Name)
petDisplayName := sourceChar.Pet.DisplayName()
targetDisplayName := fmt.Sprintf(`<ansi fg="%sname">%s</ansi>`, string(targetType), targetChar.Name)
petDisplayName := sourceChar.Pet.DisplayName()

toAttackerMsg := combatMsgs.ApplyTokens(combatMsgs.ToOwner, petDisplayName, attackTargetDamage, targetDisplayName)
attackResult.SendToSource(toAttackerMsg)
toAttackerMsg := combatMsgs.ApplyTokens(combatMsgs.ToOwner, petDisplayName, attackTargetDamage, targetDisplayName)
attackResult.SendToSource(toAttackerMsg)

toDefenderMsg := combatMsgs.ApplyTokens(combatMsgs.ToTarget, petDisplayName, attackTargetDamage, targetDisplayName)
attackResult.SendToTarget(toDefenderMsg)
toDefenderMsg := combatMsgs.ApplyTokens(combatMsgs.ToTarget, petDisplayName, attackTargetDamage, targetDisplayName)
attackResult.SendToTarget(toDefenderMsg)

toAttackerRoomMsg := combatMsgs.ApplyTokens(combatMsgs.ToRoom, petDisplayName, attackTargetDamage, targetDisplayName)
attackResult.SendToTargetRoom(toAttackerRoomMsg)
toAttackerRoomMsg := combatMsgs.ApplyTokens(combatMsgs.ToRoom, petDisplayName, attackTargetDamage, targetDisplayName)
attackResult.SendToTargetRoom(toAttackerRoomMsg)

// pets doing max damage are considered "crits" and will always apply any special critBuffs
if len(critBuffs) > 0 && (attackTargetDamage == (pDCount*pDSides)+pDBonus) {
attackResult.BuffTarget = critBuffs
// pets doing max damage are considered "crits" and will always apply any special critBuffs
if len(critBuffs) > 0 && (attackTargetDamage == (pDCount*pDSides)+pDBonus) {
attackResult.BuffTarget = critBuffs
}
}
}

}
}
}
}

}

return attackResult

}
13 changes: 11 additions & 2 deletions internal/hooks/NewRound_UserRoundTick.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,18 @@ func UserRoundTick(e events.Event) events.ListenerReturn {
user.Command(`zombieact`)
}

// Decrement pet missing countdown each round.
petJustReturned := false
if user.Character.Pet.Exists() && user.Character.Pet.IsMissing() {
if user.Character.Pet.DecrementMissing() {
petJustReturned = true
scripting.TryPetScriptEvent(`PetReturn`, uId)
}
}

// Fire PetAct script using the pet type's configured RoundActChance.
// Not called while the owner is in combat.
if user.Character.Pet.Exists() && user.Character.Aggro == nil && user.Character.Pet.RoundActChance > 0 && util.Rand(100) < user.Character.Pet.RoundActChance {
// Not called while the owner is in combat, or on the same round the pet returns.
if !petJustReturned && user.Character.Pet.Exists() && !user.Character.Pet.IsMissing() && user.Character.Aggro == nil && user.Character.Pet.RoundActChance > 0 && util.Rand(100) < user.Character.Pet.RoundActChance {
scripting.TryPetScriptEvent(`PetAct`, uId)
}

Expand Down
45 changes: 35 additions & 10 deletions internal/pets/pets.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ import (
)

type Pet struct {
Name string `yaml:"name,omitempty"` // Name of the pet (player provided hopefully)
NameStyle string `yaml:"namestyle,omitempty"` // Optional color pattern to apply
Type string `yaml:"type"` // type of pet
RoundActChance int `yaml:"roundactchance,omitempty"` // 0-100 chance per round to fire PetAct script
Food Food `yaml:"food,omitempty"` // how much food the pet has
Level int `yaml:"level,omitempty"` // Pet level (1-10)
LastMealRound uint8 `yaml:"lastmealround,omitempty"` // When the pet was last fed
LastLevelCheck string `yaml:"lastlevelcheck,omitempty"` // "{year}.{day}" of last daily tick
Abilities []PetAbility `yaml:"abilities,omitempty"` // Refreshed from definition file on Validate()
Items []items.Item `yaml:"items,omitempty"` // Items held by this pet
Name string `yaml:"name,omitempty"` // Name of the pet (player provided hopefully)
NameStyle string `yaml:"namestyle,omitempty"` // Optional color pattern to apply
Type string `yaml:"type"` // type of pet
RoundActChance int `yaml:"roundactchance,omitempty"` // 0-100 chance per round to fire PetAct script
Food Food `yaml:"food,omitempty"` // how much food the pet has
Level int `yaml:"level,omitempty"` // Pet level (1-10)
LastMealRound uint8 `yaml:"lastmealround,omitempty"` // When the pet was last fed
LastLevelCheck string `yaml:"lastlevelcheck,omitempty"` // "{year}.{day}" of last daily tick
Abilities []PetAbility `yaml:"abilities,omitempty"` // Refreshed from definition file on Validate()
Items []items.Item `yaml:"items,omitempty"` // Items held by this pet
MissingCountdown int `yaml:"missingcountdown,omitempty"` // When non-zero, pet is absent

cachedAbility *PetAbility `yaml:"-"` // cached current ability
cachedLevel int `yaml:"-"` // level when cache was set
Expand Down Expand Up @@ -86,6 +87,30 @@ func (p *Pet) Exists() bool {
return p.Type != ``
}

// IsMissing returns true when the pet is temporarily absent.
func (p *Pet) IsMissing() bool {
return p.MissingCountdown > 0
}

// GoMissing sets the missing countdown to the given number of rounds.
// A value of zero clears the missing state immediately.
func (p *Pet) GoMissing(rounds int) {
if rounds < 0 {
rounds = 0
}
p.MissingCountdown = rounds
}

// DecrementMissing decrements the missing countdown by one.
// Returns true if the countdown just reached zero (pet is returning this round).
func (p *Pet) DecrementMissing() bool {
if p.MissingCountdown <= 0 {
return false
}
p.MissingCountdown--
return p.MissingCountdown == 0
}

func (p *Pet) DisplayName() string {

name := p.Name
Expand Down
2 changes: 1 addition & 1 deletion internal/rooms/roomdetails.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ func GetDetails(r *Room, user *users.UserRecord, tinymap ...[]string) RoomTempla
}
}

if user.Character.Pet.Exists() && r.RoomId == user.Character.RoomId {
if user.Character.Pet.Exists() && !user.Character.Pet.IsMissing() && r.RoomId == user.Character.RoomId {
details.VisiblePlayers = append(details.VisiblePlayers, fmt.Sprintf(`%s (your pet)`, user.Character.Pet.DisplayName()))
}

Expand Down
4 changes: 2 additions & 2 deletions internal/rooms/rooms.go
Original file line number Diff line number Diff line change
Expand Up @@ -1207,7 +1207,7 @@ func (r *Room) GetMobs(findTypes ...FindFlag) []int {
continue
}

if typeFlag&FindHasPet == FindHasPet && mob.Character.Pet.Exists() {
if typeFlag&FindHasPet == FindHasPet && mob.Character.Pet.Exists() && !mob.Character.Pet.IsMissing() {
mobMatches = append(mobMatches, mobId)
continue
}
Expand Down Expand Up @@ -1297,7 +1297,7 @@ func (r *Room) GetPlayers(findTypes ...FindFlag) []int {
continue
}

if typeFlag&FindHasPet == FindHasPet && user.Character.Pet.Exists() {
if typeFlag&FindHasPet == FindHasPet && user.Character.Pet.Exists() && !user.Character.Pet.IsMissing() {
playerMatches = append(playerMatches, userId)
continue
}
Expand Down
2 changes: 1 addition & 1 deletion internal/scripting/actor_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ func (a ScriptActor) GetPet() *ScriptPet {
if !a.characterRecord.Pet.Exists() {
return nil
}
return GetPet(&a.characterRecord.Pet)
return GetPet(&a.characterRecord.Pet, a.userId)
}

func (a ScriptActor) GrantXP(xpAmt int, reason string) {
Expand Down
4 changes: 2 additions & 2 deletions internal/scripting/pet.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TryPetScriptEvent(eventName string, userId int) (bool, error) {
return false, errors.New("user has no pet")
}

sPet := GetPet(&user.Character.Pet)
sPet := GetPet(&user.Character.Pet, userId)
if sPet == nil {
return false, errors.New("pet not found")
}
Expand Down Expand Up @@ -112,7 +112,7 @@ func TryPetCommand(cmd string, rest string, userId int) (bool, error) {
return false, ErrEventNotFound
}

sPet := GetPet(&user.Character.Pet)
sPet := GetPet(&user.Character.Pet, userId)
if sPet == nil {
return false, ErrEventNotFound
}
Expand Down
Loading