#priority init -200 import tabs.Tab; import tabs.tabbar; import elements.GuiButton; import elements.GuiText; import elements.GuiMarkupText; import elements.GuiEmpire; import elements.GuiSkinElement; import elements.GuiListbox; import elements.GuiPanel; import elements.MarkupTooltip; import util.formatting; import icons; from multiplayer import wantSpectator; class VictoryTab : Tab { GuiText@ heading; GuiSkinElement@ victorPanel; GuiMarkupText@ description; GuiEmpire@ victor; GuiSkinElement@ buttonBar; GuiPanel@ buttonPanel; GuiButton@ rankingsButton; GuiListbox@ rankings; array stats; array statButtons; Alignment@ statAlign; Sprite typeIcon; VictoryTab() { super(); @heading = GuiText(this, Alignment(Left, Top+30, Right, Top+90)); heading.horizAlign = 0.5; heading.font = FT_Big; heading.color = colors::Green; heading.stroke = colors::Black; @victorPanel = GuiSkinElement(this, Alignment(Left+0.1f, Top+90, Right-0.1f, Top+242), SS_Panel); @victor = GuiEmpire(victorPanel, Alignment(Left+6, Top+6, Width=140, Height=140)); @description = GuiMarkupText(victorPanel, Alignment(Left+220, Top+20, Right-6, Bottom-6)); description.defaultFont = FT_Subtitle; @buttonBar = GuiSkinElement(this, Alignment(Left+0.1f, Top+242+20, Right-0.1f, Height=42), SS_PlainBox); @buttonPanel = GuiPanel(this, Alignment(Left+0.1f, Top+242, Right-0.1f, Height=62)); buttonPanel.vertType = ST_Never; @statAlign = Alignment(Left+0.1f, Top+242+62, Right-0.1f, Bottom-30); if(!playerEmpire.valid) { victorPanel.visible = false; heading.visible = false; buttonBar.alignment.top.pixels = 32; buttonPanel.alignment.top.pixels = 12; statAlign.top.pixels = 74; } @rankingsButton = GuiButton(buttonPanel, Alignment(Left, Bottom-40, Width=135, Height=40), locale::V_RANKINGS); rankingsButton.buttonIcon = Sprite(material::PointsIcon); rankingsButton.toggleButton = true; rankingsButton.pressed = true; rankingsButton.font = FT_Bold; @rankings = GuiListbox(this, statAlign); rankings.itemHeight = 40; rankings.disabled = true; rankings.vertPadding = 8; rankings.style = SS_Panel; addStat(ST_Int, stat::Points, 60, locale::STAT_POINTS, Sprite(material::PointsIcon), 2000); addStat(ST_Float, stat::Military, 60, locale::STAT_STRENGTH, icons::Strength, 5000); addStat(ST_Int, stat::Planets, 15, locale::STAT_PLANETS, icons::Planet, 10); addStat(ST_Int, stat::Ships, 15, locale::STAT_SHIPS, icons::Defense, 10); addStat(ST_Float, stat::Budget, 60, locale::STAT_BUDGET, icons::Money, 2000); addStat(ST_Float, stat::NetBudget, 60, locale::STAT_NET_BUDGET, icons::Money, 2000); addStat(ST_Float, stat::EnergyIncome, 60, locale::STAT_ENERGY_INCOME, icons::Energy, 2); addStat(ST_Float, stat::InfluenceIncome, 60, locale::STAT_INFLUENCE_INCOME, icons::Influence, 3/60.0); addStat(ST_Float, stat::ResearchIncome, 60, locale::STAT_RESEARCH_INCOME, icons::Research, 2); addStat(ST_Float, stat::ResearchTotal, 60, locale::STAT_RESEARCH_TOTAL, icons::Research, 1000); updateAbsolutePosition(); update(); locked = true; } void addStat(StatType type, stat::EmpireStat Stat, int FilterDuration, const string& name, const Sprite& icon, double baseMax = 1) { StatPage page(this, statAlign, type, Stat, FilterDuration, name, baseMax); page.visible = false; stats.insertLast(page); int index = stats.length; GuiButton btn(buttonPanel, Alignment(Left+135*index, Bottom-40, Width=135, Height=40), name); btn.toggleButton = true; btn.pressed = false; btn.buttonIcon = icon; btn.font = FT_Bold; statButtons.insertLast(btn); } Color get_activeColor() { return Color(0xffc283ff); } Color get_inactiveColor() { return Color(0xffb900ff); } Color get_seperatorColor() { return Color(0x8d7949ff); } Sprite get_icon() { return typeIcon; } void switchPage(uint page) { rankings.visible = page == 0; rankingsButton.pressed = page == 0; for(uint i = 0, cnt = stats.length; i < cnt; ++i) { stats[i].visible = i+1 == page; statButtons[i].pressed = i+1 == page; } } void tick(double time) { update(); } int lastUpdate = systemTime - 1000; bool fullUpdate = true; uint nextUpdateIndex = 0; void update() { if(systemTime - lastUpdate < 1000) return; lastUpdate = systemTime; Empire@ winner = playerEmpire; int myVictory = playerEmpire.Victory; for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { if(getEmpire(i).Victory == 1) { @winner = getEmpire(i); break; } } //Show the appropriate victory message if(myVictory == 1) { heading.text = locale::V_WON_TITLE; heading.color = colors::Green; description.text = format(locale::V_WON_TEXT, formatEmpireName(winner), formatEmpireName(playerEmpire)); typeIcon = Sprite(material::PointsIcon); } else if(myVictory == -2 && playerEmpire.SubjugatedBy.Victory == 1) { heading.text = locale::V_LESSER_TITLE; heading.color = Color(0xfff500ff); description.text = format(locale::V_LESSER_TEXT, formatEmpireName(winner), formatEmpireName(playerEmpire)); typeIcon = Sprite(material::PointsIcon); } else { heading.text = locale::V_LOST_TITLE; heading.color = colors::Red; description.text = format(locale::V_LOST_TEXT, formatEmpireName(winner), formatEmpireName(playerEmpire)); typeIcon = Sprite(material::SystemUnderAttack); } if(victorPanel.visible) title = heading.text; else title = locale::V_SCORES; //Update rankings array sorted; for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { Empire@ other = getEmpire(i); if(other.major) { EmpSorter sort; @sort.emp = other; sorted.insertLast(sort); } } sorted.sortAsc(); rankings.clearItems(); for(uint i = 0, cnt = sorted.length; i < cnt; ++i) { Empire@ emp = sorted[i].emp; string text = ""; text += format(" #$1", toString(i+1)); text += format("[offset=100][img=$2;28;$3/] $1[/offset]", formatEmpireName(emp), getSpriteDesc(Sprite(emp.flag)), toString(emp.color)); if(hasGameEnded()) { string victory = ""; if(emp.SubjugatedBy !is null) victory = format("[color=#aaa]$1[/color]", format(locale::V_SUBJUGATED, formatEmpireName(emp.SubjugatedBy))); else if(emp.Victory == 1) victory = format("[color=#0f0]$1[/color]", locale::V_WON_TITLE); else victory = format("[color=#f00]$1[/color]", locale::V_LOST_TITLE); text += format("[offset=450]$1[/offset]", victory); } else { string victory; if(emp.SubjugatedBy !is null) victory = format("[color=#aaa]$1[/color]", format(locale::V_SUBJUGATED, formatEmpireName(emp.SubjugatedBy))); text += format("[offset=450]$1[/offset]", victory); } text += format("[offset=880]$1[/offset]", format(locale::EMPIRE_POINTS, toString(emp.points.value))); rankings.addItem(GuiMarkupListText(text, FT_Subtitle)); } //Update stats data if(fullUpdate) { fullUpdate = false; for(uint i = 0, cnt = stats.length; i < cnt; ++i) stats[i].update(); } else { //TODO: Even this probably isn't necessary, make sure stats[(nextUpdateIndex++) % stats.length].update(); } victorPanel.color = winner.color; @victor.empire = winner; if(!victorPanel.visible && playerEmpire.valid) { locked = false; shownVictory = false; closeTab(this); } } bool onKeyEvent(const KeyboardEvent& event, IGuiElement@ source) { return BaseGuiElement::onKeyEvent(event, source); } bool onGuiEvent(const GuiEvent& event) { if(event.type == GUI_Clicked) { if(event.caller is rankingsButton) { switchPage(0); return true; } else { for(uint i = 0, cnt = statButtons.length; i < cnt; ++i) { if(event.caller is statButtons[i]) { switchPage(i+1); return true; } } } } return BaseGuiElement::onGuiEvent(event); } void draw() { skin.draw(SS_DesignOverviewBG, SF_Normal, AbsolutePosition); BaseGuiElement::draw(); } }; class EmpSorter { Empire@ emp; int opCmp(const EmpSorter& other) const { if(other.emp.Victory != emp.Victory) { if(emp.Victory == 1) return -1; if(emp.Victory == -1) return 1; if(other.emp.Victory == 1) return 1; if(other.emp.Victory == -1) return -1; if(emp.Victory == -2) { if(other.emp.Victory == 0) return 1; if(emp.SubjugatedBy.Victory == 1) return -1; } if(other.emp.Victory == -2) { if(emp.Victory == 0) return -1; if(other.emp.SubjugatedBy.Victory == 1) return 1; } } if(emp.points.value > other.emp.points.value) return -1; if(emp.points.value < other.emp.points.value) return 1; return 0; } }; class StatPage : BaseGuiElement { stat::EmpireStat stat; StatType type; array trackers; double endTime = 0; double minVal = 0; double maxVal = 0; double baseMin = 0; double baseMax = 1; bool sqrScale = false; StatPage(IGuiElement@ parent, Alignment@ align, StatType type, stat::EmpireStat Stat, int FilterDuration, const string& name, double baseMax = 1) { super(parent, align); this.stat = Stat; this.type = type; this.baseMax = baseMax; if(stat == stat::Military) { this.sqrScale = true; this.baseMin = 2000; } for(uint i = 0, cnt = getEmpireCount(); i = 1000) return standardize(amt, true); else return toString(floor(amt), 0); } return standardize(amt, true); } string get_tooltip() override { int mx = mousePos.x; if(mx < AbsolutePosition.topLeft.x+52 || mx > AbsolutePosition.botRight.x-12) return ""; string text; double time = getOffsetTime(mx - absolutePosition.topLeft.x); text += format("[center][font=Subtitle]$1[/font][/center]", formatGameTime(time)); for(uint i = 0, cnt = trackers.length; i < cnt; ++i) { double amt = trackers[i].getClosest(time).y; text += format("[b]$1[offset=150]$2[/offset][/b]\n", formatEmpireName(trackers[i].emp), formatValue(amt)); } return text; } void update() { endTime = gameTime; minVal = baseMin; maxVal = baseMax; for(uint i = 0, cnt = trackers.length; i < cnt; ++i) { trackers[i].update(); minVal = min(minVal, trackers[i].minVal); maxVal = max(maxVal, trackers[i].maxVal); } for(uint i = 0, cnt = trackers.length; i < cnt; ++i) { trackers[i].minVal = minVal; trackers[i].maxVal = maxVal; } } void draw() { skin.draw(SS_Panel, SF_Normal, AbsolutePosition); const Font@ ft = skin.getFont(FT_Normal); { int interval = 100; vec2i pos(154, size.height-15); while(pos.x < size.x) { double time = double(pos.x-52) / double(size.x-64) * endTime; ft.draw(pos=recti_centered(pos+AbsolutePosition.topLeft, vec2i(200,30)), horizAlign=0.5, text=formatTime(time), color=Color(0xaaaaaaff)); drawLine(vec2i(pos.x, 12)+AbsolutePosition.topLeft, vec2i(pos.x, size.height-40)+AbsolutePosition.topLeft, Color(0xaaaaaa20)); pos.x += interval; } } { int interval = 40; vec2i pos(26, size.height-40); string prevVal; if(type == ST_Int) { double mult = ceil(double(size.height-52) / ceil(maxVal-minVal)); interval = ceil(ceil(double(interval) / mult) * mult); } while(pos.y > 14) { double val = 0; if(sqrScale) val = minVal + (maxVal - minVal) * sqr(1.0 - (double(pos.y-12) / double(size.y-52))); else val = minVal + (maxVal - minVal) * (1.0 - (double(pos.y-12) / double(size.y-52))); string valDisp = formatValue(val, true); if(valDisp != prevVal) { ft.draw(pos=recti_centered(pos+AbsolutePosition.topLeft, vec2i(200,30)), horizAlign=0.5, text=valDisp, color=Color(0xaaaaaaff)); prevVal = valDisp; } drawLine(vec2i(42, pos.y)+AbsolutePosition.topLeft, vec2i(size.width-12, pos.y)+AbsolutePosition.topLeft, Color(0xaaaaaa20)); pos.y -= interval; } if(minVal < 0 && maxVal > 0 && !sqrScale) { int y = size.height-40 - size.y * (-minVal) / (maxVal - minVal); drawLine(vec2i(42, y)+AbsolutePosition.topLeft, vec2i(size.width-12, y)+AbsolutePosition.topLeft, Color(0xaa000040), size=3); } } for(uint i = 0, cnt = trackers.length; i < cnt; ++i) trackers[i].draw(skin, AbsolutePosition.padded(42,12,12,40)); if(AbsolutePosition.isWithin(mousePos)) { int mx = mousePos.x; if(mx >= AbsolutePosition.topLeft.x+52 && mx <= AbsolutePosition.botRight.x-12) { drawLine(vec2i(mx, AbsolutePosition.topLeft.y+12), vec2i(mx, AbsolutePosition.botRight.y-40), Color(0xaaaaaaff)); } } BaseGuiElement::draw(); } }; enum StatType { ST_Int, ST_Float, } final class StatTracker { double minVal = 0, maxVal = 1; stat::EmpireStat stat; Color color; int filterDuration = 1; array data; Empire@ emp; StatType type; double endTime; bool sqrScale = false; array points; vec2i lastSize; StatTracker(StatType type, stat::EmpireStat Stat, const Color& col, int FilterDuration, Empire@ emp = playerEmpire, bool sqrScale = false) { stat = Stat; color = col; filterDuration = FilterDuration; this.type = type; @this.emp = emp; this.sqrScale = sqrScale; } vec2d getClosest(double time) { vec2d closest(0, 0); double dist = INFINITY; //Don't make me do a binary search you monster for(uint i = 0, cnt = data.length; i < cnt; ++i) { double d = abs(data[i].x - time); if(d < dist) { dist = d; closest = data[i]; } } return closest; } void rebuildPoints(vec2i size) { array pts(1); vec2d factor(double(size.x) / endTime, -double(size.y) / (maxVal - minVal)); vec2d offset(0.0, -minVal); uint lastX = 0; for(uint i = 0, cnt = data.length; i < cnt; ++i) { vec2d pt = data[i] + offset; pt.x *= factor.x; if(!sqrScale) { pt.y *= factor.y; } else { if(pt.y != 0) pt.y = -double(size.y) * sqrt(pt.y / (maxVal - minVal)); } uint x = int(pt.x); if(i == 0 && x != 0) pts[0] = vec3d(0.0, pt.y, 1.0); if(x != lastX) pts.resize(pts.length + 1); pts[pts.length-1] += vec3d(pt.x, pt.y, 1.0); } int maxTimeDelta = max(int(ceil(double(size.x * filterDuration) / endTime)), int(1)); points.length = 0; points.reserve(pts.length + 1); for(uint i = 0, cnt = pts.length; i < cnt; ++i) { vec3d pt = pts[i]; vec2i px = vec2i(pt.x / pt.z, pt.y / pt.z + 0.5); if(i > 0) { vec2i prev = points.last; if(prev.x < px.x - maxTimeDelta) points.insertLast(vec2i(px.x - 1, prev.y)); } points.insertLast(px); } if(points.last.x < size.x - 1) points.insertLast(vec2i(size.x - 1, points.last.y)); lastSize = size; } void update() { minVal = INFINITY; maxVal = -INFINITY; data.length = 0; StatHistory history(emp, stat); endTime = gameTime; while(history.advance(1)) { double val = 0; if(type == ST_Int) val = history.intVal; else val = history.floatVal; if(val > maxVal) maxVal = val; if(val < minVal) minVal = val; data.insertLast(vec2d(history.time, val)); } lastSize = vec2i(); } void draw(const Skin@ skin, recti bound) { vec2i size = bound.size; if(size != lastSize) rebuildPoints(size); if(points.length == 0) { drawLine(bound.topLeft + vec2i(0, bound.height), bound.botRight, color); return; } vec2i corner = bound.topLeft + vec2i(0,size.y); vec2i prev = points[0] + corner; uint lines = points.length - 1; uint index = 0; while(index < lines) { drawPolygonStart(PT_LineStrip, min(lines - index, 250)); drawPolygonPoint(prev, color); for(uint i = 0; i < 250 && index + i < lines; ++i) { vec2i pt = points[index + i + 1] + corner; drawPolygonPoint(pt); prev = pt; } drawPolygonEnd(); index += 250; } } }; Tab@ createVictoryTab() { return VictoryTab(); } void init() { /*auto@ tab = createVictoryTab();*/ /*newTab(tab);*/ /*switchToTab(tab);*/ } bool shownVictory = false; void tick(double time) { if(!shownVictory && (hasGameEnded() || (wantSpectator && !playerEmpire.valid))) { auto@ tab = createVictoryTab(); newTab(tab); if(hasGameEnded()) switchToTab(tab); shownVictory = true; } }