From a3ae1e6aad696f584d138d9363c05dedff136a65 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sun, 21 Jun 2026 22:19:17 -0400 Subject: feat: safespot system implemented --- internal/game/sys_safespot.go | 330 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 internal/game/sys_safespot.go (limited to 'internal/game/sys_safespot.go') diff --git a/internal/game/sys_safespot.go b/internal/game/sys_safespot.go new file mode 100644 index 0000000..ffb7948 --- /dev/null +++ b/internal/game/sys_safespot.go @@ -0,0 +1,330 @@ +package game + +import ( + "fmt" + "math/rand" + + "thehouseoficarus/internal/color" + "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 { + g.safespotMu.Lock() + defer g.safespotMu.Unlock() + ss, ok := g.safespotStates[playerName] + return ok && ss.Active && ss.HideCountdown <= 0 +} + +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 +} + +func (g *Game) safespotBlocksMob(p *player.Player, mob *world.MobInstance) bool { + g.safespotMu.Lock() + ss, ok := g.safespotStates[p.Name] + g.safespotMu.Unlock() + if !ok || !ss.Active || ss.HideCountdown > 0 { + return false + } + + objDef, err := g.ObjectStore.Load(ss.ObjectDefID) + if err != nil || objDef.Safespot == nil { + return false + } + + if !blocksMob(objDef.Safespot.MaxBlockSize, mob.Size) { + return false + } + + if mob.AttackType == "ranged" || mob.AttackType == "science" { + return false + } + + return true +} + +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.safespotMu.Lock() + delete(g.safespotStates, p.Name) + g.safespotMu.Unlock() + 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.safespotMu.Lock() + delete(g.safespotStates, p.Name) + g.safespotMu.Unlock() + sess.WriteLine("The safespot no longer exists.") + return + } + + if cfg.RespawnOnHide { + st.SafespotLevel = len(cfg.Levels) + st.SafespotTicksAtLevel = 0 + } else if st.SafespotLevel <= 0 { + g.safespotMu.Lock() + delete(g.safespotStates, p.Name) + g.safespotMu.Unlock() + sess.WriteLine(fmt.Sprintf("The %s has been completely destroyed.", objDef.Name)) + return + } + + st.SafespotOccupants = append(st.SafespotOccupants, p.Name) + p.ActionState = nil + + 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)) +} + +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 { + alert := p.OptionString("safespot_alert") + g.forceLeaveSafespot(sess, p, ss, "You slip and are no longer covered!") + if alert != "" { + sess.WriteLine(color.ExpandTags(g.colorMode(sess), alert)) + } + 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(color.ExpandTags(g.colorMode(rs), msg)) + } + } + } + + st.SafespotLevel-- + st.SafespotTicksAtLevel = 0 + + if st.SafespotLevel <= 0 { + for _, name := range st.SafespotOccupants { + g.safespotMu.Lock() + delete(g.safespotStates, name) + g.safespotMu.Unlock() + if occSess := g.sessionInRoom(name, roomSessions); occSess != nil { + occSess.WriteLine(color.ExpandTags(g.colorMode(occSess), + fmt.Sprintf("The %s crumbles away!", objDef.Name))) + if occSess.Player != nil { + alert := occSess.Player.OptionString("safespot_alert") + if alert != "" { + occSess.WriteLine(color.ExpandTags(g.colorMode(occSess), alert)) + } + } + g.writePrompt(occSess) + } + } + st.SafespotOccupants = nil + g.scheduleSafespotRespawn(st, objDef, cfg) + 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(color.ExpandTags(g.colorMode(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(color.ExpandTags(g.colorMode(sess), + fmt.Sprintf("The %s reforms and looks stable again.", objDef.Name))) + } + return false + }) +} + +func (g *Game) leaveSafespot(sess *net.Session, p *player.Player) { + g.safespotMu.Lock() + ss, ok := g.safespotStates[p.Name] + if !ok { + g.safespotMu.Unlock() + return + } + delete(g.safespotStates, p.Name) + g.safespotMu.Unlock() + + 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)) + p.ActionState = nil +} + +func (g *Game) forceLeaveSafespot(sess *net.Session, p *player.Player, ss *SafespotState, reason string) { + g.safespotMu.Lock() + delete(g.safespotStates, p.Name) + g.safespotMu.Unlock() + + g.removeSafespotOccupant(p.Name, ss) + + if reason != "" { + sess.WriteLine(reason) + } + p.ActionState = nil +} + +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 + } + + g.safespotMu.Lock() + ss, ok := g.safespotStates[p.Name] + if !ok || !ss.Active { + g.safespotMu.Unlock() + continue + } + + if ss.HideCountdown > 0 { + ss.HideCountdown-- + if ss.HideCountdown > 0 { + g.safespotMu.Unlock() + continue + } + g.safespotMu.Unlock() + g.activateSafespot(sess, p, ss) + continue + } + g.safespotMu.Unlock() + + g.safespotMaintenance(sess, p, ss) + } +} -- cgit v1.2.3