diff --git a/README.md b/README.md
index be03c63..9287e69 100644
--- a/README.md
+++ b/README.md
@@ -101,8 +101,4 @@ as well as see all models made in any window.
- Music by [Nicholas Palmer](https://soundcloud.com/nicholas-palmer-4)
-worlds:
-
-- co_zZjA7PJnSUXEP7VVKomrT3gL7hf
-- co_z7FMaWBpkYGHixseZu9iuA2LmTL
-- co_zQLdXMAmNc7kkzeRBu7MyWDoRD1
\ No newline at end of file
+- co_zVcp8qwBoWE6X95YWHMQHEuM1dX
\ No newline at end of file
diff --git a/src/lib/player/OtherPlayer.svelte b/src/lib/player/OtherPlayer.svelte
new file mode 100644
index 0000000..35e75ce
--- /dev/null
+++ b/src/lib/player/OtherPlayer.svelte
@@ -0,0 +1,103 @@
+
+
+{#if show}
+
+ {#if player.character === 'protagonist a'}
+
+ {:else if player.character === 'protagonist b'}
+
+ {:else if player.character === 'hiker'}
+
+ {:else if player.character === 'caveman'}
+
+ {:else if player.character === 'witch'}
+
+ {:else if player.character === 'combat mech'}
+
+ {:else if player.character === 'superhero'}
+
+ {:else if player.character === 'vampire'}
+
+ {/if}
+
+{/if}
diff --git a/src/lib/player/Player.svelte b/src/lib/player/Player.svelte
index 6ca8823..5ed375f 100644
--- a/src/lib/player/Player.svelte
+++ b/src/lib/player/Player.svelte
@@ -5,7 +5,7 @@
import Controller from './ThirdPersonControls.svelte';
import type { RigidBody as RapierRigidBody } from '@dimforge/rapier3d-compat';
import { onMount } from 'svelte';
- import { editingState } from '$lib/world-editor/state.svelte';
+ import { editingState, updatePlayerData } from '$lib/world-editor/state.svelte';
import ProtagonistA, { type ActionName } from './Protagonist_A.svelte';
import ProtagonistB from './Protagonist_B.svelte';
import Hiker from './Hiker.svelte';
@@ -14,12 +14,14 @@
import CombatMech from './CombatMech.svelte';
import Superhero from './Superhero.svelte';
import Vampire from './Vampire.svelte';
+ import { AccountCoState } from 'jazz-svelte';
+ import { MyAppAccount } from '$lib/schema';
let {
position = [0, 10, 0],
radius = 0.4,
height = 1.4,
- speed = 4,
+ speed = 5,
runningSpeed = 8
}: {
position?: [number, number, number];
@@ -29,6 +31,14 @@
runningSpeed?: number;
} = $props();
+ const me = new AccountCoState(MyAppAccount, {
+ resolve: {
+ profile: true
+ }
+ });
+
+ const playerId = $derived(me.current?.profile?.id);
+
let isRunning = false;
let capsule: Group | undefined = $state();
@@ -55,9 +65,12 @@
let direction = new Vector3();
+ let timeSinceLastUpdate = 0;
+
usePhysicsTask((delta) => {
if (!rigidBody || !capsule) return;
+ timeSinceLastUpdate += delta;
total += delta;
// get direction
@@ -108,7 +121,7 @@
if (temp.length() > 3 && isRunning) {
animation = 'Running_A';
} else if (temp.length() > 0.5) {
- animation = 'Walking_B';
+ animation = 'Walking_A';
} else {
animation = 'Idle';
}
@@ -131,6 +144,16 @@
rigidBody.setTranslation(new Vector3(0, 10, 0), true);
rigidBody.setLinvel(new Vector3(0, 0, 0), true);
}
+
+ if (timeSinceLastUpdate > 0.1 && playerId) {
+ timeSinceLastUpdate = 0;
+ updatePlayerData(playerId, pos, rotation);
+
+ } else {
+ if(!playerId) {
+ console.log("no player id", me.current?.profile?.id)
+ }
+ }
});
function jump() {
@@ -208,7 +231,7 @@
-
+ ;
export const InstanceList = co.list(Instance);
-export const World = co.map({
+
+export const PlayerData = co.map({
+ position: z.object({
+ x: z.number(),
+ y: z.number(),
+ z: z.number()
+ }),
+ rotation: z.number(),
+ timestamp: z.number(),
+ character: z.string(),
+ velocity: z.object({
+ x: z.number(),
+ y: z.number(),
+ z: z.number()
+ })
+});
+
+export const PlayerDataList = co.record(z.string(), PlayerData);
+
+export const Cell = co.map({
instances: InstanceList
});
+export const WorldCells = co.record(z.string(), Cell);
+
+export const World = co.map({
+ cells: WorldCells,
+ players: PlayerDataList
+});
+
+
export const MyAppProfile = co.profile({
name: z.string(),
image: co.image().optional(),
diff --git a/src/lib/utils.svelte.ts b/src/lib/utils.svelte.ts
index afda905..026a56a 100644
--- a/src/lib/utils.svelte.ts
+++ b/src/lib/utils.svelte.ts
@@ -1,6 +1,14 @@
import { goto } from '$app/navigation';
import { Group } from 'jazz-tools';
-import { Instance, InstanceList, Transform, World } from './schema';
+import {
+ Cell,
+ Instance,
+ InstanceList,
+ PlayerDataList,
+ Transform,
+ World,
+ WorldCells
+} from './schema';
import { editingState } from './world-editor/state.svelte';
import { getPathsForModel, models } from './world-editor/models';
@@ -35,7 +43,18 @@ export async function createWorld(base: string) {
const world = World.create(
{
- instances: InstanceList.create([startInstance], publicGroup())
+ cells: WorldCells.create(
+ {
+ '0,0': Cell.create(
+ {
+ instances: InstanceList.create([startInstance], publicGroup())
+ },
+ publicGroup()
+ )
+ },
+ publicGroup()
+ ),
+ players: PlayerDataList.create({}, publicGroup())
},
publicGroup()
);
diff --git a/src/lib/world-editor/Instance.svelte b/src/lib/world-editor/Instance.svelte
index faf5ed9..8d57ca5 100644
--- a/src/lib/world-editor/Instance.svelte
+++ b/src/lib/world-editor/Instance.svelte
@@ -10,7 +10,7 @@
import AutoColliderWrapper from '$lib/world/AutoColliderWrapper.svelte';
import { Vector2 } from 'three';
- let { instance }: { instance: Loaded } = $props();
+ let { instance, interactive, cellId }: { instance: Loaded; interactive: boolean; cellId: string } = $props();
let scale = new Spring(0);
@@ -21,6 +21,8 @@
let pointerDownPosition: Vector2 | null = $state(null);
async function onclick(e: IntersectionEvent) {
+ if (!interactive) return;
+
e.stopPropagation();
if (editingState.selectedModel) {
@@ -31,6 +33,7 @@
await applyTransform();
editingState.selectedInstance = instance;
+ editingState.selectedCellId = cellId;
return;
}
diff --git a/src/lib/world-editor/WorldEditorScene.svelte b/src/lib/world-editor/WorldEditorScene.svelte
index 27255de..a3561e5 100644
--- a/src/lib/world-editor/WorldEditorScene.svelte
+++ b/src/lib/world-editor/WorldEditorScene.svelte
@@ -10,6 +10,17 @@
import Ground from '$lib/world/Ground.svelte';
import Dof from '$lib/world/DOF.svelte';
import { Debug } from '@threlte/rapier';
+ import OtherPlayer from '$lib/player/OtherPlayer.svelte';
+ import { AccountCoState } from 'jazz-svelte';
+ import { MyAppAccount } from '$lib/schema';
+
+ const me = new AccountCoState(MyAppAccount, {
+ resolve: {
+ profile: true
+ }
+ });
+
+ const playerId = $derived(me.current?.profile?.id);
interactivity({
filter: (hits) => {
@@ -19,6 +30,12 @@
const { renderer } = useThrelte();
+ let {
+ interactive = true,
+ y = 0,
+ position = [0, 0, 0]
+ }: { interactive?: boolean; y?: number; position?: [number, number, number] } = $props();
+
onMount(async () => {
window.addEventListener('keydown', (e) => {
if (
@@ -70,13 +87,10 @@
shadow.bias={-0.00001}
/>
-
-
-
-
-
{
+ if (!interactive) return;
+
clickedOn(e.point, true);
}}
/>
@@ -86,10 +100,30 @@
-{#each editingState.world?.current?.instances ?? [] as instance (instance.id)}
-
-{/each}
+
+
+
+
+
+
+ {#each Object.entries(editingState.world?.current?.cells ?? {}) as [cellId, cell]}
+ {#if cell && cell.instances}
+ {#each cell.instances ?? [] as instance (instance?.id)}
+ {#if instance}
+
+ {/if}
+ {/each}
+ {/if}
+ {/each}
+ {#if playerId}
+ {#each Object.entries(editingState.world?.current?.players ?? {}) as [id, player] (id)}
+ {#if player && id !== playerId}
+
+ {/if}
+ {/each}
+ {/if}
+
diff --git a/src/lib/world-editor/state.svelte.ts b/src/lib/world-editor/state.svelte.ts
index 5b73c88..5b6a969 100644
--- a/src/lib/world-editor/state.svelte.ts
+++ b/src/lib/world-editor/state.svelte.ts
@@ -1,4 +1,12 @@
-import { Instance, Transform, World } from '$lib/schema';
+import {
+ Cell,
+ Instance,
+ InstanceList,
+ PlayerData,
+ PlayerDataList,
+ Transform,
+ World
+} from '$lib/schema';
import { publicGroup } from '$lib/utils.svelte';
import type { EntityIdStr } from '@muni-town/leaf';
import type { Loaded } from 'jazz-tools';
@@ -9,6 +17,7 @@ import { toast } from '$lib/sonner';
export const editingState = $state({
selectedInstance: null as Loaded | null,
+ selectedCellId: null as string | null,
selectedModelId: null as string | null,
selectedModel: null as Model | null,
@@ -75,7 +84,13 @@ export function clickedOn(point: Vector3, isFloor?: boolean) {
}
const currentColor = (editingState.modelPickerColor ?? 0) + 1;
- const paths = getPathsForModel(editingState.selectedModel, undefined, currentColor.toString());
+ let variant = undefined;
+ if (editingState.selectedModel.variants) {
+ const randomVariant = Math.floor(Math.random() * editingState.selectedModel.variants?.length);
+ variant = editingState.selectedModel.variants[randomVariant];
+ }
+
+ const paths = getPathsForModel(editingState.selectedModel, variant, currentColor.toString());
const randomIndex = Math.floor(Math.random() * paths.length);
addInstance(
paths[randomIndex],
@@ -87,6 +102,14 @@ export function clickedOn(point: Vector3, isFloor?: boolean) {
);
}
+export const cellSize = 50;
+
+export function getCellId(position: { x: number; y: number; z: number }) {
+ const x = Math.floor(position.x / cellSize);
+ const z = Math.floor(position.z / cellSize);
+ return `${x},${z}`;
+}
+
export async function addInstance(
id: string,
model: string,
@@ -95,6 +118,8 @@ export async function addInstance(
variant?: string,
collision?: boolean
) {
+ if (!editingState.world) return;
+
const quaternion = new Quaternion();
quaternion.setFromAxisAngle(new Vector3(0, 1, 0), Math.random() * 2 * Math.PI);
const instance = Instance.create(
@@ -129,14 +154,85 @@ export async function addInstance(
{ owner: publicGroup() }
);
- editingState.world?.current?.instances?.push(instance);
+ // check if cell exists
+ const cellId = getCellId(position);
+ const cell = editingState.world?.current?.cells[cellId];
+ if (!cell) {
+ createCell(cellId);
+ }
+
+ cell.instances.push(instance);
+ // editingState.world?.current?.instances?.push(instance);
+}
+
+export function createCell(cellId: string) {
+ if (!editingState.world) return;
+
+ const cell = Cell.create(
+ {
+ instances: InstanceList.create([], publicGroup())
+ },
+ publicGroup()
+ );
+
+ editingState.world.current.cells[cellId] = cell;
+
+ return cell;
}
export async function deleteInstance(id: string) {
+ const cell = editingState.selectedCellId;
+ if (!cell) return;
+
// find voxel by id
- const instance = editingState.world?.current?.instances?.findIndex((v) => v?.id === id);
+ const instance = editingState.world?.current?.cells[cell]?.instances?.findIndex(
+ (v) => v?.id === id
+ );
console.log(instance);
if (instance === undefined || instance < 0) return;
- editingState.world?.current?.instances?.splice(instance, 1);
+ editingState.world?.current?.cells[cell]?.instances?.splice(instance, 1);
+}
+
+export function updatePlayerData(
+ playerId: string,
+ position: { x: number; y: number; z: number },
+ rotation: number
+) {
+ if (!editingState.world) return;
+
+ const players = editingState.world.current.players;
+
+ if (!players[playerId]) {
+ console.log('creating player data', playerId);
+ players[playerId] = PlayerData.create(
+ {
+ position: position,
+ rotation: rotation,
+ timestamp: Date.now(),
+ character: editingState.selectedCharacter ?? 'player',
+ velocity: {
+ x: 0,
+ y: 0,
+ z: 0
+ }
+ },
+ publicGroup()
+ );
+ } else {
+ console.log('updating player data', playerId);
+ players[playerId].position = {
+ x: position.x,
+ y: position.y,
+ z: position.z
+ };
+ players[playerId].rotation = rotation;
+ players[playerId].timestamp = Date.now();
+ players[playerId].character = editingState.selectedCharacter ?? 'player';
+ players[playerId].velocity = {
+ x: 0,
+ y: 0,
+ z: 0
+ };
+ }
}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index d57b3f7..e49c51d 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -3,10 +3,43 @@
import { createWorld } from '$lib/utils.svelte';
import { Badge, Button, Prose } from '@fuxui/base';
import { goto } from '$app/navigation';
-
+ import { Canvas } from '@threlte/core';
+ import { CineonToneMapping } from 'three';
+ import Scene from '$lib/world-editor/WorldEditorScene.svelte';
+ import { onMount } from 'svelte';
+ import { editingState } from '$lib/world-editor/state.svelte';
+ import { CoState } from 'jazz-svelte';
+ import { World as RapierWorld } from '@threlte/rapier';
+ import { World } from '$lib/schema';
+
let showText = $state(true);
+
+ onMount(() => {
+ editingState.worldId = 'co_zioLGB74w8coU8iQ6tab4EbZbCL';
+ editingState.world = new CoState(World, 'co_zioLGB74w8coU8iQ6tab4EbZbCL', {
+ resolve: {
+ cells: {
+ $each: {
+ instances: {
+ $each: true,
+ $onError: null
+ }
+ },
+ $onError: null
+ }
+ }
+ });
+ });
+
+
+
+
{#if showText}
@@ -31,10 +64,10 @@
Roomy Worlds
work in progress!
-
Roomy Worlds is a tool for creating and sharing small 3D worlds as a community.
+
Roomy Worlds is a tool for creating, sharing and editing 3D worlds as a community.
- Work in progress/Preview only: You can create new worlds/models, but might
+ Work in progress/Preview only: You can create new worlds/models, but they might
disappear/change unexpectedly with new updates. Lots of bugs too.