Character manager: asset uploads, per-ghost model/colour/opacity overrides, face+torso texture decals
This commit is contained in:
+28
-2
@@ -11,18 +11,40 @@ const { WebSocketServer } = require('ws');
|
||||
const state = require('./state');
|
||||
const auth = require('./auth');
|
||||
const Playlist = require('./playlist');
|
||||
const { Assets } = require('./uploads');
|
||||
|
||||
const PORT = process.env.PORT || 33044;
|
||||
const MODE = process.env.EXHIBIT_MODE || 'dev';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
app.use(express.json({ limit: '20mb' })); // base64 asset uploads
|
||||
|
||||
// ---------- public static ----------
|
||||
app.use(express.static(path.join(__dirname, '..', 'public')));
|
||||
app.use('/data/ghosts.json', express.static(path.join(__dirname, '..', 'data', 'ghosts.json')));
|
||||
app.use('/data/gradients.json', express.static(path.join(__dirname, '..', 'data', 'gradients.json')));
|
||||
|
||||
// ---------- uploaded assets (models + textures) ----------
|
||||
const assets = new Assets(state.liveDir);
|
||||
app.use('/assets', express.static(path.join(state.liveDir, 'assets'), { maxAge: '1h' }));
|
||||
|
||||
app.get('/api/assets', (req, res) => res.json(assets.list()));
|
||||
app.post('/api/assets', guard, (req, res) => {
|
||||
try { res.json(assets.save(req.body)); }
|
||||
catch (e) { res.status(400).json({ error: String(e.message || e) }); }
|
||||
});
|
||||
app.delete('/api/assets/:id', guard, (req, res) => {
|
||||
try { res.json(assets.remove(req.params.id)); }
|
||||
catch (e) { res.status(400).json({ error: String(e.message || e) }); }
|
||||
});
|
||||
|
||||
// ---------- character overrides ----------
|
||||
app.get('/api/characters', (req, res) => res.json(state.getCharacters()));
|
||||
app.put('/api/characters', guard, (req, res) => {
|
||||
try { res.json(state.putCharacters(req.body)); }
|
||||
catch (e) { res.status(400).json({ error: String(e.message || e) }); }
|
||||
});
|
||||
|
||||
// ---------- runtime info ----------
|
||||
app.get('/api/info', (req, res) => {
|
||||
res.json({ mode: MODE, authRequired: MODE === 'final', version: 2 });
|
||||
@@ -91,6 +113,7 @@ playlist.start();
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
ws.send(JSON.stringify({ type: 'scene', scene: state.getScene() }));
|
||||
ws.send(JSON.stringify({ type: 'characters', characters: state.getCharacters() }));
|
||||
ws.send(JSON.stringify({ type: 'active', ghosts: playlist.activeSnapshot() }));
|
||||
ws.on('message', (raw) => {
|
||||
let m; try { m = JSON.parse(raw); } catch { return; }
|
||||
@@ -99,7 +122,10 @@ wss.on('connection', (ws) => {
|
||||
});
|
||||
|
||||
// push scene updates to viewers when admin saves
|
||||
state.onChange(() => broadcast({ type: 'scene', scene: state.getScene() }));
|
||||
state.onChange(() => {
|
||||
broadcast({ type: 'scene', scene: state.getScene() });
|
||||
broadcast({ type: 'characters', characters: state.getCharacters() });
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Newbury Exhibit v2 [${MODE}] on :${PORT}`);
|
||||
|
||||
@@ -12,6 +12,7 @@ const LIVE = path.join(LIVE_DIR, 'scene.live.json');
|
||||
const PLAYLIST_LIVE = path.join(LIVE_DIR, 'playlist.live.json');
|
||||
const MODELS_LIVE = path.join(LIVE_DIR, 'models.live.json');
|
||||
const MODELS_SEED = path.join(DATA, 'models.json');
|
||||
const CHARS_LIVE = path.join(LIVE_DIR, 'characters.live.json');
|
||||
|
||||
fs.mkdirSync(LIVE_DIR, { recursive: true });
|
||||
|
||||
@@ -26,6 +27,10 @@ let scene = readJSON(LIVE, null) || readJSON(SEED, { anchors: [], buildings: [],
|
||||
let ghosts = readJSON(path.join(DATA, 'ghosts.json'), []);
|
||||
let gradients = readJSON(path.join(DATA, 'gradients.json'), {});
|
||||
let models = readJSON(MODELS_LIVE, null) || readJSON(MODELS_SEED, { models: [] });
|
||||
/* characters: per-ghost visual overrides keyed by ghost id, plus defaults.
|
||||
* { defaults: {...}, byId: { 'mad-fenton': { modelId, opacity, topColor, bottomColor,
|
||||
* faceTextureUrl, torsoTextureUrl, heightCm, scale } } } */
|
||||
let characters = readJSON(CHARS_LIVE, null) || { defaults: {}, byId: {} };
|
||||
|
||||
const listeners = [];
|
||||
|
||||
@@ -71,6 +76,15 @@ module.exports = {
|
||||
return scene;
|
||||
},
|
||||
getGhosts: () => ({ ghosts, gradients }),
|
||||
getCharacters: () => characters,
|
||||
putCharacters(c) {
|
||||
if (!c || typeof c !== 'object') throw new Error('characters must be object');
|
||||
characters = { defaults: c.defaults || {}, byId: c.byId || {} };
|
||||
writeJSON(CHARS_LIVE, characters);
|
||||
listeners.forEach(f => f());
|
||||
return characters;
|
||||
},
|
||||
liveDir: LIVE_DIR,
|
||||
getModels: () => models,
|
||||
putModels(m) {
|
||||
if (!m || !Array.isArray(m.models)) throw new Error('models.models must be array');
|
||||
|
||||
Reference in New Issue
Block a user