Files
OpenFrontIO/src/client/graphics/GameRenderer.ts
T
evanpelle e58bc89b67 add spawn ads (#1228)
## Description:

Adds a bottom rail add during the spawn phase if player has played over
5 games.
Also only show the death screen ad if player has played a couple of
games.

This keeps the experience ad-free for the first few games.
<img width="1003" alt="74fb6676-273d-4b58-9fcb-50ec438c4e27"
src="https://github.com/user-attachments/assets/fedca20f-7b31-4a06-be57-bde5bd0118f0"
/>

## 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
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

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

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-06-19 10:36:46 -07:00

355 lines
10 KiB
TypeScript

import { EventBus } from "../../core/EventBus";
import { GameView } from "../../core/game/GameView";
import { GameStartingModal } from "../GameStartingModal";
import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler";
import { TransformHandler } from "./TransformHandler";
import { UIState } from "./UIState";
import { BuildMenu } from "./layers/BuildMenu";
import { ChatDisplay } from "./layers/ChatDisplay";
import { ChatModal } from "./layers/ChatModal";
import { ControlPanel } from "./layers/ControlPanel";
import { EmojiTable } from "./layers/EmojiTable";
import { EventsDisplay } from "./layers/EventsDisplay";
import { FxLayer } from "./layers/FxLayer";
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
import { GutterAdModal } from "./layers/GutterAdModal";
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
import { Layer } from "./layers/Layer";
import { Leaderboard } from "./layers/Leaderboard";
import { MainRadialMenu } from "./layers/MainRadialMenu";
import { MultiTabModal } from "./layers/MultiTabModal";
import { NameLayer } from "./layers/NameLayer";
import { OptionsMenu } from "./layers/OptionsMenu";
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
import { PlayerPanel } from "./layers/PlayerPanel";
import { ReplayPanel } from "./layers/ReplayPanel";
import { SpawnAd } from "./layers/SpawnAd";
import { SpawnTimer } from "./layers/SpawnTimer";
import { StructureLayer } from "./layers/StructureLayer";
import { TeamStats } from "./layers/TeamStats";
import { TerrainLayer } from "./layers/TerrainLayer";
import { TerritoryLayer } from "./layers/TerritoryLayer";
import { TopBar } from "./layers/TopBar";
import { UILayer } from "./layers/UILayer";
import { UnitInfoModal } from "./layers/UnitInfoModal";
import { UnitLayer } from "./layers/UnitLayer";
import { WinModal } from "./layers/WinModal";
export function createRenderer(
canvas: HTMLCanvasElement,
game: GameView,
eventBus: EventBus,
): GameRenderer {
const transformHandler = new TransformHandler(game, eventBus, canvas);
const uiState = { attackRatio: 20 };
//hide when the game renders
const startingModal = document.querySelector(
"game-starting-modal",
) as GameStartingModal;
startingModal.hide();
// TODO maybe append this to dcoument instead of querying for them?
const emojiTable = document.querySelector("emoji-table") as EmojiTable;
if (!emojiTable || !(emojiTable instanceof EmojiTable)) {
console.error("EmojiTable element not found in the DOM");
}
emojiTable.eventBus = eventBus;
emojiTable.transformHandler = transformHandler;
emojiTable.game = game;
emojiTable.initEventBus();
const buildMenu = document.querySelector("build-menu") as BuildMenu;
if (!buildMenu || !(buildMenu instanceof BuildMenu)) {
console.error("BuildMenu element not found in the DOM");
}
buildMenu.game = game;
buildMenu.eventBus = eventBus;
const leaderboard = document.querySelector("leader-board") as Leaderboard;
if (!emojiTable || !(leaderboard instanceof Leaderboard)) {
console.error("EmojiTable element not found in the DOM");
}
leaderboard.eventBus = eventBus;
leaderboard.game = game;
const gameLeftSidebar = document.querySelector(
"game-left-sidebar",
) as GameLeftSidebar;
if (!gameLeftSidebar || !(gameLeftSidebar instanceof GameLeftSidebar)) {
console.error("GameLeftSidebar element not found in the DOM");
}
gameLeftSidebar.game = game;
const teamStats = document.querySelector("team-stats") as TeamStats;
if (!emojiTable || !(teamStats instanceof TeamStats)) {
console.error("EmojiTable element not found in the DOM");
}
teamStats.eventBus = eventBus;
teamStats.game = game;
const controlPanel = document.querySelector("control-panel") as ControlPanel;
if (!(controlPanel instanceof ControlPanel)) {
console.error("ControlPanel element not found in the DOM");
}
controlPanel.eventBus = eventBus;
controlPanel.uiState = uiState;
controlPanel.game = game;
const eventsDisplay = document.querySelector(
"events-display",
) as EventsDisplay;
if (!(eventsDisplay instanceof EventsDisplay)) {
console.error("events display not found");
}
eventsDisplay.eventBus = eventBus;
eventsDisplay.game = game;
const chatDisplay = document.querySelector("chat-display") as ChatDisplay;
if (!(chatDisplay instanceof ChatDisplay)) {
console.error("chat display not found");
}
chatDisplay.eventBus = eventBus;
chatDisplay.game = game;
const playerInfo = document.querySelector(
"player-info-overlay",
) as PlayerInfoOverlay;
if (!(playerInfo instanceof PlayerInfoOverlay)) {
console.error("player info overlay not found");
}
playerInfo.eventBus = eventBus;
playerInfo.transform = transformHandler;
playerInfo.game = game;
const winModel = document.querySelector("win-modal") as WinModal;
if (!(winModel instanceof WinModal)) {
console.error("win modal not found");
}
winModel.eventBus = eventBus;
winModel.game = game;
const optionsMenu = document.querySelector("options-menu") as OptionsMenu;
if (!(optionsMenu instanceof OptionsMenu)) {
console.error("options menu not found");
}
optionsMenu.eventBus = eventBus;
optionsMenu.game = game;
const replayPanel = document.querySelector("replay-panel") as ReplayPanel;
if (!(replayPanel instanceof ReplayPanel)) {
console.error("ReplayPanel element not found in the DOM");
}
replayPanel.eventBus = eventBus;
replayPanel.game = game;
const topBar = document.querySelector("top-bar") as TopBar;
if (!(topBar instanceof TopBar)) {
console.error("top bar not found");
}
topBar.game = game;
const playerPanel = document.querySelector("player-panel") as PlayerPanel;
if (!(playerPanel instanceof PlayerPanel)) {
console.error("player panel not found");
}
playerPanel.g = game;
playerPanel.eventBus = eventBus;
playerPanel.emojiTable = emojiTable;
playerPanel.uiState = uiState;
const chatModal = document.querySelector("chat-modal") as ChatModal;
if (!(chatModal instanceof ChatModal)) {
console.error("chat modal not found");
}
chatModal.g = game;
chatModal.eventBus = eventBus;
const multiTabModal = document.querySelector(
"multi-tab-modal",
) as MultiTabModal;
if (!(multiTabModal instanceof MultiTabModal)) {
console.error("multi-tab modal not found");
}
multiTabModal.game = game;
const headsUpMessage = document.querySelector(
"heads-up-message",
) as HeadsUpMessage;
if (!(headsUpMessage instanceof HeadsUpMessage)) {
console.error("heads-up message not found");
}
headsUpMessage.game = game;
const unitInfoModal = document.querySelector(
"unit-info-modal",
) as UnitInfoModal;
if (!(unitInfoModal instanceof UnitInfoModal)) {
console.error("unit info modal not found");
}
unitInfoModal.game = game;
const structureLayer = new StructureLayer(
game,
eventBus,
transformHandler,
unitInfoModal,
);
unitInfoModal.structureLayer = structureLayer;
// unitInfoModal.eventBus = eventBus;
const spawnAd = document.querySelector("spawn-ad") as SpawnAd;
if (!(spawnAd instanceof SpawnAd)) {
console.error("spawn ad not found");
}
spawnAd.g = game;
const gutterAdModal = document.querySelector(
"gutter-ad-modal",
) as GutterAdModal;
if (!(gutterAdModal instanceof GutterAdModal)) {
console.error("gutter ad modal not found");
}
gutterAdModal.eventBus = eventBus;
const layers: Layer[] = [
new TerrainLayer(game, transformHandler),
new TerritoryLayer(game, eventBus, transformHandler),
structureLayer,
new UnitLayer(game, eventBus, transformHandler),
new FxLayer(game),
new UILayer(game, eventBus, transformHandler),
new NameLayer(game, transformHandler),
eventsDisplay,
chatDisplay,
buildMenu,
new MainRadialMenu(
eventBus,
game,
transformHandler,
emojiTable as EmojiTable,
buildMenu,
uiState,
playerInfo,
playerPanel,
),
new SpawnTimer(game, transformHandler),
leaderboard,
gameLeftSidebar,
controlPanel,
playerInfo,
winModel,
optionsMenu,
replayPanel,
teamStats,
topBar,
playerPanel,
headsUpMessage,
unitInfoModal,
multiTabModal,
spawnAd,
gutterAdModal,
];
return new GameRenderer(
game,
eventBus,
canvas,
transformHandler,
uiState,
layers,
);
}
export class GameRenderer {
private context: CanvasRenderingContext2D;
constructor(
private game: GameView,
private eventBus: EventBus,
private canvas: HTMLCanvasElement,
public transformHandler: TransformHandler,
public uiState: UIState,
private layers: Layer[],
) {
const context = canvas.getContext("2d");
if (context === null) throw new Error("2d context not supported");
this.context = context;
}
initialize() {
this.eventBus.on(RedrawGraphicsEvent, (e) => {
this.layers.forEach((l) => {
if (l.redraw) {
l.redraw();
}
});
});
this.layers.forEach((l) => l.init?.());
document.body.appendChild(this.canvas);
window.addEventListener("resize", () => this.resizeCanvas());
this.resizeCanvas();
//show whole map on startup
this.transformHandler.centerAll(0.9);
requestAnimationFrame(() => this.renderGame());
}
resizeCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
//this.redraw()
}
renderGame() {
const start = performance.now();
// Set background
this.context.fillStyle = this.game
.config()
.theme()
.backgroundColor()
.toHex();
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Save the current context state
this.context.save();
this.transformHandler.handleTransform(this.context);
this.layers.forEach((l) => {
if (l.shouldTransform?.()) {
l.renderLayer?.(this.context);
}
});
this.context.restore();
this.layers.forEach((l) => {
if (!l.shouldTransform?.()) {
l.renderLayer?.(this.context);
}
});
requestAnimationFrame(() => this.renderGame());
const duration = performance.now() - start;
if (duration > 50) {
console.warn(
`tick ${this.game.ticks()} took ${duration}ms to render frame`,
);
}
}
tick() {
this.layers.forEach((l) => l.tick?.());
}
resize(width: number, height: number): void {
this.canvas.width = Math.ceil(width / window.devicePixelRatio);
this.canvas.height = Math.ceil(height / window.devicePixelRatio);
}
}