aboutsummaryrefslogtreecommitdiff
path: root/skill_plans/technology.md
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-19 18:33:43 -0400
committerhistoria <[not public]>2026-06-19 18:33:43 -0400
commit2bec24859af8f03c80a0a58e3240519942450167 (patch)
treedcb8baa5591fdeca51e179a63a08d46b014cb85c /skill_plans/technology.md
parent0ea4eec5dd2122c6704216971eb1249276297867 (diff)
downloadthehouseoficarus-2bec24859af8f03c80a0a58e3240519942450167.tar.gz
readme
Diffstat (limited to 'skill_plans/technology.md')
-rw-r--r--skill_plans/technology.md1907
1 files changed, 0 insertions, 1907 deletions
diff --git a/skill_plans/technology.md b/skill_plans/technology.md
deleted file mode 100644
index c51e5dc..0000000
--- a/skill_plans/technology.md
+++ /dev/null
@@ -1,1907 +0,0 @@
-# Technology Skill Implementation Plan
-
-## 1. Overview
-
-Technology is the Prayer equivalent in The House of Icarus.
-
-| OSRS Concept | THOI Equivalent |
-|---|---|
-| Prayer | Technology |
-| Prayer points | Battery |
-| Prayers | Techs |
-| Prayer bonus (equipment) | Technology bonus (equipment, `ItemStats.TechnologyBonus`) |
-| Altar | Charging Station / Power Conduit |
-| Prayer level | Technology level (already exists: `player.Technology`) |
-
-Battery is a `float64` that drains while techs are active. Max battery = Technology level. Battery is saved to character YAML. Active techs are NOT saved (deactivate on logout/death). `TechnologyBonus` on equipment (`ItemStats.TechnologyBonus` at `internal/object/item.go:72`) already exists but is currently unused.
-
----
-
-## 2. Player State Changes
-
-### File: `internal/player/player.go`
-
-Add these fields to the `Player` struct (after line 174, near `VisualTickCurrent`):
-
-```go
-Battery float64 `yaml:"battery"`
-ActiveTechs map[string]bool `yaml:"-"`
-QuickTech string `yaml:"quick_tech,omitempty"`
-TechActivatedAt map[string]int64 `yaml:"-"` // tick number when each tech was last activated
-```
-
-**Field details:**
-
-| Field | YAML | Description |
-|---|---|---|
-| `Battery` | `yaml:"battery"` | Current battery level. Saved to character YAML. Float64 for drain precision. |
-| `ActiveTechs` | `yaml:"-"` | Map of currently active tech IDs. NOT saved. Cleared on logout/death. |
-| `QuickTech` | `yaml:"quick_tech,omitempty"` | ID of the quick-toggle tech. Saved to character YAML. |
-| `TechActivatedAt` | `yaml:"-"` | Maps tech ID to the tick number it was last activated. Used for 1-tick flicking grace period. |
-
-### New methods on `*Player`:
-
-```go
-func (p *Player) MaxBattery() float64 {
- return float64(p.Level(Technology))
-}
-
-func (p *Player) HasActiveTech(techID string) bool {
- if p.ActiveTechs == nil {
- return false
- }
- return p.ActiveTechs[techID]
-}
-
-func (p *Player) ActivateTech(techID string, tickNum int64) {
- if p.ActiveTechs == nil {
- p.ActiveTechs = make(map[string]bool)
- }
- if p.TechActivatedAt == nil {
- p.TechActivatedAt = make(map[string]int64)
- }
- p.ActiveTechs[techID] = true
- p.TechActivatedAt[techID] = tickNum
-}
-
-func (p *Player) DeactivateTech(techID string) {
- if p.ActiveTechs != nil {
- delete(p.ActiveTechs, techID)
- }
-}
-
-func (p *Player) DeactivateAllTechs() {
- p.ActiveTechs = nil
- p.TechActivatedAt = nil
-}
-
-func (p *Player) ActiveTechList() []string {
- var result []string
- for id := range p.ActiveTechs {
- result = append(result, id)
- }
- sort.Strings(result)
- return result
-}
-```
-
-### Initialization in `New()`:
-
-Add to `New()` function at `internal/player/player.go:276`:
-
-```go
-// After p.HP = p.MaxHP() (line 289):
-p.Battery = p.MaxBattery()
-```
-
-New characters start with full battery.
-
-### Add tick counter to Game struct
-
-The `Game` struct at `internal/game/game.go:36` needs a tick counter for tracking activation times:
-
-```go
-type Game struct {
- // ... existing fields ...
- tickCount int64
-}
-```
-
-Increment `tickCount` at the start of each tick cycle. See Section 7 for where this happens.
-
----
-
-## 3. Tech Definitions
-
-### New file: `internal/game/tech.go`
-
-Hardcoded in Go (not YAML-driven). All tech definitions, mutual exclusivity, and helper functions live here.
-
-```go
-package game
-
-import "sort"
-
-type TechEffects struct {
- AttackPercent int
- StrengthPercent int
- DefensePercent int
- RangedPercent int
- SciencePercent int
- ProtectMelee bool
- ProtectRanged bool
- ProtectScience bool
- DamageReduction float64
- HPRegenMulti float64 // multiplier on HP regen rate (2.0 = 2x faster)
- PreserveDrain float64 // reduces other tech drain by this fraction (0.2 = 20% reduction)
- RetributionPct float64 // on death, deal this fraction of max HP as damage
-}
-
-type TechDef struct {
- ID string
- Name string
- Level int
- DrainRate float64
- Category string
- Group string // mutual exclusivity group
- Effects TechEffects
-}
-
-var AllTechs []TechDef
-
-var techByID map[string]*TechDef
-
-func init() {
- AllTechs = []TechDef{
- // ── Attack Techs ──
- {
- 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},
- },
-
- // ── Strength Techs ──
- {
- 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},
- },
-
- // ── Defense Techs ──
- {
- 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},
- },
-
- // ── Ranged Techs ──
- {
- 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},
- },
-
- // ── Science Techs ──
- {
- 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},
- },
-
- // ── Protection Techs ──
- {
- 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},
- },
-
- // ── Utility Techs ──
- {
- 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},
- },
-
- // ── Combo Techs (high level, multi-stat) ──
- {
- 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 TechsForLevel(level int) []*TechDef {
- var result []*TechDef
- for i := range AllTechs {
- if AllTechs[i].Level <= level {
- result = append(result, &AllTechs[i])
- }
- }
- return result
-}
-
-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
-}
-```
-
-**Total: 25 techs.**
-
----
-
-## 4. Full Tech List Reference
-
-| # | ID | Name | Level | Drain/tick | Category | Group | Effect |
-|---|---|---|---|---|---|---|---|
-| 1 | `clarity_1` | Clarity | 4 | 0.05 | attack | attack_tier | +5% Attack level |
-| 2 | `clarity_2` | Enhanced Clarity | 16 | 0.10 | attack | attack_tier | +10% Attack level |
-| 3 | `clarity_3` | Superior Clarity | 44 | 0.15 | attack | attack_tier | +15% Attack level |
-| 4 | `amplifier_1` | Power Amplifier | 7 | 0.05 | strength | strength_tier | +5% Strength level |
-| 5 | `amplifier_2` | Enhanced Amplifier | 23 | 0.10 | strength | strength_tier | +10% Strength level |
-| 6 | `amplifier_3` | Superior Amplifier | 49 | 0.15 | strength | strength_tier | +15% Strength level |
-| 7 | `shield_1` | Energy Shield | 10 | 0.05 | defense | defense_tier | +5% Defense level |
-| 8 | `shield_2` | Enhanced Shield | 28 | 0.10 | defense | defense_tier | +10% Defense level |
-| 9 | `shield_3` | Superior Shield | 52 | 0.15 | defense | defense_tier | +15% Defense level |
-| 10 | `targeting_1` | Targeting System | 8 | 0.05 | ranged | ranged_tier | +5% Ranged level |
-| 11 | `targeting_2` | Enhanced Targeting | 22 | 0.10 | ranged | ranged_tier | +10% Ranged level |
-| 12 | `targeting_3` | Superior Targeting | 46 | 0.15 | ranged | ranged_tier | +15% Ranged level |
-| 13 | `focus_1` | Neural Focus | 9 | 0.05 | science | science_tier | +5% Science level |
-| 14 | `focus_2` | Enhanced Focus | 27 | 0.10 | science | science_tier | +10% Science level |
-| 15 | `focus_3` | Superior Focus | 55 | 0.15 | science | science_tier | +15% Science level |
-| 16 | `protect_melee` | Kinetic Barrier | 37 | 0.20 | protection | protection | 40% melee damage reduction |
-| 17 | `protect_ranged` | Projectile Screen | 40 | 0.20 | protection | protection | 40% ranged damage reduction |
-| 18 | `protect_science` | Neural Firewall | 43 | 0.20 | protection | protection | 40% science damage reduction |
-| 19 | `regen` | Nano Repair | 22 | 0.10 | utility | regen_tier | 2x HP regen rate |
-| 20 | `rapid_heal` | Rapid Repair | 31 | 0.15 | utility | regen_tier | 4x HP regen rate |
-| 21 | `preserve` | Power Saver | 55 | 0.05 | utility | (none) | 20% drain reduction on other techs |
-| 22 | `retribution` | Dead Man's Switch | 46 | 0.10 | utility | (none) | 25% max HP damage to mob on death |
-| 23 | `overclock` | Overclock | 60 | 0.25 | combo | attack_tier | +15% Attack, +15% Strength |
-| 24 | `fortify` | Fortify | 65 | 0.25 | combo | defense_tier | +15% Defense, 2x HP regen |
-
----
-
-## 5. Mutual Exclusivity Rules
-
-Techs with the same non-empty `Group` value are mutually exclusive. Activating one deactivates all others in the same group.
-
-| Group | Techs | Rule |
-|---|---|---|
-| `attack_tier` | clarity_1, clarity_2, clarity_3, overclock | Only one at a time |
-| `strength_tier` | amplifier_1, amplifier_2, amplifier_3 | Only one at a time |
-| `defense_tier` | shield_1, shield_2, shield_3, fortify | Only one at a time |
-| `ranged_tier` | targeting_1, targeting_2, targeting_3 | Only one at a time |
-| `science_tier` | focus_1, focus_2, focus_3 | Only one at a time |
-| `protection` | protect_melee, protect_ranged, protect_science | Only one at a time |
-| `regen_tier` | regen, rapid_heal | Only one at a time |
-| (empty) | preserve, retribution | No exclusivity, can stack with anything |
-
-**Cross-category stacking IS allowed.** Example: `clarity_3 + amplifier_3 + protect_melee + preserve` is valid (4 active techs from 4 different groups).
-
-Implementation in `toggleTech()`:
-
-```go
-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
-}
-```
-
----
-
-## 6. Commands
-
-### New file: `internal/game/cmd_tech.go`
-
-```go
-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 == "" {
- // No args: toggle quick tech, or show list if no quick tech set
- 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
- }
-
- // Toggle a specific tech by name/id prefix match
- 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 already active, deactivate
- if p.HasActiveTech(techID) {
- p.DeactivateTech(techID)
- sess.WriteLine(fmt.Sprintf("\n%s deactivated.", def.Name))
- return
- }
-
- // Check level requirement
- if techLevel < def.Level {
- sess.WriteLine(fmt.Sprintf("\nYou need level %d Technology to use %s.", def.Level, def.Name))
- return
- }
-
- // Check battery
- if p.Battery <= 0 {
- sess.WriteLine("\nYour battery is depleted!")
- return
- }
-
- // Deactivate conflicting techs in same group
- deactivated := g.deactivateGroup(p, def.Group)
- for _, name := range deactivated {
- sess.WriteLine(fmt.Sprintf("\n%s deactivated.", name))
- }
-
- // Activate
- p.ActivateTech(techID, g.tickCount)
- 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{
- color.Render(mode, color.Parse("75"), "Tech"),
- color.Render(mode, color.Parse("245"), "Level"),
- color.Render(mode, color.Parse("222"), "Drain/tick"),
- color.Render(mode, color.Parse("179"), "Effect"),
- color.Render(mode, color.Parse("230"), "Status"),
- }}
-
- for _, tech := range AllTechs {
- status := ""
- nameColor := "245"
- if tech.Level <= techLevel {
- nameColor = "75"
- if p.HasActiveTech(tech.ID) {
- status = color.Render(mode, color.Parse("82"), "ON")
- } else {
- status = color.Render(mode, color.Parse("240"), "off")
- }
- } else {
- status = color.Render(mode, color.Parse("160"), "locked")
- }
-
- effect := techEffectString(tech)
-
- t.Rows = append(t.Rows, []string{
- color.Render(mode, color.Parse(nameColor), tech.Name),
- color.Render(mode, color.Parse("230"), fmt.Sprint(tech.Level)),
- color.Render(mode, color.Parse("222"), fmt.Sprintf("%.2f", tech.DrainRate)),
- color.Render(mode, color.Parse("179"), 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))
-}
-
-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, ", ")
-}
-```
-
-### Command Classification
-
-**File: `internal/game/game.go`**
-
-In `classifyCommand()` at line 136, add `"tech"` and `"t"` to the `ClassInstant` case:
-
-```go
-case "say", "score", "sc", "inventory", "i", "inv",
- "look", "l", "exits", "help",
- "map", "option", "options", "alias", "unalias",
- "description", "desc", "queued", "color", "colors",
- "colortable", "prompt", "style",
- "tech", "t": // <-- ADD THESE
- return ClassInstant
-```
-
-### Command Execution
-
-In `executeCommand()` at line 249, add a case for `"tech"` and `"t"`:
-
-```go
-case "tech", "t":
- g.doTech(sess, strings.Join(args, " "))
-```
-
-Insert after the `"style"` case (around line 294).
-
----
-
-## 7. Drain Mechanics - `TechTick()`
-
-### File: `internal/game/tick.go`
-
-Add `TechTick()` method. This runs once per game tick.
-
-```go
-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
- }
-
- // Calculate total drain
- totalDrain := 0.0
- hasPreserve := false
- preserveReduction := 0.0
-
- for id := range p.ActiveTechs {
- def := GetTechDef(id)
- if def == nil {
- continue
- }
-
- // 1-tick flicking: skip drain for techs activated THIS tick
- if p.TechActivatedAt != nil {
- if activatedAt, ok := p.TechActivatedAt[id]; ok && activatedAt == g.tickCount {
- continue
- }
- }
-
- if def.Effects.PreserveDrain > 0 {
- hasPreserve = true
- preserveReduction = def.Effects.PreserveDrain
- }
- }
-
- for id := range p.ActiveTechs {
- def := GetTechDef(id)
- if def == nil {
- continue
- }
-
- // 1-tick flicking: skip drain for techs activated THIS tick
- if p.TechActivatedAt != nil {
- if activatedAt, ok := p.TechActivatedAt[id]; ok && activatedAt == g.tickCount {
- continue
- }
- }
-
- drain := def.DrainRate
- if hasPreserve && def.Effects.PreserveDrain == 0 {
- drain *= (1.0 - preserveReduction)
- }
- totalDrain += drain
- }
-
- // Apply equipment TechnologyBonus reduction
- 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)
- }
- }
-}
-```
-
-### Equipment Technology Bonus Helper
-
-Add to `internal/game/tech.go`:
-
-```go
-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
-}
-```
-
-### Drain Formula
-
-```
-drainPerTick = sum of each active tech's DrainRate
- * (1.0 - preserveReduction) // if Power Saver is active, 0.8x on other techs
- * (1.0 - equipTechBonus * 0.01) // equipment reduction, capped at 50%
-
-Battery -= drainPerTick
-if Battery <= 0: deactivate all techs
-```
-
-**Example:** Player has `clarity_3` (0.15) + `amplifier_3` (0.15) + `preserve` (0.05) active. Equipment tech bonus = 10.
-
-```
-preserve drain = 0.05 (not reduced by itself)
-clarity_3 drain = 0.15 * 0.8 = 0.12
-amplifier_3 drain = 0.15 * 0.8 = 0.12
-raw total = 0.05 + 0.12 + 0.12 = 0.29
-after equipment: 0.29 * (1.0 - 0.10) = 0.261 per tick
-```
-
-At 600ms ticks: 100 ticks/minute. Battery 55 would last ~210 ticks = ~126 seconds = ~2 minutes.
-
-### Register TechTick in Main Tick Loop
-
-**File: `cmd/mud/main.go`**
-
-Add `g.TechTick()` to the tick subscriber, AFTER `g.AdvanceActions()` (combat damage has already been calculated by this point in the tick cycle, so protection was already applied):
-
-```go
-g.Ticks.Subscribe(1, func() bool {
- g.tickCount++ // <-- ADD: increment tick counter
- g.MoveTick()
- g.ProcessQueuedCommands()
- g.World.Tick()
- g.MobStore.Tick()
- g.RegenTick()
- g.DisconnectTick()
- g.WanderTick()
- g.SharedDepletionTick()
- g.FireTick()
- g.AdvanceActions()
- g.TechTick() // <-- ADD: drain battery after combat/actions
- g.ConsumeTick()
- g.BroadcastRespawns()
- g.VisualTick()
- return true
-})
-```
-
-The `g.tickCount++` MUST be the first line so that `TechActivatedAt` comparisons use the current tick number. `TechTick` runs AFTER `AdvanceActions` (which includes combat subscriptions) so protection has already been applied before drain is calculated.
-
----
-
-## 8. 1-Tick Flicking Mechanic
-
-### How It Works
-
-The flicking mechanic uses `TechActivatedAt` to grant a 1-tick grace period on activation:
-
-1. `ActivateTech(techID, tickNum)` records the current `tickCount` in `TechActivatedAt[techID]`
-2. `TechTick()` skips drain for any tech where `TechActivatedAt[techID] == g.tickCount`
-3. This means: a tech activated during the current tick is NOT drained on that tick. Drain starts the NEXT tick.
-
-### Flicking Sequence
-
-**Continuous 1-tick flicking (no drain):**
-
-```
-Tick N: TechTick runs. Tech is OFF. No drain.
- Player types "tech kinetic" → tech activates, TechActivatedAt = N+1 (next tick hasn't started)
-
-Wait... let me reconsider. The tick counter increments at the START of each tick cycle. So:
-
-Tick N starts: tickCount = N
- - ProcessQueuedCommands (no tech-related queue, tech is instant)
- - Combat: mob attacks, Kinetic Barrier is ON → damage reduced 40%
- - TechTick: check clarity. TechActivatedAt == N → skip drain! No drain.
-
-Between tick N and tick N+1:
- - Player types "tech kinetic" → deactivates tech
-
-Tick N+1 starts: tickCount = N+1
- - Combat: mob attacks, Kinetic Barrier is OFF → full damage
- - TechTick: no active techs, no drain.
-
-Between tick N+1 and tick N+2:
- - Player types "tech kinetic" → activates tech, TechActivatedAt = N+1
-
-Tick N+2 starts: tickCount = N+2
- - Combat: Kinetic Barrier is ON → damage reduced
- - TechTick: TechActivatedAt = N+1, tickCount = N+2 → N+1 != N+2 → DRAIN occurs
-```
-
-**Correction:** The above shows drain on tick N+2. For true 0-drain flicking, the player must:
-
-1. Have tech active at the start of tick N (activated between N-1 and N, so `TechActivatedAt = N-1`)
-2. Wait: TechTick on tick N would drain because `N-1 != N`. So the player needs to toggle OFF before tick N's TechTick, then back ON.
-
-**Revised flicking sequence for zero drain:**
-
-The key insight: `tech` is an **Instant** command. It executes immediately when typed, NOT during tick processing. Ticks run in a goroutine. The player's input is processed on the session goroutine. But since all game state is accessed from the tick goroutine via the hub, there's a concurrency concern.
-
-**Actually**, re-reading `HandleSession` at `game.go:92`: input handling happens synchronously when the session receives input. The tick loop is a separate goroutine. Both access `p.ActiveTechs`. For thread safety, we need to ensure that tech toggling is safe.
-
-**Simpler approach that guarantees flicking works:** Instead of relying on timing between ticks, use the activation-tick grace:
-
-```
-Between tick N-1 and tick N:
- Player types "t" → deactivate tech
- Player types "t" → reactivate tech, TechActivatedAt = N (tickCount hasn't incremented yet, still N-1... wait)
-```
-
-**The issue:** `tickCount` is only updated inside the tick goroutine. Between ticks, it's the OLD value. So `TechActivatedAt` would be set to the old tick count, and TechTick on the NEXT tick (count+1) would see a mismatch and drain.
-
-**Fix:** Compare `TechActivatedAt >= g.tickCount` instead of `==`. No, that breaks things differently.
-
-**Better fix:** Use a **"tech toggled off and back on this period" flag**. Here's the definitive approach:
-
-### Definitive 1-Tick Flicking Implementation
-
-Add to `Player`:
-
-```go
-TechFlickWindow map[string]bool `yaml:"-"` // techs that were deactivated and reactivated between ticks
-```
-
-When `DeactivateTech` is called, if the tech was active, mark it in a "recently deactivated" set. When `ActivateTech` is called, if the tech was recently deactivated, set `TechFlickWindow[id] = true`.
-
-At the start of each tick in `TechTick`, clear `TechFlickWindow` for techs that were NOT toggled this period.
-
-**Actually, let me simplify even further.** The cleanest approach matching OSRS:
-
-### Final Flicking Approach: Drain-Tick Snapshot
-
-At the **start** of `TechTick()`, snapshot which techs are active. Only drain techs in the snapshot. Then clear the snapshot.
-
-If a player deactivates and reactivates between ticks, the snapshot at the start of the NEXT tick will show the tech as active, but we add a check: if the tech was toggled (deactivated then reactivated) since the last snapshot, skip it.
-
-**Implementation:**
-
-Add to `Player`:
-
-```go
-TechToggleCount map[string]int `yaml:"-"` // incremented on each toggle (activate or deactivate)
-```
-
-In `TechTick`:
-
-```go
-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
-
- // First pass: check for preserve
- for id := range p.ActiveTechs {
- def := GetTechDef(id)
- if def == nil {
- continue
- }
- if def.Effects.PreserveDrain > 0 {
- hasPreserve = true
- preserveReduction = def.Effects.PreserveDrain
- }
- }
-
- // Second pass: calculate drain
- for id := range p.ActiveTechs {
- def := GetTechDef(id)
- if def == nil {
- continue
- }
-
- // 1-tick flicking: skip drain if tech was activated on the current tick
- if p.TechActivatedAt != nil {
- if activatedAt, ok := p.TechActivatedAt[id]; ok && activatedAt == g.tickCount {
- continue
- }
- }
-
- drain := def.DrainRate
- if hasPreserve && def.Effects.PreserveDrain == 0 {
- drain *= (1.0 - preserveReduction)
- }
- totalDrain += drain
- }
-
- // Equipment tech bonus reduction
- 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)
- }
- }
-}
-```
-
-### Flicking Works Because:
-
-1. `g.tickCount` increments at the very start of the tick cycle (first line of the tick subscriber)
-2. `tech` is Instant, so it executes on the session goroutine BETWEEN ticks
-3. When a player activates a tech between ticks, `ActivateTech(id, g.tickCount)` stores the CURRENT tick count (which hasn't been incremented yet for the next tick)
-4. When the next tick starts, `g.tickCount++` runs, making `g.tickCount = oldCount + 1`
-5. `TechTick` compares `TechActivatedAt[id] (== oldCount)` with `g.tickCount (== oldCount + 1)` → NOT equal → drain happens
-
-**For flicking to produce zero drain:**
-
-1. Tech is active (activated on tick N-1, so `TechActivatedAt = N-1`)
-2. Between tick N-1 and tick N, player types "t" (deactivate), then "t" (reactivate)
-3. Reactivation sets `TechActivatedAt = N-1` (tickCount hasn't changed yet)
-4. Tick N starts: `tickCount = N`. `TechActivatedAt = N-1`. `N-1 != N` → **DRAIN HAPPENS**
-
-This doesn't give zero-drain flicking. We need a different mechanism.
-
-### Correct Implementation: Grace Period from Re-Activation
-
-The correct approach: **compare with `g.tickCount - 1`** (activation happened this tick period = between the last tick and the current one).
-
-**Or better:** Set `TechActivatedAt` to `g.tickCount + 1` when activating between ticks, so it matches the NEXT tick count.
-
-**Or simplest:** Don't use tick counts at all. Use a boolean flag:
-
-```go
-TechActivatedSinceTick map[string]bool `yaml:"-"`
-```
-
-- When a tech is activated, set `TechActivatedSinceTick[id] = true`
-- At the START of `TechTick`, before calculating drain: for any tech in `TechActivatedSinceTick`, skip drain. Then CLEAR `TechActivatedSinceTick`.
-- This means: any tech activated between the previous `TechTick` and the current `TechTick` gets 1 free tick of no drain.
-
-**Flicking with this approach:**
-
-1. TechTick runs (tick N). Tech was active, drain applied. TechActivatedSinceTick is empty.
-2. Player types "t" → deactivates tech.
-3. Player types "t" → reactivates tech. `TechActivatedSinceTick[id] = true`.
-4. TechTick runs (tick N+1). Tech is active. `TechActivatedSinceTick[id]` is true → **skip drain**. Clear flag.
-5. Player types "t" → deactivates.
-6. Player types "t" → reactivates. Flag set again.
-7. TechTick runs (tick N+2). Flag is true → skip drain. Clear.
-8. ...repeat forever: **zero drain** as long as player toggles off+on between each tick.
-
-**This is the correct OSRS-like 1-tick flicking behavior.**
-
-### Final Player Fields for Flicking
-
-Replace the earlier `TechActivatedAt` with:
-
-```go
-TechActivatedSinceTick map[string]bool `yaml:"-"`
-```
-
-Update `ActivateTech`:
-
-```go
-func (p *Player) ActivateTech(techID string) {
- if p.ActiveTechs == nil {
- p.ActiveTechs = make(map[string]bool)
- }
- if p.TechActivatedSinceTick == nil {
- p.TechActivatedSinceTick = make(map[string]bool)
- }
- p.ActiveTechs[techID] = true
- p.TechActivatedSinceTick[techID] = true
-}
-```
-
-Remove the `tickNum` parameter from `ActivateTech`. Remove `TechActivatedAt` from the plan entirely. Remove `tickCount` from `Game` struct.
-
-Update `TechTick`:
-
-```go
-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
- }
-
- // 1-tick flicking grace: skip drain for freshly activated techs
- if p.TechActivatedSinceTick[id] {
- continue
- }
-
- drain := def.DrainRate
- if hasPreserve && def.Effects.PreserveDrain == 0 {
- drain *= (1.0 - preserveReduction)
- }
- totalDrain += drain
- }
-
- // Clear the activation flags AFTER drain calculation
- 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)
- }
- }
-}
-```
-
-### Protection with Flicking
-
-Protection techs work independently of drain. `mobAttack()` checks `p.HasActiveTech("protect_melee")` at the moment damage is calculated. If the tech is currently active (toggled on between ticks, even if it will be toggled off before next TechTick), protection applies.
-
-**1-tick protection sequence:**
-1. Player types "tech kinetic" → Kinetic Barrier activates, `TechActivatedSinceTick["protect_melee"] = true`
-2. Tick fires: mob attacks → `p.HasActiveTech("protect_melee")` is true → 40% damage reduction
-3. TechTick: `TechActivatedSinceTick` has protect_melee → skip drain. Clear flags. **Zero drain, full protection for this tick.**
-4. Player types "tech kinetic" → deactivates. Next tick: no protection, no drain.
-
-**For sustained protection with minimal drain (1 tick of drain per 2 ticks):**
-1. Tech activated. TechActivatedSinceTick set.
-2. Tick: protection ON, drain skipped (grace).
-3. Player does NOT toggle. Tech stays active.
-4. Next tick: protection ON, drain APPLIED (grace expired). TechActivatedSinceTick cleared already.
-5. Player toggles off, then on. Grace set again.
-6. Next tick: protection ON, drain skipped.
-7. ...alternating pattern.
-
----
-
-## 9. Protection in Combat
-
-### File: `internal/game/cmd_attack.go`
-
-Modify `mobAttack()` (line 238). After damage is calculated (line 254: `dmg := combat.RollDamage(maxHit)`), add protection check:
-
-```go
-func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) {
- _, _, defBonus := combat.AttackStyleBonus(string(p.AttackStyle))
-
- equipDef := 0
- for _, itemID := range p.Equipment {
- def, err := g.ItemStore.Load(itemID)
- if err == nil {
- equipDef += def.Stats.DefenseBonus
- }
- }
-
- attRoll := combat.AttackRoll(mob.Attack, 0, 0)
- defRoll := combat.DefenseRoll(p.Level(player.Defense), defBonus, equipDef)
-
- if combat.HitCheck(attRoll, defRoll) {
- maxHit := combat.MaxHit(mob.Strength, 0, 0)
- dmg := combat.RollDamage(maxHit)
-
- // ── TECH PROTECTION ──
- dmg = g.applyTechProtection(p, mob, dmg)
-
- p.HP -= dmg
- // ... rest of existing code unchanged ...
-```
-
-### New helper function in `internal/game/tech.go`:
-
-```go
-func (g *Game) applyTechProtection(p *player.Player, mob *world.MobInstance, dmg int) int {
- if len(p.ActiveTechs) == 0 {
- return dmg
- }
-
- weaponType := mob.WeaponType // need to add WeaponType to MobInstance or MobDef
- // Default: melee if not specified
- if weaponType == "" {
- weaponType = "melee"
- }
-
- var protectTechID string
- switch weaponType {
- case "melee":
- 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
-}
-```
-
-### Mob WeaponType
-
-**File: `internal/world/mobs.go`** (or wherever `MobDef` is defined)
-
-Add `WeaponType` field to `MobDef`:
-
-```go
-type MobDef struct {
- // ... existing fields ...
- WeaponType string `yaml:"weapon_type"` // "melee" (default), "ranged", "science"
-}
-```
-
-Propagate to `MobInstance`:
-
-```go
-type MobInstance struct {
- // ... existing fields ...
- WeaponType string
-}
-```
-
-When spawning mob instances, copy `WeaponType` from def. Default to `"melee"` if empty.
-
----
-
-## 10. Tech Bonuses in Combat
-
-### File: `internal/game/cmd_attack.go`
-
-Modify `playerAttack()` to add tech-boosted levels.
-
-Current code (line 192):
-```go
-attRoll := combat.AttackRoll(p.Level(player.Attack), attBonus, equipAtt)
-```
-
-Change to:
-```go
-effectiveAttack := p.Level(player.Attack) + g.techLevelBonus(p, "attack")
-effectiveStrength := p.Level(player.Strength) + g.techLevelBonus(p, "strength")
-
-attRoll := combat.AttackRoll(effectiveAttack, attBonus, equipAtt)
-```
-
-And for the maxHit calculation (line 196):
-```go
-maxHit := combat.MaxHit(effectiveStrength, strBonus, equipStr)
-```
-
-For defense in `mobAttack()` (line 250):
-```go
-effectiveDefense := p.Level(player.Defense) + g.techLevelBonus(p, "defense")
-defRoll := combat.DefenseRoll(effectiveDefense, defBonus, equipDef)
-```
-
-### Tech Level Bonus Helper
-
-Add to `internal/game/tech.go`:
-
-```go
-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
-}
-```
-
-**Example:** Player has Attack level 80 and `clarity_3` active (+15%):
-```
-techLevelBonus = 80 * 15 / 100 = 12
-effectiveAttack = 80 + 12 = 92
-```
-
-This matches OSRS where prayers provide a percentage boost to the effective level.
-
----
-
-## 11. HP Regen with Tech
-
-### File: `internal/game/tick.go`
-
-Modify `RegenTick()` to check for regen techs. Currently (line 49-50):
-
-```go
-p.RegenerateTick--
-if p.RegenerateTick <= 0 {
- p.HP++
-```
-
-Change the decrement to account for regen multiplier:
-
-```go
-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++
-```
-
-With `Nano Repair` (2x), regen tick decrements by 2 instead of 1, halving the time to heal. With `Rapid Repair` (4x), decrements by 4.
-
----
-
-## 12. Retribution (Dead Man's Switch)
-
-### File: `internal/game/cmd_attack.go`
-
-In `endCombat()`, when the player dies (line 296-308), before dropping items and teleporting, check for retribution:
-
-```go
-if p.HP <= 0 {
- // ── RETRIBUTION CHECK ──
- 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))
- }
- }
- }
-
- // Deactivate all techs on death
- p.DeactivateAllTechs()
-
- sess.WriteLine(g.colorize(sess, "death", "\nOh dear, you are dead!"))
- // ... rest of existing death code ...
-```
-
----
-
-## 13. Battery Recharge
-
-### Charging Station Object
-
-**File: `data/objects/charging_station.yaml`**
-
-```yaml
-id: charging_station
-name: Charging Station
-description: "A humming terminal with exposed capacitor banks. Its indicator light pulses with stored energy. You could recharge your battery here."
-inroom_description: "A charging station hums quietly against the wall."
-behavior: recharge
-```
-
-**File: `data/behaviors/recharge.yaml`**
-
-```yaml
-id: recharge
-type: use
-actions:
- - message: "You connect to the charging station. Energy flows into your systems..."
- set_player_flags: {}
-```
-
-**Actually**, recharging is simpler as a `use_interaction` on the object, or even simpler as a hardcoded handler. Since the recharge is instant (like an OSRS altar), the cleanest approach is a **use_interaction** on the object definition.
-
-### Alternative: Hardcoded `use` handler for charging stations
-
-Add to the object's `use_interactions` in `data/objects/charging_station.yaml`:
-
-```yaml
-id: charging_station
-name: Charging Station
-description: "A humming terminal with exposed capacitor banks. You could recharge your battery here."
-inroom_description: "A charging station hums quietly against the wall."
-use_interactions:
- - verb: "use"
- message: "You connect to the charging station. Your battery is fully recharged."
- action: "recharge_battery"
-```
-
-### Handling in code
-
-**File: `internal/game/action_use.go`** (or wherever `use_interactions` are processed)
-
-In the handler for `use_interactions`, add a check for the `recharge_battery` action:
-
-```go
-if interaction.Action == "recharge_battery" {
- p.Battery = p.MaxBattery()
- g.AccountStore.SaveCharacter(p)
-}
-```
-
-**However**, looking at the existing `use_interactions` system, it might use a different mechanism. If `use_interactions` don't support custom action strings, implement recharging as a simple check in the object's interaction handler.
-
-### Simplest approach: Check object ID directly
-
-In whatever function handles `use <target>` on objects, add a special case:
-
-```go
-if obj.ID == "charging_station" || obj.ID == "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_recharge",
- "\nYou connect to the charging station. Your battery is fully recharged."))
- }
- return
-}
-```
-
-Add this check in `doUse()` in `internal/game/cmd_use.go` (or wherever `use` commands targeting objects are routed), BEFORE the normal behavior lookup.
-
-### Charging Station Room Placement
-
-Add to a starter room, e.g., `data/rooms/1.yaml`:
-
-```yaml
-objects:
- - id: charging_station
-```
-
-Or create a dedicated room. Place in any room where the player should be able to recharge.
-
----
-
-## 14. Score Page Changes
-
-### File: `internal/game/cmd_score.go`
-
-Modify `doScore()` to show battery and active techs.
-
-After the HP line (line 19), add battery:
-
-```go
-sess.WriteLines(
- "",
- 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))),
-)
-```
-
-After the skills table, add active techs section:
-
-```go
-// After the skills table render loop
-
-if len(p.ActiveTechs) > 0 {
- sess.WriteLine("")
- sess.WriteLine(g.colorize(sess, "tech_header", "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),
- ))
- }
- }
-}
-```
-
----
-
-## 15. Prompt Variables
-
-### File: `internal/game/prompt.go`
-
-Add battery prompt variables in `expandPromptVars()` after the HP variables (around line 38):
-
-```go
-text = strings.ReplaceAll(text, "%b", fmt.Sprintf("%.1f", p.Battery))
-text = strings.ReplaceAll(text, "%B", fmt.Sprintf("%.0f", p.MaxBattery()))
-```
-
-This lets players set prompts like: `prompt %h/%Hhp %b/%Bbat > `
-
----
-
-## 16. Logout & Death Tech Deactivation
-
-### Logout
-
-**File: `internal/game/game.go`**
-
-In `SetHub()` (line 81), the `OnRemove` callback handles session removal. Add tech deactivation:
-
-```go
-hub.OnRemove(func(sess *net.Session) {
- if p, ok := sess.Player.(*player.Player); ok {
- p.DeactivateAllTechs() // <-- ADD
- g.charsMu.Lock()
- delete(g.loggedInChars, p.Name)
- g.charsMu.Unlock()
- }
-})
-```
-
-### Death
-
-Already covered in Section 12 (`endCombat` death handler). Add `p.DeactivateAllTechs()` before the death message.
-
-### Quit
-
-**File: `internal/game/cmd_quit.go`** (or wherever `doQuit` is)
-
-When the player quits (rest timer expires, session closes), techs are already deactivated by `OnRemove`. No additional code needed.
-
----
-
-## 17. New Items with Technology Bonus
-
-### Equipment YAML files
-
-The `ItemStats.TechnologyBonus` field already exists at `internal/object/item.go:72`. Just create items that use it.
-
-**File: `data/items/neural_headband.yaml`**
-
-```yaml
-id: neural_headband
-name: Neural Headband
-description: "A thin band of circuitry that amplifies neural signals, reducing battery drain."
-color: "39"
-value: 500
-equip_slot: head
-stats:
- defense_bonus: 1
- technology_bonus: 4
-```
-
-**File: `data/items/power_amulet.yaml`**
-
-```yaml
-id: power_amulet
-name: Power Amulet
-description: "A pendant containing a miniature power cell that augments tech efficiency."
-color: "220"
-value: 800
-equip_slot: neck
-stats:
- technology_bonus: 3
-```
-
-**File: `data/items/capacitor_gloves.yaml`**
-
-```yaml
-id: capacitor_gloves
-name: Capacitor Gloves
-description: "Gloves woven with superconducting filaments."
-color: "250"
-value: 300
-equip_slot: hands
-stats:
- defense_bonus: 2
- technology_bonus: 2
-```
-
-**File: `data/items/holy_wrench_equiv.yaml`** (equivalent to OSRS holy wrench)
-
-```yaml
-id: power_regulator
-name: Power Regulator
-description: "A small device that optimizes battery recharge efficiency. Keep it in your inventory when recharging."
-color: "45"
-value: 1000
-```
-
-Optional: If the player has a `power_regulator` in inventory when recharging at a station, they get +2 bonus battery above max. (Stretch goal, not required for initial implementation.)
-
----
-
-## 18. Color Targets
-
-### File: `internal/config/colors.go` (or wherever `DefaultColors()` is defined)
-
-Add new color targets:
-
-```go
-"battery": "45", // cyan for battery display
-"battery_recharge": "82", // green for recharge message
-"tech_header": "75", // blue for "Active Tech:" header
-"tech_depleted": "196", // red for battery depleted warning
-"tech_activated": "82", // green for activation messages
-"tech_deactivated": "240", // gray for deactivation messages
-```
-
----
-
-## 19. Help Files
-
-### File: `data/help/tech.yaml`
-
-```yaml
-id: tech
-title: Technology & Tech
-aliases:
- - technology
- - battery
- - techs
-body: |
- Technology is a skill that allows you to activate powerful tech modules
- that enhance your combat abilities. Each tech drains battery while active.
-
- Battery is your tech power supply. Max battery equals your Technology level.
- Battery does not regenerate naturally -- recharge at a Charging Station.
-
- Commands:
- tech Toggle your quick tech (or show list if none set)
- tech <name> Toggle a specific tech on/off
- tech list Show all available techs
- tech quick <name> Set your quick tech
- t Shortcut for 'tech' (toggle quick tech)
-
- Tips:
- - Multiple techs can be active simultaneously (from different groups)
- - Equipment with Technology bonus reduces drain rate
- - Toggling tech off and back on between ticks avoids drain ("flicking")
- - Protection techs reduce incoming damage by 40%
-```
-
----
-
-## 20. Concurrency Safety
-
-Tech toggling happens on the session goroutine (instant command). Tech drain happens on the tick goroutine. Both access `p.ActiveTechs`.
-
-**Options:**
-
-1. **Accept the race.** In OSRS, prayer toggling and drain are both on the game thread. In this MUD, instant commands execute on the session goroutine while ticks run on the engine goroutine. However, looking at the existing codebase, ALL commands (including instant ones like `style`, `say`, `score`) access player state without locks. The same race already exists for `AttackStyle`, `HP`, `Skills`, etc. So this is an accepted pattern in the codebase.
-
-2. **If races must be fixed:** Add a `sync.Mutex` to `Player` for tech state. But this would be inconsistent with the rest of the codebase. Only do this if the existing code uses mutexes for player state (it doesn't).
-
-**Recommendation:** Follow the existing pattern. Don't add mutexes. The worst case is a tech being drained for one extra tick or one fewer tick due to a race, which is acceptable for a MUD.
-
----
-
-## 21. Edge Cases
-
-| Edge Case | Behavior |
-|---|---|
-| Battery is 0, player tries to activate tech | "Your battery is depleted!" — blocked |
-| Battery drains to 0 mid-tick | All techs deactivated, message sent |
-| Player logs out | All techs deactivated (not saved) |
-| Player dies | All techs deactivated, battery stays at current value |
-| Player levels Technology | Max battery increases, current battery unchanged (no auto-refill) |
-| Player levels Technology from 0 to 1 | Can now use techs. Battery was initialized to 0 from `New()`. Need to handle: set `Battery = MaxBattery()` when Technology XP is first gained, OR let player recharge at station. **Recommendation:** Battery starts at MaxBattery() for new characters (already handled in `New()`). For existing characters loaded from YAML with `Battery: 0`, they just need to visit a charging station. |
-| Quick tech is set to a tech the player can't use (level too low) | "You need level X Technology to use Y." — same as manual toggle |
-| Quick tech ID is invalid (tech was removed from code) | "Unknown tech." — handle gracefully |
-| All techs deactivated by battery depletion during combat | Combat continues normally, just no tech bonuses/protection |
-| Player with 0 Technology level types "tech" | "You need at least level 1 Technology to use tech." |
-| Multiple protection techs (mutual exclusivity) | Only one protection tech at a time (group: "protection"). Activating one deactivates the other. |
-| `tech list` with no Technology levels unlocked | Shows all techs with "locked" status |
-
----
-
-## 22. Complete File Change List
-
-| File | Change Type | Description |
-|---|---|---|
-| `internal/player/player.go` | MODIFY | Add `Battery`, `ActiveTechs`, `QuickTech`, `TechActivatedSinceTick` fields to `Player`. Add `MaxBattery()`, `HasActiveTech()`, `ActivateTech()`, `DeactivateTech()`, `DeactivateAllTechs()`, `ActiveTechList()` methods. Set `Battery` in `New()`. |
-| `internal/game/tech.go` | NEW | `TechDef`, `TechEffects` structs. All 25 tech definitions. `GetTechDef()`, `TechsForLevel()`, `TechByPrefixMatch()`, `techEffectString()`, `totalTechBonus()`, `techLevelBonus()`, `applyTechProtection()`, `deactivateGroup()` functions. |
-| `internal/game/cmd_tech.go` | NEW | `doTech()`, `toggleTech()`, `doTechList()`, `doTechQuick()` handlers. |
-| `internal/game/game.go` | MODIFY | Add `"tech"`, `"t"` to `classifyCommand()` ClassInstant. Add `case "tech", "t"` to `executeCommand()`. |
-| `internal/game/tick.go` | MODIFY | Add `TechTick()` function. Modify `RegenTick()` for tech regen multiplier. |
-| `cmd/mud/main.go` | MODIFY | Add `g.TechTick()` call to tick subscriber (after `g.AdvanceActions()`). |
-| `internal/game/cmd_attack.go` | MODIFY | Add `applyTechProtection()` call in `mobAttack()`. Add `techLevelBonus()` calls in `playerAttack()` and `mobAttack()` defense. Add retribution check in `endCombat()` death handler. Add `p.DeactivateAllTechs()` on death. |
-| `internal/game/cmd_score.go` | MODIFY | Add battery line after HP. Add active techs section after skills table. |
-| `internal/game/prompt.go` | MODIFY | Add `%b` (battery) and `%B` (max battery) prompt variables. |
-| `internal/game/game.go` (SetHub) | MODIFY | Add `p.DeactivateAllTechs()` in `OnRemove` callback. |
-| `internal/game/color.go` | NO CHANGE | Color helpers already support arbitrary targets via `resolveColor`. |
-| `internal/config/colors.go` | MODIFY | Add default color entries for `battery`, `tech_depleted`, etc. |
-| `internal/object/item.go` | NO CHANGE | `TechnologyBonus` field already exists. |
-| `internal/world/mobs.go` | MODIFY | Add `WeaponType` field to `MobDef` and `MobInstance`. |
-| `data/objects/charging_station.yaml` | NEW | Charging station object definition. |
-| `data/items/neural_headband.yaml` | NEW | Tech-bonus headband item. |
-| `data/items/power_amulet.yaml` | NEW | Tech-bonus amulet item. |
-| `data/items/capacitor_gloves.yaml` | NEW | Tech-bonus gloves item. |
-| `data/help/tech.yaml` | NEW | Help file for technology/tech commands. |
-
----
-
-## 23. Implementation Order
-
-### Phase 1: Core Infrastructure
-- [ ] 1. Add `Battery`, `ActiveTechs`, `QuickTech`, `TechActivatedSinceTick` fields to `Player` struct in `internal/player/player.go`
-- [ ] 2. Add `MaxBattery()`, `HasActiveTech()`, `ActivateTech()`, `DeactivateTech()`, `DeactivateAllTechs()`, `ActiveTechList()` methods to `Player`
-- [ ] 3. Set `p.Battery = p.MaxBattery()` in `New()`
-- [ ] 4. Create `internal/game/tech.go` with all `TechDef` definitions, `GetTechDef()`, `TechsForLevel()`, `TechByPrefixMatch()`, `techEffectString()`
-
-### Phase 2: Commands
-- [ ] 5. Create `internal/game/cmd_tech.go` with `doTech()`, `toggleTech()`, `doTechList()`, `doTechQuick()`
-- [ ] 6. Add `"tech"`, `"t"` to `classifyCommand()` as `ClassInstant` in `internal/game/game.go`
-- [ ] 7. Add `case "tech", "t"` to `executeCommand()` in `internal/game/game.go`
-- [ ] 8. **Test:** Verify `tech list`, `tech <name>`, `tech quick <name>`, `t` commands work. Verify mutual exclusivity. Verify level checks.
-
-### Phase 3: Battery Drain
-- [ ] 9. Add `totalTechBonus()` helper to `internal/game/tech.go`
-- [ ] 10. Add `TechTick()` to `internal/game/tick.go`
-- [ ] 11. Add `g.TechTick()` to tick subscriber in `cmd/mud/main.go` (after `g.AdvanceActions()`)
-- [ ] 12. **Test:** Verify drain reduces battery. Verify battery depletion deactivates all techs. Verify equipment tech bonus reduces drain. Verify Power Saver reduces drain on other techs.
-
-### Phase 4: 1-Tick Flicking
-- [ ] 13. Verify `TechActivatedSinceTick` flag is set on `ActivateTech()` and checked in `TechTick()`
-- [ ] 14. **Test:** Activate tech, verify drain on next tick. Deactivate and reactivate between ticks, verify NO drain on next tick. Repeat to confirm sustained zero-drain flicking.
-
-### Phase 5: Combat Integration
-- [ ] 15. Add `techLevelBonus()` to `internal/game/tech.go`
-- [ ] 16. Modify `playerAttack()` in `cmd_attack.go` to use `techLevelBonus` for attack and strength
-- [ ] 17. Modify `mobAttack()` in `cmd_attack.go` to use `techLevelBonus` for defense
-- [ ] 18. Add `WeaponType` to `MobDef`/`MobInstance` in `internal/world/`
-- [ ] 19. Add `applyTechProtection()` to `internal/game/tech.go`
-- [ ] 20. Call `applyTechProtection()` in `mobAttack()` after damage calculation
-- [ ] 21. **Test:** Verify attack/strength/defense bonuses affect combat rolls. Verify protection reduces damage by 40%.
-
-### Phase 6: Retribution & Regen
-- [ ] 22. Add retribution check in `endCombat()` death handler
-- [ ] 23. Add `p.DeactivateAllTechs()` on death in `endCombat()`
-- [ ] 24. Modify `RegenTick()` to check regen tech multiplier
-- [ ] 25. **Test:** Verify retribution deals damage on death. Verify Nano Repair doubles regen speed.
-
-### Phase 7: Recharge & UI
-- [ ] 26. Create `data/objects/charging_station.yaml`
-- [ ] 27. Add recharge handler (in `doUse` or `use_interactions` handler)
-- [ ] 28. Place charging station in at least one room
-- [ ] 29. Modify `doScore()` in `cmd_score.go` to show battery and active techs
-- [ ] 30. Add `%b` and `%B` prompt variables in `prompt.go`
-- [ ] 31. Add `p.DeactivateAllTechs()` in `OnRemove` callback in `game.go`
-
-### Phase 8: Polish
-- [ ] 32. Add default color targets for `battery`, `tech_depleted`, etc.
-- [ ] 33. Create tech-bonus equipment items (neural_headband, power_amulet, capacitor_gloves)
-- [ ] 34. Create `data/help/tech.yaml`
-- [ ] 35. Run `make vet` and `make test`
-- [ ] 36. Manual testing: full tech lifecycle (activate, drain, flick, protect, recharge, death, logout)
-
----
-
-## 24. Test Cases
-
-### Unit tests to add (in `internal/game/` or `internal/player/`):
-
-```go
-func TestMaxBattery(t *testing.T) {
- p := player.New("test")
- p.Skills[player.Technology] = player.XPForLevel(43)
- if p.MaxBattery() != 43.0 {
- t.Errorf("expected MaxBattery 43, got %f", p.MaxBattery())
- }
-}
-
-func TestTechMutualExclusivity(t *testing.T) {
- p := player.New("test")
- p.ActivateTech("clarity_1")
- p.ActivateTech("clarity_2") // should NOT auto-deactivate clarity_1 (that's Game's job)
- // The Game.toggleTech function handles deactivation via deactivateGroup
-}
-
-func TestTechLevelBonus(t *testing.T) {
- // Player with Attack 80, clarity_3 active (+15%)
- // Expected bonus: 80 * 15 / 100 = 12
- p := player.New("test")
- p.Skills[player.Attack] = player.XPForLevel(80)
- p.ActivateTech("clarity_3")
- // g.techLevelBonus(p, "attack") should return 12
-}
-
-func TestDrainWithPreserve(t *testing.T) {
- // clarity_3 (0.15) + preserve (0.05)
- // clarity drain = 0.15 * 0.8 = 0.12
- // preserve drain = 0.05
- // total = 0.17
-}
-
-func TestFlickingNoDrain(t *testing.T) {
- // Activate tech, set TechActivatedSinceTick
- // TechTick should skip drain for that tech
- // Clear flag
- // Next TechTick should drain
-}
-```
-
----
-
-## 25. Summary of All New Functions
-
-| Function | File | Purpose |
-|---|---|---|
-| `Player.MaxBattery()` | `player.go` | Returns Technology level as float64 |
-| `Player.HasActiveTech(id)` | `player.go` | Checks if tech is currently active |
-| `Player.ActivateTech(id)` | `player.go` | Activates tech, sets flick flag |
-| `Player.DeactivateTech(id)` | `player.go` | Deactivates single tech |
-| `Player.DeactivateAllTechs()` | `player.go` | Clears all active techs |
-| `Player.ActiveTechList()` | `player.go` | Returns sorted list of active tech IDs |
-| `GetTechDef(id)` | `tech.go` | Looks up tech by ID |
-| `TechsForLevel(level)` | `tech.go` | Returns techs available at given level |
-| `TechByPrefixMatch(input)` | `tech.go` | Prefix-matches tech name or ID |
-| `techEffectString(tech)` | `tech.go` | Human-readable effect description |
-| `Game.totalTechBonus(p)` | `tech.go` | Sums equipment TechnologyBonus |
-| `Game.techLevelBonus(p, stat)` | `tech.go` | Calculates percentage level bonus from active techs |
-| `Game.applyTechProtection(p, mob, dmg)` | `tech.go` | Applies protection damage reduction |
-| `Game.deactivateGroup(p, group)` | `tech.go` | Deactivates all techs in a mutual-exclusivity group |
-| `Game.doTech(sess, input)` | `cmd_tech.go` | Main tech command handler |
-| `Game.toggleTech(sess, p, techID)` | `cmd_tech.go` | Toggle specific tech on/off |
-| `Game.doTechList(sess)` | `cmd_tech.go` | Display tech list table |
-| `Game.doTechQuick(sess, p, input)` | `cmd_tech.go` | Set quick tech |
-| `Game.TechTick()` | `tick.go` | Per-tick battery drain |