[game] Big actor refactor, spawn actors from world
This commit is contained in:
parent
4042cd28a4
commit
3af0bb4002
4 changed files with 281 additions and 107 deletions
100
game/actor.ts
100
game/actor.ts
|
|
@ -3,8 +3,7 @@ import { btn, Button } from "./input";
|
||||||
import { Vec2, new_v2, vadd, vnorm, vmul } from "./vector";
|
import { Vec2, new_v2, vadd, vnorm, vmul } from "./vector";
|
||||||
import { spr, use_texture, Texture } from "./graphics";
|
import { spr, use_texture, Texture } from "./graphics";
|
||||||
|
|
||||||
interface ActorSnapshot {
|
export interface ActorProps {
|
||||||
__type__: string;
|
|
||||||
velocity: Vec2;
|
velocity: Vec2;
|
||||||
friction: number;
|
friction: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -12,23 +11,33 @@ interface ActorSnapshot {
|
||||||
bounds: Vec2;
|
bounds: Vec2;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Actor {
|
export function new_actor_props(
|
||||||
velocity: Vec2 = new_v2(0);
|
id: string,
|
||||||
friction: number = 0.6;
|
position: Vec2,
|
||||||
id: string;
|
bounds: Vec2
|
||||||
position: Vec2;
|
): ActorProps {
|
||||||
bounds: Vec2;
|
return {
|
||||||
|
velocity: new_v2(0),
|
||||||
|
friction: 0.6,
|
||||||
|
id,
|
||||||
|
position,
|
||||||
|
bounds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
constructor(id: string, position: Vec2, bounds: Vec2) {
|
export class Actor {
|
||||||
this.id = id;
|
type: ActorType;
|
||||||
this.position = position;
|
props: ActorProps;
|
||||||
this.bounds = bounds;
|
|
||||||
|
constructor(type: ActorType, props: ActorProps) {
|
||||||
|
this.type = type;
|
||||||
|
this.props = props;
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {}
|
update() {}
|
||||||
|
|
||||||
update_physics() {
|
update_physics() {
|
||||||
const velocity = vmul(this.velocity, this.friction);
|
const velocity = vmul(this.props.velocity, this.props.friction);
|
||||||
// Zero if we're smaller than some epsilon.
|
// Zero if we're smaller than some epsilon.
|
||||||
if (Math.abs(velocity.x) < 0.01) {
|
if (Math.abs(velocity.x) < 0.01) {
|
||||||
velocity.x = 0;
|
velocity.x = 0;
|
||||||
|
|
@ -37,36 +46,22 @@ export class Actor {
|
||||||
velocity.y = 0;
|
velocity.y = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const new_position = vadd(this.position, velocity);
|
const new_position = vadd(this.props.position, velocity);
|
||||||
|
|
||||||
// TODO: Collision detection
|
// TODO: Collision detection
|
||||||
// const { w, h } = this.bounds;
|
// const { w, h } = this.bounds;
|
||||||
|
|
||||||
this.velocity = velocity;
|
this.props.velocity = velocity;
|
||||||
this.position = new_position;
|
this.props.position = new_position;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(_clock: number) {}
|
draw(_clock: number) {}
|
||||||
|
|
||||||
bonk(_other: Actor) {}
|
bonk(_other: Actor) {}
|
||||||
|
|
||||||
snapshot(): ActorSnapshot {
|
|
||||||
return { ...this, __type__: "??" };
|
|
||||||
}
|
|
||||||
|
|
||||||
assign_snapshot(s: ActorSnapshot) {
|
|
||||||
this.id = s.id;
|
|
||||||
this.position = s.position;
|
|
||||||
this.bounds = s.bounds;
|
|
||||||
this.velocity = s.velocity;
|
|
||||||
this.friction = s.friction;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const robo_info = {
|
const robo_info = {
|
||||||
width: 32,
|
bounds: new_v2(32),
|
||||||
height: 32,
|
|
||||||
sprite: "./bot.png",
|
sprite: "./bot.png",
|
||||||
animations: [
|
animations: [
|
||||||
{ start: 0, length: 1, speed: 20 },
|
{ start: 0, length: 1, speed: 20 },
|
||||||
|
|
@ -74,15 +69,11 @@ const robo_info = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface RoboSnapshot extends ActorSnapshot {
|
|
||||||
__type__: "robo";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Robo extends Actor {
|
export class Robo extends Actor {
|
||||||
bot_sprite: Texture | undefined = undefined;
|
bot_sprite: Texture | undefined = undefined;
|
||||||
|
|
||||||
constructor(pos: Vec2) {
|
constructor(props: ActorProps) {
|
||||||
super("robo", pos, new_v2(robo_info.width, robo_info.height));
|
super("robo", props);
|
||||||
load_texture(robo_info.sprite).then((texture) => {
|
load_texture(robo_info.sprite).then((texture) => {
|
||||||
this.bot_sprite = texture;
|
this.bot_sprite = texture;
|
||||||
});
|
});
|
||||||
|
|
@ -104,43 +95,36 @@ export class Robo extends Actor {
|
||||||
a.x += 1;
|
a.x += 1;
|
||||||
}
|
}
|
||||||
vnorm(a);
|
vnorm(a);
|
||||||
this.velocity = vadd(this.velocity, vmul(a, 1.5));
|
this.props.velocity = vadd(this.props.velocity, vmul(a, 1.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(clock: number) {
|
draw(clock: number) {
|
||||||
if (this.bot_sprite != undefined) {
|
if (this.bot_sprite != undefined) {
|
||||||
use_texture(this.bot_sprite);
|
use_texture(this.bot_sprite);
|
||||||
|
|
||||||
const vel = this.velocity;
|
const vel = this.props.velocity;
|
||||||
const moving = vel.x != 0 || vel.y != 0;
|
const moving = vel.x != 0 || vel.y != 0;
|
||||||
const anim = robo_info.animations[moving ? 1 : 0];
|
const anim = robo_info.animations[moving ? 1 : 0];
|
||||||
|
|
||||||
const { x: w, y: h } = this.bounds;
|
const { x: w, y: h } = this.props.bounds;
|
||||||
const { x, y } = this.position;
|
const { x, y } = this.props.position;
|
||||||
|
|
||||||
const frame = (anim.start + ((clock / anim.speed) % anim.length)) >> 0;
|
const frame = (anim.start + ((clock / anim.speed) % anim.length)) >> 0;
|
||||||
spr(x, y, w, h, frame * w, 0, 32, 32);
|
spr(x, y, w, h, frame * w, 0, 32, 32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot(): RoboSnapshot {
|
|
||||||
return { ...super.snapshot(), __type__: "robo" };
|
|
||||||
}
|
|
||||||
|
|
||||||
assign_snapshot(s: RoboSnapshot) {
|
|
||||||
super.assign_snapshot(s);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SNAPSHOT_TABLE: { [key: string]: (s: any) => Actor } = {
|
const ACTOR_TABLE = {
|
||||||
robo: (s: any) => new Robo(new_v2(0)).assign_snapshot(s),
|
robo: (s: ActorProps) => new Robo(s),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function actor_from_snapshot(s: ActorSnapshot): Actor {
|
export type ActorType = keyof typeof ACTOR_TABLE;
|
||||||
const f = SNAPSHOT_TABLE[s.__type__];
|
|
||||||
if (f == undefined) {
|
export function is_actor_type(type: string): type is ActorType {
|
||||||
throw new Error("No handler for " + s.__type__);
|
return ACTOR_TABLE.hasOwnProperty(type);
|
||||||
}
|
}
|
||||||
return f(s);
|
|
||||||
|
export function spawn_actor(type: ActorType, props: ActorProps): Actor {
|
||||||
|
return ACTOR_TABLE[type](props);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
128
game/level.ts
128
game/level.ts
|
|
@ -23,14 +23,26 @@ export type TileLayer = {
|
||||||
tiles: Tile[];
|
tiles: Tile[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Layer = TileLayer;
|
export type Entity = {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
px: [number, number];
|
||||||
|
bounds: [number, number];
|
||||||
|
// TODO: More props here.
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EntityLayer = {
|
||||||
|
type: "entity";
|
||||||
|
entities: Entity[];
|
||||||
|
};
|
||||||
|
|
||||||
export type Level = {
|
export type Level = {
|
||||||
world_x: number;
|
world_x: number;
|
||||||
world_y: number;
|
world_y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
layers: Layer[];
|
tile_layers: TileLayer[];
|
||||||
|
entity_layers: EntityLayer[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TileSet = { id: number; texture: Texture };
|
export type TileSet = { id: number; texture: Texture };
|
||||||
|
|
@ -52,6 +64,74 @@ async function load_tileset(def: {
|
||||||
return { id: def.uid, texture };
|
return { id: def.uid, texture };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TileLayerInstance = {
|
||||||
|
__type: "Tiles";
|
||||||
|
__gridSize: number;
|
||||||
|
__pxTotalOffsetX: number;
|
||||||
|
__pxTotalOffsetY: number;
|
||||||
|
__tilesetDefUid: number;
|
||||||
|
gridTiles: {
|
||||||
|
px: [number, number];
|
||||||
|
src: [number, number];
|
||||||
|
f: number;
|
||||||
|
t: number;
|
||||||
|
a: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function load_tile_layer_instance(
|
||||||
|
tile_sets: Map<number, TileSet>,
|
||||||
|
li: TileLayerInstance
|
||||||
|
): TileLayer {
|
||||||
|
const tileset = tile_sets.get(li.__tilesetDefUid);
|
||||||
|
if (!tileset) {
|
||||||
|
throw new Error("Unable to find texture!!! " + li.__tilesetDefUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "tile",
|
||||||
|
texture: tileset.texture,
|
||||||
|
grid_size: li.__gridSize,
|
||||||
|
offset: [li.__pxTotalOffsetX, li.__pxTotalOffsetY],
|
||||||
|
tiles: li.gridTiles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type EntityLayerInstance = {
|
||||||
|
__type: "Entities";
|
||||||
|
entityInstances: {
|
||||||
|
__identifier: string;
|
||||||
|
iid: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
px: [number, number];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function load_entity_layer_instance(li: EntityLayerInstance): EntityLayer {
|
||||||
|
return {
|
||||||
|
type: "entity",
|
||||||
|
entities: li.entityInstances.map((ei) => {
|
||||||
|
return {
|
||||||
|
type: ei.__identifier,
|
||||||
|
id: ei.iid,
|
||||||
|
px: ei.px,
|
||||||
|
bounds: [ei.width, ei.height],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayerInstance = TileLayerInstance | EntityLayerInstance;
|
||||||
|
|
||||||
|
function is_tile_layer_instance(x: LayerInstance): x is TileLayerInstance {
|
||||||
|
return x.__type == "Tiles";
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_entity_layer_instance(x: LayerInstance): x is EntityLayerInstance {
|
||||||
|
return x.__type == "Entities";
|
||||||
|
}
|
||||||
|
|
||||||
function load_level(
|
function load_level(
|
||||||
tile_sets: Map<number, TileSet>,
|
tile_sets: Map<number, TileSet>,
|
||||||
def: {
|
def: {
|
||||||
|
|
@ -59,41 +139,27 @@ function load_level(
|
||||||
worldY: number;
|
worldY: number;
|
||||||
pxWid: number;
|
pxWid: number;
|
||||||
pxHei: number;
|
pxHei: number;
|
||||||
layerInstances: {
|
layerInstances: LayerInstance[];
|
||||||
__gridSize: number;
|
|
||||||
__pxTotalOffsetX: number;
|
|
||||||
__pxTotalOffsetY: number;
|
|
||||||
__tilesetDefUid: number;
|
|
||||||
gridTiles: {
|
|
||||||
px: [number, number];
|
|
||||||
src: [number, number];
|
|
||||||
f: number;
|
|
||||||
t: number;
|
|
||||||
a: number;
|
|
||||||
}[];
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
): Level {
|
): Level {
|
||||||
return {
|
const result: Level = {
|
||||||
world_x: def.worldX,
|
world_x: def.worldX,
|
||||||
world_y: def.worldY,
|
world_y: def.worldY,
|
||||||
width: def.pxWid,
|
width: def.pxWid,
|
||||||
height: def.pxHei,
|
height: def.pxHei,
|
||||||
layers: def.layerInstances.map((li) => {
|
tile_layers: [],
|
||||||
const tileset = tile_sets.get(li.__tilesetDefUid);
|
entity_layers: [],
|
||||||
if (!tileset) {
|
|
||||||
throw new Error("Unable to find texture!!! " + li.__tilesetDefUid);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "tile",
|
|
||||||
texture: tileset.texture,
|
|
||||||
grid_size: li.__gridSize,
|
|
||||||
offset: [li.__pxTotalOffsetX, li.__pxTotalOffsetY],
|
|
||||||
tiles: li.gridTiles,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const li of def.layerInstances) {
|
||||||
|
if (is_tile_layer_instance(li)) {
|
||||||
|
result.tile_layers.push(load_tile_layer_instance(tile_sets, li));
|
||||||
|
} else if (is_entity_layer_instance(li)) {
|
||||||
|
result.entity_layers.push(load_entity_layer_instance(li));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function load_world(path: string): Promise<World> {
|
export async function load_world(path: string): Promise<World> {
|
||||||
|
|
@ -120,7 +186,7 @@ export function draw_level(
|
||||||
offset_x: number = 0,
|
offset_x: number = 0,
|
||||||
offset_y: number = 0
|
offset_y: number = 0
|
||||||
) {
|
) {
|
||||||
for (const layer of level.layers) {
|
for (const layer of level.tile_layers) {
|
||||||
use_texture(layer.texture);
|
use_texture(layer.texture);
|
||||||
|
|
||||||
let [ofx, ofy] = layer.offset;
|
let [ofx, ofy] = layer.offset;
|
||||||
|
|
|
||||||
34
game/main.ts
34
game/main.ts
|
|
@ -2,13 +2,21 @@ import { cls, print } from "./graphics";
|
||||||
import { since_start } from "./time";
|
import { since_start } from "./time";
|
||||||
import { new_v2 } from "./vector";
|
import { new_v2 } from "./vector";
|
||||||
import { load_world, World, Level, draw_level } from "./level";
|
import { load_world, World, Level, draw_level } from "./level";
|
||||||
import { Actor, Robo, ActorSnapshot, actor_from_snapshot } from "./actor";
|
import {
|
||||||
|
Actor,
|
||||||
|
ActorProps,
|
||||||
|
ActorType,
|
||||||
|
new_actor_props,
|
||||||
|
is_actor_type,
|
||||||
|
spawn_actor,
|
||||||
|
} from "./actor";
|
||||||
|
|
||||||
/// A nice looping frame counter.
|
/// A nice looping frame counter.
|
||||||
let clock = 0;
|
let clock = 0;
|
||||||
|
|
||||||
let world: World | undefined = undefined;
|
let world: World | undefined = undefined;
|
||||||
let level: Level | undefined = undefined;
|
let level: Level | undefined = undefined;
|
||||||
|
let actors: Actor[] = [];
|
||||||
|
|
||||||
// Note zelda overworld is 16x8 screens
|
// Note zelda overworld is 16x8 screens
|
||||||
// zelda screen is 16x11 tiles
|
// zelda screen is 16x11 tiles
|
||||||
|
|
@ -27,6 +35,19 @@ function load_assets() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: SPAWN ACTORS BASED ON LEVEL.
|
// TODO: SPAWN ACTORS BASED ON LEVEL.
|
||||||
|
actors.length = 0;
|
||||||
|
for (const entity_layer of level.entity_layers) {
|
||||||
|
for (const entity of entity_layer.entities) {
|
||||||
|
if (is_actor_type(entity.type)) {
|
||||||
|
const [x, y] = entity.px;
|
||||||
|
const [w, h] = entity.bounds;
|
||||||
|
const props = new_actor_props(entity.id, new_v2(x, y), new_v2(w, h));
|
||||||
|
actors.push(spawn_actor(entity.type, props));
|
||||||
|
} else {
|
||||||
|
print("WARNING: Ignoring entity of type", entity.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.all([map_load]).then(() => {
|
Promise.all([map_load]).then(() => {
|
||||||
|
|
@ -34,32 +55,31 @@ function load_assets() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let actors: Actor[] = [];
|
|
||||||
|
|
||||||
// TODO: Build a system whereby the signatures of the fundamental functions can be checked.
|
// TODO: Build a system whereby the signatures of the fundamental functions can be checked.
|
||||||
|
|
||||||
export function init() {
|
export function init() {
|
||||||
print("Hello world!");
|
print("Hello world!");
|
||||||
load_assets();
|
load_assets();
|
||||||
actors.push(new Robo(new_v2(10, 10)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Snapshot {
|
interface Snapshot {
|
||||||
clock: number;
|
clock: number;
|
||||||
actors: ActorSnapshot[];
|
actors: { type: ActorType; props: ActorProps }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function suspend(): Snapshot {
|
export function suspend(): Snapshot {
|
||||||
return {
|
return {
|
||||||
clock,
|
clock,
|
||||||
actors: actors.map((a) => a.snapshot()),
|
actors: actors.map((a) => {
|
||||||
|
return { type: a.type, props: a.props };
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resume(snapshot: Snapshot | undefined) {
|
export function resume(snapshot: Snapshot | undefined) {
|
||||||
if (snapshot) {
|
if (snapshot) {
|
||||||
clock = snapshot.clock || 0;
|
clock = snapshot.clock || 0;
|
||||||
actors = snapshot.actors.map((a) => actor_from_snapshot(a));
|
actors = snapshot.actors.map((s) => spawn_actor(s.type, s.props));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue