Initial commit — Godot space roguelite source

- Touch controls: direct InputEventScreenTouch in shop_ui (bypass relay)
- ItemDB: static preload list instead of DirAccess scan (export fix)
- All 18 items with EN localisation (name_en, desc_en, category_en)
- Ship playstyles: NOVA-1 shield, INFERNO ram, AURORA agile/tank
- Quasar: SMBH visual, jet boost, merge, push, BH-eating
- Atlas & UI text updated EN+DE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 14:38:09 +02:00
commit edc40f9008
108 changed files with 10068 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"name": "spacel-leaderboard",
"version": "1.0.0",
"description": "spacel online leaderboard server",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"better-sqlite3": "^11.0.0",
"express": "^4.19.0",
"express-rate-limit": "^7.3.0"
}
}
+100
View File
@@ -0,0 +1,100 @@
// spacel leaderboard server
// Setup: cd server && npm install && node server.js
// Config: set env var SPACEL_SECRET to the same value as HMAC_SECRET in online_leaderboard.gd
// set env var PORT to override default 3000
const express = require('express');
const Database = require('better-sqlite3');
const crypto = require('crypto');
const rateLimit = require('express-rate-limit');
const app = express();
const db = new Database(process.env.DB_PATH || 'scores.db');
const SECRET = process.env.SPACEL_SECRET || 'CHANGE-ME-BEFORE-DEPLOY';
const PORT = parseInt(process.env.PORT || '3000', 10);
db.exec(`
CREATE TABLE IF NOT EXISTS scores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
score INTEGER NOT NULL CHECK(score >= 0 AND score <= 500000),
wave INTEGER NOT NULL CHECK(wave >= 1 AND wave <= 100),
created_at TEXT NOT NULL
)
`);
app.use(express.json({ limit: '4kb' }));
const submitLimit = rateLimit({
windowMs: 10 * 60 * 1000,
max: 1,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Rate limit: 1 submission per 10 minutes per IP.' }
});
// GET /scores — top 10 by score
app.get('/scores', (_req, res) => {
const rows = db
.prepare('SELECT name, score, wave, created_at FROM scores ORDER BY score DESC LIMIT 10')
.all();
res.json(rows);
});
// POST /scores — submit a new score
// Body: { name, score, wave, timestamp, hmac }
// HMAC = SHA256(SECRET, name + score + wave + timestamp)
app.post('/scores', submitLimit, (req, res) => {
const { name, score, wave, timestamp, hmac } = req.body ?? {};
if (name == null || score == null || wave == null || timestamp == null || hmac == null)
return res.status(400).json({ error: 'Missing fields' });
if (typeof name !== 'string' ||
!Number.isInteger(score) ||
!Number.isInteger(wave) ||
!Number.isInteger(timestamp) ||
typeof hmac !== 'string')
return res.status(400).json({ error: 'Invalid field types' });
// Reject stale timestamps (±5 minutes)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300)
return res.status(400).json({ error: 'Timestamp expired' });
if (score < 0 || score > 500000)
return res.status(400).json({ error: 'Score out of range' });
if (wave < 1 || wave > 100)
return res.status(400).json({ error: 'Wave out of range' });
const cleanName = name.trim().slice(0, 12);
if (cleanName.length === 0)
return res.status(400).json({ error: 'Name required' });
// Verify HMAC — must match what online_leaderboard.gd sends
const raw = name + String(score) + String(wave) + String(timestamp);
const expected = crypto.createHmac('sha256', SECRET).update(raw).digest('hex');
let valid = false;
try {
// timingSafeEqual requires equal-length buffers; mismatch length → invalid
if (hmac.length === expected.length) {
valid = crypto.timingSafeEqual(
Buffer.from(hmac, 'hex'),
Buffer.from(expected, 'hex')
);
}
} catch { /* invalid hex */ }
if (!valid)
return res.status(403).json({ error: 'Invalid signature' });
db.prepare(
'INSERT INTO scores (name, score, wave, created_at) VALUES (?, ?, ?, ?)'
).run(cleanName, score, wave, new Date().toISOString().split('T')[0]);
res.json({ ok: true });
});
app.listen(PORT, () =>
console.log(`spacel leaderboard running on port ${PORT}`)
);