aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/combat/state.go26
-rw-r--r--internal/config/config.go2
-rw-r--r--internal/game/action.go1
-rw-r--r--internal/game/action_state.go2
-rw-r--r--internal/game/cmd_attack.go9
-rw-r--r--internal/game/cmd_move.go73
-rw-r--r--internal/game/cmd_prompt.go9
-rw-r--r--internal/game/cmd_remove.go48
-rw-r--r--internal/game/cmd_walk.go7
-rw-r--r--internal/game/color.go10
-rw-r--r--internal/game/game.go36
-rw-r--r--internal/game/tick.go38
-rw-r--r--internal/object/item.go2
-rw-r--r--internal/player/player.go24
14 files changed, 235 insertions, 52 deletions
diff --git a/internal/combat/state.go b/internal/combat/state.go
index 43b693b..9c8d346 100644
--- a/internal/combat/state.go
+++ b/internal/combat/state.go
@@ -3,10 +3,9 @@ package combat
import "sync"
type State struct {
- PlayerName string
- MobID string
- Active bool
- LockedTicks int
+ PlayerName string
+ MobID string
+ Active bool
}
var (
@@ -19,10 +18,9 @@ func EnterCombat(playerName, mobID string) {
mu.Lock()
defer mu.Unlock()
combatants[playerName] = &State{
- PlayerName: playerName,
- MobID: mobID,
- Active: true,
- LockedTicks: 15,
+ PlayerName: playerName,
+ MobID: mobID,
+ Active: true,
}
mobTargets[mobID] = playerName
}
@@ -56,15 +54,3 @@ func GetMobTarget(mobID string) string {
defer mu.Unlock()
return mobTargets[mobID]
}
-
-func TickCombat() {
- mu.Lock()
- defer mu.Unlock()
- for _, state := range combatants {
- if state.LockedTicks > 0 {
- state.LockedTicks--
- }
- }
-}
-
-
diff --git a/internal/config/config.go b/internal/config/config.go
index d2065d2..af85c61 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -46,6 +46,8 @@ func DefaultColors() ColorsConfig {
"eat_food": "156",
"drop_message": "186",
"credits_pickup": "220",
+ "visual_tick": "33 dim",
+ "visual_first_tick": "196 bold",
}
}
diff --git a/internal/game/action.go b/internal/game/action.go
index 04e2b0a..c08295c 100644
--- a/internal/game/action.go
+++ b/internal/game/action.go
@@ -199,6 +199,7 @@ func (g *Game) CancelAction(p *player.Player) {
p.Action = nil
p.ActionState = nil
p.WalkSequence = nil
+ p.ClearMoveState()
}
func (g *Game) AdvanceActions() {
diff --git a/internal/game/action_state.go b/internal/game/action_state.go
index c5d086e..b28e170 100644
--- a/internal/game/action_state.go
+++ b/internal/game/action_state.go
@@ -45,7 +45,7 @@ func (a *ActionState) Description() string {
case ActionCombating:
return "fighting a " + a.TargetName
case ActionMoving:
- return "walking in from the " + a.Direction
+ return "heading " + a.Direction
case ActionUsing:
return "using a " + a.TargetName
case ActionTalking:
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index 359e0ac..d7fb365 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -272,6 +272,12 @@ func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInst
prefix := fmt.Sprintf(" %s hits you for %s damage.", g.colorize(sess, "mob_name", attacker), g.colorize(sess, "damage", fmt.Sprint(dmg)))
hpPart := fmt.Sprintf("[%s/%dhp]", g.colorize(sess, "character_hp", fmt.Sprint(p.HP)), p.MaxHP())
sess.WriteLine(fmt.Sprintf("%-*s %s", g.combatPadWidth, prefix, hpPart))
+
+ if p.MoveTicks > 0 {
+ p.ClearMoveState()
+ p.ActionState = &ActionState{Type: ActionCombating, TargetName: mob.Name}
+ sess.WriteLine("Can't escape!")
+ }
} else {
attacker := mob.Name
if !mob.Unique {
@@ -288,6 +294,7 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
if p.HP <= 0 {
sess.WriteLine(g.colorize(sess, "death", "\nOh dear, you are dead!"))
+ p.ClearMoveState()
g.dropItemsOnDeath(p)
p.HP = p.MaxHP()
p.RoomID = 1
@@ -476,7 +483,7 @@ func (g *Game) respawnMob(instanceID string) {
if p, ok := sess.Player.(*player.Player); ok && p.OptionBool("mob_spawn") {
mobLvl := mobCombatLevel(inst)
levelStr := g.levelColorize(sess, p.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl))
- sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s %s spawns in the area.", mobDisplayName(inst, false), levelStr)))
+ sess.WriteLine(fmt.Sprintf("\n%s %s spawns in the area.", g.colorize(sess, "mob_name", mobDisplayName(inst, false)), levelStr))
}
}
}
diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go
index a140003..2a3e698 100644
--- a/internal/game/cmd_move.go
+++ b/internal/game/cmd_move.go
@@ -4,7 +4,9 @@ import (
"fmt"
"thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/engine"
"thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
"thehouseoficarus/internal/player"
)
@@ -45,17 +47,74 @@ func (g *Game) doMove(sess *net.Session, dir string) {
return
}
- if cs := combat.GetCombat(p.Name); cs != nil && cs.LockedTicks > 0 {
- sess.WriteLine(fmt.Sprintf("You are locked in combat for another %d tick%s!", cs.LockedTicks, plural(cs.LockedTicks)))
+ inCombat := combat.GetCombat(p.Name) != nil
+
+ if !inCombat && p.Action != nil {
+ g.CancelAction(p)
+ }
+
+ ticks := g.moveTicks(p)
+ p.MoveDirection = string(exitDir)
+ p.MoveTarget = targetID
+
+ if ticks <= 0 {
+ g.completeMove(sess, p)
return
}
- g.stopCombat(p.Name)
+ p.MoveTicks = ticks
+ p.ActionState = &ActionState{Type: ActionMoving, Direction: string(exitDir)}
- if p.Action != nil {
- g.CancelAction(p)
+ if inCombat && p.OptionBool("run_countdown") {
+ if p.MoveTicks > 1 {
+ sess.WriteLine(fmt.Sprintf("Running in %d ticks...", p.MoveTicks))
+ } else {
+ sess.WriteLine("Running next tick!")
+ }
+ }
+}
+
+var gracefulItems = map[string]bool{
+ "graceful_hat": true,
+ "graceful_torso": true,
+ "graceful_legs": true,
+ "graceful_gloves": true,
+ "graceful_boots": true,
+ "graceful_cape": true,
+}
+
+func (g *Game) moveTicks(p *player.Player) int {
+ if id, ok := p.Equipment[object.SlotBack]; ok && id == "cape_of_agility" {
+ return 0
}
+ count := 0
+ for _, id := range p.Equipment {
+ if gracefulItems[id] {
+ count++
+ }
+ }
+
+ base := 4.0 - float64(count)*0.25
+ if count == 6 {
+ base -= 0.5
+ }
+
+ return engine.ToTicks(base) - 1
+}
+
+func (g *Game) completeMove(sess *net.Session, p *player.Player) {
+ exitDir := p.MoveDirection
+ targetID := p.MoveTarget
+
+ p.ClearMoveState()
+
+ if combat.GetCombat(p.Name) != nil {
+ g.stopCombat(p.Name)
+ }
+
+ p.Action = nil
+
oldRoom := p.RoomID
p.RoomID = targetID
g.AccountStore.SaveCharacter(p)
@@ -78,8 +137,8 @@ func (g *Game) doMove(sess *net.Session, dir string) {
}
}
- sess.WriteLine(fmt.Sprintf("\nYou walk %s.", g.colorize(sess, "direction", string(exitDir))))
- p.ActionState = &ActionState{Type: ActionMoving, Direction: string(exitDir)}
+ sess.WriteLine(fmt.Sprintf("\nYou walk %s.", g.colorize(sess, "direction", exitDir)))
+ p.ActionState = &ActionState{Type: ActionMoving, Direction: exitDir}
if p.OptionBool("description") {
g.doLook(sess)
} else {
diff --git a/internal/game/cmd_prompt.go b/internal/game/cmd_prompt.go
index 81c3409..6cc45f9 100644
--- a/internal/game/cmd_prompt.go
+++ b/internal/game/cmd_prompt.go
@@ -16,7 +16,7 @@ func (g *Game) doPrompt(sess *net.Session, input string) {
prompt = "> "
}
sess.WriteLine(fmt.Sprintf("\nPrompt: %s", prompt))
- sess.WriteLine("Use 'prompt <value>' to change. Use 'prompt reset' to restore default.")
+ sess.WriteLine("Use 'prompt <value>' to change. Use 'prompt none' for blank. Use 'prompt reset' to restore default.")
return
}
@@ -27,6 +27,13 @@ func (g *Game) doPrompt(sess *net.Session, input string) {
return
}
+ if input == "none" {
+ p.Prompt = " "
+ g.AccountStore.SaveCharacter(p)
+ sess.WriteLine("\nPrompt set to blank.")
+ return
+ }
+
p.Prompt = input
g.AccountStore.SaveCharacter(p)
sess.WriteLine(fmt.Sprintf("\nPrompt set to: %s", input))
diff --git a/internal/game/cmd_remove.go b/internal/game/cmd_remove.go
index 61fdc29..8f1a413 100644
--- a/internal/game/cmd_remove.go
+++ b/internal/game/cmd_remove.go
@@ -13,6 +13,10 @@ func (g *Game) doRemove(sess *net.Session, input string) {
p := sess.Player.(*player.Player)
lower := strings.ToLower(strings.TrimSpace(input))
+ if lower == "all" {
+ g.doRemoveAll(sess)
+ return
+ }
if lower == "" {
sess.WriteLine("Remove what?")
return
@@ -74,3 +78,47 @@ func (g *Game) doRemove(sess *net.Session, input string) {
}
sess.WriteLine(fmt.Sprintf("You remove %s.", g.itemColorize(sess, def, name)))
}
+
+func (g *Game) doRemoveAll(sess *net.Session) {
+ p := sess.Player.(*player.Player)
+
+ if len(p.Equipment) == 0 {
+ sess.WriteLine("You aren't wearing anything.")
+ return
+ }
+
+ if len(p.Equipment) > p.FreeSlots() {
+ sess.WriteLine("You don't have room to remove everything!")
+ return
+ }
+
+ g.CancelAction(p)
+
+ for _, slot := range EquipSlots {
+ itemID, ok := p.Equipment[slot]
+ if !ok {
+ continue
+ }
+
+ freeSlot := p.FirstFreeSlot()
+ delete(p.Equipment, slot)
+ invSlot := &player.InventorySlot{ItemID: itemID, Quantity: 1}
+ if q, ok := p.EquipQuality[itemID]; ok {
+ invSlot.Quality = q
+ if def, _ := g.ItemStore.Load(itemID); def != nil {
+ invSlot.MaxQuality = def.MaxQuality
+ }
+ delete(p.EquipQuality, itemID)
+ }
+ p.SetInvSlot(freeSlot, invSlot)
+
+ name := itemID
+ def, _ := g.ItemStore.Load(itemID)
+ if def != nil {
+ name = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("You remove %s.", g.itemColorize(sess, def, name)))
+ }
+
+ g.AccountStore.SaveCharacter(p)
+}
diff --git a/internal/game/cmd_walk.go b/internal/game/cmd_walk.go
index 4ff6187..2104782 100644
--- a/internal/game/cmd_walk.go
+++ b/internal/game/cmd_walk.go
@@ -92,18 +92,21 @@ func (g *Game) startWalk(sess *net.Session, p *player.Player, dirs []string) {
}
func (g *Game) advanceWalk(sess *net.Session, p *player.Player) {
+ if p.MoveTicks > 0 {
+ return
+ }
if len(p.WalkSequence) == 0 {
return
}
dir := p.WalkSequence[0]
- oldRoom := p.RoomID
remaining := condensePath(p.WalkSequence[1:])
p.WalkSequence = p.WalkSequence[1:]
+ oldRoom := p.RoomID
g.doMove(sess, dir)
- if p.RoomID == oldRoom {
+ if p.MoveTicks == 0 && p.RoomID == oldRoom {
p.WalkSequence = nil
p.ActionState = nil
return
diff --git a/internal/game/color.go b/internal/game/color.go
index 4fd8000..c3b3d83 100644
--- a/internal/game/color.go
+++ b/internal/game/color.go
@@ -46,15 +46,15 @@ func levelColorSpec(myLevel, theirLevel int) color.ColorSpec {
diff := theirLevel - myLevel
switch {
case diff == 0:
- return color.Parse("7")
+ return color.Parse("231")
case diff > 0 && diff < 5:
- return color.Parse("11")
+ return color.Parse("208")
case diff >= 5:
- return color.Parse("9")
+ return color.Parse("196")
case diff < 0 && diff > -5:
- return color.Parse("3")
+ return color.Parse("190")
default:
- return color.Parse("2")
+ return color.Parse("34")
}
}
diff --git a/internal/game/game.go b/internal/game/game.go
index 9d07ead..5af35be 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -448,7 +448,9 @@ func (g *Game) ProcessQueuedCommands() {
case ActionGathering, ActionCombating, ActionUsing, ActionTalking,
ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionCooking:
default:
- p.ActionState = nil
+ if as.Type != ActionMoving || p.MoveTicks <= 0 {
+ p.ActionState = nil
+ }
}
}
@@ -483,11 +485,14 @@ func (g *Game) ProcessQueuedCommands() {
if !ok || p == nil {
continue
}
+ if p.MoveTicks > 0 {
+ p.ClearMoveState()
+ }
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)
}
- isBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil
+ isBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil || p.MoveTicks > 0
_, isResting := g.restTimers[p.Name]
if !isResting && (wasBusy || !isBusy) {
g.writePrompt(qc.Session)
@@ -501,7 +506,7 @@ func (g *Game) ProcessQueuedCommands() {
continue
}
g.advanceWalk(sess, p)
- if len(p.WalkSequence) == 0 {
+ if len(p.WalkSequence) == 0 && p.MoveTicks == 0 {
g.writePrompt(sess)
}
}
@@ -516,6 +521,27 @@ type pendingDepletion struct {
playerNames []string
}
-func (g *Game) TickCombat() {
- combat.TickCombat()
+func (g *Game) MoveTick() {
+ if g.Hub == nil {
+ return
+ }
+ for _, sess := range g.Hub.AllSessions() {
+ p, ok := sess.Player.(*player.Player)
+ if !ok || p == nil || p.MoveTicks <= 0 {
+ continue
+ }
+ p.MoveTicks--
+ if p.MoveTicks > 0 {
+ if combat.GetCombat(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)
+ }
+ }
}
diff --git a/internal/game/tick.go b/internal/game/tick.go
index ab82254..8c40c96 100644
--- a/internal/game/tick.go
+++ b/internal/game/tick.go
@@ -129,16 +129,18 @@ func (g *Game) WanderTick() {
}
if p.RoomID == m.fromRoom && p.OptionBool("mob_leave") {
if m.level > 0 {
- sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s (level %d) leaves.", m.name, m.level)))
+ levelStr := g.levelColorize(sess, p.CombatLevel(), m.level, fmt.Sprintf("(level %d)", m.level))
+ sess.WriteLine(fmt.Sprintf("\n%s %s leaves.", g.colorize(sess, "mob_name", m.name), levelStr))
} else {
- sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s moves away.", m.name)))
+ sess.WriteLine(fmt.Sprintf("\n%s moves away.", g.colorize(sess, "mob_name", m.name)))
}
}
if p.RoomID == m.toRoom && p.OptionBool("mob_enter") {
if m.level > 0 {
- sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s (level %d) enters.", m.name, m.level)))
+ levelStr := g.levelColorize(sess, p.CombatLevel(), m.level, fmt.Sprintf("(level %d)", m.level))
+ sess.WriteLine(fmt.Sprintf("\n%s %s enters.", g.colorize(sess, "mob_name", m.name), levelStr))
} else {
- sess.WriteLine(g.colorize(sess, "broadcast", fmt.Sprintf("\n%s drifts in.", m.name)))
+ sess.WriteLine(fmt.Sprintf("\n%s drifts in.", g.colorize(sess, "mob_name", m.name)))
}
}
}
@@ -213,3 +215,31 @@ func (g *Game) ConsumeTick() {
}
}
}
+
+func (g *Game) VisualTick() {
+ if g.Hub == nil {
+ return
+ }
+ for _, sess := range g.Hub.AllSessions() {
+ p, ok := sess.Player.(*player.Player)
+ if !ok || 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))
+ }
+ }
+ }
+}
diff --git a/internal/object/item.go b/internal/object/item.go
index 6078712..efcb2c5 100644
--- a/internal/object/item.go
+++ b/internal/object/item.go
@@ -27,7 +27,7 @@ type WeaponType string
const (
WeaponMelee WeaponType = "melee"
WeaponRanged WeaponType = "ranged"
- WeaponTechnology WeaponType = "technology"
+ WeaponScience WeaponType = "science"
)
type ItemDef struct {
diff --git a/internal/player/player.go b/internal/player/player.go
index 1cd8677..48c9359 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -123,6 +123,10 @@ var OptionDefs = []OptionDef{
{"queue_silently", OptBool, true, nil, "Suppress messages for queued tick actions"},
{"room_desc_width", OptInt, 70, nil, "Maximum width for room descriptions"},
{"unicode", OptBool, true, nil, "Unicode box-drawing characters"},
+ {"run_countdown", OptBool, false, nil, "Show countdown messages when fleeing combat"},
+ {"visual_ticks", OptBool, false, nil, "Display a tick marker every game tick"},
+ {"visual_tick_count", OptInt, 0, nil, "Cycle length for tick counter (0 = no counter)"},
+ {"visual_tick_text", OptString, "Tick", nil, "Text displayed for visual ticks"},
}
var optionByName map[string]*OptionDef
@@ -153,10 +157,20 @@ type Player struct {
Options map[string]any `yaml:"-"`
Flags map[string]any `yaml:"flags"`
RegenerateTick int
- Action *action.Action `yaml:"-"`
- ActionState any `yaml:"-"`
- WalkSequence []string `yaml:"-"`
+ Action *action.Action `yaml:"-"`
+ ActionState any `yaml:"-"`
+ WalkSequence []string `yaml:"-"`
ConsumeCooldown int `yaml:"-"`
+ MoveTicks int `yaml:"-"`
+ MoveDirection string `yaml:"-"`
+ MoveTarget int `yaml:"-"`
+ VisualTickCurrent int `yaml:"-"`
+}
+
+func (p *Player) ClearMoveState() {
+ p.MoveTicks = 0
+ p.MoveDirection = ""
+ p.MoveTarget = 0
}
func (p *Player) OptionBool(name string) bool {
@@ -291,7 +305,7 @@ func (p *Player) AddSkillXP(s SkillName, amount int) int {
}
func (p *Player) CombatLevel() int {
- base := 0.25 * float64(p.Level(Defense)+p.Level(Hitpoints)+p.Level(Science))
+ base := 0.25 * float64(p.Level(Defense)+p.Level(Hitpoints)+p.Level(Technology))
att := float64(p.Level(Attack))
str := float64(p.Level(Strength))
@@ -301,7 +315,7 @@ func (p *Player) CombatLevel() int {
base += 0.25 * (att + str)
}
- base += 0.125 * float64(p.Level(Technology))
+ base += 0.125 * float64(p.Level(Science))
return int(base)
}