Skip to content

Commit a79e2cf

Browse files
committed
PW changes - attacker charges always spent, separate passive recharge maximum + owner faction in API. fix #3043
1 parent 34c42db commit a79e2cf

8 files changed

Lines changed: 229 additions & 18 deletions

File tree

Shared/LobbyClient/Protocol/Messages.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,9 @@ public class VoteOption
715715
/// key that identifies this attack slot. Clients must echo it back in <see cref="PwJoinPlanet"/>.
716716
/// </summary>
717717
public string AttackerFaction { get; set; }
718+
/// <summary>Faction shortcut of the planet's current owner, or null for neutral planets. Lets the
719+
/// lobby surface "you own this", "ally owns this", "neutral" hints during attack/defense pick.</summary>
720+
public string OwnerFaction { get; set; }
718721
/// <summary>Average PW-WHR of the projected attacker squad (top-TeamSize volunteers). 0 when none.</summary>
719722
public int AttackerAvgWhr { get; set; }
720723
/// <summary>Average PW-WHR of the projected defender squad. Null in AttackCollect phase or when no volunteers.</summary>
@@ -782,6 +785,9 @@ public class PwStatus
782785
public int MinLevel { get; set; }
783786
public int AttackerPhaseMinutes { get; set; }
784787
public int DefenderPhaseMinutes { get; set; }
788+
/// <summary>Server's configured max attack charges. Lobby renders X/Max in the charges UI;
789+
/// before this field existed Chobby hardcoded 2 as the fallback default.</summary>
790+
public int MaxAttackCharges { get; set; }
785791
}
786792

787793
[Message(Origin.Server)]

Zero-K.info/Controllers/PlanetwarsAdminController.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,10 +366,12 @@ public ActionResult StartGalaxy(int galaxyID)
366366
gal.EndMessage = null;
367367
db.SaveChanges();
368368

369-
var maxCharges = DynamicConfig.Instance.PwAttackChargesMax;
369+
// Seed everyone at the passive recharge limit, not the absolute max — defending as the very
370+
// first PW action of the new galaxy then has a tangible reward (stockpiling above passive cap).
371+
var initialCharges = Math.Min(DynamicConfig.Instance.PwAttackChargesPassiveLimit, DynamicConfig.Instance.PwAttackChargesMax);
370372
db.Accounts.Where(x => x.FactionID != null).Update(x => new Account()
371373
{
372-
PwAttackCharges = maxCharges,
374+
PwAttackCharges = initialCharges,
373375
PwLastChargeChange = null,
374376
});
375377
}

ZkData/Ef/DynamicConfig.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public class DynamicConfig
4949
[Description("PlanetWars: minutes a player must be idle (no spend/gain) before their next +1 passive charge, up to max. Every charge gain or loss resets this clock, so the average regen rate is at most 1 charge per this many minutes.")]
5050
public int PwAttackChargesCooldownMinutes { get; set; } = 60;
5151

52+
[Description("PlanetWars: cap on charges obtainable through passive idle recharge. Players may exceed this via active grants (e.g. defense rewards) and keep what they have, but the recharge tick will not push them above this value. Should be <= PwAttackChargesMax.")]
53+
public int PwAttackChargesPassiveLimit { get; set; } = 1;
54+
5255
[Description("PlanetWars: fraction of enemy bomber IP damage also applied to the bomber's own faction. 0 disables self-damage.")]
5356
public double PwBomberSelfIpRate { get; set; } = 0.5;
5457

ZkData/Migrations/202604271235020_AddPwAttackChargesPassiveLimit.Designer.cs

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace ZkData.Migrations
2+
{
3+
using System;
4+
using System.Data.Entity.Migrations;
5+
6+
public partial class AddPwAttackChargesPassiveLimit : DbMigration
7+
{
8+
public override void Up()
9+
{
10+
AddColumn("dbo.DynamicConfigs", "PwAttackChargesPassiveLimit", c => c.Int(nullable: false, defaultValue: 1));
11+
}
12+
13+
public override void Down()
14+
{
15+
DropColumn("dbo.DynamicConfigs", "PwAttackChargesPassiveLimit");
16+
}
17+
}
18+
}

ZkData/Migrations/202604271235020_AddPwAttackChargesPassiveLimit.resx

Lines changed: 126 additions & 0 deletions
Large diffs are not rendered by default.

ZkData/ZkData.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,10 @@
515515
<Compile Include="Migrations\202604251056090_AddPwChargesWallclock.Designer.cs">
516516
<DependentUpon>202604251056090_AddPwChargesWallclock.cs</DependentUpon>
517517
</Compile>
518+
<Compile Include="Migrations\202604271235020_AddPwAttackChargesPassiveLimit.cs" />
519+
<Compile Include="Migrations\202604271235020_AddPwAttackChargesPassiveLimit.Designer.cs">
520+
<DependentUpon>202604271235020_AddPwAttackChargesPassiveLimit.cs</DependentUpon>
521+
</Compile>
518522
<Compile Include="SpringFilesUnitsyncAttempt.cs" />
519523
<Compile Include="SteamWebApi.cs" />
520524
<Compile Include="UserLanguageNoteAttribute.cs" />
@@ -981,6 +985,9 @@
981985
<EmbeddedResource Include="Migrations\202604251056090_AddPwChargesWallclock.resx">
982986
<DependentUpon>202604251056090_AddPwChargesWallclock.cs</DependentUpon>
983987
</EmbeddedResource>
988+
<EmbeddedResource Include="Migrations\202604271235020_AddPwAttackChargesPassiveLimit.resx">
989+
<DependentUpon>202604271235020_AddPwAttackChargesPassiveLimit.cs</DependentUpon>
990+
</EmbeddedResource>
984991
<EmbeddedResource Include="ZkDataResources.resx">
985992
<Generator>ResXFileCodeGenerator</Generator>
986993
<LastGenOutput>ZkDataResources.Designer.cs</LastGenOutput>

ZkLobbyServer/SpringieInterface/PlanetWarsMatchMaker.cs

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ private async void TimerOnElapsed(object sender, ElapsedEventArgs elapsedEventAr
129129
RunSquadFormation();
130130
if (FormedSquads.Any())
131131
{
132+
// Charge spend happens here, the moment squads are formed — every attacker in
133+
// a FormedSquad has irrevocably committed (see disconnect locking above), so
134+
// there is no path that gives the charge back. LaunchAllBattles no longer
135+
// touches charges.
136+
var attackerNames = FormedSquads.SelectMany(s => s.Attackers).Distinct().ToList();
137+
if (attackerNames.Count > 0) await SpendAttackCharges(attackerNames);
138+
132139
Phase = PwPhase.DefendCollect;
133140
PhaseStartTime = DateTime.UtcNow;
134141
UpdateLobby();
@@ -447,15 +454,15 @@ private int AvgTopNWhr(IEnumerable<string> names, int slots)
447454

448455
private async Task LaunchAllBattles()
449456
{
450-
// charges are spent only for attackers whose squad reached a confirmed outcome — either
451-
// the Spring battle successfully started, or it was a concede. Battles that fail to start
452-
// (StartGame returned false, or setup threw) refund the commitment.
453-
var attackerNamesToChargeSpend = new List<string>();
457+
// Charges are spent at end of AttackCollect (squad formation), not here. By the time we launch,
458+
// the commitment is final regardless of whether StartGame succeeds or the squad concedes.
454459

455460
// one battle per squad — no merging across attacker factions (each (planet, attacker-faction) is its own slot)
456461
foreach (var squad in FormedSquads.ToList())
457462
{
458-
squad.Attackers = squad.Attackers.Where(x => server.ConnectedUsers.ContainsKey(x)).ToList();
463+
// Attackers are locked at squad formation; we do NOT drop disconnected ones — they keep their
464+
// slot in the Spring script and can reconnect into the running battle. Defenders are looser —
465+
// a defender who disconnected after RunDefenderAssignment is dropped here.
459466
squad.Defenders = squad.Defenders.Where(x => server.ConnectedUsers.ContainsKey(x)).ToList();
460467

461468
if (squad.Attackers.Count == 0) continue;
@@ -474,7 +481,6 @@ private async Task LaunchAllBattles()
474481

475482
if (await battle.StartGame())
476483
{
477-
attackerNamesToChargeSpend.AddRange(squad.Attackers);
478484
var attackerFactionShortcut = GetFactionShortcut(squad.AttackerFactionID) ?? "?";
479485
var text = $"Battle for planet {squad.Name} ({attackerFactionShortcut} attacks) starts on zk://@join_player:{squad.Attackers.FirstOrDefault()} Roster: {string.Join(",", squad.Attackers)} vs {string.Join(",", squad.Defenders)}";
480486
foreach (var fac in factions) await server.GhostChanSay(fac.Shortcut, text);
@@ -492,15 +498,12 @@ private async Task LaunchAllBattles()
492498
}
493499
else
494500
{
495-
// concede — zero defenders. Attackers still "attacked", so charge applies.
496-
attackerNamesToChargeSpend.AddRange(squad.Attackers);
501+
// concede — zero defenders. Charge was already spent at end of AttackCollect.
497502
RecordPlanetwarsLoss(squad);
498503
}
499504
}
500505

501506
FormedSquads.Clear();
502-
503-
if (attackerNamesToChargeSpend.Count > 0) await SpendAttackCharges(attackerNamesToChargeSpend);
504507
}
505508

506509
private async Task SpendAttackCharges(List<string> playerNames)
@@ -738,11 +741,12 @@ public async Task OnUserDisconnected(string name)
738741
}
739742
else if (Phase == PwPhase.DefendCollect)
740743
{
744+
// Attackers are locked at squad formation: a disconnect must NOT release them, otherwise
745+
// they could reconnect and switch into a defense slot (or simply dodge their charge spend).
746+
// They stay in squad.Attackers and are carried into the launched Spring battle so they can
747+
// reconnect into the running game.
741748
foreach (var squad in FormedSquads)
742-
{
743749
changed |= squad.DefenderVotes.RemoveAll(x => x == name) > 0;
744-
changed |= squad.Attackers.RemoveAll(x => x == name) > 0;
745-
}
746750
if (changed) UpdateDefendersFullTime();
747751
}
748752

@@ -767,6 +771,7 @@ private sealed class OptionSnapshot
767771
public int PlanetId;
768772
public int? AttackerFactionId;
769773
public string AttackerFactionShortcut;
774+
public string OwnerFactionShortcut;
770775
public string PlanetName;
771776
public string Map;
772777
public int IconSize;
@@ -808,6 +813,7 @@ private LobbySnapshot ComputeLobbySnapshot(PwPhase phase)
808813
PlanetId = opt.PlanetID,
809814
AttackerFactionId = opt.AttackerFactionID,
810815
AttackerFactionShortcut = GetFactionShortcut(opt.AttackerFactionID),
816+
OwnerFactionShortcut = GetFactionShortcut(opt.OwnerFactionID),
811817
PlanetName = opt.Name,
812818
Map = opt.Map,
813819
IconSize = opt.IconSize,
@@ -841,6 +847,7 @@ private LobbySnapshot ComputeLobbySnapshot(PwPhase phase)
841847
PlanetId = squad.PlanetID,
842848
AttackerFactionId = squad.AttackerFactionID,
843849
AttackerFactionShortcut = GetFactionShortcut(squad.AttackerFactionID),
850+
OwnerFactionShortcut = GetFactionShortcut(squad.OwnerFactionID),
844851
PlanetName = squad.Name,
845852
Map = squad.Map,
846853
IconSize = squad.IconSize,
@@ -904,6 +911,7 @@ private PwMatchCommand StampLobbyCommand(LobbySnapshot snapshot, PwPhase phase,
904911
PlayerIsAttacker = playerName != null && s.AttackerNames.Contains(playerName),
905912
PlayerIsDefender = false,
906913
AttackerFaction = s.AttackerFactionShortcut,
914+
OwnerFaction = s.OwnerFactionShortcut,
907915
AttackerAvgWhr = s.AttackerAvgWhr,
908916
DefenderAvgWhr = null,
909917
WinChance = null,
@@ -938,6 +946,7 @@ private PwMatchCommand StampLobbyCommand(LobbySnapshot snapshot, PwPhase phase,
938946
PlayerIsAttacker = playerIsAttacker,
939947
PlayerIsDefender = playerName != null && s.DefenderNames.Contains(playerName),
940948
AttackerFaction = s.AttackerFactionShortcut,
949+
OwnerFaction = s.OwnerFactionShortcut,
941950
AttackerAvgWhr = s.AttackerAvgWhr,
942951
DefenderAvgWhr = s.DefenderAvgWhr,
943952
WinChance = s.WinChance,
@@ -1184,8 +1193,12 @@ public void RemoveFromRunningBattles(int battleID)
11841193
public static PwAttackCharges BuildPwAttackCharges(Account account)
11851194
{
11861195
var max = DynamicConfig.Instance.PwAttackChargesMax;
1196+
// Passive recharge stops at PwAttackChargesPassiveLimit; charges above that come only from active
1197+
// grants (e.g. defense rewards). Show NextRechargeTime only while the player is still under the
1198+
// passive cap — once they're at or above it, no passive tick is coming.
1199+
var passiveLimit = Math.Min(DynamicConfig.Instance.PwAttackChargesPassiveLimit, max);
11871200
DateTime? nextRechargeTime = null;
1188-
if (max > 0 && account.PwAttackCharges < max && account.PwLastChargeChange != null)
1201+
if (max > 0 && account.PwAttackCharges < passiveLimit && account.PwLastChargeChange != null)
11891202
nextRechargeTime = account.PwLastChargeChange.Value.AddMinutes(DynamicConfig.Instance.PwAttackChargesCooldownMinutes).CeilingToMinute();
11901203
return new PwAttackCharges
11911204
{
@@ -1219,6 +1232,12 @@ private async Task ProcessChargeRecharge()
12191232
{
12201233
var max = DynamicConfig.Instance.PwAttackChargesMax;
12211234
if (max <= 0) return;
1235+
// Passive recharge tops out at PwAttackChargesPassiveLimit (clamped to max). Active grants
1236+
// (defense rewards) can push players above this and they keep what they have, but no idle
1237+
// tick will. Default of 1 means a player who logged off at 0 wakes up tomorrow with one
1238+
// charge — enough to attack OR to incentivise defending to stockpile more.
1239+
var passiveLimit = Math.Min(DynamicConfig.Instance.PwAttackChargesPassiveLimit, max);
1240+
if (passiveLimit <= 0) return;
12221241
var cooldownMinutes = DynamicConfig.Instance.PwAttackChargesCooldownMinutes;
12231242
// +35s offset: displayed nextRechargeTime is rounded up to a full minute. Bumping the
12241243
// eligibility window forward absorbs ≤1min jitter between the recharge check and the
@@ -1230,11 +1249,11 @@ private async Task ProcessChargeRecharge()
12301249
{
12311250
bumped = db.Accounts.Where(a =>
12321251
a.FactionID != null &&
1233-
a.PwAttackCharges < max &&
1252+
a.PwAttackCharges < passiveLimit &&
12341253
a.PwLastChargeChange != null &&
12351254
a.PwLastChargeChange <= threshold).ToList();
12361255

1237-
foreach (var acc in bumped) acc.GrantPwAttackCharge(max);
1256+
foreach (var acc in bumped) acc.GrantPwAttackCharge(passiveLimit);
12381257

12391258
if (bumped.Count > 0) db.SaveChanges();
12401259
}
@@ -1285,6 +1304,7 @@ private static PwStatus GeneratePwStatus()
12851304
PlanetWarsNextModeTime = MiscVar.PlanetWarsNextModeTime,
12861305
AttackerPhaseMinutes = GlobalConst.PlanetWarsMinutesToAttack,
12871306
DefenderPhaseMinutes = GlobalConst.PlanetWarsMinutesToAccept,
1307+
MaxAttackCharges = DynamicConfig.Instance.PwAttackChargesMax,
12881308
};
12891309
}
12901310

0 commit comments

Comments
 (0)