mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-22 00:28:34 +02:00
Wownerogue
A browser-based roguelike synchronized with crypto block timing. A run is a race against the next block: reach the exit before the block lands, or the house wins. The same codebase runs the solo dungeon, the social Tavern, and operator-selected PvP and race rulesets, with separate free and paid leaderboards. Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md for the design, and docs/DEPLOY.md to operate an instance.
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
function loadRenderKit() {
|
||||
const context = {
|
||||
console,
|
||||
window: {
|
||||
console,
|
||||
localStorage: {
|
||||
getItem: () => null,
|
||||
setItem: () => {}
|
||||
}
|
||||
}
|
||||
};
|
||||
context.window.window = context.window;
|
||||
|
||||
vm.runInNewContext(
|
||||
fs.readFileSync(path.join(__dirname, '../html/js/render/assetPacks.js'), 'utf8'),
|
||||
context
|
||||
);
|
||||
|
||||
const RK = context.window.RK;
|
||||
RK.normalizeAppearance = input => {
|
||||
if (input && input.appearance && !input.avatar) input = input.appearance;
|
||||
if (typeof input === 'string') input = { avatar: input };
|
||||
input = input || {};
|
||||
return {
|
||||
avatar: input.avatar || 'default',
|
||||
tint: input.tint || 'none',
|
||||
equipment: input.equipment || { body: 'none', head: 'none', shield: 'none', weapon: 'none' },
|
||||
colors: input.colors
|
||||
};
|
||||
};
|
||||
RK.appearance = input => ({ label: (input && input.avatar) || 'Character' });
|
||||
RK.SKINS = {
|
||||
'monero-knight': { id: 'monero-knight', label: 'Monero Knight', pack: 'generated-skins' }
|
||||
};
|
||||
RK.isSkin = id => !!RK.SKINS[id];
|
||||
RK.CHAR_TINTS = {
|
||||
none: { id: 'none', label: 'Natural', color: null },
|
||||
gold: { id: 'gold', label: 'Gold', color: '#d29922' }
|
||||
};
|
||||
RK.CHARS = { 'char-ranger': { id: 'char-ranger', label: 'Ranger', frame: 432 } };
|
||||
RK.isChar = id => !!RK.CHARS[id];
|
||||
|
||||
vm.runInNewContext(
|
||||
fs.readFileSync(path.join(__dirname, '../html/js/render/avatarVisuals.js'), 'utf8'),
|
||||
context
|
||||
);
|
||||
|
||||
RK.TileRenderer = function TileRenderer() { this.name = 'tiles'; };
|
||||
RK.IsoRenderer = function IsoRenderer() { this.name = 'iso'; };
|
||||
RK.ThreeRenderer = function ThreeRenderer() { this.name = '3d'; };
|
||||
RK.THREE = { THREE: {}, GLTFLoader: function GLTFLoader() {} };
|
||||
vm.runInNewContext(
|
||||
fs.readFileSync(path.join(__dirname, '../html/js/render/renderModes.js'), 'utf8'),
|
||||
context
|
||||
);
|
||||
RK.RENDER_MODE_TEST_UNLOCKS = false;
|
||||
|
||||
return RK;
|
||||
}
|
||||
|
||||
describe('render pack visual resolver', () => {
|
||||
test('resolves iso and 3D visuals through pack entitlements', () => {
|
||||
const RK = loadRenderKit();
|
||||
RK.setEntitlementSnapshot({ totalCreditsPurchased: 0, premium: false, packs: {} });
|
||||
|
||||
const locked3d = RK.avatarVisuals.resolve(
|
||||
{ avatar: 'kenney-survivor-male' },
|
||||
{ projection: '3d' }
|
||||
);
|
||||
expect(locked3d).toMatchObject({
|
||||
projection: '3d',
|
||||
kind: 'model3d',
|
||||
pack: 'kenney-3d-characters',
|
||||
allowed: false
|
||||
});
|
||||
expect(locked3d.model.url).toContain('survivorMaleB.glb');
|
||||
|
||||
RK.setEntitlementSnapshot({ totalCreditsPurchased: 1 });
|
||||
const iso = RK.avatarVisuals.resolve({ avatar: 'char-ranger' }, { projection: 'iso' });
|
||||
expect(iso).toMatchObject({ projection: 'iso', kind: 'iso', pack: 'iso-dungeon', allowed: true });
|
||||
expect(iso.character.idle).toContain('Male_3_Idle0.png');
|
||||
|
||||
const compat = RK.resolveAppearance(
|
||||
{ appearance: { avatar: 'kenney-survivor-female' } },
|
||||
'3d'
|
||||
);
|
||||
expect(compat.model.url).toContain('survivorFemaleA.glb');
|
||||
});
|
||||
|
||||
test('iso floor uses a flat tile instead of a raised block tile', () => {
|
||||
const RK = loadRenderKit();
|
||||
expect(RK.isoAssets.tiles.floor).toContain('planks_S.png');
|
||||
expect(RK.isoAssets.tiles.floor).not.toContain('stone_S.png');
|
||||
expect(RK.isoAssets.tiles.wall).toContain('stoneWall_S.png'); // full wall from the rich set
|
||||
expect(RK.isoAssets.tiles.window).toContain('stoneWallWindow_S.png'); // real iso window now
|
||||
expect(RK.isoAssets.tiles.table).toContain('tableShortChairs_S.png'); // distinct furniture art
|
||||
});
|
||||
|
||||
test('iso pack treats imported Male files as directions, not class variants', () => {
|
||||
const RK = loadRenderKit();
|
||||
expect(RK.isoAssets.avatar).toBe('char-villager');
|
||||
expect(RK.isoAssets.characters['char-villager'].idle).toContain('Male_3_Idle0.png');
|
||||
expect(RK.isoAssets.characters['char-merchant']).toBeUndefined();
|
||||
expect(RK.isoAssets.characters['char-wizard']).toBeUndefined();
|
||||
expect(RK.isoAssets.characters['char-goblin']).toBeUndefined();
|
||||
// Sprites are angled clockwise from NE: 0=NE,2=SE,4=SW,6=NW (verified against live movement).
|
||||
// Facing follows visual movement (up->NE, down->SW, left->NW, right->SE).
|
||||
expect(RK.isoAssets.directions.up.idle).toContain('Male_0_Idle0.png');
|
||||
expect(RK.isoAssets.directions.right.idle).toContain('Male_2_Idle0.png');
|
||||
expect(RK.isoAssets.directions.down.idle).toContain('Male_4_Idle0.png');
|
||||
expect(RK.isoAssets.directions.left.idle).toContain('Male_6_Idle0.png');
|
||||
});
|
||||
|
||||
test('projection visuals expose the saved base tint as a render color', () => {
|
||||
const RK = loadRenderKit();
|
||||
const visual = RK.avatarVisuals.resolve(
|
||||
{ avatar: 'char-ranger', colors: { base: 'gold' }, tint: 'gold' },
|
||||
{ projection: 'iso' }
|
||||
);
|
||||
expect(RK.avatarVisuals.tintColorFor(visual.appearance)).toBe('#d29922');
|
||||
});
|
||||
|
||||
test('renderer factory falls back when premium modes are locked', () => {
|
||||
const RK = loadRenderKit();
|
||||
RK.setEntitlementSnapshot({ totalCreditsPurchased: 0, premium: false, packs: {} });
|
||||
|
||||
expect(RK.canUseMode('iso')).toBe(false);
|
||||
expect(RK.createRenderer('iso', {}, {}).name).toBe('tiles');
|
||||
|
||||
RK.setEntitlementSnapshot({ totalCreditsPurchased: 1 });
|
||||
expect(RK.canUseMode('iso')).toBe(true);
|
||||
expect(RK.createRenderer('iso', {}, {}).name).toBe('iso');
|
||||
});
|
||||
|
||||
test('temporary render test unlocks bypass all premium gates for QA', () => {
|
||||
const RK = loadRenderKit();
|
||||
RK.setEntitlementSnapshot({ totalCreditsPurchased: 0, premium: false, packs: {} });
|
||||
|
||||
// With test unlocks off, premium modes and packs are gated by entitlements
|
||||
expect(RK.canUseMode('iso')).toBe(false);
|
||||
expect(RK.canUsePack('iso-dungeon')).toBe(false);
|
||||
|
||||
// With test unlocks on, ALL premium modes and packs are available for QA
|
||||
RK.RENDER_MODE_TEST_UNLOCKS = true;
|
||||
expect(RK.canUseMode('iso')).toBe(true); // 'fancy' is no longer a mode; it's a topdown pack
|
||||
expect(RK.canUseMode('3d')).toBe(true);
|
||||
expect(RK.canUsePack('iso-dungeon')).toBe(true);
|
||||
expect(RK.canUsePack('kenney-3d-characters')).toBe(true);
|
||||
expect(RK.canUsePack('generated-skins')).toBe(true);
|
||||
|
||||
// Free modes and packs are always available regardless
|
||||
RK.RENDER_MODE_TEST_UNLOCKS = false;
|
||||
expect(RK.canUseMode('tiles')).toBe(true);
|
||||
expect(RK.canUseMode('ascii')).toBe(true); // ASCII is the free accessible fallback
|
||||
});
|
||||
|
||||
test('render pack code stays out of payment and account APIs', () => {
|
||||
const files = [
|
||||
'../html/js/render/assetPacks.js',
|
||||
'../html/js/render/avatarVisuals.js',
|
||||
'../html/js/render/renderModes.js',
|
||||
'../html/js/render/isoRenderer.js',
|
||||
'../html/js/render/threeRenderer.js',
|
||||
'../html/js/render/tileRenderer.js'
|
||||
];
|
||||
const source = files.map(file => fs.readFileSync(path.join(__dirname, file), 'utf8')).join('\n');
|
||||
[
|
||||
'PaymentUI',
|
||||
'request_payment',
|
||||
'payment_',
|
||||
'wallet',
|
||||
'subaddress',
|
||||
'/api/',
|
||||
'user_id',
|
||||
'socket.emit'
|
||||
].forEach(forbidden => {
|
||||
expect(source).not.toContain(forbidden);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('ASCII is the free accessible fallback, not premium-gated', () => {
|
||||
const RK = loadRenderKit();
|
||||
RK.setEntitlementSnapshot({ totalCreditsPurchased: 0, premium: false, packs: {} });
|
||||
RK.RENDER_MODE_TEST_UNLOCKS = false;
|
||||
|
||||
const asciiMeta = RK.modeMeta('ascii');
|
||||
expect(asciiMeta.premium).toBe(false);
|
||||
expect(RK.canUseMode('ascii')).toBe(true);
|
||||
});
|
||||
|
||||
test('render mode test unlocks default to false in production', () => {
|
||||
const RK = loadRenderKit();
|
||||
expect(RK.RENDER_MODE_TEST_UNLOCKS).toBe(false);
|
||||
});
|
||||
Reference in New Issue
Block a user