diff options
| author | historia <[not public]> | 2026-06-27 02:29:55 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-27 02:29:55 -0400 |
| commit | 6f3d21d6e7f01155ce0c461857a053b43d329392 (patch) | |
| tree | 51e96b62deb5f99a3d70c3a15ab94c520a1ee8e8 /internal | |
| parent | 3102525d97aa6f1ad152c74b2ccfbd4b3a9b83f5 (diff) | |
| download | thehouseoficarus-6f3d21d6e7f01155ce0c461857a053b43d329392.tar.gz | |
feat: recall/home cmd, line break option
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/behavior/types.go | 14 | ||||
| -rw-r--r-- | internal/game/act.go | 2 | ||||
| -rw-r--r-- | internal/game/act_state.go | 2 | ||||
| -rw-r--r-- | internal/game/act_trigger_module.go | 61 | ||||
| -rw-r--r-- | internal/game/cmd_home.go | 7 | ||||
| -rw-r--r-- | internal/game/cmd_mods.go | 11 | ||||
| -rw-r--r-- | internal/game/cmd_option.go | 1 | ||||
| -rw-r--r-- | internal/game/cmd_registry.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_trigger.go | 22 | ||||
| -rw-r--r-- | internal/game/cmd_trigger_combat.go | 3 | ||||
| -rw-r--r-- | internal/game/cmd_trigger_transport.go | 9 | ||||
| -rw-r--r-- | internal/game/combat_attack.go | 5 | ||||
| -rw-r--r-- | internal/game/combat_mob.go | 7 | ||||
| -rw-r--r-- | internal/game/game.go | 4 | ||||
| -rw-r--r-- | internal/game/sys_science.go | 35 | ||||
| -rw-r--r-- | internal/game/tick.go | 3 | ||||
| -rw-r--r-- | internal/net/server.go | 8 | ||||
| -rw-r--r-- | internal/player/player.go | 2 | ||||
| -rw-r--r-- | internal/world/room.go | 5 |
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 { |
