diff options
| author | historia <[not public]> | 2026-06-19 02:40:22 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-19 02:40:22 -0400 |
| commit | 2ae4a052fd1c15c3c6ba0f1dd324d896371aaffc (patch) | |
| tree | 3caef60f71a462b7e7ace668053e1f02fb61db15 /internal | |
| parent | 86aec0e14eb6fec0a39d742bab1761dad8d7012c (diff) | |
| download | thehouseoficarus-2ae4a052fd1c15c3c6ba0f1dd324d896371aaffc.tar.gz | |
feat: rough technology skill added
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/config/config.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_attack.go | 28 | ||||
| -rw-r--r-- | internal/game/cmd_score.go | 15 | ||||
| -rw-r--r-- | internal/game/cmd_tech.go | 196 | ||||
| -rw-r--r-- | internal/game/cmd_use.go | 4 | ||||
| -rw-r--r-- | internal/game/game.go | 6 | ||||
| -rw-r--r-- | internal/game/prompt.go | 2 | ||||
| -rw-r--r-- | internal/game/tech.go | 250 | ||||
| -rw-r--r-- | internal/game/tick.go | 77 | ||||
| -rw-r--r-- | internal/player/player.go | 55 |
10 files changed, 626 insertions, 9 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index af85c61..a9f0020 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -48,6 +48,8 @@ func DefaultColors() ColorsConfig { "credits_pickup": "220", "visual_tick": "33 dim", "visual_first_tick": "196 bold", + "battery": "45", + "tech_depleted": "196", } } 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 diff --git a/internal/player/player.go b/internal/player/player.go index 4b26a84..a970a00 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -1,7 +1,11 @@ package player -import "thehouseoficarus/internal/action" -import "thehouseoficarus/internal/object" +import ( + "sort" + + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/object" +) type SkillName string @@ -173,6 +177,10 @@ type Player struct { MoveDirection string `yaml:"-"` MoveTarget int `yaml:"-"` VisualTickCurrent int `yaml:"-"` + Battery float64 `yaml:"battery"` + ActiveTechs map[string]bool `yaml:"-"` + QuickTech string `yaml:"quick_tech,omitempty"` + TechActivatedSinceTick map[string]bool `yaml:"-"` } func (p *Player) ClearMoveState() { @@ -288,6 +296,7 @@ func New(name string) *Player { } p.Skills[Hitpoints] = XPForLevel(10) p.HP = p.MaxHP() + p.Battery = p.MaxBattery() return p } @@ -375,3 +384,45 @@ func (p *Player) RemoveItem(itemID string, qty int) bool { } return remaining <= 0 } + +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) { + 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 +} + +func (p *Player) DeactivateTech(techID string) { + if p.ActiveTechs != nil { + delete(p.ActiveTechs, techID) + } +} + +func (p *Player) DeactivateAllTechs() { + p.ActiveTechs = nil + p.TechActivatedSinceTick = nil +} + +func (p *Player) ActiveTechList() []string { + var result []string + for id := range p.ActiveTechs { + result = append(result, id) + } + sort.Strings(result) + return result +} |
