package game import ( "fmt" "math/rand" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) func (g *Game) DisconnectTick() { if g.Hub == nil { return } for _, sess := range g.Hub.AllSessions() { if !sess.Disconnecting { continue } p := sess.Player if p == nil { g.Hub.Remove(sess) continue } if g.Combat.Get(p.Name) != nil { continue } sess.DisconnectTicks-- if sess.DisconnectTicks <= 0 { g.restoreGodPlayer(p) g.AccountStore.SaveCharacter(p) g.Hub.HardRemove(sess) } } } func (g *Game) RegenTick() { if g.Hub == nil { return } for _, sess := range g.Hub.AllSessions() { p := sess.Player if p == nil { continue } if p.HP <= 0 || p.HP >= p.MaxHP() { p.RegenerateTick = 0 continue } regenSpeed := 1 if p.ActiveTechs != nil { for id := range p.ActiveTechs { def := GetTechDef(id) if def != nil && def.Effects.HPRegenMulti > 0 { multi := int(def.Effects.HPRegenMulti) if multi > regenSpeed { regenSpeed = multi } } } } p.RegenerateTick -= regenSpeed if p.RegenerateTick <= 0 { p.HP++ if p.HP >= p.MaxHP() { p.HP = p.MaxHP() p.RegenerateTick = 0 } else { p.RegenerateTick = player.RegenTickInterval } } } } func (g *Game) WanderTick() { if g.Hub == nil { return } type moveEvent struct { name string fromRoom int toRoom int level int } var moves []moveEvent // Mob wandering — exit-based for _, inst := range g.MobStore.AllInstances() { if inst.HP <= 0 || inst.WanderInterval <= 0 { continue } if g.Combat.IsMobInCombat(inst.InstanceID) { continue } inst.WanderTickCounter++ if float64(inst.WanderTickCounter) < inst.WanderInterval { continue } inst.WanderTickCounter = 0 legal := g.legalMobExits(inst) if len(legal) == 0 { continue } toRoom := legal[rand.Intn(len(legal))] moves = append(moves, moveEvent{ name: mobDisplayName(inst, false), fromRoom: inst.RoomID, toRoom: toRoom, level: mobCombatLevel(inst), }) g.MobStore.SetInstanceRoom(inst.InstanceID, toRoom) } // Object wandering (fishing spots, etc.) g.World.TickObjWander() objMoves := g.World.FlushObjMoves() for _, om := range objMoves { for _, sess := range g.Hub.PlayersInRoom(om.FromRoom) { if p := sess.Player; p != nil && p.Action != nil { if p.Action.TargetID == om.DefID { g.cancelAction(p) g.writePrompt(sess) } } } moves = append(moves, moveEvent{ name: "a " + om.Name, fromRoom: om.FromRoom, toRoom: om.ToRoom, level: 0, }) } for _, m := range moves { for _, sess := range g.Hub.AllSessions() { p := sess.Player if p == nil { continue } if p.RoomID == m.fromRoom && p.OptionBool("mob_leave") { if m.level > 0 { levelStr := g.levelColorize(sess, p.CombatLevel(), m.level, fmt.Sprintf("(level %d)", m.level)) sess.WriteLine(fmt.Sprintf("%s %s leaves.", g.colorize(sess, "mob", m.name), levelStr)) } else { sess.WriteLine(fmt.Sprintf("%s moves away.", g.colorize(sess, "mob", m.name))) } } if p.RoomID == m.toRoom && p.OptionBool("mob_enter") { if m.level > 0 { levelStr := g.levelColorize(sess, p.CombatLevel(), m.level, fmt.Sprintf("(level %d)", m.level)) sess.WriteLine(fmt.Sprintf("%s %s enters.", g.colorize(sess, "mob", m.name), levelStr)) } else { sess.WriteLine(fmt.Sprintf("%s drifts in.", g.colorize(sess, "mob", m.name))) } } } } } func (g *Game) SharedDepletionTick() { activeKeys := make(map[string]bool) for _, sess := range g.Hub.AllSessions() { p := sess.Player if p == nil || p.Action == nil || p.Action.Type != "gather" { continue } if gd, ok := p.Action.Data.(*behavior.GatherData); ok { activeKeys[gd.InstanceKey] = true } } all := g.World.AllSharedObjStates() for _, st := range all { if st.Depleted { continue } key := g.World.ObjStateKey(st.RoomID, st.DefID, st.Index) if activeKeys[key] { if st.SharedTimer > 0 { st.SharedTimer-- } } else { if st.SharedTimer < int(st.SharedMax) { st.SharedTimer++ } } } } func (g *Game) legalMobExits(inst *world.MobInstance) []int { room, err := g.World.LoadRoom(inst.RoomID) if err != nil || len(room.Exits) == 0 { return nil } allowed := make(map[int]bool) if len(inst.WanderRooms) > 0 { for _, r := range inst.WanderRooms { allowed[r] = true } } var legal []int for _, exitDef := range room.Exits { if exitDef.Condition != nil { continue } if len(allowed) > 0 && !allowed[exitDef.Room] { continue } legal = append(legal, exitDef.Room) } return legal } func (g *Game) TechTick() { if g.Hub == nil { return } for _, sess := range g.Hub.AllSessions() { p := sess.Player if p == nil || len(p.ActiveTechs) == 0 { continue } totalDrain := 0.0 hasPreserve := false preserveReduction := 0.0 for id := range p.ActiveTechs { def := GetTechDef(id) if def != nil && def.Effects.PreserveDrain > 0 { hasPreserve = true preserveReduction = def.Effects.PreserveDrain } } for id := range p.ActiveTechs { def := GetTechDef(id) if def == nil { continue } if p.TechActivatedSinceTick[id] { continue } drain := def.DrainRate if hasPreserve && def.Effects.PreserveDrain == 0 { drain *= (1.0 - preserveReduction) } totalDrain += drain } p.TechActivatedSinceTick = nil equipTechBonus := g.totalTechBonus(p) if equipTechBonus > 0 { reduction := float64(equipTechBonus) * 0.01 if reduction > 0.5 { reduction = 0.5 } totalDrain *= (1.0 - reduction) } if totalDrain <= 0 { continue } p.Battery -= totalDrain if p.Battery <= 0 { p.Battery = 0 p.DeactivateAllTechs() sess.WriteLine(g.colorize(sess, "tech_depleted", "Your battery is depleted! All tech has been disabled.")) g.writePrompt(sess) } } } func (g *Game) ConsumeTick() { if g.Hub == nil { return } for _, sess := range g.Hub.AllSessions() { p := sess.Player if p == nil { continue } if p.ConsumeCooldown > 0 { p.ConsumeCooldown-- } if p.HomeTransportCooldown > 0 { p.HomeTransportCooldown-- } } } func (g *Game) VisualTick() { if g.Hub == nil { return } for _, sess := range g.Hub.AllSessions() { p := sess.Player if p == nil || !p.OptionBool("visual_ticks") { continue } text := p.OptionString("visual_tick_text") count := p.OptionInt("visual_tick_count") if count <= 0 { sess.WriteLine(g.colorize(sess, "visual_tick", text)) } else { p.VisualTickCurrent++ if p.VisualTickCurrent > count { p.VisualTickCurrent = 1 } msg := fmt.Sprintf("%s %d", text, p.VisualTickCurrent) if p.VisualTickCurrent == 1 { sess.WriteLine(g.colorize(sess, "visual_first_tick", msg)) } else { sess.WriteLine(g.colorize(sess, "visual_tick", msg)) } } } } func (g *Game) MoveTick() { if g.Hub == nil { return } for _, sess := range g.Hub.AllSessions() { p := sess.Player if p == nil || p.MoveTicks <= 0 { continue } p.MoveTicks-- if p.MoveTicks > 0 { if g.Combat.Get(p.Name) != nil && p.OptionBool("run_countdown") { if p.MoveTicks > 1 { sess.WriteLine(fmt.Sprintf("Running in %d ticks...", p.MoveTicks)) } else { sess.WriteLine("Running next tick!") } } } else { g.completeMove(sess, p) g.writePrompt(sess) } } }