aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/cmd_attack.go28
-rw-r--r--internal/game/cmd_score.go15
-rw-r--r--internal/game/cmd_tech.go196
-rw-r--r--internal/game/cmd_use.go4
-rw-r--r--internal/game/game.go6
-rw-r--r--internal/game/prompt.go2
-rw-r--r--internal/game/tech.go250
-rw-r--r--internal/game/tick.go77
8 files changed, 571 insertions, 7 deletions
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index 4d2d19f..401837f 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -209,25 +209,27 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI
if isRanged {
rangedBonus, _ := combat.RangedStyleBonus(string(p.AttackStyle))
equipAttack := totals.RangedAttack
- attRoll = combat.AttackRoll(p.Level(player.Ranged), rangedBonus, equipAttack)
+ effectiveRanged := p.Level(player.Ranged) + g.techLevelBonus(p, "ranged")
+ attRoll = combat.AttackRoll(effectiveRanged, rangedBonus, equipAttack)
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.Ranged), rangedBonus, totals.RangedStrength)
+ maxHit = combat.MaxHit(effectiveRanged, rangedBonus, totals.RangedStrength)
} else {
attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle))
equipAttack := combat.SelectAttackBonus(attackType,
totals.StabAttack, totals.SlashAttack, totals.CrushAttack,
totals.ScienceAttack, totals.RangedAttack)
- attRoll = combat.AttackRoll(p.Level(player.Attack), attBonus, equipAttack)
+ effectiveAttack := p.Level(player.Attack) + g.techLevelBonus(p, "attack")
+ attRoll = combat.AttackRoll(effectiveAttack, attBonus, equipAttack)
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), strBonus, totals.StrengthBonus)
+ maxHit = combat.MaxHit(p.Level(player.Strength)+g.techLevelBonus(p, "strength"), strBonus, totals.StrengthBonus)
}
if combat.HitCheck(attRoll, defRoll) {
@@ -311,11 +313,12 @@ func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInst
equipDef := combat.SelectDefenseBonus(mobAttackType,
totals.StabDefense, totals.SlashDefense, totals.CrushDefense,
totals.ScienceDefense, totals.RangedDefense)
- defRoll := combat.DefenseRoll(p.Level(player.Defense), defStyleBonus, equipDef)
+ defRoll := combat.DefenseRoll(p.Level(player.Defense)+g.techLevelBonus(p, "defense"), defStyleBonus, equipDef)
if combat.HitCheck(attRoll, defRoll) {
maxHit := combat.MaxHit(mob.Strength, 0, mob.StrengthBonus)
dmg := combat.RollDamage(maxHit)
+ dmg = g.applyTechProtection(p, mob, dmg)
p.HP -= dmg
if p.HP < 0 {
@@ -358,6 +361,21 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
p.ActionState = nil
if p.HP <= 0 {
+ if p.HasActiveTech("retribution") {
+ retDef := GetTechDef("retribution")
+ if retDef != nil && mob != nil && mob.HP > 0 {
+ retDmg := int(float64(p.MaxHP()) * retDef.Effects.RetributionPct)
+ if retDmg > 0 {
+ mob.HP -= retDmg
+ if mob.HP < 0 {
+ mob.HP = 0
+ }
+ sess.WriteLine(fmt.Sprintf("\nDead Man's Switch activates! %s takes %d damage!",
+ mobDisplayName(mob, true), retDmg))
+ }
+ }
+ }
+ p.DeactivateAllTechs()
sess.WriteLine(g.colorize(sess, "death", "\nOh dear, you are dead!"))
p.ClearMoveState()
g.dropItemsOnDeath(p)
diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go
index c31433b..2005903 100644
--- a/internal/game/cmd_score.go
+++ b/internal/game/cmd_score.go
@@ -17,6 +17,7 @@ func (g *Game) doScore(sess *net.Session) {
fmt.Sprintf("Name: %s", g.colorize(sess, "player_name", p.Name)),
fmt.Sprintf("Combat Level: %s", color.Render(mode, color.Parse("230"), fmt.Sprint(p.CombatLevel()))),
fmt.Sprintf("HP: %s/%s", g.colorize(sess, "character_hp", fmt.Sprint(p.HP)), color.Render(mode, color.Parse("230"), fmt.Sprint(p.MaxHP()))),
+ fmt.Sprintf("Battery: %s/%s", g.colorize(sess, "battery", fmt.Sprintf("%.1f", p.Battery)), color.Render(mode, color.Parse("230"), fmt.Sprintf("%.0f", p.MaxBattery()))),
fmt.Sprintf("Credits: %s", g.colorize(sess, "credits_pickup", fmt.Sprint(p.Credits))),
)
@@ -42,4 +43,18 @@ func (g *Game) doScore(sess *net.Session) {
for _, line := range t.Render(p.OptionBool("unicode")) {
sess.WriteLine(line)
}
+
+ if len(p.ActiveTechs) > 0 {
+ sess.WriteLine("")
+ sess.WriteLine("Active Tech:")
+ for _, id := range p.ActiveTechList() {
+ def := GetTechDef(id)
+ if def != nil {
+ sess.WriteLine(fmt.Sprintf(" %s %s",
+ color.Render(mode, color.Parse("82"), "[ON]"),
+ color.Render(mode, color.Parse("75"), def.Name),
+ ))
+ }
+ }
+ }
}
diff --git a/internal/game/cmd_tech.go b/internal/game/cmd_tech.go
new file mode 100644
index 0000000..f51ccd9
--- /dev/null
+++ b/internal/game/cmd_tech.go
@@ -0,0 +1,196 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) doTech(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+ techLevel := p.Level(player.Technology)
+
+ if techLevel < 1 {
+ sess.WriteLine("\nYou need at least level 1 Technology to use tech.")
+ return
+ }
+
+ input = strings.TrimSpace(input)
+
+ if input == "" {
+ if p.QuickTech == "" {
+ g.doTechList(sess)
+ return
+ }
+ g.toggleTech(sess, p, p.QuickTech)
+ return
+ }
+
+ lower := strings.ToLower(input)
+
+ if lower == "list" {
+ g.doTechList(sess)
+ return
+ }
+
+ if strings.HasPrefix(lower, "quick ") {
+ name := strings.TrimSpace(input[6:])
+ g.doTechQuick(sess, p, name)
+ return
+ }
+
+ matches := TechByPrefixMatch(lower)
+ if len(matches) == 0 {
+ sess.WriteLine(fmt.Sprintf("\nUnknown tech: %s", input))
+ return
+ }
+ if len(matches) > 1 {
+ var names []string
+ for _, m := range matches {
+ names = append(names, m.Name)
+ }
+ sess.WriteLine(fmt.Sprintf("\nAmbiguous tech: %s. Matches: %s", input, strings.Join(names, ", ")))
+ return
+ }
+
+ g.toggleTech(sess, p, matches[0].ID)
+}
+
+func (g *Game) toggleTech(sess *net.Session, p *player.Player, techID string) {
+ def := GetTechDef(techID)
+ if def == nil {
+ sess.WriteLine("\nUnknown tech.")
+ return
+ }
+
+ techLevel := p.Level(player.Technology)
+
+ if p.HasActiveTech(techID) {
+ p.DeactivateTech(techID)
+ sess.WriteLine(fmt.Sprintf("\n%s deactivated.", def.Name))
+ return
+ }
+
+ if techLevel < def.Level {
+ sess.WriteLine(fmt.Sprintf("\nYou need level %d Technology to use %s.", def.Level, def.Name))
+ return
+ }
+
+ if p.Battery <= 0 {
+ sess.WriteLine("\nYour battery is depleted!")
+ return
+ }
+
+ deactivated := g.deactivateGroup(p, def.Group)
+ for _, name := range deactivated {
+ sess.WriteLine(fmt.Sprintf("\n%s deactivated.", name))
+ }
+
+ p.ActivateTech(techID)
+ sess.WriteLine(fmt.Sprintf("\n%s activated.", def.Name))
+}
+
+func (g *Game) deactivateGroup(p *player.Player, group string) []string {
+ if group == "" {
+ return nil
+ }
+ var deactivated []string
+ for id := range p.ActiveTechs {
+ def := GetTechDef(id)
+ if def != nil && def.Group == group {
+ p.DeactivateTech(id)
+ deactivated = append(deactivated, def.Name)
+ }
+ }
+ return deactivated
+}
+
+func (g *Game) doTechList(sess *net.Session) {
+ p := sess.Player.(*player.Player)
+ mode := g.colorMode(sess)
+ techLevel := p.Level(player.Technology)
+
+ sess.WriteLines(
+ "",
+ fmt.Sprintf("Battery: %s/%s",
+ g.colorize(sess, "battery", fmt.Sprintf("%.1f", p.Battery)),
+ color.Render(mode, color.Parse("230"), fmt.Sprintf("%.0f", p.MaxBattery())),
+ ),
+ )
+
+ t := &Table{Title: "Technology", Columns: []string{"Tech", "Level", "Drain/tick", "Effect", "Status"}}
+
+ for _, tech := range AllTechs {
+ status := ""
+ nameStr := tech.Name
+ if tech.Level <= techLevel {
+ if p.HasActiveTech(tech.ID) {
+ status = color.Render(mode, color.Parse("82"), "ON")
+ } else {
+ status = color.Render(mode, color.Parse("240"), "off")
+ }
+ nameStr = color.Render(mode, color.Parse("75"), tech.Name)
+ } else {
+ status = color.Render(mode, color.Parse("160"), "locked")
+ nameStr = color.Render(mode, color.Parse("240"), tech.Name)
+ }
+
+ effect := techEffectString(tech)
+
+ t.Rows = append(t.Rows, []string{
+ nameStr,
+ fmt.Sprint(tech.Level),
+ fmt.Sprintf("%.2f", tech.DrainRate),
+ effect,
+ status,
+ })
+ }
+
+ for _, line := range t.Render(p.OptionBool("unicode")) {
+ sess.WriteLine(line)
+ }
+
+ if p.QuickTech != "" {
+ qDef := GetTechDef(p.QuickTech)
+ if qDef != nil {
+ sess.WriteLine(fmt.Sprintf("\nQuick tech: %s", qDef.Name))
+ }
+ }
+}
+
+func (g *Game) doTechQuick(sess *net.Session, p *player.Player, input string) {
+ if input == "" {
+ if p.QuickTech == "" {
+ sess.WriteLine("\nNo quick tech set. Usage: tech quick <name>")
+ } else {
+ def := GetTechDef(p.QuickTech)
+ name := p.QuickTech
+ if def != nil {
+ name = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("\nQuick tech: %s", name))
+ }
+ return
+ }
+
+ matches := TechByPrefixMatch(strings.ToLower(input))
+ if len(matches) == 0 {
+ sess.WriteLine(fmt.Sprintf("\nUnknown tech: %s", input))
+ return
+ }
+ if len(matches) > 1 {
+ var names []string
+ for _, m := range matches {
+ names = append(names, m.Name)
+ }
+ sess.WriteLine(fmt.Sprintf("\nAmbiguous tech: %s. Matches: %s", input, strings.Join(names, ", ")))
+ return
+ }
+
+ p.QuickTech = matches[0].ID
+ g.AccountStore.SaveCharacter(p)
+ sess.WriteLine(fmt.Sprintf("\nQuick tech set to %s.", matches[0].Name))
+}
diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go
index 4d4ce88..7d40cc2 100644
--- a/internal/game/cmd_use.go
+++ b/internal/game/cmd_use.go
@@ -19,6 +19,10 @@ func (g *Game) doUse(sess *net.Session, input string) {
p := sess.Player.(*player.Player)
g.CancelAction(p)
+ if g.tryRecharge(sess, p, input) {
+ return
+ }
+
lower := strings.ToLower(input)
sep := ""
diff --git a/internal/game/game.go b/internal/game/game.go
index 8b5f924..15236f4 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -82,6 +82,7 @@ func (g *Game) SetHub(hub *net.Hub) {
g.Hub = hub
hub.OnRemove(func(sess *net.Session) {
if p, ok := sess.Player.(*player.Player); ok {
+ p.DeactivateAllTechs()
g.charsMu.Lock()
delete(g.loggedInChars, p.Name)
g.charsMu.Unlock()
@@ -139,7 +140,8 @@ func classifyCommand(cmd string) CommandClass {
"look", "l", "exits", "help",
"map", "option", "options", "alias", "unalias",
"description", "desc", "queued", "color", "colors",
- "colortable", "prompt", "style", "stats":
+ "colortable", "prompt", "style", "stats",
+ "tech", "t":
return ClassInstant
case "equip", "eq", "equipment", "wear", "wield", "remove", "unwear", "unwield":
return ClassFree
@@ -316,6 +318,8 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
g.doScore(sess)
case "stats":
g.doStats(sess)
+ case "tech", "t":
+ g.doTech(sess, strings.Join(args, " "))
case "i", "inv", "inventory":
g.doInventory(sess)
case "quit":
diff --git a/internal/game/prompt.go b/internal/game/prompt.go
index de88bc6..c3768fe 100644
--- a/internal/game/prompt.go
+++ b/internal/game/prompt.go
@@ -36,6 +36,8 @@ func (g *Game) expandPromptVars(sess *net.Session, text string) string {
text = strings.ReplaceAll(text, "%%", "\x00")
text = strings.ReplaceAll(text, "%h", fmt.Sprint(p.HP))
text = strings.ReplaceAll(text, "%H", fmt.Sprint(p.MaxHP()))
+ text = strings.ReplaceAll(text, "%b", fmt.Sprintf("%.1f", p.Battery))
+ text = strings.ReplaceAll(text, "%B", fmt.Sprintf("%.0f", p.MaxBattery()))
text = strings.ReplaceAll(text, "%c", fmt.Sprint(p.Credits))
text = strings.ReplaceAll(text, "%i", fmt.Sprint(p.FreeSlots()))
text = strings.ReplaceAll(text, "%s", attackStyleShort(p.AttackStyle))
diff --git a/internal/game/tech.go b/internal/game/tech.go
new file mode 100644
index 0000000..a24c66d
--- /dev/null
+++ b/internal/game/tech.go
@@ -0,0 +1,250 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+type TechEffects struct {
+ AttackPercent int
+ StrengthPercent int
+ DefensePercent int
+ RangedPercent int
+ SciencePercent int
+ ProtectMelee bool
+ ProtectRanged bool
+ ProtectScience bool
+ DamageReduction float64
+ HPRegenMulti float64
+ PreserveDrain float64
+ RetributionPct float64
+}
+
+type TechDef struct {
+ ID string
+ Name string
+ Level int
+ DrainRate float64
+ Category string
+ Group string
+ Effects TechEffects
+}
+
+var AllTechs []TechDef
+var techByID map[string]*TechDef
+
+func init() {
+ AllTechs = []TechDef{
+ {ID: "clarity_1", Name: "Clarity", Level: 4, DrainRate: 0.05, Category: "attack", Group: "attack_tier", Effects: TechEffects{AttackPercent: 5}},
+ {ID: "clarity_2", Name: "Enhanced Clarity", Level: 16, DrainRate: 0.10, Category: "attack", Group: "attack_tier", Effects: TechEffects{AttackPercent: 10}},
+ {ID: "clarity_3", Name: "Superior Clarity", Level: 44, DrainRate: 0.15, Category: "attack", Group: "attack_tier", Effects: TechEffects{AttackPercent: 15}},
+
+ {ID: "amplifier_1", Name: "Power Amplifier", Level: 7, DrainRate: 0.05, Category: "strength", Group: "strength_tier", Effects: TechEffects{StrengthPercent: 5}},
+ {ID: "amplifier_2", Name: "Enhanced Amplifier", Level: 23, DrainRate: 0.10, Category: "strength", Group: "strength_tier", Effects: TechEffects{StrengthPercent: 10}},
+ {ID: "amplifier_3", Name: "Superior Amplifier", Level: 49, DrainRate: 0.15, Category: "strength", Group: "strength_tier", Effects: TechEffects{StrengthPercent: 15}},
+
+ {ID: "shield_1", Name: "Energy Shield", Level: 10, DrainRate: 0.05, Category: "defense", Group: "defense_tier", Effects: TechEffects{DefensePercent: 5}},
+ {ID: "shield_2", Name: "Enhanced Shield", Level: 28, DrainRate: 0.10, Category: "defense", Group: "defense_tier", Effects: TechEffects{DefensePercent: 10}},
+ {ID: "shield_3", Name: "Superior Shield", Level: 52, DrainRate: 0.15, Category: "defense", Group: "defense_tier", Effects: TechEffects{DefensePercent: 15}},
+
+ {ID: "targeting_1", Name: "Targeting System", Level: 8, DrainRate: 0.05, Category: "ranged", Group: "ranged_tier", Effects: TechEffects{RangedPercent: 5}},
+ {ID: "targeting_2", Name: "Enhanced Targeting", Level: 22, DrainRate: 0.10, Category: "ranged", Group: "ranged_tier", Effects: TechEffects{RangedPercent: 10}},
+ {ID: "targeting_3", Name: "Superior Targeting", Level: 46, DrainRate: 0.15, Category: "ranged", Group: "ranged_tier", Effects: TechEffects{RangedPercent: 15}},
+
+ {ID: "focus_1", Name: "Neural Focus", Level: 9, DrainRate: 0.05, Category: "science", Group: "science_tier", Effects: TechEffects{SciencePercent: 5}},
+ {ID: "focus_2", Name: "Enhanced Focus", Level: 27, DrainRate: 0.10, Category: "science", Group: "science_tier", Effects: TechEffects{SciencePercent: 10}},
+ {ID: "focus_3", Name: "Superior Focus", Level: 55, DrainRate: 0.15, Category: "science", Group: "science_tier", Effects: TechEffects{SciencePercent: 15}},
+
+ {ID: "protect_melee", Name: "Kinetic Barrier", Level: 37, DrainRate: 0.20, Category: "protection", Group: "protection", Effects: TechEffects{ProtectMelee: true, DamageReduction: 0.4}},
+ {ID: "protect_ranged", Name: "Projectile Screen", Level: 40, DrainRate: 0.20, Category: "protection", Group: "protection", Effects: TechEffects{ProtectRanged: true, DamageReduction: 0.4}},
+ {ID: "protect_science", Name: "Neural Firewall", Level: 43, DrainRate: 0.20, Category: "protection", Group: "protection", Effects: TechEffects{ProtectScience: true, DamageReduction: 0.4}},
+
+ {ID: "regen", Name: "Nano Repair", Level: 22, DrainRate: 0.10, Category: "utility", Group: "regen_tier", Effects: TechEffects{HPRegenMulti: 2.0}},
+ {ID: "rapid_heal", Name: "Rapid Repair", Level: 31, DrainRate: 0.15, Category: "utility", Group: "regen_tier", Effects: TechEffects{HPRegenMulti: 4.0}},
+ {ID: "preserve", Name: "Power Saver", Level: 55, DrainRate: 0.05, Category: "utility", Group: "", Effects: TechEffects{PreserveDrain: 0.2}},
+ {ID: "retribution", Name: "Dead Man's Switch", Level: 46, DrainRate: 0.10, Category: "utility", Group: "", Effects: TechEffects{RetributionPct: 0.25}},
+
+ {ID: "overclock", Name: "Overclock", Level: 60, DrainRate: 0.25, Category: "combo", Group: "attack_tier", Effects: TechEffects{AttackPercent: 15, StrengthPercent: 15}},
+ {ID: "fortify", Name: "Fortify", Level: 65, DrainRate: 0.25, Category: "combo", Group: "defense_tier", Effects: TechEffects{DefensePercent: 15, HPRegenMulti: 2.0}},
+ }
+
+ techByID = make(map[string]*TechDef, len(AllTechs))
+ for i := range AllTechs {
+ techByID[AllTechs[i].ID] = &AllTechs[i]
+ }
+}
+
+func GetTechDef(id string) *TechDef {
+ return techByID[id]
+}
+
+func TechByPrefixMatch(input string) []*TechDef {
+ input = strings.ToLower(input)
+ var exact []*TechDef
+ var prefix []*TechDef
+ for i := range AllTechs {
+ lower := strings.ToLower(AllTechs[i].Name)
+ lowerID := strings.ToLower(AllTechs[i].ID)
+ if lower == input || lowerID == input {
+ exact = append(exact, &AllTechs[i])
+ } else if strings.HasPrefix(lower, input) || strings.HasPrefix(lowerID, input) {
+ prefix = append(prefix, &AllTechs[i])
+ }
+ }
+ if len(exact) > 0 {
+ return exact
+ }
+ return prefix
+}
+
+func techEffectString(tech TechDef) string {
+ var parts []string
+ e := tech.Effects
+ if e.AttackPercent > 0 {
+ parts = append(parts, fmt.Sprintf("+%d%% Attack", e.AttackPercent))
+ }
+ if e.StrengthPercent > 0 {
+ parts = append(parts, fmt.Sprintf("+%d%% Strength", e.StrengthPercent))
+ }
+ if e.DefensePercent > 0 {
+ parts = append(parts, fmt.Sprintf("+%d%% Defense", e.DefensePercent))
+ }
+ if e.RangedPercent > 0 {
+ parts = append(parts, fmt.Sprintf("+%d%% Ranged", e.RangedPercent))
+ }
+ if e.SciencePercent > 0 {
+ parts = append(parts, fmt.Sprintf("+%d%% Science", e.SciencePercent))
+ }
+ if e.ProtectMelee {
+ parts = append(parts, fmt.Sprintf("%.0f%% melee protection", e.DamageReduction*100))
+ }
+ if e.ProtectRanged {
+ parts = append(parts, fmt.Sprintf("%.0f%% ranged protection", e.DamageReduction*100))
+ }
+ if e.ProtectScience {
+ parts = append(parts, fmt.Sprintf("%.0f%% science protection", e.DamageReduction*100))
+ }
+ if e.HPRegenMulti > 0 {
+ parts = append(parts, fmt.Sprintf("%.0fx HP regen", e.HPRegenMulti))
+ }
+ if e.PreserveDrain > 0 {
+ parts = append(parts, fmt.Sprintf("%.0f%% drain reduction", e.PreserveDrain*100))
+ }
+ if e.RetributionPct > 0 {
+ parts = append(parts, fmt.Sprintf("%.0f%% retribution", e.RetributionPct*100))
+ }
+ return strings.Join(parts, ", ")
+}
+
+func (g *Game) totalTechBonus(p *player.Player) int {
+ total := 0
+ for _, itemID := range p.Equipment {
+ def, err := g.ItemStore.Load(itemID)
+ if err == nil {
+ total += def.Stats.TechnologyBonus
+ }
+ }
+ return total
+}
+
+func (g *Game) techLevelBonus(p *player.Player, stat string) int {
+ if len(p.ActiveTechs) == 0 {
+ return 0
+ }
+
+ totalPercent := 0
+ for id := range p.ActiveTechs {
+ def := GetTechDef(id)
+ if def == nil {
+ continue
+ }
+ switch stat {
+ case "attack":
+ totalPercent += def.Effects.AttackPercent
+ case "strength":
+ totalPercent += def.Effects.StrengthPercent
+ case "defense":
+ totalPercent += def.Effects.DefensePercent
+ case "ranged":
+ totalPercent += def.Effects.RangedPercent
+ case "science":
+ totalPercent += def.Effects.SciencePercent
+ }
+ }
+
+ if totalPercent == 0 {
+ return 0
+ }
+
+ var baseLevel int
+ switch stat {
+ case "attack":
+ baseLevel = p.Level(player.Attack)
+ case "strength":
+ baseLevel = p.Level(player.Strength)
+ case "defense":
+ baseLevel = p.Level(player.Defense)
+ case "ranged":
+ baseLevel = p.Level(player.Ranged)
+ case "science":
+ baseLevel = p.Level(player.Science)
+ }
+
+ return baseLevel * totalPercent / 100
+}
+
+func (g *Game) tryRecharge(sess *net.Session, p *player.Player, input string) bool {
+ lower := strings.ToLower(strings.TrimSpace(input))
+ instances := g.World.FindObjInstances(p.RoomID, lower)
+ for _, st := range instances {
+ if st.DefID == "charging_station" || st.DefID == "power_conduit" {
+ if p.Battery >= p.MaxBattery() {
+ sess.WriteLine("\nYour battery is already full.")
+ } else {
+ p.Battery = p.MaxBattery()
+ g.AccountStore.SaveCharacter(p)
+ sess.WriteLine(g.colorize(sess, "battery",
+ "\nYou connect to the charging station. Your battery is fully recharged."))
+ }
+ return true
+ }
+ }
+ return false
+}
+
+func (g *Game) applyTechProtection(p *player.Player, mob *world.MobInstance, dmg int) int {
+ if len(p.ActiveTechs) == 0 {
+ return dmg
+ }
+
+ attackType := mob.AttackType
+ if attackType == "" {
+ attackType = "crush"
+ }
+
+ var protectTechID string
+ switch attackType {
+ case "stab", "slash", "crush":
+ protectTechID = "protect_melee"
+ case "ranged":
+ protectTechID = "protect_ranged"
+ case "science":
+ protectTechID = "protect_science"
+ }
+
+ if protectTechID != "" && p.HasActiveTech(protectTechID) {
+ def := GetTechDef(protectTechID)
+ if def != nil {
+ reduced := int(float64(dmg) * (1.0 - def.Effects.DamageReduction))
+ if reduced < 0 {
+ reduced = 0
+ }
+ return reduced
+ }
+ }
+ return dmg
+}
diff --git a/internal/game/tick.go b/internal/game/tick.go
index 8c40c96..4ab7e13 100644
--- a/internal/game/tick.go
+++ b/internal/game/tick.go
@@ -46,7 +46,19 @@ func (g *Game) RegenTick() {
p.RegenerateTick = 0
continue
}
- p.RegenerateTick--
+ regenSpeed := 1
+ if p.ActiveTechs != nil {
+ for id := range p.ActiveTechs {
+ def := GetTechDef(id)
+ if def != nil && def.Effects.HPRegenMulti > 0 {
+ multi := int(def.Effects.HPRegenMulti)
+ if multi > regenSpeed {
+ regenSpeed = multi
+ }
+ }
+ }
+ }
+ p.RegenerateTick -= regenSpeed
if p.RegenerateTick <= 0 {
p.HP++
if p.HP >= p.MaxHP() {
@@ -201,6 +213,69 @@ func (g *Game) legalMobExits(inst *world.MobInstance) []int {
return legal
}
+func (g *Game) TechTick() {
+ if g.Hub == nil {
+ return
+ }
+ for _, sess := range g.Hub.AllSessions() {
+ p, ok := sess.Player.(*player.Player)
+ if !ok || p == nil || len(p.ActiveTechs) == 0 {
+ continue
+ }
+
+ totalDrain := 0.0
+ hasPreserve := false
+ preserveReduction := 0.0
+
+ for id := range p.ActiveTechs {
+ def := GetTechDef(id)
+ if def != nil && def.Effects.PreserveDrain > 0 {
+ hasPreserve = true
+ preserveReduction = def.Effects.PreserveDrain
+ }
+ }
+
+ for id := range p.ActiveTechs {
+ def := GetTechDef(id)
+ if def == nil {
+ continue
+ }
+ if p.TechActivatedSinceTick[id] {
+ continue
+ }
+ drain := def.DrainRate
+ if hasPreserve && def.Effects.PreserveDrain == 0 {
+ drain *= (1.0 - preserveReduction)
+ }
+ totalDrain += drain
+ }
+
+ p.TechActivatedSinceTick = nil
+
+ equipTechBonus := g.totalTechBonus(p)
+ if equipTechBonus > 0 {
+ reduction := float64(equipTechBonus) * 0.01
+ if reduction > 0.5 {
+ reduction = 0.5
+ }
+ totalDrain *= (1.0 - reduction)
+ }
+
+ if totalDrain <= 0 {
+ continue
+ }
+
+ p.Battery -= totalDrain
+ if p.Battery <= 0 {
+ p.Battery = 0
+ p.DeactivateAllTechs()
+ sess.WriteLine(g.colorize(sess, "tech_depleted",
+ "\nYour battery is depleted! All tech has been disabled."))
+ g.writePrompt(sess)
+ }
+ }
+}
+
func (g *Game) ConsumeTick() {
if g.Hub == nil {
return