Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [ "main", "dev" ]
pull_request:
branches: [ "main", "dev" ]
workflow_dispatch:

jobs:
build:
Expand Down Expand Up @@ -64,3 +65,59 @@ jobs:
timeout-minutes: 1
run: npx concurrently --kill-others --success first "npm run dev" "npx wait-on http://localhost:8050/api/game/health --timeout 20000"
working-directory: ./packages/multiplayer-template


e2e:
needs: build
runs-on: ubuntu-latest
container: mcr.microsoft.com/playwright:v1.52.0-noble

permissions:
contents: read

steps:
- uses: actions/checkout@v4
- name: Use Node.js 24.x
uses: actions/setup-node@v4
with:
node-version: 24.x
cache: 'npm'
cache-dependency-path: |
package-lock.json
packages/bootstrap/package-lock.json
packages/multiplayer-template/package-lock.json

- name: Install dependencies
run: npm ci

- name: Install multiplayer-template dependencies
run: npm ci
working-directory: ./packages/multiplayer-template

- name: npm link core locally
run: npm link
working-directory: ./packages/core

- name: Link core in editor
run: npm link @mavonengine/core
working-directory: ./packages/editor

- name: npm link editor locally
run: npm link
working-directory: ./packages/editor

- name: Link core and editor in multiplayer-template
run: npm link @mavonengine/core @mavonengine/editor
working-directory: ./packages/multiplayer-template

- name: Run editor e2e tests
run: npx playwright test ${{ github.event_name == 'workflow_dispatch' && '--update-snapshots' || '' }}
working-directory: ./packages/editor

- name: Upload updated snapshots
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: playwright-snapshots
path: packages/editor/e2e/editor.spec.ts-snapshots/
retention-days: 30
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ dist-ssr
.env
coverage
*.tsbuildinfo
test-results
playwright-report
packages/core/src/version.ts
64 changes: 64 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/core/src/Types/IEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default interface IEditor {
on(event: string, callback: (event?: any) => void): void
off(event: string, callback: (event?: any) => void): void
trigger(name: string, event?: any): void
ready: boolean
availableAssetCategories: string[]
activeAssetCategory: 'Object' | '_texture' | 'CubeTexture' | 'AudioBuffer' | 'Font'
setActiveAssetCategory(value: 'Object' | '_texture' | 'CubeTexture' | 'AudioBuffer' | 'Font'): void
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/Utils/LoadingScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default class LoadingScreen extends EventEmitter {
private overlay: Mesh<BufferGeometry, ShaderMaterial> | undefined

loaded = false
finished = false
progress = 0
duration = 2

Expand Down Expand Up @@ -73,8 +74,10 @@ export default class LoadingScreen extends EventEmitter {
Game.instance().uiRoot.style.opacity = `${progress}`

// Optional: remove overlay from scene after fade completes
if (progress >= 1) {
if (progress >= 1 && !this.finished) {
this.finished = true
Game.instance().scene.remove(this.overlay!)
this.trigger('finished')
}
}
}
Expand Down
103 changes: 103 additions & 0 deletions packages/core/tests/Utils/LoadingScreen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import EventEmitter from '../../src/Utils/EventEmitter'
import LoadingScreen from '../../src/Utils/LoadingScreen'

vi.mock('../../src/Particles/Shaders/LoadingScreen/Fragment.glsl', () => ({ default: '' }))
vi.mock('../../src/Particles/Shaders/LoadingScreen/Vertex.glsl', () => ({ default: '' }))

vi.mock('three', async () => {
const actual = await vi.importActual<typeof import('three')>('three')
return {
...actual,
PlaneGeometry: vi.fn(),
ShaderMaterial: vi.fn().mockImplementation(({ uniforms }: any) => ({ uniforms })),
Mesh: vi.fn().mockImplementation((_geo: any, material: any) => ({ material })),
}
})

const mockScene = { add: vi.fn(), remove: vi.fn() }
const mockUiRoot = { style: { opacity: '0', removeProperty: vi.fn() } }
const gameListeners: Record<string, (...args: any[]) => void> = {}
const mockGame = {
on: vi.fn((event: string, cb: (...args: any[]) => void) => { gameListeners[event] = cb }),
scene: mockScene,
uiRoot: mockUiRoot,
}

vi.mock('../../src/Game', () => ({
default: { instance: vi.fn(() => mockGame) },
}))

describe('loadingScreen', () => {
let resources: EventEmitter
let loadingBar: HTMLDivElement
let loadingScreen: LoadingScreen

beforeEach(() => {
vi.useFakeTimers()
vi.clearAllMocks()
Object.keys(gameListeners).forEach(k => delete gameListeners[k])

loadingBar = document.createElement('div')
loadingBar.id = 'loadingBar'
document.body.appendChild(loadingBar)

resources = new EventEmitter()
loadingScreen = new LoadingScreen(resources as any)
gameListeners.uiMounted?.()
})

afterEach(() => {
document.body.innerHTML = ''
vi.useRealTimers()
})

it('does nothing in update() before resources are loaded', () => {
loadingScreen.update(0.016)
expect(mockScene.remove).not.toHaveBeenCalled()
expect(loadingScreen.progress).toBe(0)
})

it('sets loaded=true when resources emit loaded', () => {
resources.trigger('loaded')
expect(loadingScreen.loaded).toBe(true)
})

it('updates loadingBar transform on progress event', () => {
resources.trigger('progress', { loaded: 1, total: 4 })
expect(loadingBar.style.transform).toBe('scaleX(0.25)')
})

it('adds ended class and removes visibility after 500ms on loaded', () => {
resources.trigger('loaded')
vi.advanceTimersByTime(500)
expect(loadingBar.classList.contains('ended')).toBe(true)
expect(mockUiRoot.style.removeProperty).toHaveBeenCalledWith('visibility')
})

it('fades the overlay during update() after loading', () => {
resources.trigger('loaded')
loadingScreen.update(1) // 1s into a 2s fade
expect(loadingScreen.progress).toBeGreaterThan(0)
expect(loadingScreen.progress).toBeLessThan(1)
})

it('emits finished, sets finished flag, removes overlay, and zeroes progress when fade completes', () => {
resources.trigger('loaded')
const onFinished = vi.fn()
loadingScreen.on('finished', onFinished)
loadingScreen.update(3) // past the 2s duration
expect(onFinished).toHaveBeenCalledOnce()
expect(loadingScreen.finished).toBe(true)
expect(mockScene.remove).toHaveBeenCalled()
expect(loadingScreen.progress).toBe(0)
})

it('does not emit finished or remove overlay more than once', () => {
resources.trigger('loaded')
loadingScreen.update(3)
loadingScreen.update(1) // extra update after completion
expect(mockScene.remove).toHaveBeenCalledOnce()
})
})
39 changes: 39 additions & 0 deletions packages/editor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# @mavonengine/editor

**Early WIP** — map saving and loading are not yet implemented. Currently useful mainly for browsing and viewing assets in a 3D scene.

An in-engine editor for [MavonEngine](https://mavonengine.com) — a [threejs game engine](https://mavonengine.com) — that provides a 3D scene editing interface. It can be activated at runtime by pressing `Insert` or `.` (Period), which boots the editor, destroys the current world, and mounts a React-based UI overlay.

Current features include:
- Fly camera controls for scene navigation
- Object selection and transform controls (translate, rotate, scale)
- Shade modes: solid, flat, and wireframe
- Light and object helpers
- Asset browser with category filtering
- Primitives and lights available for placement in the scene

## Testing

### E2E Tests

E2E tests **must** be run via the official Playwright Docker image. Running them directly on your local system will cause snapshot diffs to fail due to rendering differences between environments (fonts, etc.).

```bash
docker run --rm \
-v $(pwd):/app \
-w /app/packages/editor \
mcr.microsoft.com/playwright:v1.52.0-noble \
npm run test:e2e
```

### Updating Snapshots

To update snapshots, pass the `--update-snapshots` flag via the same Docker container:

```bash
docker run --rm \
-v $(pwd):/app \
-w /app/packages/editor \
mcr.microsoft.com/playwright:v1.52.0-noble \
npm run test:e2e -- --update-snapshots
```
Loading
Loading