Files
OpenFrontIO/src/client/graphics/layers/WinModal.ts
T
evanpelle 1d8484843f Show random pattern on end screen (#1930)
## Description:

To advertise patterns, show a random, purchasable pattern on the end
screen.

* Refactored the pattern button into a reusable PatternButton lit
component
* Used tailwind instead of CSS for styling because the CSS affects lit
components due to using the light-dom
* Removed the tooltip, didn't seem necessary since there is already a
big "purchase" button under the pattern

<img width="383" height="556" alt="Screenshot 2025-08-25 at 1 26 26 PM"
src="https://github.com/user-attachments/assets/3f109cea-2759-4a07-9322-4a1a30b43503"
/>

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
2025-08-25 13:27:08 -07:00

232 lines
6.5 KiB
TypeScript

import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { Pattern } from "../../../core/CosmeticSchemas";
import { EventBus } from "../../../core/EventBus";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import "../../components/PatternButton";
import { fetchPatterns, handlePurchase } from "../../Cosmetics";
import { getUserMe } from "../../jwt";
import { SendWinnerEvent } from "../../Transport";
import { GutterAdModalEvent } from "./GutterAdModal";
import { Layer } from "./Layer";
@customElement("win-modal")
export class WinModal extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
private hasShownDeathModal = false;
@state()
isVisible = false;
@state()
showButtons = false;
@state()
private patternContent: TemplateResult | null = null;
private _title: string;
// Override to prevent shadow DOM creation
createRenderRoot() {
return this;
}
constructor() {
super();
}
render() {
return html`
<div
class="${this.isVisible
? "fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-gray-800/70 p-6 rounded-lg z-[9999] shadow-2xl backdrop-blur-sm text-white w-[350px] max-w-[90%] md:max-w-[350px] animate-fadeIn"
: "hidden"}"
>
<h2 class="m-0 mb-4 text-[26px] text-center text-white">
${this._title || ""}
</h2>
${this.innerHtml()}
<div
class="${this.showButtons
? "flex justify-between gap-2.5"
: "hidden"}"
>
<button
@click=${this._handleExit}
class="flex-1 px-3 py-3 text-base cursor-pointer bg-blue-500/60 text-white border-0 rounded transition-all duration-200 hover:bg-blue-500/80 hover:-translate-y-px active:translate-y-px"
>
${translateText("win_modal.exit")}
</button>
<button
@click=${this.hide}
class="flex-1 px-3 py-3 text-base cursor-pointer bg-blue-500/60 text-white border-0 rounded transition-all duration-200 hover:bg-blue-500/80 hover:-translate-y-px active:translate-y-px"
>
${translateText("win_modal.keep")}
</button>
</div>
</div>
<style>
@keyframes fadeIn {
from {
opacity: 0;
transform: translate(-50%, -48%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-out;
}
</style>
`;
}
innerHtml() {
return this.renderPatternButton();
}
renderPatternButton() {
return html`
<div class="text-center mb-6 bg-black/30 p-2.5 rounded">
<h3 class="text-xl font-semibold text-white mb-3">
${translateText("win_modal.support_openfront")}
</h3>
<p class="text-white mb-3">
${translateText("win_modal.territory_pattern")}
</p>
<div class="flex justify-center">${this.patternContent}</div>
</div>
`;
}
async loadPatternContent() {
const me = await getUserMe();
const patterns = await fetchPatterns(me !== false ? me : null);
const purchasable = Array.from(patterns.values()).filter(
(p) => p.product !== null,
);
if (purchasable.length === 0) {
this.patternContent = html``;
return;
}
const pattern = purchasable[Math.floor(Math.random() * purchasable.length)];
this.patternContent = html`
<pattern-button
.pattern=${pattern}
.onSelect=${(p: Pattern | null) => {}}
.onPurchase=${(priceId: string) => handlePurchase(priceId)}
></pattern-button>
`;
}
steamWishlist(): TemplateResult {
return html`<p class="m-0 mb-5 text-center bg-black/30 p-2.5 rounded">
<a
href="https://store.steampowered.com/app/3560670"
target="_blank"
rel="noopener noreferrer"
class="text-[#4a9eff] underline font-medium transition-colors duration-200 text-2xl hover:text-[#6db3ff]"
>
${translateText("win_modal.wishlist")}
</a>
</p>`;
}
async show() {
await this.loadPatternContent();
this.eventBus.emit(new GutterAdModalEvent(true));
setTimeout(() => {
this.isVisible = true;
this.requestUpdate();
}, 1500);
setTimeout(() => {
this.showButtons = true;
this.requestUpdate();
}, 3000);
}
hide() {
this.eventBus.emit(new GutterAdModalEvent(false));
this.isVisible = false;
this.showButtons = false;
this.requestUpdate();
}
private _handleExit() {
this.hide();
window.location.href = "/";
}
init() {}
tick() {
const myPlayer = this.game.myPlayer();
if (
!this.hasShownDeathModal &&
myPlayer &&
!myPlayer.isAlive() &&
!this.game.inSpawnPhase() &&
myPlayer.hasSpawned()
) {
this.hasShownDeathModal = true;
this._title = translateText("win_modal.died");
this.show();
}
const updates = this.game.updatesSinceLastTick();
const winUpdates = updates !== null ? updates[GameUpdateType.Win] : [];
winUpdates.forEach((wu) => {
if (wu.winner === undefined) {
// ...
} else if (wu.winner[0] === "team") {
this.eventBus.emit(new SendWinnerEvent(wu.winner, wu.allPlayersStats));
if (wu.winner[1] === this.game.myPlayer()?.team()) {
this._title = translateText("win_modal.your_team");
} else {
this._title = translateText("win_modal.other_team", {
team: wu.winner[1],
});
}
this.show();
} else {
const winner = this.game.playerByClientID(wu.winner[1]);
if (!winner?.isPlayer()) return;
const winnerClient = winner.clientID();
if (winnerClient !== null) {
this.eventBus.emit(
new SendWinnerEvent(["player", winnerClient], wu.allPlayersStats),
);
}
if (
winnerClient !== null &&
winnerClient === this.game.myPlayer()?.clientID()
) {
this._title = translateText("win_modal.you_won");
} else {
this._title = translateText("win_modal.other_won", {
player: winner.name(),
});
}
this.show();
}
});
}
renderLayer(/* context: CanvasRenderingContext2D */) {}
shouldTransform(): boolean {
return false;
}
}