package game import ( "fmt" "math/rand" "sort" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func (g *Game) dropItemsOnDeath(p *player.Player) { roomID := p.RoomID if p.Credits > 0 { g.World.AddGroundItem(roomID, "credits", p.Credits) p.Credits = 0 } var items []deathDrop for slot, inv := range p.Inventory { if inv == nil || inv.Quantity <= 0 { continue } val := 0 if def, err := g.ItemStore.Load(inv.ItemID); err == nil { val = def.Value * inv.Quantity } items = append(items, deathDrop{ itemID: inv.ItemID, quantity: inv.Quantity, totalValue: val, invSlot: slot, }) } for eqSlot, itemID := range p.Equipment { quantity := 1 // Ammo keeps its stack count in AmmoQty, not in an inventory slot; // dropping the equipment entry without moving that count would destroy // the rest of the stack and leave a stale ammo_qty on disk. if eqSlot == item.SlotAmmo { quantity = p.AmmoQty if quantity <= 0 { quantity = 1 } } val := 0 if def, err := g.ItemStore.Load(itemID); err == nil { val = def.Value * quantity } items = append(items, deathDrop{ itemID: itemID, quantity: quantity, totalValue: val, isEquip: true, equipSlot: eqSlot, }) } if len(items) <= 3 { return } sort.Slice(items, func(i, j int) bool { return items[i].totalValue > items[j].totalValue }) for i := 3; i < len(items); i++ { it := items[i] if it.isEquip { delete(p.Equipment, it.equipSlot) // Equipped quality is keyed by item id and must not outlive the // equipment entry, or a later drop/wear reads a stale quality. delete(p.EquipQuality, it.itemID) if it.equipSlot == item.SlotAmmo { p.AmmoQty = 0 } } else { p.SetInvSlot(it.invSlot, nil) } g.World.AddGroundItem(roomID, it.itemID, it.quantity) } } // aggroChancePerTick is the probability per engine tick that an aggressive // mob in the player's room initiates combat. const aggroChancePerTick = 0.33 var rollAggroRand = rand.Float64 func (g *Game) checkAggro(sess *net.Session) { p := sess.Player if p.GodMode { return } if g.Combat.Get(p.Name) != nil { return } playerLevel := p.CombatLevel() mobs := g.MobStore.MobsInRoom(p.RoomID) for _, mob := range mobs { if !mob.Aggressive || mob.HP <= 0 || mob.Protected { continue } if g.Combat.IsMobInCombat(mob.InstanceID) { continue } mobLevel := mobCombatLevel(mob) if playerLevel > mobLevel*2 { continue } if g.isSafespotted(p.Name) && g.safespotBlocksMob(p, mob) { continue } aggMob := mob keepRolling := func() (keep bool) { // The retry subscription outlives the session that armed it (only // mob/combat/room state ends it), so a player who has since logged // out, disconnected, or logged in again on a new connection must // not have combat started for them here: the mob's own subscribers // would then keep fighting a character who is not in the world. if sess.Player == nil || sess.IsDisconnecting() { return false } g.charsMu.Lock() current := g.loggedInChars[p.Name] g.charsMu.Unlock() if current != nil && current != sess { return false } if g.Combat.Get(p.Name) != nil { return false } if aggMob.HP <= 0 || aggMob.RoomID != p.RoomID { return false } if g.Combat.IsMobInCombat(aggMob.InstanceID) { return false } // The retry outlives the immediate roll: a player can god or enter a // safespot after the first miss, so re-check both gates on every // retry before rolling again. if p.GodMode { return false } if g.isSafespotted(p.Name) && g.safespotBlocksMob(p, aggMob) { return false } if rollAggroRand() >= aggroChancePerTick { return true } attacker := aggMob.Name if !aggMob.Unique { attacker = "The " + aggMob.Name } sess.WriteLine(fmt.Sprintf("%s attacks you!", g.colorize(sess, "mob", attacker))) g.startCombat(sess, p, aggMob) return false } if !keepRolling() { break } g.SubscribeTicks(1, keepRolling) break } }