Initial commit: Game Maker phase 1
Canvas-only 2D game editor/runtime built on PixiJS v8 + Vite + TypeScript. - core: asset model (Scene/Camera/Sprite), scene objects with sparse overrides, property schema + resolve, command stack (undo/redo), serialization and project store interface (16 unit tests) - ui: custom in-canvas widget toolkit (panels, fields, color picker, list/scroll, popup) with a hidden <input> keyboard bridge - editor: asset/hierarchy panels, schema-driven inspector, toolbar - viewport: box rendering, camera frame, selection gizmo, drag/zoom/pan - runtime: world/entity instantiation for play mode, input, camera follow - api: PHP JSON project store with revision counter (409 on conflict) - tools: CDP-based e2e smoke test Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
5a46124454
65 files changed
+7193
No files matched your search
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Game Maker project API.
|
||||
*
|
||||
* Routing uses the `r` query parameter (e.g. `index.php?r=projects/abc123`) because the
|
||||
* site's nginx configuration does not pass PATH_INFO to PHP.
|
||||
*
|
||||
* GET ?r=projects → [{id,name,revision,updatedAt}]
|
||||
* POST ?r=projects {name} → {id,name,revision,updatedAt,project}
|
||||
* GET ?r=projects/{id} → {id,name,revision,updatedAt,project}
|
||||
* PUT ?r=projects/{id} {revision, project} → {revision,updatedAt} | 409 {error,revision}
|
||||
* DELETE ?r=projects/{id} → 204
|
||||
*/
|
||||
|
||||
require __DIR__ . '/lib/Response.php';
|
||||
require __DIR__ . '/lib/Store.php';
|
||||
|
||||
const MAX_BODY_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
set_error_handler(static function (int $severity, string $message, string $file, int $line): bool {
|
||||
throw new ErrorException($message, 0, $severity, $file, $line);
|
||||
});
|
||||
|
||||
try {
|
||||
if ((int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > MAX_BODY_BYTES) {
|
||||
Response::error('Request body too large', 413);
|
||||
}
|
||||
|
||||
$store = new Store(dirname(__DIR__) . '/data/projects');
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
$route = trim((string) ($_GET['r'] ?? ''), '/');
|
||||
$parts = $route === '' ? [] : explode('/', $route);
|
||||
|
||||
if (($parts[0] ?? '') !== 'projects' || count($parts) > 2) {
|
||||
Response::error('Not found', 404);
|
||||
}
|
||||
|
||||
// Collection: /projects
|
||||
if (count($parts) === 1) {
|
||||
if ($method === 'GET') {
|
||||
Response::json($store->list());
|
||||
}
|
||||
if ($method === 'POST') {
|
||||
$body = Response::body();
|
||||
$name = trim((string) ($body['name'] ?? ''));
|
||||
if ($name === '' || mb_strlen($name) > 120) {
|
||||
Response::error('Project name must be 1–120 characters', 422);
|
||||
}
|
||||
Response::json($store->create($name), 201);
|
||||
}
|
||||
Response::error('Method not allowed', 405);
|
||||
}
|
||||
|
||||
// Item: /projects/{id}
|
||||
$id = $parts[1];
|
||||
if (!Store::isValidId($id)) {
|
||||
Response::error('Invalid project id', 400);
|
||||
}
|
||||
|
||||
if ($method === 'GET') {
|
||||
$doc = $store->load($id);
|
||||
if ($doc === null) {
|
||||
Response::error('Project not found', 404);
|
||||
}
|
||||
Response::json($doc);
|
||||
}
|
||||
|
||||
if ($method === 'PUT') {
|
||||
$body = Response::body();
|
||||
if (!isset($body['revision']) || !is_int($body['revision']) || !is_array($body['project'] ?? null)) {
|
||||
Response::error('Body must contain integer "revision" and object "project"', 422);
|
||||
}
|
||||
try {
|
||||
$result = $store->save($id, $body['revision'], $body['project']);
|
||||
} catch (StaleRevisionException $e) {
|
||||
Response::error('Project was modified elsewhere', 409, ['revision' => $e->currentRevision]);
|
||||
}
|
||||
if ($result === null) {
|
||||
Response::error('Project not found', 404);
|
||||
}
|
||||
Response::json(['revision' => $result['revision'], 'updatedAt' => $result['updatedAt']]);
|
||||
}
|
||||
|
||||
if ($method === 'DELETE') {
|
||||
if (!$store->delete($id)) {
|
||||
Response::error('Project not found', 404);
|
||||
}
|
||||
Response::noContent();
|
||||
}
|
||||
|
||||
Response::error('Method not allowed', 405);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[game-maker api] ' . $e->getMessage());
|
||||
Response::error('Internal server error', 500);
|
||||
}
|
||||
Reference in new issue
Block a user