aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/behavior/types.go14
-rw-r--r--internal/game/act.go2
-rw-r--r--internal/game/act_state.go2
-rw-r--r--internal/game/act_trigger_module.go61
-rw-r--r--internal/game/cmd_home.go7
-rw-r--r--internal/game/cmd_mods.go11
-rw-r--r--internal/game/cmd_option.go1
-rw-r--r--internal/game/cmd_registry.go2
-rw-r--r--internal/game/cmd_trigger.go22
-rw-r--r--internal/game/cmd_trigger_combat.go3
-rw-r--r--internal/game/cmd_trigger_transport.go9
-rw-r--r--internal/game/combat_attack.go5
-rw-r--r--internal/game/combat_mob.go7
-rw-r--r--internal/game/game.go4
-rw-r--r--internal/game/sys_science.go35
-rw-r--r--internal/game/tick.go3
-rw-r--r--internal/net/server.go8
-rw-r--r--internal/player/player.go2
-rw-r--r--internal/world/room.go5
19 files changed, 176 insertions, 27 deletions
diff --git a/internal/behavior/types.go b/internal/behavior/types.go
index 3d55d79..1b7ac1a 100644
--- a/internal/behavior/types.go
+++ b/internal/behavior/types.go
@@ -53,7 +53,8 @@ const (
TypeCraft ActionType = "craft"
TypeCombine ActionType = "combine"
TypeMix ActionType = "mix"
- TypeConstruct ActionType = "construct"
+ TypeConstruct ActionType = "construct"
+ TypeTriggerModule ActionType = "trigger_module"
)
type Action struct {
@@ -183,6 +184,17 @@ type CleanData struct {
Filter string
}
+type ModTriggerStep struct {
+ Message string `yaml:"message"`
+ Delay int `yaml:"delay"`
+}
+
+type TriggerModuleData struct {
+ ModID string
+ Steps []ModTriggerStep
+ StepIndex int
+}
+
type DropEntry struct {
ItemID string `yaml:"item_id"`
Table string `yaml:"table"`
diff --git a/internal/game/act.go b/internal/game/act.go
index 31d8a33..78f298d 100644
--- a/internal/game/act.go
+++ b/internal/game/act.go
@@ -232,6 +232,8 @@ func (g *Game) AdvanceActions() {
g.advanceObstacle(sess, p)
case behavior.TypeTalk:
g.advanceTalk(sess, p)
+ case behavior.TypeTriggerModule:
+ g.advanceTriggerModule(sess, p)
default:
if productionActionTypes[string(p.Action.Type)] {
g.advanceProduction(sess, p)
diff --git a/internal/game/act_state.go b/internal/game/act_state.go
index a219875..051390f 100644
--- a/internal/game/act_state.go
+++ b/internal/game/act_state.go
@@ -81,6 +81,8 @@ func (g *Game) playerActionDisplay(p *player.Player) string {
return "mixing some " + a.TargetName
case behavior.TypeConstruct:
return "constructing some " + a.TargetName
+ case behavior.TypeTriggerModule:
+ return "triggering " + a.TargetName
}
}
diff --git a/internal/game/act_trigger_module.go b/internal/game/act_trigger_module.go
new file mode 100644
index 0000000..467cb83
--- /dev/null
+++ b/internal/game/act_trigger_module.go
@@ -0,0 +1,61 @@
+package game
+
+import (
+ "fmt"
+
+ "thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) startTriggerModule(sess *net.Session, p *player.Player, mod *ModDef) {
+ if p.Action != nil {
+ g.cancelAction(p)
+ }
+ p.Action = &behavior.Action{
+ Type: behavior.TypeTriggerModule,
+ TargetID: mod.ID,
+ TargetName: mod.Name,
+ WaitLeft: 0,
+ Data: &behavior.TriggerModuleData{
+ ModID: mod.ID,
+ Steps: mod.Sequence,
+ StepIndex: 0,
+ },
+ }
+ g.writePrompt(sess)
+}
+
+func (g *Game) advanceTriggerModule(sess *net.Session, p *player.Player) {
+ data := p.Action.Data.(*behavior.TriggerModuleData)
+
+ mod := GetMod(data.ModID)
+ if mod == nil {
+ g.cancelAction(p)
+ sess.WriteLine("The module has vanished.")
+ return
+ }
+
+ if data.StepIndex < len(data.Steps) {
+ step := data.Steps[data.StepIndex]
+ sess.WriteLine(step.Message)
+ data.StepIndex++
+ p.Action.WaitLeft = step.Delay
+ } else {
+ g.triggerModReward(sess, p, mod, 1.0)
+ if g.Combat.Get(p.Name) != nil {
+ g.Combat.Leave(p.Name)
+ }
+ switch mod.Category {
+ case ModTransport:
+ g.executeTransportEffect(sess, p, mod)
+ if mod.ID == "transport_home" {
+ p.HomeTransportCooldown = 3000
+ }
+ default:
+ sess.WriteLine(fmt.Sprintf("%s completes.", mod.Name))
+ }
+ g.cancelAction(p)
+ g.writePrompt(sess)
+ }
+}
diff --git a/internal/game/cmd_home.go b/internal/game/cmd_home.go
new file mode 100644
index 0000000..bc432c6
--- /dev/null
+++ b/internal/game/cmd_home.go
@@ -0,0 +1,7 @@
+package game
+
+import "thehouseoficarus/internal/net"
+
+func (g *Game) executeHome(sess *net.Session, args []string, rawInput string) {
+ g.doTrigger(sess, "transport_home")
+}
diff --git a/internal/game/cmd_mods.go b/internal/game/cmd_mods.go
index 54c8067..2fe9ae5 100644
--- a/internal/game/cmd_mods.go
+++ b/internal/game/cmd_mods.go
@@ -67,8 +67,15 @@ func (g *Game) doMods(sess *net.Session, showAll bool) {
levelStr := fmt.Sprintf("Lv %d", m.Level)
costStr := g.junkCostString(p, m)
xpStr := fmt.Sprintf("%d XP", m.BaseXP)
-
- if unlocked {
+ if m.ID == "transport_home" && p.HomeTransportCooldown > 0 {
+ nameStr := fmt.Sprintf("%s (%d ticks)", m.Name, p.HomeTransportCooldown)
+ t.Rows = append(t.Rows, []string{
+ g.colorize(sess, "dim", nameStr),
+ g.colorize(sess, "dim", levelStr),
+ g.colorize(sess, "dim", costStr),
+ g.colorize(sess, "dim", xpStr),
+ })
+ } else if unlocked {
t.Rows = append(t.Rows, []string{
color.Render(mode, color.Parse("75"), m.Name),
color.Render(mode, color.Parse("245"), levelStr),
diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go
index d8ef0a4..836a150 100644
--- a/internal/game/cmd_option.go
+++ b/internal/game/cmd_option.go
@@ -16,7 +16,6 @@ func (g *Game) doOption(sess *net.Session, input string) {
mode := g.colorMode(sess)
if input == "" {
- sess.WriteLine("")
table := &ui.Table{
Columns: []string{
color.Render(mode, color.Parse("75"), "Option"),
diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go
index 7ac9d0b..2cf0de1 100644
--- a/internal/game/cmd_registry.go
+++ b/internal/game/cmd_registry.go
@@ -93,6 +93,8 @@ var commandRegistry = map[string]commandDef{
"module": {(*Game).executeMods, ClassInstant},
"sneak": {(*Game).executeSneak, ClassInstant},
"trigger": {(*Game).executeTrigger, ClassActive},
+ "home": {(*Game).executeHome, ClassInstant},
+ "recall": {(*Game).executeHome, ClassInstant},
"talk": {(*Game).executeTalk, ClassActive},
"speak": {(*Game).executeTalk, ClassActive},
"ask": {(*Game).executeTalk, ClassActive},
diff --git a/internal/game/cmd_trigger.go b/internal/game/cmd_trigger.go
index 34e438a..f74efec 100644
--- a/internal/game/cmd_trigger.go
+++ b/internal/game/cmd_trigger.go
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
+ "thehouseoficarus/internal/behavior"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
)
@@ -37,6 +38,24 @@ func (g *Game) doTrigger(sess *net.Session, input string) {
return
}
+ if mod.Category == ModTransport {
+ room, err := g.World.LoadRoom(p.RoomID)
+ if err == nil && room.BlockTransport {
+ sess.WriteLine("You can't imagine transport would pick you up here")
+ return
+ }
+ }
+
+ if mod.ID == "transport_home" && p.HomeTransportCooldown > 0 {
+ sess.WriteLine(fmt.Sprintf("The transport service won't shuttle you that frequently! Wait %d ticks!", p.HomeTransportCooldown))
+ return
+ }
+
+ if len(mod.Sequence) > 0 {
+ g.startTriggerModule(sess, p, mod)
+ return
+ }
+
switch mod.Category {
case ModCombat:
g.triggerCombatMod(sess, p, mod, targetArg)
@@ -65,6 +84,9 @@ func (g *Game) parseTriggerArgs(input string, sciLevel int) (*ModDef, string) {
func (g *Game) triggerCombatMod(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) {
if cs := g.Combat.Get(p.Name); cs != nil {
+ if p.Action != nil && p.Action.Type == behavior.TypeTriggerModule {
+ g.cancelAction(p)
+ }
p.QueuedTrigger = mod.ID
sess.WriteLine(fmt.Sprintf("You queue %s for your next attack.", mod.Name))
return
diff --git a/internal/game/cmd_trigger_combat.go b/internal/game/cmd_trigger_combat.go
index 990ba86..cce9781 100644
--- a/internal/game/cmd_trigger_combat.go
+++ b/internal/game/cmd_trigger_combat.go
@@ -48,9 +48,6 @@ func (g *Game) scienceAttack(sess *net.Session, p *player.Player, mob *world.Mob
sciXP := mod.BaseXP
hpXP := mod.BaseXP / 3
- if hpXP < 1 {
- hpXP = 1
- }
var gains []xpGain
g.awardSkillXP(sess, p, player.Science, sciXP)
diff --git a/internal/game/cmd_trigger_transport.go b/internal/game/cmd_trigger_transport.go
index ba86ebe..e2053b0 100644
--- a/internal/game/cmd_trigger_transport.go
+++ b/internal/game/cmd_trigger_transport.go
@@ -9,7 +9,7 @@ import (
func (g *Game) triggerTransport(sess *net.Session, p *player.Player, mod *ModDef) {
if g.Combat.Get(p.Name) != nil {
- sess.WriteLine("You can't teleport during combat!")
+ sess.WriteLine("You can't transport during combat!")
return
}
@@ -18,11 +18,14 @@ func (g *Game) triggerTransport(sess *net.Session, p *player.Player, mod *ModDef
}
g.triggerModReward(sess, p, mod, 1.0)
+ g.executeTransportEffect(sess, p, mod)
+}
+func (g *Game) executeTransportEffect(sess *net.Session, p *player.Player, mod *ModDef) {
if g.Hub != nil {
for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
if other != sess {
- other.WriteLine(fmt.Sprintf("%s teleports away.", p.Name))
+ other.WriteLine(fmt.Sprintf("%s requests a transport shuttle and departs.", p.Name))
}
}
}
@@ -41,7 +44,7 @@ func (g *Game) triggerTransport(sess *net.Session, p *player.Player, mod *ModDef
if room != nil {
destName = room.Name
}
- sess.WriteLine(fmt.Sprintf("You materialize at %s.", destName))
+ sess.WriteLine(fmt.Sprintf("You arrive at %s.", destName))
g.AccountStore.SaveCharacter(p)
g.doLook(sess)
}
diff --git a/internal/game/combat_attack.go b/internal/game/combat_attack.go
index 36a496b..bae210b 100644
--- a/internal/game/combat_attack.go
+++ b/internal/game/combat_attack.go
@@ -7,6 +7,7 @@ import (
"strconv"
"strings"
+ "thehouseoficarus/internal/behavior"
"thehouseoficarus/internal/color"
"thehouseoficarus/internal/combat"
"thehouseoficarus/internal/engine"
@@ -198,6 +199,10 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
return false
}
+ if p.Action != nil && p.Action.Type == behavior.TypeTriggerModule {
+ return true
+ }
+
p.AttackTimer++
if float64(p.AttackTimer) >= g.playerWeaponSpeed(p) {
p.AttackTimer = 0
diff --git a/internal/game/combat_mob.go b/internal/game/combat_mob.go
index 2abaf81..7096b66 100644
--- a/internal/game/combat_mob.go
+++ b/internal/game/combat_mob.go
@@ -115,6 +115,12 @@ func (g *Game) applyMobHit(sess *net.Session, p *player.Player, mob *world.MobIn
p.ClearMoveState()
sess.WriteLine("Can't escape!")
}
+
+ if p.Action != nil && p.Action.Type == behavior.TypeTriggerModule {
+ g.cancelAction(p)
+ p.AttackTimer = 0
+ sess.WriteLine("Your concentration is broken!")
+ }
}
func (g *Game) applyMobMiss(sess *net.Session, mob *world.MobInstance) {
@@ -250,6 +256,7 @@ func (g *Game) killPlayer(sess *net.Session, p *player.Player, mob *world.MobIns
p.DeactivateAllTechs()
p.Stats.RecordDeath()
sess.WriteLine(g.colorize(sess, "death", "Oh dear, you are dead!"))
+ p.Action = nil
p.ClearMoveState()
p.HazardTimer = 0
if ss, ok := g.safespot.Get(p.Name); ok {
diff --git a/internal/game/game.go b/internal/game/game.go
index 3cfd811..22dd74d 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -190,6 +190,10 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) {
return
}
+ if sess.Player.OptionString("prompt_break") == "off" {
+ sess.ClearPrompt()
+ }
+
if sess.Account != nil && sess.Account.Aliases != nil {
firstSpace := strings.Index(input, " ")
var firstWord, rest string
diff --git a/internal/game/sys_science.go b/internal/game/sys_science.go
index 0ee446b..b04072f 100644
--- a/internal/game/sys_science.go
+++ b/internal/game/sys_science.go
@@ -25,15 +25,16 @@ const (
)
type ModDef struct {
- ID string `yaml:"id"`
- Name string `yaml:"name"`
- Level int `yaml:"level"`
- MaxHit int `yaml:"max_hit"`
- BaseXP int `yaml:"base_xp"`
- JunkCost map[string]int `yaml:"junk_cost"`
- Category ModCategory `yaml:"category"`
- Element string `yaml:"element"`
- Destination int `yaml:"destination"`
+ ID string `yaml:"id"`
+ Name string `yaml:"name"`
+ Level int `yaml:"level"`
+ MaxHit int `yaml:"max_hit"`
+ BaseXP int `yaml:"base_xp"`
+ JunkCost map[string]int `yaml:"junk_cost"`
+ Category ModCategory `yaml:"category"`
+ Element string `yaml:"element"`
+ Destination int `yaml:"destination"`
+ Sequence []behavior.ModTriggerStep `yaml:"sequence"`
}
var AllMods []*ModDef
@@ -241,9 +242,6 @@ func (g *Game) triggerModReward(sess *net.Session, p *player.Player, mod *ModDef
g.consumeJunkCost(p, mod)
sciXP := int(float64(mod.BaseXP) * multiplier)
- if sciXP < 1 {
- sciXP = 1
- }
g.awardSkillXP(sess, p, player.Science, sciXP)
for _, gain := range extraGains {
@@ -253,11 +251,18 @@ func (g *Game) triggerModReward(sess *net.Session, p *player.Player, mod *ModDef
g.AccountStore.SaveCharacter(p)
if p.OptionBool("xp_drops") {
- parts := []string{fmt.Sprintf("+%dxp %s", sciXP, player.SkillAbbr[player.Science])}
+ var parts []string
+ if sciXP > 0 {
+ parts = append(parts, fmt.Sprintf("+%dxp %s", sciXP, player.SkillAbbr[player.Science]))
+ }
for _, gain := range extraGains {
- parts = append(parts, fmt.Sprintf("+%dxp %s", gain.XP, player.SkillAbbr[player.SkillName(gain.Skill)]))
+ if gain.XP > 0 {
+ parts = append(parts, fmt.Sprintf("+%dxp %s", gain.XP, player.SkillAbbr[player.SkillName(gain.Skill)]))
+ }
+ }
+ if len(parts) > 0 {
+ sess.WriteLine(g.colorize(sess, "xp", "("+strings.Join(parts, ", ")+")"))
}
- sess.WriteLine(g.colorize(sess, "xp", "("+strings.Join(parts, ", ")+")"))
}
return sciXP
}
diff --git a/internal/game/tick.go b/internal/game/tick.go
index f8d6f04..7f593a4 100644
--- a/internal/game/tick.go
+++ b/internal/game/tick.go
@@ -288,6 +288,9 @@ func (g *Game) ConsumeTick() {
if p.ConsumeCooldown > 0 {
p.ConsumeCooldown--
}
+ if p.HomeTransportCooldown > 0 {
+ p.HomeTransportCooldown--
+ }
}
}
diff --git a/internal/net/server.go b/internal/net/server.go
index 731beef..8114df2 100644
--- a/internal/net/server.go
+++ b/internal/net/server.go
@@ -344,6 +344,14 @@ func (sess *Session) wrap(msg string) string {
return color.WrapANSI(msg, width)
}
+// ClearPrompt marks the prompt as consumed so the next Write/WriteLine does not
+// emit a leading \r\n. Useful for clients that locally advance the line on Enter.
+func (sess *Session) ClearPrompt() {
+ sess.writeMu.Lock()
+ defer sess.writeMu.Unlock()
+ sess.promptVisible = false
+}
+
// Write sends raw text with no wrapping. Used for prompts and partial lines.
func (sess *Session) Write(msg string) {
sess.writeMu.Lock()
diff --git a/internal/player/player.go b/internal/player/player.go
index 14ed06e..1b59ab1 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -141,6 +141,7 @@ var OptionDefs = []OptionDef{
{"craft_all", OptBool, false, nil, "Auto-start crafting when only one product is possible"},
{"safespot_alert", OptString, "{196 bold}** Your safespot has been compromised! **{/}", nil, "Message shown when forced out of a safespot"},
{"danger_warning", OptBool, true, nil, "Confirm before entering a dangerous (hazardous) area"},
+ {"prompt_break", OptString, "on", []string{"on", "off"}, "Line break after prompt before output"},
}
var optionByName map[string]*OptionDef
@@ -178,6 +179,7 @@ type Player struct {
BackgroundAction *behavior.Action `yaml:"-"`
WalkSequence []string `yaml:"-"`
ConsumeCooldown int `yaml:"-"`
+ HomeTransportCooldown int `yaml:"-"`
MoveTicks int `yaml:"-"`
MoveDirection string `yaml:"-"`
MoveTarget int `yaml:"-"`
diff --git a/internal/world/room.go b/internal/world/room.go
index aced969..e0e2d16 100644
--- a/internal/world/room.go
+++ b/internal/world/room.go
@@ -74,8 +74,9 @@ type Room struct {
ItemSpawns []SpawnDef `yaml:"item_spawns"`
Mobs []RoomMob `yaml:"mobs"`
OnEnter []EnterStep `yaml:"on_enter"`
- Hazard string `yaml:"hazard"`
- Triggers []TriggerDef `yaml:"triggers"`
+ Hazard string `yaml:"hazard"`
+ BlockTransport bool `yaml:"block_transport"`
+ Triggers []TriggerDef `yaml:"triggers"`
}
type RoomMob struct {