aboutsummaryrefslogtreecommitdiff
path: root/internal/game/action.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-10 01:48:28 -0400
committerhistoria <[not public]>2026-06-10 01:48:28 -0400
commita226d72e51eecb768b13600303f73483118d9104 (patch)
tree458d15ef049732c15dacc591a830d3beddbedfde /internal/game/action.go
parent6a0f3d7a252de4b1741cfe5e1c561412c602becc (diff)
downloadthehouseoficarus-a226d72e51eecb768b13600303f73483118d9104.tar.gz
feat: implemented janky object interaction model
Diffstat (limited to 'internal/game/action.go')
-rw-r--r--internal/game/action.go765
1 files changed, 765 insertions, 0 deletions
diff --git a/internal/game/action.go b/internal/game/action.go
new file mode 100644
index 0000000..029fec3
--- /dev/null
+++ b/internal/game/action.go
@@ -0,0 +1,765 @@
+package game
+
+import (
+ "fmt"
+ "math/rand"
+ "sort"
+ "strconv"
+ "strings"
+
+ "thirdcollapse/internal/action"
+ "thirdcollapse/internal/combat"
+ "thirdcollapse/internal/net"
+ "thirdcollapse/internal/object"
+ "thirdcollapse/internal/player"
+ "thirdcollapse/internal/world"
+)
+
+func (g *Game) StartAction(sess *net.Session, verb, target string) {
+ p := sess.Player.(*player.Player)
+
+ verb = normalizeVerb(verb)
+
+ if combat.GetCombat(p.Name) != nil {
+ sess.WriteLine("You can't do that during combat!")
+ return
+ }
+
+ if p.Action != nil {
+ g.CancelAction(p)
+ }
+
+ lower := strings.ToLower(target)
+ instanceIdx := -1
+ if dotPos := strings.Index(lower, "."); dotPos > 0 {
+ if n, err := strconv.Atoi(lower[:dotPos]); err == nil && n > 0 {
+ instanceIdx = n - 1
+ lower = lower[dotPos+1:]
+ }
+ }
+
+ instances := g.World.FindObjInstances(p.RoomID, lower)
+ sort.Slice(instances, func(i, j int) bool {
+ return instances[i].Index < instances[j].Index
+ })
+
+ var obj *object.ObjectDef
+ var mob *world.MobInstance
+ var behaviorID string
+
+ if len(instances) > 0 {
+ var chosen *world.ObjState
+ if instanceIdx >= 0 && instanceIdx < len(instances) {
+ chosen = &instances[instanceIdx]
+ } else {
+ for i := range instances {
+ if !instances[i].Depleted {
+ chosen = &instances[i]
+ break
+ }
+ }
+ if chosen == nil {
+ chosen = &instances[0]
+ }
+ }
+ if chosen == nil {
+ sess.WriteLine(fmt.Sprintf("There's nothing here to %s.", verb))
+ return
+ }
+ var err error
+ obj, err = g.ObjectStore.Load(chosen.DefID)
+ if err != nil || obj.BehaviorID == "" {
+ sess.WriteLine(fmt.Sprintf("You can't %s the %s.", verb, obj.Name))
+ return
+ }
+ behaviorID = obj.BehaviorID
+ } else {
+ mobs := g.MobStore.MobsInRoom(p.RoomID)
+ var candidates []*world.MobInstance
+ for _, m := range mobs {
+ if m.BehaviorID == "" {
+ continue
+ }
+ if q := m.MatchQuality(lower); q >= world.MatchPrefix {
+ candidates = append(candidates, m)
+ }
+ }
+ if len(candidates) == 0 {
+ sess.WriteLine(fmt.Sprintf("There's nothing here to %s.", verb))
+ return
+ }
+ sort.Slice(candidates, func(i, j int) bool {
+ return candidates[i].InstanceID < candidates[j].InstanceID
+ })
+ if instanceIdx >= 0 && instanceIdx < len(candidates) {
+ mob = candidates[instanceIdx]
+ } else if len(candidates) == 1 {
+ mob = candidates[0]
+ } else {
+ sess.WriteLine("Which one?")
+ return
+ }
+ behaviorID = mob.BehaviorID
+ }
+
+ bh, err := g.BehaviorStore.Load(behaviorID)
+ if err != nil {
+ if obj != nil {
+ sess.WriteLine(fmt.Sprintf("Something is wrong with the %s.", obj.Name))
+ } else {
+ sess.WriteLine("Something is wrong with that.")
+ }
+ return
+ }
+
+ if bh.Type != verb {
+ if obj != nil {
+ sess.WriteLine(fmt.Sprintf("You can't %s the %s.", verb, obj.Name))
+ } else {
+ sess.WriteLine(fmt.Sprintf("You can't %s that.", verb))
+ }
+ return
+ }
+
+ switch bh.Type {
+ case "gather":
+ if obj == nil {
+ sess.WriteLine(fmt.Sprintf("You can't %s that.", verb))
+ return
+ }
+ // Find the actual ObjState for the chosen object
+ chosenObj := g.World.FindObjInstances(p.RoomID, obj.ID)
+ sort.Slice(chosenObj, func(i, j int) bool {
+ return chosenObj[i].Index < chosenObj[j].Index
+ })
+ var st *world.ObjState
+ for i := range chosenObj {
+ if !chosenObj[i].Depleted {
+ st = &chosenObj[i]
+ break
+ }
+ }
+ if st == nil && len(chosenObj) > 0 {
+ st = &chosenObj[0]
+ }
+ if st == nil {
+ sess.WriteLine(fmt.Sprintf("There's nothing here to %s.", verb))
+ return
+ }
+ g.startGather(sess, p, obj, st)
+ case "talk":
+ if mob != nil {
+ g.startMobTalk(sess, p, mob)
+ } else {
+ g.startTalk(sess, p, obj)
+ }
+ case "use":
+ g.startUse(sess, p, obj)
+ case "toggle":
+ g.startToggle(sess, p, obj)
+ default:
+ sess.WriteLine(fmt.Sprintf("You can't %s that.", verb))
+ }
+}
+
+func (g *Game) CancelAction(p *player.Player) {
+ p.Action = nil
+}
+
+func (g *Game) AdvanceActions() {
+ if g.Hub == nil {
+ return
+ }
+ for _, sess := range g.Hub.AllSessions() {
+ p, ok := sess.Player.(*player.Player)
+ if !ok || p.Action == nil {
+ continue
+ }
+ if !p.Action.Advance() {
+ continue
+ }
+ switch p.Action.Type {
+ case "gather":
+ g.advanceGather(sess, p)
+ case "use":
+ g.advanceUse(sess, p)
+ }
+ }
+}
+
+func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.ObjectDef, st *world.ObjState) {
+ cfg, err := g.BehaviorStore.LoadGather(obj.BehaviorID)
+ if err != nil {
+ sess.WriteLine("Something is wrong with this object.")
+ return
+ }
+
+ skillLevel := p.Level(player.SkillName(cfg.Skill))
+ if cfg.Level > 0 && skillLevel < cfg.Level {
+ sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", cfg.Level, cfg.Skill))
+ return
+ }
+
+ if st.Depleted {
+ sess.WriteLine(fmt.Sprintf("The %s is depleted for %d more ticks.", obj.Name, st.DepleteTimer))
+ return
+ }
+
+ wait := cfg.BaseWait
+
+ if cfg.Tool != "" {
+ bestSpeed := -1
+ for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot == nil {
+ continue
+ }
+ def, err := g.ItemStore.Load(slot.ItemID)
+ if err == nil && def.ToolType == cfg.Tool && def.ToolSpeed > bestSpeed {
+ bestSpeed = def.ToolSpeed
+ }
+ }
+ for _, itemID := range p.Toolbelt {
+ def, err := g.ItemStore.Load(itemID)
+ if err == nil && def.ToolType == cfg.Tool && def.ToolSpeed > bestSpeed {
+ bestSpeed = def.ToolSpeed
+ }
+ }
+ if bestSpeed < 0 {
+ sess.WriteLine(fmt.Sprintf("You need a %s to mine the %s.", cfg.Tool, obj.Name))
+ return
+ }
+ wait = cfg.BaseWait - bestSpeed
+ if wait < 1 {
+ wait = 1
+ }
+ }
+
+ if p.FirstFreeSlot() == -1 {
+ sess.WriteLine("Your inventory is too full!")
+ return
+ }
+
+ p.Action = &action.Action{
+ Type: "gather",
+ TargetID: st.DefID,
+ TargetName: obj.Name,
+ WaitLeft: 0,
+ Data: map[string]any{
+ "behavior_id": obj.BehaviorID,
+ "instance_key": g.World.ObjStateKey(st.RoomID, st.DefID, st.Index),
+ "instance_idx": st.Index + 1,
+ "effective_wait": wait,
+ "step": 0,
+ },
+ }
+}
+
+func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
+ behaviorID := p.Action.Data["behavior_id"].(string)
+ cfg, err := g.BehaviorStore.LoadGather(behaviorID)
+ if err != nil {
+ g.CancelAction(p)
+ return
+ }
+
+ step := p.Action.Data["step"].(int)
+ wait := p.Action.Data["effective_wait"].(int)
+ instanceKey := p.Action.Data["instance_key"].(string)
+
+ if step == 0 {
+ sess.WriteLine(fmt.Sprintf("\n%s", cfg.GatherMsg))
+ p.Action.Data["step"] = 1
+ p.Action.WaitLeft = wait
+ return
+ }
+
+ if step == 2 {
+ st := g.World.GetObjStateByKey(instanceKey)
+ if st != nil && st.Depleted {
+ p.Action.WaitLeft = 3
+ return
+ }
+ if cfg.RespawnMsg != "" {
+ sess.WriteLine(fmt.Sprintf("\n%s", cfg.RespawnMsg))
+ }
+ p.Action.Data["step"] = 0
+ p.Action.WaitLeft = wait
+ return
+ }
+
+ skillLevel := p.Level(player.SkillName(cfg.Skill))
+ chance := action.SuccessChance(cfg.Success, skillLevel, cfg.Level)
+
+ if rand.Float64() < chance {
+ drop := g.BehaviorStore.ResolveDrop(cfg.Drops)
+ if drop != nil {
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot == -1 {
+ sess.WriteLine("Your inventory is too full!")
+ g.CancelAction(p)
+ return
+ }
+
+ qty := drop.Quantity
+ if qty <= 0 {
+ qty = 1
+ }
+
+ itemName := drop.ItemID
+ if def, err := g.ItemStore.Load(drop.ItemID); err == nil {
+ itemName = def.Name
+ }
+
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty})
+
+ msg := drop.Message
+ if msg == "" {
+ msg = fmt.Sprintf("You manage to get some %s.", itemName)
+ }
+ sess.WriteLine(msg)
+
+ g.AccountStore.SaveCharacter(p)
+
+ if drop.Depletes {
+ delay := cfg.DepleteDelay
+ if delay <= 0 {
+ delay = 10
+ }
+ st := g.World.GetObjStateByKey(instanceKey)
+ if st != nil {
+ st.Depleted = true
+ st.DepleteTimer = delay
+ }
+
+ for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
+ if other == sess {
+ continue
+ }
+ op, ok := other.Player.(*player.Player)
+ if !ok || op.Action == nil || op.Action.Type != "gather" {
+ continue
+ }
+ if op.Action.Data["instance_key"] == instanceKey {
+ other.WriteLine(fmt.Sprintf("\nThe %s was depleted by %s!", p.Action.TargetName, p.Name))
+ g.CancelAction(op)
+ }
+ }
+
+ p.Action.Data["step"] = 2
+ p.Action.WaitLeft = 1
+ return
+ }
+
+ // Non-depleting drop: loop back. Check inventory before looping.
+ if p.FirstFreeSlot() == -1 {
+ sess.WriteLine("Your inventory is too full!")
+ g.CancelAction(p)
+ return
+ }
+ p.Action.Data["step"] = 0
+ p.Action.WaitLeft = wait
+ return
+ }
+ }
+
+ // Failed: show fail message, loop back
+ sess.WriteLine(cfg.FailMsg)
+ if p.FirstFreeSlot() == -1 {
+ sess.WriteLine("Your inventory is too full!")
+ g.CancelAction(p)
+ return
+ }
+ p.Action.Data["step"] = 0
+ p.Action.WaitLeft = wait
+}
+
+func (g *Game) startMobTalk(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ cfg, err := g.BehaviorStore.LoadTalk(mob.BehaviorID)
+ if err != nil {
+ sess.WriteLine("This person has nothing to say.")
+ return
+ }
+
+ if node, ok := cfg.Nodes["start"]; ok {
+ p.Action = &action.Action{
+ Type: "talk",
+ TargetID: mob.DefID,
+ TargetName: mob.Name,
+ Data: map[string]any{"node": "start", "behavior_id": mob.BehaviorID},
+ }
+ g.showTalkNode(sess, node)
+ } else {
+ sess.WriteLine("This person has nothing to say.")
+ }
+}
+
+func (g *Game) startTalk(sess *net.Session, p *player.Player, obj *object.ObjectDef) {
+ cfg, err := g.BehaviorStore.LoadTalk(obj.BehaviorID)
+ if err != nil {
+ sess.WriteLine("This person has nothing to say.")
+ return
+ }
+
+ if node, ok := cfg.Nodes["start"]; ok {
+ p.Action = &action.Action{
+ Type: "talk",
+ TargetID: obj.ID,
+ TargetName: obj.Name,
+ Data: map[string]any{"node": "start", "behavior_id": obj.BehaviorID},
+ }
+ g.showTalkNode(sess, node)
+ } else {
+ sess.WriteLine("This person has nothing to say.")
+ }
+}
+
+func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) {
+ sess.WriteLine(fmt.Sprintf("\n%s", node.Message))
+
+ if node.Action != nil {
+ g.applyNodeAction(sess, node.Action)
+ }
+
+ if len(node.Options) == 0 {
+ sess.State = net.StateGame
+ sess.Write("\r\n> ")
+ return
+ }
+
+ for i, opt := range node.Options {
+ if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) { continue
+ }
+ sess.WriteLine(fmt.Sprintf(" %d. %s", i+1, opt.Text))
+ }
+ sess.State = net.StateTalk
+ sess.Write("\nChoice: ")
+}
+
+func (g *Game) handleTalkInput(sess *net.Session, input string) {
+ p, ok := sess.Player.(*player.Player)
+ if !ok || p.Action == nil || p.Action.Type != "talk" {
+ sess.State = net.StateGame
+ sess.Write("\r\n> ")
+ return
+ }
+
+ input = strings.TrimSpace(input)
+ lower := strings.ToLower(input)
+ if lower == "bye" || lower == "exit" || lower == "end" || lower == "quit" {
+ g.CancelAction(p)
+ sess.State = net.StateGame
+ sess.Write("\r\n> ")
+ return
+ }
+
+ behaviorID := p.Action.Data["behavior_id"].(string)
+ cfg, err := g.BehaviorStore.LoadTalk(behaviorID)
+ if err != nil {
+ g.CancelAction(p)
+ sess.State = net.StateGame
+ sess.Write("\r\n> ")
+ return
+ }
+
+ nodeKey := p.Action.Data["node"].(string)
+ node, ok := cfg.Nodes[nodeKey]
+ if !ok {
+ g.CancelAction(p)
+ sess.State = net.StateGame
+ sess.Write("\r\n> ")
+ return
+ }
+
+ idx, err := parseIndex(input)
+ if err != nil || idx <= 0 {
+ g.CancelAction(p)
+ sess.State = net.StateGame
+ sess.Write("\r\n> ")
+ return
+ }
+
+ validIdx := 0
+ var chosen *action.TalkOption
+ for i := range node.Options {
+ opt := &node.Options[i]
+ if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) { continue
+ }
+ validIdx++
+ if validIdx == idx {
+ chosen = opt
+ break
+ }
+ }
+
+ if chosen == nil {
+ g.CancelAction(p)
+ sess.State = net.StateGame
+ sess.Write("\r\n> ")
+ return
+ }
+
+ if chosen.End {
+ g.CancelAction(p)
+ sess.State = net.StateGame
+ sess.Write("\r\n> ")
+ return
+ }
+
+ if chosen.Goto != "" {
+ if nextNode, ok := cfg.Nodes[chosen.Goto]; ok {
+ p.Action.Data["node"] = chosen.Goto
+ g.showTalkNode(sess, nextNode)
+ return
+ }
+ }
+
+ g.CancelAction(p)
+ sess.State = net.StateGame
+ sess.Write("\r\n> ")
+}
+
+func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) {
+ p, ok := sess.Player.(*player.Player)
+ if !ok {
+ return
+ }
+ for k, v := range na.SetFlags {
+ g.WorldFlags[k] = v
+ }
+ if na.TakeItem != "" {
+ if p.HasItem(na.TakeItem) {
+ p.RemoveItem(na.TakeItem, 1)
+ }
+ }
+ if na.GiveItem != "" {
+ slot := p.FirstFreeSlot()
+ if slot == -1 {
+ sess.WriteLine("Your inventory is too full to receive that.")
+ } else {
+ p.SetInvSlot(slot, &player.InventorySlot{ItemID: na.GiveItem, Quantity: 1})
+ g.AccountStore.SaveCharacter(p)
+ }
+ }
+}
+
+func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool {
+ p, _ := sess.Player.(*player.Player)
+ if c.Flag != "" {
+ val, ok := g.WorldFlags[c.Flag]
+ if c.Not {
+ return !ok || val != c.Value
+ }
+ return ok && val == c.Value
+ }
+ if c.HasItem != "" {
+ has := p != nil && p.HasItem(c.HasItem)
+ if c.Not {
+ return !has
+ }
+ return has
+ }
+ return true
+}
+
+func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectDef) {
+ cfg, err := g.BehaviorStore.LoadUse(obj.BehaviorID)
+ if err != nil {
+ sess.WriteLine("You can't use this.")
+ return
+ }
+
+ for itemID, qty := range cfg.Consume {
+ if !p.HasItem(itemID) {
+ defName := itemID
+ if def, err := g.ItemStore.Load(itemID); err == nil {
+ defName = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("You need %s to use this.", defName))
+ return
+ }
+ _ = qty
+ }
+
+ if cfg.Success == nil && p.FirstFreeSlot() == -1 {
+ sess.WriteLine("Your inventory is full.")
+ return
+ }
+
+ p.Action = &action.Action{
+ Type: "use",
+ TargetID: obj.ID,
+ TargetName: obj.Name,
+ WaitLeft: 1,
+ Data: map[string]any{"step": 0, "behavior_id": obj.BehaviorID},
+ }
+
+ sess.WriteLine(fmt.Sprintf("\n%s", cfg.Message))
+}
+
+func (g *Game) advanceUse(sess *net.Session, p *player.Player) {
+ behaviorID := p.Action.Data["behavior_id"].(string)
+ cfg, err := g.BehaviorStore.LoadUse(behaviorID)
+ if err != nil {
+ g.CancelAction(p)
+ return
+ }
+
+ for itemID, qty := range cfg.Consume {
+ if !p.HasItem(itemID) {
+ defName := itemID
+ if def, err := g.ItemStore.Load(itemID); err == nil {
+ defName = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("You've run out of %s.", defName))
+ g.CancelAction(p)
+ return
+ }
+ if !p.RemoveItem(itemID, qty) {
+ sess.WriteLine(fmt.Sprintf("You need %d of %s.", qty, itemID))
+ g.CancelAction(p)
+ return
+ }
+ }
+
+ if cfg.Success != nil {
+ skillLevel := p.Level(player.SkillName(cfg.Skill))
+ chance := action.SuccessChance(*cfg.Success, skillLevel, cfg.Level)
+ if rand.Float64() >= chance {
+ if cfg.FailMsg != "" {
+ sess.WriteLine(cfg.FailMsg)
+ }
+ p.Action.WaitLeft = cfg.Wait
+ return
+ }
+ }
+
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot == -1 {
+ sess.WriteLine("Your inventory is full.")
+ g.CancelAction(p)
+ return
+ }
+
+ qty := cfg.Reward.Quantity
+ if qty <= 0 {
+ qty = 1
+ }
+
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: cfg.Reward.ItemID, Quantity: qty})
+
+ g.AccountStore.SaveCharacter(p)
+
+ itemName := cfg.Reward.ItemID
+ if def, err := g.ItemStore.Load(cfg.Reward.ItemID); err == nil {
+ itemName = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("You make a %s.", itemName))
+
+ if p.FirstFreeSlot() == -1 {
+ sess.WriteLine("Your inventory is full.")
+ g.CancelAction(p)
+ return
+ }
+
+ p.Action.WaitLeft = cfg.Wait
+}
+
+func (g *Game) startToggle(sess *net.Session, p *player.Player, obj *object.ObjectDef) {
+ cfg, err := g.BehaviorStore.LoadToggle(obj.BehaviorID)
+ if err != nil {
+ sess.WriteLine("Nothing happens.")
+ return
+ }
+
+ if cfg.Check != nil {
+ val, ok := g.WorldFlags[cfg.Check.Flag]
+ if cfg.Check.Not {
+ if ok && val == cfg.Check.Value {
+ sess.WriteLine("Nothing happens.")
+ return
+ }
+ } else {
+ if !ok || val != cfg.Check.Value {
+ sess.WriteLine("Nothing happens.")
+ return
+ }
+ }
+ }
+
+ for flag, val := range cfg.SetFlags {
+ g.WorldFlags[flag] = val
+ }
+
+ sess.WriteLine(fmt.Sprintf("\n%s", cfg.Message))
+
+ if g.Hub != nil {
+ for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
+ if other != sess && other.Player != nil {
+ other.WriteLine(fmt.Sprintf("\n%s uses the %s.", p.Name, obj.Name))
+ }
+ }
+ }
+}
+
+func normalizeVerb(v string) string {
+ switch v {
+ case "mine", "chop", "fish":
+ return "gather"
+ case "talk", "speak", "ask":
+ return "talk"
+ case "pull", "push":
+ return "toggle"
+ }
+ return v
+}
+
+func (g *Game) CheckExitCondition(c *world.ExitCondition) bool {
+ if c.Flag != "" {
+ val, ok := g.WorldFlags[c.Flag]
+ if c.Not {
+ return !ok || val != c.Value
+ }
+ return ok && val == c.Value
+ }
+ return true
+}
+
+func (g *Game) RunEnterSteps(sess *net.Session, roomID int) {
+ room, err := g.World.LoadRoom(roomID)
+ if err != nil || len(room.OnEnter) == 0 {
+ return
+ }
+ for _, step := range room.OnEnter {
+ if step.Condition != nil && !g.CheckExitCondition(step.Condition) {
+ continue
+ }
+ if step.Message != "" {
+ sess.WriteLine(fmt.Sprintf("\n%s", step.Message))
+ }
+ }
+}
+
+func (g *Game) BroadcastRespawns() {
+ respawns := g.World.FlushObjRespawns()
+ for _, st := range respawns {
+ def, err := g.ObjectStore.Load(st.DefID)
+ if err != nil || def.BehaviorID == "" {
+ continue
+ }
+ cfg, err := g.BehaviorStore.LoadGather(def.BehaviorID)
+ if err != nil || cfg.RespawnBroadcast == "" {
+ continue
+ }
+ name := def.Name
+ if idx := st.Index + 1; len(g.World.FindObjInstances(st.RoomID, st.DefID)) > 1 {
+ name += fmt.Sprintf(" [%d]", idx)
+ }
+ msg := strings.ReplaceAll(cfg.RespawnBroadcast, "{name}", name)
+ if g.Hub != nil {
+ for _, sess := range g.Hub.PlayersInRoom(st.RoomID) {
+ sess.WriteLine(fmt.Sprintf("\n%s", msg))
+ }
+ }
+ }
+}