diff --git a/plan.txt b/plan.txt
index 64616ea..eebd8b1 100644
--- a/plan.txt
+++ b/plan.txt
@@ -1,10 +1,11 @@
 Open:
-- Rework State implementation for easier cloning/deserialization
-- Insecured websocket server implementation
 - Refactor input messages for more than one player
+- Insecured websocket server implementation
 - Cloneable RNG that goes in state (use MurmurHash3 finalizer in counter mode?)
+- remove all random() calls
 
 Done:
+- Rework State implementation for easier cloning/deserialization
 - Test Lockstep/rollback
 - Smarter typings for Join/Lookup functions
 - Parcel scripts
diff --git a/src/Ecs/Components.ts b/src/Ecs/Components.ts
index a2624e0..bdbe794 100644
--- a/src/Ecs/Components.ts
+++ b/src/Ecs/Components.ts
@@ -1,6 +1,6 @@
 
 import { Layer, SpriteSheet } from "../Applet/Render";
-import { copy, Data as CoreData, SparseStore, Store } from "./Data";
+import { Component, copyDense, copySparse, EntityState, StateForSchema, Store } from "./Data";
 
 export class Box {
     constructor(
@@ -24,64 +24,116 @@ export function Approach(source: number, target: number, speed: number): number
 /**
  * pairs of vertex coordinates in ccw winding order
  */
-export class Polygon {
-    constructor(
-        public points: number[]
-    ) {};
+export interface Polygon {
+    points: number[];
 }
-
-export class Location {
-    constructor(init?: Partial<Location>) {
-        init && Object.assign(this, init);
+export class PolygonComponent extends Component<PolygonComponent> {
+    points: number[];
+    constructor(from: Partial<PolygonComponent>) {
+        super(from);
+        this.points = from.points?.slice() ?? []
     };
-    X = 0;
-    Y = 0;
-    Angle = 0;
-    VX = 0;
-    VY = 0;
-    VAngle = 0;
-}
-
-export class CollisionClass {
-    constructor(
-        public name: string
-    ) {};
-}
-
-export class RenderBounds  {
-    constructor(
-        public color = "#f00",
-        public layer: number
-    ) {};
-};
-
-export class RenderSprite  {
-    constructor(
-        public sheet: SpriteSheet,
-        public layer: number,
-        public index = 0,
-        public offsetX = 0,
-        public offsetY = 0
-    ) {};
-};
-
-export class Data extends CoreData {
-    location: Store<Location> = [];
-    bounds: Store<Polygon> = [];
-    renderBounds: SparseStore<RenderBounds> = {};
-    renderSprite: SparseStore<RenderSprite> = {};
-    collisionSourceClass: SparseStore<CollisionClass> = {};
-    collisionTargetClass: SparseStore<CollisionClass> = {};
-
-    layers: Layer[] = [new Layer(0)];
-
-    constructor(source?: Partial<Data>) {
-        super(source);
-        if(source?.location) this.location = source.location.map(b => ({...b}));
-        if(source?.bounds) this.bounds = source.bounds.map(b => ({...b}));
-        if(source?.renderBounds) this.renderBounds = copy(source.renderBounds);
-        if(source?.renderSprite) this.renderSprite = copy(source.renderSprite);
-        if(source?.collisionSourceClass) this.collisionSourceClass = copy(source.collisionSourceClass);
-        if(source?.collisionTargetClass) this.collisionTargetClass = copy(source.collisionTargetClass);
+    clone() {
+        return new PolygonComponent(this);
+    }
+}
+
+export class Location extends Component<Location> {
+    X: number;
+    Y: number;
+    Angle: number;
+    VX: number;
+    VY: number;
+    VAngle: number;
+    constructor(from: Partial<Location>) {
+        super(from);
+        this.X = from.X ?? 0;
+        this.Y = from.Y ?? 0;
+        this.Angle = from.Angle ?? 0;
+        this.VX = from.VX ?? 0;
+        this.VY = from.VY ?? 0;
+        this.VAngle = from.VAngle ?? 0;
+    };
+    clone() {
+        return new Location(this);
+    }
+}
+
+export class CollisionClass extends Component<CollisionClass> {
+    public name: string;
+    constructor(from: Partial<CollisionClass>) {
+        super(from);
+        this.name = from.name ?? "unknown";
+    };
+    clone() {
+        return new CollisionClass(this);
+    }
+}
+
+export class RenderBounds extends Component<RenderBounds>  {
+    public color: string;
+    public layer: number;
+    constructor(from: Partial<RenderBounds>) {
+        super(from);
+        this.color = from.color ?? "#f00";
+        this.layer = from.layer ?? 1;
+    };
+    clone() {
+        return new RenderBounds(this);
+    }
+};
+
+export class RenderSprite extends Component<RenderSprite>  {
+    // TODO: make this an id/handle for serializability
+    public sheet: SpriteSheet;
+    public layer: number;
+    public index: number;
+    public offsetX: number;
+    public offsetY: number;
+    constructor(from: Partial<RenderSprite> & {sheet: SpriteSheet}) {
+        super(from);
+        this.sheet = from.sheet;
+        this.layer = from.layer ?? 1;
+        this.index = from.index ?? 0;
+        this.offsetX = from.offsetX ?? 0;
+        this.offsetY = from.offsetY ?? 0;
+    };
+    clone() {
+        return new RenderSprite(this);
+    }
+};
+
+export interface ComponentSchema {
+    location: Location;
+    bounds: PolygonComponent;
+    renderBounds: RenderBounds;
+    renderSprite: RenderSprite;
+    collisionSourceClass: CollisionClass;
+    collisionTargetClass: CollisionClass;
+}
+
+export class Data implements StateForSchema<ComponentSchema> {
+    entity: EntityState[];
+
+    location: Store<Location>;
+    bounds: Store<PolygonComponent>;
+    renderBounds: Store<RenderBounds>;
+    renderSprite: Store<RenderSprite>;
+    collisionSourceClass: Store<CollisionClass>;
+    collisionTargetClass: Store<CollisionClass>;
+
+    layers: Layer[] = [new Layer(0), new Layer(1)];
+
+    constructor(from: Partial<Data>) {
+        this.entity = copyDense(from.entity);
+        this.location = copyDense(from.location);
+        this.bounds = copyDense(from.bounds);
+        this.renderBounds = copySparse(from.renderBounds);
+        this.renderSprite = copySparse(from.renderSprite);
+        this.collisionSourceClass = copySparse(from.collisionSourceClass);
+        this.collisionTargetClass = copySparse(from.collisionTargetClass);
+    }
+    clone() {
+        return new Data(this);
     }
 }
diff --git a/src/Ecs/Data.ts b/src/Ecs/Data.ts
index 8c2e156..40ec0ab 100644
--- a/src/Ecs/Data.ts
+++ b/src/Ecs/Data.ts
@@ -1,4 +1,33 @@
 
+export type Store<T extends Component<T>> = Record<number, T>;
+export interface Clone<T> {
+    clone(): T;
+}
+
+export function copyDense<T extends Component<T>, S extends Store<T>>(storage?: S): T[] {
+    if(Array.isArray(storage)) {
+        return storage.map(x => x.clone());
+    } else if(storage) {
+        const result: T[] = [];
+        for(const key in storage) {
+            result[key] = storage[key].clone();
+        }
+        return result;
+    } else {
+        return [];
+    }
+}
+export function copySparse<T extends Component<T>, S extends Store<T>>(storage?: S): Store<T> {
+    if(storage) {
+        const result: Store<T> = {};
+        for(const key in storage) {
+            result[key] = storage[key].clone();
+        }
+        return result;
+    } else {
+        return {};
+    }
+}
 
 export type Id = [number, number];
 
@@ -8,41 +37,36 @@ export const enum Liveness {
     INACTIVE = 2
 }
 
-export interface HasGeneration {
-    generation: number;
-}
-export interface EntityState {
-    alive: Liveness;
+export abstract class Component<T> implements Clone<Component<T>> {
+    public generation: number;
+    public constructor(from: Partial<Component<T>>) {
+        this.generation = from.generation ?? -1;
+    }
+    abstract clone(): Component<T> & T;
 }
+export class EntityState extends Component<EntityState> {
+    public alive: Liveness;
+    public constructor(from: Partial<EntityState>) {
+        super(from);
+        this.alive = from.alive ?? Liveness.ALIVE;
+    }
 
-export type Store<T> = (T & HasGeneration)[];
-export type SparseStore<T> = Record<number, T & HasGeneration>;
-export class Data {
-    entity: Store<EntityState> = [];
-
-    constructor(source?: Partial<Data>) {
-        if(source?.entity) this.entity = source.entity.slice();
+    clone(): EntityState {
+        return new EntityState(this);
     }
 }
 
-export function copy<T>(source: SparseStore<T>): SparseStore<T> {
-    return JSON.parse(JSON.stringify(source));
-}
+export type StateForSchema<T> = {
+    [K in keyof T]: Record<number, Component<T[K]>>;
+} & {
+    entity: EntityState[];
+} & Clone<StateForSchema<T>>;
 
 // Ergonomic Lookup typings
 type StoreKeysOf<DATA> = {
-    [K in keyof DATA]: DATA[K] extends Record<number, infer T & HasGeneration> ? K : never;
-};
-type StoreKeys<DATA extends Data> = StoreKeysOf<DATA>[keyof DATA];
-type ItemType<S> = S extends Record<number, infer T & HasGeneration> ? T : never;
-type StoreType<DATA extends Data, K> = K extends "id" ? Id : K extends keyof DATA ? ItemType<DATA[K]> : never;
-type StoreTypes<DATA extends Data, K extends (keyof DATA | "id")[]> = {
-    [I in keyof K]: StoreType<DATA, K[I]>;
-};
-type MaybeStoreTypes<DATA extends Data, K> = {
-    [I in keyof K]: StoreType<DATA, K[I]> | null;
-};
-
+    [K in keyof DATA]: DATA[K] extends Record<number, Component<infer T>> ? K : never;
+}[keyof DATA];
+type StoreTypeOf<DATA, K> = K extends keyof DATA ? DATA[K] extends Record<number, Component<infer T>> ? T : never : never;
 
 /**
  * Create an entity in the store
@@ -51,35 +75,37 @@ type MaybeStoreTypes<DATA extends Data, K> = {
  * @param state Liveness state, allows creating an inactive entity
  * @returns the new entity's ID and generation
  */
-type Assigner<DATA extends Data> = {
-    [S in StoreKeys<DATA>]?: StoreType<DATA, S>
+type Assigner<DATA> = {
+    [K in StoreKeysOf<DATA>]?: StoreTypeOf<DATA, K>;
 };
-export function Create<DATA extends Data>(data: DATA, assign: Assigner<DATA>, state = Liveness.ALIVE): Id {
+export function Create<DATA extends StateForSchema<unknown>>(data: DATA, assign: Assigner<DATA>, state = Liveness.ALIVE): Id {
     const entities = data.entity;
     // find free ID
     let freeId = -1;
     let generation = -1;
-    for(let id = 0; id < entities.length; id++) {
-        if(entities[id].alive == Liveness.DEAD) {
+    for (let id = 0; id < entities.length; id++) {
+        if (entities[id].alive == Liveness.DEAD) {
             freeId = id;
             generation = entities[id].generation + 1;
             break;
         }
     }
 
-    if(freeId == -1) {
+    if (freeId == -1) {
         freeId = entities.length;
         generation = 1;
     }
 
-    entities[freeId] = {
+    entities[freeId] = new EntityState({
         generation,
         alive: state
-    };
+    });
 
-    for(const key in assign) {
-        const store = data[key as keyof Data] as Store<{}>|SparseStore<{}>;
-        store[freeId] = {...(assign as Record<string, HasGeneration>)[key] as {}, generation};
+    for (const key in assign) {
+        const store = data[key as keyof DATA] as Store<any>;
+        const component = (assign[key as keyof Assigner<DATA>] as Component<unknown>).clone();
+        component.generation = generation;
+        store[freeId] = component;
     }
 
     return [freeId, generation];
@@ -92,8 +118,8 @@ export function Create<DATA extends Data>(data: DATA, assign: Assigner<DATA>, st
  * @param generation entity ID generation
  * @param state can be set to Liveness.INACTIVE to disable an entity without actually killing it, for later resurrection
  */
-export function Remove<DATA extends Data>(data: DATA, [id, generation]: Id, state = Liveness.DEAD) {
-    if(data.entity[id] && data.entity[id].generation == generation) {
+export function Remove(data: StateForSchema<unknown>, [id, generation]: Id, state = Liveness.DEAD) {
+    if (data.entity[id] && data.entity[id].generation == generation) {
         data.entity[id].alive = state;
     }
 }
@@ -105,21 +131,24 @@ export function Remove<DATA extends Data>(data: DATA, [id, generation]: Id, stat
  * @param components names of components to look for
  * @returns the cooresponding components, with unfound ones replaced by nulls
  */
-export function Lookup<DATA extends Data, K extends StoreKeys<DATA>[]>(data: DATA, [id, generation]: Id, ...components: K): MaybeStoreTypes<DATA, K> {
+type MaybeStoreTypes<DATA, Q> = {
+    [I in keyof Q]: StoreTypeOf<DATA, Q[I]> | null;
+};
+export function Lookup<DATA extends StateForSchema<unknown>, Q extends StoreKeysOf<DATA>[]>(data: DATA, [id, generation]: Id, ...components: Q): MaybeStoreTypes<DATA, Q> {
     const entity = data.entity[id];
     // inactive entities are fine to lookup, but dead ones are not
-    if(entity && entity.generation == generation && entity.alive != Liveness.DEAD) {
+    if (entity && entity.generation == generation && entity.alive != Liveness.DEAD) {
         return components.map(storeName => {
-            const store = data[storeName] as unknown as Store<{}>|SparseStore<{}>;
+            const store = data[storeName as unknown as keyof DATA] as Store<any>;
             const component = store[id];
-            if(component && component.generation == generation) {
+            if (component && component.generation == generation) {
                 return component;
             } else {
                 return null;
             }
-        }) as MaybeStoreTypes<DATA, K>;
+        }) as MaybeStoreTypes<DATA, Q>;
     } else {
-        return components.map(() => null) as MaybeStoreTypes<DATA, K>;
+        return components.map(() => null) as MaybeStoreTypes<DATA, Q>;
     }
 }
 
@@ -128,36 +157,39 @@ export function Lookup<DATA extends Data, K extends StoreKeys<DATA>[]>(data: DAT
  * "id" can be used as a pseudo-component to get the Ids for a match
  * @returns an array of tuples containing the matching entity Components
  */
-export function Join<DATA extends Data, K extends (StoreKeys<DATA> | "id")[]>(data: DATA, ...components: K): StoreTypes<DATA, K>[] {
+type ResultTypes<DATA, Q> = {
+    [I in keyof Q]: Q[I] extends "id" ? Id : StoreTypeOf<DATA, Q[I]>;
+};
+export function Join<DATA extends StateForSchema<unknown>, Q extends (StoreKeysOf<DATA> | "id")[]>(data: DATA, ...components: Q): ResultTypes<DATA, Q>[] {
     const entities = data.entity;
     const stores = components.map(name => {
-        if(name == "id") {
+        if (name == "id") {
             return "id";
         } else {
-            return data[name] as unknown as (Store<{}>|SparseStore<{}>)
+            return data[name as unknown as keyof DATA] as Store<any>;
         };
     });
 
-    const results: StoreTypes<DATA, K>[] = [];
-    const firstStore = stores.filter(store => store !== "id")[0] as Store<{}>|SparseStore<{}>;
-    if(Array.isArray(firstStore)) {
-        for(let id = 0; id < firstStore.length; id++) {
+    const results: ResultTypes<DATA, Q>[] = [];
+    const firstStore = stores.filter(store => store !== "id")[0] as Store<any>;
+    if (Array.isArray(firstStore)) {
+        for (let id = 0; id < firstStore.length; id++) {
             JoinLoop(id, entities, stores, results);
         }
     } else {
-        for(const id in firstStore) {
+        for (const id in firstStore) {
             JoinLoop(Number(id), entities, stores, results);
         }
     }
     return results;
 }
-function JoinLoop<DATA extends Data, K extends (StoreKeys<DATA> | "id")[]>(id: number, entities: Store<EntityState>, stores: (Store<{}> | SparseStore<{}> | "id")[], results: StoreTypes<DATA, K>[]) {
+function JoinLoop<DATA extends StateForSchema<unknown>, Q extends (StoreKeysOf<DATA> | "id")[]>(id: number, entities: EntityState[], stores: (Record<number, Component<{}>> | "id")[], results: ResultTypes<DATA, Q>[]) {
     const fullId: Id = [id, -1];
-    const result: (HasGeneration | Id)[] = [];
+    const result: (Component<{}> | Id)[] = [];
 
     let generation = -1;
     for (const store of stores) {
-        if(store === "id") {
+        if (store === "id") {
             result.push(fullId);
             continue;
         }
@@ -177,5 +209,5 @@ function JoinLoop<DATA extends Data, K extends (StoreKeys<DATA> | "id")[]>(id: n
     // backpatch generation now that it's known
     fullId[1] = generation;
 
-    results.push(result as StoreTypes<DATA, K>);
+    results.push(result as ResultTypes<DATA, Q>);
 }
diff --git a/src/Ecs/Location.ts b/src/Ecs/Location.ts
index 2bea3d9..9189867 100644
--- a/src/Ecs/Location.ts
+++ b/src/Ecs/Location.ts
@@ -9,7 +9,7 @@ export function TransformCx(cx: CanvasRenderingContext2D, location: Location, dt
 export function TfPolygon({points}: Polygon, {X, Y, Angle}: Location): Polygon {
     const sin = Math.sin(Angle);
     const cos = Math.cos(Angle);
-    const result = new Polygon(new Array(points.length));
+    const result = {points: new Array(points.length)};
     for(let i = 0; i < points.length; i += 2) {
         const x = points[i];
         const y = points[i+1];
diff --git a/src/Ecs/test.ts b/src/Ecs/test.ts
index 3cae37b..ca39f7d 100644
--- a/src/Ecs/test.ts
+++ b/src/Ecs/test.ts
@@ -3,11 +3,42 @@ import { KeyControl } from "../Applet/Keyboard";
 import { Loop } from "../Applet/Loop";
 import { DrawSet, Layer } from "../Applet/Render";
 import { FindCollisions } from "./Collision";
-import { CollisionClass, Data, Location, Polygon, RenderBounds } from "./Components";
-import { Create, Join, Liveness, Lookup, Remove, SparseStore, Store } from "./Data";
+import {
+    CollisionClass,
+    ComponentSchema,
+    Data,
+    Location,
+    PolygonComponent,
+    RenderBounds,
+} from "./Components";
+import {
+    Component,
+    copySparse,
+    Create,
+    EntityState,
+    Join,
+    Liveness,
+    Lookup,
+    Remove,
+    StateForSchema,
+    Store,
+} from "./Data";
 import { DumbMotion } from "./Location";
 import { RunRenderBounds } from "./Renderers";
 
+class Generic<T> extends Component<T> {
+    constructor(from: T) {
+        super(from);
+        Object.assign(this, from);
+    }
+    clone(): Generic<T> & T {
+        return new Generic<T>(this as unknown as T) as Generic<T> & T;
+    }
+}
+function generic<T>(from: T): Component<T> & T {
+    return new Generic<T>(from) as Component<T> & T;
+}
+
 interface Apple {}
 interface Banana {
     peeled: boolean
@@ -16,38 +47,63 @@ interface Carrot {
     cronch: number
 }
 
-class TestData extends Data {
-    entity = [
-        {generation: 5, alive: Liveness.ALIVE},
-        {generation: 5, alive: Liveness.DEAD},
-        {generation: 5, alive: Liveness.ALIVE},
-        {generation: 5, alive: Liveness.ALIVE},
-        {generation: 5, alive: Liveness.INACTIVE},
-        {generation: 5, alive: Liveness.ALIVE},
-    ];
-    apple: Store<Apple> = [
-        {generation: 5},
-        {generation: 5},
-        {generation: -1},
-        {generation: -1},
-        {generation: 5},
-        {generation: 5},
-    ];
-    banana: SparseStore<Banana> = {
-        3: {generation: 5, peeled: false},
-        4: {generation: 5, peeled: true},
-    };
-    carrot: SparseStore<Carrot> = {
-        0: {generation: 5, cronch: 1},
-        1: {generation: 5, cronch: 1},
-        2: {generation: 4, cronch: 10},
-        3: {generation: 5, cronch: 1},
-    };
+interface TestSchema extends ComponentSchema {
+    apple: Apple;
+    banana: Banana;
+    carrot: Carrot;
+}
+
+class TestData extends Data implements StateForSchema<TestSchema> {
+    apple: Store<Generic<Apple>>;
+    banana: Store<Generic<Banana>>;
+    carrot: Store<Generic<Carrot>>;
+    constructor(from: Partial<TestData>) {
+        super(from);
+        this.apple = copySparse(from.apple);
+        this.banana = copySparse(from.banana);
+        this.carrot = copySparse(from.carrot);
+    }
+    clone(): TestData {
+        return new TestData(this);
+    }
+}
+
+function makeTestData(): TestData {
+    return new TestData({
+        entity: [
+            new EntityState({generation: 5, alive: Liveness.ALIVE}),
+            new EntityState({generation: 5, alive: Liveness.DEAD}),
+            new EntityState({generation: 5, alive: Liveness.ALIVE}),
+            new EntityState({generation: 5, alive: Liveness.ALIVE}),
+            new EntityState({generation: 5, alive: Liveness.INACTIVE}),
+            new EntityState({generation: 5, alive: Liveness.ALIVE}),
+        ],
+        apple: [
+            generic({generation: 5}),
+            generic({generation: 5}),
+            generic({generation: -1}),
+            generic({generation: -1}),
+            generic({generation: 5}),
+            generic({generation: 5}),
+        ],
+        banana: {
+            3: generic({generation: 5, peeled: false}),
+            4: generic({generation: 5, peeled: true}),
+        },
+        carrot: {
+            0: generic({generation: 5, cronch: 1}),
+            1: generic({generation: 5, cronch: 1}),
+            2: generic({generation: 4, cronch: 10}),
+            3: generic({generation: 5, cronch: 1}),
+        },
+    });
 }
 
 class EcsJoinTest {
     constructor(pre: HTMLElement) {
-        const data = new TestData();
+        const data = new TestData({
+
+        });
         pre.innerText = JSON.stringify({
             "apples": Join(data, "apple"),
             "bananas": Join(data, "banana"),
@@ -59,7 +115,7 @@ class EcsJoinTest {
 
 class EcsLookupTest {
     constructor(pre: HTMLElement) {
-        const data = new TestData();
+        const data = makeTestData();
         const applesMaybeCarrots = Join(data, "apple", "id").map(([apple, id]) => ({
             apple,
             maybeCarrot: Lookup(data, id, "carrot")[0]
@@ -70,7 +126,7 @@ class EcsLookupTest {
 
 class EcsRemoveTest {
     constructor(pre: HTMLElement) {
-        const data = new TestData();
+        const data = makeTestData();
         const beforeDelete = Join(data, "apple", "carrot", "id",);
         Remove(data, [0, 5]);
         const afterDelete = Join(data, "apple", "carrot", "id");
@@ -83,12 +139,12 @@ class EcsRemoveTest {
 
 class EcsCreateTest {
     constructor(pre: HTMLElement) {
-        const data = new TestData();
+        const data = makeTestData();
         const beforeCreate = Join(data, "apple", "banana", "carrot", "id");
         const createdId = Create(data, {
-            apple: {},
-            banana: {peeled: false},
-            carrot: {cronch: 11}
+            apple: generic({}),
+            banana: generic({peeled: false}),
+            carrot: generic({cronch: 11})
         });
         const afterCreate = Join(data, "apple", "banana", "carrot", "id");
         pre.innerText = JSON.stringify({
@@ -100,7 +156,7 @@ class EcsCreateTest {
 }
 
 class LoopTest {
-    data = new Data();
+    data = new Data({});
 
     constructor(public canvas: HTMLCanvasElement, cx: CanvasRenderingContext2D, keys: KeyControl) {
         const drawSet = new DrawSet();
@@ -112,9 +168,9 @@ class LoopTest {
                 Y: 200,
                 VAngle: Math.PI
             }),
-            bounds: new Polygon([-50, 50, -60, 250, 60, 250, 50, 50]),
-            collisionTargetClass: new CollisionClass("block"),
-            renderBounds: new RenderBounds("#0a0", 0),
+            bounds: new PolygonComponent({points: [-50, 50, -60, 250, 60, 250, 50, 50]}),
+            collisionTargetClass: new CollisionClass({ name: "block"}),
+            renderBounds: new RenderBounds({color: "#0a0", layer: 0}),
         });
 
         // triangles
@@ -125,12 +181,12 @@ class LoopTest {
                 Angle: angle,
                 VAngle: -Math.PI/10
             }),
-            bounds: new Polygon([70, 0, 55, 40, 85, 40]),
-            collisionSourceClass: new CollisionClass("tri"),
-            renderBounds: new RenderBounds(
-                "#d40",
-                0
-            )
+            bounds: new PolygonComponent({points: [70, 0, 55, 40, 85, 40]}),
+            collisionSourceClass: new CollisionClass({ name: "tri"}),
+            renderBounds: new RenderBounds({
+                color: "#d40",
+                layer: 0,
+            })
         }));
 
         const loop = new Loop(30,
diff --git a/src/Game/Death.ts b/src/Game/Death.ts
index e4348c6..cdb2fbe 100644
--- a/src/Game/Death.ts
+++ b/src/Game/Death.ts
@@ -1,5 +1,5 @@
 import { PlaySfx } from "../Applet/Audio";
-import { Location, Polygon, RenderBounds } from "../Ecs/Components";
+import { Location, Polygon, PolygonComponent, RenderBounds } from "../Ecs/Components";
 import { Create, Id, Join, Lookup, Remove } from "../Ecs/Data";
 import { Data, Lifetime, Teams, World } from "./GameComponents";
 
@@ -60,13 +60,20 @@ function SpawnPuff(data: Data, world: World, x: number, y: number, size: number,
             VX: (Math.random() + 0.5) * 400 * Math.cos(angle),
             VY: (Math.random() + 0.5) * 400 * -Math.sin(angle)
         }),
-        bounds: new Polygon([
+        bounds: new PolygonComponent({points: [
             -size, -size,
             -size, size,
             size, size,
             size, -size
-        ]),
-        renderBounds: new RenderBounds(color, /*world.smokeLayer*/ 0),
-        lifetime: new Lifetime(Math.random() / 3)
+        ]}),
+        renderBounds: new RenderBounds({
+            color,
+            // TODO: work out standard layers
+            layer: 1
+        }),
+        // TODO: randomization breaks determinism. Might be safe for a smoke puff that doesn't effect gameplay, but bad hygeine
+        lifetime: new Lifetime({
+            time: Math.random() / 3
+        })
     });
 }
diff --git a/src/Game/GameComponents.ts b/src/Game/GameComponents.ts
index f73fb01..b6460ad 100644
--- a/src/Game/GameComponents.ts
+++ b/src/Game/GameComponents.ts
@@ -1,7 +1,7 @@
 import { KeyName } from "../Applet/Keyboard";
 import { DrawSet, Layer } from "../Applet/Render";
-import { Data as EcsData } from "../Ecs/Components";
-import { copy, Join, SparseStore } from "../Ecs/Data";
+import { ComponentSchema, Data as EcsData } from "../Ecs/Components";
+import { Component, copySparse, Join, StateForSchema, Store } from "../Ecs/Data";
 import { DumbMotion } from "../Ecs/Location";
 import { INPUT_FREQUENCY, LockstepProcessor } from "../Ecs/Lockstep";
 import { Buttons } from "./Input";
@@ -57,20 +57,35 @@ export class World {
     }
 }
 
-export class Data extends EcsData {
-    boss: SparseStore<Boss> = {};
-    bullet: SparseStore<Bullet> = {};
-    hp: SparseStore<Hp> = {};
-    lifetime: SparseStore<Lifetime> = {};
-    message: SparseStore<Message> = {};
+interface GameSchema extends ComponentSchema {
+    boss: Boss;
+    bullet: Bullet;
+    hp: Hp;
+    lifetime: Lifetime;
+    message: Message;
+}
 
-    constructor(source?: Partial<Data>) {
-        super(source);
-        if(source?.boss) this.boss = copy(source.boss);
-        if(source?.bullet) this.bullet = copy(source.bullet);
-        if(source?.hp) this.hp = copy(source.hp);
-        if(source?.lifetime) this.lifetime = copy(source.lifetime);
-        if(source?.message) this.message = copy(source.message);
+export class Data extends EcsData implements StateForSchema<GameSchema> {
+    boss: Store<Boss>;
+    bullet: Store<Bullet>;
+    hp: Store<Hp>;
+    lifetime: Store<Lifetime>;
+    message: Store<Message>;
+
+    // globals
+    debugLayer = new Layer(2);
+
+    constructor(from: Partial<Data>) {
+        super(from);
+        this.boss = copySparse(from.boss);
+        this.bullet = copySparse(from.bullet);
+        this.hp = copySparse(from.hp);
+        this.lifetime = copySparse(from.lifetime);
+        this.message = copySparse(from.message);
+    }
+
+    clone() {
+        return new Data(this);
     }
 }
 
@@ -78,41 +93,74 @@ export enum Teams {
     PLAYER,
     ENEMY
 }
-export class Bullet {
-    hit = false;
-    constructor(
-        public team: Teams,
-        public attack: number
-    ) {};
+export class Bullet extends Component<Bullet> {
+    hit: boolean;
+    team: Teams;
+    attack: number;
+    constructor(from: Partial<Bullet>) {
+        super(from);
+        this.hit = from.hit ?? false;
+        this.team = from.team ?? Teams.ENEMY;
+        this.attack = from.attack ?? 1;
+    }
+    clone(): Bullet {
+        return new Bullet(this);
+    }
 }
-export class Hp {
-    receivedDamage = 0;
-    constructor(
-        public team: Teams,
-        public hp: number
-    ) {};
+export class Hp extends Component<Hp> {
+    receivedDamage: number;
+    team: Teams;
+    hp: number;
+    constructor(from: Partial<Hp>) {
+        super(from);
+        this.receivedDamage = from.receivedDamage ?? 0;
+        this.team = from.team ?? Teams.ENEMY;
+        this.hp = from.hp ?? 10;
+    }
+    clone(): Hp {
+        return new Hp(this);
+    }
 }
 
-export class Lifetime {
-    constructor(
-        public time: number
-    ) {};
+export class Lifetime extends Component<Lifetime> {
+    time: number;
+    constructor(from: Partial<Lifetime> & {time: number}) {
+        super(from);
+        this.time = from.time;
+    }
+    clone(): Lifetime {
+        return new Lifetime(this);
+    }
 }
 
-export class Boss {
-    constructor(
-        public name: string
-    ) {}
+export class Boss extends Component<Boss> {
+    name: string;
+    constructor(from: Partial<Boss>) {
+        super(from);
+        this.name = from.name ?? "";
+    }
+    clone(): Boss {
+        return new Boss(this);
+    }
 }
 
-export class Message {
+export class Message extends Component<Message> {
     targetY = 0;
-    constructor(
-        public layer: Layer,
-        public color: string,
-        public message: string,
-        public timeout = 3
-    ) {}
+    layer: number;
+    color: string;
+    message: string;
+    timeout = 3;
+    constructor(from: Partial<Message>) {
+        super(from);
+        this.targetY = from.targetY ?? 0;
+        this.layer = from.layer ?? 1;
+        this.color = from.color ?? "#000";
+        this.message = from.message ?? "";
+        this.timeout = from.timeout ?? 3;
+    }
+    clone(): Message {
+        return new Message(this);
+    }
 }
 
 export class Engine implements LockstepProcessor<KeyName[], Data> {
diff --git a/src/Game/Main.ts b/src/Game/Main.ts
index 49c59d2..7e477af 100644
--- a/src/Game/Main.ts
+++ b/src/Game/Main.ts
@@ -3,7 +3,7 @@ import subscribe from "callbag-subscribe";
 
 import { KeyControl, KeyName } from "../Applet/Keyboard";
 import { DrawSet } from "../Applet/Render";
-import { Location, Polygon, RenderBounds } from "../Ecs/Components";
+import { Location, Polygon, PolygonComponent, RenderBounds } from "../Ecs/Components";
 import { Create } from "../Ecs/Data";
 import { RunRenderBounds } from "../Ecs/Renderers";
 import { LockstepClient } from "../Net/LockstepClient";
@@ -40,15 +40,18 @@ export class Main extends LockstepClient<KeyName[], Data> {
     }
 
     initState(patch: Partial<Data>) {
-        const newState = new Data();
+        const newState = new Data(patch);
 
         Create(newState, {
             location: new Location({
                 X: 200,
                 Y: 200,
             }),
-            bounds: new Polygon([-30, 0, 30, 0, 0, 40]),
-            renderBounds: new RenderBounds("#a0f", 0),
+            bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40]}),
+            renderBounds: new RenderBounds({
+                color: "#a0f",
+                layer: 0
+            }),
         });
 
         return newState;
diff --git a/src/Game/Message.ts b/src/Game/Message.ts
index 00e6307..8f26abd 100644
--- a/src/Game/Message.ts
+++ b/src/Game/Message.ts
@@ -1,7 +1,7 @@
-import { Join, Remove } from "../Ecs/Data";
-import { Data, World, GamePhase } from "./GameComponents";
 import { DrawSet } from "../Applet/Render";
+import { Join, Remove } from "../Ecs/Data";
 import { TransformCx } from "../Ecs/Location";
+import { Data, GamePhase, World } from "./GameComponents";
 
 /*export function SpawnMessage(color: string, text: string) {
     return function(data: Data, world: World, x: number, timeoutDelta = 0): Id {
@@ -57,7 +57,7 @@ export function ReapMessages(data: Data, {width, height, debug}: World) {
 
 export function RenderMessages(data: Data, drawSet: DrawSet) {
     drawSet.queue(...Join(data, "message", "location").map(
-        ([{layer, color, message}, location]) => layer.toRender((cx, dt) => {
+        ([{layer, color, message}, location]) => data.layers[layer].toRender((cx, dt) => {
             TransformCx(cx, location, dt);
             cx.font = `${FONT_SIZE}px monospace`;
             cx.fillStyle = color;