# Jelly piggies

> Minipiggies as soft bodies: rings of springs that squash when they land and bulge as they roll, each face stretched over its own corners. Tilt the world and they roll downhill.

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

`useSoftBody2d` makes each piggy a soft body in the 2D world: a ring of sixteen corners held to each other, to the area they enclose and to the shape they started as. It falls, squashes when it lands, rolls down the hills of the map and springs back.

Its face is a `createPolygon` with the same sixteen corners, handed to the body as `polygon`: every frame the body moves them, and the picture squashes and turns with it.

You never move a piggy: ← → tilt the world, which pushes each one (`push`) and turns the camera, and Space makes them hop (`kick`).

## 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 { createPolygon, useLoadTexture, useTransform } from 'nacatamalon';
import { useSoftBody2d } from 'nacatamalon/physics2d';

import * as Settings from '../settings';

const circle = (radius: number, count: number) =>
    Array.from({
        length: count
    }, (_, i) => ({
        x: Math.cos((i / count) * Math.PI * 2) * radius,
        y: Math.sin((i / count) * Math.PI * 2) * radius,
    }));

export const Piggy = (x: number, y: number, face: string) => {
    useTransform({
        x,
        y
    });

    const texture = useLoadTexture({
        src: '/example-assets/assets/piggies/' + face + '.png'
    });

    const polygon = createPolygon({
        texture,
        points: circle(Settings.RADIUS, Settings.CORNERS),
        zIndex: 1
    });

    // Firm enough to roll, soft enough to squash when it lands.
    return useSoftBody2d({
        shape: 'disk',
        radius: Settings.RADIUS,
        corners: Settings.CORNERS,
        softness: 4,
        damping: 0.6,
        friction: 1,
        polygon
    });
};
```

`scenes/demo.ts`:

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

import type { TGameObject } from 'nacatamalon';
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'
    });

    // 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 spawnPiggy = useSpawn(Piggy);
    const piggies: TGameObject<ReturnType<typeof Piggy>>[] = Settings.FACES.map((face, i) => spawnPiggy(80 + i * 52, 120 - 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 piggy
        // is pushed by the difference, every frame, for as long as the key is held.
        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) {
            piggy.api.push(900 * Math.sin(tilt), 900 * (Math.cos(tilt) - 1));

            if (jump) {
                piggy.api.kick(0, -260);
            }
        }
    });

    // The camera follows the piggies' middle, and turns with the tilt about the centre of the screen.
    useLateUpdate(() => {
        const middle = {
            x: Math.min(Settings.MAP_W - Settings.WIDTH / 2, Math.max(Settings.WIDTH / 2, piggies.reduce((sum, piggy) => sum + piggy.transform!.x, 0) / piggies.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 made of jelly: soft bodies.
//
// Each piggy is a useSoftBody2d disk, sixteen corners held to each other and to the area they
// enclose by springs, and its face is a createPolygon with the same sixteen corners: every frame the
// body moves them, and the face squashes, bulges and turns with it. 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 RADIUS = 22;
export const CORNERS = 16;
export const FACES = ['piggie-01', 'piggie-03', 'piggie-06', 'piggie-08', 'piggie-10'];
```
