aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-15 02:57:35 -0400
committerhistoria <[not public]>2026-06-15 02:58:50 -0400
commit4cc1ddcd185509080b4b3e15d1646567edd51c9d (patch)
tree2a564846ad4fd159a2d7e55451bfa9569fe8c0b8 /internal/game
parent445c4612cc5cf2c226f85894b6ad1e2419a374e2 (diff)
downloadthehouseoficarus-4cc1ddcd185509080b4b3e15d1646567edd51c9d.tar.gz
removed game speed for being broken and an antifeature anyway. added color support.
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/action.go41
-rw-r--r--internal/game/action_burn.go12
-rw-r--r--internal/game/action_default_target.go10
-rw-r--r--internal/game/action_gather.go71
-rw-r--r--internal/game/action_search.go3
-rw-r--r--internal/game/action_talk.go20
-rw-r--r--internal/game/action_use.go7
-rw-r--r--internal/game/cmd_attack.go36
-rw-r--r--internal/game/cmd_description.go2
-rw-r--r--internal/game/cmd_drop.go14
-rw-r--r--internal/game/cmd_get.go55
-rw-r--r--internal/game/cmd_look.go6
-rw-r--r--internal/game/cmd_move.go7
-rw-r--r--internal/game/cmd_quit.go3
-rw-r--r--internal/game/color.go22
-rw-r--r--internal/game/game.go114
-rw-r--r--internal/game/tick.go1
17 files changed, 268 insertions, 156 deletions
diff --git a/internal/game/action.go b/internal/game/action.go
index bb4bdac..26484e0 100644
--- a/internal/game/action.go
+++ b/internal/game/action.go
@@ -14,6 +14,32 @@ import (
"thirdcollapse/internal/world"
)
+var verbAliases = map[string]string{
+ "mine": "gather",
+ "chop": "gather",
+ "fish": "gather",
+ "cut": "gather",
+ "talk": "talk",
+ "speak": "talk",
+ "ask": "talk",
+ "pull": "toggle",
+ "push": "toggle",
+}
+
+var verbSkill = map[string]string{
+ "mine": "mining",
+ "chop": "woodcutting",
+ "cut": "woodcutting",
+ "fish": "fishing",
+}
+
+func normalizeVerb(v string) string {
+ if a, ok := verbAliases[v]; ok {
+ return a
+ }
+ return v
+}
+
func (g *Game) StartAction(sess *net.Session, verb, target string) {
p := sess.Player.(*player.Player)
@@ -199,21 +225,12 @@ func (g *Game) AdvanceActions() {
case "search":
g.advanceSearch(sess, p)
}
+ if p.Action == nil {
+ g.writePrompt(sess)
+ }
}
}
-func normalizeVerb(v string) string {
- switch v {
- case "mine", "chop", "fish", "cut":
- return "gather"
- case "talk", "speak", "ask":
- return "talk"
- case "pull", "push":
- return "toggle"
- }
- return v
-}
-
func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool {
p, _ := sess.Player.(*player.Player)
diff --git a/internal/game/action_burn.go b/internal/game/action_burn.go
index b16ba42..5c19510 100644
--- a/internal/game/action_burn.go
+++ b/internal/game/action_burn.go
@@ -5,6 +5,7 @@ import (
"math/rand"
"thirdcollapse/internal/action"
+ "thirdcollapse/internal/engine"
"thirdcollapse/internal/net"
"thirdcollapse/internal/object"
"thirdcollapse/internal/player"
@@ -146,7 +147,7 @@ func (g *Game) startStoke(sess *net.Session, p *player.Player, itemID string) {
Type: "stoke",
TargetID: itemID,
TargetName: logDef.Name,
- WaitLeft: g.computeTicks(10),
+ WaitLeft: engine.ToTicks(10),
Data: map[string]any{
"item_id": itemID,
"fire_key": fireKey,
@@ -182,7 +183,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) {
g.broadcastDrop(p, logDef.Name)
data["phase"] = 1
- p.Action.WaitLeft = g.computeTicks(toolSpeed)
+ p.Action.WaitLeft = engine.ToTicks(toolSpeed)
return
}
@@ -196,7 +197,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) {
if phase == 1 {
sess.WriteLine(fmt.Sprintf("You try burning the %s on the ground...", logDef.Name))
data["phase"] = 2
- p.Action.WaitLeft = g.computeTicks(toolSpeed)
+ p.Action.WaitLeft = engine.ToTicks(toolSpeed)
return
}
@@ -240,7 +241,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) {
sess.WriteLine("You can't seem to get a fire going.")
data["phase"] = 1
- p.Action.WaitLeft = g.computeTicks(toolSpeed)
+ p.Action.WaitLeft = engine.ToTicks(toolSpeed)
}
}
@@ -293,7 +294,7 @@ func (g *Game) advanceStoke(sess *net.Session, p *player.Player) {
return
}
- p.Action.WaitLeft = g.computeTicks(10.0)
+ p.Action.WaitLeft = engine.ToTicks(10.0)
}
func (g *Game) findFireTool(sess *net.Session, p *player.Player) (toolSpeed float64, ok bool) {
@@ -423,6 +424,7 @@ func (g *Game) cancelStokers(roomID int) {
if ok && op.Action != nil && op.Action.Type == "stoke" {
other.WriteLine("The fire went out!")
g.CancelAction(op)
+ g.writePrompt(other)
}
}
}
diff --git a/internal/game/action_default_target.go b/internal/game/action_default_target.go
index 45060cc..b3825ca 100644
--- a/internal/game/action_default_target.go
+++ b/internal/game/action_default_target.go
@@ -5,15 +5,7 @@ import (
)
func verbToSkill(verb string) string {
- switch verb {
- case "mine":
- return "mining"
- case "chop", "cut":
- return "woodcutting"
- case "fish":
- return "fishing"
- }
- return ""
+ return verbSkill[verb]
}
func (g *Game) resolveDefaultTarget(roomID int, verb string) string {
diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go
index 4e1e609..08d2589 100644
--- a/internal/game/action_gather.go
+++ b/internal/game/action_gather.go
@@ -6,6 +6,8 @@ import (
"strings"
"thirdcollapse/internal/action"
+ "thirdcollapse/internal/color"
+ "thirdcollapse/internal/engine"
"thirdcollapse/internal/net"
"thirdcollapse/internal/object"
"thirdcollapse/internal/player"
@@ -176,6 +178,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
wait := p.Action.Data["effective_wait"].(float64)
instanceKey := p.Action.Data["instance_key"].(string)
_, shared := p.Action.Data["deplete_timer"]
+ mode := g.colorMode(sess)
if step == 0 {
if cfg.Bait != "" {
@@ -184,28 +187,28 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
if def, err := g.ItemStore.Load(cfg.Bait); err == nil {
baitName = def.Name
}
- sess.WriteLine(fmt.Sprintf("You've run out of %s.", baitName))
+ sess.WriteLine(color.Wrap(mode, "red", fmt.Sprintf("You've run out of %s.", baitName)))
g.CancelAction(p)
return
}
}
- sess.WriteLine(fmt.Sprintf("\n%s", cfg.GatherMsg))
+ sess.WriteLine(fmt.Sprintf("\n%s", color.Tag(mode, cfg.GatherMsg)))
p.Action.Data["step"] = 1
- p.Action.WaitLeft = g.computeTicks(wait)
+ p.Action.WaitLeft = engine.ToTicks(wait)
return
}
if step == 2 {
st := g.World.GetObjStateByKey(instanceKey)
if st != nil && st.Depleted {
- p.Action.WaitLeft = g.computeTicks(3)
+ p.Action.WaitLeft = engine.ToTicks(3)
return
}
if cfg.RespawnMsg != "" {
- sess.WriteLine(fmt.Sprintf("\n%s", cfg.RespawnMsg))
+ sess.WriteLine(fmt.Sprintf("\n%s", color.Tag(mode, cfg.RespawnMsg)))
}
p.Action.Data["step"] = 0
- p.Action.WaitLeft = g.computeTicks(wait)
+ p.Action.WaitLeft = engine.ToTicks(wait)
return
}
@@ -215,7 +218,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
if def, err := g.ItemStore.Load(cfg.Bait); err == nil {
baitName = def.Name
}
- sess.WriteLine(fmt.Sprintf("You've run out of %s.", baitName))
+ sess.WriteLine(color.Wrap(mode, "red", fmt.Sprintf("You've run out of %s.", baitName)))
g.CancelAction(p)
return
}
@@ -231,7 +234,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
if drop != nil {
freeSlot := p.FirstFreeSlot()
if freeSlot == -1 {
- sess.WriteLine("Your inventory is too full!")
+ sess.WriteLine(color.Wrap(mode, "red", "Your inventory is too full!"))
g.CancelAction(p)
return
}
@@ -253,17 +256,19 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
}
if xp > 0 {
if newLevel := p.AddSkillXP(player.SkillName(cfg.Skill), xp); newLevel > 0 {
- sess.WriteLine(fmt.Sprintf("*** You are now level %d %s! ***", newLevel, cfg.Skill))
+ sess.WriteLine(color.Wrap(mode, "bright_yellow", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, cfg.Skill)))
}
}
g.AccountStore.SaveCharacter(p)
msg := drop.Message
if msg == "" {
- msg = fmt.Sprintf("You manage to get some %s.", itemName)
+ msg = fmt.Sprintf("You manage to get some %s.", color.Wrap(mode, "green", itemName))
+ } else {
+ msg = color.Tag(mode, msg)
}
if xp > 0 && p.OptionBool("xp_drops") {
- msg += fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.SkillName(cfg.Skill)])
+ msg += color.Wrap(mode, "yellow", fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.SkillName(cfg.Skill)]))
}
sess.WriteLine(msg)
@@ -279,13 +284,13 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
playerNames: []string{p.Name},
})
p.Action.Data["step"] = 2
- p.Action.WaitLeft = g.computeTicks(1)
+ p.Action.WaitLeft = engine.ToTicks(1)
return
}
}
if !shared && drop.Depletes {
- delay := g.computeTicks(cfg.RespawnTimer)
+ delay := engine.ToTicks(cfg.RespawnTimer)
if delay <= 0 {
delay = 10
}
@@ -295,44 +300,45 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
st.DepleteTimer = float64(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)
- }
+ 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!", color.Wrap(mode, "green", p.Action.TargetName), color.Wrap(mode, "bright_white", p.Name)))
+ g.CancelAction(op)
+ g.writePrompt(other)
+ }
+ }
p.Action.Data["step"] = 2
- p.Action.WaitLeft = g.computeTicks(1)
+ p.Action.WaitLeft = engine.ToTicks(1)
return
}
if p.FirstFreeSlot() == -1 {
- sess.WriteLine("Your inventory is too full!")
+ sess.WriteLine(color.Wrap(mode, "red", "Your inventory is too full!"))
g.CancelAction(p)
return
}
p.Action.Data["step"] = 0
- p.Action.WaitLeft = g.computeTicks(wait)
+ p.Action.WaitLeft = engine.ToTicks(wait)
return
}
}
- sess.WriteLine(cfg.FailMsg)
+ sess.WriteLine(color.Tag(mode, cfg.FailMsg))
if p.FirstFreeSlot() == -1 {
- sess.WriteLine("Your inventory is too full!")
+ sess.WriteLine(color.Wrap(mode, "red", "Your inventory is too full!"))
g.CancelAction(p)
return
}
p.Action.Data["step"] = 0
- p.Action.WaitLeft = g.computeTicks(wait)
+ p.Action.WaitLeft = engine.ToTicks(wait)
}
func filterDropsByLevel(drops []action.DropEntry, level int) []action.DropEntry {
@@ -350,7 +356,7 @@ func filterDropsByLevel(drops []action.DropEntry, level int) []action.DropEntry
func (g *Game) depleteSharedTree(st *world.ObjState, cfg *action.GatherConfig, targetName string, playerNames []string) {
st.Depleted = true
- st.DepleteTimer = float64(g.computeTicks(cfg.RespawnTimer))
+ st.DepleteTimer = float64(engine.ToTicks(cfg.RespawnTimer))
st.SharedTimer = 0
msg := cfg.ExhaustedMessage
@@ -379,6 +385,7 @@ func (g *Game) depleteSharedTree(st *world.ObjState, cfg *action.GatherConfig, t
}
sess.WriteLine(fmt.Sprintf("\n%s", msg))
g.CancelAction(op)
+ g.writePrompt(sess)
}
}
diff --git a/internal/game/action_search.go b/internal/game/action_search.go
index 07bae44..e9d3d46 100644
--- a/internal/game/action_search.go
+++ b/internal/game/action_search.go
@@ -4,6 +4,7 @@ import (
"fmt"
"thirdcollapse/internal/action"
+ "thirdcollapse/internal/engine"
"thirdcollapse/internal/net"
"thirdcollapse/internal/player"
)
@@ -24,7 +25,7 @@ func (g *Game) startSearch(sess *net.Session, p *player.Player, itemID string, s
Type: "search",
TargetID: itemID,
TargetName: def.Name,
- WaitLeft: g.computeTicks(ticks),
+ WaitLeft: engine.ToTicks(ticks),
Data: map[string]any{
"item_id": itemID,
"slot_idx": slotIdx,
diff --git a/internal/game/action_talk.go b/internal/game/action_talk.go
index 8aa95be..4102841 100644
--- a/internal/game/action_talk.go
+++ b/internal/game/action_talk.go
@@ -54,7 +54,7 @@ func (g *Game) startTalk(sess *net.Session, p *player.Player, obj *object.Object
}
func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) {
- sess.WriteLine(fmt.Sprintf("\n%s", node.Message))
+ sess.WriteLine(fmt.Sprintf("\n%s", g.colorTag(sess, node.Message)))
if node.Action != nil {
g.applyNodeAction(sess, node.Action)
@@ -62,7 +62,7 @@ func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) {
if len(node.Options) == 0 {
sess.State = net.StateGame
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
return
}
@@ -82,7 +82,7 @@ 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> ")
+ g.writePrompt(sess)
return
}
@@ -91,7 +91,7 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
if lower == "bye" || lower == "exit" || lower == "end" || lower == "quit" {
g.CancelAction(p)
sess.State = net.StateGame
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
return
}
@@ -100,7 +100,7 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
if err != nil {
g.CancelAction(p)
sess.State = net.StateGame
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
return
}
@@ -109,7 +109,7 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
if !ok {
g.CancelAction(p)
sess.State = net.StateGame
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
return
}
@@ -117,7 +117,7 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
if err != nil || idx <= 0 {
g.CancelAction(p)
sess.State = net.StateGame
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
return
}
@@ -138,14 +138,14 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
if chosen == nil {
g.CancelAction(p)
sess.State = net.StateGame
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
return
}
if chosen.End {
g.CancelAction(p)
sess.State = net.StateGame
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
return
}
@@ -159,7 +159,7 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
g.CancelAction(p)
sess.State = net.StateGame
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
}
func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) {
diff --git a/internal/game/action_use.go b/internal/game/action_use.go
index ec63723..d953521 100644
--- a/internal/game/action_use.go
+++ b/internal/game/action_use.go
@@ -5,6 +5,7 @@ import (
"math/rand"
"thirdcollapse/internal/action"
+ "thirdcollapse/internal/engine"
"thirdcollapse/internal/net"
"thirdcollapse/internal/object"
"thirdcollapse/internal/player"
@@ -39,7 +40,7 @@ func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectD
Type: "use",
TargetID: obj.ID,
TargetName: obj.Name,
- WaitLeft: g.computeTicks(1),
+ WaitLeft: engine.ToTicks(1),
Data: map[string]any{"step": 0, "behavior_id": obj.BehaviorID},
}
@@ -78,7 +79,7 @@ func (g *Game) advanceUse(sess *net.Session, p *player.Player) {
if cfg.FailMsg != "" {
sess.WriteLine(cfg.FailMsg)
}
- p.Action.WaitLeft = g.computeTicks(cfg.Wait)
+ p.Action.WaitLeft = engine.ToTicks(cfg.Wait)
return
}
}
@@ -119,5 +120,5 @@ func (g *Game) advanceUse(sess *net.Session, p *player.Player) {
return
}
- p.Action.WaitLeft = g.computeTicks(cfg.Wait)
+ p.Action.WaitLeft = engine.ToTicks(cfg.Wait)
}
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index ca5bc4c..08780c3 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -6,7 +6,9 @@ import (
"strconv"
"strings"
+ "thirdcollapse/internal/color"
"thirdcollapse/internal/combat"
+ "thirdcollapse/internal/engine"
"thirdcollapse/internal/net"
"thirdcollapse/internal/object"
"thirdcollapse/internal/player"
@@ -130,10 +132,11 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
if len(styleParts) > 0 {
styleStr = " Style: " + string(p.AttackStyle) + " (" + strings.Join(styleParts, ", ") + ")"
}
- sess.WriteLine(fmt.Sprintf("\nYou attack %s!%s", mobDisplayName(mob, true), styleStr))
+ mode := g.colorMode(sess)
+ sess.WriteLine(fmt.Sprintf("\nYou attack %s!%s", color.Wrap(mode, "bright_red", mobDisplayName(mob, true)), styleStr))
p.ActionState = &ActionState{Type: ActionCombating, TargetName: mob.Name}
- g.Ticks.Subscribe(g.computeTicks(playerSpeed), func() bool {
+ g.Ticks.Subscribe(engine.ToTicks(playerSpeed), func() bool {
cs := combat.GetCombat(p.Name)
if cs == nil || !cs.Active {
return false
@@ -151,7 +154,7 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
return true
})
- g.Ticks.Subscribe(g.computeTicks(mob.Speed), func() bool {
+ g.Ticks.Subscribe(engine.ToTicks(mob.Speed), func() bool {
cs := combat.GetCombat(p.Name)
if cs == nil || !cs.Active {
return false
@@ -182,6 +185,8 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI
}
}
+ mode := g.colorMode(sess)
+
attRoll := combat.AttackRoll(p.Level(player.Attack), attBonus, equipAtt)
defRoll := combat.DefenseRoll(mob.Defense, 0, 0)
@@ -199,7 +204,7 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI
gains, leveled := g.awardCombatXP(p, dmg)
for _, skill := range leveled {
- sess.WriteLine(fmt.Sprintf("*** You are now level %d %s! ***", p.Level(skill), skill))
+ sess.WriteLine(color.Wrap(mode, "bright_yellow", fmt.Sprintf("*** You are now level %d %s! ***", p.Level(skill), skill)))
}
mobName := mobDisplayName(mob, true)
@@ -212,7 +217,7 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI
w = w2
}
g.combatPadWidth = w
- prefix := fmt.Sprintf(" You hit %s for %d damage.", mobName, dmg)
+ prefix := fmt.Sprintf(" You hit %s for %s damage.", color.Wrap(mode, "bright_red", mobName), color.Wrap(mode, "red", fmt.Sprint(dmg)))
hpPart := fmt.Sprintf("[%d/%dhp]", mob.HP, mob.MaxHP)
line := fmt.Sprintf("%-*s %s", g.combatPadWidth, prefix, hpPart)
if p.OptionBool("xp_drops") && len(gains) > 0 {
@@ -220,11 +225,11 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI
for _, gain := range gains {
parts = append(parts, fmt.Sprintf("+%dxp %s", gain.XP, player.SkillAbbr[player.SkillName(gain.Skill)]))
}
- line += " (" + strings.Join(parts, ", ") + ")"
+ line += color.Wrap(mode, "yellow", " ("+strings.Join(parts, ", ")+")")
}
sess.WriteLine(line)
} else {
- sess.WriteLine(fmt.Sprintf(" You miss %s.", mobDisplayName(mob, true)))
+ sess.WriteLine(color.Wrap(mode, "dim", fmt.Sprintf(" You miss %s.", mobDisplayName(mob, true))))
}
}
@@ -242,6 +247,8 @@ func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInst
attRoll := combat.AttackRoll(mob.Attack, 0, 0)
defRoll := combat.DefenseRoll(p.Level(player.Defense), defBonus, equipDef)
+ mode := g.colorMode(sess)
+
if combat.HitCheck(attRoll, defRoll) {
maxHit := combat.MaxHit(mob.Strength, 0, 0)
dmg := combat.RollDamage(maxHit)
@@ -263,7 +270,7 @@ func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInst
w = w2
}
g.combatPadWidth = w
- prefix := fmt.Sprintf(" %s hits you for %d damage.", attacker, dmg)
+ prefix := fmt.Sprintf(" %s hits you for %s damage.", color.Wrap(mode, "bright_red", attacker), color.Wrap(mode, "red", fmt.Sprint(dmg)))
hpPart := fmt.Sprintf("[%d/%dhp]", p.HP, p.MaxHP())
sess.WriteLine(fmt.Sprintf("%-*s %s", g.combatPadWidth, prefix, hpPart))
} else {
@@ -271,7 +278,7 @@ func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInst
if !mob.Unique {
attacker = "The " + mob.Name
}
- sess.WriteLine(fmt.Sprintf(" %s misses you.", attacker))
+ sess.WriteLine(color.Wrap(mode, "dim", fmt.Sprintf(" %s misses you.", attacker)))
}
}
@@ -279,9 +286,10 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
g.combatPadWidth = 0
combat.LeaveCombat(p.Name)
p.ActionState = nil
+ mode := g.colorMode(sess)
if p.HP <= 0 {
- sess.WriteLine("\nOh dear, you are dead!")
+ sess.WriteLine(color.Wrap(mode, "red", "\nOh dear, you are dead!"))
g.dropItemsOnDeath(p)
p.HP = p.MaxHP()
p.RoomID = 1
@@ -290,11 +298,12 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
g.Hub.EnterRoom(sess, p.RoomID)
}
g.doLook(sess)
+ g.writePrompt(sess)
return
}
if mob != nil && mob.HP <= 0 {
- sess.WriteLine(fmt.Sprintf("\nYou have defeated %s!", mobDisplayName(mob, true)))
+ sess.WriteLine(color.Wrap(mode, "green", fmt.Sprintf("\nYou have defeated %s!", mobDisplayName(mob, true))))
if g.Hub != nil {
for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
if other != sess && other.Player != nil {
@@ -342,15 +351,16 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
}
}
- respawnTicks := g.computeTicks(mob.RespawnTicks)
+ respawnTicks := engine.ToTicks(mob.RespawnTicks)
if respawnTicks <= 0 {
- respawnTicks = g.computeTicks(30)
+ respawnTicks = engine.ToTicks(30)
}
instanceID := mob.InstanceID
g.Ticks.Subscribe(respawnTicks, func() bool {
g.respawnMob(instanceID)
return false
})
+ g.writePrompt(sess)
}
}
diff --git a/internal/game/cmd_description.go b/internal/game/cmd_description.go
index 6875840..56ce441 100644
--- a/internal/game/cmd_description.go
+++ b/internal/game/cmd_description.go
@@ -30,5 +30,5 @@ func (g *Game) handleDescriptionChange(sess *net.Session, input string) {
sess.WriteLine("Description left unchanged.")
}
sess.State = net.StateGame
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
}
diff --git a/internal/game/cmd_drop.go b/internal/game/cmd_drop.go
index dc506f0..2bf00ce 100644
--- a/internal/game/cmd_drop.go
+++ b/internal/game/cmd_drop.go
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
+ "thirdcollapse/internal/color"
"thirdcollapse/internal/net"
"thirdcollapse/internal/player"
)
@@ -90,6 +91,7 @@ func (g *Game) doDropAllNamed(sess *net.Session, input string) {
func (g *Game) doDrop(sess *net.Session, input string) {
p := sess.Player.(*player.Player)
g.CancelAction(p)
+ mode := g.colorMode(sess)
qty, itemName := parseQty(input)
@@ -149,9 +151,9 @@ func (g *Game) doDrop(sess *net.Session, input string) {
g.World.AddGroundItem(p.RoomID, itemID, qty)
g.AccountStore.SaveCharacter(p)
if qty == 1 {
- sess.WriteLine(fmt.Sprintf("You drop a %s.", name))
+ sess.WriteLine(fmt.Sprintf("You drop a %s.", color.Wrap(mode, "green", name)))
} else {
- sess.WriteLine(fmt.Sprintf("You drop %d x %s.", qty, name))
+ sess.WriteLine(fmt.Sprintf("You drop %d x %s.", qty, color.Wrap(mode, "green", name)))
}
return
}
@@ -190,15 +192,15 @@ func (g *Game) handleDropAllConfirm(sess *net.Session, input string) {
p, ok := sess.Player.(*player.Player)
if !ok {
sess.State = net.StateGame
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
return
}
- g.CancelAction(p)
+ g.CancelAction(p)
input = strings.TrimSpace(strings.ToLower(input))
if input != "y" && input != "yes" {
sess.State = net.StateGame
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
return
}
@@ -227,5 +229,5 @@ func (g *Game) handleDropAllConfirm(sess *net.Session, input string) {
sess.Write(fmt.Sprintf("\nYou drop your "))
formatPickupList(sess, dropped)
}
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
}
diff --git a/internal/game/cmd_get.go b/internal/game/cmd_get.go
index 7ec183f..9591657 100644
--- a/internal/game/cmd_get.go
+++ b/internal/game/cmd_get.go
@@ -3,6 +3,7 @@ package game
import (
"fmt"
+ "thirdcollapse/internal/color"
"thirdcollapse/internal/net"
"thirdcollapse/internal/player"
)
@@ -10,6 +11,7 @@ import (
func (g *Game) doGet(sess *net.Session, input string) {
p := sess.Player.(*player.Player)
g.CancelAction(p)
+ mode := g.colorMode(sess)
ground := g.World.GroundItems(p.RoomID)
if len(ground) == 0 {
sess.WriteLine("There's nothing on the ground to pick up.")
@@ -85,7 +87,7 @@ func (g *Game) doGet(sess *net.Session, input string) {
}
slot.Quantity += qty
g.AccountStore.SaveCharacter(p)
- sess.WriteLine(fmt.Sprintf("You pick up %d x %s. (now %d)", qty, def.Name, slot.Quantity))
+ sess.WriteLine(fmt.Sprintf("You pick up %d x %s. (now %d)", qty, color.Wrap(mode, "green", def.Name), slot.Quantity))
return
}
}
@@ -101,9 +103,9 @@ func (g *Game) doGet(sess *net.Session, input string) {
p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, qty))
g.AccountStore.SaveCharacter(p)
if qty == 1 {
- sess.WriteLine(fmt.Sprintf("You pick up a %s.", def.Name))
+ sess.WriteLine(fmt.Sprintf("You pick up a %s.", color.Wrap(mode, "green", def.Name)))
} else {
- sess.WriteLine(fmt.Sprintf("You pick up %d x %s.", qty, def.Name))
+ sess.WriteLine(fmt.Sprintf("You pick up %d x %s.", qty, color.Wrap(mode, "green", def.Name)))
}
return
}
@@ -393,35 +395,42 @@ func (g *Game) pickupCredits(sess *net.Session, p *player.Player, itemID string)
}
}
-func (g *Game) findGroundMatches(input string, roomID int) []itemMatch {
- ground := g.World.GroundItems(roomID)
+func (g *Game) collectMatches(input string, each func(func(itemID string, slot int) bool)) []itemMatch {
var matches []itemMatch
- for itemID := range ground {
+ each(func(itemID string, slot int) bool {
def, err := g.ItemStore.Load(itemID)
if err != nil {
- continue
+ return true
}
if def.MatchesName(input) {
- matches = append(matches, itemMatch{ID: itemID, Name: def.Name, Slot: -1})
+ matches = append(matches, itemMatch{ID: itemID, Name: def.Name, Slot: slot})
}
- }
+ return true
+ })
return matches
}
-func (g *Game) findInventoryMatches(input string, p *player.Player) []itemMatch {
- var matches []itemMatch
- for i := 0; i < 28; i++ {
- slot := p.InvSlot(i)
- if slot == nil {
- continue
- }
- def, err := g.ItemStore.Load(slot.ItemID)
- if err != nil {
- continue
+func (g *Game) findGroundMatches(input string, roomID int) []itemMatch {
+ ground := g.World.GroundItems(roomID)
+ return g.collectMatches(input, func(yield func(string, int) bool) {
+ for itemID := range ground {
+ if !yield(itemID, -1) {
+ return
+ }
}
- if def.MatchesName(input) {
- matches = append(matches, itemMatch{ID: slot.ItemID, Name: def.Name, Slot: i})
+ })
+}
+
+func (g *Game) findInventoryMatches(input string, p *player.Player) []itemMatch {
+ return g.collectMatches(input, func(yield func(string, int) bool) {
+ for i := 0; i < 28; i++ {
+ slot := p.InvSlot(i)
+ if slot == nil {
+ continue
+ }
+ if !yield(slot.ItemID, i) {
+ return
+ }
}
- }
- return matches
+ })
}
diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go
index ac0c42a..992f3d0 100644
--- a/internal/game/cmd_look.go
+++ b/internal/game/cmd_look.go
@@ -6,6 +6,7 @@ import (
"strconv"
"strings"
+ "thirdcollapse/internal/color"
"thirdcollapse/internal/combat"
"thirdcollapse/internal/net"
"thirdcollapse/internal/player"
@@ -20,16 +21,17 @@ func (g *Game) doLook(sess *net.Session) {
return
}
+ mode := g.colorMode(sess)
sess.WriteLines(
"",
- fmt.Sprintf("%s (#%d)", room.Name, room.ID),
+ fmt.Sprintf("%s (#%d)", color.Wrap(mode, "cyan", room.Name), room.ID),
)
descWidth := p.OptionInt("room_desc_width")
if descWidth <= 0 {
descWidth = 70
}
- descLines := wrapText(room.Description, descWidth)
+ descLines := wrapText(g.colorTag(sess, room.Description), descWidth)
wroteDesc := false
if p.OptionString("tiny_map") != "off" {
mapLines := buildTinyMap(g, p.RoomID, mapGlyphsForPlayer(p.OptionBool("unicode")))
diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go
index 0b6571e..65a2875 100644
--- a/internal/game/cmd_move.go
+++ b/internal/game/cmd_move.go
@@ -3,6 +3,7 @@ package game
import (
"fmt"
+ "thirdcollapse/internal/color"
"thirdcollapse/internal/combat"
"thirdcollapse/internal/net"
"thirdcollapse/internal/player"
@@ -64,6 +65,8 @@ func (g *Game) doMove(sess *net.Session, dir string) {
g.seedRoomMobs(p.RoomID)
g.seedRoomObjects(p.RoomID)
+ mode := g.colorMode(sess)
+
if g.Hub != nil {
for _, other := range g.Hub.PlayersInRoom(oldRoom) {
if other != sess {
@@ -78,14 +81,14 @@ func (g *Game) doMove(sess *net.Session, dir string) {
}
}
- sess.WriteLine(fmt.Sprintf("\nYou walk %s.", exitDir))
+ sess.WriteLine(fmt.Sprintf("\nYou walk %s.", color.Wrap(mode, "cyan", string(exitDir))))
p.ActionState = &ActionState{Type: ActionMoving, Direction: string(exitDir)}
if p.OptionBool("description") {
g.doLook(sess)
} else {
targetRoom, _ := g.World.LoadRoom(targetID)
if targetRoom != nil {
- sess.WriteLine(targetRoom.Name)
+ sess.WriteLine(color.Wrap(mode, "cyan", targetRoom.Name))
}
}
diff --git a/internal/game/cmd_quit.go b/internal/game/cmd_quit.go
index 90e7019..446305b 100644
--- a/internal/game/cmd_quit.go
+++ b/internal/game/cmd_quit.go
@@ -2,6 +2,7 @@ package game
import (
"thirdcollapse/internal/combat"
+ "thirdcollapse/internal/engine"
"thirdcollapse/internal/net"
"thirdcollapse/internal/player"
)
@@ -19,7 +20,7 @@ func (g *Game) doQuit(sess *net.Session) {
g.cancelRest(p.Name)
ticksLeft := 10
- id := g.Ticks.Subscribe(g.computeTicks(1), func() bool {
+ id := g.Ticks.Subscribe(engine.ToTicks(1), func() bool {
switch ticksLeft {
case 10:
sess.WriteLine("You sit down to rest...")
diff --git a/internal/game/color.go b/internal/game/color.go
new file mode 100644
index 0000000..45df212
--- /dev/null
+++ b/internal/game/color.go
@@ -0,0 +1,22 @@
+package game
+
+import (
+ "thirdcollapse/internal/color"
+ "thirdcollapse/internal/net"
+ "thirdcollapse/internal/player"
+)
+
+func (g *Game) colorMode(sess *net.Session) string {
+ if p, ok := sess.Player.(*player.Player); ok && p != nil {
+ return p.OptionString("color")
+ }
+ return "none"
+}
+
+func (g *Game) colorize(sess *net.Session, name, text string) string {
+ return color.Wrap(g.colorMode(sess), name, text)
+}
+
+func (g *Game) colorTag(sess *net.Session, text string) string {
+ return color.Tag(g.colorMode(sess), text)
+}
diff --git a/internal/game/game.go b/internal/game/game.go
index b08c97e..3c00d7e 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -8,6 +8,7 @@ import (
"time"
"thirdcollapse/internal/action"
+ "thirdcollapse/internal/combat"
"thirdcollapse/internal/engine"
"thirdcollapse/internal/net"
"thirdcollapse/internal/object"
@@ -21,6 +22,7 @@ const (
ClassInstant CommandClass = iota
ClassFree
ClassActive
+ ClassUnknown
)
type QueuedCommand struct {
@@ -45,7 +47,6 @@ type Game struct {
charsMu sync.Mutex
loggedInChars map[string]*net.Session
combatPadWidth int
- GameSpeed float64
freeQueue map[string][]QueuedCommand
activeQueue map[string]*QueuedCommand
pendingDepletions []pendingDepletion
@@ -126,14 +127,40 @@ func classifyCommand(cmd string) CommandClass {
return ClassInstant
case "wear", "wield", "remove", "unwear", "unwield", "style":
return ClassFree
- default:
+ case "get", "take", "grab", "pick", "drop",
+ "attack", "kill",
+ "north", "n", "south", "s", "east", "e",
+ "west", "w", "up", "u", "down", "d",
+ "quit", "use", "burn", "stoke", "search", "walk":
+ return ClassActive
+ }
+ if _, ok := verbAliases[cmd]; ok {
return ClassActive
}
+ return ClassUnknown
+}
+
+func (g *Game) promptStr(sess *net.Session) string {
+ prompt := "> "
+ if p, ok := sess.Player.(*player.Player); ok && p != nil {
+ if custom := p.OptionString("prompt"); custom != "" {
+ prompt = custom
+ }
+ }
+ return g.colorTag(sess, prompt)
+}
+
+func (g *Game) writePrompt(sess *net.Session) {
+ sess.Write("\r\n" + g.promptStr(sess))
+}
+
+func (g *Game) reprompt(sess *net.Session) {
+ sess.Write(g.promptStr(sess))
}
func (g *Game) handleGameCommand(sess *net.Session, input string) {
if input == "" {
- sess.Write("> ")
+ g.reprompt(sess)
return
}
@@ -161,7 +188,7 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) {
if class == ClassInstant {
g.executeCommand(sess, cmd, parts[1:], input)
- sess.Write("\r\n> ")
+ g.writePrompt(sess)
return
}
@@ -183,6 +210,12 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) {
return
}
+ if class == ClassUnknown {
+ sess.WriteLine("Unknown command.")
+ g.writePrompt(sess)
+ return
+ }
+
g.activeQueue[p.Name] = &QueuedCommand{
Session: sess,
Command: cmd,
@@ -224,9 +257,6 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
g.doDrop(sess, strings.Join(args, " "))
}
case "attack", "kill":
- if p == nil {
- return
- }
if len(args) == 0 {
target := g.resolveDefaultMob(p.RoomID)
if target == "" {
@@ -290,9 +320,6 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
g.doHelp(sess, strings.Join(args, " "))
}
case "mine", "chop", "fish", "cut", "use", "pull", "push":
- if p == nil {
- return
- }
g.CancelAction(p)
if len(args) == 0 {
target := g.resolveDefaultTarget(p.RoomID, cmd)
@@ -307,9 +334,6 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
return
}
case "talk", "speak", "ask":
- if p == nil {
- return
- }
g.CancelAction(p)
if len(args) == 0 {
sess.WriteLine("Talk to whom?")
@@ -318,16 +342,10 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
return
}
case "burn":
- if p == nil {
- return
- }
g.CancelAction(p)
g.doBurn(sess, p, strings.Join(args, " "))
return
case "stoke":
- if p == nil {
- return
- }
g.CancelAction(p)
g.doStoke(sess, p, strings.Join(args, " "))
return
@@ -338,9 +356,6 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
g.doUnalias(sess, args)
return
case "search":
- if p == nil {
- return
- }
g.CancelAction(p)
if len(args) == 0 {
sess.WriteLine("Search what?")
@@ -349,25 +364,41 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
}
return
case "walk":
- if p == nil {
- return
- }
g.doWalk(sess, args)
return
case "wear", "wield":
- if p == nil {
- return
- }
g.doWear(sess, strings.Join(args, " "))
return
case "remove", "unwear", "unwield":
- if p == nil {
- return
- }
g.doRemove(sess, strings.Join(args, " "))
return
default:
- sess.WriteLine("Unknown command.")
+ if a, ok := verbAliases[cmd]; ok {
+ g.CancelAction(p)
+ switch a {
+ case "gather", "toggle":
+ if len(args) == 0 {
+ target := g.resolveDefaultTarget(p.RoomID, cmd)
+ if target == "" {
+ sess.WriteLine(fmt.Sprintf("%s what?", cmd))
+ break
+ }
+ g.StartAction(sess, cmd, target)
+ return
+ }
+ g.StartAction(sess, cmd, strings.Join(args, " "))
+ return
+ case "talk":
+ if len(args) == 0 {
+ sess.WriteLine("Talk to whom?")
+ } else {
+ g.StartAction(sess, "talk", strings.Join(args, " "))
+ return
+ }
+ }
+ } else {
+ sess.WriteLine("Unknown command.")
+ }
}
}
@@ -405,7 +436,7 @@ func (g *Game) ProcessQueuedCommands() {
if len(parts) > 0 {
g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args)
}
- qc.Session.Write("\r\n> ")
+ g.writePrompt(qc.Session)
}
delete(g.freeQueue, p.Name)
}
@@ -420,10 +451,18 @@ func (g *Game) ProcessQueuedCommands() {
for _, qc := range actives {
parts := strings.Fields(qc.Command + " " + qc.Args)
+ p, ok := qc.Session.Player.(*player.Player)
+ if !ok || p == nil {
+ continue
+ }
+ wasBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil
if len(parts) > 0 {
g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args)
}
- qc.Session.Write("\r\n> ")
+ isBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil
+ if wasBusy || !isBusy {
+ g.writePrompt(qc.Session)
+ }
}
g.activeQueue = make(map[string]*QueuedCommand)
@@ -433,6 +472,9 @@ func (g *Game) ProcessQueuedCommands() {
continue
}
g.advanceWalk(sess, p)
+ if len(p.WalkSequence) == 0 {
+ g.writePrompt(sess)
+ }
}
g.flushPendingDepletions()
@@ -445,6 +487,6 @@ type pendingDepletion struct {
playerNames []string
}
-func (g *Game) computeTicks(base float64) int {
- return engine.FractionalTicks(base, g.GameSpeed)
+func (g *Game) TickCombat() {
+ combat.TickCombat()
}
diff --git a/internal/game/tick.go b/internal/game/tick.go
index 767109e..f326ad8 100644
--- a/internal/game/tick.go
+++ b/internal/game/tick.go
@@ -109,6 +109,7 @@ func (g *Game) WanderTick() {
if p, ok := sess.Player.(*player.Player); ok && p.Action != nil {
if p.Action.TargetID == om.DefID {
g.CancelAction(p)
+ g.writePrompt(sess)
}
}
}