# Rolling piggies

> Minipiggies that roll like jelly, made of nothing but springs. You never move them: tilt the world and they roll downhill.

Example `piggy-roll`, running at https://nacatamalon.com/en/examples/piggy-roll. The whole catalogue: https://nacatamalon.com/llms/examples.md.

Each piggy is a ring of twelve balls held to its neighbours and to its middle by `'spring'` joints, with its face stretched over the ring and turning as it rolls. It squashes when it lands and bulges as it goes.

You never move them: ← → tilt the world, which turns the gravity and the camera, and they roll down the hills, slope tiles in the 2D world. Space makes them hop.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';
import { installPhysics2d } from 'nacatamalon/physics2d';
import { WIDTH, HEIGHT } from './settings';
import { Hills } from './scenes/demo';

installPhysics2d();

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    background: '#b8e4ff',
    step: 60
});

// Declare Scenes
game({
    Hills
});
```

`components/hud.ts`:

```ts
import { createText, useScreenSpace } from 'nacatamalon';

export const Hud = () => {
    useScreenSpace();
    createText({
        text: '← → TILT THE WORLD   SPACE HOP',
        transform: {
            x: 8,
            y: 8
        },
        tint: '#9aa4c0'
    });
};
```

`components/piggy.ts`:

```ts
import { createSprite, useLateUpdate, useLoadTexture, usePhysicsBody2d, useSpawn, useTransform } from 'nacatamalon';
import { useJoint2d } from 'nacatamalon/physics2d';
import type { TGameObject, TSprite } from 'nacatamalon';
import * as Settings from '../settings';

const Ball = (x: number, y: number, radius: number) => {
    useTransform({
        x,
        y
    });
    usePhysicsBody2d({
        body: 'dynamic',
        collider: {
            shape: 'circle',
            radius
        },
        density: 1,
        friction: 1.5,
        bounce: 0.1
    });
};

type TPiggy = { balls: TGameObject[]; face: TSprite; };

export const Piggy = (x: number, y: number, face: string): TPiggy => {
    const spawnBall = useSpawn(Ball);
    const middle = spawnBall(x, y, Settings.BALL + 2);
    const balls: TGameObject[] = [];

    for (let i = 0; i < Settings.RING; i++) {
        const angle = (i / Settings.RING) * Math.PI * 2;

        balls.push(spawnBall(x + Math.cos(angle) * Settings.RADIUS, y + Math.sin(angle) * Settings.RADIUS, Settings.BALL));
    }

    // Each ball to the middle, holding the piggy round, and to the next one round the ring, holding
    // its skin together. Soft enough to squash, damped enough not to wobble for ever.
    for (let i = 0; i < Settings.RING; i++) {
        useJoint2d(balls[i], middle, {
            type: 'spring',
            frequency: 4.5,
            damping: 0.4
        });
        useJoint2d(balls[i], balls[(i + 1) % Settings.RING], {
            type: 'spring',
            frequency: 9,
            damping: 0.4
        });
    }

    const sprite = createSprite({
        texture: useLoadTexture({
            src: `/example-assets/assets/piggies/${face}.png`
        }),
        transform: {
            x,
            y
        },
        zIndex: 1
    });

    // The face over the ring: in its middle, turned as far as the ring has rolled, and as wide and
    // tall as the ring is now, measured along the piggy's own turned sides.
    useLateUpdate(() => {
        let sumX = 0;
        let sumY = 0;

        for (const ball of balls) {
            sumX += ball.transform!.x;
            sumY += ball.transform!.y;
        }

        const cx = sumX / Settings.RING;
        const cy = sumY / Settings.RING;

        // How far it has rolled: each ball's angle round the middle against the one it started at,
        // averaged as directions (so a turn past half way does not average out to nothing).
        let turnX = 0;
        let turnY = 0;

        balls.forEach((ball, i) => {
            const angle = Math.atan2(ball.transform!.y - cy, ball.transform!.x - cx) - (i / Settings.RING) * Math.PI * 2;

            turnX += Math.cos(angle);
            turnY += Math.sin(angle);
        });

        const turn = Math.atan2(turnY, turnX);

        // Its size along its own sides: how far the ring reaches either way along each.
        const cos = Math.cos(turn);
        const sin = Math.sin(turn);
        let across = 0;
        let down = 0;

        for (const ball of balls) {
            const dx = ball.transform!.x - cx;
            const dy = ball.transform!.y - cy;

            across = Math.max(across, Math.abs(dx * cos + dy * sin));
            down = Math.max(down, Math.abs(-dx * sin + dy * cos));
        }

        const rest = (Settings.RADIUS + Settings.BALL) * 2;

        sprite.transform.x = cx;
        sprite.transform.y = cy;
        sprite.transform.rotation = turn;
        sprite.transform.scaleX = (across * 2 + Settings.BALL * 2) / rest;
        sprite.transform.scaleY = (down * 2 + Settings.BALL * 2) / rest;
    });

    return {
        balls: [middle, ...balls],
        face: sprite
    };
};
```

`scenes/demo.ts`:

```ts
import { createScene, createSprite, createTilemap, getColor, useCamera2d, useKeyboard, useLateUpdate, usePhysicsWorld2d, useSpawn, useUpdate } from 'nacatamalon';
import { getPhysicsWorld2d } from 'nacatamalon/physics2d';

import * as Settings from '../settings';
import { Piggy } from '../components/piggy';
import { Hud } from '../components/hud';

export const Hills = () => {
    usePhysicsWorld2d();
    createTilemap({
        src: '/example-assets/assets/platformer/hills.tilemap'
    });

    // The hills climb half a tile a column: gentle enough to roll up with the world tilted.
    // Earth past the map's edges, behind it: what a tilted view sees round its corners.
    const earth = getColor('#6e4220');

    const backdrop = (x: number, y: number, width: number, height: number) =>
        createSprite({
            width,
            height,
            tint: earth,
            anchor: {
                x: 0,
                y: 0
            },
            transform: {
                x,
                y
            },
            zIndex: -200
        });

    backdrop(-400, 448, Settings.MAP_W + 800, 400);
    backdrop(-400, 160, 400, 300);
    backdrop(Settings.MAP_W, 160, 400, 300);

    const camera = useCamera2d();
    const keys = useKeyboard();
    const world = getPhysicsWorld2d();

    const spawnPiggy = useSpawn(Piggy);
    const piggies = Settings.FACES.map((face, i) => spawnPiggy(80 + i * 44, 130 - i * 12, face));

    let tilt = 0;
    let hop = 0;

    useUpdate((delta) => {
        // Tilting the world is turning its gravity: the world still pulls straight down, so each ball
        // is pushed by the difference, every frame, for as long as the key is held. Turned far enough,
        // gravity pulls up the gentle hills.
        const asked = (keys.isDown('ArrowRight') ? 1 : 0) - (keys.isDown('ArrowLeft') ? 1 : 0);

        tilt += (asked * 0.7 - tilt) * Math.min(1, delta * 4);
        hop = Math.max(0, hop - delta);

        const jump = keys.justPressed(' ') && hop === 0;

        if (jump) {
            hop = 0.7;
        }

        for (const piggy of piggies.map((spawned) => spawned.api)) {
            for (const ball of piggy.balls) {
                const body = world?.bodyOf(ball);

                if (body === null || body === undefined) {
                    continue;
                }

                const mass = Math.PI * (ball === piggy.balls[0] ? (Settings.BALL + 2) ** 2 : Settings.BALL ** 2);

                body.applyForce(mass * 900 * Math.sin(tilt), mass * 900 * (Math.cos(tilt) - 1));

                if (jump) {
                    body.applyImpulse(0, -mass * 260);
                }
            }
        }
    });

    // The camera follows the piggies' middle, and turns with the tilt about the centre of the screen.
    useLateUpdate(() => {
        const faces = piggies.map((spawned) => spawned.api.face.transform);

        const middle = {
            x: Math.min(Settings.MAP_W - Settings.WIDTH / 2, Math.max(Settings.WIDTH / 2, faces.reduce((sum, at) => sum + at.x, 0) / faces.length)),
            y: Settings.VIEW_Y,
        };

        const angle = -tilt * 0.5;

        camera.transform.rotation = angle;
        // The view turns about its top-left corner: placed so the middle stays in the middle.
        camera.transform.x = middle.x - (Math.cos(angle) * Settings.WIDTH / 2 - Math.sin(angle) * Settings.HEIGHT / 2);
        camera.transform.y = middle.y - (Math.sin(angle) * Settings.WIDTH / 2 + Math.cos(angle) * Settings.HEIGHT / 2);
    });

    useSpawn(Hud)();

    return createScene();
};
```

`settings.ts`:

```ts
// Minipiggies that roll like jelly, with nothing but springs.
//
// Each piggy is a ring of twelve little balls around a middle one, every ball held to its
// neighbours and to the middle by a spring. The ring squashes when it lands, bulges as it rolls and
// springs back; the face is stretched over it to match, and turns as the ring rolls. You never
// move a piggy: you tilt the world with ← →, and they roll downhill. Space makes them hop.

export const WIDTH = 480;
export const HEIGHT = 320;
export const MAP_W = 960;
// The level is taller than the screen: the view sits on its lower part, where the ground is.
export const VIEW_Y = 288;
export const RING = 12;
export const RADIUS = 18;
export const BALL = 4;
export const FACES = ['piggie-01', 'piggie-03', 'piggie-06', 'piggie-08', 'piggie-10'];
```
