1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2008, Sven2
6 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// game over dialog showing winners and losers
19
20#include "C4GuiResource.h"
21#include <C4Include.h>
22#include <C4GameOverDlg.h>
23
24#include <C4Game.h>
25#include <C4FullScreen.h>
26#include <C4Player.h>
27#include <C4PlayerInfo.h>
28#include <C4PlayerInfoListBox.h>
29
30#include <format>
31
32// C4GoalDisplay
33
34C4GoalDisplay::GoalPicture::GoalPicture(const C4Rect &rcBounds, C4ID idGoal, bool fFulfilled)
35 : idGoal(idGoal), fFulfilled(fFulfilled), C4GUI::Window()
36{
37 // bounds
38 SetBounds(rcBounds);
39 // can't get specialized desc from object at the moment because of potential script callbacks!
40 StdStrBuf strGoalName, strGoalDesc;
41 // just get desc from def
42 C4Def *pGoalDef = Game.Defs.ID2Def(id: idGoal);
43 if (pGoalDef)
44 {
45 strGoalName.Copy(pnData: pGoalDef->GetName());
46 strGoalDesc.Copy(pnData: pGoalDef->GetDesc());
47 }
48 // get tooltip
49 const std::string toolTip{LoadResStrChoice(condition: fFulfilled, ifTrue: C4ResStrTableKey::IDS_DESC_GOALFULFILLED, ifFalse: C4ResStrTableKey::IDS_DESC_GOALNOTFULFILLED, args: strGoalName.getData(), args: strGoalDesc.getData())};
50 SetToolTip(toolTip.c_str());
51 // create buffered picture of goal definition
52 C4Def *pDrawDef = Game.Defs.ID2Def(id: idGoal);
53 if (pDrawDef)
54 {
55 Picture.Create(iWdt: C4PictureSize, iHgt: C4PictureSize);
56 // get an object instance to draw (optional; may be zero)
57 C4Object *pGoalObj = Game.Objects.FindInternal(id: idGoal);
58 // draw goal def!
59 pDrawDef->Draw(cgo&: Picture, fSelected: false, iColor: 0, pObj: pGoalObj);
60 }
61 // unfulfilled goal: grey out picture
62 if (!fFulfilled)
63 Picture.Grayscale(iOffset: 30);
64}
65
66void C4GoalDisplay::GoalPicture::DrawElement(C4FacetEx &cgo)
67{
68 // draw area
69 C4Facet cgoDraw;
70 cgoDraw.Set(nsfc: cgo.Surface, nx: cgo.X + rcBounds.x + cgo.TargetX, ny: cgo.Y + rcBounds.y + cgo.TargetY, nwdt: rcBounds.Wdt, nhgt: rcBounds.Hgt);
71 // draw buffered picture
72 Picture.Draw(cgo&: cgoDraw);
73 // draw star symbol if fulfilled
74 if (fFulfilled)
75 {
76 cgoDraw.Set(nsfc: cgoDraw.Surface, nx: cgoDraw.X + cgoDraw.Wdt * 1 / 2, ny: cgoDraw.Y + cgoDraw.Hgt * 1 / 2, nwdt: cgoDraw.Wdt / 2, nhgt: cgoDraw.Hgt / 2);
77 C4GUI::Icon::GetIconFacet(icoIconIndex: C4GUI::Ico_Star).Draw(cgo&: cgoDraw);
78 }
79}
80
81void C4GoalDisplay::SetGoals(const C4IDList &rAllGoals, const C4IDList &rFulfilledGoals, int32_t iGoalSymbolHeight)
82{
83 // clear previous
84 ClearChildren();
85 // determine goal display area by goal count
86 int32_t iGoalSymbolMargin = C4GUI_DefDlgSmallIndent;
87 int32_t iGoalSymbolAreaHeight = 2 * iGoalSymbolMargin + iGoalSymbolHeight;
88 int32_t iGoalAreaWdt = GetClientRect().Wdt;
89 int32_t iGoalsPerRow = std::max<int32_t>(a: 1, b: iGoalAreaWdt / iGoalSymbolAreaHeight);
90 int32_t iGoalCount = rAllGoals.GetNumberOfIDs();
91 int32_t iRowCount = (iGoalCount - 1) / iGoalsPerRow + 1;
92 C4Rect rcNewBounds = GetBounds();
93 rcNewBounds.Hgt = GetMarginTop() + GetMarginBottom() + iRowCount * iGoalSymbolAreaHeight;
94 SetBounds(rcNewBounds);
95 C4GUI::ComponentAligner caAll(GetClientRect(), 0, 0, true);
96 // place goal symbols in this area
97 int32_t iGoal = 0;
98 for (int32_t iRow = 0; iRow < iRowCount; ++iRow)
99 {
100 int32_t iColCount = std::min<int32_t>(a: iGoalCount - iGoal, b: iGoalsPerRow);
101 C4GUI::ComponentAligner caGoalArea(caAll.GetFromTop(iHgt: iGoalSymbolAreaHeight, iWdt: iColCount * iGoalSymbolAreaHeight), iGoalSymbolMargin, iGoalSymbolMargin, false);
102 for (int32_t iCol = 0; iCol < iColCount; ++iCol, ++iGoal)
103 {
104 C4ID idGoal = rAllGoals.GetID(index: iGoal);
105 bool fFulfilled = !!rFulfilledGoals.GetIDCount(id: idGoal, zeroDefVal: 1);
106 AddElement(pChild: new GoalPicture(caGoalArea.GetGridCell(iSectX: iCol, iSectXMax: iColCount, iSectY: iRow, iSectYMax: iRowCount), idGoal, fFulfilled));
107 }
108 }
109}
110
111// C4GameOverDlg
112
113bool C4GameOverDlg::is_shown = false;
114
115C4GameOverDlg::C4GameOverDlg() : C4GUI::Dialog((C4GUI::GetScreenWdt() < 800) ? (C4GUI::GetScreenWdt() - 10) : std::min<int32_t>(a: C4GUI::GetScreenWdt() - 150, b: 800),
116 (C4GUI::GetScreenHgt() < 600) ? (C4GUI::GetScreenHgt() - 10) : std::min<int32_t>(a: C4GUI::GetScreenHgt() - 150, b: 600),
117 LoadResStr(id: C4ResStrTableKey::IDS_TEXT_EVALUATION),
118 false), pNetResultLabel(nullptr), fIsNetDone(false), fIsQuitBtnVisible(false)
119{
120 is_shown = true; // assume dlg will be shown, soon
121
122 pBtnRestart = nullptr;
123 pBtnNextMission = nullptr;
124
125 bool hideRestart = false;
126 int32_t buttonCount = 2;
127 if (Game.Control.isCtrlHost() || (Game.C4S.Head.Film == 2))
128 {
129 ++buttonCount;
130 if (Game.NextMission)
131 {
132 if (C4GUI::GetScreenWdt() < 1280)
133 {
134 hideRestart = true;
135 }
136 else
137 {
138 ++buttonCount;
139 }
140 }
141 SetBounds(C4Rect(0, 0, (C4GUI::GetScreenWdt() < 1280) ? (C4GUI::GetScreenWdt() - 10) : std::min<int32_t>(a: C4GUI::GetScreenWdt() - 150, b: 1280), (C4GUI::GetScreenHgt() < 720) ? (C4GUI::GetScreenHgt() - 10) : std::min<int32_t>(a: C4GUI::GetScreenHgt() - 150, b: 720)));
142 }
143
144 UpdateOwnPos();
145
146 // indents / sizes
147 int32_t iDefBtnHeight = 32;
148 int32_t iIndentX1 = 10;
149 int32_t iIndentY1 = 6, iIndentY2 = 0;
150 // main screen components
151 C4GUI::ComponentAligner caMain(GetClientRect(), 0, iIndentY1, true);
152 int32_t iMainTextWidth = caMain.GetWidth() - 6 * iIndentX1;
153 caMain.GetFromBottom(iHgt: iIndentY2);
154 // lower button-area
155 C4GUI::ComponentAligner caBottom(caMain.GetFromBottom(iHgt: iDefBtnHeight + iIndentY1 * 2), iIndentX1, 0);
156 int32_t iBottomButtonSize = caBottom.GetInnerWidth();
157 iBottomButtonSize = std::min<int32_t>(a: iBottomButtonSize / 2 - 2 * iIndentX1, b: C4GUI::GetRes()->CaptionFont.GetTextWidth(szText: "Quit it, baby! And some.") * 13 / 10);
158 // goal display
159 const C4IDList &rGoals = Game.RoundResults.GetGoals();
160 const C4IDList &rFulfilledGoals = Game.RoundResults.GetFulfilledGoals();
161 if (rGoals.GetNumberOfIDs())
162 {
163 C4GoalDisplay *pGoalDisplay = new C4GoalDisplay(caMain.GetFromTop(C4GUI_IconExHgt));
164 pGoalDisplay->SetGoals(rAllGoals: rGoals, rFulfilledGoals, C4GUI_IconExHgt);
165 AddElement(pChild: pGoalDisplay);
166 // goal display may have resized itself; adjust component aligner
167 caMain.ExpandTop(C4GUI_IconExHgt - pGoalDisplay->GetBounds().Hgt);
168 }
169 // league/network result, present or pending
170 fIsNetDone = false;
171 bool fHasNetResult = Game.RoundResults.HasNetResult();
172 const char *szNetResult = nullptr;
173 if (Game.Parameters.isLeague() || fHasNetResult)
174 {
175 if (fHasNetResult)
176 szNetResult = Game.RoundResults.GetNetResultString();
177 else
178 szNetResult = LoadResStr(id: C4ResStrTableKey::IDS_TEXT_LEAGUEWAITINGFOREVALUATIO);
179 pNetResultLabel = new C4GUI::Label("", caMain.GetFromTop(iHgt: C4GUI::GetRes()->TextFont.GetLineHeight() * 2, iWdt: iMainTextWidth), ACenter, C4GUI_Caption2FontClr, nullptr, false, false, true);
180 AddElement(pChild: pNetResultLabel);
181 // only add label - contents and fIsNetDone will be set in next update
182 }
183 else
184 {
185 // otherwise, network is always done
186 fIsNetDone = true;
187 }
188 // extra evaluation string area
189 const char *szCustomEvaluationStrings = Game.RoundResults.GetCustomEvaluationStrings();
190 if (szCustomEvaluationStrings && *szCustomEvaluationStrings)
191 {
192 int32_t iMaxHgt = caMain.GetInnerHeight() / 3; // max 1/3rd of height for extra data
193 C4GUI::MultilineLabel *pCustomStrings = new C4GUI::MultilineLabel(caMain.GetFromTop(iHgt: 0 /* resized later*/, iWdt: iMainTextWidth), 0, 0, " ", true, true);
194 pCustomStrings->AddLine(szLine: szCustomEvaluationStrings, pFont: &C4GUI::GetRes()->TextFont, C4GUI_MessageFontClr, fDoUpdate: true, fMakeReadableOnBlack: false, pCaptionFont: nullptr);
195 C4Rect rcCustomStringBounds = pCustomStrings->GetBounds();
196 if (rcCustomStringBounds.Hgt > iMaxHgt)
197 {
198 // Buffer too large: Use a scrollbox instead
199 delete pCustomStrings;
200 rcCustomStringBounds.Hgt = iMaxHgt;
201 C4GUI::TextWindow *pCustomStringsWin = new C4GUI::TextWindow(rcCustomStringBounds, 0, 0, 0, 0, 0, " ", true, nullptr, 0, true);
202 pCustomStringsWin->SetDecoration(fDrawBG: false, fDrawFrame: false, pToGfx: nullptr, fAutoScroll: false);
203 pCustomStringsWin->AddTextLine(szText: szCustomEvaluationStrings, pFont: &C4GUI::GetRes()->TextFont, C4GUI_MessageFontClr, fDoUpdate: true, fMakeReadableOnBlack: false, pCaptionFont: nullptr);
204 caMain.ExpandTop(iByHgt: -iMaxHgt);
205 AddElement(pChild: pCustomStringsWin);
206 }
207 else
208 {
209 // buffer size OK: Reserve required space
210 caMain.ExpandTop(iByHgt: -rcCustomStringBounds.Hgt);
211 AddElement(pChild: pCustomStrings);
212 }
213 }
214 // player list area
215 C4GUI::ComponentAligner caPlayerArea(caMain.GetAll(), iIndentX1, 0);
216 iPlrListCount = 1; bool fSepTeamLists = false;
217 if (Game.Teams.GetTeamCount() == 2 && !Game.Teams.IsAutoGenerateTeams())
218 {
219 // exactly two predefined teams: Use two player list boxes; one for each team
220 iPlrListCount = 2;
221 fSepTeamLists = true;
222 }
223 ppPlayerLists = new C4PlayerInfoListBox *[iPlrListCount];
224 for (int32_t i = 0; i < iPlrListCount; ++i)
225 {
226 ppPlayerLists[i] = new C4PlayerInfoListBox(caPlayerArea.GetGridCell(iSectX: i, iSectXMax: iPlrListCount, iSectY: 0, iSectYMax: 1), C4PlayerInfoListBox::PILBM_Evaluation, fSepTeamLists ? Game.Teams.GetTeamByIndex(iIndex: i)->GetID() : 0);
227 ppPlayerLists[i]->SetSelectionDiabled(true);
228 ppPlayerLists[i]->SetDecoration(fDrawBG: false, pToGfx: nullptr, fAutoScroll: true, fDrawBorder: false);
229 AddElement(pChild: ppPlayerLists[i]);
230 }
231
232 // add buttons
233 pBtnExit = new C4GUI::CallbackButton<C4GameOverDlg>(LoadResStr(id: C4ResStrTableKey::IDS_BTN_ENDROUND), caBottom.GetGridCell(iSectX: 0, iSectXMax: buttonCount, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonSize, iSectSizeY: -1, fCenterPos: true), &C4GameOverDlg::OnExitBtn);
234 pBtnExit->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_ENDTHEROUND));
235 AddElement(pChild: pBtnExit);
236 pBtnContinue = new C4GUI::CallbackButton<C4GameOverDlg>(LoadResStr(id: C4ResStrTableKey::IDS_BTN_CONTINUEGAME), caBottom.GetGridCell(iSectX: 1, iSectXMax: buttonCount, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonSize, iSectSizeY: -1, fCenterPos: true), &C4GameOverDlg::OnContinueBtn);
237 pBtnContinue->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_CONTINUETHEROUNDWITHNOFUR));
238 AddElement(pChild: pBtnContinue);
239
240 // not available for regular replay and network clients, obviously
241 // it is available for films though, so you can create cinematics for adventures
242 if (Game.Control.isCtrlHost() || (Game.C4S.Head.Film == 2))
243 {
244 if (!hideRestart)
245 {
246 pBtnRestart = new C4GUI::CallbackButton<C4GameOverDlg>(LoadResStr(id: C4ResStrTableKey::IDS_BTN_RESTART), caBottom.GetGridCell(iSectX: 2, iSectXMax: buttonCount, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonSize, iSectSizeY: -1, fCenterPos: true), &C4GameOverDlg::OnRestartBtn);
247 pBtnRestart->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DESC_RESTART));
248 AddElement(pChild: pBtnRestart);
249 }
250
251 // convert continue button to "next mission" button if available
252 if (Game.NextMission)
253 {
254 pBtnNextMission = new C4GUI::CallbackButton<C4GameOverDlg>(Game.NextMissionText.getData(), caBottom.GetGridCell(iSectX: 3 - hideRestart, iSectXMax: buttonCount, iSectY: 0, iSectYMax: 1, iSectSizeX: iBottomButtonSize, iSectSizeY: -1, fCenterPos: true), &C4GameOverDlg::OnNextMissionBtn);
255 pBtnNextMission->SetToolTip(Game.NextMissionDesc.getData());
256 AddElement(pChild: pBtnNextMission);
257 }
258 }
259 // updates
260 pSec1Timer = new C4Sec1TimerCallback<C4GameOverDlg>(this);
261 Update();
262 // initial focus on quit button if visible, so space/enter/low gamepad buttons quit
263 fIsQuitBtnVisible = fIsNetDone || !Game.Network.isHost();
264}
265
266C4GameOverDlg::~C4GameOverDlg()
267{
268 pSec1Timer->Release();
269 delete[] ppPlayerLists;
270 is_shown = false;
271}
272
273void C4GameOverDlg::Update()
274{
275 for (int32_t i = 0; i < iPlrListCount; ++i) ppPlayerLists[i]->Update();
276 if (pNetResultLabel)
277 {
278 SetNetResult(szResultString: Game.RoundResults.GetNetResultString(), eResultType: Game.RoundResults.GetNetResult(), iPendingStreamingData: Game.Network.getPendingStreamData(), fIsStreaming: Game.Network.isStreaming());
279 }
280 // exit/continue button only visible for host if league streaming finished
281 bool fBtnsVisible = fIsNetDone || !Game.Network.isHost();
282 if (fBtnsVisible != fIsQuitBtnVisible)
283 {
284 fIsQuitBtnVisible = fBtnsVisible;
285 pBtnExit->SetVisibility(fBtnsVisible);
286 pBtnContinue->SetVisibility(fBtnsVisible);
287 }
288}
289
290void C4GameOverDlg::SetNetResult(const char *szResultString, C4RoundResults::NetResult eResultType, size_t iPendingStreamingData, bool fIsStreaming)
291{
292 // add info about pending streaming data
293 StdStrBuf sResult(szResultString, false);
294 if (fIsStreaming)
295 {
296 sResult.AppendChar(cChar: '|');
297 sResult.Append(pnData: std::format(fmt: "[!]Transmitting record to league server... ({} kb remaining)", args: iPendingStreamingData / 1024).c_str());
298 }
299 // message linebreak into box
300 StdStrBuf sBrokenResult;
301 C4GUI::GetRes()->TextFont.BreakMessage(szMsg: sResult.getData(), iWdt: pNetResultLabel->GetBounds().Wdt, pOut: &sBrokenResult, fCheckMarkup: true);
302 pNetResultLabel->SetText(szText: sBrokenResult.getData(), fAllowHotkey: false);
303 // all done?
304 if (eResultType != C4RoundResults::NR_None && !fIsStreaming)
305 {
306 // a final result is determined and all streaming data has been transmitted
307 fIsNetDone = true;
308 }
309 // network error?
310 if (eResultType == C4RoundResults::NR_NetError)
311 {
312 // disconnected. Do not show winners/losers
313 for (int32_t i = 0; i < iPlrListCount; ++i) ppPlayerLists[i]->SetMode(C4PlayerInfoListBox::PILBM_EvaluationNoWinners);
314 }
315}
316
317bool C4GameOverDlg::OnEnter()
318{
319 Game.MessageInput.KeyStartTypeIn(mode: C4ChatInputDialog::All);
320 return true;
321}
322
323void C4GameOverDlg::OnExitBtn(C4GUI::Control *btn)
324{
325 // callback: exit button pressed.
326 Close(fOK: false);
327}
328
329void C4GameOverDlg::OnContinueBtn(C4GUI::Control *btn)
330{
331 // callback: continue button pressed
332 Close(fOK: true);
333}
334
335void C4GameOverDlg::OnRestartBtn(C4GUI::Control *btn)
336{
337 // callback: restart button pressed
338 nextMissionMode = Restart;
339 Close(fOK: true);
340}
341
342void C4GameOverDlg::OnNextMissionBtn(C4GUI::Control *btn)
343{
344 // callback: next mission button pressed
345 nextMissionMode = NextMission;
346 Close(fOK: true);
347}
348
349void C4GameOverDlg::OnShown()
350{
351 // close some other dialogs
352 Game.Scoreboard.HideDlg();
353 FullScreen.CloseMenu();
354 for (C4Player *plr = Game.Players.First; plr; plr = plr->Next)
355 plr->CloseMenu();
356 // pause game when round results dlg is shown
357 Game.Pause();
358}
359
360void C4GameOverDlg::OnClosed(bool fOK)
361{
362 typedef C4GUI::Dialog BaseClass;
363 auto _nextMissionMode = nextMissionMode;
364 BaseClass::OnClosed(fOK); // deletes this!
365 if (_nextMissionMode != Restart)
366 {
367 Game.RestartRestoreInfos.Clear();
368 }
369 // continue round
370 if (fOK)
371 {
372 if (_nextMissionMode != None)
373 {
374 // switch to next mission if next mission button is pressed
375 Application.SetNextMission(_nextMissionMode == NextMission ? Game.NextMission.getData() : Game.ScenarioFilename);
376 Application.QuitGame();
377 }
378 else
379 {
380 // unpause game when continue is pressed
381 Game.Unpause();
382 }
383 }
384 // end round
385 else
386 {
387 Application.QuitGame();
388 }
389}
390