package game import ( "fmt" "math/rand" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) var sizeRank = map[string]int{ "small": 1, "medium": 2, "large": 3, "massive": 4, } func (g *Game) isSafespotted(playerName string) bool { return g.safespot.IsActive(playerName) } func (g *Game) safespotTier(p *player.Player) int { tier := 1 if getPlayerFlagInt(p, "unlocked_rock_cover") > 0 { tier++ } return tier } func blocksMob(maxBlockSize, mobSize string) bool { if maxBlockSize == "" { return true } max := sizeRank[maxBlockSize] mob := sizeRank[mobSize] if mob == 0 { mob = sizeRank["medium"] } return mob <= max } // safespotBlocksHazard reports whether the player's active safespot shields them // from room hazard damage. Unlike mobs, hazard type does not matter: an active // safespot blocks ALL hazard damage, no safespot blocks none. func (g *Game) safespotBlocksHazard(p *player.Player) bool { return g.isSafespotted(p.Name) } // safespotBlocksMelee reports whether the player's active safespot shields them // from this mob's melee attacks (based on cover size vs mob size). It does NOT // consider the mob's attack types — a mob whose melee is blocked may still be // able to attack with ranged/science (see effectiveMobAttackType). func (g *Game) safespotBlocksMelee(p *player.Player, mob *world.MobInstance) bool { ss, ok := g.safespot.Get(p.Name) if !ok || !ss.Active || ss.HideCountdown > 0 { return false } objDef, err := g.ObjectStore.Load(ss.ObjectDefID) if err != nil || objDef.Safespot == nil { return false } return blocksMob(objDef.Safespot.MaxBlockSize, mob.Size) } // safespotBlocksMob reports whether the player's active safespot fully prevents // this mob from attacking at all. This is true only when the safespot blocks the // mob's melee AND the mob has no ranged/science attack type to switch to. func (g *Game) safespotBlocksMob(p *player.Player, mob *world.MobInstance) bool { if !g.safespotBlocksMelee(p, mob) { return false } return mobStrongestRangedScience(mob) == "" } func (g *Game) sessionInRoom(name string, roomSessions []*net.Session) *net.Session { for _, sess := range roomSessions { if sess.Player != nil && sess.Player.Name == name { return sess } } return nil } func (g *Game) activateSafespot(sess *net.Session, p *player.Player, ss *SafespotState) { objDef, err := g.ObjectStore.Load(ss.ObjectDefID) if err != nil || objDef.Safespot == nil { g.safespot.Delete(p.Name) sess.WriteLine("The safespot no longer exists.") return } cfg := objDef.Safespot st := g.World.GetObjState(ss.RoomID, ss.ObjectDefID, ss.ObjectIndex) if st == nil { g.safespot.Delete(p.Name) sess.WriteLine("The safespot no longer exists.") return } if cfg.RespawnOnHide && len(st.SafespotOccupants) == 0 { st.SafespotLevel = len(cfg.Levels) st.SafespotTicksAtLevel = 0 } else if st.SafespotLevel <= 0 { g.safespot.Delete(p.Name) sess.WriteLine(fmt.Sprintf("The %s has been completely destroyed.", objDef.Name)) return } st.SafespotOccupants = append(st.SafespotOccupants, p.Name) var msg string levelIdx := st.SafespotLevel - 1 if levelIdx >= 0 && levelIdx < len(cfg.Levels) && cfg.Levels[levelIdx].Message != "" { msg = cfg.Levels[levelIdx].Message } else { msg = fmt.Sprintf("You crouch behind the %s, using it as cover.", objDef.Name) } sess.WriteLine(g.colorize(sess, "combat_info", msg)) g.writePrompt(sess) } func (g *Game) safespotMaintenance(sess *net.Session, p *player.Player, ss *SafespotState) { objDef, err := g.ObjectStore.Load(ss.ObjectDefID) if err != nil || objDef.Safespot == nil { g.forceLeaveSafespot(sess, p, ss, "The safespot no longer exists.") return } cfg := objDef.Safespot st := g.World.GetObjState(ss.RoomID, ss.ObjectDefID, ss.ObjectIndex) if st == nil { g.forceLeaveSafespot(sess, p, ss, "The safespot no longer exists.") return } if st.SafespotLevel <= 0 { if cfg.RespawnTicks > 0 { return } g.forceLeaveSafespot(sess, p, ss, fmt.Sprintf("The %s has crumbled away!", objDef.Name)) return } if len(st.SafespotOccupants) == 0 { return } if cfg.UnsafeChance > 0 && rand.Float64() < cfg.UnsafeChance { g.forceLeaveSafespot(sess, p, ss, "You slip and are no longer covered!") return } levelIdx := st.SafespotLevel - 1 degraded := false if cfg.DecayChance > 0 && rand.Float64() < cfg.DecayChance { degraded = true } if !degraded && cfg.DecayTicks > 0 { st.SafespotTicksAtLevel++ if st.SafespotTicksAtLevel >= engine.ToTicks(cfg.DecayTicks) { degraded = true } } if degraded { g.degradeSafespot(st, objDef, cfg, levelIdx) } } func (g *Game) degradeSafespot(st *world.ObjState, objDef *object.ObjectDef, cfg *object.SafespotConfig, currentLevelIdx int) { roomSessions := g.Hub.PlayersInRoom(st.RoomID) if currentLevelIdx >= 0 && currentLevelIdx < len(cfg.Levels) { msg := cfg.Levels[currentLevelIdx].DegradeMessage if msg != "" { for _, rs := range roomSessions { rs.WriteLine("") rs.WriteLine(g.expandTags(rs, msg)) } } } st.SafespotLevel-- st.SafespotTicksAtLevel = 0 if st.SafespotLevel <= 0 { for _, name := range st.SafespotOccupants { g.safespot.Delete(name) if occSess := g.sessionInRoom(name, roomSessions); occSess != nil { occSess.WriteLine(g.expandTags(occSess, fmt.Sprintf("The %s crumbles away!", objDef.Name))) occSess.WriteLine(g.colorize(occSess, "safespot_alert", "Your safespot has been compromised!")) g.writePrompt(occSess) } } st.SafespotOccupants = nil if cfg.RespawnTicks > 0 { g.scheduleSafespotRespawn(st, objDef, cfg) } else { g.World.RemoveObjInstance(st.RoomID, objDef.ID) } return } newLevelIdx := st.SafespotLevel - 1 if newLevelIdx >= 0 && newLevelIdx < len(cfg.Levels) && cfg.Levels[newLevelIdx].Message != "" { msg := cfg.Levels[newLevelIdx].Message for _, name := range st.SafespotOccupants { if occSess := g.sessionInRoom(name, roomSessions); occSess != nil { occSess.WriteLine("") occSess.WriteLine(g.expandTags(occSess, msg)) } } } } func (g *Game) scheduleSafespotRespawn(st *world.ObjState, objDef *object.ObjectDef, cfg *object.SafespotConfig) { if cfg.RespawnTicks <= 0 { return } roomID := st.RoomID defID := objDef.ID g.Ticks.Subscribe(engine.ToTicks(cfg.RespawnTicks), func() bool { respawnSt := g.World.GetObjState(roomID, defID, 0) if respawnSt == nil { return false } respawnSt.SafespotLevel = len(cfg.Levels) respawnSt.SafespotTicksAtLevel = 0 for _, sess := range g.Hub.PlayersInRoom(roomID) { sess.WriteLine("") sess.WriteLine(g.expandTags(sess, fmt.Sprintf("The %s reforms and looks stable again.", objDef.Name))) } return false }) } func (g *Game) leaveSafespot(sess *net.Session, p *player.Player) { ss, ok := g.safespot.Get(p.Name) if !ok { return } g.safespot.Delete(p.Name) g.removeSafespotOccupant(p.Name, &ss) objDef, _ := g.ObjectStore.Load(ss.ObjectDefID) objName := "cover" if objDef != nil { objName = objDef.Name } sess.WriteLine(fmt.Sprintf("You step out from behind the %s.", objName)) } func (g *Game) forceLeaveSafespot(sess *net.Session, p *player.Player, ss *SafespotState, reason string) { g.safespot.Delete(p.Name) g.removeSafespotOccupant(p.Name, ss) if reason != "" { sess.WriteLine(reason) sess.WriteLine(g.colorize(sess, "safespot_alert", "Your safespot has been compromised!")) } } func (g *Game) removeSafespotOccupant(playerName string, ss *SafespotState) { st := g.World.GetObjState(ss.RoomID, ss.ObjectDefID, ss.ObjectIndex) if st == nil { return } for i, name := range st.SafespotOccupants { if name == playerName { st.SafespotOccupants = append(st.SafespotOccupants[:i], st.SafespotOccupants[i+1:]...) return } } } func (g *Game) SafespotTick() { if g.Hub == nil { return } for _, sess := range g.Hub.AllSessions() { p := sess.Player if p == nil { continue } ss, ok := g.safespot.Get(p.Name) if !ok || !ss.Active { continue } if ss.HideCountdown > 0 { newCountdown, _ := g.safespot.DecrementHideCountdown(p.Name) if newCountdown > 0 { continue } g.activateSafespot(sess, p, &ss) continue } g.safespotMaintenance(sess, p, &ss) } }