aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-19 21:20:32 -0400
committerhistoria <[not public]>2026-06-19 21:20:32 -0400
commit5ea082ab4e82409aebc889b496f43e52b003d4f7 (patch)
tree942f2c4a18ecbee42ba25726048d09827ea68894 /internal
parent8ee8982b8759bcae116647cc993867f4c8b0a6ce (diff)
downloadthehouseoficarus-5ea082ab4e82409aebc889b496f43e52b003d4f7.tar.gz
feat: science combat/triggers overhauled. decks now use autotrigger attacks.
Diffstat (limited to 'internal')
-rw-r--r--internal/game/cmd_attack.go85
-rw-r--r--internal/game/cmd_autotrigger.go5
-rw-r--r--internal/game/cmd_registry.go2
-rw-r--r--internal/game/cmd_trigger.go16
-rw-r--r--internal/game/cmd_trigger_combat.go3
-rw-r--r--internal/game/sys_science.go53
-rw-r--r--internal/player/player.go2
7 files changed, 139 insertions, 27 deletions
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index 222e3c3..f9cfba2 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -129,12 +129,9 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
g.cancelBackgroundAction(p)
combat.EnterCombat(p.Name, mob.InstanceID)
+ p.AttackTimer = 0
autotriggerActive := p.AutotriggerMod != ""
- playerSpeed := g.playerWeaponSpeed(p)
- if autotriggerActive {
- playerSpeed = 5.0
- }
if !autotriggerActive {
attBonus, strBonus, defBonus := combat.AttackStyleBonus(string(p.AttackStyle))
@@ -173,7 +170,7 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
}
}
- g.Ticks.Subscribe(engine.ToTicks(playerSpeed), func() bool {
+ g.Ticks.Subscribe(1, func() bool {
cs := combat.GetCombat(p.Name)
if cs == nil || !cs.Active {
return false
@@ -183,21 +180,46 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
g.endCombat(sess, p, currentMob)
return false
}
- if p.AutotriggerMod != "" {
- mod := GetMod(p.AutotriggerMod)
- if mod != nil {
- if !g.scienceAttack(sess, p, currentMob, mod) {
- p.AutotriggerMod = ""
- sess.WriteLine("You've run out of junk. Switching to melee.")
- g.playerAttack(sess, p, currentMob)
+
+ p.AttackTimer++
+ if float64(p.AttackTimer) >= g.playerWeaponSpeed(p) {
+ p.AttackTimer = 0
+
+ if g.hasDeckEquipped(p) {
+ if p.AutotriggerMod != "" {
+ mod := GetMod(p.AutotriggerMod)
+ if mod != nil && g.hasJunkCost(p, mod) {
+ g.scienceAttack(sess, p, currentMob, mod)
+ } else if mod != nil {
+ sess.WriteLine(g.colorize(sess, "error",
+ fmt.Sprintf("\nYou're out of junk for %s!! You flail with your fists in desperation!", mod.Name)))
+ g.playerAttackUnarmed(sess, p, currentMob)
+ } else {
+ p.AutotriggerMod = ""
+ sess.WriteLine(g.colorize(sess, "error",
+ "\nNo autotrigger set! Set a module with the 'autotrigger' command."))
+ g.playerAttackUnarmed(sess, p, currentMob)
+ }
+ } else {
+ sess.WriteLine(g.colorize(sess, "error",
+ "\nNo autotrigger set! Set a module with the 'autotrigger' command."))
+ g.playerAttackUnarmed(sess, p, currentMob)
}
} else {
- p.AutotriggerMod = ""
- g.playerAttack(sess, p, currentMob)
+ if p.QueuedTrigger != "" {
+ mod := GetMod(p.QueuedTrigger)
+ p.QueuedTrigger = ""
+ if mod != nil && g.hasJunkCost(p, mod) {
+ g.scienceAttack(sess, p, currentMob, mod)
+ } else {
+ g.playerAttack(sess, p, currentMob)
+ }
+ } else {
+ g.playerAttack(sess, p, currentMob)
+ }
}
- } else {
- g.playerAttack(sess, p, currentMob)
}
+
if currentMob.HP <= 0 {
g.endCombat(sess, p, currentMob)
return false
@@ -284,6 +306,37 @@ func (g *Game) calculatePlayerAttackRoll(p *player.Player, mob *world.MobInstanc
return
}
+func (g *Game) playerAttackUnarmed(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ if g.processConsumeQueue(p, sess) {
+ p.ActionState = &ActionState{Type: ActionEating, TargetName: "food"}
+ return
+ }
+
+ attackType, _, attRoll, defRoll, maxHit := g.calculateUnarmedAttackRoll(p, mob)
+
+ if combat.HitCheck(attRoll, defRoll) {
+ g.applyPlayerHit(sess, p, mob, attackType, object.WeaponMelee, maxHit)
+ } else {
+ g.applyPlayerMiss(sess, p, mob, object.WeaponMelee)
+ }
+}
+
+func (g *Game) calculateUnarmedAttackRoll(p *player.Player, mob *world.MobInstance) (attackType string, weaponType object.WeaponType, attRoll int, defRoll int, maxHit int) {
+ attackType = "crush"
+ weaponType = object.WeaponMelee
+
+ attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle))
+ effectiveAttack := p.Level(player.Attack) + g.techLevelBonus(p, "attack") + g.buffLevelBonus(p, "attack")
+ attRoll = combat.AttackRoll(effectiveAttack, attBonus, 0)
+
+ mobDefBonus := combat.SelectDefenseBonus(attackType,
+ mob.StabDefense, mob.SlashDefense, mob.CrushDefense,
+ mob.ScienceDefense, mob.RangedDefense)
+ defRoll = combat.DefenseRoll(mob.Defense, 0, mobDefBonus)
+ maxHit = combat.MaxHit(p.Level(player.Strength)+g.techLevelBonus(p, "strength")+g.buffLevelBonus(p, "strength"), strBonus, 0)
+ return
+}
+
func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.MobInstance, attackType string, weaponType object.WeaponType, maxHit int) {
dmg := combat.RollDamage(maxHit)
diff --git a/internal/game/cmd_autotrigger.go b/internal/game/cmd_autotrigger.go
index e2ecd1a..acfa76e 100644
--- a/internal/game/cmd_autotrigger.go
+++ b/internal/game/cmd_autotrigger.go
@@ -27,13 +27,14 @@ func (g *Game) doAutotrigger(sess *net.Session, input string) {
return
}
- if strings.ToLower(input) == "off" {
+ lower := strings.ToLower(input)
+ if lower == "off" || lower == "none" {
p.AutotriggerMod = ""
sess.WriteLine("Autotrigger disabled.")
return
}
- mod := FindMod(strings.ToLower(input))
+ mod := FindModForPlayer(lower, p.Level(player.Science))
if mod == nil {
sess.WriteLine("Unknown mod.")
return
diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go
index 0439cc2..c4f7700 100644
--- a/internal/game/cmd_registry.go
+++ b/internal/game/cmd_registry.go
@@ -83,6 +83,8 @@ var commandRegistry = map[string]commandDef{
"mods": {(*Game).executeMods, ClassInstant},
"modlist": {(*Game).executeMods, ClassInstant},
"mod": {(*Game).executeMods, ClassInstant},
+ "modules": {(*Game).executeMods, ClassInstant},
+ "module": {(*Game).executeMods, ClassInstant},
"sneak": {(*Game).executeSneak, ClassInstant},
"trigger": {(*Game).executeTrigger, ClassActive},
"talk": {(*Game).executeTalk, ClassActive},
diff --git a/internal/game/cmd_trigger.go b/internal/game/cmd_trigger.go
index 7d637ca..0acbeff 100644
--- a/internal/game/cmd_trigger.go
+++ b/internal/game/cmd_trigger.go
@@ -22,7 +22,7 @@ func (g *Game) doTrigger(sess *net.Session, input string) {
return
}
- mod, targetArg := g.parseTriggerArgs(input)
+ mod, targetArg := g.parseTriggerArgs(input, p.Level(player.Science))
if mod == nil {
sess.WriteLine("Unknown mod. Type 'mods' to see available mods.")
return
@@ -50,12 +50,12 @@ func (g *Game) doTrigger(sess *net.Session, input string) {
}
}
-func (g *Game) parseTriggerArgs(input string) (*ModDef, string) {
+func (g *Game) parseTriggerArgs(input string, sciLevel int) (*ModDef, string) {
lower := strings.ToLower(input)
words := strings.Fields(lower)
for i := len(words); i > 0; i-- {
candidate := strings.Join(words[:i], " ")
- mod := FindMod(candidate)
+ mod := FindModForPlayer(candidate, sciLevel)
if mod != nil {
target := strings.TrimSpace(strings.Join(words[i:], " "))
return mod, target
@@ -66,8 +66,8 @@ 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.AutotriggerMod = mod.ID
- sess.WriteLine(fmt.Sprintf("You switch to triggering %s.", mod.Name))
+ p.QueuedTrigger = mod.ID
+ sess.WriteLine(fmt.Sprintf("You queue %s for your next attack.", mod.Name))
return
}
@@ -106,6 +106,10 @@ func (g *Game) triggerCombatMod(sess *net.Session, p *player.Player, mod *ModDef
return
}
- p.AutotriggerMod = mod.ID
+ if g.hasDeckEquipped(p) {
+ p.AutotriggerMod = mod.ID
+ } else {
+ p.QueuedTrigger = mod.ID
+ }
g.startCombat(sess, p, mob)
}
diff --git a/internal/game/cmd_trigger_combat.go b/internal/game/cmd_trigger_combat.go
index 545a117..62c5083 100644
--- a/internal/game/cmd_trigger_combat.go
+++ b/internal/game/cmd_trigger_combat.go
@@ -15,9 +15,6 @@ func (g *Game) scienceAttack(sess *net.Session, p *player.Player, mob *world.Mob
sess.WriteLine(fmt.Sprintf("You need level %d science to trigger %s.", mod.Level, mod.Name))
return false
}
- if !g.hasJunkCost(p, mod) {
- return false
- }
if g.processConsumeQueue(p, sess) {
p.ActionState = &ActionState{Type: ActionEating, TargetName: "food"}
diff --git a/internal/game/sys_science.go b/internal/game/sys_science.go
index 23aeef4..e35105a 100644
--- a/internal/game/sys_science.go
+++ b/internal/game/sys_science.go
@@ -81,6 +81,59 @@ func FindMod(input string) *ModDef {
return nil
}
+func findAllModMatches(input string) []*ModDef {
+ inputWords := strings.Fields(strings.ToLower(input))
+ if len(inputWords) == 0 {
+ return nil
+ }
+ var matches []*ModDef
+ for _, m := range AllMods {
+ lowerName := strings.ToLower(m.Name)
+ lowerID := strings.ToLower(m.ID)
+ allMatch := true
+ for _, word := range inputWords {
+ if !strings.Contains(lowerName, word) && !strings.Contains(lowerID, word) {
+ allMatch = false
+ break
+ }
+ }
+ if allMatch {
+ matches = append(matches, m)
+ }
+ }
+ return matches
+}
+
+func FindModForPlayer(input string, sciLevel int) *ModDef {
+ if m, ok := modByID[input]; ok {
+ return m
+ }
+ lower := strings.ToLower(strings.ReplaceAll(input, " ", "_"))
+ for _, m := range AllMods {
+ if strings.HasPrefix(m.ID, lower) {
+ return m
+ }
+ }
+
+ matches := findAllModMatches(input)
+ if len(matches) == 0 {
+ return nil
+ }
+ if len(matches) == 1 {
+ return matches[0]
+ }
+
+ sort.Slice(matches, func(i, j int) bool {
+ return matches[i].Level > matches[j].Level
+ })
+ for _, m := range matches {
+ if m.Level <= sciLevel {
+ return m
+ }
+ }
+ return matches[len(matches)-1]
+}
+
type chipEntry struct {
Input string
Output string
diff --git a/internal/player/player.go b/internal/player/player.go
index 3842348..1ddb3ea 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -180,6 +180,8 @@ type Player struct {
MoveTarget int `yaml:"-"`
VisualTickCurrent int `yaml:"-"`
AutotriggerMod string `yaml:"-"`
+ QueuedTrigger string `yaml:"-"`
+ AttackTimer int `yaml:"-"`
Battery float64 `yaml:"battery"`
ActiveTechs map[string]bool `yaml:"-"`
QuickTech string `yaml:"quick_tech,omitempty"`