diff --git a/Fixer/Program.cs b/Fixer/Program.cs index a76099d18..03a10430d 100644 --- a/Fixer/Program.cs +++ b/Fixer/Program.cs @@ -431,7 +431,6 @@ private static void TestPwMatchMaker() { var server = new global::ZkLobbyServer.ZkLobbyServer("", new PlanetwarsEventCreator()); var mm = server.PlanetWarsMatchMaker; - mm.AttackerSideCounter = 1; mm.GenerateLobbyCommand(); // simulate defend phase with a formed squad diff --git a/Shared/LobbyClient/Protocol/Messages.cs b/Shared/LobbyClient/Protocol/Messages.cs index b272d2726..af3512c38 100644 --- a/Shared/LobbyClient/Protocol/Messages.cs +++ b/Shared/LobbyClient/Protocol/Messages.cs @@ -674,7 +674,11 @@ public enum ModeType Defend = 2 } - public string AttackerFaction { get; set; } + /// + /// Distinct attacker faction shortcuts across all . In parallel-turn PW every + /// faction can be attacking simultaneously; each option also carries its own . + /// + public List AttackerFactions { get; set; } public DateTime Deadline { get; set; } @@ -690,6 +694,7 @@ public PwMatchCommand(ModeType mode) Mode = mode; Options = new List(); DefenderFactions = new List(); + AttackerFactions = new List(); } public class VoteOption @@ -702,6 +707,23 @@ public class VoteOption public List StructureImages { get; set; } public int IconSize { get; set; } public string PlanetName { get; set; } + public bool CanSelectForBattle { get; set; } + public bool PlayerIsAttacker { get; set; } + public bool PlayerIsDefender { get; set; } + /// + /// Faction shortcut of the attacker. Together with forms the (planet, attacker) + /// key that identifies this attack slot. Clients must echo it back in . + /// + public string AttackerFaction { get; set; } + /// Faction shortcut of the planet's current owner, or null for neutral planets. Lets the + /// lobby surface "you own this", "ally owns this", "neutral" hints during attack/defense pick. + public string OwnerFaction { get; set; } + /// Average PW-WHR of the projected attacker squad (top-TeamSize volunteers). 0 when none. + public int AttackerAvgWhr { get; set; } + /// Average PW-WHR of the projected defender squad. Null in AttackCollect phase or when no volunteers. + public int? DefenderAvgWhr { get; set; } + /// Attacker win chance 0-100 derived from WHR delta. Null when either side is empty. + public int? WinChance { get; set; } } } @@ -709,12 +731,27 @@ public class VoteOption public class PwJoinPlanet { public int PlanetID { get; set; } + /// + /// Which attack slot the player is interacting with. Required — a planet can be attacked by multiple + /// factions simultaneously, each a separate slot. For an attacker this should equal the user's own + /// faction; for a defender it identifies which incoming attack to defend against. + /// + public string AttackerFaction { get; set; } + } + + /// + /// Client → server: cancel the player's current attack or defense commitment for the cycle. + /// + [Message(Origin.Client)] + public class PwCancel + { } [Message(Origin.Server)] public class PwRequestJoinPlanet { public int PlanetID { get; set; } + public string AttackerFaction { get; set; } } @@ -722,12 +759,14 @@ public class PwRequestJoinPlanet public class PwJoinPlanetSuccess { public int PlanetID { get; set; } + public string AttackerFaction { get; set; } } [Message(Origin.Server)] public class PwAttackingPlanet { public int PlanetID { get; set; } + public string AttackerFaction { get; set; } } [Message(Origin.Client)] @@ -744,6 +783,19 @@ public class PwStatus public PlanetWarsModes? PlanetWarsNextMode { get; set; } public DateTime? PlanetWarsNextModeTime { get; set; } public int MinLevel { get; set; } + public int AttackerPhaseMinutes { get; set; } + public int DefenderPhaseMinutes { get; set; } + /// Server's configured max attack charges. Lobby renders X/Max in the charges UI; + /// before this field existed Chobby hardcoded 2 as the fallback default. + public int MaxAttackCharges { get; set; } + } + + [Message(Origin.Server)] + public class PwAttackCharges + { + public int Current { get; set; } + /// UTC time at which the next charge will be granted, rounded up to a full minute. Null when at max or charges are disabled. + public DateTime? NextRechargeTime { get; set; } } diff --git a/Shared/PlasmaShared/GlobalConst.cs b/Shared/PlasmaShared/GlobalConst.cs index b76b1ee48..88db7165b 100644 --- a/Shared/PlasmaShared/GlobalConst.cs +++ b/Shared/PlasmaShared/GlobalConst.cs @@ -161,7 +161,7 @@ static void SetMode(ModeType newMode) public const double PlanetMetalPerTurn = 1; public const double PlanetWarsEnergyToMetalRatio = 0.0; public const double PlanetWarsMaximumIP = 100.0; //maximum IP on each planet - public const int PlanetWarsVictoryPointsToWin = 100; + public const int PlanetWarsVictoryPointsToWin = 50; public const int VictoryPointDecay = 1; public const int BaseInfluencePerBattle = 32; public const int InfluencePerAttacker = 1; @@ -211,8 +211,8 @@ static void SetMode(ModeType newMode) public const int PostVoteHideThreshold = -6; public const bool OnlyAdminsSeePostVoters = false; public const int PlanetWarsMinutesToAttackIfNoOption = 2; - public const int PlanetWarsMinutesToAttack = 5; - public const int PlanetWarsMinutesToAccept = 10; + public const int PlanetWarsMinutesToAttack = 2; + public const int PlanetWarsMinutesToAccept = 2; public const int PlanetWarsDropshipsStayForMinutes = 2*60; public const int PlanetWarsMaxTeamsize = 4; public const double PlanetWarsDefenderWinKillCcMultiplier = 0.2; diff --git a/Shared/PlasmaShared/Utils.cs b/Shared/PlasmaShared/Utils.cs index 24baf9a70..3482db0e5 100644 --- a/Shared/PlasmaShared/Utils.cs +++ b/Shared/PlasmaShared/Utils.cs @@ -34,6 +34,12 @@ public static void SafeDispose(this IDisposable o) if (o != null) o.Dispose(); } + public static DateTime CeilingToMinute(this DateTime d) + { + var floored = new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, 0, d.Kind); + return d == floored ? d : floored.AddMinutes(1); + } + public static IEnumerable> ToIndexedList(this IEnumerable enumeration) { return enumeration.Select((x, i) => new Indexed(x, i)); diff --git a/Zero-K.info/Controllers/PlanetwarsAdminController.cs b/Zero-K.info/Controllers/PlanetwarsAdminController.cs index 84596c39b..20799cb46 100644 --- a/Zero-K.info/Controllers/PlanetwarsAdminController.cs +++ b/Zero-K.info/Controllers/PlanetwarsAdminController.cs @@ -365,6 +365,15 @@ public ActionResult StartGalaxy(int galaxyID) gal.Ended = null; gal.EndMessage = null; db.SaveChanges(); + + // Seed everyone at the passive recharge limit, not the absolute max — defending as the very + // first PW action of the new galaxy then has a tangible reward (stockpiling above passive cap). + var initialCharges = Math.Min(DynamicConfig.Instance.PwAttackChargesPassiveLimit, DynamicConfig.Instance.PwAttackChargesMax); + db.Accounts.Where(x => x.FactionID != null).Update(x => new Account() + { + PwAttackCharges = initialCharges, + PwLastChargeChange = null, + }); } return RedirectToAction("Index"); diff --git a/Zero-K.info/Controllers/PlanetwarsController.cs b/Zero-K.info/Controllers/PlanetwarsController.cs index 5b562e28b..b2b1b9dcd 100644 --- a/Zero-K.info/Controllers/PlanetwarsController.cs +++ b/Zero-K.info/Controllers/PlanetwarsController.cs @@ -70,19 +70,23 @@ public ActionResult BombPlanet(int planetID, int count, bool? useWarp) double ipKillAmmount = ipKillCount * GlobalConst.BomberKillIpAmount; if (ipKillAmmount > 0) { - var influenceDecayMin = planet.PlanetStructures.Where(x => x.IsActive && x.StructureType.EffectPreventInfluenceDecayBelow != null).Select(x => x.StructureType.EffectPreventInfluenceDecayBelow).OrderByDescending(x => x).FirstOrDefault() ?? 0; + var structureFloor = planet.PlanetStructures.Where(x => x.IsActive && x.StructureType.EffectPreventInfluenceDecayBelow != null).Select(x => x.StructureType.EffectPreventInfluenceDecayBelow).OrderByDescending(x => x).FirstOrDefault() ?? 0; + var globalFloor = DynamicConfig.Instance.PwBomberMinimumIpFloor; + var selfRate = DynamicConfig.Instance.PwBomberSelfIpRate; - - foreach (PlanetFaction pf in planet.PlanetFactions.Where(x => x.FactionID != acc.FactionID)) + foreach (PlanetFaction pf in planet.PlanetFactions) { - pf.Influence -= ipKillAmmount; - if (pf.Influence < 0) pf.Influence = 0; - - // prevent bombing below influence decaymin for owner - set by active structures - if (pf.FactionID == planet.OwnerFactionID && pf.Influence < influenceDecayMin) pf.Influence = influenceDecayMin; - } + bool isOwn = pf.FactionID == acc.FactionID; + double damage = isOwn ? ipKillAmmount * selfRate : ipKillAmmount; + if (damage <= 0) continue; + // owner gets max of global floor and any structure-provided floor; everyone else gets global only + double floor = (pf.FactionID == planet.OwnerFactionID) ? Math.Max(globalFloor, structureFloor) : globalFloor; + // floor only prevents pushing below it — a faction already below the floor is not raised by bombing + double effectiveFloor = Math.Min(pf.Influence, floor); + pf.Influence = Math.Max(pf.Influence - damage, Math.Max(effectiveFloor, 0)); + } } var args = new List @@ -793,10 +797,11 @@ public ActionResult MatchMakerAttack(int planetID) { var db = new ZkDataContext(); var planet = db.Planets.Single(x => x.PlanetID == planetID); - if (Global.IsAccountAuthorized && Global.Account.CanPlayerPlanetWars() && planet.CanMatchMakerPlay(db.CurrentAccount().Faction)) + var account = db.CurrentAccount(); + if (Global.IsAccountAuthorized && Global.Account.CanPlayerPlanetWars() && account?.FactionID != null && planet.CanMatchMakerPlay(account.Faction)) { - Global.Server.PlanetWarsMatchMaker.AddAttackOption(planet); - Global.Server.RequestJoinPlanet(Global.Account.Name, planet.PlanetID); + Global.Server.PlanetWarsMatchMaker.AddAttackOption(planet, account.FactionID.Value); + Global.Server.RequestJoinPlanet(Global.Account.Name, planet.PlanetID, account.Faction.Shortcut); } return RedirectToAction("Planet", new { id = planetID }); } @@ -808,16 +813,17 @@ public ActionResult MatchMaker() var pwm = Global.Server.PlanetWarsMatchMaker; if (pwm != null) { - var state = Global.Server.PlanetWarsMatchMaker.GenerateLobbyCommand(); + // admin view gets a per-viewer command so per-option flags render correctly + var state = Global.Server.PlanetWarsMatchMaker.GenerateLobbyCommand(Global.Account?.Name, Global.Account?.Faction?.Shortcut); if (state != null) return View("PwMatchMaker", state); } return Content("Match maker offline"); } [Auth] - public ActionResult MatchMakerJoin(int planetID) + public ActionResult MatchMakerJoin(int planetID, string attackerFaction) { - Global.Server.RequestJoinPlanet(Global.Account.Name, planetID); + Global.Server.RequestJoinPlanet(Global.Account.Name, planetID, attackerFaction); return MatchMaker(); } diff --git a/Zero-K.info/Views/Planetwars/Planet.cshtml b/Zero-K.info/Views/Planetwars/Planet.cshtml index 6e2a5b484..d9e390319 100644 --- a/Zero-K.info/Views/Planetwars/Planet.cshtml +++ b/Zero-K.info/Views/Planetwars/Planet.cshtml @@ -38,7 +38,7 @@ -@if (Global.IsAccountAuthorized && db.CurrentAccount().CanPlayerPlanetWars() && (Global.Server.PlanetWarsMatchMaker != null && Global.Server?.PlanetWarsMatchMaker?.AttackingFaction?.FactionID == Global.FactionID) && Model.CanMatchMakerPlay(db.CurrentAccount().Faction)) +@if (Global.IsAccountAuthorized && db.CurrentAccount().CanPlayerPlanetWars() && Global.Server.PlanetWarsMatchMaker != null && Global.Server.PlanetWarsMatchMaker.Phase == ZeroKWeb.PwPhase.AttackCollect && Model.OwnerFactionID != Global.FactionID && Model.CanMatchMakerPlay(db.CurrentAccount().Faction)) { ATTACK PLANET } diff --git a/Zero-K.info/Views/Planetwars/PwMatchMaker.cshtml b/Zero-K.info/Views/Planetwars/PwMatchMaker.cshtml index 85ce5ba16..b33da51be 100644 --- a/Zero-K.info/Views/Planetwars/PwMatchMaker.cshtml +++ b/Zero-K.info/Views/Planetwars/PwMatchMaker.cshtml @@ -1,23 +1,21 @@ -@using LobbyClient +@using LobbyClient @using PlasmaShared @using ZeroKWeb @using ZkData @model LobbyClient.PwMatchCommand @{ PwMatchCommand pw = Model; - string text = ""; + string text; var db = new ZkDataContext(); if (pw.Mode == PwMatchCommand.ModeType.Attack) { - text = string.Format("{0} picks a planet to attack", pw.AttackerFaction); + text = "Pick a planet to attack"; } else { - var planetNames = string.Join(", ", pw.Options.Select(o => o.PlanetName)); - text = string.Format("{0} attacks {2}, {1} defends", pw.AttackerFaction, string.Join(",", pw.DefenderFactions), planetNames); + var planetNames = string.Join(", ", pw.Options.Select(o => o.PlanetName + " (" + (o.AttackerFaction ?? "?") + ")")); + text = string.Format("Defend options: {0}", planetNames); } - - bool canClick = (pw.Mode == PwMatchCommand.ModeType.Attack && pw.AttackerFaction == Global.Account.Faction.Shortcut) || (pw.Mode == PwMatchCommand.ModeType.Defend && pw.DefenderFactions.Contains(Global.Account.Faction.Shortcut)); }
@@ -30,14 +28,23 @@ foreach (PwMatchCommand.VoteOption opt in pw.Options) { - @if (canClick) + @if (opt.CanSelectForBattle) { - @Ajax.ActionLink("Join", "MatchMakerJoin", new { planetID = opt.PlanetID }, new AjaxOptions { UpdateTargetId = "matchMaker", InsertionMode = InsertionMode.Replace }) + @Ajax.ActionLink("Join", "MatchMakerJoin", new { planetID = opt.PlanetID, attackerFaction = opt.AttackerFaction }, new AjaxOptions { UpdateTargetId = "matchMaker", InsertionMode = InsertionMode.Replace }) } @Html.PrintPlanet(db.Planets.First(x => x.PlanetID == opt.PlanetID)) + [@opt.AttackerFaction] [@opt.Count/@opt.Needed] + @if (opt.WinChance != null) + { + (WHR A:@opt.AttackerAvgWhr D:@opt.DefenderAvgWhr, @opt.WinChance% attacker) + } + else if (opt.AttackerAvgWhr > 0) + { + (WHR @opt.AttackerAvgWhr) + }      } } -
\ No newline at end of file + diff --git a/ZeroKLobby/Notifications/PwBar.cs b/ZeroKLobby/Notifications/PwBar.cs index c346b9df1..d3ea2167b 100644 --- a/ZeroKLobby/Notifications/PwBar.cs +++ b/ZeroKLobby/Notifications/PwBar.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Windows.Forms; using LobbyClient; @@ -59,9 +60,11 @@ void UpdateGui() deadline = DateTime.Now.AddSeconds(pw.DeadlineSeconds); timerLabel.Text = PlasmaShared.Utils.PrintTimeRemaining(pw.DeadlineSeconds); + var attackerFactionsText = string.Join(",", pw.AttackerFactions ?? new List()); + if (pw.Mode == PwMatchCommand.ModeType.Attack) { - headerLabel.Text = string.Format("{0} picks a planet to attack", pw.AttackerFaction); + headerLabel.Text = string.Format("{0} picks a planet to attack", attackerFactionsText); foreach (Button c in pnl.Controls.OfType