Kidnapped Castle Breakout

A browser-based point-and-click escape game called Kidnapped Castle Breakout, built in TypeScript as part of a school project at HBO-ICT. The game uses a component-based architecture with a Web Components frontend and an Express.js backend communicating over HTTPS. Players explore rooms within a castle, interact with objects, solve puzzles, and manage an inventory to ultimately escape. The team worked within a provided game engine framework and extended it with custom systems — I focused on building the interactive hitbox system, a flashlight mechanic, and minigames (like a button-mashing vomit minigame). I also took on the Scrum Master role, organizing sprint planning, stand-ups, and retrospectives to keep the team on track across three sprints.

technologies

TypeScript
HTML
CSS
Node.js
codeSnippet

Responsive HitBox System (HitBox.ts) This was one of my main contributions. The HitBox class dynamically creates invisible clickable regions over game objects on the canvas. Each hitbox scales and repositions itself responsively based on the rendered image size, so the game works across different screen sizes and orientations. The z-index is calculated from the object's Y-position to create a natural depth ordering. Teammates only needed to set a _position, _size, and _action on their GameObject subclass — the hitbox system handled the rest.

1/** Updates hitbox position and size based on current viewport and image scaling */
2private updateHitboxPosition(): void {
3    if (!this._headerElement) return;
4
5    const headerImage: HTMLImageElement | null = this._headerElement.querySelector("img");
6    if (!headerImage) return;
7
8    const originalImageWidth: number = 1022;
9    const displayedWidth: number = headerImage.getBoundingClientRect().width;
10    const scale: number = displayedWidth / originalImageWidth;
11
12    const scaledWidth: number = this._originalSize.x * scale;
13    const scaledHeight: number = this._originalSize.y * scale;
14    const scaledPosX: number = this._originalPosition.x * scale;
15    const scaledPosY: number = this._originalPosition.y * scale;
16
17    const imageRect: DOMRect = headerImage.getBoundingClientRect();
18    const headerRect: DOMRect = this._headerElement.getBoundingClientRect();
19    const imageLeftInHeader: number = imageRect.left - headerRect.left;
20
21    this._hitboxDiv.style.width = `${scaledWidth}px`;
22    this._hitboxDiv.style.height = `${scaledHeight}px`;
23    this._hitboxDiv.style.left = `${imageLeftInHeader + (displayedWidth / 2) + scaledPosX}px`;
24    this._hitboxDiv.style.top = `${scaledPosY}px`;
25}

Simple GameObject Declaration (SafeItem.ts) This shows how easy it was for teammates to create new interactable objects thanks to the hitbox system. They just had to define position, size, an action type, and implement the relevant action interfaces (like Examine or Open). The HitBox class picks up these values automatically when the room renders.

1export class SafeItem extends Item implements Examine, Open {
2    public static readonly Alias: string = "Safe";
3
4    public _position: Vector2 = { x: 30, y: 105 };
5    public _size: Vector2 = { x: 70, y: 70 };
6    public _action: ActionTypes = ActionTypes.Examine;
7    public _isDebugHitboxVisible: boolean = false;
8    public static readonly validActions: string[] = ["open"];
9    // ...
10}

After the project, i built a full-screen CRT monitor effect using WebGL shaders, giving the game a retro horror atmosphere. It renders scanlines, screen curvature, vignette darkening, noise grain, flicker, and RGB chromatic aberration — all running in real-time as a transparent overlay on top of the game.

1// Fragment shader excerpt — combining all CRT post-processing effects
2float scan = scanline(curvedUV);
3float vig = vignette(curvedUV);
4float vignetteEffect = mix(1.0, vig, VIGNETTE_OPACITY);
5float noiseEffect = noise(curvedUV) * NOISE_INTENSITY;
6float flicker = 1.0 + FLICKER_AMOUNT * sin(u_time * FLICKER_SPEED);
7vec3 rgbEffect = rgbShift(curvedUV);
8
9float darkness = scan + noiseEffect;
10darkness *= flicker;
11darkness += (1.0 - vignetteEffect) * 0.5;
12
13vec3 color = vec3(darkness) + rgbEffect;
14float alpha = darkness * 0.4 + (1.0 - vignetteEffect) * 0.3;
15gl_FragColor = vec4(color, alpha);
CLICK ME!