Hinter MatchOptions.Extended (Standard im Spiel, Schalter im Startbildschirm): Söldner-Einkommen (Angriffschampion +Preis/12 Einkommen dauerhaft), Kasernen (gefallener innerer Turm: Gegnerwellen +25 % und ein Monster mehr), Erschöpfung und Rückkauf (60 % Leben, Rückkauf 60 + 20 x Stufe), Mondsegen (Grottensieg +4 % je Stapel, Mondseele bei 4). Bots wählen mit erweiterten Regeln auch Sylvara und kaufen Helden zurück. Netz überträgt Regeln und neue Zustände. HUD: Einkommen, Kasernen-Symbol, Rückkauf, Segen im Teamkopf. Kern 531/531 (s1 31), EditMode 31/31, PlayMode 16/16, Verify 718/0, Selbsttest 30/30, Golden 0 Abweichungen, Fingerabdruck unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
573 lines
25 KiB
C#
573 lines
25 KiB
C#
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using Luneraria.Core;
|
|
|
|
namespace Luneraria.UI
|
|
{
|
|
/// <summary>Network state the HUD shows (role, connection); implemented by the session in U4.</summary>
|
|
public interface ILuneHudSession
|
|
{
|
|
bool IsNetworked { get; }
|
|
bool IsAuthority { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// What one seat may see, prepared for display (port of scripts/luneraria/ui/lune_hud_model.gd).
|
|
/// Every HUD element reads through this model, so the privacy rules live in one place:
|
|
/// - gold, draft, undo and uncommitted bookings exist only for the viewer's own seat;
|
|
/// - of other players the model exposes names, lanes, readiness, human/bot/offline and the
|
|
/// committed hero (public since the last combat), never their gold or current plans;
|
|
/// - lanes, buildings, the boss and the waves come from public state only.
|
|
/// The model never changes the match; commands still go through the game controller.
|
|
/// </summary>
|
|
public sealed class LuneHudModel
|
|
{
|
|
public static readonly Dictionary<string, string> TowerNames = new Dictionary<string, string>
|
|
{ { "outer", "Grenzwacht" }, { "inner", "Bastion" }, { "base", "Kernwacht" }, { "core", "Ancient" } };
|
|
public static readonly Dictionary<string, string> TierText = new Dictionary<string, string>
|
|
{ { "outer", "Außenturm" }, { "inner", "Innenturm" }, { "base", "Basisturm" }, { "core", "Ancient" } };
|
|
|
|
public readonly LuneMatch State;
|
|
public readonly int Slot;
|
|
/// <summary>Optional network session; only read for the display (role, connection).</summary>
|
|
public ILuneHudSession Session;
|
|
readonly HashSet<int> _wasHuman = new HashSet<int>();
|
|
|
|
public LuneHudModel(LuneMatch state, int viewer, ILuneHudSession session = null)
|
|
{
|
|
State = state;
|
|
Slot = System.Math.Max(0, viewer);
|
|
Session = session;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- phase and own seat
|
|
public bool Planning { get { return State.Phase == Phase.Planning; } }
|
|
public bool Fighting { get { return State.Phase == Phase.Combat && State.Combat != null; } }
|
|
public bool Over { get { return State.Phase == Phase.Over; } }
|
|
public Player Me { get { return State.Players[Slot]; } }
|
|
public int Team { get { return Me.Team; } }
|
|
public int Enemy { get { return 1 - Team; } }
|
|
public int OwnGold { get { return State.GoldOf(Slot); } }
|
|
/// <summary>Base income paid at the end of the current round's combat (rules: LuneBalance.RoundIncome).</summary>
|
|
public int Income { get { return State.Balance.RoundIncome(State.RoundNumber) + IncomeBonus; } }
|
|
/// <summary>S1: the own permanent champion income (Legion TD 2), 0 under the reference rules.</summary>
|
|
public int IncomeBonus { get { return State.Extended ? Me.IncomeBonus : 0; } }
|
|
/// <summary>After Ready the plan is binding (the host refuses later commands).</summary>
|
|
public bool Locked { get { return Planning && Me.Ready; } }
|
|
public List<RosterEntry> OwnRoster { get { return Planning ? Me.Draft.Roster : Me.Roster; } }
|
|
/// <summary>The own hero: the draft in planning, the committed hero otherwise; null without a hero.</summary>
|
|
public HeroState OwnHero { get { return Planning ? Me.Draft.Hero : Me.Hero; } }
|
|
public bool CanUndo { get { return Planning && !Locked && Me.Undo.Count > 0; } }
|
|
public int SupplyCap { get { return State.Balance.SupplyCap; } }
|
|
public bool Networked { get { return Session != null && Session.IsNetworked; } }
|
|
public bool IsGuest { get { return Networked && !Session.IsAuthority; } }
|
|
|
|
public sealed class BookingView
|
|
{
|
|
public string Uid = "";
|
|
public string ChampionId = "";
|
|
public string Lane = "";
|
|
public int DeployRound;
|
|
public int Cost;
|
|
/// <summary>Booked in this planning (still refundable).</summary>
|
|
public bool Fresh;
|
|
|
|
public override string ToString() { return Uid + ":" + ChampionId + ":" + Lane + ":" + DeployRound + ":" + Fresh; }
|
|
}
|
|
|
|
public List<BookingView> OwnBookings()
|
|
{
|
|
var result = new List<BookingView>();
|
|
if (Planning)
|
|
{
|
|
foreach (Booking booking in Me.Draft.NewBookings)
|
|
result.Add(new BookingView { Uid = booking.Uid, ChampionId = booking.ChampionId, Lane = booking.Lane, Cost = booking.Cost, DeployRound = State.RoundNumber + 1, Fresh = true });
|
|
}
|
|
foreach (Booking booking in Me.Bookings)
|
|
{
|
|
if (booking.DeployRound >= State.RoundNumber)
|
|
result.Add(new BookingView { Uid = booking.Uid, ChampionId = booking.ChampionId, Lane = booking.Lane, Cost = booking.Cost, DeployRound = booking.DeployRound });
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- units and heroes
|
|
public string RoleOf(string typeId) { return State.RoleOf(typeId); }
|
|
|
|
public sealed class TroopInfo
|
|
{
|
|
public UnitDef Def;
|
|
public string Role = "";
|
|
public int Stage;
|
|
}
|
|
|
|
/// <summary>Catalogue definition of a troop with its role and stage.</summary>
|
|
public TroopInfo Troop(string typeId)
|
|
{
|
|
return new TroopInfo { Def = DataCatalog.GetUnit(typeId), Role = RoleOf(typeId), Stage = LuneStats.StageOf(typeId) };
|
|
}
|
|
|
|
/// <summary>Live combat unit for a persistent uid of the viewer (or null).</summary>
|
|
public LuneUnit LiveUnit(string uid)
|
|
{
|
|
if (!Fighting || string.IsNullOrEmpty(uid))
|
|
return null;
|
|
foreach (LuneUnit unit in State.Combat.Units)
|
|
{
|
|
if (unit.PersistentUid == uid)
|
|
return unit;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public UnitDef HeroDefinition(HeroState hero)
|
|
{
|
|
UnitDef definition = LuneContent.HeroDefinition(hero);
|
|
definition.Role = ContentData.HeroRoles.GetValueOrDefault(hero.HeroId, "front");
|
|
return definition;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- buildings
|
|
public sealed class BuildingInfo
|
|
{
|
|
public string Id = "";
|
|
public int Team;
|
|
public string Tier = "outer";
|
|
public string Lane = "";
|
|
public bool Alive;
|
|
public double Hp;
|
|
public double MaxHp = 1.0;
|
|
public double Fraction;
|
|
public bool Attackable;
|
|
public BuildingInfo[] Base;
|
|
|
|
public override string ToString()
|
|
{
|
|
return Id + ":" + Alive + ":" + LuneUiTheme.Round(Hp) + ":" + Attackable;
|
|
}
|
|
}
|
|
|
|
public BuildingInfo Building(string id)
|
|
{
|
|
BuildingDef definition = State.Map.BuildingDef(id);
|
|
double maxHp = definition != null ? definition.MaxHp : 1.0;
|
|
double hp = State.BuildingHp.GetValueOrDefault(id, 0.0);
|
|
bool alive = hp > 0.0;
|
|
if (Fighting && State.Combat.Buildings.TryGetValue(id, out LuneUnit unit))
|
|
{
|
|
alive = unit.Alive;
|
|
hp = alive ? unit.Hp : 0.0;
|
|
}
|
|
return new BuildingInfo
|
|
{
|
|
Id = id, Team = definition != null ? definition.Team : 0, Tier = definition != null ? definition.Tier : "outer", Lane = definition != null ? definition.Lane : "",
|
|
Alive = alive, Hp = hp, MaxHp = maxHp, Fraction = GdMath.Clamp(hp / GdMath.Max(1.0, maxHp), 0.0, 1.0),
|
|
Attackable = alive && definition != null && LuneMap.IsAttackable(definition, AliveMap()),
|
|
};
|
|
}
|
|
|
|
Dictionary<string, bool> AliveMap()
|
|
{
|
|
if (Fighting)
|
|
return State.Combat.AliveMap();
|
|
var result = new Dictionary<string, bool>();
|
|
foreach (KeyValuePair<string, double> pair in State.BuildingHp)
|
|
result[pair.Key] = pair.Value > 0.0;
|
|
return result;
|
|
}
|
|
|
|
public static string BuildingTitle(string id)
|
|
{
|
|
string[] parts = id.Split('_');
|
|
if (parts[1] == "ancient")
|
|
return "Ancient";
|
|
if (parts[1] == "base")
|
|
return "Kernwacht";
|
|
return TowerNames.TryGetValue(parts[2], out string name) ? name : id;
|
|
}
|
|
|
|
public static string BuildingPlace(string id)
|
|
{
|
|
string[] parts = id.Split('_');
|
|
string team = parts[0] == "blau" ? "Blau" : "Rot";
|
|
if (parts[1] == "ancient")
|
|
return "Ancient " + team;
|
|
if (parts[1] == "base")
|
|
return string.Format("Basis {0} · Kernwacht {1}", team, parts[2]);
|
|
return string.Format("{0} · {1} {2}", LuneMap.LaneNames[parts[1]].ToUpperInvariant(), BuildingTitle(id), team);
|
|
}
|
|
|
|
/// <summary>A team's Ancient with its two base towers (Kernwacht) in Base.</summary>
|
|
public BuildingInfo Ancient(int team)
|
|
{
|
|
string prefix = team == 0 ? "blau" : "rot";
|
|
BuildingInfo core = Building(prefix + "_ancient");
|
|
core.Base = new[] { Building(prefix + "_base_1"), Building(prefix + "_base_2") };
|
|
return core;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- players
|
|
public string PlayerName(int seat)
|
|
{
|
|
return seat == Slot ? "Du" : State.Players[seat].Name;
|
|
}
|
|
|
|
/// <summary>Seat status for display: self, human, bot, or offline (was human, a bot plays the seat).</summary>
|
|
public string StatusOf(int seat)
|
|
{
|
|
Player player = State.Players[seat];
|
|
if (player.IsHuman)
|
|
_wasHuman.Add(seat);
|
|
if (seat == Slot)
|
|
return "self";
|
|
if (player.IsHuman)
|
|
return "human";
|
|
return _wasHuman.Contains(seat) ? "offline" : "bot";
|
|
}
|
|
|
|
public sealed class MemberInfo
|
|
{
|
|
public int Slot;
|
|
public string Name = "";
|
|
public string Lane = "";
|
|
public bool IsSelf;
|
|
public bool Ready;
|
|
public string Status = "";
|
|
public string Bund = "";
|
|
public string HeroId = "";
|
|
public int HeroLevel;
|
|
|
|
public override string ToString()
|
|
{
|
|
return Slot + ":" + Name + ":" + Lane + ":" + IsSelf + ":" + Ready + ":" + Status + ":" + Bund + ":" + HeroId + ":" + HeroLevel;
|
|
}
|
|
}
|
|
|
|
public List<MemberInfo> Members(int team)
|
|
{
|
|
var result = new List<MemberInfo>();
|
|
for (int seat = team * 3; seat < team * 3 + 3; seat++)
|
|
{
|
|
Player player = State.Players[seat];
|
|
// Own seat: current draft. Everyone else: the committed hero, public since the last combat.
|
|
HeroState hero = seat == Slot ? OwnHero : player.Hero;
|
|
result.Add(new MemberInfo
|
|
{
|
|
Slot = seat, Name = PlayerName(seat), Lane = player.Lane, IsSelf = seat == Slot, Ready = player.Ready, Status = StatusOf(seat), Bund = player.Bund,
|
|
HeroId = hero != null ? hero.HeroId : "", HeroLevel = hero != null ? LuneContent.LevelFor(hero.Xp) : 0,
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>Ready humans and humans in the match.</summary>
|
|
public void ReadyCount(out int ready, out int humans)
|
|
{
|
|
ready = 0;
|
|
humans = 0;
|
|
foreach (Player player in State.Players)
|
|
{
|
|
if (!player.IsHuman)
|
|
continue;
|
|
humans++;
|
|
if (player.Ready)
|
|
ready++;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------- lanes
|
|
public sealed class HeroBrief
|
|
{
|
|
public string Model = "";
|
|
public string Name = "";
|
|
public int Level;
|
|
|
|
public override string ToString() { return Model + ":" + Level; }
|
|
}
|
|
|
|
public sealed class LaneInfo
|
|
{
|
|
public string Lane = "";
|
|
public string Title = "";
|
|
public string OwnPlayer = "";
|
|
public string EnemyPlayer = "";
|
|
public bool OwnIsMe;
|
|
/// <summary>Own inner, own outer, enemy outer, enemy inner: the chain from the own base to the enemy's.</summary>
|
|
public BuildingInfo[] Towers;
|
|
public LuneLaneThreat.Result Threat;
|
|
public readonly List<Announcement> Dangers = new List<Announcement>();
|
|
public HeroBrief OwnHero;
|
|
public HeroBrief EnemyHero;
|
|
/// <summary>Unit counts exist only in combat.</summary>
|
|
public bool HasCounts;
|
|
public int OwnCount;
|
|
public int EnemyCount;
|
|
/// <summary>S1 barracks: own waves reinforced here (enemy inner tower fell) / enemy waves reinforced.</summary>
|
|
public bool OwnBarracks;
|
|
public bool EnemyBarracks;
|
|
}
|
|
|
|
public LaneInfo Lane(string lane)
|
|
{
|
|
int ownSeat = State.SlotOf(Team, lane);
|
|
int enemySeat = State.SlotOf(Enemy, lane);
|
|
string own = Team == 0 ? "blau" : "rot";
|
|
string enemy = Team == 0 ? "rot" : "blau";
|
|
var result = new LaneInfo
|
|
{
|
|
Lane = lane, Title = LuneMap.LaneNames[lane].ToUpperInvariant(), OwnPlayer = PlayerName(ownSeat), EnemyPlayer = PlayerName(enemySeat), OwnIsMe = ownSeat == Slot,
|
|
Towers = new[] { Building(own + "_" + lane + "_inner"), Building(own + "_" + lane + "_outer"), Building(enemy + "_" + lane + "_outer"), Building(enemy + "_" + lane + "_inner") },
|
|
Threat = LuneLaneThreat.Evaluate(State, Team, lane), OwnHero = HeroOf(ownSeat), EnemyHero = HeroOf(enemySeat),
|
|
OwnBarracks = State.Extended && State.BarracksBroken(Team, lane), EnemyBarracks = State.Extended && State.BarracksBroken(Enemy, lane),
|
|
};
|
|
foreach (Announcement entry in State.Announcements())
|
|
{
|
|
if (entry.Lane == lane)
|
|
result.Dangers.Add(entry);
|
|
}
|
|
if (Fighting)
|
|
{
|
|
LuneLaneThreat.Battle battle = LuneLaneThreat.Battlefield(State, Team, lane);
|
|
result.HasCounts = true;
|
|
result.OwnCount = battle.OwnCount;
|
|
result.EnemyCount = battle.EnemyCount;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>A seat's hero for display: the own draft for the viewer, the committed (public) hero for everyone else; null without a hero.</summary>
|
|
public HeroBrief HeroOf(int seat)
|
|
{
|
|
HeroState hero = seat == Slot ? OwnHero : State.Players[seat].Hero;
|
|
if (hero == null)
|
|
return null;
|
|
HeroSpec spec = LuneContent.Heroes[hero.HeroId];
|
|
return new HeroBrief { Model = spec.Model, Name = spec.Name, Level = LuneContent.LevelFor(hero.Xp) };
|
|
}
|
|
|
|
// ---------------------------------------------------------------- boss grotto
|
|
public sealed class BossInfo
|
|
{
|
|
/// <summary>available, contested, conquered, failed, deploy_next, deploying, defeated or cooldown.</summary>
|
|
public string State = "cooldown";
|
|
public int Team = -1;
|
|
public string Lane = "";
|
|
public int DeployRound;
|
|
public int WonRound;
|
|
public int Bounty;
|
|
public int RoundsLeft;
|
|
public bool Own;
|
|
public bool Chooser;
|
|
public int NextRound;
|
|
public bool HasHp;
|
|
public double Hp;
|
|
public double MaxHp = 1.0;
|
|
|
|
/// <summary>The public part (without the viewer's own/chooser flags).</summary>
|
|
public string PublicText()
|
|
{
|
|
return string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}:{3}:{4}:{5}:{6}:{7}:{8}", State, Team, Lane, DeployRound, WonRound, Bounty, RoundsLeft, NextRound,
|
|
HasHp ? LuneUiTheme.Round(Hp) + "/" + LuneUiTheme.Round(MaxHp) : "-");
|
|
}
|
|
|
|
public override string ToString() { return PublicText() + ":" + Own + ":" + Chooser; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Public boss state: available, contested, conquered, failed, deploy_next, deploying,
|
|
/// defeated, cooldown. On a guest the grotto fight is read from the mirrored units. The
|
|
/// chooser sees the lane of the own draft; everyone else the public claim lane.
|
|
/// </summary>
|
|
public BossInfo Boss()
|
|
{
|
|
return Boss(false);
|
|
}
|
|
|
|
BossInfo Boss(bool publicOnly)
|
|
{
|
|
BossClaim claim = State.Boss.Claim;
|
|
int round = State.RoundNumber;
|
|
var result = new BossInfo { RoundsLeft = System.Math.Max(0, State.Boss.NextAvailableRound - round), NextRound = State.Boss.NextAvailableRound };
|
|
if (Fighting)
|
|
{
|
|
foreach (LuneUnit unit in State.Combat.Units)
|
|
{
|
|
if (unit.Kind != "boss")
|
|
continue;
|
|
if (unit.Zone == "grotto")
|
|
{
|
|
if (unit.Alive)
|
|
{
|
|
result.State = "contested";
|
|
return result;
|
|
}
|
|
int winner = -1;
|
|
foreach (LuneUnit other in State.Combat.Units)
|
|
{
|
|
if (other.Alive && other.Zone == "grotto" && other.Team <= 1)
|
|
winner = other.Team;
|
|
}
|
|
result.State = winner >= 0 ? "conquered" : "failed";
|
|
result.Team = winner;
|
|
result.Own = winner == Team;
|
|
result.DeployRound = round + 1;
|
|
return result;
|
|
}
|
|
if (unit.Zone == "lane")
|
|
{
|
|
result.State = unit.Alive ? "deploying" : "defeated";
|
|
result.Team = unit.Team;
|
|
result.Own = unit.Team == Team;
|
|
result.Lane = unit.Lane;
|
|
result.DeployRound = round;
|
|
result.Bounty = LuneContent.BossBountyFor(round);
|
|
result.HasHp = true;
|
|
result.Hp = unit.Hp;
|
|
result.MaxHp = unit.MaxHp;
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
if (claim != null && claim.DeployRound >= round)
|
|
{
|
|
result.State = Planning ? "deploy_next" : "deploying";
|
|
result.Team = claim.Team;
|
|
result.Own = claim.Team == Team;
|
|
result.WonRound = claim.WonRound;
|
|
result.DeployRound = claim.DeployRound;
|
|
result.Bounty = LuneContent.BossBountyFor(claim.DeployRound);
|
|
result.Chooser = claim.ChooserSlot == Slot;
|
|
// The chooser's lane change is private until combat; others see the public claim lane.
|
|
string chosen = !publicOnly && result.Chooser && Planning ? Me.Draft.BossLane ?? "" : "";
|
|
result.Lane = chosen.Length > 0 ? chosen : claim.Lane;
|
|
return result;
|
|
}
|
|
if (State.GrottoAvailable())
|
|
result.State = "available";
|
|
return result;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- waves
|
|
public sealed class MonsterGroup
|
|
{
|
|
public string Id = "";
|
|
public string Name = "";
|
|
public int Count;
|
|
public double Hp;
|
|
public double Damage;
|
|
public double Range;
|
|
public bool New;
|
|
public int Armor;
|
|
public int Attack;
|
|
|
|
public override string ToString() { return Id + "x" + Count + (New ? "+" : ""); }
|
|
}
|
|
|
|
public sealed class ChampionNotice
|
|
{
|
|
public int Team;
|
|
public string Lane = "";
|
|
public string ChampionId = "";
|
|
public string Name = "";
|
|
public bool Own;
|
|
|
|
public override string ToString() { return Team + ":" + Lane + ":" + ChampionId; }
|
|
}
|
|
|
|
public sealed class BossNotice
|
|
{
|
|
public int Team;
|
|
public string Lane = "";
|
|
public int Bounty;
|
|
public bool Own;
|
|
|
|
public override string ToString() { return Team + ":" + Lane + ":" + Bounty; }
|
|
}
|
|
|
|
public sealed class WaveInfo
|
|
{
|
|
public int Round;
|
|
public readonly List<MonsterGroup> Monsters = new List<MonsterGroup>();
|
|
public int Count;
|
|
public readonly List<ChampionNotice> Champions = new List<ChampionNotice>();
|
|
public BossNotice Boss;
|
|
|
|
public override string ToString()
|
|
{
|
|
return Round + "|" + string.Join(",", Monsters) + "|" + Count + "|" + string.Join(",", Champions) + "|" + (Boss != null ? Boss.ToString() : "-");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The wave of the coming combat (planning) or the one after (combat). Monsters are public
|
|
/// and identical for both sides; champions and the boss only once they are announced.
|
|
/// </summary>
|
|
public WaveInfo NextWave()
|
|
{
|
|
int round = Fighting ? State.RoundNumber + 1 : State.RoundNumber;
|
|
List<UnitDef> wave = LuneWaves.Build(State.Balance, round);
|
|
List<string> before = round > 1 ? LuneWaves.UnlockedTypes(State.Balance, round - 1) : new List<string>();
|
|
var result = new WaveInfo { Round = round, Count = wave.Count };
|
|
var groups = new Dictionary<string, MonsterGroup>();
|
|
foreach (UnitDef monster in wave)
|
|
{
|
|
if (!groups.TryGetValue(monster.Id, out MonsterGroup group))
|
|
{
|
|
group = new MonsterGroup
|
|
{
|
|
Id = monster.Id, Name = monster.DisplayName, Hp = monster.MaxHp, Damage = monster.Damage, Range = monster.AttackRange,
|
|
New = round > 1 && !before.Contains(monster.Id), Armor = monster.ArmorType, Attack = monster.AttackType,
|
|
};
|
|
groups[monster.Id] = group;
|
|
result.Monsters.Add(group);
|
|
}
|
|
group.Count++;
|
|
}
|
|
if (!Fighting)
|
|
{
|
|
foreach (Announcement entry in State.Announcements())
|
|
{
|
|
if (entry.Kind == "boss")
|
|
result.Boss = new BossNotice { Team = entry.Team, Lane = entry.Lane, Bounty = entry.Bounty, Own = entry.Team == Team };
|
|
else
|
|
result.Champions.Add(new ChampionNotice { Team = entry.Team, Lane = entry.Lane, ChampionId = entry.ChampionId, Name = LuneContent.Champions[entry.ChampionId].Name, Own = entry.Team == Team });
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- network test support
|
|
/// <summary>
|
|
/// Everything the HUD shows that must be identical for every seat (host, guest, bot views),
|
|
/// as one string. The boss uses the public claim lane: the chooser's draft lane is private.
|
|
/// </summary>
|
|
public string PublicDigest()
|
|
{
|
|
var text = new StringBuilder();
|
|
text.Append("round ").Append(State.RoundNumber).Append(" phase ").Append(State.Phase).Append('\n');
|
|
foreach (BuildingDef definition in State.Map.Buildings)
|
|
{
|
|
BuildingInfo entry = Building(definition.Id);
|
|
text.Append(definition.Id).Append('=').Append(entry.Alive).Append('/').Append(LuneUiTheme.Round(entry.Hp)).Append(' ');
|
|
}
|
|
text.Append('\n');
|
|
foreach (string name in LuneMap.Lanes)
|
|
{
|
|
for (int side = 0; side < 2; side++)
|
|
{
|
|
LuneLaneThreat.Result threat = LuneLaneThreat.Evaluate(State, side, name);
|
|
text.Append(name).Append('/').Append(side).Append('=').Append(threat.Level).Append(' ').Append(string.Join(";", threat.Reasons)).Append('\n');
|
|
}
|
|
}
|
|
foreach (Player player in State.Players)
|
|
{
|
|
text.Append(player.Slot).Append(':').Append(player.Lane).Append(':').Append(player.Ready).Append(':').Append(player.IsHuman).Append(':')
|
|
.Append(player.Hero != null ? player.Hero.HeroId + "@" + player.Hero.Xp : "-").Append(' ');
|
|
}
|
|
text.Append('\n').Append("boss ").Append(Boss(true).PublicText()).Append('\n');
|
|
text.Append("wave ").Append(NextWave()).Append('\n');
|
|
return text.ToString();
|
|
}
|
|
}
|
|
}
|