AlexAegis/advent-of-code

View on GitHub
solutions/typescript/libs/ecs/src/renderer/renderer.system.ts

Summary

Maintainability
A
1 hr
Test Coverage
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { renderMatrix, Vec2 } from '@alexaegis/advent-of-code-lib';
import { AsciiDisplayComponent } from '../components/prebuilt/ascii-display.component.js';
import { CameraComponent } from '../components/prebuilt/camera.component.js';
import {
    PositionComponent,
    StaticPositionComponent,
} from '../components/prebuilt/position.component.js';
import type { Entity } from '../entity/entity.class.js';
import type { Initializable } from '../system/initializable.interface.js';
import { System } from '../system/system.type.js';
import type { GridWorld } from '../world/grid-world.class.js';
import type { IOBackend } from './backend/io-backend.interface.js';
import { Sprite } from './sprite.class.js';

export interface RendererSystemOptions {
    cameraEntity: Entity;
    backend: IOBackend;
    renderColliders?: boolean;
}

export type NormalizedRendererSystemOptions = Required<RendererSystemOptions>;

const normalizeRendererSystemOptions = (
    options: RendererSystemOptions,
): NormalizedRendererSystemOptions => {
    return {
        renderColliders: false,
        ...options,
    };
};

export class RendererSystem extends System implements Initializable {
    order = Number.POSITIVE_INFINITY;
    camera: CameraComponent | undefined;
    currentFrame?: Sprite;

    options: NormalizedRendererSystemOptions;

    constructor(options: RendererSystemOptions) {
        super();
        this.options = normalizeRendererSystemOptions(options);
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        this.camera = this.options.cameraEntity.getComponent(CameraComponent)!;
    }

    async init(): Promise<void> {
        await this.options.backend.init((size) => {
            this.camera?.resize(size);
        });
    }

    tick(world: GridWorld): boolean {
        this.currentFrame = this.render(world);
        this.options.backend.pushFrame(this.currentFrame);
        return false;
    }

    render(world: GridWorld): Sprite {
        const frame: Sprite = new Sprite();
        if (!this.camera) {
            return frame;
        }
        frame.blank(this.camera.screenViewport);

        for (const [, positionComponent, displayComponent] of [
            ...world.query(StaticPositionComponent, AsciiDisplayComponent),
            ...world.query(PositionComponent, AsciiDisplayComponent),
        ].sort((a, b) => a[1].z - b[1].z)) {
            const screenPosition = this.camera.getScreenPositionFromWorldPosition(
                positionComponent.position,
            );
            const entityScreenBox = displayComponent.sprite.boundingBox
                .clone()
                .moveAnchorTo(screenPosition);

            const screenIntersection = this.camera.screenViewport.intersection(entityScreenBox);
            screenIntersection?.forEach((screenX, screenY) => {
                const worldX = this.camera!.worldAnchor.x + screenX;
                const worldY = this.camera!.worldAnchor.y + screenY;
                const localX = worldX - positionComponent.position.x;
                const localY = worldY - positionComponent.position.y;
                const tile = displayComponent.sprite.getTileAt(localX, localY);
                if (tile) {
                    frame.merge(screenX, screenY, tile);
                }
            });
        }

        if (this.options.renderColliders) {
            this.renderCollidersOntoSprite(frame);
        }

        return frame;
    }

    private renderCollidersOntoSprite(frame: Sprite): void {
        this.camera?.screenViewport.forEach((x, y) => {
            const sp = new Vec2(x, y);
            const wp = this.camera?.getWorldPositionFromScreenPosition(sp) ?? Vec2.ORIGIN;
            const collidingEntities = this.camera?.world.entitiesCollidingAt(wp).length;
            if (collidingEntities !== undefined) {
                frame.merge(x, y, collidingEntities.toString());
            }
        });
    }

    printCurrentFrame(): void {
        if (this.currentFrame) {
            const render = renderMatrix(this.currentFrame.asStringMatrix());
            console.log(render);
        }
    }
}