aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/action/action.go23
-rw-r--r--internal/action/behavior.go6
-rw-r--r--internal/action/recipe.go110
-rw-r--r--internal/action/store.go107
-rw-r--r--internal/config/config.go1
-rw-r--r--internal/game/action.go56
-rw-r--r--internal/game/action_agility.go18
-rw-r--r--internal/game/action_burn.go40
-rw-r--r--internal/game/action_clean.go11
-rw-r--r--internal/game/action_default_target.go14
-rw-r--r--internal/game/action_farm.go68
-rw-r--r--internal/game/action_finishing_blow.go78
-rw-r--r--internal/game/action_fletch.go22
-rw-r--r--internal/game/action_gather.go114
-rw-r--r--internal/game/action_identify.go11
-rw-r--r--internal/game/action_room.go8
-rw-r--r--internal/game/action_search.go14
-rw-r--r--internal/game/action_state.go11
-rw-r--r--internal/game/action_steal.go67
-rw-r--r--internal/game/action_talk.go127
-rw-r--r--internal/game/action_toggle.go48
-rw-r--r--internal/game/action_use.go25
-rw-r--r--internal/game/aggro.go3
-rw-r--r--internal/game/assassin.go112
-rw-r--r--internal/game/buff.go2
-rw-r--r--internal/game/cmd_agility.go6
-rw-r--r--internal/game/cmd_attack.go394
-rw-r--r--internal/game/cmd_autocast.go54
-rw-r--r--internal/game/cmd_autotrigger.go54
-rw-r--r--internal/game/cmd_bank.go14
-rw-r--r--internal/game/cmd_clean.go4
-rw-r--r--internal/game/cmd_color.go3
-rw-r--r--internal/game/cmd_colortable.go3
-rw-r--r--internal/game/cmd_construct.go157
-rw-r--r--internal/game/cmd_cook.go12
-rw-r--r--internal/game/cmd_craft.go78
-rw-r--r--internal/game/cmd_description.go5
-rw-r--r--internal/game/cmd_drink.go4
-rw-r--r--internal/game/cmd_drop.go19
-rw-r--r--internal/game/cmd_eat.go4
-rw-r--r--internal/game/cmd_equipment.go3
-rw-r--r--internal/game/cmd_estate_directory.go27
-rw-r--r--internal/game/cmd_farm.go14
-rw-r--r--internal/game/cmd_fletch.go5
-rw-r--r--internal/game/cmd_get.go12
-rw-r--r--internal/game/cmd_handlers.go370
-rw-r--r--internal/game/cmd_id.go4
-rw-r--r--internal/game/cmd_inventory.go3
-rw-r--r--internal/game/cmd_jack.go11
-rw-r--r--internal/game/cmd_look.go43
-rw-r--r--internal/game/cmd_map.go3
-rw-r--r--internal/game/cmd_mix.go15
-rw-r--r--internal/game/cmd_mods.go109
-rw-r--r--internal/game/cmd_move.go24
-rw-r--r--internal/game/cmd_option.go2
-rw-r--r--internal/game/cmd_prompt.go3
-rw-r--r--internal/game/cmd_queued.go3
-rw-r--r--internal/game/cmd_quit.go5
-rw-r--r--internal/game/cmd_registry.go170
-rw-r--r--internal/game/cmd_remove.go8
-rw-r--r--internal/game/cmd_say.go3
-rw-r--r--internal/game/cmd_score.go2
-rw-r--r--internal/game/cmd_search.go5
-rw-r--r--internal/game/cmd_shop.go276
-rw-r--r--internal/game/cmd_smelt.go12
-rw-r--r--internal/game/cmd_smith.go4
-rw-r--r--internal/game/cmd_sneak.go7
-rw-r--r--internal/game/cmd_stats.go2
-rw-r--r--internal/game/cmd_style.go2
-rw-r--r--internal/game/cmd_task.go3
-rw-r--r--internal/game/cmd_tech.go4
-rw-r--r--internal/game/cmd_trigger.go121
-rw-r--r--internal/game/cmd_use.go151
-rw-r--r--internal/game/cmd_walk.go6
-rw-r--r--internal/game/cmd_wear.go8
-rw-r--r--internal/game/color.go3
-rw-r--r--internal/game/construction.go74
-rw-r--r--internal/game/death.go66
-rw-r--r--internal/game/equip_stats.go10
-rw-r--r--internal/game/game.go468
-rw-r--r--internal/game/hacking.go88
-rw-r--r--internal/game/hacking/hacking.go63
-rw-r--r--internal/game/hacking/hacking_liars_dice.go (renamed from internal/game/hacking_liars_dice.go)2
-rw-r--r--internal/game/hacking/hacking_mastermind.go (renamed from internal/game/hacking_mastermind.go)2
-rw-r--r--internal/game/hacking/hacking_wumpus.go (renamed from internal/game/hacking_wumpus.go)2
-rw-r--r--internal/game/help.go25
-rw-r--r--internal/game/login_account.go8
-rw-r--r--internal/game/messages.go13
-rw-r--r--internal/game/player_flags.go55
-rw-r--r--internal/game/production_core.go (renamed from internal/game/action_production.go)218
-rw-r--r--internal/game/production_menu.go209
-rw-r--r--internal/game/prompt.go6
-rw-r--r--internal/game/science.go194
-rw-r--r--internal/game/tick.go59
-rw-r--r--internal/game/utils.go7
-rw-r--r--internal/net/server.go95
-rw-r--r--internal/net/welcome.go82
-rw-r--r--internal/object/item.go4
-rw-r--r--internal/object/object.go48
-rw-r--r--internal/player/player.go18
-rw-r--r--internal/world/ground.go189
-rw-r--r--internal/world/mob.go41
-rw-r--r--internal/world/objects.go332
-rw-r--r--internal/world/world.go506
104 files changed, 3165 insertions, 3069 deletions
diff --git a/internal/action/action.go b/internal/action/action.go
index 44f27f8..5e66708 100644
--- a/internal/action/action.go
+++ b/internal/action/action.go
@@ -1,5 +1,28 @@
package action
+import "strings"
+
+func WordPrefixMatch(input, name string) bool {
+ inputWords := strings.Fields(strings.ToLower(input))
+ if len(inputWords) == 0 {
+ return false
+ }
+ nameWords := strings.Fields(strings.ToLower(name))
+ for _, iw := range inputWords {
+ found := false
+ for _, nw := range nameWords {
+ if strings.HasPrefix(nw, iw) || strings.HasPrefix(iw, nw) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return false
+ }
+ }
+ return true
+}
+
type Action struct {
Type string
TargetID string
diff --git a/internal/action/behavior.go b/internal/action/behavior.go
index e1a79e3..4071f0d 100644
--- a/internal/action/behavior.go
+++ b/internal/action/behavior.go
@@ -91,9 +91,3 @@ type UseConfig struct {
Level int `yaml:"level"`
XP int `yaml:"xp"`
}
-
-type ToggleConfig struct {
- Message string `yaml:"message"`
- SetFlags map[string]any `yaml:"set_flags"`
- Check *Condition `yaml:"check"`
-}
diff --git a/internal/action/recipe.go b/internal/action/recipe.go
index d1acaa5..c1f30fb 100644
--- a/internal/action/recipe.go
+++ b/internal/action/recipe.go
@@ -1,7 +1,6 @@
package action
import (
- "fmt"
"os"
"path/filepath"
@@ -34,13 +33,52 @@ type RecipeDef struct {
type RecipeStore struct {
dataDir string
+ cache map[string][]RecipeDef
}
func NewRecipeStore(dataDir string) *RecipeStore {
- return &RecipeStore{dataDir: dataDir}
+ return &RecipeStore{dataDir: dataDir, cache: make(map[string][]RecipeDef)}
}
func (s *RecipeStore) LoadAll() ([]RecipeDef, error) {
+ return s.LoadByType("")
+}
+
+func (s *RecipeStore) LoadByType(recipeType string) ([]RecipeDef, error) {
+ if recipeType == "" {
+ recipeType = "_all"
+ }
+ if cached, ok := s.cache[recipeType]; ok {
+ result := make([]RecipeDef, len(cached))
+ copy(result, cached)
+ return result, nil
+ }
+
+ all, err := s.loadAllRaw()
+ if err != nil {
+ return nil, err
+ }
+
+ if recipeType == "_all" {
+ s.cache[recipeType] = all
+ result := make([]RecipeDef, len(all))
+ copy(result, all)
+ return result, nil
+ }
+
+ var filtered []RecipeDef
+ for _, r := range all {
+ if r.Type == recipeType {
+ filtered = append(filtered, r)
+ }
+ }
+ s.cache[recipeType] = filtered
+ result := make([]RecipeDef, len(filtered))
+ copy(result, filtered)
+ return result, nil
+}
+
+func (s *RecipeStore) loadAllRaw() ([]RecipeDef, error) {
dir := filepath.Join(s.dataDir, "recipes")
entries, err := os.ReadDir(dir)
if err != nil {
@@ -76,14 +114,11 @@ func (r *RecipeDef) MatchesEntry(itemID string) bool {
}
func (s *RecipeStore) FindByInput(recipeType string, itemID string) *RecipeDef {
- all, err := s.LoadAll()
+ all, err := s.LoadByType(recipeType)
if err != nil {
return nil
}
for i := range all {
- if recipeType != "" && all[i].Type != recipeType {
- continue
- }
for _, e := range all[i].Consume {
for _, id := range e.Items {
if id == itemID {
@@ -95,54 +130,9 @@ func (s *RecipeStore) FindByInput(recipeType string, itemID string) *RecipeDef {
return nil
}
-func (s *RecipeStore) FindByItem(itemID, recipeType string) ([]RecipeDef, error) {
- all, err := s.LoadAll()
- if err != nil {
- return nil, err
- }
- var matches []RecipeDef
- for _, r := range all {
- if recipeType != "" && r.Type != recipeType {
- continue
- }
- if r.MatchesEntry(itemID) {
- matches = append(matches, r)
- }
- }
- return matches, nil
-}
-
-func (s *RecipeStore) FindByItems(a, b string) ([]RecipeDef, error) {
- all, err := s.LoadAll()
- if err != nil {
- return nil, err
- }
- var matches []RecipeDef
- for _, r := range all {
- if len(r.Consume) < 2 {
- continue
- }
- aEntry := -1
- bEntry := -1
- for ei, e := range r.Consume {
- for _, id := range e.Items {
- if id == a && aEntry < 0 {
- aEntry = ei
- }
- if id == b && bEntry < 0 {
- bEntry = ei
- }
- }
- }
- if aEntry >= 0 && bEntry >= 0 && aEntry != bEntry {
- matches = append(matches, r)
- }
- }
- return matches, nil
-}
func (s *RecipeStore) FindByStation(itemID string, stationIDs []string, recipeType string) ([]RecipeDef, error) {
- all, err := s.LoadAll()
+ all, err := s.LoadByType(recipeType)
if err != nil {
return nil, err
}
@@ -181,21 +171,7 @@ func (r *RecipeDef) EffectiveSkill() string {
return r.Type
}
-func (s *RecipeStore) FindByNameOrID(query string, recipeType string) (*RecipeDef, error) {
- all, err := s.LoadAll()
- if err != nil {
- return nil, err
- }
- for _, r := range all {
- if recipeType != "" && r.Type != recipeType {
- continue
- }
- if r.ID == query || r.Output == query {
- return &r, nil
- }
- }
- return nil, fmt.Errorf("recipe not found: %s", query)
-}
+
func (r *RecipeDef) FirstItemName(load func(string) (string, bool)) string {
for _, e := range r.Consume {
diff --git a/internal/action/store.go b/internal/action/store.go
index fd58281..a8db9df 100644
--- a/internal/action/store.go
+++ b/internal/action/store.go
@@ -5,107 +5,10 @@ import (
"math/rand"
"os"
"path/filepath"
- "sync"
"gopkg.in/yaml.v3"
)
-type Store struct {
- dataDir string
- mu sync.Mutex
- cache map[string]*RawBehavior
-}
-
-type RawBehavior struct {
- ID string
- Type string
- Raw map[string]any
-}
-
-func NewStore(dataDir string) *Store {
- return &Store{
- dataDir: dataDir,
- cache: make(map[string]*RawBehavior),
- }
-}
-
-func (s *Store) Load(id string) (*RawBehavior, error) {
- s.mu.Lock()
- if b, ok := s.cache[id]; ok {
- s.mu.Unlock()
- return b, nil
- }
- s.mu.Unlock()
-
- path := filepath.Join(s.dataDir, "behaviors", id+".yaml")
- data, err := os.ReadFile(path)
- if err != nil {
- return nil, fmt.Errorf("read behavior %s: %w", id, err)
- }
-
- var raw map[string]any
- if err := yaml.Unmarshal(data, &raw); err != nil {
- return nil, fmt.Errorf("parse behavior %s: %w", id, err)
- }
-
- rb := &RawBehavior{ID: id, Raw: raw}
- if t, ok := raw["type"].(string); ok {
- rb.Type = t
- }
- if rid, ok := raw["id"].(string); ok {
- rb.ID = rid
- }
-
- s.mu.Lock()
- s.cache[id] = rb
- s.mu.Unlock()
- return rb, nil
-}
-
-func unmarshalRaw[T any](rb *RawBehavior, expectedType string) (*T, error) {
- if rb.Type != expectedType {
- return nil, fmt.Errorf("behavior %s is type %s, expected %s", rb.ID, rb.Type, expectedType)
- }
- var cfg T
- raw, _ := yaml.Marshal(rb.Raw)
- if err := yaml.Unmarshal(raw, &cfg); err != nil {
- return nil, fmt.Errorf("parse %s config %s: %w", expectedType, rb.ID, err)
- }
- return &cfg, nil
-}
-
-func (s *Store) LoadGather(id string) (*GatherConfig, error) {
- rb, err := s.Load(id)
- if err != nil {
- return nil, err
- }
- return unmarshalRaw[GatherConfig](rb, "gather")
-}
-
-func (s *Store) LoadTalk(id string) (*TalkConfig, error) {
- rb, err := s.Load(id)
- if err != nil {
- return nil, err
- }
- return unmarshalRaw[TalkConfig](rb, "talk")
-}
-
-func (s *Store) LoadUse(id string) (*UseConfig, error) {
- rb, err := s.Load(id)
- if err != nil {
- return nil, err
- }
- return unmarshalRaw[UseConfig](rb, "use")
-}
-
-func (s *Store) LoadToggle(id string) (*ToggleConfig, error) {
- rb, err := s.Load(id)
- if err != nil {
- return nil, err
- }
- return unmarshalRaw[ToggleConfig](rb, "toggle")
-}
-
func SuccessChance(cfg SuccessFormula, level int, requiredLevel int) float64 {
chance := cfg.Base + float64(level-requiredLevel)*cfg.PerLevel
if chance > cfg.Cap {
@@ -117,8 +20,8 @@ func SuccessChance(cfg SuccessFormula, level int, requiredLevel int) float64 {
return chance
}
-func (s *Store) LoadDropTable(id string) (*DropTableDef, error) {
- path := filepath.Join(s.dataDir, "drops", id+".yaml")
+func LoadDropTable(dataDir, id string) (*DropTableDef, error) {
+ path := filepath.Join(dataDir, "drops", id+".yaml")
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read drop table %s: %w", id, err)
@@ -130,7 +33,7 @@ func (s *Store) LoadDropTable(id string) (*DropTableDef, error) {
return &dt, nil
}
-func (s *Store) ResolveDrop(drops []DropEntry) *DropEntry {
+func ResolveDrop(dataDir string, drops []DropEntry) *DropEntry {
if len(drops) == 0 {
return nil
}
@@ -147,9 +50,9 @@ func (s *Store) ResolveDrop(drops []DropEntry) *DropEntry {
cumulative += drops[i].Weight
if roll < cumulative {
if drops[i].Table != "" {
- sub, err := s.LoadDropTable(drops[i].Table)
+ sub, err := LoadDropTable(dataDir, drops[i].Table)
if err == nil {
- if resolved := s.ResolveDrop(sub.Drops); resolved != nil {
+ if resolved := ResolveDrop(dataDir, sub.Drops); resolved != nil {
qty := drops[i].Quantity
if qty <= 0 {
qty = resolved.Quantity
diff --git a/internal/config/config.go b/internal/config/config.go
index e7e1b02..122c311 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -52,6 +52,7 @@ func DefaultColors() ColorsConfig {
"tech_depleted": "196",
"science_mod": "99",
"assassin_task": "219",
+ "dim": "243 dim",
"warning": "208",
"farm_grow": "34",
"farm_disease": "196",
diff --git a/internal/game/action.go b/internal/game/action.go
index dc21927..f25cabc 100644
--- a/internal/game/action.go
+++ b/internal/game/action.go
@@ -23,8 +23,6 @@ var verbAliases = map[string]string{
"talk": "talk",
"speak": "talk",
"ask": "talk",
- "pull": "toggle",
- "push": "toggle",
"steal": "steal",
"thieve": "steal",
}
@@ -46,8 +44,8 @@ func normalizeVerb(v string) string {
return v
}
-func (g *Game) StartAction(sess *net.Session, verb, target string) {
- p := sess.Player.(*player.Player)
+func (g *Game) startAction(sess *net.Session, verb, target string) {
+ p := sess.Player
verb = normalizeVerb(verb)
@@ -57,9 +55,9 @@ func (g *Game) StartAction(sess *net.Session, verb, target string) {
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
lower := strings.ToLower(target)
instanceIdx := -1
@@ -77,7 +75,6 @@ func (g *Game) StartAction(sess *net.Session, verb, target string) {
var obj *object.ObjectDef
var mob *world.MobInstance
- var behaviorID string
if len(instances) > 0 {
uniqueDefs := make(map[string]bool)
@@ -109,16 +106,15 @@ func (g *Game) StartAction(sess *net.Session, verb, target string) {
}
var err error
obj, err = g.ObjectStore.Load(chosen.DefID)
- if err != nil || obj.BehaviorID == "" {
+ if err != nil || !obj.IsInteractable() {
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 == "" {
+ if !m.IsTalkable() {
continue
}
if q := m.MatchQuality(lower); q >= world.MatchPrefix {
@@ -140,17 +136,6 @@ func (g *Game) StartAction(sess *net.Session, verb, target string) {
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 obj != nil && obj.ID == "estate_directory" {
@@ -158,7 +143,16 @@ func (g *Game) StartAction(sess *net.Session, verb, target string) {
return
}
- if bh.Type != verb {
+ bhType := ""
+ if obj != nil {
+ bhType = obj.BehaviorType()
+ } else if mob != nil {
+ if mob.IsTalkable() {
+ bhType = "talk"
+ }
+ }
+
+ if bhType != verb {
if obj != nil {
sess.WriteLine(fmt.Sprintf("You can't %s the %s.", verb, obj.Name))
} else {
@@ -167,7 +161,7 @@ func (g *Game) StartAction(sess *net.Session, verb, target string) {
return
}
- switch bh.Type {
+ switch bhType {
case "gather":
if obj == nil {
sess.WriteLine(fmt.Sprintf("You can't %s that.", verb))
@@ -200,21 +194,19 @@ func (g *Game) StartAction(sess *net.Session, verb, target string) {
}
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) {
+func (g *Game) cancelAction(p *player.Player) {
p.Action = nil
p.ActionState = nil
p.WalkSequence = nil
p.ClearMoveState()
}
-func (g *Game) CancelBackgroundAction(p *player.Player) {
+func (g *Game) cancelBackgroundAction(p *player.Player) {
p.BackgroundAction = nil
p.BackgroundActionState = nil
}
@@ -224,8 +216,8 @@ func (g *Game) AdvanceActions() {
return
}
for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p.Action == nil {
+ p := sess.Player
+ if p == nil || p.Action == nil {
continue
}
if !p.Action.Advance() {
@@ -269,8 +261,8 @@ func (g *Game) AdvanceActions() {
}
for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p.BackgroundAction == nil {
+ p := sess.Player
+ if p == nil || p.BackgroundAction == nil {
continue
}
if !p.BackgroundAction.Advance() {
@@ -289,7 +281,7 @@ func (g *Game) AdvanceActions() {
}
func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool {
- p, _ := sess.Player.(*player.Player)
+ p := sess.Player
if c.AllOf != nil {
for _, sub := range c.AllOf {
diff --git a/internal/game/action_agility.go b/internal/game/action_agility.go
index 089f026..37fbab9 100644
--- a/internal/game/action_agility.go
+++ b/internal/game/action_agility.go
@@ -68,7 +68,7 @@ func (g *Game) advanceObstacle(sess *net.Session, p *player.Player) {
ticksPerPhase := data["ticks_per_phase"].(float64)
if phase >= len(messages) {
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
@@ -95,10 +95,7 @@ func (g *Game) advanceObstacle(sess *net.Session, p *player.Player) {
courseID := data["course_id"].(string)
if obstacleXP > 0 {
- if newLevel := p.AddSkillXP(player.Agility, obstacleXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up",
- fmt.Sprintf("*** You are now level %d agility! ***", newLevel)))
- }
+ g.awardSkillXP(sess, p, player.Agility, obstacleXP)
if p.OptionBool("xp_drops") {
sess.WriteLine(g.colorize(sess, "xp",
fmt.Sprintf("(+%dxp %s)", obstacleXP, player.SkillAbbr[player.Agility])))
@@ -109,10 +106,7 @@ func (g *Game) advanceObstacle(sess *net.Session, p *player.Player) {
if isLastObstacle {
if completionXP > 0 {
- if newLevel := p.AddSkillXP(player.Agility, completionXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up",
- fmt.Sprintf("*** You are now level %d agility! ***", newLevel)))
- }
+ g.awardSkillXP(sess, p, player.Agility, completionXP)
if p.OptionBool("xp_drops") {
sess.WriteLine(g.colorize(sess, "xp",
fmt.Sprintf("(+%dxp %s course bonus)", completionXP, player.SkillAbbr[player.Agility])))
@@ -127,10 +121,10 @@ func (g *Game) advanceObstacle(sess *net.Session, p *player.Player) {
sess.WriteLine(g.colorize(sess, "broadcast",
fmt.Sprintf("Course complete! %s lap %d finished.", courseName, laps)))
- g.CancelAction(p)
+ g.cancelAction(p)
g.teleportPlayer(sess, p, startRoom)
} else {
- g.CancelAction(p)
+ g.cancelAction(p)
g.teleportPlayer(sess, p, nextRoom)
}
@@ -162,7 +156,7 @@ func (g *Game) obstacleFail(sess *net.Session, p *player.Player, data map[string
fmt.Sprintf("You take %d damage. HP: %d/%d", damage, p.HP, p.MaxHP())))
g.AccountStore.SaveCharacter(p)
- g.CancelAction(p)
+ g.cancelAction(p)
g.teleportPlayer(sess, p, startRoom)
}
diff --git a/internal/game/action_burn.go b/internal/game/action_burn.go
index 0230f24..4d8b65e 100644
--- a/internal/game/action_burn.go
+++ b/internal/game/action_burn.go
@@ -13,9 +13,9 @@ import (
func (g *Game) doBurn(sess *net.Session, p *player.Player, input string) {
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
itemID, fromGround, ambiguous := g.resolveBurnTarget(p, input)
if ambiguous {
@@ -32,9 +32,9 @@ func (g *Game) doBurn(sess *net.Session, p *player.Player, input string) {
func (g *Game) doStoke(sess *net.Session, p *player.Player, input string) {
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
if !g.hasFireObject(p.RoomID) {
sess.WriteLine("There's no fire here to stoke.")
@@ -183,14 +183,14 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) {
logDef, err := g.ItemStore.Load(itemID)
if err != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
if phase == 0 {
if !p.HasItem(itemID) {
sess.WriteLine(fmt.Sprintf("You're out of %s.", g.itemColorize(sess, logDef, logDef.Name)))
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
p.RemoveItem(itemID, 1)
@@ -208,7 +208,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) {
ground := g.World.GroundItems(p.RoomID)
if ground[itemID] <= 0 {
sess.WriteLine("The thing you were trying to burn is gone.")
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
@@ -235,13 +235,11 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) {
xp := logDef.FireXP
if xp > 0 {
- if newLevel := p.AddSkillXP(player.Firemaking, xp); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d firemaking! ***", newLevel)))
- }
+ g.awardSkillXP(sess, p, player.Firemaking, xp)
}
g.AccountStore.SaveCharacter(p)
- g.CancelAction(p)
+ g.cancelAction(p)
msg := g.colorize(sess, "fire", "You manage to get a fire going!")
if xp > 0 && p.OptionBool("xp_drops") {
@@ -273,7 +271,7 @@ func (g *Game) advanceStoke(sess *net.Session, p *player.Player) {
st := g.World.GetObjStateByKey(fireKey)
if st == nil || st.Quality <= 0 {
sess.WriteLine("The fire went out!")
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
@@ -285,7 +283,7 @@ func (g *Game) advanceStoke(sess *net.Session, p *player.Player) {
if !p.HasItem(itemID) {
sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You're out of %s and the fire is roaring.", g.itemColorize(sess, logDef, name))))
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
@@ -293,9 +291,7 @@ func (g *Game) advanceStoke(sess *net.Session, p *player.Player) {
st.Quality += burnTicks / 2
if xp > 0 {
- if newLevel := p.AddSkillXP(player.Firemaking, xp); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d firemaking! ***", newLevel)))
- }
+ g.awardSkillXP(sess, p, player.Firemaking, xp)
}
g.AccountStore.SaveCharacter(p)
@@ -308,7 +304,7 @@ func (g *Game) advanceStoke(sess *net.Session, p *player.Player) {
if !p.HasItem(itemID) {
sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You're out of %s and the fire is roaring.", g.itemColorize(sess, logDef, name))))
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
@@ -416,8 +412,8 @@ func (g *Game) hasFireObject(roomID int) bool {
func (g *Game) broadcastDrop(p *player.Player, itemName string) {
for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
- op, ok := other.Player.(*player.Player)
- if ok && op.Name != p.Name {
+ op := other.Player
+ if op != nil && op.Name != p.Name {
other.WriteLine(fmt.Sprintf("\n%s drops a %s.", p.Name, itemName))
}
}
@@ -438,10 +434,10 @@ func (g *Game) FireTick() {
func (g *Game) cancelStokers(roomID int) {
for _, other := range g.Hub.PlayersInRoom(roomID) {
- op, ok := other.Player.(*player.Player)
- if ok && op.Action != nil && op.Action.Type == "stoke" {
+ op := other.Player
+ if op != nil && op.Action != nil && op.Action.Type == "stoke" {
other.WriteLine("The fire went out!")
- g.CancelAction(op)
+ g.cancelAction(op)
g.writePrompt(other)
}
}
diff --git a/internal/game/action_clean.go b/internal/game/action_clean.go
index dab3619..104dcb0 100644
--- a/internal/game/action_clean.go
+++ b/internal/game/action_clean.go
@@ -21,7 +21,7 @@ func (g *Game) advanceClean(sess *net.Session, p *player.Player) {
allRecipes, err := g.RecipeStore.LoadAll()
if err != nil {
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
return
}
@@ -51,7 +51,7 @@ func (g *Game) advanceClean(sess *net.Session, p *player.Player) {
if recipe == nil {
sess.WriteLine("\nYou've finished cleaning herbs.")
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
return
}
@@ -64,10 +64,7 @@ func (g *Game) advanceClean(sess *net.Session, p *player.Player) {
}
if recipe.xp > 0 {
- if newLevel := p.AddSkillXP(player.Pharmacy, recipe.xp); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up",
- fmt.Sprintf("*** You are now level %d pharmacy! ***", newLevel)))
- }
+ g.awardSkillXP(sess, p, player.Pharmacy, recipe.xp)
}
g.AccountStore.SaveCharacter(p)
@@ -108,7 +105,7 @@ func (g *Game) advanceClean(sess *net.Session, p *player.Player) {
if !hasMore {
sess.WriteLine("\nYou've finished cleaning herbs.")
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
return
}
diff --git a/internal/game/action_default_target.go b/internal/game/action_default_target.go
index b3825ca..2ca0bd3 100644
--- a/internal/game/action_default_target.go
+++ b/internal/game/action_default_target.go
@@ -30,21 +30,11 @@ func (g *Game) resolveDefaultTarget(roomID int, verb string) string {
seen[st.DefID] = true
objDef, err := g.ObjectStore.Load(st.DefID)
- if err != nil || objDef.BehaviorID == "" {
+ if err != nil || !objDef.IsGatherable() {
continue
}
- bh, err := g.BehaviorStore.Load(objDef.BehaviorID)
- if err != nil || bh.Type != "gather" {
- continue
- }
-
- cfg, err := g.BehaviorStore.LoadGather(objDef.BehaviorID)
- if err != nil {
- continue
- }
-
- if strings.EqualFold(cfg.Skill, skill) {
+ if strings.EqualFold(objDef.Gather.Skill, skill) {
candidates = append(candidates, candidate{defID: st.DefID, name: objDef.Name})
}
}
diff --git a/internal/game/action_farm.go b/internal/game/action_farm.go
index 12b99c0..d79150f 100644
--- a/internal/game/action_farm.go
+++ b/internal/game/action_farm.go
@@ -6,6 +6,7 @@ import (
"sort"
"strings"
+ "thehouseoficarus/internal/action"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
"thehouseoficarus/internal/world"
@@ -38,9 +39,7 @@ func farmFlagPrefix(defID string, index int) string {
}
func (g *Game) ensureFarmState(p *player.Player, prefix string) {
- if p.Flags == nil {
- p.Flags = make(map[string]any)
- }
+ p.EnsureFlags()
if _, exists := p.Flags[prefix+"_seed"]; !exists {
p.Flags[prefix+"_weeds"] = true
p.Flags[prefix+"_seed"] = ""
@@ -86,8 +85,8 @@ func (g *Game) FarmTick() {
}
for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil || p.Flags == nil {
+ p := sess.Player
+ if p == nil || p.Flags == nil {
continue
}
g.advanceFarmGrowth(sess, p)
@@ -120,7 +119,7 @@ func (g *Game) advanceFarmGrowth(sess *net.Session, p *player.Player) {
continue
}
- stage := intFlag(p.Flags, prefix+"_stage")
+ stage := intFromFlag(p.Flags, prefix+"_stage")
watered, _ := p.Flags[prefix+"_watered"].(bool)
seedDef, err := g.ItemStore.Load(seedID)
@@ -188,10 +187,7 @@ func (g *Game) advancePlant(sess *net.Session, p *player.Player) {
p.Flags[prefix+"_ready"] = false
if intXP := int(xp); intXP > 0 {
- if newLevel := p.AddSkillXP(player.Farming, intXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up",
- fmt.Sprintf("*** You are now level %d farming! ***", newLevel)))
- }
+ g.awardSkillXP(sess, p, player.Farming, intXP)
}
g.AccountStore.SaveCharacter(p)
@@ -275,10 +271,7 @@ func (g *Game) advanceHarvest(sess *net.Session, p *player.Player) {
}
if intXP := int(xp); intXP > 0 {
- if newLevel := p.AddSkillXP(player.Farming, intXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up",
- fmt.Sprintf("*** You are now level %d farming! ***", newLevel)))
- }
+ g.awardSkillXP(sess, p, player.Farming, intXP)
}
g.ensureFarmState(p, prefix)
@@ -416,7 +409,7 @@ func (g *Game) farmPatchLookSuffix(p *player.Player, defID string, index int) st
return fmt.Sprintf("\nA fully grown %s is ready to harvest!", name)
}
- stage := intFlag(p.Flags, prefix+"_stage")
+ stage := intFromFlag(p.Flags, prefix+"_stage")
maxStages := 4
if def, err := g.ItemStore.Load(seedID); err == nil && def.FarmStages > 0 {
maxStages = def.FarmStages
@@ -450,20 +443,6 @@ func (g *Game) toolShedLookSuffix(p *player.Player) string {
return fmt.Sprintf("\nInside: %s.", strings.Join(stored, ", "))
}
-func intFlag(flags map[string]any, key string) int {
- v, ok := flags[key]
- if !ok {
- return 0
- }
- switch n := v.(type) {
- case int:
- return n
- case float64:
- return int(n)
- }
- return 0
-}
-
func (g *Game) farmMatchPrefix(p *player.Player, input string) (prefix string, defID string, index int) {
objInstances := g.World.AllObjInstances(p.RoomID)
if len(objInstances) == 0 {
@@ -482,24 +461,17 @@ func (g *Game) farmMatchPrefix(p *player.Player, input string) (prefix string, d
if !farmPatchDefIDs[obj.DefID] {
continue
}
- for i, inst := range objInstances {
- if inst.DefID != obj.DefID {
- continue
- }
- pref := farmFlagPrefix(obj.DefID, i)
- if pref == "" {
- break
- }
- var name string
- if def, err := g.ObjectStore.Load(obj.DefID); err == nil {
- name = def.Name
- } else {
- name = obj.DefID
- }
- farms = append(farms, indexedFarm{prefix: pref, defID: obj.DefID, index: i, name: name})
- break
+ pref := farmFlagPrefix(obj.DefID, obj.Index)
+ if pref == "" {
+ continue
+ }
+ var name string
+ if def, err := g.ObjectStore.Load(obj.DefID); err == nil {
+ name = def.Name
+ } else {
+ name = obj.DefID
}
- break
+ farms = append(farms, indexedFarm{prefix: pref, defID: obj.DefID, index: obj.Index, name: name})
}
if len(farms) == 0 {
@@ -512,7 +484,7 @@ func (g *Game) farmMatchPrefix(p *player.Player, input string) (prefix string, d
lower := strings.ToLower(input)
for _, f := range farms {
- if strings.Contains(lower, strings.ToLower(f.name)) || world.WordPrefixMatch(lower, f.name) {
+ if strings.Contains(lower, strings.ToLower(f.name)) || action.WordPrefixMatch(lower, f.name) {
return f.prefix, f.defID, f.index
}
}
@@ -620,7 +592,7 @@ func (g *Game) showFarmPatches(sess *net.Session, p *player.Player) {
continue
}
- stage := intFlag(p.Flags, prefix+"_stage")
+ stage := intFromFlag(p.Flags, prefix+"_stage")
maxStages := 4
if sd, err := g.ItemStore.Load(seedID); err == nil && sd.FarmStages > 0 {
maxStages = sd.FarmStages
diff --git a/internal/game/action_finishing_blow.go b/internal/game/action_finishing_blow.go
new file mode 100644
index 0000000..1a31d86
--- /dev/null
+++ b/internal/game/action_finishing_blow.go
@@ -0,0 +1,78 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+func (g *Game) tryFinishingBlow(sess *net.Session, input string) bool {
+ p := sess.Player
+ cs := combat.GetCombat(p.Name)
+ if cs == nil {
+ return false
+ }
+ mob := g.MobStore.GetInstance(cs.MobID)
+ if mob == nil || mob.FinishingBlow == "" || mob.HP != 1 {
+ return false
+ }
+ lower := strings.ToLower(input)
+ var itemPart, targetPart string
+ for _, sep := range []string{" on ", " with "} {
+ if idx := strings.Index(lower, sep); idx > 0 {
+ itemPart = strings.TrimSpace(input[:idx])
+ targetPart = strings.TrimSpace(input[idx+len(sep):])
+ break
+ }
+ }
+ if targetPart == "" {
+ itemPart = strings.TrimSpace(input)
+ if mob.MatchQuality(itemPart) != world.MatchNone {
+ targetPart = itemPart
+ itemPart = strings.TrimSpace(mob.FinishingBlow)
+ }
+ }
+ if targetPart == "" {
+ return false
+ }
+ if mob.MatchQuality(targetPart) == world.MatchNone {
+ return false
+ }
+ g.doFinishingBlow(sess, p, mob, itemPart)
+ return true
+}
+
+func (g *Game) doFinishingBlow(sess *net.Session, p *player.Player, mob *world.MobInstance, itemInput string) {
+ fbDef, err := g.ItemStore.Load(mob.FinishingBlow)
+ if err != nil {
+ sess.WriteLine("Something went wrong.")
+ return
+ }
+ if !fbDef.MatchesName(itemInput) && !strings.EqualFold(itemInput, fbDef.ID) {
+ sess.WriteLine(fmt.Sprintf("That won't work on %s. You need %s.", mobDisplayName(mob, true), fbDef.Name))
+ return
+ }
+ if !p.HasItem(mob.FinishingBlow) {
+ sess.WriteLine(fmt.Sprintf("You don't have any %s.", fbDef.Name))
+ return
+ }
+ autoKey := "assassin_unlocked_auto_" + mob.FinishingBlow
+ consumed := true
+ if p.Flags != nil {
+ if val, ok := p.Flags[autoKey]; ok {
+ if b, ok := val.(bool); ok && b {
+ consumed = false
+ }
+ }
+ }
+ if consumed {
+ p.RemoveItem(mob.FinishingBlow, 1)
+ }
+ sess.WriteLine(fmt.Sprintf("\nYou use the %s on %s!", g.itemColorize(sess, fbDef, fbDef.Name), g.colorize(sess, "mob_name", mobDisplayName(mob, true))))
+ mob.HP = 0
+ g.endCombat(sess, p, mob)
+}
diff --git a/internal/game/action_fletch.go b/internal/game/action_fletch.go
index ddb4d2c..aa5dfc7 100644
--- a/internal/game/action_fletch.go
+++ b/internal/game/action_fletch.go
@@ -10,9 +10,7 @@ import (
)
func (g *Game) startFletchAction(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) {
- if p.Flags == nil {
- p.Flags = make(map[string]any)
- }
+ p.EnsureFlags()
p.Flags["last_fletch"] = recipe.ID
g.AccountStore.SaveCharacter(p)
@@ -61,9 +59,7 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, recipe *acti
}
if recipe.XP > 0 {
- if newLevel := p.AddSkillXP(skill, recipe.XP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, recipe.EffectiveSkill())))
- }
+ g.awardSkillXP(sess, p, skill, recipe.XP)
}
batches++
@@ -138,7 +134,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) {
recipe := g.loadProductionRecipe(recipeID)
if recipe == nil {
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
return
}
@@ -150,7 +146,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) {
if !g.canDoFletchRecipe(p, recipe) {
sess.WriteLine("\nYou've run out of fletching materials.")
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
return
}
@@ -179,16 +175,14 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) {
freeSlot := p.FirstFreeSlot()
if freeSlot == -1 {
sess.WriteLine("\nYour inventory is too full!")
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
return
}
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Output, Quantity: outputQty})
}
if recipe.XP > 0 {
- if newLevel := p.AddSkillXP(skill, recipe.XP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, recipe.EffectiveSkill())))
- }
+ g.awardSkillXP(sess, p, skill, recipe.XP)
}
g.AccountStore.SaveCharacter(p)
@@ -211,14 +205,14 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) {
p.BackgroundAction.Data["remaining"] = remaining
if remaining <= 0 {
sess.WriteLine("\nYou've finished fletching.")
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
return
}
}
if !g.canContinueProduction(p, recipe) {
sess.WriteLine("\nYou've run out of fletching materials.")
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
return
}
diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go
index edc8545..e169beb 100644
--- a/internal/game/action_gather.go
+++ b/internal/game/action_gather.go
@@ -14,9 +14,19 @@ import (
"thehouseoficarus/internal/world"
)
-func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.ObjectDef, st *world.ObjState) {
- cfg, err := g.BehaviorStore.LoadGather(obj.BehaviorID)
+const birdsNestItemID = "birds_nest"
+
+func loadGatherConfig(g *Game, objDefID string) *action.GatherConfig {
+ def, err := g.ObjectStore.Load(objDefID)
if err != nil {
+ return nil
+ }
+ return def.Gather
+}
+
+func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.ObjectDef, st *world.ObjState) {
+ cfg := obj.Gather
+ if cfg == nil {
sess.WriteLine("Something is wrong with this object.")
return
}
@@ -34,7 +44,7 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje
msg += fmt.Sprintf(" (%d ticks to respawn)", int(st.DepleteTimer))
}
sess.WriteLine(msg)
- } else if cfg.DepleteTimer > 0 {
+ } else if cfg.DepleteTimer > 0 {
sess.WriteLine(fmt.Sprintf("The %s has been cut down for %d more ticks.", obj.Name, int(st.DepleteTimer)))
} else {
sess.WriteLine(fmt.Sprintf("The %s is depleted for %d more ticks.", obj.Name, int(st.DepleteTimer)))
@@ -156,13 +166,13 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje
}
data := map[string]any{
- "behavior_id": obj.BehaviorID,
+ "obj_def_id": obj.ID,
"instance_key": g.World.ObjStateKey(st.RoomID, st.DefID, st.Index),
"instance_idx": st.Index + 1,
"effective_wait": wait,
"step": 0,
}
- if cfg.DepleteTimer > 0 {
+ if cfg.DepleteTimer > 0 {
data["deplete_timer"] = true
}
p.Action = &action.Action{
@@ -175,10 +185,10 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje
}
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)
+ objDefID := p.Action.Data["obj_def_id"].(string)
+ cfg := loadGatherConfig(g, objDefID)
+ if cfg == nil {
+ g.cancelAction(p)
return
}
@@ -190,7 +200,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
if step == 0 {
if p.FirstFreeSlot() == -1 {
sess.WriteLine(g.colorize(sess, "error", "Your inventory is too full!"))
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
if cfg.Bait != "" {
@@ -200,7 +210,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
baitName = def.Name
}
sess.WriteLine(g.colorize(sess, "error", fmt.Sprintf("You've run out of %s.", baitName)))
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
}
@@ -228,7 +238,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
baitName = def.Name
}
sess.WriteLine(g.colorize(sess, "error", fmt.Sprintf("You've run out of %s.", baitName)))
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
p.RemoveItem(cfg.Bait, 1)
@@ -239,12 +249,12 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
if rand.Float64() < chance {
eligible := filterDropsByLevel(cfg.Drops, skillLevel)
- drop := g.BehaviorStore.ResolveDrop(eligible)
+ drop := action.ResolveDrop(g.DataDir, eligible)
if drop != nil {
freeSlot := p.FirstFreeSlot()
if freeSlot == -1 {
sess.WriteLine(g.colorize(sess, "error", "Your inventory is too full!"))
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
@@ -265,9 +275,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
xp = cfg.XP
}
if xp > 0 {
- if newLevel := p.AddSkillXP(player.SkillName(cfg.Skill), xp); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, cfg.Skill)))
- }
+ g.awardSkillXP(sess, p, player.SkillName(cfg.Skill), xp)
}
g.AccountStore.SaveCharacter(p)
@@ -284,20 +292,20 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
g.checkBirdNest(sess, p, cfg, freeSlot)
- if shared {
- st := g.World.GetObjStateByKey(instanceKey)
- if st != nil && st.SharedTimer <= 0 {
- g.pendingDepletions = append(g.pendingDepletions, pendingDepletion{
- instanceKey: instanceKey,
- behaviorID: behaviorID,
- targetName: p.Action.TargetName,
- playerNames: []string{p.Name},
- })
- p.Action.Data["step"] = 2
- p.Action.WaitLeft = engine.ToTicks(1)
- return
+ if shared {
+ st := g.World.GetObjStateByKey(instanceKey)
+ if st != nil && st.SharedTimer <= 0 {
+ g.pendingDepletions = append(g.pendingDepletions, pendingDepletion{
+ instanceKey: instanceKey,
+ objDefID: objDefID,
+ targetName: p.Action.TargetName,
+ playerNames: []string{p.Name},
+ })
+ p.Action.Data["step"] = 2
+ p.Action.WaitLeft = engine.ToTicks(1)
+ return
+ }
}
- }
if !shared && drop.Depletes {
delay := engine.ToTicks(cfg.RespawnTimer)
@@ -310,20 +318,20 @@ 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!", g.colorize(sess, "item", p.Action.TargetName), g.colorize(sess, "player_name", p.Name)))
- g.CancelAction(op)
- g.writePrompt(other)
+ for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
+ if other == sess {
+ continue
+ }
+ op := other.Player
+ if op == nil || 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!", g.colorize(sess, "item", p.Action.TargetName), g.colorize(sess, "player_name", p.Name)))
+ g.cancelAction(op)
+ g.writePrompt(other)
+ }
}
- }
p.Action.Data["step"] = 2
p.Action.WaitLeft = engine.ToTicks(1)
@@ -332,7 +340,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
if p.FirstFreeSlot() == -1 {
sess.WriteLine(g.colorize(sess, "error", "Your inventory is too full!"))
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
p.Action.Data["step"] = 0
@@ -344,7 +352,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
sess.WriteLine(cfg.FailMsg)
if p.FirstFreeSlot() == -1 {
sess.WriteLine(g.colorize(sess, "error", "Your inventory is too full!"))
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
p.Action.Data["step"] = 0
@@ -375,8 +383,8 @@ func (g *Game) depleteSharedTree(st *world.ObjState, cfg *action.GatherConfig, t
}
for _, sess := range g.Hub.PlayersInRoom(st.RoomID) {
- op, ok := sess.Player.(*player.Player)
- if !ok || op.Action == nil || op.Action.Type != "gather" {
+ op := sess.Player
+ if op == nil || op.Action == nil || op.Action.Type != "gather" {
continue
}
if op.Action.Data["instance_key"] != g.World.ObjStateKey(st.RoomID, st.DefID, st.Index) {
@@ -394,7 +402,7 @@ func (g *Game) depleteSharedTree(st *world.ObjState, cfg *action.GatherConfig, t
continue
}
sess.WriteLine(fmt.Sprintf("\n%s", msg))
- g.CancelAction(op)
+ g.cancelAction(op)
g.writePrompt(sess)
}
}
@@ -419,8 +427,8 @@ func (g *Game) flushPendingDepletions() {
if st == nil || st.Depleted {
continue
}
- cfg, err := g.BehaviorStore.LoadGather(pd.behaviorID)
- if err != nil {
+ cfg := loadGatherConfig(g, pd.objDefID)
+ if cfg == nil {
continue
}
g.depleteSharedTree(st, cfg, pd.targetName, pd.playerNames)
@@ -438,7 +446,7 @@ func (g *Game) checkBirdNest(sess *net.Session, p *player.Player, cfg *action.Ga
}
nestName := "bird's nest"
- if def, err := g.ItemStore.Load("birds_nest"); err == nil {
+ if def, err := g.ItemStore.Load(birdsNestItemID); err == nil {
nestName = def.Name
}
@@ -453,12 +461,12 @@ func (g *Game) checkBirdNest(sess *net.Session, p *player.Player, cfg *action.Ga
}
}
if freeSlot == -1 {
- g.World.AddGroundItem(p.RoomID, "birds_nest", 1)
+ g.World.AddGroundItem(p.RoomID, birdsNestItemID, 1)
sess.WriteLine(fmt.Sprintf("A %s falls to the ground.", nestName))
return
}
- p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: "birds_nest", Quantity: 1})
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: birdsNestItemID, Quantity: 1})
g.AccountStore.SaveCharacter(p)
sess.WriteLine(fmt.Sprintf("A %s falls out of the tree!", nestName))
}
diff --git a/internal/game/action_identify.go b/internal/game/action_identify.go
index 3f63e62..b04bf97 100644
--- a/internal/game/action_identify.go
+++ b/internal/game/action_identify.go
@@ -14,7 +14,7 @@ func (g *Game) advanceIdentify(sess *net.Session, p *player.Player) {
scrapCount := p.CountItem("scrap_metal")
if scrapCount == 0 {
sess.WriteLine("You don't have any scrap metal.")
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
@@ -32,7 +32,7 @@ func (g *Game) advanceIdentify(sess *net.Session, p *player.Player) {
}
if !hasExistingStack && p.FirstFreeSlot() == -1 {
sess.WriteLine("Your inventory is too full!")
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
@@ -65,10 +65,7 @@ func (g *Game) advanceIdentify(sess *net.Session, p *player.Player) {
msg := fmt.Sprintf("The altar hums. You identify %d scrap metal into %d %s.", scrapCount, totalJunk, junkName)
- if newLevel := p.AddSkillXP(player.Scavenging, totalXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up",
- fmt.Sprintf("*** You are now level %d scavenging! ***", newLevel)))
- }
+ g.awardSkillXP(sess, p, player.Scavenging, totalXP)
if p.OptionBool("xp_drops") && totalXP > 0 {
msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp scv)", totalXP))
@@ -76,5 +73,5 @@ func (g *Game) advanceIdentify(sess *net.Session, p *player.Player) {
sess.WriteLine(msg)
g.AccountStore.SaveCharacter(p)
- g.CancelAction(p)
+ g.cancelAction(p)
}
diff --git a/internal/game/action_room.go b/internal/game/action_room.go
index 603ac7e..b997352 100644
--- a/internal/game/action_room.go
+++ b/internal/game/action_room.go
@@ -7,7 +7,7 @@ import (
"thehouseoficarus/internal/net"
)
-func (g *Game) RunEnterSteps(sess *net.Session, roomID int) {
+func (g *Game) runEnterSteps(sess *net.Session, roomID int) {
room, err := g.World.LoadRoom(roomID)
if err != nil || len(room.OnEnter) == 0 {
return
@@ -26,11 +26,11 @@ func (g *Game) BroadcastRespawns() {
respawns := g.World.FlushObjRespawns()
for _, st := range respawns {
def, err := g.ObjectStore.Load(st.DefID)
- if err != nil || def.BehaviorID == "" {
+ if err != nil || !def.IsGatherable() {
continue
}
- cfg, err := g.BehaviorStore.LoadGather(def.BehaviorID)
- if err != nil || cfg.RespawnBroadcast == "" {
+ cfg := def.Gather
+ if cfg.RespawnBroadcast == "" {
continue
}
name := def.Name
diff --git a/internal/game/action_search.go b/internal/game/action_search.go
index 884157e..d7dabe0 100644
--- a/internal/game/action_search.go
+++ b/internal/game/action_search.go
@@ -53,7 +53,7 @@ func (g *Game) advanceSearch(sess *net.Session, p *player.Player) {
def, err := g.ItemStore.Load(itemID)
if err != nil {
sess.WriteLine("Something went wrong.")
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
@@ -70,7 +70,7 @@ func (g *Game) advanceSearch(sess *net.Session, p *player.Player) {
slot := p.InvSlot(slotIdx)
if slot == nil || slot.ItemID != itemID || slot.Quantity <= 0 {
sess.WriteLine("The item is gone.")
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
@@ -80,18 +80,18 @@ func (g *Game) advanceSearch(sess *net.Session, p *player.Player) {
p.SetInvSlot(slotIdx, nil)
}
- dt, err := g.BehaviorStore.LoadDropTable(def.SearchTable)
+ dt, err := action.LoadDropTable(g.DataDir, def.SearchTable)
if err == nil && len(dt.Drops) > 0 {
- drop := g.BehaviorStore.ResolveDrop(dt.Drops)
+ drop := action.ResolveDrop(g.DataDir, dt.Drops)
if drop != nil && drop.ItemID != "" {
g.giveSearchLoot(sess, p, drop)
}
}
if def.SearchMiscTable != "" {
- dt2, err := g.BehaviorStore.LoadDropTable(def.SearchMiscTable)
+ dt2, err := action.LoadDropTable(g.DataDir, def.SearchMiscTable)
if err == nil && len(dt2.Drops) > 0 {
- drop := g.BehaviorStore.ResolveDrop(dt2.Drops)
+ drop := action.ResolveDrop(g.DataDir, dt2.Drops)
if drop != nil && drop.ItemID != "" {
g.giveSearchLoot(sess, p, drop)
}
@@ -99,7 +99,7 @@ func (g *Game) advanceSearch(sess *net.Session, p *player.Player) {
}
g.AccountStore.SaveCharacter(p)
- g.CancelAction(p)
+ g.cancelAction(p)
}
func (g *Game) giveSearchLoot(sess *net.Session, p *player.Player, drop *action.DropEntry) {
diff --git a/internal/game/action_state.go b/internal/game/action_state.go
index 55d3392..0ba3767 100644
--- a/internal/game/action_state.go
+++ b/internal/game/action_state.go
@@ -1,7 +1,5 @@
package game
-import "fmt"
-
type ActionType string
const (
@@ -11,7 +9,6 @@ const (
ActionMoving ActionType = "moving"
ActionUsing ActionType = "using"
ActionTalking ActionType = "talking"
- ActionToggling ActionType = "toggling"
ActionBurning ActionType = "burning"
ActionStoking ActionType = "stoking"
ActionPickingUp ActionType = "picking_up"
@@ -23,9 +20,7 @@ const (
ActionFletching ActionType = "fletching"
ActionEating ActionType = "eating"
ActionCleaning ActionType = "cleaning"
- ActionMixing ActionType = "mixing"
ActionIdentifying ActionType = "identifying"
- ActionTriggering ActionType = "triggering"
ActionStealing ActionType = "stealing"
ActionPlanting ActionType = "planting"
ActionHarvesting ActionType = "harvesting_crop"
@@ -63,8 +58,6 @@ func (a *ActionState) Description() string {
return "using a " + a.TargetName
case ActionTalking:
return "talking to " + a.TargetName
- case ActionToggling:
- return fmt.Sprintf("pulling a %s", a.TargetName)
case ActionBurning:
return "trying to start a fire"
case ActionStoking:
@@ -87,12 +80,8 @@ func (a *ActionState) Description() string {
return "eating " + a.TargetName
case ActionCleaning:
return "cleaning herbs"
- case ActionMixing:
- return "mixing " + a.TargetName
case ActionIdentifying:
return "identifying scrap at " + a.TargetName
- case ActionTriggering:
- return "triggering " + a.TargetName
case ActionStealing:
return "stealing from " + a.TargetName
case ActionPlanting:
diff --git a/internal/game/action_steal.go b/internal/game/action_steal.go
index 5347c2a..ee5456c 100644
--- a/internal/game/action_steal.go
+++ b/internal/game/action_steal.go
@@ -16,6 +16,34 @@ import (
"thehouseoficarus/internal/world"
)
+var stallGuardTalk = &action.TalkConfig{
+ Nodes: map[string]action.TalkNode{
+ "start": {
+ Message: "Caught you red-handed! You have three options, thief.",
+ Options: []action.TalkOption{
+ {Text: "\"I'll pay a fine. (500 credits)\"", Goto: "bribe", Condition: &action.Condition{MinCredits: 500}},
+ {Text: "\"Take me to jail.\"", Goto: "jail"},
+ {Text: "\"You'll have to catch me first!\"", Goto: "fight"},
+ },
+ },
+ "bribe": {
+ Message: "Smart choice. Hand over 500 credits and we'll forget this happened.",
+ Action: &action.NodeAction{Cost: 500},
+ Options: []action.TalkOption{{Text: "\"Fine, take it.\"", End: true}},
+ },
+ "jail": {
+ Message: "Off to the detention cell with you!",
+ Action: &action.NodeAction{Teleport: 162},
+ Options: []action.TalkOption{{Text: "(You are dragged away)", End: true}},
+ },
+ "fight": {
+ Message: "Then defend yourself!",
+ Action: &action.NodeAction{SetFlags: map[string]any{"guard_hostile": true}},
+ Options: []action.TalkOption{{Text: "(The guard attacks!)", End: true}},
+ },
+ },
+}
+
var stealSuccess = action.SuccessFormula{
Base: 0.5,
PerLevel: 0.03,
@@ -23,14 +51,14 @@ var stealSuccess = action.SuccessFormula{
}
func (g *Game) doSteal(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if combat.GetCombat(p.Name) != nil {
sess.WriteLine("You can't do that during combat!")
return
}
- g.CancelAction(p)
+ g.cancelAction(p)
targetType, mob, objDef, errMsg := g.resolveStealTarget(sess, p, input)
if errMsg != "" {
@@ -131,7 +159,7 @@ func (g *Game) resolveStealTarget(sess *net.Session, p *player.Player, input str
var matchingObjs []*object.ObjectDef
for _, o := range stealObjs {
- if world.WordPrefixMatch(searchName, o.Name) {
+ if action.WordPrefixMatch(searchName, o.Name) {
matchingObjs = append(matchingObjs, o)
}
}
@@ -265,12 +293,12 @@ func (g *Game) advanceSteal(sess *net.Session, p *player.Player) {
mob := g.MobStore.GetInstance(mobInstanceID)
if mob == nil || mob.HP <= 0 || mob.RoomID != p.RoomID {
sess.WriteLine("Your target is gone.")
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
if combat.IsMobInCombat(mob.InstanceID) {
sess.WriteLine(fmt.Sprintf("The %s is busy.", targetName))
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
data["steal_level"] = mob.StealLevel
@@ -290,7 +318,7 @@ func (g *Game) advanceSteal(sess *net.Session, p *player.Player) {
}
if !found {
sess.WriteLine("Your target is gone.")
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
objDef, err := g.ObjectStore.Load(objDefID)
@@ -328,11 +356,11 @@ func (g *Game) advanceSteal(sess *net.Session, p *player.Player) {
}
if rand.Float64() < chance {
- dt, err := g.BehaviorStore.LoadDropTable(stealTable)
+ dt, err := action.LoadDropTable(g.DataDir, stealTable)
if err != nil || len(dt.Drops) == 0 {
sess.WriteLine("You steal nothing of value.")
} else {
- drop := g.BehaviorStore.ResolveDrop(dt.Drops)
+ drop := action.ResolveDrop(g.DataDir, dt.Drops)
if drop == nil || drop.ItemID == "" {
sess.WriteLine("You steal nothing of value.")
} else {
@@ -341,19 +369,14 @@ func (g *Game) advanceSteal(sess *net.Session, p *player.Player) {
}
if stealXP > 0 {
- prevLevel := p.Level(player.Thieving)
- p.AddSkillXP(player.Thieving, stealXP)
- newLevel := p.Level(player.Thieving)
- if newLevel > prevLevel {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d thieving! ***", newLevel)))
- }
+ g.awardSkillXP(sess, p, player.Thieving, stealXP)
}
g.AccountStore.SaveCharacter(p)
if p.FirstFreeSlot() == -1 {
sess.WriteLine("Your inventory is full.")
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
@@ -363,7 +386,7 @@ func (g *Game) advanceSteal(sess *net.Session, p *player.Player) {
mobInstanceID := data["mob_instance_id"].(string)
mob := g.MobStore.GetInstance(mobInstanceID)
sess.WriteLine(fmt.Sprintf("The %s notices you! They attack!", targetName))
- g.CancelAction(p)
+ g.cancelAction(p)
if mob != nil && mob.HP > 0 {
g.startCombat(sess, p, mob)
}
@@ -372,7 +395,7 @@ func (g *Game) advanceSteal(sess *net.Session, p *player.Player) {
guard := g.findGuardInRoom(p.RoomID, guardMob)
if guard != nil {
sess.WriteLine("You fumble and the Guard spots you!")
- g.CancelAction(p)
+ g.cancelAction(p)
g.startStealGuardTalk(sess, p, guard)
return
}
@@ -433,14 +456,10 @@ func (g *Game) isGuardWatching(roomID int, objDefID string) bool {
}
func (g *Game) startStealGuardTalk(sess *net.Session, p *player.Player, guardMob *world.MobInstance) {
- cfg, err := g.BehaviorStore.LoadTalk("stall_guard_talk")
- if err != nil {
- sess.WriteLine("The Guard glares at you but says nothing.")
- return
- }
-
+ cfg := stallGuardTalk
node, ok := cfg.Nodes["start"]
if !ok {
+ sess.WriteLine("The Guard glares at you but says nothing.")
return
}
@@ -450,7 +469,7 @@ func (g *Game) startStealGuardTalk(sess *net.Session, p *player.Player, guardMob
Type: "talk",
TargetID: guardMob.DefID,
TargetName: guardMob.Name,
- Data: map[string]any{"node": "start", "behavior_id": "stall_guard_talk", "steal_guard": true},
+ Data: map[string]any{"node": "start", "talk_cfg": cfg, "steal_guard": true},
}
g.showTalkNode(sess, node)
diff --git a/internal/game/action_talk.go b/internal/game/action_talk.go
index c18e254..2f463c7 100644
--- a/internal/game/action_talk.go
+++ b/internal/game/action_talk.go
@@ -12,8 +12,8 @@ import (
)
func (g *Game) startMobTalk(sess *net.Session, p *player.Player, mob *world.MobInstance) {
- cfg, err := g.BehaviorStore.LoadTalk(mob.BehaviorID)
- if err != nil {
+ cfg := mob.TalkConfig
+ if cfg == nil {
sess.WriteLine("This person has nothing to say.")
return
}
@@ -24,7 +24,7 @@ func (g *Game) startMobTalk(sess *net.Session, p *player.Player, mob *world.MobI
Type: "talk",
TargetID: mob.DefID,
TargetName: mob.Name,
- Data: map[string]any{"node": "start", "behavior_id": mob.BehaviorID},
+ Data: map[string]any{"node": "start", "talk_cfg": cfg},
}
g.showTalkNode(sess, node)
} else {
@@ -33,8 +33,8 @@ func (g *Game) startMobTalk(sess *net.Session, p *player.Player, mob *world.MobI
}
func (g *Game) startTalk(sess *net.Session, p *player.Player, obj *object.ObjectDef) {
- cfg, err := g.BehaviorStore.LoadTalk(obj.BehaviorID)
- if err != nil {
+ cfg := obj.Talk
+ if cfg == nil {
sess.WriteLine("This person has nothing to say.")
return
}
@@ -45,7 +45,7 @@ func (g *Game) startTalk(sess *net.Session, p *player.Player, obj *object.Object
Type: "talk",
TargetID: obj.ID,
TargetName: obj.Name,
- Data: map[string]any{"node": "start", "behavior_id": obj.BehaviorID},
+ Data: map[string]any{"node": "start", "talk_cfg": cfg},
}
g.showTalkNode(sess, node)
} else {
@@ -66,12 +66,12 @@ func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) {
if g.WorldFlags["guard_hostile"] != nil {
delete(g.WorldFlags, "guard_hostile")
- p, _ := sess.Player.(*player.Player)
+ p := sess.Player
if p != nil && p.Action != nil && p.Action.Data["steal_guard"] == true {
guardMob := g.findGuardInRoom(p.RoomID, "guard")
if guardMob != nil {
sess.State = net.StateGame
- g.CancelAction(p)
+ g.cancelAction(p)
guardMob.Protected = false
g.startCombat(sess, p, guardMob)
}
@@ -99,33 +99,30 @@ func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) {
}
func (g *Game) handleTalkInput(sess *net.Session, input string) {
- p, ok := sess.Player.(*player.Player)
- if !ok || p.Action == nil || p.Action.Type != "talk" {
+ p := sess.Player
+ if p == nil || p.Action == nil || p.Action.Type != "talk" {
sess.State = net.StateGame
g.writePrompt(sess)
return
}
- if p.Action.Data != nil {
- if bid, ok := p.Action.Data["behavior_id"].(string); ok && bid == "estate_directory_talk" {
- g.handleEstateDirectoryTalk(sess, input)
- return
- }
- }
-
input = strings.TrimSpace(input)
lower := strings.ToLower(input)
if lower == "bye" || lower == "exit" || lower == "end" || lower == "quit" {
- g.CancelAction(p)
+ g.cancelAction(p)
sess.State = net.StateGame
g.writePrompt(sess)
return
}
- behaviorID := p.Action.Data["behavior_id"].(string)
- cfg, err := g.BehaviorStore.LoadTalk(behaviorID)
- if err != nil {
- g.CancelAction(p)
+ if _, ok := p.Action.Data["estate_directory"].(bool); ok {
+ g.handleEstateDirectoryTalk(sess, input)
+ return
+ }
+
+ cfg, ok := p.Action.Data["talk_cfg"].(*action.TalkConfig)
+ if !ok || cfg == nil {
+ g.cancelAction(p)
sess.State = net.StateGame
g.writePrompt(sess)
return
@@ -134,7 +131,7 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
nodeKey := p.Action.Data["node"].(string)
node, ok := cfg.Nodes[nodeKey]
if !ok {
- g.CancelAction(p)
+ g.cancelAction(p)
sess.State = net.StateGame
g.writePrompt(sess)
return
@@ -142,7 +139,7 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
idx, err := parseChoiceIndex(input)
if err != nil || idx <= 0 {
- g.CancelAction(p)
+ g.cancelAction(p)
sess.State = net.StateGame
g.writePrompt(sess)
return
@@ -163,14 +160,14 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
}
if chosen == nil {
- g.CancelAction(p)
+ g.cancelAction(p)
sess.State = net.StateGame
g.writePrompt(sess)
return
}
if chosen.End {
- g.CancelAction(p)
+ g.cancelAction(p)
sess.State = net.StateGame
g.writePrompt(sess)
return
@@ -184,7 +181,7 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
}
}
- g.CancelAction(p)
+ g.cancelAction(p)
sess.State = net.StateGame
g.writePrompt(sess)
}
@@ -197,8 +194,8 @@ func (g *Game) enterShop(sess *net.Session, cfg *action.ShopConfig) {
}
func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) {
- p, ok := sess.Player.(*player.Player)
- if !ok {
+ p := sess.Player
+ if p == nil {
return
}
@@ -225,9 +222,7 @@ func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) {
g.WorldFlags[k] = v
}
for k, v := range na.SetPlayerFlags {
- if p.Flags == nil {
- p.Flags = make(map[string]any)
- }
+ p.EnsureFlags()
p.Flags[k] = v
}
if na.TakeItem != "" {
@@ -254,7 +249,7 @@ func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) {
g.Hub.EnterRoom(sess, p.RoomID)
}
g.doLook(sess)
- g.RunEnterSteps(sess, p.RoomID)
+ g.runEnterSteps(sess, p.RoomID)
}
if na.Heal > 0 {
p.HP += na.Heal
@@ -274,69 +269,3 @@ func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) {
g.processSawmill(sess, p)
}
}
-
-func (g *Game) processSawmill(sess *net.Session, p *player.Player) {
- logTypes := map[string]struct {
- plankID string
- cost int
- name string
- }{
- "logs": {"planks", 5, "regular planks"},
- "oak_logs": {"oak_planks", 10, "oak planks"},
- "teak_logs": {"teak_planks", 20, "teak planks"},
- "mahogany_logs": {"mahogany_planks", 40, "mahogany planks"},
- }
-
- totalCost := 0
- totalPlanks := 0
- type plankResult struct {
- plankID string
- qty int
- }
-
- var results []plankResult
-
- for logID, info := range logTypes {
- qty := p.CountItem(logID)
- if qty == 0 {
- continue
- }
- cost := qty * info.cost
- totalCost += cost
- totalPlanks += qty
- results = append(results, plankResult{info.plankID, qty})
- p.RemoveItem(logID, qty)
- }
-
- if totalPlanks == 0 {
- sess.WriteLine("You don't have any logs to process.")
- return
- }
-
- if p.Credits < totalCost {
- sess.WriteLine(fmt.Sprintf("You need %d credits to process all your logs (you have %d).", totalCost, p.Credits))
- return
- }
-
- p.Credits -= totalCost
-
- processed := 0
- for _, r := range results {
- for i := 0; i < r.qty; i++ {
- freeSlot := p.FirstFreeSlot()
- if freeSlot == -1 {
- g.World.AddGroundItem(p.RoomID, r.plankID, r.qty-i)
- break
- }
- p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: r.plankID, Quantity: 1})
- processed++
- }
- }
-
- g.AccountStore.SaveCharacter(p)
-
- sess.WriteLine(fmt.Sprintf("The sawmill operator processes your logs into %d planks for %d credits.", processed, totalCost))
- if processed < totalPlanks {
- sess.WriteLine(fmt.Sprintf("Your inventory is full. The remaining %d planks fall to the ground.", totalPlanks-processed))
- }
-}
diff --git a/internal/game/action_toggle.go b/internal/game/action_toggle.go
deleted file mode 100644
index db7a199..0000000
--- a/internal/game/action_toggle.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package game
-
-import (
- "fmt"
-
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/object"
- "thehouseoficarus/internal/player"
-)
-
-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
- }
-
- p.ActionState = &ActionState{Type: ActionToggling, TargetName: obj.Name}
-
- 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(g.colorize(other, "broadcast", fmt.Sprintf("\n%s uses the %s.", p.Name, obj.Name)))
- }
- }
- }
-}
diff --git a/internal/game/action_use.go b/internal/game/action_use.go
index 78bf028..368162b 100644
--- a/internal/game/action_use.go
+++ b/internal/game/action_use.go
@@ -12,8 +12,8 @@ import (
)
func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectDef) {
- cfg, err := g.BehaviorStore.LoadUse(obj.BehaviorID)
- if err != nil {
+ cfg := obj.Use
+ if cfg == nil {
sess.WriteLine("You can't use this.")
return
}
@@ -49,17 +49,16 @@ func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectD
TargetID: obj.ID,
TargetName: obj.Name,
WaitLeft: engine.ToTicks(1),
- Data: map[string]any{"step": 0, "behavior_id": obj.BehaviorID},
+ Data: map[string]any{"step": 0, "use_cfg": cfg},
}
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)
+ cfg, ok := p.Action.Data["use_cfg"].(*action.UseConfig)
+ if !ok || cfg == nil {
+ g.cancelAction(p)
return
}
@@ -70,12 +69,12 @@ func (g *Game) advanceUse(sess *net.Session, p *player.Player) {
defName = def.Name
}
sess.WriteLine(fmt.Sprintf("You've run out of %s.", defName))
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
if !p.RemoveItem(itemID, qty) {
sess.WriteLine(fmt.Sprintf("You need %d of %s.", qty, itemID))
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
}
@@ -95,7 +94,7 @@ func (g *Game) advanceUse(sess *net.Session, p *player.Player) {
freeSlot := p.FirstFreeSlot()
if freeSlot == -1 {
sess.WriteLine("Your inventory is full.")
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
@@ -106,9 +105,7 @@ func (g *Game) advanceUse(sess *net.Session, p *player.Player) {
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: cfg.Reward.ItemID, Quantity: qty})
if cfg.XP > 0 {
- if newLevel := p.AddSkillXP(player.SkillName(cfg.Skill), cfg.XP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, cfg.Skill)))
- }
+ g.awardSkillXP(sess, p, player.SkillName(cfg.Skill), cfg.XP)
}
g.AccountStore.SaveCharacter(p)
@@ -125,7 +122,7 @@ func (g *Game) advanceUse(sess *net.Session, p *player.Player) {
if p.FirstFreeSlot() == -1 {
sess.WriteLine("Your inventory is full.")
- g.CancelAction(p)
+ g.cancelAction(p)
return
}
diff --git a/internal/game/aggro.go b/internal/game/aggro.go
index 8f4da42..2a9fff4 100644
--- a/internal/game/aggro.go
+++ b/internal/game/aggro.go
@@ -6,11 +6,10 @@ import (
"thehouseoficarus/internal/combat"
"thehouseoficarus/internal/engine"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) checkAggro(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if combat.GetCombat(p.Name) != nil {
return
diff --git a/internal/game/assassin.go b/internal/game/assassin.go
index d7af1a4..1927399 100644
--- a/internal/game/assassin.go
+++ b/internal/game/assassin.go
@@ -3,52 +3,12 @@ package game
import (
"fmt"
"math/rand"
- "strings"
- "thehouseoficarus/internal/combat"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
"thehouseoficarus/internal/world"
)
-func getPlayerFlagInt(p *player.Player, key string) int {
- if p.Flags == nil {
- return 0
- }
- val, ok := p.Flags[key]
- if !ok {
- return 0
- }
- switch v := val.(type) {
- case int:
- return v
- case int64:
- return int(v)
- case float64:
- return int(v)
- }
- return 0
-}
-
-func getPlayerFlagString(p *player.Player, key string) string {
- if p.Flags == nil {
- return ""
- }
- val, ok := p.Flags[key]
- if !ok {
- return ""
- }
- s, _ := val.(string)
- return s
-}
-
-func setPlayerFlag(p *player.Player, key string, val any) {
- if p.Flags == nil {
- p.Flags = make(map[string]any)
- }
- p.Flags[key] = val
-}
-
type assassinTaskEntry struct {
MobID string
MinLevel int
@@ -114,10 +74,7 @@ func (g *Game) onAssassinKill(sess *net.Session, p *player.Player, mob *world.Mo
return
}
xp := mob.MaxHP * 2
- newLevel := p.AddSkillXP(player.Assassin, xp)
- if newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d Assassin! ***", newLevel)))
- }
+ g.awardSkillXP(sess, p, player.Assassin, xp)
if p.OptionBool("xp_drops") {
sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp asm)", xp)))
}
@@ -215,70 +172,3 @@ func streakBonus(streak int) int {
}
return 0
}
-
-func (g *Game) tryFinishingBlow(sess *net.Session, input string) bool {
- p := sess.Player.(*player.Player)
- cs := combat.GetCombat(p.Name)
- if cs == nil {
- return false
- }
- mob := g.MobStore.GetInstance(cs.MobID)
- if mob == nil || mob.FinishingBlow == "" || mob.HP != 1 {
- return false
- }
- lower := strings.ToLower(input)
- var itemPart, targetPart string
- for _, sep := range []string{" on ", " with "} {
- if idx := strings.Index(lower, sep); idx > 0 {
- itemPart = strings.TrimSpace(input[:idx])
- targetPart = strings.TrimSpace(input[idx+len(sep):])
- break
- }
- }
- if targetPart == "" {
- itemPart = strings.TrimSpace(input)
- if mob.MatchQuality(itemPart) != world.MatchNone {
- targetPart = itemPart
- itemPart = strings.TrimSpace(mob.FinishingBlow)
- }
- }
- if targetPart == "" {
- return false
- }
- if mob.MatchQuality(targetPart) == world.MatchNone {
- return false
- }
- g.doFinishingBlow(sess, p, mob, itemPart)
- return true
-}
-
-func (g *Game) doFinishingBlow(sess *net.Session, p *player.Player, mob *world.MobInstance, itemInput string) {
- fbDef, err := g.ItemStore.Load(mob.FinishingBlow)
- if err != nil {
- sess.WriteLine("Something went wrong.")
- return
- }
- if !fbDef.MatchesName(itemInput) && !strings.EqualFold(itemInput, fbDef.ID) {
- sess.WriteLine(fmt.Sprintf("That won't work on %s. You need %s.", mobDisplayName(mob, true), fbDef.Name))
- return
- }
- if !p.HasItem(mob.FinishingBlow) {
- sess.WriteLine(fmt.Sprintf("You don't have any %s.", fbDef.Name))
- return
- }
- autoKey := "assassin_unlocked_auto_" + mob.FinishingBlow
- consumed := true
- if p.Flags != nil {
- if val, ok := p.Flags[autoKey]; ok {
- if b, ok := val.(bool); ok && b {
- consumed = false
- }
- }
- }
- if consumed {
- p.RemoveItem(mob.FinishingBlow, 1)
- }
- sess.WriteLine(fmt.Sprintf("\nYou use the %s on %s!", g.itemColorize(sess, fbDef, fbDef.Name), g.colorize(sess, "mob_name", mobDisplayName(mob, true))))
- mob.HP = 0
- g.endCombat(sess, p, mob)
-}
diff --git a/internal/game/buff.go b/internal/game/buff.go
index 39abd8a..9449731 100644
--- a/internal/game/buff.go
+++ b/internal/game/buff.go
@@ -15,7 +15,7 @@ func (g *Game) BuffTick() {
if sess.Player == nil {
continue
}
- p := sess.Player.(*player.Player)
+ p := sess.Player
remaining := make([]player.PotionBuff, 0, len(p.ActiveBuffs))
dirty := false
for _, buff := range p.ActiveBuffs {
diff --git a/internal/game/cmd_agility.go b/internal/game/cmd_agility.go
index 3bd777f..084c5cf 100644
--- a/internal/game/cmd_agility.go
+++ b/internal/game/cmd_agility.go
@@ -9,7 +9,7 @@ import (
)
func (g *Game) doObstacle(sess *net.Session, verb string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if combat.GetCombat(p.Name) != nil {
sess.WriteLine("You can't do that during combat!")
@@ -29,9 +29,9 @@ func (g *Game) doObstacle(sess *net.Session, verb string) {
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
g.startObstacle(sess, p, info)
}
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index 77cd569..b0ce214 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -6,6 +6,7 @@ import (
"strconv"
"strings"
+ "thehouseoficarus/internal/action"
"thehouseoficarus/internal/combat"
"thehouseoficarus/internal/engine"
"thehouseoficarus/internal/net"
@@ -15,7 +16,7 @@ import (
)
func (g *Game) doAttack(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if combat.GetCombat(p.Name) != nil {
sess.WriteLine("You are already in combat!")
@@ -23,7 +24,7 @@ func (g *Game) doAttack(sess *net.Session, input string) {
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
mob := g.findMob(sess, input, p.RoomID)
@@ -125,17 +126,17 @@ func (g *Game) findMob(sess *net.Session, input string, roomID int) *world.MobIn
func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) {
g.cancelRest(p.Name)
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
combat.EnterCombat(p.Name, mob.InstanceID)
- autocastActive := p.AutocastMod != ""
+ autotriggerActive := p.AutotriggerMod != ""
playerSpeed := g.playerWeaponSpeed(p)
- if autocastActive {
+ if autotriggerActive {
playerSpeed = 5.0
}
- if !autocastActive {
+ if !autotriggerActive {
attBonus, strBonus, defBonus := combat.AttackStyleBonus(string(p.AttackStyle))
var styleParts []string
if attBonus > 0 {
@@ -153,8 +154,8 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
}
sess.WriteLine(fmt.Sprintf("\nYou attack %s!%s", g.colorize(sess, "mob_name", mobDisplayName(mob, true)), styleStr))
} else {
- mod := GetMod(p.AutocastMod)
- modName := p.AutocastMod
+ mod := GetMod(p.AutotriggerMod)
+ modName := p.AutotriggerMod
if mod != nil {
modName = mod.Name
}
@@ -182,16 +183,16 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
g.endCombat(sess, p, currentMob)
return false
}
- if p.AutocastMod != "" {
- mod := GetMod(p.AutocastMod)
+ if p.AutotriggerMod != "" {
+ mod := GetMod(p.AutotriggerMod)
if mod != nil {
if !g.scienceAttack(sess, p, currentMob, mod) {
- p.AutocastMod = ""
+ p.AutotriggerMod = ""
sess.WriteLine("You've run out of junk. Switching to melee.")
g.playerAttack(sess, p, currentMob)
}
} else {
- p.AutocastMod = ""
+ p.AutotriggerMod = ""
g.playerAttack(sess, p, currentMob)
}
} else {
@@ -228,8 +229,17 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI
return
}
- attackType := "crush"
- var weaponType object.WeaponType
+ attackType, weaponType, attRoll, defRoll, maxHit := g.calculatePlayerAttackRoll(p, mob)
+
+ if combat.HitCheck(attRoll, defRoll) {
+ g.applyPlayerHit(sess, p, mob, attackType, weaponType, maxHit)
+ } else {
+ g.applyPlayerMiss(sess, p, mob, weaponType)
+ }
+}
+
+func (g *Game) calculatePlayerAttackRoll(p *player.Player, mob *world.MobInstance) (attackType string, weaponType object.WeaponType, attRoll int, defRoll int, maxHit int) {
+ attackType = "crush"
if itemID, ok := p.Equipment[object.SlotMainHand]; ok {
if def, err := g.ItemStore.Load(itemID); err == nil {
if def.AttackType != "" {
@@ -246,8 +256,6 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI
totals := g.playerEquipBonuses(p)
isRanged := weaponType == object.WeaponRanged
- var attRoll, defRoll, maxHit int
-
if isRanged {
rangedBonus, _ := combat.RangedStyleBonus(string(p.AttackStyle))
equipAttack := totals.RangedAttack
@@ -273,90 +281,99 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI
defRoll = combat.DefenseRoll(mob.Defense, 0, mobDefBonus)
maxHit = combat.MaxHit(p.Level(player.Strength)+g.techLevelBonus(p, "strength")+g.buffLevelBonus(p, "strength"), strBonus, totals.StrengthBonus)
}
+ return
+}
- if combat.HitCheck(attRoll, defRoll) {
- dmg := combat.RollDamage(maxHit)
+func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.MobInstance, attackType string, weaponType object.WeaponType, maxHit int) {
+ dmg := combat.RollDamage(maxHit)
- mob.HP -= dmg
- if mob.HP < 0 {
- mob.HP = 0
- }
- if mob.FinishingBlow != "" && mob.HP <= 0 {
- mob.HP = 1
- }
- if mob.HP < mob.MaxHP && mob.HP > 0 {
- mob.StartRegen()
- }
+ mob.HP -= dmg
+ if mob.HP < 0 {
+ mob.HP = 0
+ }
+ if mob.FinishingBlow != "" && mob.HP <= 0 {
+ mob.HP = 1
+ }
+ if mob.HP < mob.MaxHP && mob.HP > 0 {
+ mob.StartRegen()
+ }
- if mob.FinishingBlow != "" && mob.HP == 1 {
- fbDef, _ := g.ItemStore.Load(mob.FinishingBlow)
- fbName := mob.FinishingBlow
- if fbDef != nil {
- fbName = fbDef.Name
- }
- sess.WriteLine(g.colorize(sess, "warning",
- fmt.Sprintf(" %s resists death! Use %s on it to finish it off.",
- mobDisplayName(mob, false), fbName)))
+ if mob.FinishingBlow != "" && mob.HP == 1 {
+ fbDef, _ := g.ItemStore.Load(mob.FinishingBlow)
+ fbName := mob.FinishingBlow
+ if fbDef != nil {
+ fbName = fbDef.Name
}
- gains, leveled := g.awardCombatXP(p, dmg, isRanged)
+ sess.WriteLine(g.colorize(sess, "warning",
+ fmt.Sprintf(" %s resists death! Use %s on it to finish it off.",
+ mobDisplayName(mob, false), fbName)))
+ }
- if isRanged {
- p.AmmoQty--
- if p.AmmoQty <= 0 {
- delete(p.Equipment, object.SlotAmmo)
- p.AmmoQty = 0
- }
- g.AccountStore.SaveCharacter(p)
- }
+ isRanged := weaponType == object.WeaponRanged
+ gains := g.awardCombatXP(sess, p, dmg, isRanged)
- for _, skill := range leveled {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", p.Level(skill), skill)))
+ if isRanged {
+ p.AmmoQty--
+ if p.AmmoQty <= 0 {
+ delete(p.Equipment, object.SlotAmmo)
+ p.AmmoQty = 0
}
+ g.AccountStore.SaveCharacter(p)
+ }
- mobName := mobDisplayName(mob, true)
- attacker := mob.Name
- if !mob.Unique {
- attacker = "The " + mob.Name
- }
- w := len(fmt.Sprintf(" You hit %s for %d damage.", mobName, dmg))
- if w2 := len(fmt.Sprintf(" %s hits you for %d damage.", attacker, dmg)); w2 > w {
- w = w2
- }
- g.combatPadWidth = w
- prefix := fmt.Sprintf(" You hit %s for %s damage.", g.colorize(sess, "mob_name", mobName), g.colorize(sess, "damage", fmt.Sprint(dmg)))
- hpPart := fmt.Sprintf("[%s/%dhp]", g.colorize(sess, "enemy_hp", fmt.Sprint(mob.HP)), mob.MaxHP)
- line := fmt.Sprintf("%-*s %s", g.combatPadWidth, prefix, hpPart)
- if p.OptionBool("xp_drops") && len(gains) > 0 {
- var parts []string
- for _, gain := range gains {
- parts = append(parts, fmt.Sprintf("+%dxp %s", gain.XP, player.SkillAbbr[player.SkillName(gain.Skill)]))
- }
- line += g.colorize(sess, "xp", " ("+strings.Join(parts, ", ")+")")
+ mobName := mobDisplayName(mob, true)
+ attacker := mob.Name
+ if !mob.Unique {
+ attacker = "The " + mob.Name
+ }
+ w := len(fmt.Sprintf(" You hit %s for %d damage.", mobName, dmg))
+ if w2 := len(fmt.Sprintf(" %s hits you for %d damage.", attacker, dmg)); w2 > w {
+ w = w2
+ }
+ prefix := fmt.Sprintf(" You hit %s for %s damage.", g.colorize(sess, "mob_name", mobName), g.colorize(sess, "damage", fmt.Sprint(dmg)))
+ hpPart := fmt.Sprintf("[%s/%dhp]", g.colorize(sess, "enemy_hp", fmt.Sprint(mob.HP)), mob.MaxHP)
+ line := fmt.Sprintf("%-*s %s", w, prefix, hpPart)
+ if p.OptionBool("xp_drops") && len(gains) > 0 {
+ var parts []string
+ for _, gain := range gains {
+ parts = append(parts, fmt.Sprintf("+%dxp %s", gain.XP, player.SkillAbbr[player.SkillName(gain.Skill)]))
}
- sess.WriteLine(line)
+ line += g.colorize(sess, "xp", " ("+strings.Join(parts, ", ")+")")
+ }
+ sess.WriteLine(line)
- if isRanged && p.AmmoQty <= 0 {
+ if isRanged && p.AmmoQty <= 0 {
+ sess.WriteLine("You've run out of ammo!")
+ combat.LeaveCombat(p.Name)
+ }
+}
+
+func (g *Game) applyPlayerMiss(sess *net.Session, p *player.Player, mob *world.MobInstance, weaponType object.WeaponType) {
+ sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf(" You miss %s.", mobDisplayName(mob, true))))
+
+ if weaponType == object.WeaponRanged {
+ p.AmmoQty--
+ if p.AmmoQty <= 0 {
+ delete(p.Equipment, object.SlotAmmo)
+ p.AmmoQty = 0
+ g.AccountStore.SaveCharacter(p)
sess.WriteLine("You've run out of ammo!")
combat.LeaveCombat(p.Name)
}
- } else {
- sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf(" You miss %s.", mobDisplayName(mob, true))))
-
- if isRanged {
- p.AmmoQty--
- if p.AmmoQty <= 0 {
- delete(p.Equipment, object.SlotAmmo)
- p.AmmoQty = 0
- g.AccountStore.SaveCharacter(p)
- sess.WriteLine("You've run out of ammo!")
- combat.LeaveCombat(p.Name)
- }
- }
}
}
func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) {
- cs := combat.GetCombat(p.Name)
+ attRoll, defRoll := g.calculateMobAttack(p, mob)
+
+ if combat.HitCheck(attRoll, defRoll) {
+ g.applyMobHit(sess, p, mob)
+ } else {
+ g.applyMobMiss(sess, mob)
+ }
+}
+
+func (g *Game) calculateMobAttack(p *player.Player, mob *world.MobInstance) (attRoll int, defRoll int) {
_, _, defStyleBonus := combat.AttackStyleBonus(string(p.AttackStyle))
mobAttackType := mob.AttackType
@@ -364,87 +381,89 @@ func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInst
mobAttackType = "crush"
}
- attRoll := combat.AttackRoll(mob.Attack, 0, mob.AttackBonus)
+ attRoll = combat.AttackRoll(mob.Attack, 0, mob.AttackBonus)
totals := g.playerEquipBonuses(p)
equipDef := combat.SelectDefenseBonus(mobAttackType,
totals.StabDefense, totals.SlashDefense, totals.CrushDefense,
totals.ScienceDefense, totals.RangedDefense)
- defRoll := combat.DefenseRoll(p.Level(player.Defense)+g.techLevelBonus(p, "defense")+g.buffLevelBonus(p, "defense"), defStyleBonus, equipDef)
+ defRoll = combat.DefenseRoll(p.Level(player.Defense)+g.techLevelBonus(p, "defense")+g.buffLevelBonus(p, "defense"), defStyleBonus, equipDef)
+ return
+}
- if combat.HitCheck(attRoll, defRoll) {
- maxHit := combat.MaxHit(mob.Strength, 0, mob.StrengthBonus)
- dmg := combat.RollDamage(maxHit)
- dmg = g.applyTechProtection(p, mob, dmg)
-
- if mob.DamageWithout != "" {
- hasProtection := false
- for _, itemID := range p.Equipment {
- if itemID == mob.DamageWithout {
- hasProtection = true
- break
- }
+func (g *Game) applyMobHit(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ maxHit := combat.MaxHit(mob.Strength, 0, mob.StrengthBonus)
+ dmg := combat.RollDamage(maxHit)
+ dmg = g.applyTechProtection(p, mob, dmg)
+
+ if mob.DamageWithout != "" {
+ hasProtection := false
+ for _, itemID := range p.Equipment {
+ if itemID == mob.DamageWithout {
+ hasProtection = true
+ break
+ }
+ }
+ if !hasProtection {
+ dmg = dmg * 3 / 2
+ if dmg < 1 {
+ dmg = 1
}
- if !hasProtection {
- dmg = dmg * 3 / 2
- if dmg < 1 {
- dmg = 1
+ cs := combat.GetCombat(p.Name)
+ if cs != nil && !cs.DamageWarningShown {
+ cs.DamageWarningShown = true
+ fbDef, _ := g.ItemStore.Load(mob.DamageWithout)
+ fbName := mob.DamageWithout
+ if fbDef != nil {
+ fbName = fbDef.Name
}
- if cs != nil && !cs.DamageWarningShown {
- cs.DamageWarningShown = true
- fbDef, _ := g.ItemStore.Load(mob.DamageWithout)
- fbName := mob.DamageWithout
- if fbDef != nil {
- fbName = fbDef.Name
- }
- attacker := mob.Name
- if !mob.Unique {
- attacker = "The " + mob.Name
- }
- sess.WriteLine(g.colorize(sess, "warning",
- fmt.Sprintf(" %s's attack is extra effective! Equip %s for protection.",
- attacker, fbName)))
+ attacker := mob.Name
+ if !mob.Unique {
+ attacker = "The " + mob.Name
}
+ sess.WriteLine(g.colorize(sess, "warning",
+ fmt.Sprintf(" %s's attack is extra effective! Equip %s for protection.",
+ attacker, fbName)))
}
}
+ }
- p.HP -= dmg
- if p.HP < 0 {
- p.HP = 0
- }
- p.StartRegen()
- g.AccountStore.SaveCharacter(p)
+ p.HP -= dmg
+ if p.HP < 0 {
+ p.HP = 0
+ }
+ p.StartRegen()
+ g.AccountStore.SaveCharacter(p)
- attacker := mob.Name
- if !mob.Unique {
- attacker = "The " + mob.Name
- }
- mobName := mobDisplayName(mob, true)
- w := len(fmt.Sprintf(" %s hits you for %d damage.", attacker, dmg))
- if w2 := len(fmt.Sprintf(" You hit %s for %d damage.", mobName, dmg)); w2 > w {
- w = w2
- }
- g.combatPadWidth = w
- 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))
+ attacker := mob.Name
+ if !mob.Unique {
+ attacker = "The " + mob.Name
+ }
+ mobName := mobDisplayName(mob, true)
+ w := len(fmt.Sprintf(" %s hits you for %d damage.", attacker, dmg))
+ if w2 := len(fmt.Sprintf(" You hit %s for %d damage.", mobName, dmg)); w2 > w {
+ w = w2
+ }
+ 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", w, 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 {
- attacker = "The " + mob.Name
- }
- sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf(" %s misses you.", attacker)))
+ if p.MoveTicks > 0 {
+ p.ClearMoveState()
+ p.ActionState = &ActionState{Type: ActionCombating, TargetName: mob.Name}
+ sess.WriteLine("Can't escape!")
+ }
+}
+
+func (g *Game) applyMobMiss(sess *net.Session, mob *world.MobInstance) {
+ attacker := mob.Name
+ if !mob.Unique {
+ attacker = "The " + mob.Name
}
+ sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf(" %s misses you.", attacker)))
}
func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) {
- g.combatPadWidth = 0
combat.LeaveCombat(p.Name)
p.ActionState = nil
@@ -486,7 +505,7 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
if g.Hub != nil {
for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
if other != sess && other.Player != nil {
- op := other.Player.(*player.Player)
+ op := other.Player
mobLvl := mobCombatLevel(mob)
levelStr := g.levelColorize(other, op.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl))
other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s has slain %s %s!", p.Name, mobDisplayName(mob, false), levelStr)))
@@ -510,7 +529,7 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
}
if len(mob.Drops.Loot) > 0 {
- entry := g.BehaviorStore.ResolveDrop(mob.Drops.Loot)
+ entry := action.ResolveDrop(g.DataDir, mob.Drops.Loot)
if entry != nil && entry.ItemID != "" {
qty := entry.Quantity
if qty <= 0 {
@@ -548,10 +567,9 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
}
}
-func (g *Game) awardCombatXP(p *player.Player, dmg int, isRanged bool) ([]xpGain, []player.SkillName) {
+func (g *Game) awardCombatXP(sess *net.Session, p *player.Player, dmg int, isRanged bool) []xpGain {
baseXP := dmg * 4
var gains []xpGain
- var leveledUp []player.SkillName
if isRanged {
gains = []xpGain{
@@ -578,71 +596,10 @@ func (g *Game) awardCombatXP(p *player.Player, dmg int, isRanged bool) ([]xpGain
}
for _, gain := range gains {
- if newLevel := p.AddSkillXP(player.SkillName(gain.Skill), gain.XP); newLevel > 0 {
- leveledUp = append(leveledUp, player.SkillName(gain.Skill))
- }
+ g.awardSkillXP(sess, p, player.SkillName(gain.Skill), gain.XP)
}
g.AccountStore.SaveCharacter(p)
- return gains, leveledUp
-}
-
-func (g *Game) dropItemsOnDeath(p *player.Player) {
- roomID := p.RoomID
-
- if p.Credits > 0 {
- g.World.AddGroundItem(roomID, "credits", p.Credits)
- p.Credits = 0
- }
-
- var items []deathDrop
-
- for slot, inv := range p.Inventory {
- if inv == nil || inv.Quantity <= 0 {
- continue
- }
- val := 0
- if def, err := g.ItemStore.Load(inv.ItemID); err == nil {
- val = def.Value * inv.Quantity
- }
- items = append(items, deathDrop{
- itemID: inv.ItemID,
- quantity: inv.Quantity,
- totalVal: val,
- invSlot: slot,
- })
- }
-
- for eqSlot, itemID := range p.Equipment {
- val := 0
- if def, err := g.ItemStore.Load(itemID); err == nil {
- val = def.Value
- }
- items = append(items, deathDrop{
- itemID: itemID,
- quantity: 1,
- totalVal: val,
- isEquip: true,
- equipSlot: eqSlot,
- })
- }
-
- if len(items) <= 3 {
- return
- }
-
- sort.Slice(items, func(i, j int) bool {
- return items[i].totalVal > items[j].totalVal
- })
-
- for i := 3; i < len(items); i++ {
- it := items[i]
- if it.isEquip {
- delete(p.Equipment, it.equipSlot)
- } else {
- p.SetInvSlot(it.invSlot, nil)
- }
- g.World.AddGroundItem(roomID, it.itemID, it.quantity)
- }
+ return gains
}
func (g *Game) stopCombat(playerName string) {
@@ -663,7 +620,7 @@ func (g *Game) respawnMob(instanceID string) {
if g.Hub != nil {
for _, sess := range g.Hub.PlayersInRoom(homeRoom) {
- if p, ok := sess.Player.(*player.Player); ok && p.OptionBool("mob_spawn") {
+ if p := sess.Player; p != nil && p.OptionBool("mob_spawn") {
mobLvl := mobCombatLevel(inst)
levelStr := g.levelColorize(sess, p.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl))
sess.WriteLine(fmt.Sprintf("\n%s %s spawns in the area.", g.colorize(sess, "mob_name", mobDisplayName(inst, false)), levelStr))
@@ -672,12 +629,3 @@ func (g *Game) respawnMob(instanceID string) {
}
}
-func (g *Game) playerWeaponSpeed(p *player.Player) float64 {
- if itemID, ok := p.Equipment[object.SlotMainHand]; ok {
- def, err := g.ItemStore.Load(itemID)
- if err == nil && def.Speed > 0 {
- return def.Speed
- }
- }
- return 5.0
-}
diff --git a/internal/game/cmd_autocast.go b/internal/game/cmd_autocast.go
deleted file mode 100644
index 205fd8d..0000000
--- a/internal/game/cmd_autocast.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package game
-
-import (
- "fmt"
- "strings"
-
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
-)
-
-func (g *Game) doAutocast(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- input = strings.TrimSpace(input)
-
- if input == "" {
- if p.AutocastMod == "" {
- sess.WriteLine("No autocast mod set. Use 'autocast <mod>' to set one.")
- } else {
- mod := GetMod(p.AutocastMod)
- if mod == nil {
- sess.WriteLine("Autocast: none (invalid mod)")
- p.AutocastMod = ""
- } else {
- sess.WriteLine(fmt.Sprintf("Autocast: %s (Lv%d)", mod.Name, mod.Level))
- }
- }
- return
- }
-
- if strings.ToLower(input) == "off" {
- p.AutocastMod = ""
- sess.WriteLine("Autocast disabled.")
- return
- }
-
- mod := FindMod(strings.ToLower(input))
- if mod == nil {
- sess.WriteLine("Unknown mod.")
- return
- }
-
- if mod.Category != ModCombat {
- sess.WriteLine("You can only autocast combat mods.")
- return
- }
-
- if p.Level(player.Science) < mod.Level {
- sess.WriteLine(fmt.Sprintf("You need level %d science to autocast %s.", mod.Level, mod.Name))
- return
- }
-
- p.AutocastMod = mod.ID
- sess.WriteLine(fmt.Sprintf("Autocast set to: %s", mod.Name))
-}
diff --git a/internal/game/cmd_autotrigger.go b/internal/game/cmd_autotrigger.go
new file mode 100644
index 0000000..3c41735
--- /dev/null
+++ b/internal/game/cmd_autotrigger.go
@@ -0,0 +1,54 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) doAutotrigger(sess *net.Session, input string) {
+ p := sess.Player
+ input = strings.TrimSpace(input)
+
+ if input == "" {
+ if p.AutotriggerMod == "" {
+ sess.WriteLine("No autotrigger mod set. Use 'autotrigger <mod>' to set one.")
+ } else {
+ mod := GetMod(p.AutotriggerMod)
+ if mod == nil {
+ sess.WriteLine("Autotrigger: none (invalid mod)")
+ p.AutotriggerMod = ""
+ } else {
+ sess.WriteLine(fmt.Sprintf("Autotrigger: %s (Lv%d)", mod.Name, mod.Level))
+ }
+ }
+ return
+ }
+
+ if strings.ToLower(input) == "off" {
+ p.AutotriggerMod = ""
+ sess.WriteLine("Autotrigger disabled.")
+ return
+ }
+
+ mod := FindMod(strings.ToLower(input))
+ if mod == nil {
+ sess.WriteLine("Unknown mod.")
+ return
+ }
+
+ if mod.Category != ModCombat {
+ sess.WriteLine("You can only autotrigger combat mods.")
+ return
+ }
+
+ if p.Level(player.Science) < mod.Level {
+ sess.WriteLine(fmt.Sprintf("You need level %d science to autotrigger %s.", mod.Level, mod.Name))
+ return
+ }
+
+ p.AutotriggerMod = mod.ID
+ sess.WriteLine(fmt.Sprintf("Autotrigger set to: %s", mod.Name))
+}
diff --git a/internal/game/cmd_bank.go b/internal/game/cmd_bank.go
index ff3a3ac..0785b81 100644
--- a/internal/game/cmd_bank.go
+++ b/internal/game/cmd_bank.go
@@ -5,9 +5,9 @@ import (
"sort"
"strings"
+ "thehouseoficarus/internal/action"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
- "thehouseoficarus/internal/world"
)
func (g *Game) handleBankInput(sess *net.Session, input string) {
@@ -57,7 +57,7 @@ func (g *Game) writeBankPrompt(sess *net.Session) {
}
func (g *Game) showBankBrowse(sess *net.Session) {
- p, _ := sess.Player.(*player.Player)
+ p := sess.Player
unicode := p.OptionBool("unicode")
@@ -110,7 +110,7 @@ func (g *Game) showBankBrowse(sess *net.Session) {
}
func (g *Game) doBankDeposit(sess *net.Session, input string) {
- p, _ := sess.Player.(*player.Player)
+ p := sess.Player
matches := g.findInventoryMatches(input, p)
if len(matches) == 0 {
@@ -158,7 +158,7 @@ func (g *Game) doBankDeposit(sess *net.Session, input string) {
}
func (g *Game) doBankDepositAll(sess *net.Session) {
- p, _ := sess.Player.(*player.Player)
+ p := sess.Player
var deposited int
for i := 0; i < 28; i++ {
@@ -187,7 +187,7 @@ func (g *Game) doBankDepositAll(sess *net.Session) {
}
func (g *Game) doBankWithdraw(sess *net.Session, input string) {
- p, _ := sess.Player.(*player.Player)
+ p := sess.Player
freeSlot := p.FirstFreeSlot()
if freeSlot == -1 {
@@ -235,7 +235,7 @@ func (g *Game) doBankWithdraw(sess *net.Session, input string) {
}
lower := strings.ToLower(input)
for _, e := range entries {
- if world.WordPrefixMatch(e.name, lower) {
+ if action.WordPrefixMatch(e.name, lower) {
matches = append(matches, e)
}
}
@@ -326,7 +326,7 @@ func (g *Game) canUseBank(p *player.Player) bool {
}
func (g *Game) doBank(sess *net.Session) {
- p, _ := sess.Player.(*player.Player)
+ p := sess.Player
if !g.canUseBank(p) {
sess.WriteLine("There is no bank here.")
diff --git a/internal/game/cmd_clean.go b/internal/game/cmd_clean.go
index 70dedb9..1f00d75 100644
--- a/internal/game/cmd_clean.go
+++ b/internal/game/cmd_clean.go
@@ -12,7 +12,7 @@ import (
)
func (g *Game) doClean(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if combat.GetCombat(p.Name) != nil {
sess.WriteLine("You can't do that during combat!")
@@ -50,7 +50,7 @@ func (g *Game) doClean(sess *net.Session, input string) {
return
}
- g.CancelBackgroundAction(p)
+ g.cancelBackgroundAction(p)
p.BackgroundAction = &action.Action{
Type: "clean",
diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go
index 1dd14fa..3fb7716 100644
--- a/internal/game/cmd_color.go
+++ b/internal/game/cmd_color.go
@@ -7,7 +7,6 @@ import (
"thehouseoficarus/internal/color"
"thehouseoficarus/internal/config"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doColor(sess *net.Session, input string) {
@@ -160,7 +159,7 @@ var colorCategoryOrder = []string{
}
func showColorTable(g *Game, sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
mode := g.colorMode(sess)
table := &Table{
Columns: []string{
diff --git a/internal/game/cmd_colortable.go b/internal/game/cmd_colortable.go
index 90ae8c4..5aa36d9 100644
--- a/internal/game/cmd_colortable.go
+++ b/internal/game/cmd_colortable.go
@@ -6,7 +6,6 @@ import (
"thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doColortable(sess *net.Session) {
@@ -16,7 +15,7 @@ func (g *Game) doColortable(sess *net.Session) {
return
}
- p := sess.Player.(*player.Player)
+ p := sess.Player
unicode := p.OptionBool("unicode")
sess.WriteLine(fmt.Sprintf("\nColor mode: %s", mode))
diff --git a/internal/game/cmd_construct.go b/internal/game/cmd_construct.go
deleted file mode 100644
index 4ab6409..0000000
--- a/internal/game/cmd_construct.go
+++ /dev/null
@@ -1,157 +0,0 @@
-package game
-
-import (
- "strings"
-
- "thehouseoficarus/internal/action"
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
- "thehouseoficarus/internal/world"
-)
-
-func (g *Game) doConstruct(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
-
- allRecipes, err := g.RecipeStore.LoadAll()
- if err != nil {
- sess.WriteLine("Error loading recipes.")
- return
- }
-
- var constructRecipes []action.RecipeDef
- for _, r := range allRecipes {
- if r.Type != "construction" {
- continue
- }
- if !g.constructRecipeVisible(p, &r) {
- continue
- }
- constructRecipes = append(constructRecipes, r)
- }
-
- if input != "" {
- g.doConstructWithInput(sess, p, constructRecipes, input)
- return
- }
-
- lastRecipeID, _ := p.Flags["last_construct"].(string)
- if lastRecipeID != "" {
- for i := range constructRecipes {
- if constructRecipes[i].ID == lastRecipeID {
- r := &constructRecipes[i]
- if g.canDoConstructRecipe(p, r) {
- g.startConstructRecipe(sess, p, r, 0)
- return
- }
- break
- }
- }
- }
-
- available := g.availableConstructRecipes(p, constructRecipes)
- if len(available) == 0 {
- sess.WriteLine("You don't have any materials to construct.")
- return
- }
-
- if p.OptionBool("construct_all") && len(available) == 1 {
- g.startConstructRecipe(sess, p, &available[0], 0)
- return
- }
-
- g.showProductionTable(sess, p, available, "Construction", "construction", "last_construct", "Construct", false)
-}
-
-func (g *Game) doConstructWithInput(sess *net.Session, p *player.Player, constructRecipes []action.RecipeDef, input string) {
- qty, productName := parseQty(input)
- productName = strings.TrimSpace(productName)
-
- var matched []action.RecipeDef
- for _, r := range constructRecipes {
- outDef, _ := g.ItemStore.Load(r.Output)
- name := r.Output
- if outDef != nil {
- name = outDef.Name
- }
- if world.WordPrefixMatch(productName, name) {
- matched = append(matched, r)
- }
- }
-
- if len(matched) == 0 {
- sess.WriteLine("You can't construct that.")
- return
- }
-
- var available []action.RecipeDef
- for _, r := range matched {
- if g.canDoConstructRecipe(p, &r) {
- available = append(available, r)
- }
- }
-
- if len(available) == 0 {
- sess.WriteLine("You don't have the materials or level for that.")
- return
- }
-
- if len(available) == 1 {
- g.startConstructRecipe(sess, p, &available[0], qty)
- return
- }
-
- g.showProductionTable(sess, p, available, "Construction", "construction", "last_construct", "Construct", false)
-}
-
-func (g *Game) constructRecipeVisible(p *player.Player, r *action.RecipeDef) bool {
- if len(r.Station) > 0 {
- stID, _ := g.findStation(p.RoomID, r.Station)
- if stID == "" {
- return false
- }
- }
- if r.Tool != "" && !g.hasToolType(p, r.Tool) {
- return false
- }
- return true
-}
-
-func (g *Game) canDoConstructRecipe(p *player.Player, r *action.RecipeDef) bool {
- if p.Level(player.Construction) < r.Level {
- return false
- }
- if !r.HasAllItemsQty(p.CountItem) {
- return false
- }
- if len(r.Station) > 0 {
- stID, _ := g.findStation(p.RoomID, r.Station)
- if stID == "" {
- return false
- }
- }
- if r.Tool != "" && !g.hasToolType(p, r.Tool) {
- return false
- }
- return true
-}
-
-func (g *Game) availableConstructRecipes(p *player.Player, allConstruct []action.RecipeDef) []action.RecipeDef {
- var result []action.RecipeDef
- for _, r := range allConstruct {
- if r.HasAllItemsQty(p.CountItem) {
- result = append(result, r)
- }
- }
- return result
-}
-
-func (g *Game) startConstructRecipe(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) {
- if p.Flags == nil {
- p.Flags = make(map[string]any)
- }
- p.Flags["last_construct"] = recipe.ID
- g.AccountStore.SaveCharacter(p)
-
- g.startProductionFromRecipe(sess, p, recipe, count)
-}
diff --git a/internal/game/cmd_cook.go b/internal/game/cmd_cook.go
index 9b76a84..5b78745 100644
--- a/internal/game/cmd_cook.go
+++ b/internal/game/cmd_cook.go
@@ -9,8 +9,8 @@ import (
)
func (g *Game) doCook(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
stationDefID, stationName := g.findStation(p.RoomID, []string{"fire", "cooking_range"})
if stationDefID == "" {
@@ -66,7 +66,7 @@ func (g *Game) doCook(sess *net.Session, input string) {
return
}
- sess.PendingMenu = recipeMenuData(recipes)
+ sess.PendingMenu = recipeMenuData(recipeDefIDs(recipes))
sess.State = net.StateRecipeChoice
var names []string
for _, r := range recipes {
@@ -116,7 +116,11 @@ func (g *Game) showCookMenu(sess *net.Session, p *player.Player, allRecipes []ac
names = append(names, e.ItemName)
}
g.showMenuTable(sess, fmt.Sprintf("What would you like to cook on the %s?", stationName), names)
- sess.PendingMenu = entryMenuData(entries)
+ ids := make([]string, len(entries))
+ for i, e := range entries {
+ ids[i] = e.Recipe.ID
+ }
+ sess.PendingMenu = recipeMenuData(ids)
}
func (g *Game) recipeName(sess *net.Session, r *action.RecipeDef) string {
diff --git a/internal/game/cmd_craft.go b/internal/game/cmd_craft.go
index 50a99e0..7f8808b 100644
--- a/internal/game/cmd_craft.go
+++ b/internal/game/cmd_craft.go
@@ -6,12 +6,19 @@ import (
"thehouseoficarus/internal/action"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
- "thehouseoficarus/internal/world"
)
func (g *Game) doCraft(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ g.doGenericStationSkill(sess, input, "crafting", player.Crafting, "last_craft", "Crafting", "craft", "Craft")
+}
+
+func (g *Game) doConstruct(sess *net.Session, input string) {
+ g.doGenericStationSkill(sess, input, "construction", player.Construction, "last_construct", "Construction", "construct", "Construct")
+}
+
+func (g *Game) doGenericStationSkill(sess *net.Session, input string, recipeType string, skill player.SkillName, flagKey, label, verbPast, verbCap string) {
+ p := sess.Player
+ g.cancelAction(p)
allRecipes, err := g.RecipeStore.LoadAll()
if err != nil {
@@ -19,29 +26,29 @@ func (g *Game) doCraft(sess *net.Session, input string) {
return
}
- var craftRecipes []action.RecipeDef
+ var recipes []action.RecipeDef
for _, r := range allRecipes {
- if r.Type != "crafting" {
+ if r.Type != recipeType {
continue
}
- if !g.craftRecipeVisible(p, &r) {
+ if !g.genericRecipeVisible(p, &r) {
continue
}
- craftRecipes = append(craftRecipes, r)
+ recipes = append(recipes, r)
}
if input != "" {
- g.doCraftWithInput(sess, p, craftRecipes, input)
+ g.doGenericWithInput(sess, p, recipes, input, skill, flagKey, label, verbCap)
return
}
- lastRecipeID, _ := p.Flags["last_craft"].(string)
+ lastRecipeID, _ := p.Flags[flagKey].(string)
if lastRecipeID != "" {
- for i := range craftRecipes {
- if craftRecipes[i].ID == lastRecipeID {
- r := &craftRecipes[i]
- if g.canDoCraftRecipe(p, r) {
- g.startCraftRecipe(sess, p, r, 0)
+ for i := range recipes {
+ if recipes[i].ID == lastRecipeID {
+ r := &recipes[i]
+ if g.canDoGenericRecipe(p, r, skill) {
+ g.startGenericRecipe(sess, p, r, 0, flagKey)
return
}
break
@@ -49,44 +56,45 @@ func (g *Game) doCraft(sess *net.Session, input string) {
}
}
- available := g.availableCraftRecipes(p, craftRecipes)
+ available := g.availableGenericRecipes(p, recipes)
if len(available) == 0 {
- sess.WriteLine("You don't have any materials to craft.")
+ sess.WriteLine("You don't have any materials to " + verbPast + ".")
return
}
- if p.OptionBool("craft_all") && len(available) == 1 {
- g.startCraftRecipe(sess, p, &available[0], 0)
+ optionKey := verbPast + "_all"
+ if p.OptionBool(optionKey) && len(available) == 1 {
+ g.startGenericRecipe(sess, p, &available[0], 0, flagKey)
return
}
- g.showProductionTable(sess, p, available, "Crafting", "crafting", "last_craft", "Craft", false)
+ g.showProductionTable(sess, p, available, label, recipeType, flagKey, verbCap, false)
}
-func (g *Game) doCraftWithInput(sess *net.Session, p *player.Player, craftRecipes []action.RecipeDef, input string) {
+func (g *Game) doGenericWithInput(sess *net.Session, p *player.Player, recipes []action.RecipeDef, input string, skill player.SkillName, flagKey, label, verbCap string) {
qty, productName := parseQty(input)
productName = strings.TrimSpace(productName)
var matched []action.RecipeDef
- for _, r := range craftRecipes {
+ for _, r := range recipes {
outDef, _ := g.ItemStore.Load(r.Output)
name := r.Output
if outDef != nil {
name = outDef.Name
}
- if world.WordPrefixMatch(productName, name) {
+ if action.WordPrefixMatch(productName, name) {
matched = append(matched, r)
}
}
if len(matched) == 0 {
- sess.WriteLine("You can't craft that.")
+ sess.WriteLine("You can't " + strings.ToLower(verbCap) + " that.")
return
}
var available []action.RecipeDef
for _, r := range matched {
- if g.canDoCraftRecipe(p, &r) {
+ if g.canDoGenericRecipe(p, &r, skill) {
available = append(available, r)
}
}
@@ -97,14 +105,14 @@ func (g *Game) doCraftWithInput(sess *net.Session, p *player.Player, craftRecipe
}
if len(available) == 1 {
- g.startCraftRecipe(sess, p, &available[0], qty)
+ g.startGenericRecipe(sess, p, &available[0], qty, flagKey)
return
}
- g.showProductionTable(sess, p, available, "Crafting", "crafting", "last_craft", "Craft", false)
+ g.showProductionTable(sess, p, available, label, label, flagKey, verbCap, false)
}
-func (g *Game) craftRecipeVisible(p *player.Player, r *action.RecipeDef) bool {
+func (g *Game) genericRecipeVisible(p *player.Player, r *action.RecipeDef) bool {
if len(r.Station) > 0 {
stID, _ := g.findStation(p.RoomID, r.Station)
if stID == "" {
@@ -117,8 +125,8 @@ func (g *Game) craftRecipeVisible(p *player.Player, r *action.RecipeDef) bool {
return true
}
-func (g *Game) canDoCraftRecipe(p *player.Player, r *action.RecipeDef) bool {
- if p.Level(player.Crafting) < r.Level {
+func (g *Game) canDoGenericRecipe(p *player.Player, r *action.RecipeDef, skill player.SkillName) bool {
+ if p.Level(skill) < r.Level {
return false
}
if !r.HasAllItemsQty(p.CountItem) {
@@ -136,9 +144,9 @@ func (g *Game) canDoCraftRecipe(p *player.Player, r *action.RecipeDef) bool {
return true
}
-func (g *Game) availableCraftRecipes(p *player.Player, allCraft []action.RecipeDef) []action.RecipeDef {
+func (g *Game) availableGenericRecipes(p *player.Player, allRecipes []action.RecipeDef) []action.RecipeDef {
var result []action.RecipeDef
- for _, r := range allCraft {
+ for _, r := range allRecipes {
if r.HasAllItemsQty(p.CountItem) {
result = append(result, r)
}
@@ -146,11 +154,9 @@ func (g *Game) availableCraftRecipes(p *player.Player, allCraft []action.RecipeD
return result
}
-func (g *Game) startCraftRecipe(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) {
- if p.Flags == nil {
- p.Flags = make(map[string]any)
- }
- p.Flags["last_craft"] = recipe.ID
+func (g *Game) startGenericRecipe(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int, flagKey string) {
+ p.EnsureFlags()
+ p.Flags[flagKey] = recipe.ID
g.AccountStore.SaveCharacter(p)
g.startProductionFromRecipe(sess, p, recipe, count)
diff --git a/internal/game/cmd_description.go b/internal/game/cmd_description.go
index 20ada37..ced5dcc 100644
--- a/internal/game/cmd_description.go
+++ b/internal/game/cmd_description.go
@@ -4,11 +4,10 @@ import (
"fmt"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doDescription(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
sess.WriteLine("")
if p.Description != "" {
sess.WriteLine(fmt.Sprintf("Current description: %s", p.Description))
@@ -22,7 +21,7 @@ func (g *Game) doDescription(sess *net.Session) {
func (g *Game) handleDescriptionChange(sess *net.Session, input string) {
if input != "" {
- p := sess.Player.(*player.Player)
+ p := sess.Player
p.Description = input
g.AccountStore.SaveCharacter(p)
sess.WriteLine(fmt.Sprintf("Description set to: %s", input))
diff --git a/internal/game/cmd_drink.go b/internal/game/cmd_drink.go
index c7f9b34..44ef991 100644
--- a/internal/game/cmd_drink.go
+++ b/internal/game/cmd_drink.go
@@ -12,7 +12,7 @@ import (
)
func (g *Game) doDrink(sess *net.Session, args []string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if len(args) == 0 {
sess.WriteLine("Drink what?")
@@ -57,7 +57,7 @@ func (g *Game) doDrink(sess *net.Session, args []string) {
return
}
- g.CancelAction(p)
+ g.cancelAction(p)
g.consumeQueue[p.Name] = &QueuedCommand{
Session: sess,
diff --git a/internal/game/cmd_drop.go b/internal/game/cmd_drop.go
index 1fd99b4..d3141e3 100644
--- a/internal/game/cmd_drop.go
+++ b/internal/game/cmd_drop.go
@@ -5,12 +5,11 @@ import (
"strings"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doDropAll(sess *net.Session) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
p.ActionState = &ActionState{Type: ActionDropping, TargetName: "inventory"}
count := 0
@@ -28,8 +27,8 @@ func (g *Game) doDropAll(sess *net.Session) {
}
func (g *Game) doDropAllNamed(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
matches := g.findInventoryMatches(input, p)
if len(matches) == 0 {
@@ -86,8 +85,8 @@ 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)
+ p := sess.Player
+ g.cancelAction(p)
qty, itemName := parseQty(input)
@@ -182,14 +181,14 @@ func (g *Game) doDrop(sess *net.Session, input string) {
}
func (g *Game) handleDropAllConfirm(sess *net.Session, input string) {
- p, ok := sess.Player.(*player.Player)
- if !ok {
+ p := sess.Player
+ if p == nil {
sess.State = net.StateGame
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
diff --git a/internal/game/cmd_eat.go b/internal/game/cmd_eat.go
index e6a0386..f94ade3 100644
--- a/internal/game/cmd_eat.go
+++ b/internal/game/cmd_eat.go
@@ -10,7 +10,7 @@ import (
)
func (g *Game) doEat(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if input == "" {
sess.WriteLine("Eat what?")
@@ -52,7 +52,7 @@ func (g *Game) doEat(sess *net.Session, input string) {
return
}
- g.CancelAction(p)
+ g.cancelAction(p)
g.consumeQueue[p.Name] = &QueuedCommand{
Session: sess,
diff --git a/internal/game/cmd_equipment.go b/internal/game/cmd_equipment.go
index b256bc3..f5a9a11 100644
--- a/internal/game/cmd_equipment.go
+++ b/internal/game/cmd_equipment.go
@@ -5,11 +5,10 @@ import (
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/object"
- "thehouseoficarus/internal/player"
)
func (g *Game) doEquipment(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
sess.WriteLine("")
sess.WriteLine("Equipment:")
diff --git a/internal/game/cmd_estate_directory.go b/internal/game/cmd_estate_directory.go
index 6c66ca4..c79135f 100644
--- a/internal/game/cmd_estate_directory.go
+++ b/internal/game/cmd_estate_directory.go
@@ -15,12 +15,7 @@ import (
)
func (g *Game) startEstateDirectory(sess *net.Session, verb string) {
- p := sess.Player.(*player.Player)
-
- if verb == "toggle" {
- sess.WriteLine("You can't do that with the directory.")
- return
- }
+ p := sess.Player
homeowners := g.scanHomeowners("owns_house_local_neighborhood")
ownedByMe := false
@@ -40,7 +35,7 @@ func (g *Game) startEstateDirectory(sess *net.Session, verb string) {
if ownedByMe {
houseRoom := g.getOwnedHouseRoom(p, "owns_house_local_neighborhood")
if houseRoom > 0 {
- g.CancelAction(p)
+ g.cancelAction(p)
g.teleportPlayer(sess, p, houseRoom)
return
}
@@ -82,7 +77,7 @@ func (g *Game) showEstateDirectoryTalk(sess *net.Session, p *player.Player, home
Type: "talk",
TargetID: "estate_directory",
TargetName: "Estate Directory",
- Data: map[string]any{"node": "start", "behavior_id": "estate_directory_talk"},
+ Data: map[string]any{"node": "start", "estate_directory": true},
}
sess.Write("\nChoice: ")
return
@@ -101,8 +96,8 @@ func (g *Game) handleEstateDirectoryTalk(sess *net.Session, input string) {
switch input {
case "enter":
- p, ok := sess.Player.(*player.Player)
- if !ok {
+ p := sess.Player
+ if p == nil {
return
}
houseRoom := g.getOwnedHouseRoom(p, "owns_house_local_neighborhood")
@@ -118,8 +113,8 @@ func (g *Game) handleEstateDirectoryTalk(sess *net.Session, input string) {
p.ActionState = nil
g.writePrompt(sess)
case "leave", "bye", "exit", "quit":
- p, ok := sess.Player.(*player.Player)
- if ok {
+ p := sess.Player
+ if p != nil {
p.Action = nil
p.ActionState = nil
}
@@ -134,7 +129,7 @@ func (g *Game) handleEstateDirectoryTalk(sess *net.Session, input string) {
func (g *Game) scanHomeowners(flagKey string) []string {
var homeowners []string
- charsDir := filepath.Join(g.dataDir, "players", "characters")
+ charsDir := filepath.Join(g.DataDir, "players", "characters")
entries, err := os.ReadDir(charsDir)
if err != nil {
return homeowners
@@ -179,9 +174,7 @@ func (g *Game) scanHomeowners(flagKey string) []string {
}
func (g *Game) getOwnedHouseRoom(p *player.Player, flagKey string) int {
- if p.Flags == nil {
- return 0
- }
+ p.EnsureFlags()
val, ok := p.Flags[flagKey]
if !ok || val == nil {
return 0
@@ -218,6 +211,6 @@ func (g *Game) teleportPlayer(sess *net.Session, p *player.Player, roomID int) {
g.seedRoomObjects(p.RoomID)
g.AccountStore.SaveCharacter(p)
g.doLook(sess)
- g.RunEnterSteps(sess, p.RoomID)
+ g.runEnterSteps(sess, p.RoomID)
g.writePrompt(sess)
}
diff --git a/internal/game/cmd_farm.go b/internal/game/cmd_farm.go
index 0a374f0..5ef36d9 100644
--- a/internal/game/cmd_farm.go
+++ b/internal/game/cmd_farm.go
@@ -10,7 +10,7 @@ import (
)
func (g *Game) doPlant(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if input == "" {
sess.WriteLine("Plant what?")
@@ -93,7 +93,7 @@ func (g *Game) doPlant(sess *net.Session, input string) {
}
func (g *Game) doHarvest(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
prefix, _, _ := g.findFarmPatch(sess, p, input, func(flags map[string]any, prefix string) bool {
ready, _ := flags[prefix+"_ready"].(bool)
@@ -179,7 +179,7 @@ func (g *Game) doHarvest(sess *net.Session, input string) {
}
func (g *Game) doRake(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if !g.hasToolType(p, "rake") {
sess.WriteLine("You need a rake to do that.")
@@ -225,7 +225,7 @@ func (g *Game) doRake(sess *net.Session, input string) {
}
func (g *Game) doWater(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if !g.hasToolType(p, "watering_can") {
sess.WriteLine("You need a watering can to do that.")
@@ -285,7 +285,7 @@ func (g *Game) doWater(sess *net.Session, input string) {
}
func (g *Game) doCure(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if !p.HasItem("plant_cure") {
sess.WriteLine("You don't have any plant cure.")
@@ -329,7 +329,7 @@ func (g *Game) doCure(sess *net.Session, input string) {
}
func (g *Game) doInspect(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
objInstances := g.World.AllObjInstances(p.RoomID)
var farmPatches []struct {
@@ -437,7 +437,7 @@ func (g *Game) doInspect(sess *net.Session, input string) {
continue
}
- stage := intFlag(p.Flags, fp.prefix+"_stage")
+ stage := intFromFlag(p.Flags, fp.prefix+"_stage")
maxStages := 4
if def, err := g.ItemStore.Load(seedID); err == nil && def.FarmStages > 0 {
maxStages = def.FarmStages
diff --git a/internal/game/cmd_fletch.go b/internal/game/cmd_fletch.go
index 60acb92..1f6a075 100644
--- a/internal/game/cmd_fletch.go
+++ b/internal/game/cmd_fletch.go
@@ -7,7 +7,6 @@ import (
"thehouseoficarus/internal/combat"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
- "thehouseoficarus/internal/world"
)
var fletchLogItems = map[string]bool{
@@ -51,7 +50,7 @@ func fletchNeedsChisel(r *action.RecipeDef) bool {
}
func (g *Game) doFletch(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if combat.GetCombat(p.Name) != nil {
sess.WriteLine("You can't do that during combat!")
@@ -121,7 +120,7 @@ func (g *Game) doFletchWithInput(sess *net.Session, p *player.Player, fletchReci
if outDef != nil {
name = outDef.Name
}
- if world.WordPrefixMatch(productName, name) {
+ if action.WordPrefixMatch(productName, name) {
matched = append(matched, r)
}
}
diff --git a/internal/game/cmd_get.go b/internal/game/cmd_get.go
index 6f08892..bae721b 100644
--- a/internal/game/cmd_get.go
+++ b/internal/game/cmd_get.go
@@ -8,8 +8,8 @@ import (
)
func (g *Game) doGet(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
ground := g.World.GroundItems(p.RoomID)
if len(ground) == 0 {
sess.WriteLine("There's nothing on the ground to pick up.")
@@ -129,8 +129,8 @@ func (g *Game) doGet(sess *net.Session, input string) {
}
func (g *Game) doGetAllNamed(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
ground := g.World.GroundItems(p.RoomID)
if len(ground) == 0 {
sess.WriteLine("There's nothing on the ground to pick up.")
@@ -251,8 +251,8 @@ func (g *Game) doGetAllNamed(sess *net.Session, input string) {
}
func (g *Game) doGetAll(sess *net.Session) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
ground := g.World.GroundItems(p.RoomID)
if len(ground) == 0 {
sess.WriteLine("There's nothing on the ground to pick up.")
diff --git a/internal/game/cmd_handlers.go b/internal/game/cmd_handlers.go
new file mode 100644
index 0000000..b8db359
--- /dev/null
+++ b/internal/game/cmd_handlers.go
@@ -0,0 +1,370 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/net"
+)
+
+func (g *Game) executeGet(sess *net.Session, args []string, rawInput string) {
+ if len(args) == 0 {
+ sess.WriteLine("Get what?")
+ } else if args[0] == "all" {
+ if len(args) == 1 {
+ g.doGetAll(sess)
+ } else {
+ g.doGetAllNamed(sess, strings.Join(args[1:], " "))
+ }
+ } else {
+ g.doGet(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeDrop(sess *net.Session, args []string, rawInput string) {
+ if len(args) == 0 {
+ sess.WriteLine("Drop what?")
+ } else if args[0] == "all" {
+ if len(args) == 1 {
+ g.doDropAll(sess)
+ } else {
+ g.doDropAllNamed(sess, strings.Join(args[1:], " "))
+ }
+ } else {
+ g.doDrop(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeAttack(sess *net.Session, args []string, rawInput string) {
+ if len(args) == 0 {
+ p := sess.Player
+ target := g.resolveDefaultMob(p.RoomID)
+ if target == "" {
+ sess.WriteLine("Attack what?")
+ return
+ }
+ g.doAttack(sess, target)
+ } else {
+ g.doAttack(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeStyle(sess *net.Session, args []string, rawInput string) {
+ if len(args) == 0 {
+ g.doStyle(sess, "")
+ } else {
+ g.doStyle(sess, args[0])
+ }
+}
+
+func (g *Game) executeLook(sess *net.Session, args []string, rawInput string) {
+ if len(args) == 0 {
+ g.doLook(sess)
+ } else {
+ g.doLookTarget(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeMove(sess *net.Session, args []string, rawInput string) {
+ g.doMove(sess, strings.TrimSpace(rawInput), 1.0)
+}
+
+func (g *Game) executeSay(sess *net.Session, args []string, rawInput string) {
+ if len(args) == 0 {
+ sess.WriteLine("Say what?")
+ } else {
+ msgStart := strings.Index(strings.ToLower(rawInput), "say ") + 4
+ if msgStart >= 4 && msgStart < len(rawInput) {
+ g.doSay(sess, rawInput[msgStart:])
+ } else {
+ g.doSay(sess, strings.Join(args, " "))
+ }
+ }
+}
+
+func (g *Game) executeScore(sess *net.Session, args []string, rawInput string) {
+ g.doScore(sess)
+}
+
+func (g *Game) executeTask(sess *net.Session, args []string, rawInput string) {
+ g.doTask(sess)
+}
+
+func (g *Game) executeStats(sess *net.Session, args []string, rawInput string) {
+ g.doStats(sess)
+}
+
+func (g *Game) executeTech(sess *net.Session, args []string, rawInput string) {
+ g.doTech(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeInventory(sess *net.Session, args []string, rawInput string) {
+ g.doInventory(sess)
+}
+
+func (g *Game) executeQuit(sess *net.Session, args []string, rawInput string) {
+ g.doQuit(sess)
+}
+
+func (g *Game) executeDescription(sess *net.Session, args []string, rawInput string) {
+ g.doDescription(sess)
+}
+
+func (g *Game) executeOption(sess *net.Session, args []string, rawInput string) {
+ g.doOption(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeColor(sess *net.Session, args []string, rawInput string) {
+ g.doColor(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeColortable(sess *net.Session, args []string, rawInput string) {
+ g.doColortable(sess)
+}
+
+func (g *Game) executePrompt(sess *net.Session, args []string, rawInput string) {
+ if len(args) == 0 {
+ g.doPrompt(sess, "")
+ } else {
+ msgStart := strings.Index(strings.ToLower(rawInput), "prompt ") + 7
+ if msgStart >= 7 && msgStart < len(rawInput) {
+ g.doPrompt(sess, rawInput[msgStart:])
+ } else {
+ g.doPrompt(sess, strings.Join(args, " "))
+ }
+ }
+}
+
+func (g *Game) executeExits(sess *net.Session, args []string, rawInput string) {
+ g.doExits(sess)
+}
+
+func (g *Game) executeMap(sess *net.Session, args []string, rawInput string) {
+ g.doMap(sess)
+}
+
+func (g *Game) executeQueued(sess *net.Session, args []string, rawInput string) {
+ g.doQueued(sess)
+}
+
+func (g *Game) executeHelp(sess *net.Session, args []string, rawInput string) {
+ if len(args) == 0 {
+ g.doHelp(sess, "")
+ } else {
+ g.doHelp(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeGather(sess *net.Session, args []string, rawInput string) {
+ verb := strings.TrimSpace(rawInput)
+ p := sess.Player
+ g.cancelAction(p)
+ if len(args) == 0 {
+ target := g.resolveDefaultTarget(p.RoomID, verb)
+ if target == "" {
+ sess.WriteLine(fmt.Sprintf("%s what?", verb))
+ return
+ }
+ g.startAction(sess, verb, target)
+ } else {
+ g.startAction(sess, verb, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeUse(sess *net.Session, args []string, rawInput string) {
+ if g.tryFinishingBlow(sess, strings.Join(args, " ")) {
+ return
+ }
+ g.doUse(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeCook(sess *net.Session, args []string, rawInput string) {
+ g.doCook(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeSmelt(sess *net.Session, args []string, rawInput string) {
+ g.doSmelt(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeSmith(sess *net.Session, args []string, rawInput string) {
+ g.doSmith(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeCraft(sess *net.Session, args []string, rawInput string) {
+ g.doCraft(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeEat(sess *net.Session, args []string, rawInput string) {
+ g.doEat(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeDrink(sess *net.Session, args []string, rawInput string) {
+ g.doDrink(sess, args)
+}
+
+func (g *Game) executeFletch(sess *net.Session, args []string, rawInput string) {
+ g.doFletch(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeClean(sess *net.Session, args []string, rawInput string) {
+ g.doClean(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeMix(sess *net.Session, args []string, rawInput string) {
+ g.doMix(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeIdentify(sess *net.Session, args []string, rawInput string) {
+ g.doIdentify(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeAutotrigger(sess *net.Session, args []string, rawInput string) {
+ g.doAutotrigger(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeMods(sess *net.Session, args []string, rawInput string) {
+ showAll := len(args) > 0 && args[0] == "all"
+ g.doMods(sess, showAll)
+}
+
+func (g *Game) executeSneak(sess *net.Session, args []string, rawInput string) {
+ g.doSneak(sess)
+}
+
+func (g *Game) executeTrigger(sess *net.Session, args []string, rawInput string) {
+ g.doTrigger(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeTalk(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ g.cancelAction(p)
+ if len(args) == 0 {
+ sess.WriteLine("Talk to whom?")
+ } else {
+ g.startAction(sess, "talk", strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeBurn(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ g.cancelAction(p)
+ g.doBurn(sess, p, strings.Join(args, " "))
+}
+
+func (g *Game) executeStoke(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ g.cancelAction(p)
+ g.doStoke(sess, p, strings.Join(args, " "))
+}
+
+func (g *Game) executeAlias(sess *net.Session, args []string, rawInput string) {
+ g.doAlias(sess, args)
+}
+
+func (g *Game) executeUnalias(sess *net.Session, args []string, rawInput string) {
+ g.doUnalias(sess, args)
+}
+
+func (g *Game) executeSearch(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ g.cancelAction(p)
+ if len(args) == 0 {
+ sess.WriteLine("Search what?")
+ } else {
+ g.doSearch(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeSteal(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ g.cancelAction(p)
+ if len(args) == 0 {
+ g.doSteal(sess, "")
+ } else {
+ g.doSteal(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executePlant(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ g.cancelAction(p)
+ if len(args) == 0 {
+ sess.WriteLine("Plant what?")
+ } else {
+ g.doPlant(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeHarvest(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ g.cancelAction(p)
+ if len(args) == 0 {
+ g.doHarvest(sess, "")
+ } else {
+ g.doHarvest(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeRake(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ g.cancelAction(p)
+ if len(args) == 0 {
+ g.doRake(sess, "")
+ } else {
+ g.doRake(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeWater(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ g.cancelAction(p)
+ if len(args) == 0 {
+ g.doWater(sess, "")
+ } else {
+ g.doWater(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeCure(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ g.cancelAction(p)
+ if len(args) == 0 {
+ g.doCure(sess, "")
+ } else {
+ g.doCure(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeInspect(sess *net.Session, args []string, rawInput string) {
+ if len(args) == 0 {
+ g.doInspect(sess, "")
+ } else {
+ g.doInspect(sess, strings.Join(args, " "))
+ }
+}
+
+func (g *Game) executeWalk(sess *net.Session, args []string, rawInput string) {
+ g.doWalk(sess, args)
+}
+
+func (g *Game) executeBank(sess *net.Session, args []string, rawInput string) {
+ g.doBank(sess)
+}
+
+func (g *Game) executeConstruct(sess *net.Session, args []string, rawInput string) {
+ g.doConstruct(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeJack(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+ g.cancelAction(p)
+ g.doJack(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeWear(sess *net.Session, args []string, rawInput string) {
+ g.doWear(sess, strings.Join(args, " "))
+}
+
+func (g *Game) executeRemove(sess *net.Session, args []string, rawInput string) {
+ g.doRemove(sess, strings.Join(args, " "))
+}
diff --git a/internal/game/cmd_id.go b/internal/game/cmd_id.go
index 5ea146d..f7158e9 100644
--- a/internal/game/cmd_id.go
+++ b/internal/game/cmd_id.go
@@ -82,8 +82,8 @@ func junkMultiplier(junkID string, level int) int {
}
func (g *Game) doIdentify(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
var found altarConfig
foundAny := false
diff --git a/internal/game/cmd_inventory.go b/internal/game/cmd_inventory.go
index 8684fcd..ae7b328 100644
--- a/internal/game/cmd_inventory.go
+++ b/internal/game/cmd_inventory.go
@@ -4,11 +4,10 @@ import (
"fmt"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doInventory(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
sess.WriteLine("")
sess.WriteLine(fmt.Sprintf("Inventory (%d free):", p.FreeSlots()))
diff --git a/internal/game/cmd_jack.go b/internal/game/cmd_jack.go
index db157f3..8cb5062 100644
--- a/internal/game/cmd_jack.go
+++ b/internal/game/cmd_jack.go
@@ -5,12 +5,13 @@ import (
"strings"
"thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/game/hacking"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
)
func (g *Game) doJack(sess *net.Session, target string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if combat.GetCombat(p.Name) != nil {
sess.WriteLine("You can't do that during combat!")
@@ -32,7 +33,7 @@ func (g *Game) doJack(sess *net.Session, target string) {
}
defID := instances[0].DefID
- tDef, ok := terminalDefs[defID]
+ tDef, ok := hacking.Terminals[defID]
if !ok {
sess.WriteLine("You can't jack into that.")
return
@@ -44,13 +45,13 @@ func (g *Game) doJack(sess *net.Session, target string) {
return
}
- g.CancelAction(p)
- g.CancelBackgroundAction(p)
+ g.cancelAction(p)
+ g.cancelBackgroundAction(p)
minigame := tDef.Minigame()
display := minigame.Init(hackLevel)
- g.hackingStates[p.Name] = &HackingSession{
+ g.hackingStates[p.Name] = &hacking.Session{
Minigame: minigame,
TerminalID: defID,
Level: hackLevel,
diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go
index 6c83130..5fa2cf4 100644
--- a/internal/game/cmd_look.go
+++ b/internal/game/cmd_look.go
@@ -15,18 +15,30 @@ import (
)
func (g *Game) doLook(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
room, err := g.World.LoadRoom(p.RoomID)
if err != nil {
sess.WriteLine("You are in a void.")
return
}
+ g.showRoomName(sess, p, room)
+ g.showRoomDescription(sess, p, room)
+ g.showRoomMobs(sess, p, room)
+ g.showRoomObjects(sess, p, room)
+ g.showGroundItems(sess, p, room)
+ g.showRoomExits(sess, p, room)
+ g.showRoomPlayers(sess, p, room)
+}
+
+func (g *Game) showRoomName(sess *net.Session, p *player.Player, room *world.Room) {
sess.WriteLines(
"",
fmt.Sprintf("%s (%s)", g.colorize(sess, "room_name", room.Name), g.colorize(sess, "room_number", fmt.Sprintf("#%d", room.ID))),
)
+}
+func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *world.Room) {
descWidth := p.OptionInt("room_desc_width")
if descWidth <= 0 {
descWidth = 70
@@ -51,8 +63,10 @@ func (g *Game) doLook(sess *net.Session) {
sess.WriteLine(line)
}
}
+}
- mobs := g.MobStore.MobsInRoom(p.RoomID)
+func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Room) {
+ mobs := g.MobStore.MobsInRoom(p.RoomID)
if len(mobs) > 0 {
sort.Slice(mobs, func(i, j int) bool {
iDamaged := mobs[i].HP < mobs[i].MaxHP
@@ -93,7 +107,9 @@ func (g *Game) doLook(sess *net.Session) {
sess.WriteLine(fmt.Sprintf(" %s %s%s%s", g.colorize(sess, npcColor, displayName), levelStr, hp, desc))
}
}
+}
+func (g *Game) showRoomObjects(sess *net.Session, p *player.Player, room *world.Room) {
objs := g.World.AllObjInstances(p.RoomID)
if len(objs) > 0 {
sess.WriteLine("")
@@ -155,6 +171,9 @@ func (g *Game) doLook(sess *net.Session) {
}
roomDesc := def.InRoomDescription
+ if roomDesc != "" {
+ roomDesc = color.ExpandTags(g.colorMode(sess), roomDesc)
+ }
coloredName := g.objColorize(sess, def, def.Name)
coloredPlural := g.objColorize(sess, def, def.Name+"s")
@@ -217,7 +236,9 @@ func (g *Game) doLook(sess *net.Session) {
}
g.showFarmPatches(sess, p)
+}
+func (g *Game) showGroundItems(sess *net.Session, p *player.Player, room *world.Room) {
ground := g.World.GroundItemsDetailed(p.RoomID)
if len(ground) > 0 {
showDespawn := p.OptionBool("despawn")
@@ -334,13 +355,15 @@ func (g *Game) doLook(sess *net.Session) {
}
}
}
+}
+func (g *Game) showRoomExits(sess *net.Session, p *player.Player, room *world.Room) {
if len(room.Exits) > 0 {
sess.WriteLine("")
- if p.OptionBool("exits") {
+ if p.OptionBool("exits") {
sess.WriteLine("Exits:")
type exitLine struct {
- dir string
+ dir string
targetName string
}
var lines []exitLine
@@ -383,11 +406,13 @@ func (g *Game) doLook(sess *net.Session) {
sess.WriteLine("")
}
}
+}
+func (g *Game) showRoomPlayers(sess *net.Session, p *player.Player, room *world.Room) {
others := g.Hub.PlayersInRoom(p.RoomID)
for _, other := range others {
if other != sess && other.Player != nil {
- op := other.Player.(*player.Player)
+ op := other.Player
line := fmt.Sprintf("\n%s is here", g.colorize(sess, "player_name", op.Name))
if cs := combat.GetCombat(op.Name); cs != nil {
if mob := g.MobStore.GetInstance(cs.MobID); mob != nil && mob.HP > 0 {
@@ -408,7 +433,7 @@ func (g *Game) doLook(sess *net.Session) {
}
func (g *Game) doLookTarget(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
lower := strings.ToLower(input)
if exitDir := g.World.ResolveExit(lower); exitDir != "" {
@@ -572,7 +597,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
if other == sess || other.Player == nil {
continue
}
- op := other.Player.(*player.Player)
+ op := other.Player
if strings.ToLower(op.Name) != lower {
continue
}
@@ -584,7 +609,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
}
func showPlayerInfo(g *Game, sess *net.Session, p *player.Player) {
- myP := sess.Player.(*player.Player)
+ myP := sess.Player
theirLevel := p.CombatLevel()
levelStr := g.levelColorize(sess, myP.CombatLevel(), theirLevel, fmt.Sprint(theirLevel))
sess.WriteLines(
@@ -623,7 +648,7 @@ func showPlayerInfo(g *Game, sess *net.Session, p *player.Player) {
}
func (g *Game) doExits(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
room, err := g.World.LoadRoom(p.RoomID)
if err != nil || len(room.Exits) == 0 {
sess.WriteLine("There are no exits here.")
diff --git a/internal/game/cmd_map.go b/internal/game/cmd_map.go
index 1c78be3..7cad008 100644
--- a/internal/game/cmd_map.go
+++ b/internal/game/cmd_map.go
@@ -2,11 +2,10 @@ package game
import (
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doMap(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
mapWidth := p.OptionInt("mapwidth")
mapHeight := p.OptionInt("mapheight")
if mapWidth < 5 {
diff --git a/internal/game/cmd_mix.go b/internal/game/cmd_mix.go
index 3082870..8fc600b 100644
--- a/internal/game/cmd_mix.go
+++ b/internal/game/cmd_mix.go
@@ -4,12 +4,11 @@ import (
"thehouseoficarus/internal/action"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
- "thehouseoficarus/internal/world"
)
func (g *Game) doMix(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
allRecipes, err := g.RecipeStore.LoadAll()
if err != nil {
@@ -35,7 +34,7 @@ func (g *Game) doMix(sess *net.Session, input string) {
if outDef != nil {
name = outDef.Name
}
- if world.WordPrefixMatch(itemName, name) {
+ if action.WordPrefixMatch(itemName, name) {
matched = append(matched, r)
}
}
@@ -68,7 +67,7 @@ func (g *Game) doMix(sess *net.Session, input string) {
names = append(names, g.recipeName(sess, &r))
}
g.showMenuTable(sess, "What would you like to mix?", names)
- sess.PendingMenu = recipeMenuData(available)
+ sess.PendingMenu = recipeMenuData(recipeDefIDs(available))
}
func (g *Game) showMixMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef) {
@@ -109,5 +108,9 @@ func (g *Game) showMixMenu(sess *net.Session, p *player.Player, allRecipes []act
names = append(names, e.ItemName)
}
g.showMenuTable(sess, "What would you like to mix?", names)
- sess.PendingMenu = entryMenuData(entries)
+ ids := make([]string, len(entries))
+ for i, e := range entries {
+ ids[i] = e.Recipe.ID
+ }
+ sess.PendingMenu = recipeMenuData(ids)
}
diff --git a/internal/game/cmd_mods.go b/internal/game/cmd_mods.go
index b9161e3..65d7ad7 100644
--- a/internal/game/cmd_mods.go
+++ b/internal/game/cmd_mods.go
@@ -3,109 +3,96 @@ package game
import (
"fmt"
"sort"
- "strings"
"thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
)
-func (g *Game) doMods(sess *net.Session) {
- p := sess.Player.(*player.Player)
- mode := g.colorMode(sess)
+func (g *Game) doMods(sess *net.Session, showAll bool) {
+ p := sess.Player
sciLevel := p.Level(player.Science)
+ mode := g.colorMode(sess)
categories := []struct {
Name string
Cat ModCategory
}{
{"Combat", ModCombat},
- {"Processing", ModProcessing},
{"Utility", ModUtility},
{"Transport", ModTransport},
{"Enchantment", ModEnchant},
}
sess.WriteLine("")
-
anyMods := false
for _, cat := range categories {
- var mods []*ModDef
+ var catMods []*ModDef
for _, m := range AllMods {
- if m.Category == cat.Cat && m.Level <= sciLevel {
- mods = append(mods, m)
+ if m.Category != cat.Cat {
+ continue
+ }
+ if !showAll && m.Level > sciLevel {
+ continue
}
+ catMods = append(catMods, m)
}
- if len(mods) == 0 {
+ if len(catMods) == 0 {
continue
}
+ anyMods = true
- sort.Slice(mods, func(i, j int) bool {
- return mods[i].Level < mods[j].Level
+ sort.Slice(catMods, func(i, j int) bool {
+ return catMods[i].Level < catMods[j].Level
})
- t := &Table{Title: cat.Name + " Mods", Columns: []string{
- color.Render(mode, color.Parse("75"), "Mod"),
- color.Render(mode, color.Parse("230"), "Lv"),
- color.Render(mode, color.Parse("245"), "Cost"),
- color.Render(mode, color.Parse("222"), "XP"),
- }}
+ t := &Table{
+ Title: cat.Name,
+ Columns: []string{
+ color.Render(mode, color.Parse("75"), "Module"),
+ color.Render(mode, color.Parse("245"), "Level"),
+ color.Render(mode, color.Parse("222"), "Cost"),
+ color.Render(mode, color.Parse("179"), "XP"),
+ },
+ }
+
+ for _, m := range catMods {
+ unlocked := m.Level <= sciLevel
+ levelStr := fmt.Sprintf("Lv %d", m.Level)
+ costStr := g.junkCostString(p, m)
+ xpStr := fmt.Sprintf("%d XP", m.BaseXP)
- for _, m := range mods {
- costStr := g.modCostDisplay(p, m)
- xpStr := fmt.Sprintf("%.1f", m.BaseXP)
- if m.MaxHit > 0 {
- xpStr += fmt.Sprintf(" (max %d)", m.MaxHit)
+ if unlocked {
+ t.Rows = append(t.Rows, []string{
+ color.Render(mode, color.Parse("75"), m.Name),
+ color.Render(mode, color.Parse("245"), levelStr),
+ color.Render(mode, color.Parse("222"), costStr),
+ color.Render(mode, color.Parse("179"), xpStr),
+ })
+ } else {
+ t.Rows = append(t.Rows, []string{
+ g.colorize(sess, "dim", m.Name),
+ g.colorize(sess, "dim", levelStr),
+ g.colorize(sess, "dim", costStr),
+ g.colorize(sess, "dim", xpStr),
+ })
}
- t.Rows = append(t.Rows, []string{
- color.Render(mode, color.Parse("75"), m.Name),
- color.Render(mode, color.Parse("230"), fmt.Sprint(m.Level)),
- color.Render(mode, color.Parse("245"), costStr),
- color.Render(mode, color.Parse("222"), xpStr),
- })
}
for _, line := range t.Render(p.OptionBool("unicode")) {
sess.WriteLine(line)
}
- anyMods = true
+ sess.WriteLine("")
}
if !anyMods {
- sess.WriteLine("You don't know any mods yet. Train Science to unlock mods.")
+ sess.WriteLine("No modules found.")
}
- if p.AutocastMod != "" {
- mod := GetMod(p.AutocastMod)
+ if p.AutotriggerMod != "" {
+ mod := GetMod(p.AutotriggerMod)
if mod != nil {
- sess.WriteLine(fmt.Sprintf("\nAutocast: %s", g.colorize(sess, "science_mod", mod.Name)))
- }
- }
-}
-
-func (g *Game) modCostDisplay(p *player.Player, mod *ModDef) string {
- cost := g.effectiveJunkCost(p, mod)
- if len(cost) == 0 {
- return "free"
- }
- var parts []string
- keys := make([]string, 0, len(cost))
- for k := range cost {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- for _, itemID := range keys {
- qty := cost[itemID]
- def, _ := g.ItemStore.Load(itemID)
- name := itemID
- if def != nil {
- name = def.Name
- }
- if qty > 1 {
- parts = append(parts, fmt.Sprintf("%d %s", qty, name))
- } else {
- parts = append(parts, name)
+ sess.WriteLine(fmt.Sprintf("Autotrigger: %s", g.colorize(sess, "science_mod", mod.Name)))
}
}
- return strings.Join(parts, ", ")
}
diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go
index b3fe254..5d6ea66 100644
--- a/internal/game/cmd_move.go
+++ b/internal/game/cmd_move.go
@@ -10,8 +10,8 @@ import (
"thehouseoficarus/internal/player"
)
-func (g *Game) doMove(sess *net.Session, dir string) {
- p := sess.Player.(*player.Player)
+func (g *Game) doMove(sess *net.Session, dir string, multiplier float64) {
+ p := sess.Player
exitDir := g.World.ResolveExit(dir)
if exitDir == "" {
sess.WriteLine("Go where?")
@@ -50,10 +50,10 @@ func (g *Game) doMove(sess *net.Session, dir string) {
inCombat := combat.GetCombat(p.Name) != nil
if !inCombat && p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
- ticks := g.moveTicks(p)
+ ticks := g.moveTicks(p, multiplier)
p.MoveDirection = string(exitDir)
p.MoveTarget = targetID
@@ -83,7 +83,7 @@ var gracefulItems = map[string]bool{
"graceful_cape": true,
}
-func (g *Game) moveTicks(p *player.Player) int {
+func (g *Game) moveTicks(p *player.Player, multiplier float64) int {
if id, ok := p.Equipment[object.SlotBack]; ok && id == "cape_of_agility" {
return 0
}
@@ -95,11 +95,12 @@ func (g *Game) moveTicks(p *player.Player) int {
}
}
- base := 4.0 - float64(count)*0.25
+ base := 2.0 - float64(count)*0.15
if count == 6 {
- base -= 0.5
+ base -= 0.1
}
+ base *= multiplier
return engine.ToTicks(base) - 1
}
@@ -152,7 +153,7 @@ func (g *Game) completeMove(sess *net.Session, p *player.Player) {
g.doMap(sess)
}
- g.RunEnterSteps(sess, targetID)
+ g.runEnterSteps(sess, targetID)
g.checkAggro(sess)
}
@@ -183,11 +184,8 @@ func (g *Game) seedRoomObjects(roomID int) {
if len(obj.WanderRooms) > 0 {
g.World.SetObjWander(roomID, obj.ID, obj.WanderRooms, obj.WanderInterval)
}
- if def.BehaviorID != "" {
- cfg, err := g.BehaviorStore.LoadGather(def.BehaviorID)
- if err == nil && cfg.DepleteTimer > 0 {
- g.World.SetObjDepleteTimer(roomID, obj.ID, cfg.DepleteTimer)
- }
+ if def.IsGatherable() && def.Gather.DepleteTimer > 0 {
+ g.World.SetObjDepleteTimer(roomID, obj.ID, def.Gather.DepleteTimer)
}
}
}
diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go
index 3d4d420..fdf5371 100644
--- a/internal/game/cmd_option.go
+++ b/internal/game/cmd_option.go
@@ -11,7 +11,7 @@ import (
)
func (g *Game) doOption(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
mode := g.colorMode(sess)
if input == "" {
diff --git a/internal/game/cmd_prompt.go b/internal/game/cmd_prompt.go
index 6cc45f9..8850ea1 100644
--- a/internal/game/cmd_prompt.go
+++ b/internal/game/cmd_prompt.go
@@ -4,11 +4,10 @@ import (
"fmt"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doPrompt(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if input == "" {
prompt := p.Prompt
diff --git a/internal/game/cmd_queued.go b/internal/game/cmd_queued.go
index d9e92d5..fdb1643 100644
--- a/internal/game/cmd_queued.go
+++ b/internal/game/cmd_queued.go
@@ -5,11 +5,10 @@ import (
"sort"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doQueued(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
freeCmds := g.freeQueue[p.Name]
activeCmd := g.activeQueue[p.Name]
diff --git a/internal/game/cmd_quit.go b/internal/game/cmd_quit.go
index 2ec0221..d52c9ae 100644
--- a/internal/game/cmd_quit.go
+++ b/internal/game/cmd_quit.go
@@ -6,18 +6,17 @@ import (
"thehouseoficarus/internal/combat"
"thehouseoficarus/internal/engine"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doQuit(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if combat.GetCombat(p.Name) != nil {
sess.WriteLine("You can't rest during combat!")
return
}
- g.CancelAction(p)
+ g.cancelAction(p)
p.ActionState = &ActionState{Type: ActionResting}
g.cancelRest(p.Name)
diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go
new file mode 100644
index 0000000..e5bf30e
--- /dev/null
+++ b/internal/game/cmd_registry.go
@@ -0,0 +1,170 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/net"
+)
+
+type commandHandler func(g *Game, sess *net.Session, args []string, rawInput string)
+
+type commandDef struct {
+ handler commandHandler
+ class CommandClass
+}
+
+var commandRegistry = map[string]commandDef{
+ "get": {(*Game).executeGet, ClassActive},
+ "take": {(*Game).executeGet, ClassActive},
+ "grab": {(*Game).executeGet, ClassActive},
+ "pick": {(*Game).executeGet, ClassActive},
+ "drop": {(*Game).executeDrop, ClassActive},
+ "attack": {(*Game).executeAttack, ClassActive},
+ "kill": {(*Game).executeAttack, ClassActive},
+ "style": {(*Game).executeStyle, ClassInstant},
+ "look": {(*Game).executeLook, ClassInstant},
+ "l": {(*Game).executeLook, ClassInstant},
+ "north": {(*Game).executeMove, ClassActive},
+ "n": {(*Game).executeMove, ClassActive},
+ "south": {(*Game).executeMove, ClassActive},
+ "s": {(*Game).executeMove, ClassActive},
+ "east": {(*Game).executeMove, ClassActive},
+ "e": {(*Game).executeMove, ClassActive},
+ "west": {(*Game).executeMove, ClassActive},
+ "w": {(*Game).executeMove, ClassActive},
+ "up": {(*Game).executeMove, ClassActive},
+ "u": {(*Game).executeMove, ClassActive},
+ "down": {(*Game).executeMove, ClassActive},
+ "d": {(*Game).executeMove, ClassActive},
+ "say": {(*Game).executeSay, ClassInstant},
+ "sc": {(*Game).executeScore, ClassInstant},
+ "score": {(*Game).executeScore, ClassInstant},
+ "task": {(*Game).executeTask, ClassInstant},
+ "stats": {(*Game).executeStats, ClassInstant},
+ "tech": {(*Game).executeTech, ClassInstant},
+ "t": {(*Game).executeTech, ClassInstant},
+ "i": {(*Game).executeInventory, ClassInstant},
+ "inv": {(*Game).executeInventory, ClassInstant},
+ "inventory": {(*Game).executeInventory, ClassInstant},
+ "quit": {(*Game).executeQuit, ClassActive},
+ "description": {(*Game).executeDescription, ClassInstant},
+ "desc": {(*Game).executeDescription, ClassInstant},
+ "option": {(*Game).executeOption, ClassInstant},
+ "options": {(*Game).executeOption, ClassInstant},
+ "color": {(*Game).executeColor, ClassInstant},
+ "colors": {(*Game).executeColor, ClassInstant},
+ "colortable": {(*Game).executeColortable, ClassInstant},
+ "prompt": {(*Game).executePrompt, ClassInstant},
+ "exits": {(*Game).executeExits, ClassInstant},
+ "map": {(*Game).executeMap, ClassInstant},
+ "queued": {(*Game).executeQueued, ClassInstant},
+ "help": {(*Game).executeHelp, ClassInstant},
+ "mine": {(*Game).executeGather, ClassActive},
+ "chop": {(*Game).executeGather, ClassActive},
+ "fish": {(*Game).executeGather, ClassActive},
+ "cut": {(*Game).executeGather, ClassActive},
+ "pull": {(*Game).executeUse, ClassActive},
+ "push": {(*Game).executeUse, ClassActive},
+ "use": {(*Game).executeUse, ClassActive},
+ "cook": {(*Game).executeCook, ClassActive},
+ "smelt": {(*Game).executeSmelt, ClassActive},
+ "smith": {(*Game).executeSmith, ClassActive},
+ "craft": {(*Game).executeCraft, ClassActive},
+ "eat": {(*Game).executeEat, ClassFree},
+ "drink": {(*Game).executeDrink, ClassFree},
+ "fletch": {(*Game).executeFletch, ClassFree},
+ "clean": {(*Game).executeClean, ClassFree},
+ "mix": {(*Game).executeMix, ClassActive},
+ "id": {(*Game).executeIdentify, ClassActive},
+ "identify": {(*Game).executeIdentify, ClassActive},
+ "autotrigger": {(*Game).executeAutotrigger, ClassInstant},
+ "auto": {(*Game).executeAutotrigger, ClassInstant},
+ "mods": {(*Game).executeMods, ClassInstant},
+ "modlist": {(*Game).executeMods, ClassInstant},
+ "mod": {(*Game).executeMods, ClassInstant},
+ "sneak": {(*Game).executeSneak, ClassInstant},
+ "trigger": {(*Game).executeTrigger, ClassActive},
+ "talk": {(*Game).executeTalk, ClassActive},
+ "speak": {(*Game).executeTalk, ClassActive},
+ "ask": {(*Game).executeTalk, ClassActive},
+ "burn": {(*Game).executeBurn, ClassActive},
+ "stoke": {(*Game).executeStoke, ClassActive},
+ "alias": {(*Game).executeAlias, ClassInstant},
+ "unalias": {(*Game).executeUnalias, ClassInstant},
+ "search": {(*Game).executeSearch, ClassActive},
+ "steal": {(*Game).executeSteal, ClassActive},
+ "thieve": {(*Game).executeSteal, ClassActive},
+ "plant": {(*Game).executePlant, ClassActive},
+ "harvest": {(*Game).executeHarvest, ClassActive},
+ "rake": {(*Game).executeRake, ClassActive},
+ "water": {(*Game).executeWater, ClassActive},
+ "cure": {(*Game).executeCure, ClassActive},
+ "inspect": {(*Game).executeInspect, ClassInstant},
+ "walk": {(*Game).executeWalk, ClassActive},
+ "bank": {(*Game).executeBank, ClassActive},
+ "construct": {(*Game).executeConstruct, ClassActive},
+ "make": {(*Game).executeConstruct, ClassActive},
+ "jack": {(*Game).executeJack, ClassActive},
+ "jackin": {(*Game).executeJack, ClassActive},
+ "eq": {(*Game).executeWear, ClassFree},
+ "equipment": {(*Game).executeWear, ClassFree},
+ "equip": {(*Game).executeWear, ClassFree},
+ "wear": {(*Game).executeWear, ClassFree},
+ "wield": {(*Game).executeWear, ClassFree},
+ "remove": {(*Game).executeRemove, ClassFree},
+ "unwear": {(*Game).executeRemove, ClassFree},
+ "unwield": {(*Game).executeRemove, ClassFree},
+}
+
+func classifyCommand(cmd string) CommandClass {
+ if def, ok := commandRegistry[cmd]; ok {
+ return def.class
+ }
+ if _, ok := verbAliases[cmd]; ok {
+ return ClassActive
+ }
+ if obstacleVerbs[cmd] {
+ return ClassActive
+ }
+ return ClassUnknown
+}
+
+func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawInput string) {
+ if def, ok := commandRegistry[cmd]; ok {
+ def.handler(g, sess, args, rawInput)
+ return
+ }
+
+ if obstacleVerbs[cmd] {
+ g.doObstacle(sess, cmd)
+ return
+ }
+
+ p := sess.Player
+ if a, ok := verbAliases[cmd]; ok {
+ g.cancelAction(p)
+ switch a {
+ case "gather":
+ if len(args) == 0 {
+ target := g.resolveDefaultTarget(p.RoomID, cmd)
+ if target == "" {
+ sess.WriteLine(fmt.Sprintf("%s what?", cmd))
+ return
+ }
+ g.startAction(sess, cmd, target)
+ } else {
+ g.startAction(sess, cmd, strings.Join(args, " "))
+ }
+ case "talk":
+ if len(args) == 0 {
+ sess.WriteLine("Talk to whom?")
+ } else {
+ g.startAction(sess, "talk", strings.Join(args, " "))
+ }
+ }
+ return
+ }
+
+ sess.WriteLine("Unknown command.")
+}
diff --git a/internal/game/cmd_remove.go b/internal/game/cmd_remove.go
index d36a8fd..e168fd9 100644
--- a/internal/game/cmd_remove.go
+++ b/internal/game/cmd_remove.go
@@ -10,7 +10,7 @@ import (
)
func (g *Game) doRemove(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
lower := strings.ToLower(strings.TrimSpace(input))
if lower == "all" {
@@ -22,7 +22,7 @@ func (g *Game) doRemove(sess *net.Session, input string) {
return
}
- g.CancelAction(p)
+ g.cancelAction(p)
qty, itemName := parseQty(input)
@@ -136,7 +136,7 @@ func (g *Game) removeAmmo(sess *net.Session, p *player.Player, qty int) {
}
func (g *Game) doRemoveAll(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if len(p.Equipment) == 0 {
sess.WriteLine("You aren't wearing anything.")
@@ -153,7 +153,7 @@ func (g *Game) doRemoveAll(sess *net.Session) {
return
}
- g.CancelAction(p)
+ g.cancelAction(p)
for _, slot := range EquipSlots {
itemID, ok := p.Equipment[slot]
diff --git a/internal/game/cmd_say.go b/internal/game/cmd_say.go
index 67d8165..3d619a0 100644
--- a/internal/game/cmd_say.go
+++ b/internal/game/cmd_say.go
@@ -4,11 +4,10 @@ import (
"fmt"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doSay(sess *net.Session, msg string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
roomID := p.RoomID
for _, other := range g.Hub.PlayersInRoom(roomID) {
diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go
index 82c0803..0b0caf2 100644
--- a/internal/game/cmd_score.go
+++ b/internal/game/cmd_score.go
@@ -10,7 +10,7 @@ import (
)
func (g *Game) doScore(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
mode := g.colorMode(sess)
sess.WriteLines(
"",
diff --git a/internal/game/cmd_search.go b/internal/game/cmd_search.go
index cf37c90..cc22d49 100644
--- a/internal/game/cmd_search.go
+++ b/internal/game/cmd_search.go
@@ -5,12 +5,11 @@ import (
"strings"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doSearch(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
lower := strings.ToLower(strings.TrimSpace(input))
if lower == "" {
diff --git a/internal/game/cmd_shop.go b/internal/game/cmd_shop.go
index a3f7613..2742151 100644
--- a/internal/game/cmd_shop.go
+++ b/internal/game/cmd_shop.go
@@ -2,11 +2,12 @@ package game
import (
"fmt"
+ "strconv"
"strings"
"thehouseoficarus/internal/action"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/object"
)
func (g *Game) handleShopInput(sess *net.Session, input string) {
@@ -27,17 +28,9 @@ func (g *Game) handleShopInput(sess *net.Session, input string) {
switch cmd {
case "buy":
- if len(parts) < 2 {
- sess.WriteLine("Buy what? Use 'browse' to see available items.")
- } else {
- g.doShopBuy(sess, strings.Join(parts[1:], " "))
- }
+ g.cmdShopBuy(sess, parts[1:])
case "sell":
- if len(parts) < 2 {
- sess.WriteLine("Sell what?")
- } else {
- g.doShopSell(sess, strings.Join(parts[1:], " "))
- }
+ g.cmdShopSell(sess, parts[1:])
case "browse", "list":
g.showShopBrowse(sess)
case "leave", "bye", "exit", "quit":
@@ -49,15 +42,65 @@ func (g *Game) handleShopInput(sess *net.Session, input string) {
g.writeShopPrompt(sess)
}
+func parseShopQty(args []string) (int, string, bool) {
+ if len(args) == 0 {
+ return 1, "", false
+ }
+ if strings.EqualFold(args[0], "all") {
+ rest := strings.Join(args[1:], " ")
+ return 0, rest, true
+ }
+ if n, err := strconv.Atoi(args[0]); err == nil && n > 0 {
+ return n, strings.Join(args[1:], " "), false
+ }
+ return 1, strings.Join(args, " "), false
+}
+
+func (g *Game) cmdShopBuy(sess *net.Session, args []string) {
+ cfg := g.shopConfig(sess)
+ if cfg == nil {
+ return
+ }
+
+ qty, itemName, all := parseShopQty(args)
+ if itemName == "" {
+ sess.WriteLine("Buy what? Use 'browse' to see available items.")
+ return
+ }
+
+ g.doShopBuy(sess, itemName, qty, all)
+}
+
+func (g *Game) cmdShopSell(sess *net.Session, args []string) {
+ cfg := g.shopConfig(sess)
+ if cfg == nil {
+ return
+ }
+
+ qty, itemName, all := parseShopQty(args)
+ if itemName == "" {
+ sess.WriteLine("Sell what?")
+ return
+ }
+
+ g.doShopSell(sess, itemName, qty, all)
+}
+
func (g *Game) writeShopPrompt(sess *net.Session) {
sess.Write(fmt.Sprintf("\n%s", g.colorize(sess, "dialog", "> ")))
}
func (g *Game) shopConfig(sess *net.Session) *action.ShopConfig {
- cfg, _ := sess.Shop.(*action.ShopConfig)
+ cfg := sess.Shop
return cfg
}
+type shopMatch struct {
+ si *action.ShopItem
+ name string
+ def *object.ItemDef
+}
+
func (g *Game) showShopBrowse(sess *net.Session) {
cfg := g.shopConfig(sess)
if cfg == nil {
@@ -74,16 +117,16 @@ func (g *Game) showShopBrowse(sess *net.Session) {
}
unicode := true
- if p, ok := sess.Player.(*player.Player); ok {
+ if p := sess.Player; p != nil {
unicode = p.OptionBool("unicode")
}
t := Table{
Title: "Shop Inventory",
- Columns: []string{"#", "Item", "Buy Price", "Sell Price"},
+ Columns: []string{"Item", "Buy Price", "Sell Price"},
}
- for i, si := range cfg.Items {
+ for _, si := range cfg.Items {
def, _ := g.ItemStore.Load(si.ItemID)
itemName := si.ItemID
if def != nil {
@@ -95,7 +138,6 @@ func (g *Game) showShopBrowse(sess *net.Session) {
sellStr = fmt.Sprintf("%d cr", si.SellPrice)
}
t.Rows = append(t.Rows, []string{
- fmt.Sprintf("%d", i+1),
g.colorize(sess, "item", itemName),
g.colorize(sess, "credits_pickup", buyStr),
g.colorize(sess, "credits_pickup", sellStr),
@@ -107,55 +149,84 @@ func (g *Game) showShopBrowse(sess *net.Session) {
}
}
-func (g *Game) doShopBuy(sess *net.Session, input string) {
+func (g *Game) findShopMatches(input string, cfg *action.ShopConfig) []shopMatch {
+ var matches []shopMatch
+ for i := range cfg.Items {
+ si := &cfg.Items[i]
+ itemName := si.ItemID
+ def, _ := g.ItemStore.Load(si.ItemID)
+ if def != nil {
+ itemName = def.Name
+ }
+ if action.WordPrefixMatch(input, itemName) {
+ matches = append(matches, shopMatch{si: si, name: itemName, def: def})
+ }
+ }
+ return matches
+}
+
+func (g *Game) doShopBuy(sess *net.Session, input string, qty int, all bool) {
cfg := g.shopConfig(sess)
if cfg == nil {
return
}
- idx, err := parseChoiceIndex(input)
- if err == nil && idx > 0 && idx <= len(cfg.Items) {
- si := cfg.Items[idx-1]
- g.buyShopItem(sess, si)
+ p := sess.Player
+
+ matches := g.findShopMatches(input, cfg)
+ if len(matches) == 0 {
+ sess.WriteLine("That item isn't for sale here. Use 'browse' to see shop inventory.")
return
}
- var match *action.ShopItem
- for i := range cfg.Items {
- si := &cfg.Items[i]
- def, _ := g.ItemStore.Load(si.ItemID)
- itemName := si.ItemID
- if def != nil {
- itemName = def.Name
+ if len(matches) > 1 {
+ sess.WriteLine("That's ambiguous, which one?")
+ for _, m := range matches {
+ sess.WriteLine(fmt.Sprintf(" - %s", g.colorize(sess, "item", m.name)))
}
+ return
+ }
- invMatches := g.collectMatches(input, func(yield func(string, int) bool) {
- yield(itemName, -1)
- })
+ si := matches[0].si
+ def := matches[0].def
- if len(invMatches) > 0 {
- if match != nil {
- sess.WriteLine(fmt.Sprintf("That's ambiguous, which one? (use #, e.g. buy %s 1)", si.ItemID))
- return
+ actualQty := qty
+ if all || qty == 0 {
+ affordable := p.Credits / si.BuyPrice
+ if def != nil && def.Stackable {
+ actualQty = affordable
+ } else {
+ freeSlots := p.FreeSlots()
+ actualQty = affordable
+ if actualQty > freeSlots {
+ actualQty = freeSlots
}
- match = si
}
}
- if match != nil {
- g.buyShopItem(sess, *match)
+ if actualQty <= 0 {
+ if p.Credits < si.BuyPrice {
+ sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits))
+ } else {
+ sess.WriteLine("Your inventory is full.")
+ }
return
}
- sess.WriteLine("That item isn't for sale here. Use 'browse' to see shop inventory.")
+ g.buyShopItem(sess, *si, actualQty)
}
-func (g *Game) buyShopItem(sess *net.Session, si action.ShopItem) {
- p, _ := sess.Player.(*player.Player)
+func (g *Game) buyShopItem(sess *net.Session, si action.ShopItem, qty int) {
+ p := sess.Player
- if p.Credits < si.BuyPrice {
- sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits))
- return
+ totalCost := si.BuyPrice * qty
+ if p.Credits < totalCost {
+ qty = p.Credits / si.BuyPrice
+ if qty <= 0 {
+ sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits))
+ return
+ }
+ totalCost = si.BuyPrice * qty
}
def, _ := g.ItemStore.Load(si.ItemID)
@@ -168,37 +239,83 @@ func (g *Game) buyShopItem(sess *net.Session, si action.ShopItem) {
for i := 0; i < 28; i++ {
slot := p.InvSlot(i)
if slot != nil && slot.ItemID == si.ItemID {
- p.Credits -= si.BuyPrice
- slot.Quantity++
+ p.Credits -= totalCost
+ slot.Quantity += qty
g.AccountStore.SaveCharacter(p)
itemColor := g.itemColorize(sess, def, displayName)
- sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice))))
+ if qty == 1 {
+ sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice))))
+ } else {
+ sess.WriteLine(fmt.Sprintf("You buy %d %s for %s credits.", qty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCost))))
+ }
return
}
}
+
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot == -1 {
+ sess.WriteLine("Your inventory is full.")
+ return
+ }
+
+ p.Credits -= totalCost
+ p.SetInvSlot(freeSlot, g.newInventorySlot(si.ItemID, qty))
+ g.AccountStore.SaveCharacter(p)
+
+ itemColor := g.itemColorize(sess, def, displayName)
+ if qty == 1 {
+ sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice))))
+ } else {
+ sess.WriteLine(fmt.Sprintf("You buy %d %s for %s credits.", qty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCost))))
+ }
+ return
+ }
+
+ freeSlots := p.FreeSlots()
+ if qty > freeSlots {
+ if freeSlots == 0 {
+ sess.WriteLine("Your inventory is full.")
+ return
+ }
+ qty = freeSlots
+ totalCost = si.BuyPrice * qty
+ if p.Credits < totalCost {
+ qty = p.Credits / si.BuyPrice
+ totalCost = si.BuyPrice * qty
+ }
}
- freeSlot := p.FirstFreeSlot()
- if freeSlot == -1 {
- sess.WriteLine("Your inventory is full.")
+ if qty <= 0 {
+ if p.Credits < si.BuyPrice {
+ sess.WriteLine(fmt.Sprintf("You need %d credits to buy that (you have %d).", si.BuyPrice, p.Credits))
+ } else {
+ sess.WriteLine("Your inventory is full.")
+ }
return
}
- p.Credits -= si.BuyPrice
- p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: si.ItemID, Quantity: 1})
+ p.Credits -= totalCost
+ for j := 0; j < qty; j++ {
+ freeSlot := p.FirstFreeSlot()
+ p.SetInvSlot(freeSlot, g.newInventorySlot(si.ItemID, 1))
+ }
g.AccountStore.SaveCharacter(p)
itemColor := g.itemColorize(sess, def, displayName)
- sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice))))
+ if qty == 1 {
+ sess.WriteLine(fmt.Sprintf("You buy a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", si.BuyPrice))))
+ } else {
+ sess.WriteLine(fmt.Sprintf("You buy %d %s for %s credits.", qty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCost))))
+ }
}
-func (g *Game) doShopSell(sess *net.Session, input string) {
+func (g *Game) doShopSell(sess *net.Session, input string, qty int, all bool) {
cfg := g.shopConfig(sess)
if cfg == nil {
return
}
- p, _ := sess.Player.(*player.Player)
+ p := sess.Player
matches := g.findInventoryMatches(input, p)
if len(matches) == 0 {
@@ -208,6 +325,9 @@ func (g *Game) doShopSell(sess *net.Session, input string) {
if len(matches) > 1 {
sess.WriteLine("That's ambiguous, which one?")
+ for _, m := range uniqueItemNames(matches) {
+ sess.WriteLine(fmt.Sprintf(" - %s", g.colorize(sess, "item", m)))
+ }
return
}
@@ -232,38 +352,52 @@ func (g *Game) doShopSell(sess *net.Session, input string) {
return
}
- p.RemoveItem(match.ID, 1)
- p.Credits += shopItem.SellPrice
+ totalInInv := p.CountItem(match.ID)
+ actualQty := qty
+ if all || qty == 0 {
+ actualQty = totalInInv
+ } else if actualQty > totalInInv {
+ actualQty = totalInInv
+ }
+
+ if actualQty <= 0 {
+ sess.WriteLine("You don't have that item.")
+ return
+ }
+
+ p.RemoveItem(match.ID, actualQty)
+ p.Credits += shopItem.SellPrice * actualQty
g.AccountStore.SaveCharacter(p)
def, _ := g.ItemStore.Load(match.ID)
- displayName := match.ID
- if def != nil {
- displayName = def.Name
- }
+ displayName := match.Name
itemColor := g.itemColorize(sess, def, displayName)
- sess.WriteLine(fmt.Sprintf("You sell a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", shopItem.SellPrice))))
+ if actualQty == 1 {
+ sess.WriteLine(fmt.Sprintf("You sell a %s for %s credits.", itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", shopItem.SellPrice))))
+ } else {
+ totalCredits := shopItem.SellPrice * actualQty
+ sess.WriteLine(fmt.Sprintf("You sell %d %s for %s credits.", actualQty, itemColor, g.colorize(sess, "credits_pickup", fmt.Sprintf("%d", totalCredits))))
+ }
}
func (g *Game) leaveShop(sess *net.Session) {
sess.Shop = nil
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil || p.Action == nil || p.Action.Type != "talk" {
+ p := sess.Player
+ if p == nil || p.Action == nil || p.Action.Type != "talk" {
sess.State = net.StateGame
if p != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
g.writePrompt(sess)
return
}
- behaviorID, _ := p.Action.Data["behavior_id"].(string)
nodeKey, _ := p.Action.Data["node"].(string)
- cfg, err := g.BehaviorStore.LoadTalk(behaviorID)
- if err != nil {
+ cfg, ok := p.Action.Data["talk_cfg"].(*action.TalkConfig)
+ if !ok || cfg == nil {
sess.State = net.StateGame
- g.CancelAction(p)
+ g.cancelAction(p)
g.writePrompt(sess)
return
}
@@ -271,7 +405,7 @@ func (g *Game) leaveShop(sess *net.Session) {
node, ok := cfg.Nodes[nodeKey]
if !ok {
sess.State = net.StateGame
- g.CancelAction(p)
+ g.cancelAction(p)
g.writePrompt(sess)
return
}
@@ -287,7 +421,7 @@ func (g *Game) leaveShop(sess *net.Session) {
}
if visible == 0 {
sess.State = net.StateGame
- g.CancelAction(p)
+ g.cancelAction(p)
g.writePrompt(sess)
return
}
diff --git a/internal/game/cmd_smelt.go b/internal/game/cmd_smelt.go
index 46f2607..53ff426 100644
--- a/internal/game/cmd_smelt.go
+++ b/internal/game/cmd_smelt.go
@@ -9,8 +9,8 @@ import (
)
func (g *Game) doSmelt(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
stationDefID, stationName := g.findStation(p.RoomID, []string{"furnace"})
if stationDefID == "" {
@@ -64,7 +64,7 @@ func (g *Game) doSmelt(sess *net.Session, input string) {
return
}
- sess.PendingMenu = recipeMenuData(recipes)
+ sess.PendingMenu = recipeMenuData(recipeDefIDs(recipes))
sess.State = net.StateRecipeChoice
var names []string
for _, r := range recipes {
@@ -108,5 +108,9 @@ func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, allRecipes []a
names = append(names, e.ItemName)
}
g.showMenuTable(sess, "What would you like to smelt?", names)
- sess.PendingMenu = entryMenuData(entries)
+ ids := make([]string, len(entries))
+ for i, e := range entries {
+ ids[i] = e.Recipe.ID
+ }
+ sess.PendingMenu = recipeMenuData(ids)
}
diff --git a/internal/game/cmd_smith.go b/internal/game/cmd_smith.go
index b539640..067d251 100644
--- a/internal/game/cmd_smith.go
+++ b/internal/game/cmd_smith.go
@@ -11,8 +11,8 @@ import (
)
func (g *Game) doSmith(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
stationDefID, _ := g.findStation(p.RoomID, []string{"anvil"})
if stationDefID == "" {
diff --git a/internal/game/cmd_sneak.go b/internal/game/cmd_sneak.go
index 97aab8a..b37862d 100644
--- a/internal/game/cmd_sneak.go
+++ b/internal/game/cmd_sneak.go
@@ -4,11 +4,10 @@ import (
"fmt"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doSneak(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
p.Sneaking = !p.Sneaking
if p.Sneaking {
p.SneakNotified = make(map[string]bool)
@@ -29,8 +28,8 @@ func (g *Game) SneakTick() {
}
for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil || !p.Sneaking {
+ p := sess.Player
+ if p == nil || !p.Sneaking {
continue
}
diff --git a/internal/game/cmd_stats.go b/internal/game/cmd_stats.go
index 0dc963e..51bacf1 100644
--- a/internal/game/cmd_stats.go
+++ b/internal/game/cmd_stats.go
@@ -10,7 +10,7 @@ import (
)
func (g *Game) doStats(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
totals := g.playerEquipBonuses(p)
attackType := "crush"
diff --git a/internal/game/cmd_style.go b/internal/game/cmd_style.go
index 901d02d..269fa3f 100644
--- a/internal/game/cmd_style.go
+++ b/internal/game/cmd_style.go
@@ -9,7 +9,7 @@ import (
)
func (g *Game) doStyle(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
styles := []string{"accurate", "aggressive", "defensive", "balanced"}
if input == "" {
diff --git a/internal/game/cmd_task.go b/internal/game/cmd_task.go
index 2dcf3f4..5b67421 100644
--- a/internal/game/cmd_task.go
+++ b/internal/game/cmd_task.go
@@ -4,11 +4,10 @@ import (
"fmt"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
)
func (g *Game) doTask(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
mobID := getPlayerFlagString(p, "assassin_task_mob")
remaining := getPlayerFlagInt(p, "assassin_task_remaining")
diff --git a/internal/game/cmd_tech.go b/internal/game/cmd_tech.go
index f51ccd9..89d5c2a 100644
--- a/internal/game/cmd_tech.go
+++ b/internal/game/cmd_tech.go
@@ -10,7 +10,7 @@ import (
)
func (g *Game) doTech(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
techLevel := p.Level(player.Technology)
if techLevel < 1 {
@@ -109,7 +109,7 @@ func (g *Game) deactivateGroup(p *player.Player, group string) []string {
}
func (g *Game) doTechList(sess *net.Session) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
mode := g.colorMode(sess)
techLevel := p.Level(player.Technology)
diff --git a/internal/game/cmd_trigger.go b/internal/game/cmd_trigger.go
index 7ef4913..89ed397 100644
--- a/internal/game/cmd_trigger.go
+++ b/internal/game/cmd_trigger.go
@@ -11,7 +11,7 @@ import (
)
func (g *Game) doTrigger(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
input = strings.TrimSpace(input)
if input == "" {
@@ -40,8 +40,6 @@ func (g *Game) doTrigger(sess *net.Session, input string) {
g.triggerCombatMod(sess, p, mod, targetArg)
case ModTransport:
g.triggerTransport(sess, p, mod)
- case ModProcessing:
- g.triggerProcessing(sess, p, mod, targetArg)
case ModUtility:
g.triggerUtility(sess, p, mod, targetArg)
case ModEnchant:
@@ -65,13 +63,13 @@ func (g *Game) parseTriggerArgs(input string) (*ModDef, string) {
func (g *Game) triggerCombatMod(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) {
if cs := combat.GetCombat(p.Name); cs != nil {
- p.AutocastMod = mod.ID
+ p.AutotriggerMod = mod.ID
sess.WriteLine(fmt.Sprintf("You switch to triggering %s.", mod.Name))
return
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
var mobTarget string
@@ -105,7 +103,7 @@ func (g *Game) triggerCombatMod(sess *net.Session, p *player.Player, mod *ModDef
return
}
- p.AutocastMod = mod.ID
+ p.AutotriggerMod = mod.ID
g.startCombat(sess, p, mob)
}
@@ -116,15 +114,13 @@ func (g *Game) triggerTransport(sess *net.Session, p *player.Player, mod *ModDef
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
g.consumeJunkCost(p, mod)
- sciXP := int(mod.BaseXP)
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
+ sciXP := mod.BaseXP
+ g.awardSkillXP(sess, p, player.Science, sciXP)
if p.OptionBool("xp_drops") {
sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf("+%dxp sci", sciXP)))
}
@@ -180,7 +176,7 @@ func (g *Game) triggerProcessing(sess *net.Session, p *player.Player, mod *ModDe
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
g.consumeJunkCost(p, mod)
@@ -201,17 +197,10 @@ func (g *Game) triggerProcessing(sess *net.Session, p *player.Player, mod *ModDe
p.Credits += creditValue
- sciXP := int(mod.BaseXP)
- var leveledUp []player.SkillName
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- leveledUp = append(leveledUp, player.Science)
- }
+ sciXP := mod.BaseXP
+ g.awardSkillXP(sess, p, player.Science, sciXP)
g.AccountStore.SaveCharacter(p)
- for _, skill := range leveledUp {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", p.Level(skill), skill)))
- }
-
sess.WriteLine(fmt.Sprintf("You process the %s. You receive %s credits.",
itemDef.Name, g.colorize(sess, "credits_pickup", fmt.Sprint(creditValue))))
@@ -222,6 +211,8 @@ func (g *Game) triggerProcessing(sess *net.Session, p *player.Player, mod *ModDe
func (g *Game) triggerUtility(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) {
switch mod.ID {
+ case "low_process", "high_process":
+ g.triggerProcessing(sess, p, mod, targetArg)
case "bones_to_nutrients":
g.triggerBonesToNutrients(sess, p, mod)
case "em_grab":
@@ -246,7 +237,7 @@ func (g *Game) triggerBonesToNutrients(sess *net.Session, p *player.Player, mod
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
g.consumeJunkCost(p, mod)
@@ -258,10 +249,8 @@ func (g *Game) triggerBonesToNutrients(sess *net.Session, p *player.Player, mod
}
}
- sciXP := int(mod.BaseXP) * boneCount
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
+ sciXP := mod.BaseXP * boneCount
+ g.awardSkillXP(sess, p, player.Science, sciXP)
g.AccountStore.SaveCharacter(p)
sess.WriteLine(fmt.Sprintf("You convert %d bones into nutrient bars.", boneCount))
@@ -301,7 +290,7 @@ func (g *Game) triggerEmGrab(sess *net.Session, p *player.Player, mod *ModDef, t
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
g.consumeJunkCost(p, mod)
@@ -318,10 +307,8 @@ func (g *Game) triggerEmGrab(sess *net.Session, p *player.Player, mod *ModDef, t
freeSlot := p.FirstFreeSlot()
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: matchedID, Quantity: qty})
- sciXP := int(mod.BaseXP)
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
+ sciXP := mod.BaseXP
+ g.awardSkillXP(sess, p, player.Science, sciXP)
g.AccountStore.SaveCharacter(p)
sess.WriteLine(fmt.Sprintf("You magnetically pull the %s toward you.", name))
@@ -377,7 +364,7 @@ func (g *Game) triggerSuperheat(sess *net.Session, p *player.Player, mod *ModDef
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
g.consumeJunkCost(p, mod)
@@ -410,14 +397,10 @@ func (g *Game) triggerSuperheat(sess *net.Session, p *player.Player, mod *ModDef
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Output, Quantity: outputQty})
}
- sciXP := int(mod.BaseXP)
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
+ sciXP := mod.BaseXP
+ g.awardSkillXP(sess, p, player.Science, sciXP)
if recipe.XP > 0 {
- if newLevel := p.AddSkillXP(player.SkillName(recipe.Skill), recipe.XP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", p.Level(player.SkillName(recipe.Skill)), recipe.Skill)))
- }
+ g.awardSkillXP(sess, p, player.SkillName(recipe.Skill), recipe.XP)
}
g.AccountStore.SaveCharacter(p)
@@ -477,7 +460,7 @@ func (g *Game) triggerPlankMake(sess *net.Session, p *player.Player, mod *ModDef
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
g.consumeJunkCost(p, mod)
@@ -492,13 +475,9 @@ func (g *Game) triggerPlankMake(sess *net.Session, p *player.Player, mod *ModDef
}
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: info.plankID, Quantity: 1})
- sciXP := int(mod.BaseXP)
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
- if newLevel := p.AddSkillXP(player.Construction, 10); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d construction! ***", p.Level(player.Construction))))
- }
+ sciXP := mod.BaseXP
+ g.awardSkillXP(sess, p, player.Science, sciXP)
+ g.awardSkillXP(sess, p, player.Construction, 10)
g.AccountStore.SaveCharacter(p)
sess.WriteLine(fmt.Sprintf("You convert the log into %s.", info.name))
@@ -521,7 +500,7 @@ func (g *Game) triggerPlankMake(sess *net.Session, p *player.Player, mod *ModDef
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
g.consumeJunkCost(p, mod)
@@ -540,14 +519,10 @@ func (g *Game) triggerPlankMake(sess *net.Session, p *player.Player, mod *ModDef
processed++
}
- sciXP := int(mod.BaseXP) * qty
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
+ sciXP := mod.BaseXP * qty
+ g.awardSkillXP(sess, p, player.Science, sciXP)
conXP := 10 * qty
- if newLevel := p.AddSkillXP(player.Construction, conXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d construction! ***", p.Level(player.Construction))))
- }
+ g.awardSkillXP(sess, p, player.Construction, conXP)
g.AccountStore.SaveCharacter(p)
sess.WriteLine(fmt.Sprintf("You convert %d logs into %s for %d credits.", processed, info.name, totalCost))
@@ -594,17 +569,15 @@ func (g *Game) triggerEnchant(sess *net.Session, p *player.Player, mod *ModDef,
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
g.consumeJunkCost(p, mod)
inv.ItemID = outputID
- sciXP := int(mod.BaseXP)
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
+ sciXP := mod.BaseXP
+ g.awardSkillXP(sess, p, player.Science, sciXP)
g.AccountStore.SaveCharacter(p)
outputDef, _ := g.ItemStore.Load(outputID)
@@ -637,7 +610,7 @@ func (g *Game) triggerChipBolts(sess *net.Session, p *player.Player, mod *ModDef
}
if p.Action != nil {
- g.CancelAction(p)
+ g.cancelAction(p)
}
g.consumeJunkCost(p, mod)
@@ -657,10 +630,8 @@ func (g *Game) triggerChipBolts(sess *net.Session, p *player.Player, mod *ModDef
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: chip.Output, Quantity: chip.Qty})
}
- sciXP := int(mod.BaseXP)
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d science! ***", p.Level(player.Science))))
- }
+ sciXP := mod.BaseXP
+ g.awardSkillXP(sess, p, player.Science, sciXP)
g.AccountStore.SaveCharacter(p)
inputDef, _ := g.ItemStore.Load(chip.Input)
@@ -714,27 +685,21 @@ func (g *Game) scienceAttack(sess *net.Session, p *player.Player, mob *world.Mob
mob.StartRegen()
}
- sciXP := int(mod.BaseXP)
- hpXP := int(mod.BaseXP * 0.33)
+ sciXP := mod.BaseXP
+ hpXP := mod.BaseXP / 3
+ if hpXP < 1 {
+ hpXP = 1
+ }
var gains []xpGain
- var leveledUp []player.SkillName
- if newLevel := p.AddSkillXP(player.Science, sciXP); newLevel > 0 {
- leveledUp = append(leveledUp, player.Science)
- }
+ g.awardSkillXP(sess, p, player.Science, sciXP)
gains = append(gains, xpGain{string(player.Science), sciXP})
- if newLevel := p.AddSkillXP(player.Hitpoints, hpXP); newLevel > 0 {
- leveledUp = append(leveledUp, player.Hitpoints)
- }
+ g.awardSkillXP(sess, p, player.Hitpoints, hpXP)
gains = append(gains, xpGain{string(player.Hitpoints), hpXP})
g.AccountStore.SaveCharacter(p)
- for _, skill := range leveledUp {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", p.Level(skill), skill)))
- }
-
mobName := mobDisplayName(mob, true)
prefix := fmt.Sprintf(" %s hits %s for %s damage.",
g.colorize(sess, "science_mod", mod.Name),
diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go
index 95a7b5b..fbe5945 100644
--- a/internal/game/cmd_use.go
+++ b/internal/game/cmd_use.go
@@ -16,8 +16,8 @@ func (g *Game) doUse(sess *net.Session, input string) {
return
}
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
if g.tryRecharge(sess, p, input) {
return
@@ -50,11 +50,144 @@ func (g *Game) doUse(sess *net.Session, input string) {
}
if len(strings.Fields(input)) >= 2 {
+ if g.useRoomObject(sess, p, input) {
+ return
+ }
g.doUseItemOnTarget(sess, p, strings.Fields(input)[0], strings.Join(strings.Fields(input)[1:], " "))
return
}
- g.StartAction(sess, "use", input)
+ if g.useRoomObject(sess, p, input) {
+ return
+ }
+
+ g.startAction(sess, "use", input)
+}
+
+func (g *Game) useRoomObject(sess *net.Session, p *player.Player, input string) bool {
+ objInstances := g.World.FindObjInstances(p.RoomID, input)
+ if len(objInstances) == 0 {
+ return false
+ }
+ objSt := &objInstances[0]
+ def, _ := g.ObjectStore.Load(objSt.DefID)
+ if def == nil {
+ return false
+ }
+ recipes := g.stationRecipes(p, objSt.DefID)
+ if len(recipes) > 0 {
+ g.showStationRecipeMenu(sess, p, recipes, def.Name)
+ return true
+ }
+ bt := def.BehaviorType()
+ if bt != "" {
+ g.startAction(sess, bt, def.Name)
+ return true
+ }
+ for _, ui := range def.UseInteractions {
+ if ui.Item != "" {
+ continue
+ }
+ if ui.Condition != nil && !g.checkCondition(sess, ui.Condition) {
+ continue
+ }
+ if ui.Message != "" {
+ sess.WriteLine(ui.Message)
+ }
+ if ui.Action != nil {
+ g.applyNodeAction(sess, ui.Action)
+ }
+ p.ActionState = &ActionState{Type: ActionUsing, TargetName: def.Name}
+ if g.Hub != nil {
+ for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
+ if other != sess && other.Player != nil {
+ other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s uses the %s.", p.Name, def.Name)))
+ }
+ }
+ }
+ return true
+ }
+ if len(def.UseInteractions) > 0 {
+ sess.WriteLine(fmt.Sprintf("Use what on the %s?", def.Name))
+ return true
+ }
+ sess.WriteLine(fmt.Sprintf("You can't use the %s.", def.Name))
+ return true
+}
+
+func (g *Game) stationRecipes(p *player.Player, stationDefID string) []action.RecipeDef {
+ allRecipes, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ return nil
+ }
+ var results []action.RecipeDef
+ for _, r := range allRecipes {
+ if len(r.Station) == 0 {
+ continue
+ }
+ stationMatch := false
+ for _, sid := range r.Station {
+ if sid == stationDefID {
+ stationMatch = true
+ break
+ }
+ }
+ if !stationMatch {
+ continue
+ }
+ if !r.HasAllItemsQty(p.CountItem) {
+ continue
+ }
+ skillName := player.SkillName(r.EffectiveSkill())
+ if p.Level(skillName) < r.Level {
+ continue
+ }
+ if r.Tool != "" && !g.hasToolType(p, r.Tool) {
+ continue
+ }
+ if stationDefID == "anvil" && !g.hasToolType(p, "hammer") {
+ continue
+ }
+ results = append(results, r)
+ }
+ return results
+}
+
+func (g *Game) showStationRecipeMenu(sess *net.Session, p *player.Player, recipes []action.RecipeDef, stationName string) {
+ if len(recipes) == 0 {
+ sess.WriteLine(fmt.Sprintf("You don't have anything you can make at the %s.", stationName))
+ return
+ }
+ if len(recipes) == 1 {
+ g.promptHowMany(sess, recipes[0].ID)
+ return
+ }
+ needTypes := false
+ for i := range recipes {
+ for j := i + 1; j < len(recipes); j++ {
+ if recipes[i].Type != recipes[j].Type {
+ needTypes = true
+ break
+ }
+ }
+ if needTypes {
+ break
+ }
+ }
+ menu := recipeMenuData(recipeDefIDs(recipes))
+ sess.PendingMenu = menu
+ sess.State = net.StateRecipeChoice
+ var names []string
+ for _, r := range recipes {
+ name := g.recipeName(sess, &r)
+ if needTypes {
+ typeLabel := strings.ToUpper(r.Type[:1]) + r.Type[1:]
+ names = append(names, fmt.Sprintf("%s (%s)", name, typeLabel))
+ } else {
+ names = append(names, name)
+ }
+ }
+ g.showMenuTable(sess, fmt.Sprintf("What would you like to make at the %s?", stationName), names)
}
func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, itemBName string) {
@@ -79,7 +212,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName,
objSt := &objInstances[0]
def, _ := g.ObjectStore.Load(objSt.DefID)
if def == nil {
- g.StartAction(sess, "use", itemBName)
+ g.startAction(sess, "use", itemBName)
return
}
@@ -109,7 +242,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName,
g.promptHowMany(sess, filtered[0].ID)
return
}
- sess.PendingMenu = recipeMenuData(filtered)
+ sess.PendingMenu = recipeMenuData(recipeDefIDs(filtered))
sess.State = net.StateRecipeChoice
var names []string
for _, r := range filtered {
@@ -131,7 +264,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName,
g.promptHowMany(sess, recipes[0].ID)
return
}
- sess.PendingMenu = recipeMenuData(recipes)
+ sess.PendingMenu = recipeMenuData(recipeDefIDs(recipes))
sess.State = net.StateRecipeChoice
var names []string
for _, r := range recipes {
@@ -157,7 +290,7 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName,
return
}
- g.StartAction(sess, "use", itemBName)
+ g.startAction(sess, "use", itemBName)
return
}
@@ -213,12 +346,12 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName,
if len(craftRecipes) > 0 {
var available []action.RecipeDef
for _, r := range craftRecipes {
- if g.canDoCraftRecipe(p, &r) {
+ if g.canDoGenericRecipe(p, &r, player.Crafting) {
available = append(available, r)
}
}
if len(available) == 1 {
- g.startCraftRecipe(sess, p, &available[0], 0)
+ g.startGenericRecipe(sess, p, &available[0], 0, "last_craft")
return
}
if len(available) > 1 {
diff --git a/internal/game/cmd_walk.go b/internal/game/cmd_walk.go
index 2104782..eca608f 100644
--- a/internal/game/cmd_walk.go
+++ b/internal/game/cmd_walk.go
@@ -11,7 +11,7 @@ import (
)
func (g *Game) doWalk(sess *net.Session, args []string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
if len(args) == 0 {
sess.WriteLine("Walk where?")
return
@@ -87,7 +87,7 @@ func (g *Game) startWalk(sess *net.Session, p *player.Player, dirs []string) {
return
}
- g.CancelAction(p)
+ g.cancelAction(p)
p.WalkSequence = dirs
}
@@ -104,7 +104,7 @@ func (g *Game) advanceWalk(sess *net.Session, p *player.Player) {
p.WalkSequence = p.WalkSequence[1:]
oldRoom := p.RoomID
- g.doMove(sess, dir)
+ g.doMove(sess, dir, 2.0)
if p.MoveTicks == 0 && p.RoomID == oldRoom {
p.WalkSequence = nil
diff --git a/internal/game/cmd_wear.go b/internal/game/cmd_wear.go
index 692a3e4..36765e8 100644
--- a/internal/game/cmd_wear.go
+++ b/internal/game/cmd_wear.go
@@ -26,7 +26,7 @@ func (g *Game) checkRequirements(p *player.Player, def *object.ItemDef) string {
}
func (g *Game) doWear(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
lower := strings.ToLower(strings.TrimSpace(input))
if lower == "all" {
@@ -39,7 +39,7 @@ func (g *Game) doWear(sess *net.Session, input string) {
return
}
- g.CancelAction(p)
+ g.cancelAction(p)
qty, itemName := parseQty(input)
@@ -168,8 +168,8 @@ func (g *Game) unequipAmmo(p *player.Player) {
}
func (g *Game) doWearAll(sess *net.Session) {
- p := sess.Player.(*player.Player)
- g.CancelAction(p)
+ p := sess.Player
+ g.cancelAction(p)
bestPerSlot := make(map[object.EquipSlot]struct {
itemID string
diff --git a/internal/game/color.go b/internal/game/color.go
index c3b3d83..962728a 100644
--- a/internal/game/color.go
+++ b/internal/game/color.go
@@ -5,11 +5,10 @@ import (
"thehouseoficarus/internal/config"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/object"
- "thehouseoficarus/internal/player"
)
func (g *Game) colorMode(sess *net.Session) string {
- if p, ok := sess.Player.(*player.Player); ok && p != nil {
+ if p := sess.Player; p != nil {
return p.OptionString("color")
}
return "none"
diff --git a/internal/game/construction.go b/internal/game/construction.go
new file mode 100644
index 0000000..06713cd
--- /dev/null
+++ b/internal/game/construction.go
@@ -0,0 +1,74 @@
+package game
+
+import (
+ "fmt"
+
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) processSawmill(sess *net.Session, p *player.Player) {
+ logTypes := map[string]struct {
+ plankID string
+ cost int
+ name string
+ }{
+ "logs": {"planks", 5, "regular planks"},
+ "oak_logs": {"oak_planks", 10, "oak planks"},
+ "teak_logs": {"teak_planks", 20, "teak planks"},
+ "mahogany_logs": {"mahogany_planks", 40, "mahogany planks"},
+ }
+
+ totalCost := 0
+ totalPlanks := 0
+ type plankResult struct {
+ plankID string
+ qty int
+ }
+
+ var results []plankResult
+
+ for logID, info := range logTypes {
+ qty := p.CountItem(logID)
+ if qty == 0 {
+ continue
+ }
+ cost := qty * info.cost
+ totalCost += cost
+ totalPlanks += qty
+ results = append(results, plankResult{info.plankID, qty})
+ p.RemoveItem(logID, qty)
+ }
+
+ if totalPlanks == 0 {
+ sess.WriteLine("You don't have any logs to process.")
+ return
+ }
+
+ if p.Credits < totalCost {
+ sess.WriteLine(fmt.Sprintf("You need %d credits to process all your logs (you have %d).", totalCost, p.Credits))
+ return
+ }
+
+ p.Credits -= totalCost
+
+ processed := 0
+ for _, r := range results {
+ for i := 0; i < r.qty; i++ {
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot == -1 {
+ g.World.AddGroundItem(p.RoomID, r.plankID, r.qty-i)
+ break
+ }
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: r.plankID, Quantity: 1})
+ processed++
+ }
+ }
+
+ g.AccountStore.SaveCharacter(p)
+
+ sess.WriteLine(fmt.Sprintf("The sawmill operator processes your logs into %d planks for %d credits.", processed, totalCost))
+ if processed < totalPlanks {
+ sess.WriteLine(fmt.Sprintf("Your inventory is full. The remaining %d planks fall to the ground.", totalPlanks-processed))
+ }
+}
diff --git a/internal/game/death.go b/internal/game/death.go
new file mode 100644
index 0000000..d2c1b73
--- /dev/null
+++ b/internal/game/death.go
@@ -0,0 +1,66 @@
+package game
+
+import (
+ "sort"
+
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) dropItemsOnDeath(p *player.Player) {
+ roomID := p.RoomID
+
+ if p.Credits > 0 {
+ g.World.AddGroundItem(roomID, "credits", p.Credits)
+ p.Credits = 0
+ }
+
+ var items []deathDrop
+
+ for slot, inv := range p.Inventory {
+ if inv == nil || inv.Quantity <= 0 {
+ continue
+ }
+ val := 0
+ if def, err := g.ItemStore.Load(inv.ItemID); err == nil {
+ val = def.Value * inv.Quantity
+ }
+ items = append(items, deathDrop{
+ itemID: inv.ItemID,
+ quantity: inv.Quantity,
+ totalVal: val,
+ invSlot: slot,
+ })
+ }
+
+ for eqSlot, itemID := range p.Equipment {
+ val := 0
+ if def, err := g.ItemStore.Load(itemID); err == nil {
+ val = def.Value
+ }
+ items = append(items, deathDrop{
+ itemID: itemID,
+ quantity: 1,
+ totalVal: val,
+ isEquip: true,
+ equipSlot: eqSlot,
+ })
+ }
+
+ if len(items) <= 3 {
+ return
+ }
+
+ sort.Slice(items, func(i, j int) bool {
+ return items[i].totalVal > items[j].totalVal
+ })
+
+ for i := 3; i < len(items); i++ {
+ it := items[i]
+ if it.isEquip {
+ delete(p.Equipment, it.equipSlot)
+ } else {
+ p.SetInvSlot(it.invSlot, nil)
+ }
+ g.World.AddGroundItem(roomID, it.itemID, it.quantity)
+ }
+}
diff --git a/internal/game/equip_stats.go b/internal/game/equip_stats.go
index 559671f..02b8348 100644
--- a/internal/game/equip_stats.go
+++ b/internal/game/equip_stats.go
@@ -29,3 +29,13 @@ func (g *Game) playerEquipBonuses(p *player.Player) object.ItemStats {
}
return totals
}
+
+func (g *Game) playerWeaponSpeed(p *player.Player) float64 {
+ if itemID, ok := p.Equipment[object.SlotMainHand]; ok {
+ def, err := g.ItemStore.Load(itemID)
+ if err == nil && def.Speed > 0 {
+ return def.Speed
+ }
+ }
+ return 5.0
+}
diff --git a/internal/game/game.go b/internal/game/game.go
index 03300a4..b8eadce 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -11,6 +11,7 @@ import (
"thehouseoficarus/internal/combat"
"thehouseoficarus/internal/config"
"thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/game/hacking"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/object"
"thehouseoficarus/internal/player"
@@ -39,25 +40,23 @@ type Game struct {
ItemStore *object.ItemStore
AccountStore *player.AccountStore
MobStore *world.MobStore
- BehaviorStore *action.Store
RecipeStore *action.RecipeStore
CourseStore *CourseStore
Hub *net.Hub
Ticks *engine.Engine
WorldFlags map[string]any
ColorConfig *config.ColorsConfig
- dataDir string
+ DataDir string
restTimers map[string]uint64
charsMu sync.Mutex
loggedInChars map[string]*net.Session
- combatPadWidth int
freeQueue map[string][]QueuedCommand
activeQueue map[string]*QueuedCommand
consumeQueue map[string]*QueuedCommand
pendingDepletions []pendingDepletion
guardWatchTimers map[string]int
farmTickCounter int
- hackingStates map[string]*HackingSession
+ hackingStates map[string]*hacking.Session
}
func New(dataDir string, colorConfig *config.ColorsConfig) *Game {
@@ -67,13 +66,12 @@ func New(dataDir string, colorConfig *config.ColorsConfig) *Game {
ItemStore: object.NewItemStore(dataDir),
AccountStore: player.NewAccountStore(dataDir),
MobStore: world.NewMobStore(dataDir),
- BehaviorStore: action.NewStore(dataDir),
RecipeStore: action.NewRecipeStore(dataDir),
CourseStore: NewCourseStore(dataDir),
Ticks: engine.New(),
WorldFlags: make(map[string]any),
ColorConfig: colorConfig,
- dataDir: dataDir,
+ DataDir: dataDir,
restTimers: make(map[string]uint64),
loggedInChars: make(map[string]*net.Session),
freeQueue: make(map[string][]QueuedCommand),
@@ -81,16 +79,17 @@ func New(dataDir string, colorConfig *config.ColorsConfig) *Game {
consumeQueue: make(map[string]*QueuedCommand),
pendingDepletions: nil,
guardWatchTimers: make(map[string]int),
- hackingStates: make(map[string]*HackingSession),
+ hackingStates: make(map[string]*hacking.Session),
}
g.CourseStore.LoadAll()
+ g.LoadMods()
return g
}
func (g *Game) SetHub(hub *net.Hub) {
g.Hub = hub
hub.OnRemove(func(sess *net.Session) {
- if p, ok := sess.Player.(*player.Player); ok {
+ if p := sess.Player; p != nil {
p.DeactivateAllTechs()
g.charsMu.Lock()
delete(g.loggedInChars, p.Name)
@@ -150,44 +149,6 @@ func (g *Game) HandleSession(sess *net.Session, input string) {
}
}
-func classifyCommand(cmd string) CommandClass {
- switch cmd {
- case "say", "score", "sc", "inventory", "i", "inv",
- "look", "l", "exits", "help",
- "map", "option", "options", "alias", "unalias",
- "description", "desc", "queued", "color", "colors",
- "colortable", "prompt", "style", "stats",
- "tech", "t", "sneak",
- "autocast", "auto", "mods", "modlist",
- "task", "inspect":
- return ClassInstant
- case "equip", "eq", "equipment", "wear", "wield", "remove", "unwear", "unwield":
- return ClassFree
- 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", "cook", "smelt", "smith", "craft", "mix",
- "id", "identify",
- "steal", "thieve",
- "trigger", "cast",
- "plant", "harvest", "rake", "water", "cure",
- "bank", "construct", "make":
- return ClassActive
- case "jack", "jackin":
- return ClassActive
- case "eat", "fletch", "clean", "drink":
- return ClassFree
- }
- if _, ok := verbAliases[cmd]; ok {
- return ClassActive
- }
- if obstacleVerbs[cmd] {
- return ClassActive
- }
- return ClassUnknown
-}
-
func (g *Game) writePrompt(sess *net.Session) {
sess.Write("\r\n" + g.promptStr(sess))
}
@@ -240,7 +201,7 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) {
return
}
- p, _ := sess.Player.(*player.Player)
+ p := sess.Player
if p == nil {
return
}
@@ -276,363 +237,60 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) {
}
}
-func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawInput string) {
- p, _ := sess.Player.(*player.Player)
-
- switch cmd {
- case "get", "take", "grab", "pick":
- if len(args) == 0 {
- sess.WriteLine("Get what?")
- } else if args[0] == "all" {
- if len(args) == 1 {
- g.doGetAll(sess)
- } else {
- g.doGetAllNamed(sess, strings.Join(args[1:], " "))
- }
- } else {
- g.doGet(sess, strings.Join(args, " "))
- }
- case "drop":
- if len(args) == 0 {
- sess.WriteLine("Drop what?")
- } else if args[0] == "all" {
- if len(args) == 1 {
- g.doDropAll(sess)
- } else {
- g.doDropAllNamed(sess, strings.Join(args[1:], " "))
- }
- } else {
- g.doDrop(sess, strings.Join(args, " "))
- }
- case "attack", "kill":
- if len(args) == 0 {
- target := g.resolveDefaultMob(p.RoomID)
- if target == "" {
- sess.WriteLine("Attack what?")
- break
- }
- g.doAttack(sess, target)
- return
- } else {
- g.doAttack(sess, strings.Join(args, " "))
- return
- }
- case "style":
- if len(args) == 0 {
- g.doStyle(sess, "")
- } else {
- g.doStyle(sess, args[0])
- }
- case "look", "l":
- if len(args) == 0 {
- g.doLook(sess)
- } else {
- g.doLookTarget(sess, strings.Join(args, " "))
- }
- case "north", "n", "south", "s", "east", "e", "west", "w", "up", "u", "down", "d":
- g.doMove(sess, cmd)
- case "say":
- if len(args) == 0 {
- sess.WriteLine("Say what?")
- } else {
- msgStart := strings.Index(strings.ToLower(rawInput), "say ") + 4
- if msgStart >= 4 && msgStart < len(rawInput) {
- g.doSay(sess, rawInput[msgStart:])
- } else {
- g.doSay(sess, strings.Join(args, " "))
- }
- }
- case "sc", "score":
- g.doScore(sess)
- case "task":
- g.doTask(sess)
- case "stats":
- g.doStats(sess)
- case "tech", "t":
- g.doTech(sess, strings.Join(args, " "))
- case "i", "inv", "inventory":
- g.doInventory(sess)
- case "quit":
- g.doQuit(sess)
- return
- case "description", "desc":
- g.doDescription(sess)
- case "option", "options":
- g.doOption(sess, strings.Join(args, " "))
- case "color", "colors":
- g.doColor(sess, strings.Join(args, " "))
- case "colortable":
- g.doColortable(sess)
- case "prompt":
- if len(args) == 0 {
- g.doPrompt(sess, "")
- } else {
- msgStart := strings.Index(strings.ToLower(rawInput), "prompt ") + 7
- if msgStart >= 7 && msgStart < len(rawInput) {
- g.doPrompt(sess, rawInput[msgStart:])
- } else {
- g.doPrompt(sess, strings.Join(args, " "))
- }
- }
- case "exits":
- g.doExits(sess)
- case "map":
- g.doMap(sess)
- case "queued":
- g.doQueued(sess)
- case "help":
- if len(args) == 0 {
- g.doHelp(sess, "")
- } else {
- g.doHelp(sess, strings.Join(args, " "))
- }
- case "mine", "chop", "fish", "cut", "pull", "push":
- g.CancelAction(p)
- 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
- } else {
- g.StartAction(sess, cmd, strings.Join(args, " "))
- return
- }
- case "use":
- if g.tryFinishingBlow(sess, strings.Join(args, " ")) {
- return
- }
- g.doUse(sess, strings.Join(args, " "))
- return
- case "cook":
- g.doCook(sess, strings.Join(args, " "))
- return
- case "smelt":
- g.doSmelt(sess, strings.Join(args, " "))
- return
- case "smith":
- g.doSmith(sess, strings.Join(args, " "))
- return
- case "craft":
- g.doCraft(sess, strings.Join(args, " "))
- return
- case "eat":
- g.doEat(sess, strings.Join(args, " "))
- return
- case "drink":
- g.doDrink(sess, args)
- return
- case "fletch":
- g.doFletch(sess, strings.Join(args, " "))
- return
- case "clean":
- g.doClean(sess, strings.Join(args, " "))
- return
- case "mix":
- g.doMix(sess, strings.Join(args, " "))
- return
- case "id", "identify":
- g.doIdentify(sess, strings.Join(args, " "))
- return
- case "autocast", "auto":
- g.doAutocast(sess, strings.Join(args, " "))
- case "mods", "modlist":
- g.doMods(sess)
- case "sneak":
- g.doSneak(sess)
- case "trigger", "cast":
- g.doTrigger(sess, strings.Join(args, " "))
- return
- case "talk", "speak", "ask":
- g.CancelAction(p)
- if len(args) == 0 {
- sess.WriteLine("Talk to whom?")
- } else {
- g.StartAction(sess, "talk", strings.Join(args, " "))
- return
- }
- case "burn":
- g.CancelAction(p)
- g.doBurn(sess, p, strings.Join(args, " "))
- return
- case "stoke":
- g.CancelAction(p)
- g.doStoke(sess, p, strings.Join(args, " "))
- return
- case "alias":
- g.doAlias(sess, args)
- return
- case "unalias":
- g.doUnalias(sess, args)
- return
- case "search":
- g.CancelAction(p)
- if len(args) == 0 {
- sess.WriteLine("Search what?")
- } else {
- g.doSearch(sess, strings.Join(args, " "))
- }
- return
- case "steal", "thieve":
- g.CancelAction(p)
- if len(args) == 0 {
- g.doSteal(sess, "")
- } else {
- g.doSteal(sess, strings.Join(args, " "))
- }
- return
- case "plant":
- g.CancelAction(p)
- if len(args) == 0 {
- sess.WriteLine("Plant what?")
- } else {
- g.doPlant(sess, strings.Join(args, " "))
- }
- return
- case "harvest":
- g.CancelAction(p)
- if len(args) == 0 {
- g.doHarvest(sess, "")
- } else {
- g.doHarvest(sess, strings.Join(args, " "))
- }
- return
- case "rake":
- g.CancelAction(p)
- if len(args) == 0 {
- g.doRake(sess, "")
- } else {
- g.doRake(sess, strings.Join(args, " "))
- }
- return
- case "water":
- g.CancelAction(p)
- if len(args) == 0 {
- g.doWater(sess, "")
- } else {
- g.doWater(sess, strings.Join(args, " "))
- }
- return
- case "cure":
- g.CancelAction(p)
- if len(args) == 0 {
- g.doCure(sess, "")
- } else {
- g.doCure(sess, strings.Join(args, " "))
- }
- return
- case "inspect":
- if len(args) == 0 {
- g.doInspect(sess, "")
- } else {
- g.doInspect(sess, strings.Join(args, " "))
- }
- case "walk":
- g.doWalk(sess, args)
- return
- case "bank":
- g.doBank(sess)
- return
- case "construct", "make":
- g.doConstruct(sess, strings.Join(args, " "))
- return
- case "jack", "jackin":
- g.CancelAction(p)
- g.doJack(sess, strings.Join(args, " "))
- return
- case "eq", "equipment", "equip", "wear", "wield":
- g.doWear(sess, strings.Join(args, " "))
- return
- case "remove", "unwear", "unwield":
- g.doRemove(sess, strings.Join(args, " "))
- return
- default:
- if obstacleVerbs[cmd] {
- g.doObstacle(sess, cmd)
- return
- }
- 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.")
- }
- }
-}
-
func (g *Game) ProcessQueuedCommands() {
if g.Hub == nil {
return
}
for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil || p.ActionState == nil {
- continue
- }
- as, ok := p.ActionState.(*ActionState)
- if !ok {
+ p := sess.Player
+ if p == nil {
continue
}
- switch as.Type {
- case ActionGathering, ActionCombating, ActionUsing, ActionTalking,
- ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionProducing,
- ActionStealing, ActionPlanting, ActionHarvesting, ActionRaking, ActionWatering, ActionCuring,
- ActionTraversing:
- default:
- if as.Type != ActionMoving || p.MoveTicks <= 0 {
+
+ if p.ActionState != nil {
+ as, ok := p.ActionState.(*ActionState)
+ if !ok {
p.ActionState = nil
+ } else {
+ switch as.Type {
+ case ActionGathering, ActionCombating, ActionUsing, ActionTalking,
+ ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionProducing,
+ ActionStealing, ActionPlanting, ActionHarvesting, ActionRaking, ActionWatering, ActionCuring,
+ ActionTraversing:
+ default:
+ if as.Type != ActionMoving || p.MoveTicks <= 0 {
+ p.ActionState = nil
+ }
+ }
}
}
- }
- for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil {
- continue
- }
if p.BackgroundAction == nil {
p.BackgroundActionState = nil
}
- }
- for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil {
- continue
- }
cmds := g.freeQueue[p.Name]
for _, qc := range cmds {
g.cancelRest(p.Name)
- parts := strings.Fields(qc.Command + " " + qc.Args)
+ raw := qc.Command
+ if qc.Args != "" {
+ raw += " " + qc.Args
+ }
+ parts := strings.Fields(raw)
if len(parts) > 0 {
- g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args)
+ g.executeCommand(qc.Session, parts[0], parts[1:], raw)
}
g.writePrompt(qc.Session)
}
delete(g.freeQueue, p.Name)
+
+ if len(p.WalkSequence) > 0 {
+ g.advanceWalk(sess, p)
+ if len(p.WalkSequence) == 0 && p.MoveTicks == 0 {
+ g.writePrompt(sess)
+ }
+ }
}
var actives []QueuedCommand
@@ -644,16 +302,20 @@ 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 {
+ raw := qc.Command
+ if qc.Args != "" {
+ raw += " " + qc.Args
+ }
+ parts := strings.Fields(raw)
+ p := qc.Session.Player
+ if p == nil {
continue
}
if p.MoveTicks > 0 {
p.ClearMoveState()
}
if len(parts) > 0 {
- g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args)
+ g.executeCommand(qc.Session, parts[0], parts[1:], raw)
}
isBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil || p.MoveTicks > 0
_, isResting := g.restTimers[p.Name]
@@ -663,48 +325,12 @@ func (g *Game) ProcessQueuedCommands() {
}
g.activeQueue = make(map[string]*QueuedCommand)
- for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil || len(p.WalkSequence) == 0 {
- continue
- }
- g.advanceWalk(sess, p)
- if len(p.WalkSequence) == 0 && p.MoveTicks == 0 {
- g.writePrompt(sess)
- }
- }
-
g.flushPendingDepletions()
}
type pendingDepletion struct {
instanceKey string
- behaviorID string
+ objDefID string
targetName string
playerNames []string
}
-
-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/hacking.go b/internal/game/hacking.go
index dfefd5c..3457728 100644
--- a/internal/game/hacking.go
+++ b/internal/game/hacking.go
@@ -4,67 +4,14 @@ import (
"fmt"
"strings"
+ "thehouseoficarus/internal/game/hacking"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
)
-type HackingMinigame interface {
- Init(level int) string
- HandleInput(input string) (output string, done bool, won bool)
- Name() string
-}
-
-type XPBonuser interface {
- BonusXP() int
-}
-
-type HackingSession struct {
- Minigame HackingMinigame
- TerminalID string
- Level int
- ReqLevel int
- Started bool
-}
-
-type TerminalDef struct {
- ObjectID string
- Level int
- Minigame func() HackingMinigame
- BaseXPWin int
- BaseXPLose int
- Name string
-}
-
-var terminalDefs = map[string]TerminalDef{
- "terminal_basic": {
- ObjectID: "terminal_basic",
- Level: 1,
- Minigame: func() HackingMinigame { return NewWumpusGame() },
- BaseXPWin: 50,
- BaseXPLose: 10,
- Name: "Hunt the Wumpus",
- },
- "terminal_advanced": {
- ObjectID: "terminal_advanced",
- Level: 20,
- Minigame: func() HackingMinigame { return NewMastermindGame() },
- BaseXPWin: 150,
- BaseXPLose: 30,
- Name: "Code Breaker",
- },
- "terminal_secure": {
- ObjectID: "terminal_secure",
- Level: 40,
- Minigame: func() HackingMinigame { return NewLiarsDiceGame() },
- BaseXPWin: 300,
- BaseXPLose: 50,
- Name: "Signal Bluff",
- },
-}
-
func (g *Game) handleHackingInput(sess *net.Session, input string) {
- p, ok := sess.Player.(*player.Player)
- if !ok {
+ p := sess.Player
+ if p == nil {
sess.State = net.StateGame
return
}
@@ -105,36 +52,33 @@ func (g *Game) endHacking(sess *net.Session, p *player.Player, completed bool, w
return
}
- tDef := terminalDefs[hs.TerminalID]
+ tDef := hacking.Terminals[hs.TerminalID]
var xp int
if completed && won {
- xp = hackingXP(tDef.BaseXPWin, hs.Level, hs.ReqLevel)
- sess.WriteLine(fmt.Sprintf("\nConnection terminated. Contract complete."))
+ xp = hacking.CalcXP(tDef.BaseXPWin, hs.Level, hs.ReqLevel)
+ sess.WriteLine("\nConnection terminated. Contract complete.")
} else if completed {
- xp = hackingXP(tDef.BaseXPLose, hs.Level, hs.ReqLevel)
- sess.WriteLine(fmt.Sprintf("\nConnection lost."))
+ xp = hacking.CalcXP(tDef.BaseXPLose, hs.Level, hs.ReqLevel)
+ sess.WriteLine("\nConnection lost.")
} else if hs.Started {
- xp = hackingXP(tDef.BaseXPLose, hs.Level, hs.ReqLevel)
+ xp = hacking.CalcXP(tDef.BaseXPLose, hs.Level, hs.ReqLevel)
sess.WriteLine("\nYou jack out of the terminal.")
} else {
sess.WriteLine("\nYou jack out of the terminal.")
}
if won {
- if b, ok := hs.Minigame.(XPBonuser); ok {
+ if b, ok := hs.Minigame.(hacking.XPBonuser); ok {
bonus := b.BonusXP()
if bonus > 0 {
- xp += hackingXP(bonus, hs.Level, hs.ReqLevel)
+ xp += hacking.CalcXP(bonus, hs.Level, hs.ReqLevel)
}
}
}
if xp > 0 {
- if newLevel := p.AddSkillXP(player.Hacking, xp); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up",
- fmt.Sprintf("*** You are now level %d hacking! ***", newLevel)))
- }
+ g.awardSkillXP(sess, p, player.Hacking, xp)
if p.OptionBool("xp_drops") {
sess.WriteLine(g.colorize(sess, "xp",
fmt.Sprintf("(+%dxp %s)", xp, player.SkillAbbr[player.Hacking])))
@@ -147,11 +91,3 @@ func (g *Game) endHacking(sess *net.Session, p *player.Player, completed bool, w
sess.State = net.StateGame
g.writePrompt(sess)
}
-
-func hackingXP(baseXP int, playerLevel int, reqLevel int) int {
- multiplier := 1.0 + 0.01*float64(playerLevel-reqLevel)
- if multiplier < 1.0 {
- multiplier = 1.0
- }
- return int(float64(baseXP) * multiplier)
-}
diff --git a/internal/game/hacking/hacking.go b/internal/game/hacking/hacking.go
new file mode 100644
index 0000000..f022ee1
--- /dev/null
+++ b/internal/game/hacking/hacking.go
@@ -0,0 +1,63 @@
+package hacking
+
+type Minigame interface {
+ Init(level int) string
+ HandleInput(input string) (output string, done bool, won bool)
+ Name() string
+}
+
+type XPBonuser interface {
+ BonusXP() int
+}
+
+type Session struct {
+ Minigame Minigame
+ TerminalID string
+ Level int
+ ReqLevel int
+ Started bool
+}
+
+type TerminalDef struct {
+ ObjectID string
+ Level int
+ Minigame func() Minigame
+ BaseXPWin int
+ BaseXPLose int
+ Name string
+}
+
+var Terminals = map[string]TerminalDef{
+ "terminal_basic": {
+ ObjectID: "terminal_basic",
+ Level: 1,
+ Minigame: func() Minigame { return NewWumpusGame() },
+ BaseXPWin: 50,
+ BaseXPLose: 10,
+ Name: "Hunt the Wumpus",
+ },
+ "terminal_advanced": {
+ ObjectID: "terminal_advanced",
+ Level: 20,
+ Minigame: func() Minigame { return NewMastermindGame() },
+ BaseXPWin: 150,
+ BaseXPLose: 30,
+ Name: "Code Breaker",
+ },
+ "terminal_secure": {
+ ObjectID: "terminal_secure",
+ Level: 40,
+ Minigame: func() Minigame { return NewLiarsDiceGame() },
+ BaseXPWin: 300,
+ BaseXPLose: 50,
+ Name: "Signal Bluff",
+ },
+}
+
+func CalcXP(baseXP int, playerLevel int, reqLevel int) int {
+ multiplier := 1.0 + 0.01*float64(playerLevel-reqLevel)
+ if multiplier < 1.0 {
+ multiplier = 1.0
+ }
+ return int(float64(baseXP) * multiplier)
+}
diff --git a/internal/game/hacking_liars_dice.go b/internal/game/hacking/hacking_liars_dice.go
index 12f76c1..bcfbd40 100644
--- a/internal/game/hacking_liars_dice.go
+++ b/internal/game/hacking/hacking_liars_dice.go
@@ -1,4 +1,4 @@
-package game
+package hacking
import (
"fmt"
diff --git a/internal/game/hacking_mastermind.go b/internal/game/hacking/hacking_mastermind.go
index 3ab7cca..8d4eb1c 100644
--- a/internal/game/hacking_mastermind.go
+++ b/internal/game/hacking/hacking_mastermind.go
@@ -1,4 +1,4 @@
-package game
+package hacking
import (
"fmt"
diff --git a/internal/game/hacking_wumpus.go b/internal/game/hacking/hacking_wumpus.go
index 600b1be..91b9243 100644
--- a/internal/game/hacking_wumpus.go
+++ b/internal/game/hacking/hacking_wumpus.go
@@ -1,4 +1,4 @@
-package game
+package hacking
import (
"fmt"
diff --git a/internal/game/help.go b/internal/game/help.go
index f9d822c..03e2b8c 100644
--- a/internal/game/help.go
+++ b/internal/game/help.go
@@ -7,7 +7,6 @@ import (
"strings"
"thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
"gopkg.in/yaml.v3"
)
@@ -26,36 +25,54 @@ type cmdEntry struct {
var commandList = []cmdEntry{
{"alias", "Instant", "Create command shortcuts"},
{"attack / kill", "Active", "Attack a mob"},
+ {"autotrigger / auto", "Instant", "Set a module to auto-trigger"},
+ {"bank", "Active", "Open bank interface"},
{"burn", "Active", "Start a fire"},
{"chop / cut", "Active", "Chop trees (Woodcutting)"},
+ {"clean", "Free", "Clean grimy herbs (Pharmacy)"},
{"color / colors", "Instant", "Customize display colors"},
{"colortable", "Instant", "Display color reference chart"},
+ {"construct / make", "Active", "Build furniture (Construction)"},
{"cook", "Active", "Cook raw food on a fire or range"},
+ {"craft", "Active", "Craft jewelry (Crafting)"},
{"description / desc", "Instant", "Set your character description"},
+ {"drink", "Free", "Drink a potion"},
{"drop", "Active", "Drop items to the ground"},
{"eat", "Free", "Eat food to restore hitpoints"},
{"equipment / eq", "Instant", "Show equipped items"},
{"exits", "Instant", "List available exits"},
+ {"farm", "Active", "Farm crops (Farming)"},
{"fish", "Active", "Fish at fishing spots (Fishing)"},
+ {"fletch", "Free", "Fletch logs into bows (Fletching)"},
{"get / take / pick", "Active", "Pick up items from the ground"},
{"help", "Instant", "Show help topics"},
+ {"id / identify", "Active", "Identify herbs (Pharmacy)"},
{"inventory / i / inv", "Instant", "Show your inventory"},
+ {"jack / jackin", "Active", "Jack into a terminal (Hacking)"},
{"look / l", "Instant", "Look around or examine things"},
{"map", "Instant", "Display an ASCII map of the area"},
{"mine", "Active", "Mine rocks (Mining)"},
+ {"mix", "Active", "Mix potions (Pharmacy)"},
+ {"mods / modlist", "Instant", "List available science modules"},
{"north / south / east / west / up / down", "Active", "Move in a direction"},
{"option / options", "Instant", "View or change settings"},
{"prompt", "Instant", "Set custom command prompt"},
- {"pull / push", "Active", "Toggle levers and switches"},
+ {"pull / push", "Active", "Interact with objects"},
{"queued", "Instant", "Show pending tick actions"},
{"quit", "Active", "Rest and disconnect"},
{"remove / unwear / unwield", "Free", "Unequip items"},
{"say", "Instant", "Chat with players in your room"},
{"score / sc", "Instant", "View your stats and skills"},
{"search", "Active", "Search items for loot"},
+ {"smelt", "Active", "Smelt ore into bars (Smithing)"},
+ {"smith", "Active", "Smith bars into items (Smithing)"},
+ {"sneak", "Instant", "Toggle sneak mode"},
+ {"steal", "Active", "Steal from mobs (Thieving)"},
{"stoke", "Active", "Add logs to a fire"},
{"style", "Instant", "Change combat style"},
{"talk / speak / ask", "Active", "Talk to NPCs"},
+ {"tech", "Instant", "Toggle technology abilities"},
+ {"trigger", "Active", "Trigger a science module"},
{"unalias", "Instant", "Remove command shortcuts"},
{"use", "Active", "Use an object (crafting)"},
{"walk", "Active", "Pathfind to a room or multi-step walk"},
@@ -90,7 +107,7 @@ func LoadHelp(dataDir string) ([]HelpDef, error) {
func (g *Game) doHelp(sess *net.Session, topic string) {
if topic == "" {
unicode := true
- if p, _ := sess.Player.(*player.Player); p != nil {
+ if p := sess.Player; p != nil {
unicode = p.OptionBool("unicode")
}
@@ -114,7 +131,7 @@ func (g *Game) doHelp(sess *net.Session, topic string) {
return
}
- helps, err := LoadHelp(g.dataDir)
+ helps, err := LoadHelp(g.DataDir)
if err != nil || len(helps) == 0 {
sess.WriteLine("No help available.")
return
diff --git a/internal/game/login_account.go b/internal/game/login_account.go
index 176a1ee..87f8c62 100644
--- a/internal/game/login_account.go
+++ b/internal/game/login_account.go
@@ -17,7 +17,7 @@ func (g *Game) handleAccountName(sess *net.Session, input string) {
}
actualName, err := g.AccountStore.FindAccount(name)
if err == nil {
- sess.Account = &net.AccountEntry{Name: actualName}
+ sess.Account = &player.Account{Name: actualName}
sess.State = net.StatePassword
sess.Conn.SetEcho(false)
sess.Write("Password: ")
@@ -27,7 +27,7 @@ func (g *Game) handleAccountName(sess *net.Session, input string) {
sess.Write("Account name: ")
return
}
- sess.Account = &net.AccountEntry{Name: name}
+ sess.Account = &player.Account{Name: name}
sess.State = net.StateNewAccountPass
sess.Conn.SetEcho(false)
sess.WriteLine("\r\nOh, a new visitor to this rock!")
@@ -63,7 +63,7 @@ func (g *Game) handlePassword(sess *net.Session, input string) {
return
}
sess.Conn.SetEcho(true)
- sess.Account = &net.AccountEntry{
+ sess.Account = &player.Account{
Name: acc.Name,
PasswordHash: acc.PasswordHash,
Characters: acc.Characters,
@@ -144,7 +144,7 @@ func (g *Game) handleNewAccountConfirm(sess *net.Session, input string) {
}
sess.Conn.SetEcho(true)
- sess.Account = &net.AccountEntry{
+ sess.Account = &player.Account{
Name: acc.Name,
PasswordHash: acc.PasswordHash,
Aliases: make(map[string]string),
diff --git a/internal/game/messages.go b/internal/game/messages.go
new file mode 100644
index 0000000..1c9e85a
--- /dev/null
+++ b/internal/game/messages.go
@@ -0,0 +1,13 @@
+package game
+
+const (
+ MsgErrLoadRecipes = "Error loading recipes."
+ MsgNoCombat = "You can't do that during combat!"
+ MsgNoDontHave = "You don't have any '%s'."
+ MsgNeverMind = "Never mind."
+ MsgSomethingWrongObject = "Something is wrong with this object."
+ MsgSomethingWrongThat = "Something is wrong with that."
+ MsgInventoryFull = "Your inventory is too full!"
+ MsgAmbiguous = "That's ambiguous, which one?"
+ MsgWhichOne = "Which one?"
+)
diff --git a/internal/game/player_flags.go b/internal/game/player_flags.go
new file mode 100644
index 0000000..6595cc4
--- /dev/null
+++ b/internal/game/player_flags.go
@@ -0,0 +1,55 @@
+package game
+
+import "thehouseoficarus/internal/player"
+
+func getPlayerFlagInt(p *player.Player, key string) int {
+ if p.Flags == nil {
+ return 0
+ }
+ val, ok := p.Flags[key]
+ if !ok {
+ return 0
+ }
+ switch v := val.(type) {
+ case int:
+ return v
+ case int64:
+ return int(v)
+ case float64:
+ return int(v)
+ }
+ return 0
+}
+
+func getPlayerFlagString(p *player.Player, key string) string {
+ if p.Flags == nil {
+ return ""
+ }
+ val, ok := p.Flags[key]
+ if !ok {
+ return ""
+ }
+ s, _ := val.(string)
+ return s
+}
+
+func setPlayerFlag(p *player.Player, key string, val any) {
+ p.EnsureFlags()
+ p.Flags[key] = val
+}
+
+func intFromFlag(flags map[string]any, key string) int {
+ val, ok := flags[key]
+ if !ok {
+ return 0
+ }
+ switch v := val.(type) {
+ case int:
+ return v
+ case int64:
+ return int(v)
+ case float64:
+ return int(v)
+ }
+ return 0
+}
diff --git a/internal/game/action_production.go b/internal/game/production_core.go
index 788cadb..42f64e1 100644
--- a/internal/game/action_production.go
+++ b/internal/game/production_core.go
@@ -13,202 +13,12 @@ import (
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/object"
"thehouseoficarus/internal/player"
- "thehouseoficarus/internal/world"
+
)
-type recipeEntry struct {
- ItemName string
- Recipe action.RecipeDef
-}
-
-type productionTypeInfo struct {
- ActionType string
- DisplayVerb string
-}
-
-var productionTypes = map[string]productionTypeInfo{
- "cooking": {"cook", "cooking"},
- "smelting": {"smelt", "smelting"},
- "smithing": {"smith", "smithing"},
- "crafting": {"craft", "crafting"},
- "combine": {"combine", "combining"},
- "fletching": {"fletch", "fletching"},
- "pharmacy": {"mix", "mixing"},
- "construction": {"construct", "constructing"},
-}
-
-var productionActionTypes map[string]bool
-
-func init() {
- productionActionTypes = make(map[string]bool)
- for _, pt := range productionTypes {
- productionActionTypes[pt.ActionType] = true
- }
-}
-
-func recipeMenuData(recipes []action.RecipeDef) []map[string]string {
- out := make([]map[string]string, len(recipes))
- for i, r := range recipes {
- out[i] = map[string]string{"recipe_id": r.ID}
- }
- return out
-}
-
-func entryMenuData(entries []recipeEntry) []map[string]string {
- out := make([]map[string]string, len(entries))
- for i, e := range entries {
- out[i] = map[string]string{"recipe_id": e.Recipe.ID}
- }
- return out
-}
-
-func (g *Game) promptHowMany(sess *net.Session, recipeID string) {
- sess.PendingRecipeID = recipeID
- sess.State = net.StateHowMany
- sess.Write("How many (return for all)?: ")
-}
-
-type recipeSelection struct {
- Recipe *action.RecipeDef
- Count int
-}
-
-func (g *Game) resolveRecipeByInput(input string, recipes []action.RecipeDef, lastRecipeID string) (sel recipeSelection, errMsg string) {
- if input == "" {
- if lastRecipeID == "" {
- return sel, "never_mind"
- }
- for i := range recipes {
- if recipes[i].ID == lastRecipeID {
- sel.Recipe = &recipes[i]
- return sel, ""
- }
- }
- return sel, "never_mind"
- }
-
- qty, productName := parseQty(input)
- productName = strings.TrimSpace(productName)
-
- if idx, err := strconv.Atoi(productName); err == nil && idx > 0 && idx <= len(recipes) {
- sel.Recipe = &recipes[idx-1]
- sel.Count = qty
- return sel, ""
- }
-
- matchCount := 0
- for i := range recipes {
- outDef, _ := g.ItemStore.Load(recipes[i].Output)
- name := recipes[i].Output
- if outDef != nil {
- name = outDef.Name
- }
- if world.WordPrefixMatch(productName, name) {
- if matchCount == 0 {
- sel.Recipe = &recipes[i]
- sel.Count = qty
- }
- matchCount++
- }
- }
- if matchCount > 1 {
- return sel, "ambiguous"
- }
- if sel.Recipe == nil {
- return sel, "never_mind"
- }
- return sel, ""
-}
-
-func (g *Game) showMenuTable(sess *net.Session, title string, names []string) {
- p := sess.Player.(*player.Player)
- tbl := &Table{Title: title}
- for i, name := range names {
- tbl.Rows = append(tbl.Rows, []string{fmt.Sprintf("%d", i+1), name})
- }
- unicode := p.OptionBool("unicode")
- sess.WriteLine("")
- for _, line := range tbl.Render(unicode) {
- sess.WriteLine(line)
- }
-}
-
-func (g *Game) handleHowMany(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
- recipeID := sess.PendingRecipeID
- sess.PendingRecipeID = ""
- sess.State = net.StateGame
-
- input = strings.TrimSpace(input)
-
- var count int
- if input == "" {
- count = 0
- } else if n, err := strconv.Atoi(input); err == nil && n > 0 {
- count = n
- } else {
- sess.WriteLine("Never mind.")
- g.reprompt(sess)
- return
- }
-
- var recipe *action.RecipeDef
- if strings.HasPrefix(recipeID, "combine_") {
- itemID := strings.TrimPrefix(recipeID, "combine_")
- itemDef, err := g.ItemStore.Load(itemID)
- if err != nil || len(itemDef.MadeFrom) == 0 {
- g.reprompt(sess)
- return
- }
- recipe = g.buildCombineRecipe(itemDef)
- } else {
- all, err := g.RecipeStore.LoadAll()
- if err != nil {
- g.reprompt(sess)
- return
- }
- for _, r := range all {
- if r.ID == recipeID {
- recipe = &r
- break
- }
- }
- }
-
- if recipe == nil {
- g.reprompt(sess)
- return
- }
-
- g.startProductionFromRecipe(sess, p, recipe, count)
-}
-
-func (g *Game) buildCombineRecipe(def *object.ItemDef) *action.RecipeDef {
- var consume []action.ConsumeEntry
- for _, mf := range def.MadeFrom {
- consume = append(consume, action.ConsumeEntry{
- Items: mf.Items,
- Qty: mf.Qty,
- Byproducts: mf.Byproducts,
- })
- }
- wait := def.Ticks
- if wait <= 0 {
- wait = 2
- }
- return &action.RecipeDef{
- ID: "combine_" + def.ID,
- Type: "combine",
- Wait: wait,
- Consume: consume,
- Output: def.ID,
- Message: fmt.Sprintf("You create %s.", def.Name),
- }
-}
-
func (g *Game) startProduction(sess *net.Session, p *player.Player, recipe *action.RecipeDef, actionType, displayVerb, startMsg, endMsg string, count int) {
- g.CancelAction(p)
- g.CancelBackgroundAction(p)
+ g.cancelAction(p)
+ g.cancelBackgroundAction(p)
skill := recipe.EffectiveSkill()
if skill != "" {
@@ -324,7 +134,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool {
recipe := g.loadProductionRecipe(recipeID)
if recipe == nil {
- g.CancelAction(p)
+ g.cancelAction(p)
return false
}
@@ -370,7 +180,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool {
freeSlot := p.FirstFreeSlot()
if freeSlot == -1 {
sess.WriteLine("Your inventory is too full!")
- g.CancelAction(p)
+ g.cancelAction(p)
return false
}
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Output, Quantity: outputQty})
@@ -427,7 +237,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool {
if endMsg != "" {
sess.WriteLine(fmt.Sprintf("\n%s", endMsg))
}
- g.CancelAction(p)
+ g.cancelAction(p)
return false
}
}
@@ -440,7 +250,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool {
if endMsg != "" {
sess.WriteLine(fmt.Sprintf("\n%s", endMsg))
}
- g.CancelAction(p)
+ g.cancelAction(p)
return false
}
@@ -517,7 +327,7 @@ func (g *Game) handleRecipeChoice(sess *net.Session, input string) {
if name == "" {
continue
}
- if strings.ToLower(name) == lower || world.WordPrefixMatch(input, name) {
+ if strings.ToLower(name) == lower || action.WordPrefixMatch(input, name) {
if matchIdx >= 0 {
sess.WriteLine("That's ambiguous.")
return
@@ -582,13 +392,13 @@ func (g *Game) dispatchMenuEntry(sess *net.Session, entry map[string]string) {
return
}
if barID, ok := entry["bar_id"]; ok {
- p := sess.Player.(*player.Player)
+ p := sess.Player
allRecipes, _ := g.RecipeStore.LoadAll()
g.showSmithTable(sess, p, barID, allRecipes)
return
}
if rid, ok := entry["fletch_recipe_id"]; ok {
- p := sess.Player.(*player.Player)
+ p := sess.Player
allRecipes, _ := g.RecipeStore.LoadAll()
for _, r := range allRecipes {
if r.ID == rid {
@@ -693,7 +503,7 @@ func (g *Game) showProductionTable(sess *net.Session, p *player.Player, recipes
sess.Write(fmt.Sprintf("%s what: ", promptVerb))
}
- sess.PendingMenu = recipeMenuData(recipes)
+ sess.PendingMenu = recipeMenuData(recipeDefIDs(recipes))
sess.PendingSkill = skill
sess.PendingLastFlag = lastFlagKey
sess.PendingBackground = background
@@ -701,7 +511,7 @@ func (g *Game) showProductionTable(sess *net.Session, p *player.Player, recipes
}
func (g *Game) handleProductChoice(sess *net.Session, input string) {
- p := sess.Player.(*player.Player)
+ p := sess.Player
menuData := sess.PendingMenu
skill := sess.PendingSkill
lastFlagKey := sess.PendingLastFlag
@@ -766,9 +576,7 @@ func (g *Game) handleProductChoice(sess *net.Session, input string) {
return
}
- if p.Flags == nil {
- p.Flags = make(map[string]any)
- }
+ p.EnsureFlags()
p.Flags[lastFlagKey] = sel.Recipe.ID
g.AccountStore.SaveCharacter(p)
diff --git a/internal/game/production_menu.go b/internal/game/production_menu.go
new file mode 100644
index 0000000..329110b
--- /dev/null
+++ b/internal/game/production_menu.go
@@ -0,0 +1,209 @@
+package game
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "thehouseoficarus/internal/action"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+)
+
+type recipeEntry struct {
+ ItemName string
+ Recipe action.RecipeDef
+}
+
+type productionTypeInfo struct {
+ ActionType string
+ DisplayVerb string
+}
+
+var productionTypes = map[string]productionTypeInfo{
+ "cooking": {"cook", "cooking"},
+ "smelting": {"smelt", "smelting"},
+ "smithing": {"smith", "smithing"},
+ "crafting": {"craft", "crafting"},
+ "combine": {"combine", "combining"},
+ "fletching": {"fletch", "fletching"},
+ "pharmacy": {"mix", "mixing"},
+ "construction": {"construct", "constructing"},
+}
+
+var productionActionTypes map[string]bool
+
+func init() {
+ productionActionTypes = make(map[string]bool)
+ for _, pt := range productionTypes {
+ productionActionTypes[pt.ActionType] = true
+ }
+}
+
+func recipeMenuData(recipeIDs []string) []map[string]string {
+ out := make([]map[string]string, len(recipeIDs))
+ for i, id := range recipeIDs {
+ out[i] = map[string]string{"recipe_id": id}
+ }
+ return out
+}
+
+func recipeDefIDs(defs []action.RecipeDef) []string {
+ ids := make([]string, len(defs))
+ for i, d := range defs {
+ ids[i] = d.ID
+ }
+ return ids
+}
+
+func entryMenuData(entries []recipeEntry) []map[string]string {
+ out := make([]map[string]string, len(entries))
+ for i, e := range entries {
+ out[i] = map[string]string{"recipe_id": e.Recipe.ID}
+ }
+ return out
+}
+
+func (g *Game) promptHowMany(sess *net.Session, recipeID string) {
+ sess.PendingRecipeID = recipeID
+ sess.State = net.StateHowMany
+ sess.Write("How many (return for all)?: ")
+}
+
+type recipeSelection struct {
+ Recipe *action.RecipeDef
+ Count int
+}
+
+func (g *Game) resolveRecipeByInput(input string, recipes []action.RecipeDef, lastRecipeID string) (sel recipeSelection, errMsg string) {
+ if input == "" {
+ if lastRecipeID == "" {
+ return sel, "never_mind"
+ }
+ for i := range recipes {
+ if recipes[i].ID == lastRecipeID {
+ sel.Recipe = &recipes[i]
+ return sel, ""
+ }
+ }
+ return sel, "never_mind"
+ }
+
+ qty, productName := parseQty(input)
+ productName = strings.TrimSpace(productName)
+
+ if idx, err := strconv.Atoi(productName); err == nil && idx > 0 && idx <= len(recipes) {
+ sel.Recipe = &recipes[idx-1]
+ sel.Count = qty
+ return sel, ""
+ }
+
+ matchCount := 0
+ for i := range recipes {
+ outDef, _ := g.ItemStore.Load(recipes[i].Output)
+ name := recipes[i].Output
+ if outDef != nil {
+ name = outDef.Name
+ }
+ if action.WordPrefixMatch(productName, name) {
+ if matchCount == 0 {
+ sel.Recipe = &recipes[i]
+ sel.Count = qty
+ }
+ matchCount++
+ }
+ }
+ if matchCount > 1 {
+ return sel, "ambiguous"
+ }
+ if sel.Recipe == nil {
+ return sel, "never_mind"
+ }
+ return sel, ""
+}
+
+func (g *Game) showMenuTable(sess *net.Session, title string, names []string) {
+ p := sess.Player
+ tbl := &Table{Title: title}
+ for i, name := range names {
+ tbl.Rows = append(tbl.Rows, []string{fmt.Sprintf("%d", i+1), name})
+ }
+ unicode := p.OptionBool("unicode")
+ sess.WriteLine("")
+ for _, line := range tbl.Render(unicode) {
+ sess.WriteLine(line)
+ }
+}
+
+func (g *Game) handleHowMany(sess *net.Session, input string) {
+ p := sess.Player
+ recipeID := sess.PendingRecipeID
+ sess.PendingRecipeID = ""
+ sess.State = net.StateGame
+
+ input = strings.TrimSpace(input)
+
+ var count int
+ if input == "" {
+ count = 0
+ } else if n, err := strconv.Atoi(input); err == nil && n > 0 {
+ count = n
+ } else {
+ sess.WriteLine("Never mind.")
+ g.reprompt(sess)
+ return
+ }
+
+ var recipe *action.RecipeDef
+ if strings.HasPrefix(recipeID, "combine_") {
+ itemID := strings.TrimPrefix(recipeID, "combine_")
+ itemDef, err := g.ItemStore.Load(itemID)
+ if err != nil || len(itemDef.MadeFrom) == 0 {
+ g.reprompt(sess)
+ return
+ }
+ recipe = g.buildCombineRecipe(itemDef)
+ } else {
+ all, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ g.reprompt(sess)
+ return
+ }
+ for _, r := range all {
+ if r.ID == recipeID {
+ recipe = &r
+ break
+ }
+ }
+ }
+
+ if recipe == nil {
+ g.reprompt(sess)
+ return
+ }
+
+ g.startProductionFromRecipe(sess, p, recipe, count)
+}
+
+func (g *Game) buildCombineRecipe(def *object.ItemDef) *action.RecipeDef {
+ var consume []action.ConsumeEntry
+ for _, mf := range def.MadeFrom {
+ consume = append(consume, action.ConsumeEntry{
+ Items: mf.Items,
+ Qty: mf.Qty,
+ Byproducts: mf.Byproducts,
+ })
+ }
+ wait := def.Ticks
+ if wait <= 0 {
+ wait = 2
+ }
+ return &action.RecipeDef{
+ ID: "combine_" + def.ID,
+ Type: "combine",
+ Wait: wait,
+ Consume: consume,
+ Output: def.ID,
+ Message: fmt.Sprintf("You create %s.", def.Name),
+ }
+}
diff --git a/internal/game/prompt.go b/internal/game/prompt.go
index c3768fe..2ecf367 100644
--- a/internal/game/prompt.go
+++ b/internal/game/prompt.go
@@ -12,7 +12,7 @@ import (
func (g *Game) promptStr(sess *net.Session) string {
prompt := "> "
- if p, ok := sess.Player.(*player.Player); ok && p != nil {
+ if p := sess.Player; p != nil {
if p.Prompt != "" {
prompt = p.Prompt
}
@@ -28,8 +28,8 @@ func (g *Game) expandPromptColors(sess *net.Session, text string) string {
}
func (g *Game) expandPromptVars(sess *net.Session, text string) string {
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil {
+ p := sess.Player
+ if p == nil {
return text
}
diff --git a/internal/game/science.go b/internal/game/science.go
index bf9a961..8c7f0af 100644
--- a/internal/game/science.go
+++ b/internal/game/science.go
@@ -2,9 +2,13 @@ package game
import (
"fmt"
+ "os"
+ "path/filepath"
"sort"
"strings"
+ "gopkg.in/yaml.v3"
+
"thehouseoficarus/internal/object"
"thehouseoficarus/internal/player"
)
@@ -20,22 +24,51 @@ const (
)
type ModDef struct {
- ID string
- Name string
- Level int
- MaxHit int
- BaseXP float64
- JunkCost map[string]int
- Category ModCategory
- Element string
- TargetType string
- Destination int
+ 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"`
}
var AllMods []*ModDef
var modByID map[string]*ModDef
+func (g *Game) LoadMods() error {
+ dir := filepath.Join(g.DataDir, "modules")
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return err
+ }
+ AllMods = nil
+ modByID = make(map[string]*ModDef)
+ for _, e := range entries {
+ if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" {
+ continue
+ }
+ data, err := os.ReadFile(filepath.Join(dir, e.Name()))
+ if err != nil {
+ continue
+ }
+ var m ModDef
+ if err := yaml.Unmarshal(data, &m); err != nil {
+ continue
+ }
+ AllMods = append(AllMods, &m)
+ modByID[m.ID] = AllMods[len(AllMods)-1]
+ }
+ modByID = make(map[string]*ModDef, len(AllMods))
+ for _, m := range AllMods {
+ modByID[m.ID] = m
+ }
+ return nil
+}
+
func GetMod(id string) *ModDef {
return modByID[id]
}
@@ -162,6 +195,7 @@ func (g *Game) consumeJunkCost(p *player.Player, mod *ModDef) bool {
func (g *Game) junkCostString(p *player.Player, mod *ModDef) string {
cost := g.effectiveJunkCost(p, mod)
+ delete(cost, "scrap_metal")
if len(cost) == 0 {
return "free"
}
@@ -172,6 +206,7 @@ func (g *Game) junkCostString(p *player.Player, mod *ModDef) string {
if def != nil {
name = def.Name
}
+ name = strings.TrimSuffix(name, "junk")
if qty > 1 {
parts = append(parts, fmt.Sprintf("%d %s", qty, name))
} else {
@@ -193,143 +228,4 @@ func (g *Game) totalEquipScienceAttack(p *player.Player) int {
return total
}
-func init() {
- AllMods = []*ModDef{
- {ID: "bio_strike", Name: "Bio Strike", Level: 1, MaxHit: 4, BaseXP: 5.5,
- JunkCost: map[string]int{"biojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
- {ID: "bio_bolt", Name: "Bio Bolt", Level: 17, MaxHit: 9, BaseXP: 13.5,
- JunkCost: map[string]int{"biojunk": 2, "chaosjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
- {ID: "bio_blast", Name: "Bio Blast", Level: 41, MaxHit: 13, BaseXP: 25.5,
- JunkCost: map[string]int{"biojunk": 3, "chaosjunk": 1, "deathjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
- {ID: "bio_wave", Name: "Bio Wave", Level: 62, MaxHit: 17, BaseXP: 36.0,
- JunkCost: map[string]int{"biojunk": 5, "deathjunk": 1, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
- {ID: "bio_surge", Name: "Bio Surge", Level: 81, MaxHit: 21, BaseXP: 44.0,
- JunkCost: map[string]int{"biojunk": 7, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "bio", TargetType: "mob"},
- {ID: "hydro_strike", Name: "Hydro Strike", Level: 5, MaxHit: 6, BaseXP: 7.5,
- JunkCost: map[string]int{"hydrojunk": 3, "ecojunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
- {ID: "hydro_bolt", Name: "Hydro Bolt", Level: 23, MaxHit: 10, BaseXP: 16.5,
- JunkCost: map[string]int{"hydrojunk": 3, "ecojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
- {ID: "hydro_blast", Name: "Hydro Blast", Level: 47, MaxHit: 14, BaseXP: 28.5,
- JunkCost: map[string]int{"hydrojunk": 5, "ecojunk": 3, "chaosjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
- {ID: "hydro_wave", Name: "Hydro Wave", Level: 65, MaxHit: 18, BaseXP: 37.5,
- JunkCost: map[string]int{"hydrojunk": 7, "ecojunk": 5, "deathjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
- {ID: "hydro_surge", Name: "Hydro Surge", Level: 85, MaxHit: 22, BaseXP: 46.0,
- JunkCost: map[string]int{"hydrojunk": 10, "ecojunk": 7, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "hydro", TargetType: "mob"},
-
- {ID: "eco_strike", Name: "Eco Strike", Level: 9, MaxHit: 7, BaseXP: 9.5,
- JunkCost: map[string]int{"ecojunk": 2, "biojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
- {ID: "eco_bolt", Name: "Eco Bolt", Level: 29, MaxHit: 11, BaseXP: 19.5,
- JunkCost: map[string]int{"ecojunk": 3, "biojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
- {ID: "eco_blast", Name: "Eco Blast", Level: 53, MaxHit: 15, BaseXP: 31.5,
- JunkCost: map[string]int{"ecojunk": 4, "biojunk": 3, "chaosjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
- {ID: "eco_wave", Name: "Eco Wave", Level: 70, MaxHit: 19, BaseXP: 40.0,
- JunkCost: map[string]int{"ecojunk": 7, "biojunk": 5, "deathjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
- {ID: "eco_surge", Name: "Eco Surge", Level: 90, MaxHit: 23, BaseXP: 48.5,
- JunkCost: map[string]int{"ecojunk": 10, "biojunk": 7, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "eco", TargetType: "mob"},
-
- {ID: "solar_strike", Name: "Solar Strike", Level: 13, MaxHit: 8, BaseXP: 11.5,
- JunkCost: map[string]int{"solarjunk": 3, "ecojunk": 2, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
- {ID: "solar_bolt", Name: "Solar Bolt", Level: 35, MaxHit: 12, BaseXP: 22.5,
- JunkCost: map[string]int{"solarjunk": 4, "ecojunk": 3, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
- {ID: "solar_blast", Name: "Solar Blast", Level: 59, MaxHit: 16, BaseXP: 34.5,
- JunkCost: map[string]int{"solarjunk": 5, "ecojunk": 4, "chaosjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
- {ID: "solar_wave", Name: "Solar Wave", Level: 75, MaxHit: 20, BaseXP: 42.5,
- JunkCost: map[string]int{"solarjunk": 7, "ecojunk": 5, "deathjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
- {ID: "solar_surge", Name: "Solar Surge", Level: 95, MaxHit: 24, BaseXP: 51.0,
- JunkCost: map[string]int{"solarjunk": 10, "ecojunk": 7, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModCombat, Element: "solar", TargetType: "mob"},
-
- {ID: "low_process", Name: "Low Level Processing", Level: 21, MaxHit: 0, BaseXP: 31.0,
- JunkCost: map[string]int{"naturejunk": 3, "solarjunk": 1, "scrap_metal": 1},
- Category: ModProcessing, Element: "", TargetType: "inventory"},
- {ID: "high_process", Name: "High Level Processing", Level: 55, MaxHit: 0, BaseXP: 65.0,
- JunkCost: map[string]int{"naturejunk": 5, "solarjunk": 1, "scrap_metal": 1},
- Category: ModProcessing, Element: "", TargetType: "inventory"},
-
- {ID: "bones_to_nutrients", Name: "Bones to Nutrients", Level: 15, MaxHit: 0, BaseXP: 25.0,
- JunkCost: map[string]int{"naturejunk": 2, "ecojunk": 2, "scrap_metal": 1},
- Category: ModUtility, Element: "", TargetType: "self"},
- {ID: "em_grab", Name: "Electromagnetic Grab", Level: 33, MaxHit: 0, BaseXP: 43.0,
- JunkCost: map[string]int{"lawjunk": 1, "biojunk": 1, "scrap_metal": 1},
- Category: ModUtility, Element: "", TargetType: "ground_item"},
- {ID: "superheat", Name: "Superheat Item", Level: 43, MaxHit: 0, BaseXP: 53.0,
- JunkCost: map[string]int{"naturejunk": 4, "solarjunk": 1, "scrap_metal": 1},
- Category: ModUtility, Element: "", TargetType: "inventory"},
- {ID: "plank_make", Name: "Plank Make", Level: 86, MaxHit: 0, BaseXP: 90.0,
- JunkCost: map[string]int{"naturejunk": 5, "solarjunk": 1, "scrap_metal": 1},
- Category: ModUtility, Element: "", TargetType: "inventory"},
-
- {ID: "transport_town", Name: "Transport: Town Square", Level: 25, MaxHit: 0, BaseXP: 27.0,
- JunkCost: map[string]int{"lawjunk": 1, "solarjunk": 1, "biojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 1},
- {ID: "transport_forge", Name: "Transport: Forge", Level: 31, MaxHit: 0, BaseXP: 35.0,
- JunkCost: map[string]int{"lawjunk": 1, "ecojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 12},
- {ID: "transport_mine", Name: "Transport: Mining Pit", Level: 37, MaxHit: 0, BaseXP: 40.0,
- JunkCost: map[string]int{"lawjunk": 1, "ecojunk": 1, "solarjunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 6},
- {ID: "transport_forest", Name: "Transport: Forest", Level: 45, MaxHit: 0, BaseXP: 48.0,
- JunkCost: map[string]int{"lawjunk": 1, "ecojunk": 1, "biojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 22},
- {ID: "transport_scavenge", Name: "Transport: Scavenging Post", Level: 51, MaxHit: 0, BaseXP: 52.0,
- JunkCost: map[string]int{"lawjunk": 1, "naturejunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 9},
- {ID: "transport_deep_mine", Name: "Transport: Deep Mine", Level: 61, MaxHit: 0, BaseXP: 60.0,
- JunkCost: map[string]int{"lawjunk": 2, "ecojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 7},
- {ID: "transport_fishing", Name: "Transport: Fishing Dock", Level: 55, MaxHit: 0, BaseXP: 56.0,
- JunkCost: map[string]int{"lawjunk": 1, "hydrojunk": 1, "biojunk": 1, "scrap_metal": 1},
- Category: ModTransport, Element: "", TargetType: "self", Destination: 10},
-
- {ID: "enchant_1", Name: "Enchant Level 1", Level: 7, MaxHit: 0, BaseXP: 17.5,
- JunkCost: map[string]int{"cosmicjunk": 1, "hydrojunk": 1, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- {ID: "enchant_2", Name: "Enchant Level 2", Level: 27, MaxHit: 0, BaseXP: 37.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "biojunk": 3, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- {ID: "enchant_3", Name: "Enchant Level 3", Level: 49, MaxHit: 0, BaseXP: 59.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "solarjunk": 5, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- {ID: "enchant_4", Name: "Enchant Level 4", Level: 57, MaxHit: 0, BaseXP: 67.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "ecojunk": 10, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
-
- {ID: "chip_sapphire", Name: "Chip Sapphire Bolts", Level: 4, MaxHit: 0, BaseXP: 9.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "hydrojunk": 1, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- {ID: "chip_emerald", Name: "Chip Emerald Bolts", Level: 27, MaxHit: 0, BaseXP: 37.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "biojunk": 3, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- {ID: "chip_ruby", Name: "Chip Ruby Bolts", Level: 49, MaxHit: 0, BaseXP: 59.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "solarjunk": 5, "bloodjunk": 1, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- {ID: "chip_diamond", Name: "Chip Diamond Bolts", Level: 57, MaxHit: 0, BaseXP: 67.0,
- JunkCost: map[string]int{"cosmicjunk": 1, "ecojunk": 10, "scrap_metal": 1},
- Category: ModEnchant, Element: "", TargetType: "inventory"},
- }
-
- modByID = make(map[string]*ModDef, len(AllMods))
- for _, m := range AllMods {
- modByID[m.ID] = m
- }
-}
diff --git a/internal/game/tick.go b/internal/game/tick.go
index 4ab7e13..ec7eda9 100644
--- a/internal/game/tick.go
+++ b/internal/game/tick.go
@@ -17,8 +17,8 @@ func (g *Game) DisconnectTick() {
if !sess.Disconnecting {
continue
}
- p, ok := sess.Player.(*player.Player)
- if !ok {
+ p := sess.Player
+ if p == nil {
g.Hub.Remove(sess)
continue
}
@@ -38,8 +38,8 @@ func (g *Game) RegenTick() {
return
}
for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok {
+ p := sess.Player
+ if p == nil {
continue
}
if p.HP <= 0 || p.HP >= p.MaxHP() {
@@ -65,7 +65,7 @@ func (g *Game) RegenTick() {
p.HP = p.MaxHP()
p.RegenerateTick = 0
} else {
- p.RegenerateTick = 100
+ p.RegenerateTick = player.RegenTickInterval
}
}
}
@@ -118,9 +118,9 @@ func (g *Game) WanderTick() {
objMoves := g.World.FlushObjMoves()
for _, om := range objMoves {
for _, sess := range g.Hub.PlayersInRoom(om.FromRoom) {
- if p, ok := sess.Player.(*player.Player); ok && p.Action != nil {
+ if p := sess.Player; p != nil && p.Action != nil {
if p.Action.TargetID == om.DefID {
- g.CancelAction(p)
+ g.cancelAction(p)
g.writePrompt(sess)
}
}
@@ -135,8 +135,8 @@ func (g *Game) WanderTick() {
for _, m := range moves {
for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok {
+ p := sess.Player
+ if p == nil {
continue
}
if p.RoomID == m.fromRoom && p.OptionBool("mob_leave") {
@@ -162,8 +162,8 @@ func (g *Game) WanderTick() {
func (g *Game) SharedDepletionTick() {
activeKeys := make(map[string]bool)
for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil || p.Action == nil || p.Action.Type != "gather" {
+ p := sess.Player
+ if p == nil || p.Action == nil || p.Action.Type != "gather" {
continue
}
if key, ok := p.Action.Data["instance_key"].(string); ok {
@@ -218,8 +218,8 @@ func (g *Game) TechTick() {
return
}
for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil || len(p.ActiveTechs) == 0 {
+ p := sess.Player
+ if p == nil || len(p.ActiveTechs) == 0 {
continue
}
@@ -281,8 +281,8 @@ func (g *Game) ConsumeTick() {
return
}
for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil {
+ p := sess.Player
+ if p == nil {
continue
}
if p.ConsumeCooldown > 0 {
@@ -296,8 +296,8 @@ func (g *Game) VisualTick() {
return
}
for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil || !p.OptionBool("visual_ticks") {
+ p := sess.Player
+ if p == nil || !p.OptionBool("visual_ticks") {
continue
}
text := p.OptionString("visual_tick_text")
@@ -318,3 +318,28 @@ func (g *Game) VisualTick() {
}
}
}
+
+func (g *Game) MoveTick() {
+ if g.Hub == nil {
+ return
+ }
+ for _, sess := range g.Hub.AllSessions() {
+ p := sess.Player
+ if 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/utils.go b/internal/game/utils.go
index 6a294c5..9148ea3 100644
--- a/internal/game/utils.go
+++ b/internal/game/utils.go
@@ -112,3 +112,10 @@ func (g *Game) newInventorySlot(itemID string, qty int) *player.InventorySlot {
}
return slot
}
+
+func (g *Game) awardSkillXP(sess *net.Session, p *player.Player, skill player.SkillName, xp int) {
+ newLevel := p.AddSkillXP(skill, xp)
+ if newLevel > 0 {
+ sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, skill)))
+ }
+}
diff --git a/internal/net/server.go b/internal/net/server.go
index 7c7a18c..f82f82b 100644
--- a/internal/net/server.go
+++ b/internal/net/server.go
@@ -6,10 +6,10 @@ import (
"log"
"net"
"net/http"
- "strings"
- "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/action"
"thehouseoficarus/internal/config"
+ "thehouseoficarus/internal/player"
)
type SessionState int
@@ -46,8 +46,8 @@ const (
type Session struct {
Conn Conn
State SessionState
- Account *AccountEntry
- Player interface{}
+ Account *player.Account
+ Player *player.Player
PendingChar string
PendingPass string
PendingMenu []map[string]string
@@ -57,17 +57,10 @@ type Session struct {
PendingBackground bool
Disconnecting bool
DisconnectTicks int
- Shop interface{}
+ Shop *action.ShopConfig
}
-type AccountEntry struct {
- Name string
- PasswordHash string
- Characters []string
- Aliases map[string]string
- Colors map[string]string
- Options map[string]any
-}
+
type Server struct {
config *config.Config
@@ -281,53 +274,6 @@ func (s *Server) serveHTTPS() {
}
}
-func artColor(r rune) string {
- switch r {
- case '@':
- return color.FgCode("ansi", 4)
- case '!', ':', '.':
- return color.StyleCode("dim") + color.FgCode("ansi", 4)
- case ',':
- return color.FgCode("ansi", 5)
- case '*':
- return color.FgCode("ansi", 5)
- case '+', '|', '-':
- return color.FgCode("ansi", 3)
- }
- return ""
-}
-
-func colorizeRunes(s string) string {
- var sb strings.Builder
- current := ""
- for _, r := range s {
- want := artColor(r)
- if want != current {
- if current != "" {
- sb.WriteString(color.Reset)
- }
- if want != "" {
- sb.WriteString(want)
- }
- current = want
- }
- sb.WriteRune(r)
- }
- if current != "" {
- sb.WriteString(color.Reset)
- }
- return sb.String()
-}
-
-func colorizeArt(art string) string {
- welcomeText := "welcome to the house of icarus"
- before, after, found := strings.Cut(art, welcomeText)
- if found {
- return colorizeRunes(before) + color.FgCode("ansi", 7) + welcomeText + color.Reset + colorizeRunes(after)
- }
- return colorizeRunes(art)
-}
-
func (s *Server) handleSession(sess *Session, handler func(*Session, string)) {
defer func() {
if r := recover(); r != nil {
@@ -339,34 +285,7 @@ func (s *Server) handleSession(sess *Session, handler func(*Session, string)) {
sess.Conn.Write([]byte("\033[2J\033[H"))
- welcomeart := colorizeArt(`
- | *
- * + -+-
- . , |
-. , . ,
- *
- @@@@@@@ @@@ @@@ @@@@@@@@ @@@ @@@ @@@@@@ @@@ @@@ @@@@@@ @@@@@@@@ +
- @@! @@! @@@ @@! + @@! @@@ @@! @@@ @@! @@@ !@@ @@!
- @!! @!@!@!@! @!!!:! @!@!@!@! @!@ !@! @!@ !@! !@@!! @!!!:! *
- !!: !!: !!! !!: . !!: !!! !!: !!! !!: !!! !:! !!:
- : : : : : :: ::: : : : : :. : :.:: : ::.: : : :: ::: .
- . +
- * welcome to the house of icarus , ,
- , .
- @@@@@@ @@@@@@@@ @@@ @@@@@@@ @@@@@@ @@@@@@@ @@@ @@@ @@@@@@
- @@! @@@ @@! , @@! !@@ @@! @@@ @@! @@@ @@! @@@ !@@
- | @!@ !@! @!!!:! !!@ !@! @!@!@!@! @!@!!@! @!@ !@! !@@!!
- -+- !!: !!! !!: !!: :!! !!: !!! !!: :!! !!: !!! !:!
- | : :. : : . : :: :: : : : : : : : :.:: : ::.: :
- ,
- . + , .
- . ,
- * . , *
- +
-`)
-
-
- sess.Write(welcomeart)
+ sess.Write(WelcomeBanner())
sess.Write("What's your account name? ")
for {
diff --git a/internal/net/welcome.go b/internal/net/welcome.go
new file mode 100644
index 0000000..e17c82e
--- /dev/null
+++ b/internal/net/welcome.go
@@ -0,0 +1,82 @@
+package net
+
+import (
+ "strings"
+
+ "thehouseoficarus/internal/color"
+)
+
+func artColor(r rune) string {
+ switch r {
+ case '@':
+ return color.FgCode("ansi", 4)
+ case '!', ':', '.':
+ return color.StyleCode("dim") + color.FgCode("ansi", 4)
+ case ',':
+ return color.FgCode("ansi", 5)
+ case '*':
+ return color.FgCode("ansi", 5)
+ case '+', '|', '-':
+ return color.FgCode("ansi", 3)
+ }
+ return ""
+}
+
+func colorizeRunes(s string) string {
+ var sb strings.Builder
+ current := ""
+ for _, r := range s {
+ want := artColor(r)
+ if want != current {
+ if current != "" {
+ sb.WriteString(color.Reset)
+ }
+ if want != "" {
+ sb.WriteString(want)
+ }
+ current = want
+ }
+ sb.WriteRune(r)
+ }
+ if current != "" {
+ sb.WriteString(color.Reset)
+ }
+ return sb.String()
+}
+
+func colorizeArt(art string) string {
+ welcomeText := "welcome to the house of icarus"
+ before, after, found := strings.Cut(art, welcomeText)
+ if found {
+ return colorizeRunes(before) + color.FgCode("ansi", 7) + welcomeText + color.Reset + colorizeRunes(after)
+ }
+ return colorizeRunes(art)
+}
+
+func WelcomeBanner() string {
+ return colorizeArt(`
+ | *
+ * + -+-
+ . , |
+. , . ,
+ *
+ @@@@@@@ @@@ @@@ @@@@@@@@ @@@ @@@ @@@@@@ @@@ @@@ @@@@@@ @@@@@@@@ +
+ @@! @@! @@@ @@! + @@! @@@ @@! @@@ @@! @@@ !@@ @@!
+ @!! @!@!@!@! @!!!:! @!@!@!@! @!@ !@! @!@ !@! !@@!! @!!!:! *
+ !!: !!: !!! !!: . !!: !!! !!: !!! !!: !!! :!; !!:
+ : : : : : :: ::: : : : : :. : :.:: : ::.: : : :: ::: .
+ . +
+ * welcome to the house of icarus , ,
+ , .
+ @@@@@@ @@@@@@@@ @@@ @@@@@@@ @@@@@@ @@@@@@@ @@@ @@@ @@@@@@
+ @@! @@@ @@! , @@! !@@ @@! @@@ @@! @@@ @@! @@@ !@@
+ | @!@ !@! @!!!:! !!@ @!@ @!@!@!@! @!@!!@! @!@ !@! !@@!!
+ -+- !!: !!! !!: !!: :!! !!: !!! !!: :!! !!: !!! :!;
+ | : :. : : . : :: :: : : : : : : : :.:: : ::.: :
+ ,
+ . + , .
+ . ,
+ * . , *
+ +
+`)
+}
diff --git a/internal/object/item.go b/internal/object/item.go
index 4959a22..7c682a4 100644
--- a/internal/object/item.go
+++ b/internal/object/item.go
@@ -3,7 +3,7 @@ package object
import (
"strings"
- "thehouseoficarus/internal/world"
+ "thehouseoficarus/internal/action"
)
type EquipSlot string
@@ -105,5 +105,5 @@ func (d *ItemDef) MatchesName(input string) bool {
if strings.ToLower(d.Name) == lower {
return true
}
- return world.WordPrefixMatch(lower, d.Name)
+ return action.WordPrefixMatch(lower, d.Name)
}
diff --git a/internal/object/object.go b/internal/object/object.go
index 8c4c752..4d8db69 100644
--- a/internal/object/object.go
+++ b/internal/object/object.go
@@ -10,18 +10,38 @@ type UseInteraction struct {
}
type ObjectDef struct {
- ID string `yaml:"id"`
- Name string `yaml:"name"`
- Color string `yaml:"color"`
- BehaviorID string `yaml:"behavior"`
- Hidden bool `yaml:"hidden"`
- InRoomDescription string `yaml:"inroom_description"`
- RemovalItem string `yaml:"removal_item"`
- Description string `yaml:"description"`
- UseInteractions []UseInteraction `yaml:"use_interactions"`
- StealTable string `yaml:"steal_table"`
- StealLevel int `yaml:"steal_level"`
- StealXP int `yaml:"steal_xp"`
- StealSpeed float64 `yaml:"steal_speed"`
- GuardMob string `yaml:"guard_mob"`
+ ID string `yaml:"id"`
+ Name string `yaml:"name"`
+ Color string `yaml:"color"`
+ Hidden bool `yaml:"hidden"`
+ InRoomDescription string `yaml:"inroom_description"`
+ RemovalItem string `yaml:"removal_item"`
+ Description string `yaml:"description"`
+ UseInteractions []UseInteraction `yaml:"use_interactions"`
+ StealTable string `yaml:"steal_table"`
+ StealLevel int `yaml:"steal_level"`
+ StealXP int `yaml:"steal_xp"`
+ StealSpeed float64 `yaml:"steal_speed"`
+ GuardMob string `yaml:"guard_mob"`
+
+ Gather *action.GatherConfig `yaml:"gather,omitempty"`
+ Talk *action.TalkConfig `yaml:"talk,omitempty"`
+ Use *action.UseConfig `yaml:"use,omitempty"`
+}
+
+func (d *ObjectDef) IsGatherable() bool { return d.Gather != nil }
+func (d *ObjectDef) IsTalkable() bool { return d.Talk != nil }
+func (d *ObjectDef) IsUsable() bool { return d.Use != nil }
+func (d *ObjectDef) IsInteractable() bool { return d.Gather != nil || d.Talk != nil || d.Use != nil }
+
+func (d *ObjectDef) BehaviorType() string {
+ switch {
+ case d.Gather != nil:
+ return "gather"
+ case d.Talk != nil:
+ return "talk"
+ case d.Use != nil:
+ return "use"
+ }
+ return ""
}
diff --git a/internal/player/player.go b/internal/player/player.go
index 88cf7f8..3842348 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -49,15 +49,15 @@ var SkillAbbr = map[SkillName]string{
Ranged: "rng",
Science: "sci",
Technology: "tec",
- Fishing: "fis",
+ Fishing: "fsh",
Cooking: "cok",
- Woodcutting: "wct",
+ Woodcutting: "wdc",
Firemaking: "fmk",
Mining: "min",
Smithing: "smt",
Crafting: "cft",
Fletching: "flt",
- Pharmacy: "pha",
+ Pharmacy: "phr",
Thieving: "thv",
Agility: "agl",
Construction: "con",
@@ -179,7 +179,7 @@ type Player struct {
MoveDirection string `yaml:"-"`
MoveTarget int `yaml:"-"`
VisualTickCurrent int `yaml:"-"`
- AutocastMod string `yaml:"-"`
+ AutotriggerMod string `yaml:"-"`
Battery float64 `yaml:"battery"`
ActiveTechs map[string]bool `yaml:"-"`
QuickTech string `yaml:"quick_tech,omitempty"`
@@ -262,6 +262,12 @@ func (p *Player) OptionInt(name string) int {
return def.Default.(int)
}
+func (p *Player) EnsureFlags() {
+ if p.Flags == nil {
+ p.Flags = make(map[string]any)
+ }
+}
+
func (p *Player) InvSlot(i int) *InventorySlot {
if p.Inventory == nil {
return nil
@@ -353,9 +359,11 @@ func (p *Player) MaxHP() int {
return p.Level(Hitpoints)
}
+const RegenTickInterval = 100
+
func (p *Player) StartRegen() {
if p.RegenerateTick == 0 {
- p.RegenerateTick = 100
+ p.RegenerateTick = RegenTickInterval
}
}
diff --git a/internal/world/ground.go b/internal/world/ground.go
new file mode 100644
index 0000000..6c3ecf3
--- /dev/null
+++ b/internal/world/ground.go
@@ -0,0 +1,189 @@
+package world
+
+import (
+ "strings"
+)
+
+const DropDespawnTicks = 1000
+const ReserveTicks = 100
+
+type GroundItemInfo struct {
+ ItemID string
+ Quantity int
+ ReservedFor string
+ ReserveTimer int
+ DespawnTimer int
+ IsSpawn bool
+}
+
+type groundEntry struct {
+ itemID string
+ quantity int
+ isSpawn bool
+ respawnTimer int // >0 = counting down to respawn
+ respawnQty int
+ respawnDelay int
+ despawnTimer int // >0 = counting down to despawn (dropped items)
+ reservedFor string
+ reserveTimer int // >0 = counting down reservation
+}
+
+func (w *World) GroundItemsDetailed(roomID int) []GroundItemInfo {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ var out []GroundItemInfo
+ for _, e := range w.groundItems[roomID] {
+ if e.quantity <= 0 {
+ continue
+ }
+ info := GroundItemInfo{
+ ItemID: e.itemID,
+ Quantity: e.quantity,
+ DespawnTimer: e.despawnTimer,
+ IsSpawn: e.isSpawn,
+ }
+ if e.reserveTimer > 0 && e.reservedFor != "" {
+ info.ReservedFor = e.reservedFor
+ info.ReserveTimer = e.reserveTimer
+ }
+ out = append(out, info)
+ }
+ return out
+}
+
+func (w *World) GroundItems(roomID int) map[string]int {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ out := make(map[string]int)
+ for _, e := range w.groundItems[roomID] {
+ if e.quantity > 0 {
+ out[e.itemID] += e.quantity
+ }
+ }
+ return out
+}
+
+func (w *World) AddReservedGroundItem(roomID int, itemID string, qty int, owner string) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ e := &groundEntry{
+ itemID: itemID,
+ quantity: qty,
+ despawnTimer: DropDespawnTicks,
+ reservedFor: owner,
+ reserveTimer: ReserveTicks,
+ }
+ w.groundItems[roomID] = append(w.groundItems[roomID], e)
+}
+
+func (w *World) AddGroundItem(roomID int, itemID string, qty int) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ e := &groundEntry{
+ itemID: itemID,
+ quantity: qty,
+ despawnTimer: DropDespawnTicks,
+ }
+ w.groundItems[roomID] = append(w.groundItems[roomID], e)
+}
+
+func (w *World) RemoveReservedGroundItem(roomID int, itemID string, qty int, owner string) (int, bool) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ removed := 0
+ remaining := qty
+ for _, e := range w.groundItems[roomID] {
+ if remaining <= 0 {
+ break
+ }
+ if !strings.EqualFold(e.itemID, itemID) {
+ continue
+ }
+ if e.quantity <= 0 {
+ continue
+ }
+ if e.reserveTimer > 0 && e.reservedFor != "" && e.reservedFor != owner {
+ return removed, false
+ }
+ take := e.quantity
+ if take > remaining {
+ take = remaining
+ }
+ e.quantity -= take
+ removed += take
+ remaining -= take
+ if e.isSpawn && e.quantity <= 0 && e.respawnDelay > 0 {
+ e.respawnTimer = e.respawnDelay
+ }
+ }
+ return removed, true
+}
+
+func (w *World) RemoveGroundItem(roomID int, itemID string, qty int) int {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ removed := 0
+ remaining := qty
+
+ for _, e := range w.groundItems[roomID] {
+ if remaining <= 0 {
+ break
+ }
+ if !strings.EqualFold(e.itemID, itemID) {
+ continue
+ }
+ if e.quantity <= 0 {
+ continue
+ }
+ take := e.quantity
+ if take > remaining {
+ take = remaining
+ }
+ e.quantity -= take
+ removed += take
+ remaining -= take
+
+ if e.isSpawn && e.quantity <= 0 && e.respawnDelay > 0 {
+ e.respawnTimer = e.respawnDelay
+ }
+ }
+ return removed
+}
+
+func (w *World) tickGroundItemsLocked() {
+ for _, entries := range w.groundItems {
+ for _, e := range entries {
+ if e.respawnTimer > 0 {
+ e.respawnTimer--
+ if e.respawnTimer <= 0 {
+ e.quantity = e.respawnQty
+ w.mergeNearbyItemsLocked(entries, e)
+ }
+ }
+ if e.despawnTimer > 0 {
+ e.despawnTimer--
+ if e.despawnTimer <= 0 {
+ e.quantity = 0
+ }
+ }
+ if e.reserveTimer > 0 {
+ e.reserveTimer--
+ if e.reserveTimer <= 0 {
+ e.reservedFor = ""
+ }
+ }
+ }
+ }
+}
+
+func (w *World) mergeNearbyItemsLocked(entries []*groundEntry, target *groundEntry) {
+ for _, other := range entries {
+ if other != target && other.quantity > 0 && strings.EqualFold(other.itemID, target.itemID) &&
+ (other.reserveTimer <= 0 || other.reservedFor == "") &&
+ other.despawnTimer <= 0 {
+ target.quantity += other.quantity
+ other.quantity = 0
+ }
+ }
+}
diff --git a/internal/world/mob.go b/internal/world/mob.go
index ce9eddd..f7988ce 100644
--- a/internal/world/mob.go
+++ b/internal/world/mob.go
@@ -12,7 +12,7 @@ import (
"gopkg.in/yaml.v3"
)
-type DropTable struct {
+type MobDropTable struct {
Remains string `yaml:"remains"`
Loot []action.DropEntry `yaml:"loot"`
}
@@ -21,7 +21,6 @@ type MobDef struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Description string `yaml:"description"`
- BehaviorID string `yaml:"behavior"`
IdleDescriptions []string `yaml:"idle_descriptions"`
CombatDescriptions []string `yaml:"combat_descriptions"`
Attack int `yaml:"attack"`
@@ -35,7 +34,7 @@ type MobDef struct {
Protected bool `yaml:"protected"`
Unique bool `yaml:"unique"`
RespawnTicks float64 `yaml:"respawn_ticks"`
- Drops DropTable `yaml:"drops"`
+ Drops MobDropTable `yaml:"drops"`
AttackBonus int `yaml:"attack_bonus"`
StrengthBonus int `yaml:"strength_bonus"`
@@ -56,13 +55,16 @@ type MobDef struct {
AssassinLevel int `yaml:"assassin_level"`
FinishingBlow string `yaml:"finishing_blow"`
DamageWithout string `yaml:"damage_without"`
+
+ Talk *action.TalkConfig `yaml:"talk,omitempty"`
}
+func (d *MobDef) IsTalkable() bool { return d.Talk != nil }
+
type MobInstance struct {
InstanceID string
DefID string
Name string
- BehaviorID string
HP int
MaxHP int
Attack int
@@ -77,7 +79,7 @@ type MobInstance struct {
RespawnTicks float64
RoomID int
HomeRoomID int
- Drops DropTable
+ Drops MobDropTable
IdleDescription string
WanderRooms []int
WanderInterval float64
@@ -103,41 +105,24 @@ type MobInstance struct {
AssassinLevel int
FinishingBlow string
DamageWithout string
+
+ TalkConfig *action.TalkConfig
}
+func (m *MobInstance) IsTalkable() bool { return m.TalkConfig != nil }
+
const (
MatchNone = 0
MatchPrefix = 1
MatchExact = 2
)
-func WordPrefixMatch(input, name string) bool {
- inputWords := strings.Fields(strings.ToLower(input))
- if len(inputWords) == 0 {
- return false
- }
- nameWords := strings.Fields(strings.ToLower(name))
- for _, iw := range inputWords {
- found := false
- for _, nw := range nameWords {
- if strings.HasPrefix(nw, iw) || strings.HasPrefix(iw, nw) {
- found = true
- break
- }
- }
- if !found {
- return false
- }
- }
- return true
-}
-
func (m *MobInstance) MatchQuality(input string) int {
lower := strings.ToLower(input)
if strings.ToLower(m.Name) == lower {
return MatchExact
}
- if WordPrefixMatch(input, m.Name) {
+ if action.WordPrefixMatch(input, m.Name) {
return MatchPrefix
}
return MatchNone
@@ -281,7 +266,6 @@ func (s *MobStore) SeedMobs(roomID int, entries []RoomMob) {
InstanceID: instID,
DefID: defID,
Name: dw.def.Name,
- BehaviorID: dw.def.BehaviorID,
HP: dw.def.HP,
MaxHP: dw.def.HP,
Attack: dw.def.Attack,
@@ -315,6 +299,7 @@ func (s *MobStore) SeedMobs(roomID int, entries []RoomMob) {
AssassinLevel: dw.def.AssassinLevel,
FinishingBlow: dw.def.FinishingBlow,
DamageWithout: dw.def.DamageWithout,
+ TalkConfig: dw.def.Talk,
}
inst.IdleDescription = pickIdleDescription(dw.def.IdleDescriptions)
s.instances[instID] = inst
diff --git a/internal/world/objects.go b/internal/world/objects.go
new file mode 100644
index 0000000..568ea40
--- /dev/null
+++ b/internal/world/objects.go
@@ -0,0 +1,332 @@
+package world
+
+import (
+ "fmt"
+ "math/rand"
+ "sort"
+ "strings"
+
+ "thehouseoficarus/internal/action"
+)
+
+type ObjState struct {
+ Depleted bool
+ DepleteTimer float64
+ DefID string
+ Name string
+ Index int
+ RoomID int
+ JustRespawned bool
+ WanderRooms []int
+ WanderInterval float64
+ WanderCounter int
+ SharedMax float64
+ SharedTimer int
+ Quality float64
+}
+
+type ObjMove struct {
+ DefID string
+ Name string
+ FromRoom int
+ ToRoom int
+}
+
+func (w *World) ObjStateKey(roomID int, defID string, index int) string {
+ return fmt.Sprintf("%d:%s:%d", roomID, defID, index)
+}
+
+func (w *World) EnsureObjectStates(roomID int, defIDs []string) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ w.ensureObjectStatesLocked(roomID, defIDs)
+}
+
+func (w *World) ensureObjectStatesLocked(roomID int, defIDs []string) {
+ if w.objStates == nil {
+ w.objStates = make(map[string]*ObjState)
+ }
+ counts := make(map[string]int)
+ for _, defID := range defIDs {
+ idx := counts[defID]
+ counts[defID]++
+ key := w.ObjStateKey(roomID, defID, idx)
+ if _, exists := w.objStates[key]; !exists {
+ w.objStates[key] = &ObjState{
+ DefID: defID,
+ Name: defID,
+ Index: idx,
+ RoomID: roomID,
+ }
+ }
+ }
+}
+
+func (w *World) AddObjInstance(roomID int, defID string, quality float64) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ if w.objStates == nil {
+ w.objStates = make(map[string]*ObjState)
+ }
+ idx := 0
+ for {
+ key := w.ObjStateKey(roomID, defID, idx)
+ if _, exists := w.objStates[key]; !exists {
+ w.objStates[key] = &ObjState{
+ DefID: defID,
+ Name: defID,
+ Index: idx,
+ RoomID: roomID,
+ Quality: quality,
+ }
+ return
+ }
+ idx++
+ }
+}
+
+func (w *World) RemoveObjInstance(roomID int, defID string) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ idx := 0
+ for {
+ key := w.ObjStateKey(roomID, defID, idx)
+ if _, exists := w.objStates[key]; !exists {
+ return
+ }
+ delete(w.objStates, key)
+ idx++
+ }
+}
+
+func (w *World) AllFireStates() []*ObjState {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ var out []*ObjState
+ for _, st := range w.objStates {
+ if st.DefID == "fire" && st.Quality > 0 {
+ out = append(out, st)
+ }
+ }
+ return out
+}
+
+func (w *World) GetObjState(roomID int, defID string, index int) *ObjState {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ if w.objStates == nil {
+ return nil
+ }
+ return w.objStates[w.ObjStateKey(roomID, defID, index)]
+}
+
+func (w *World) GetObjStateByKey(key string) *ObjState {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ if w.objStates == nil {
+ return nil
+ }
+ return w.objStates[key]
+}
+
+func (w *World) AllObjInstances(roomID int) []ObjState {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ var out []ObjState
+ for _, st := range w.objStates {
+ if st.RoomID == roomID {
+ out = append(out, *st)
+ }
+ }
+ sort.Slice(out, func(i, j int) bool {
+ if out[i].DefID != out[j].DefID {
+ return out[i].DefID < out[j].DefID
+ }
+ return out[i].Index < out[j].Index
+ })
+ return out
+}
+
+func (w *World) FindObjInstances(roomID int, name string) []ObjState {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ var out []ObjState
+ lower := strings.ToLower(name)
+ for _, st := range w.objStates {
+ if st.RoomID != roomID {
+ continue
+ }
+ if !wordMatchesObj(lower, st.DefID, st.Name) {
+ continue
+ }
+ out = append(out, ObjState{
+ DefID: st.DefID,
+ Name: st.Name,
+ Index: st.Index,
+ RoomID: st.RoomID,
+ Depleted: st.Depleted,
+ DepleteTimer: st.DepleteTimer,
+ SharedMax: st.SharedMax,
+ SharedTimer: st.SharedTimer,
+ Quality: st.Quality,
+ })
+ }
+ sort.Slice(out, func(i, j int) bool {
+ if out[i].DefID != out[j].DefID {
+ return out[i].DefID < out[j].DefID
+ }
+ return out[i].Index < out[j].Index
+ })
+ return out
+}
+
+func wordMatchesObj(lower, defID, objName string) bool {
+ if action.WordPrefixMatch(lower, objName) {
+ return true
+ }
+ inputWords := strings.Fields(strings.ToLower(lower))
+
+ if strings.HasPrefix(strings.ToLower(defID), lower) || strings.HasPrefix(lower, strings.ToLower(defID)) {
+ return true
+ }
+ for _, part := range strings.Split(defID, "_") {
+ for _, iw := range inputWords {
+ pl := strings.ToLower(part)
+ if strings.HasPrefix(pl, iw) || strings.HasPrefix(iw, pl) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func (w *World) SetObjName(roomID int, defID string, name string) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ for _, st := range w.objStates {
+ if st.RoomID == roomID && st.DefID == defID {
+ st.Name = name
+ }
+ }
+}
+
+func (w *World) SetObjWander(roomID int, defID string, rooms []int, interval float64) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ for _, st := range w.objStates {
+ if st.RoomID == roomID && st.DefID == defID {
+ st.WanderRooms = rooms
+ st.WanderInterval = interval
+ }
+ }
+}
+
+func (w *World) SetObjDepleteTimer(roomID int, defID string, max float64) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ for _, st := range w.objStates {
+ if st.RoomID == roomID && st.DefID == defID && st.SharedMax == 0 {
+ st.SharedMax = max
+ st.SharedTimer = int(max)
+ }
+ }
+}
+
+func (w *World) AllSharedObjStates() []*ObjState {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ var out []*ObjState
+ for _, st := range w.objStates {
+ if st.SharedMax > 0 {
+ out = append(out, st)
+ }
+ }
+ return out
+}
+
+func (w *World) SetObjDepleted(roomID int, defID string, index int, delay float64) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ key := w.ObjStateKey(roomID, defID, index)
+ if st, ok := w.objStates[key]; ok {
+ st.Depleted = true
+ st.DepleteTimer = delay
+ }
+}
+
+func (w *World) FlushObjRespawns() []ObjState {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ var out []ObjState
+ for _, st := range w.objStates {
+ if st.JustRespawned {
+ out = append(out, *st)
+ st.JustRespawned = false
+ }
+ }
+ return out
+}
+
+func (w *World) TickObjWander() {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ var moves []struct {
+ st *ObjState
+ toRoom int
+ }
+ for _, st := range w.objStates {
+ if len(st.WanderRooms) == 0 || st.WanderInterval <= 0 {
+ continue
+ }
+ st.WanderCounter++
+ if float64(st.WanderCounter) < st.WanderInterval {
+ continue
+ }
+ st.WanderCounter = 0
+ toRoom := st.WanderRooms[rand.Intn(len(st.WanderRooms))]
+ if toRoom == st.RoomID {
+ continue
+ }
+ moves = append(moves, struct {
+ st *ObjState
+ toRoom int
+ }{st, toRoom})
+ }
+ for _, m := range moves {
+ oldKey := w.ObjStateKey(m.st.RoomID, m.st.DefID, m.st.Index)
+ fromRoom := m.st.RoomID
+ m.st.RoomID = m.toRoom
+ newKey := w.ObjStateKey(m.st.RoomID, m.st.DefID, m.st.Index)
+ delete(w.objStates, oldKey)
+ w.objStates[newKey] = m.st
+ w.objMoves = append(w.objMoves, ObjMove{
+ DefID: m.st.DefID,
+ Name: m.st.Name,
+ FromRoom: fromRoom,
+ ToRoom: m.toRoom,
+ })
+ }
+}
+
+func (w *World) FlushObjMoves() []ObjMove {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ out := w.objMoves
+ w.objMoves = nil
+ return out
+}
+
+func (w *World) tickObjStatesLocked() {
+ for _, st := range w.objStates {
+ if st.Depleted && st.DepleteTimer > 0 {
+ st.DepleteTimer--
+ if st.DepleteTimer <= 0 {
+ st.Depleted = false
+ st.JustRespawned = true
+ if st.SharedMax > 0 {
+ st.SharedTimer = int(st.SharedMax)
+ }
+ }
+ }
+ }
+}
diff --git a/internal/world/world.go b/internal/world/world.go
index f918293..3094c54 100644
--- a/internal/world/world.go
+++ b/internal/world/world.go
@@ -2,40 +2,14 @@ package world
import (
"fmt"
- "math/rand"
"os"
"path/filepath"
- "sort"
"strings"
"sync"
"gopkg.in/yaml.v3"
)
-const DropDespawnTicks = 1000
-const ReserveTicks = 100
-
-type GroundItemInfo struct {
- ItemID string
- Quantity int
- ReservedFor string
- ReserveTimer int
- DespawnTimer int
- IsSpawn bool
-}
-
-type groundEntry struct {
- itemID string
- quantity int
- isSpawn bool
- respawnTimer int // >0 = counting down to respawn
- respawnQty int
- respawnDelay int
- despawnTimer int // >0 = counting down to despawn (dropped items)
- reservedFor string
- reserveTimer int // >0 = counting down reservation
-}
-
type World struct {
dataDir string
mu sync.Mutex
@@ -45,255 +19,6 @@ type World struct {
objMoves []ObjMove
}
-type ObjMove struct {
- DefID string
- Name string
- FromRoom int
- ToRoom int
-}
-
-type ObjState struct {
- Depleted bool
- DepleteTimer float64
- DefID string
- Name string
- Index int
- RoomID int
- JustRespawned bool
- WanderRooms []int
- WanderInterval float64
- WanderCounter int
- SharedMax float64
- SharedTimer int
- Quality float64
-}
-
-func (w *World) ObjStateKey(roomID int, defID string, index int) string {
- return fmt.Sprintf("%d:%s:%d", roomID, defID, index)
-}
-
-func (w *World) objStateKey(roomID int, defID string, index int) string {
- return w.ObjStateKey(roomID, defID, index)
-}
-
-func (w *World) EnsureObjectStates(roomID int, defIDs []string) {
- w.mu.Lock()
- defer w.mu.Unlock()
- w.ensureObjectStatesLocked(roomID, defIDs)
-}
-
-func (w *World) ensureObjectStatesLocked(roomID int, defIDs []string) {
- if w.objStates == nil {
- w.objStates = make(map[string]*ObjState)
- }
- counts := make(map[string]int)
- for _, defID := range defIDs {
- idx := counts[defID]
- counts[defID]++
- key := w.objStateKey(roomID, defID, idx)
- if _, exists := w.objStates[key]; !exists {
- w.objStates[key] = &ObjState{
- DefID: defID,
- Name: defID,
- Index: idx,
- RoomID: roomID,
- }
- }
- }
-}
-
-func (w *World) AddObjInstance(roomID int, defID string, quality float64) {
- w.mu.Lock()
- defer w.mu.Unlock()
- if w.objStates == nil {
- w.objStates = make(map[string]*ObjState)
- }
- idx := 0
- for {
- key := w.objStateKey(roomID, defID, idx)
- if _, exists := w.objStates[key]; !exists {
- w.objStates[key] = &ObjState{
- DefID: defID,
- Name: defID,
- Index: idx,
- RoomID: roomID,
- Quality: quality,
- }
- return
- }
- idx++
- }
-}
-
-func (w *World) RemoveObjInstance(roomID int, defID string) {
- w.mu.Lock()
- defer w.mu.Unlock()
- idx := 0
- for {
- key := w.objStateKey(roomID, defID, idx)
- if _, exists := w.objStates[key]; !exists {
- return
- }
- delete(w.objStates, key)
- idx++
- }
-}
-
-func (w *World) AllFireStates() []*ObjState {
- w.mu.Lock()
- defer w.mu.Unlock()
- var out []*ObjState
- for _, st := range w.objStates {
- if st.DefID == "fire" && st.Quality > 0 {
- out = append(out, st)
- }
- }
- return out
-}
-
-func (w *World) GetObjState(roomID int, defID string, index int) *ObjState {
- w.mu.Lock()
- defer w.mu.Unlock()
- if w.objStates == nil {
- return nil
- }
- return w.objStates[w.objStateKey(roomID, defID, index)]
-}
-
-func (w *World) GetObjStateByKey(key string) *ObjState {
- w.mu.Lock()
- defer w.mu.Unlock()
- if w.objStates == nil {
- return nil
- }
- return w.objStates[key]
-}
-
-func (w *World) AllObjInstances(roomID int) []ObjState {
- w.mu.Lock()
- defer w.mu.Unlock()
- var out []ObjState
- for _, st := range w.objStates {
- if st.RoomID == roomID {
- out = append(out, *st)
- }
- }
- sort.Slice(out, func(i, j int) bool {
- if out[i].DefID != out[j].DefID {
- return out[i].DefID < out[j].DefID
- }
- return out[i].Index < out[j].Index
- })
- return out
-}
-
-func (w *World) FindObjInstances(roomID int, name string) []ObjState {
- w.mu.Lock()
- defer w.mu.Unlock()
- var out []ObjState
- lower := strings.ToLower(name)
- for _, st := range w.objStates {
- if st.RoomID != roomID {
- continue
- }
- if !wordMatchesObj(lower, st.DefID, st.Name) {
- continue
- }
- out = append(out, ObjState{
- DefID: st.DefID,
- Name: st.Name,
- Index: st.Index,
- RoomID: st.RoomID,
- Depleted: st.Depleted,
- DepleteTimer: st.DepleteTimer,
- SharedMax: st.SharedMax,
- SharedTimer: st.SharedTimer,
- Quality: st.Quality,
- })
- }
- sort.Slice(out, func(i, j int) bool {
- if out[i].DefID != out[j].DefID {
- return out[i].DefID < out[j].DefID
- }
- return out[i].Index < out[j].Index
- })
- return out
-}
-
-func wordMatchesObj(lower, defID, objName string) bool {
- if WordPrefixMatch(lower, objName) {
- return true
- }
- inputWords := strings.Fields(strings.ToLower(lower))
-
- if strings.HasPrefix(strings.ToLower(defID), lower) || strings.HasPrefix(lower, strings.ToLower(defID)) {
- return true
- }
- for _, part := range strings.Split(defID, "_") {
- for _, iw := range inputWords {
- pl := strings.ToLower(part)
- if strings.HasPrefix(pl, iw) || strings.HasPrefix(iw, pl) {
- return true
- }
- }
- }
- return false
-}
-
-func (w *World) SetObjName(roomID int, defID string, name string) {
- w.mu.Lock()
- defer w.mu.Unlock()
- for _, st := range w.objStates {
- if st.RoomID == roomID && st.DefID == defID {
- st.Name = name
- }
- }
-}
-
-func (w *World) SetObjWander(roomID int, defID string, rooms []int, interval float64) {
- w.mu.Lock()
- defer w.mu.Unlock()
- for _, st := range w.objStates {
- if st.RoomID == roomID && st.DefID == defID {
- st.WanderRooms = rooms
- st.WanderInterval = interval
- }
- }
-}
-
-func (w *World) SetObjDepleteTimer(roomID int, defID string, max float64) {
- w.mu.Lock()
- defer w.mu.Unlock()
- for _, st := range w.objStates {
- if st.RoomID == roomID && st.DefID == defID && st.SharedMax == 0 {
- st.SharedMax = max
- st.SharedTimer = int(max)
- }
- }
-}
-
-func (w *World) AllSharedObjStates() []*ObjState {
- w.mu.Lock()
- defer w.mu.Unlock()
- var out []*ObjState
- for _, st := range w.objStates {
- if st.SharedMax > 0 {
- out = append(out, st)
- }
- }
- return out
-}
-
-func (w *World) SetObjDepleted(roomID int, defID string, index int, delay float64) {
- w.mu.Lock()
- defer w.mu.Unlock()
- key := w.objStateKey(roomID, defID, index)
- if st, ok := w.objStates[key]; ok {
- st.Depleted = true
- st.DepleteTimer = delay
- }
-}
-
func New(dataDir string) *World {
return &World{
dataDir: dataDir,
@@ -342,129 +67,6 @@ func (w *World) ResolveExit(input string) ExitDir {
return ""
}
-func (w *World) GroundItemsDetailed(roomID int) []GroundItemInfo {
- w.mu.Lock()
- defer w.mu.Unlock()
- var out []GroundItemInfo
- for _, e := range w.groundItems[roomID] {
- if e.quantity <= 0 {
- continue
- }
- info := GroundItemInfo{
- ItemID: e.itemID,
- Quantity: e.quantity,
- DespawnTimer: e.despawnTimer,
- IsSpawn: e.isSpawn,
- }
- if e.reserveTimer > 0 && e.reservedFor != "" {
- info.ReservedFor = e.reservedFor
- info.ReserveTimer = e.reserveTimer
- }
- out = append(out, info)
- }
- return out
-}
-
-func (w *World) GroundItems(roomID int) map[string]int {
- w.mu.Lock()
- defer w.mu.Unlock()
- out := make(map[string]int)
- for _, e := range w.groundItems[roomID] {
- if e.quantity > 0 {
- out[e.itemID] += e.quantity
- }
- }
- return out
-}
-
-func (w *World) AddReservedGroundItem(roomID int, itemID string, qty int, owner string) {
- w.mu.Lock()
- defer w.mu.Unlock()
- e := &groundEntry{
- itemID: itemID,
- quantity: qty,
- despawnTimer: DropDespawnTicks,
- reservedFor: owner,
- reserveTimer: ReserveTicks,
- }
- w.groundItems[roomID] = append(w.groundItems[roomID], e)
-}
-
-func (w *World) AddGroundItem(roomID int, itemID string, qty int) {
- w.mu.Lock()
- defer w.mu.Unlock()
- e := &groundEntry{
- itemID: itemID,
- quantity: qty,
- despawnTimer: DropDespawnTicks,
- }
- w.groundItems[roomID] = append(w.groundItems[roomID], e)
-}
-
-func (w *World) RemoveReservedGroundItem(roomID int, itemID string, qty int, owner string) (int, bool) {
- w.mu.Lock()
- defer w.mu.Unlock()
- removed := 0
- remaining := qty
- for _, e := range w.groundItems[roomID] {
- if remaining <= 0 {
- break
- }
- if !strings.EqualFold(e.itemID, itemID) {
- continue
- }
- if e.quantity <= 0 {
- continue
- }
- if e.reserveTimer > 0 && e.reservedFor != "" && e.reservedFor != owner {
- return removed, false
- }
- take := e.quantity
- if take > remaining {
- take = remaining
- }
- e.quantity -= take
- removed += take
- remaining -= take
- if e.isSpawn && e.quantity <= 0 && e.respawnDelay > 0 {
- e.respawnTimer = e.respawnDelay
- }
- }
- return removed, true
-}
-
-func (w *World) RemoveGroundItem(roomID int, itemID string, qty int) int {
- w.mu.Lock()
- defer w.mu.Unlock()
-
- removed := 0
- remaining := qty
-
- for _, e := range w.groundItems[roomID] {
- if remaining <= 0 {
- break
- }
- if !strings.EqualFold(e.itemID, itemID) {
- continue
- }
- if e.quantity <= 0 {
- continue
- }
- take := e.quantity
- if take > remaining {
- take = remaining
- }
- e.quantity -= take
- removed += take
- remaining -= take
-
- if e.isSpawn && e.quantity <= 0 && e.respawnDelay > 0 {
- e.respawnTimer = e.respawnDelay
- }
- }
- return removed
-}
-
func (w *World) SeedGroundItems(roomID int) {
w.mu.Lock()
if w.seeded[roomID] {
@@ -512,110 +114,6 @@ func (w *World) SeedGroundItems(roomID int) {
func (w *World) Tick() {
w.mu.Lock()
defer w.mu.Unlock()
-
- for _, entries := range w.groundItems {
- for _, e := range entries {
- if e.respawnTimer > 0 {
- e.respawnTimer--
- if e.respawnTimer <= 0 {
- e.quantity = e.respawnQty
- for _, other := range entries {
- if other != e && other.quantity > 0 && strings.EqualFold(other.itemID, e.itemID) &&
- (other.reserveTimer <= 0 || other.reservedFor == "") &&
- other.despawnTimer <= 0 {
- e.quantity += other.quantity
- other.quantity = 0
- }
- }
- }
- }
- if e.despawnTimer > 0 {
- e.despawnTimer--
- if e.despawnTimer <= 0 {
- e.quantity = 0
- }
- }
- if e.reserveTimer > 0 {
- e.reserveTimer--
- if e.reserveTimer <= 0 {
- e.reservedFor = ""
- }
- }
- }
- }
-
- for _, st := range w.objStates {
- if st.Depleted && st.DepleteTimer > 0 {
- st.DepleteTimer--
- if st.DepleteTimer <= 0 {
- st.Depleted = false
- st.JustRespawned = true
- if st.SharedMax > 0 {
- st.SharedTimer = int(st.SharedMax)
- }
- }
- }
- }
-}
-
-func (w *World) FlushObjRespawns() []ObjState {
- w.mu.Lock()
- defer w.mu.Unlock()
- var out []ObjState
- for _, st := range w.objStates {
- if st.JustRespawned {
- out = append(out, *st)
- st.JustRespawned = false
- }
- }
- return out
-}
-
-func (w *World) TickObjWander() {
- w.mu.Lock()
- defer w.mu.Unlock()
- var moves []struct {
- st *ObjState
- toRoom int
- }
- for _, st := range w.objStates {
- if len(st.WanderRooms) == 0 || st.WanderInterval <= 0 {
- continue
- }
- st.WanderCounter++
- if float64(st.WanderCounter) < st.WanderInterval {
- continue
- }
- st.WanderCounter = 0
- toRoom := st.WanderRooms[rand.Intn(len(st.WanderRooms))]
- if toRoom == st.RoomID {
- continue
- }
- moves = append(moves, struct {
- st *ObjState
- toRoom int
- }{st, toRoom})
- }
- for _, m := range moves {
- oldKey := w.objStateKey(m.st.RoomID, m.st.DefID, m.st.Index)
- fromRoom := m.st.RoomID
- m.st.RoomID = m.toRoom
- newKey := w.objStateKey(m.st.RoomID, m.st.DefID, m.st.Index)
- delete(w.objStates, oldKey)
- w.objStates[newKey] = m.st
- w.objMoves = append(w.objMoves, ObjMove{
- DefID: m.st.DefID,
- Name: m.st.Name,
- FromRoom: fromRoom,
- ToRoom: m.toRoom,
- })
- }
-}
-
-func (w *World) FlushObjMoves() []ObjMove {
- w.mu.Lock()
- defer w.mu.Unlock()
- out := w.objMoves
- w.objMoves = nil
- return out
+ w.tickGroundItemsLocked()
+ w.tickObjStatesLocked()
}