WIP: ECS / Network System #1

Draft
simponic wants to merge 13 commits from websockets into main
6 changed files with 136 additions and 24 deletions
Showing only changes of commit 8ac11eda05 - Show all commits

View File

@ -18,6 +18,19 @@ class SpriteSpec:
self.end_x = end_x self.end_x = end_x
self.end_y = end_y self.end_y = end_y
def to_dict(self) -> dict:
return {
"ms_per_frame": self.ms_per_frame,
"top_x": self.top_x,
"top_y": self.top_y,
"end_x": self.end_x,
"end_y": self.end_y,
"frames": self.frames,
}
def __repr__(self) -> str:
return f"SpriteSpec(ms_per_frame={self.ms_per_frame}, top_x={self.top_x}, top_y={self.top_y}, end_x={self.end_x}, end_y={self.end_y}, frames={self.frames})"
class SpriteSheet(Component): class SpriteSheet(Component):
def __init__( def __init__(
@ -44,5 +57,7 @@ class SpriteSheet(Component):
"current_frame": self.current_frame, "current_frame": self.current_frame,
"last_update": self.last_update, "last_update": self.last_update,
"current_state": self.initial_state, "current_state": self.initial_state,
"state_to_spritespec": self.state_to_spritespec, "state_to_spritespec": {
k: v.to_dict() for k, v in self.state_to_spritespec.items()
},
} }

View File

@ -4,7 +4,7 @@ from enum import Enum
from .entity import Entity, EntityType from .entity import Entity, EntityType
class CatState(Enum): class CatState(str, Enum):
IDLE = "IDLE" IDLE = "IDLE"
FROLICKING = "FROLICKING" FROLICKING = "FROLICKING"
EEPY = "EEPY" EEPY = "EEPY"
@ -13,20 +13,47 @@ class CatState(Enum):
CHASING_CAT = "CHASING_CAT" CHASING_CAT = "CHASING_CAT"
SCRATCHING = "SCRATCHING" SCRATCHING = "SCRATCHING"
ITCHY = "ITCHY" ITCHY = "ITCHY"
MAKING_BISCUITS = "MAKING_BISCUITS"
class CatSpriteState(str, Enum):
ALERT = "ALERT"
MAKING_BISCUITS = "MAKING_BISCUITS"
class Cat(Entity): class Cat(Entity):
def __init__(self, id: str, spritesheet_source: str): def __init__(self, id: str, spritesheet_source: str):
state_to_spritespec = self.get_state_to_spritespec() state_to_spritespec = self.get_state_to_spritespec()
components = [ components = [
Position(0, 0), Position(50, 50),
SpriteSheet(spritesheet_source, state_to_spritespec, CatState.ALERT), SpriteSheet(
spritesheet_source, state_to_spritespec, CatSpriteState.MAKING_BISCUITS
),
] ]
super().__init__(EntityType.CAT, id, components) super().__init__(EntityType.CAT, id, components)
def get_state_to_spritespec(self): def get_state_to_spritespec(self):
return {CatState.ALERT: SpriteSpec(100, 0, 0, 200, 40, 5)} creature_width = 32
creature_height = 32
return {
CatSpriteState.ALERT: SpriteSpec(
100,
creature_width * 7,
creature_height * 3,
creature_width * 8,
creature_height * 4,
1,
),
CatSpriteState.MAKING_BISCUITS: SpriteSpec(
300,
0,
0,
creature_width,
creature_height * 2,
2,
),
}
# #

View File

@ -36,7 +36,7 @@ export interface SpriteSpec {
export interface SpriteSheetComponent extends Component { export interface SpriteSheetComponent extends Component {
name: ComponentType.SPRITESHEET; name: ComponentType.SPRITESHEET;
source: string; source: string;
sheet: HTMLImageElement; sheet?: HTMLImageElement;
current_frame: number; current_frame: number;
last_update: number; last_update: number;

View File

@ -5,7 +5,7 @@ export class DebouncePublisher<T> {
constructor( constructor(
private readonly publisher: (data: T) => void | Promise<void>, private readonly publisher: (data: T) => void | Promise<void>,
private readonly debounce_ms = 150, private readonly debounce_ms = 100,
) {} ) {}
public start() { public start() {

View File

@ -2,6 +2,8 @@ import {
ComponentType, ComponentType,
PositionComponent, PositionComponent,
TrailingPositionComponent, TrailingPositionComponent,
SpriteSheetComponent,
SpriteSpec,
} from "./component"; } from "./component";
import { Game } from "./game"; import { Game } from "./game";
import { System, SystemType } from "./system"; import { System, SystemType } from "./system";
@ -17,7 +19,7 @@ export class RenderSystem extends System {
this.canvas.height = height; this.canvas.height = height;
} }
public update(_dt: number, game: Game) { public update(dt: number, game: Game) {
const ctx = this.canvas.getContext("2d"); const ctx = this.canvas.getContext("2d");
if (ctx === null) return; if (ctx === null) return;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
@ -32,15 +34,87 @@ export class RenderSystem extends System {
return; return;
} }
if (ComponentType.POSITION in entity.components) { if (
ComponentType.POSITION in entity.components &&
ComponentType.SPRITESHEET in entity.components
) {
const position = entity.components[ const position = entity.components[
ComponentType.POSITION ComponentType.POSITION
] as PositionComponent; ] as PositionComponent;
ctx.beginPath(); const spritesheet = entity.components[
ctx.arc(position.x, position.y, 50, 0, 2 * Math.PI); ComponentType.SPRITESHEET
ctx.stroke(); ] as SpriteSheetComponent;
if (typeof spritesheet.sheet === "undefined") {
const img = new Image();
img.src = spritesheet.source;
spritesheet.sheet = img;
}
const spritespec =
spritesheet.state_to_spritespec[spritesheet.current_state];
this.blit_sprite(ctx, spritesheet, position);
spritesheet.last_update += dt;
if (spritesheet.last_update > spritespec.ms_per_frame) {
spritesheet.current_frame++;
spritesheet.current_frame %= spritespec.frames;
spritesheet.last_update = 0;
}
return; return;
} }
}); });
} }
private blit_sprite(
ctx: CanvasRenderingContext2D,
spritesheet: SpriteSheetComponent,
position: { x: number; y: number },
) {
ctx.save();
ctx.translate(position.x, position.y);
ctx.translate(-position.x, -position.y);
if (typeof spritesheet.sheet === "undefined") return;
const spritespec =
spritesheet.state_to_spritespec[spritesheet.current_state];
ctx.drawImage(
spritesheet.sheet,
...this.get_sprite_args(spritesheet.current_frame, spritespec),
...this.get_draw_args(spritespec, position),
);
ctx.restore();
}
private get_sprite_args(
current_frame: number,
sprite_spec: SpriteSpec,
): [sx: number, sy: number, sw: number, sh: number] {
const [width, height] = this.get_dimensions(sprite_spec);
return [
sprite_spec.top_x,
sprite_spec.top_y + current_frame * height,
width,
height,
];
}
private get_draw_args(
sprite_spec: SpriteSpec,
position: { x: number; y: number },
): [dx: number, dy: number, dw: number, dh: number] {
const [width, height] = this.get_dimensions(sprite_spec);
return [position.x - width / 2, position.y - height / 2, width, height];
}
private get_dimensions(sprite_spec: SpriteSpec): [number, number] {
return [
sprite_spec.end_x - sprite_spec.top_x,
(sprite_spec.end_y - sprite_spec.top_y) / sprite_spec.frames,
];
}
} }

View File

@ -2,19 +2,15 @@ import { ComponentType, TrailingPositionComponent } from "./component";
import { Game } from "./game"; import { Game } from "./game";
import { System, SystemType } from "./system"; import { System, SystemType } from "./system";
interface Point {
x: number;
y: number;
time: number;
}
export class TrailingPositionSystem extends System { export class TrailingPositionSystem extends System {
constructor( constructor(
private readonly point_filter: ( private readonly point_filter: (trail_point: Array<Point>) => Array<Point>
trail_point: {
x: number;
y: number;
time: number;
}[],
) => {
x: number;
y: number;
time: number;
}[],
) { ) {
super(SystemType.TRAILING_POSITION); super(SystemType.TRAILING_POSITION);
} }
@ -27,7 +23,7 @@ export class TrailingPositionSystem extends System {
ComponentType.TRAILING_POSITION ComponentType.TRAILING_POSITION
] as TrailingPositionComponent; ] as TrailingPositionComponent;
trailing_position.trails = this.point_filter(trailing_position.trails); trailing_position.trails = this.point_filter(trailing_position.trails);
}, }
); );
} }
} }