Files
wownerogue/html/js/render/assetPacks.js
T

283 lines
11 KiB
JavaScript

// Themes / packs / entitlement policy for the render kit. Packs are resolved by capability
// (top-down, isometric, 3D) so the same `appearance` identity can render through different
// visual projections without changing Room state or join flow.
(function (root) {
'use strict';
var RK = root.RK = root.RK || {};
RK.TILE_KINDS = ['floor', 'wall', 'bar', 'table'];
var FREE_EQUIPMENT = { body: 'none', head: 'none', shield: 'none', weapon: 'none' };
var FREE_COLORS = { base: 'none', skin: 'natural', hair: 'copper', body: 'none', head: 'none', shield: 'none', weapon: 'none' };
RK.PACKS = {
'roguelike-interior': {
id: 'roguelike-interior',
label: 'Kenney Roguelike Interior',
premium: false,
projection: 'topdown'
},
'iso-dungeon': {
id: 'iso-dungeon',
label: 'Kenney Isometric Dungeon',
premium: true,
projection: 'iso',
unlock: { kind: 'credits_purchase', minTotalCreditsPurchased: 1 }
},
'kenney-3d-characters': {
id: 'kenney-3d-characters',
label: 'Kenney Animated 3D Characters',
premium: true,
projection: '3d',
unlock: { kind: 'credits_purchase', minTotalCreditsPurchased: 1 }
},
'generated-skins': {
id: 'generated-skins',
label: 'Premium Generated Skins',
premium: true,
projection: 'topdown',
unlock: { kind: 'credits_purchase', minTotalCreditsPurchased: 1 }
}
};
RK.entitlements = RK.entitlements || { premium: false, level: 'free', packs: {}, credits: 0, totalCreditsPurchased: 0 };
RK.setEntitlementSnapshot = function (data) {
data = data || {};
var prev = RK.entitlements || {};
var hasTotal = data.totalCreditsPurchased != null || data.total_credits_purchased != null;
var hasPremium = data.premium != null;
var hasPacks = !!data.packs;
var hasCredits = data.credits != null || data.balance != null;
var total = hasTotal
? Number(data.totalCreditsPurchased || data.total_credits_purchased || 0)
: Number(prev.totalCreditsPurchased || 0);
var credits = hasCredits
? Number(data.credits != null ? data.credits : data.balance)
: Number(prev.credits || 0);
var premium = hasPremium ? !!data.premium : (hasTotal ? total > 0 : !!prev.premium);
var packs = {};
for (var id in RK.PACKS) {
var p = RK.PACKS[id];
if (!p.premium) {
packs[id] = true;
} else if (hasPacks && Object.prototype.hasOwnProperty.call(data.packs, id)) {
packs[id] = !!data.packs[id];
} else if (!hasTotal && !hasPacks && prev.packs && Object.prototype.hasOwnProperty.call(prev.packs, id)) {
packs[id] = !!prev.packs[id];
} else {
packs[id] = premium;
}
}
for (var pid in packs) {
if (packs[pid] && RK.PACKS[pid] && RK.PACKS[pid].premium) premium = true;
}
RK.entitlements = {
premium: premium,
level: data.level || (premium ? ((prev.level && prev.level !== 'free') ? prev.level : 'credits') : 'free'),
packs: packs,
credits: credits,
totalCreditsPurchased: total
};
try { localStorage.setItem('rk_entitlements', JSON.stringify(RK.entitlements)); } catch (_) {}
return RK.entitlements;
};
RK.loadEntitlements = function () {
try {
var raw = JSON.parse(localStorage.getItem('rk_entitlements') || 'null');
if (raw) return RK.setEntitlementSnapshot(raw);
} catch (_) {}
return RK.entitlements;
};
RK.loadEntitlements();
RK.pack = function (id) { return RK.PACKS[id] || null; };
RK.canUsePack = function (id) {
var p = RK.pack(id);
if (!p) return false;
if (!p.premium) return true;
if (RK.renderModeTestUnlocks && RK.renderModeTestUnlocks() && (p.projection === 'iso' || p.projection === '3d')) return true;
return !!(RK.entitlements && RK.entitlements.packs && RK.entitlements.packs[id]);
};
RK.canUseAppearance = function (a) {
if (!a || !a.premium) return true;
return a.pack ? RK.canUsePack(a.pack) : !!(RK.entitlements && RK.entitlements.premium);
};
RK.THEMES = {
'roguelike-interior': {
id: 'roguelike-interior',
label: 'Kenney Roguelike Interior',
tileset: { url: 'assets/kenney/roguelikeIndoor.png', tile: 16, spacing: 1 },
// Defaults are null until mapped via the tile-picker (localStorage overrides win).
tiles: { floor: null, wall: null, bar: null, table: null }
}
};
RK.activeThemeId = 'roguelike-interior';
RK.theme = function (id) { return RK.THEMES[id || RK.activeThemeId] || null; };
// Effective tile map = theme defaults merged with localStorage overrides from the picker.
RK.tileMap = function (id) {
var t = RK.theme(id); if (!t) return {};
var map = {};
for (var k in t.tiles) map[k] = t.tiles[k];
try {
var ov = JSON.parse(localStorage.getItem('rk_tilemap_' + t.id) || '{}');
for (var kk in ov) map[kk] = ov[kk];
} catch (_) { /* ignore */ }
return map;
};
RK.setTile = function (id, kind, col, row) {
var t = RK.theme(id); if (!t) return;
var key = 'rk_tilemap_' + t.id, ov = {};
try { ov = JSON.parse(localStorage.getItem(key) || '{}'); } catch (_) {}
ov[kind] = [col, row];
try { localStorage.setItem(key, JSON.stringify(ov)); } catch (_) {}
};
// Lazy tileset atlas per theme.
var atlases = {};
RK.themeAtlas = function (id) {
var t = RK.theme(id); if (!t || !t.tileset) return null;
if (!atlases[t.id]) {
atlases[t.id] = new RK.TilesetAtlas(t.tileset.url, { tile: t.tileset.tile, spacing: t.tileset.spacing });
}
return atlases[t.id];
};
RK.loadTheme = function (id, cb) {
var a = RK.themeAtlas(id);
if (!a) { if (cb) cb(null); return; }
a.onReady(function () { if (cb) cb(a); });
};
RK.EXTRA_APPEARANCES = {
'kenney-survivor-male': {
id: 'kenney-survivor-male',
label: 'Survivor',
kind: 'model3d',
premium: true,
pack: 'kenney-3d-characters'
},
'kenney-survivor-female': {
id: 'kenney-survivor-female',
label: 'Survivor Scout',
kind: 'model3d',
premium: true,
pack: 'kenney-3d-characters'
}
};
function normalizeEquipment(eq) {
eq = eq || {};
var out = {};
for (var k in FREE_EQUIPMENT) out[k] = eq[k] || 'none';
return out;
}
function normalizeColors(colors, tint) {
if (RK.normalizeCharColors) return RK.normalizeCharColors(colors, tint);
colors = colors || {};
var out = {};
for (var k in FREE_COLORS) out[k] = colors[k] || (k === 'base' ? (tint || FREE_COLORS[k]) : FREE_COLORS[k]);
return out;
}
RK.packAppearances = function () {
var out = [];
for (var id in RK.EXTRA_APPEARANCES) out.push(RK.EXTRA_APPEARANCES[id]);
return out;
};
RK.isoAssets = {
pack: 'iso-dungeon',
tile: { w: 84, h: 42, imageW: 92, imageH: 184 },
tiles: {
floor: 'assets/kenney/iso-dungeon/tiles/planks_S.png',
wall: 'assets/kenney/iso-dungeon/tiles/stoneWall_S.png',
bar: 'assets/kenney/iso-dungeon/tiles/barrelsStacked_S.png',
table: 'assets/kenney/iso-dungeon/tiles/chestClosed_S.png',
fallback: 'assets/kenney/iso-dungeon/tiles/planks_S.png'
},
character: {
idle: 'assets/kenney/iso-dungeon/characters/male0/Male_0_Idle0.png',
run: [
'assets/kenney/iso-dungeon/characters/male0/Male_0_Run0.png',
'assets/kenney/iso-dungeon/characters/male0/Male_0_Run1.png',
'assets/kenney/iso-dungeon/characters/male0/Male_0_Run2.png',
'assets/kenney/iso-dungeon/characters/male0/Male_0_Run3.png',
'assets/kenney/iso-dungeon/characters/male0/Male_0_Run4.png',
'assets/kenney/iso-dungeon/characters/male0/Male_0_Run5.png',
'assets/kenney/iso-dungeon/characters/male0/Male_0_Run6.png',
'assets/kenney/iso-dungeon/characters/male0/Male_0_Run7.png',
'assets/kenney/iso-dungeon/characters/male0/Male_0_Run8.png',
'assets/kenney/iso-dungeon/characters/male0/Male_0_Run9.png'
],
imageW: 74,
imageH: 148
}
};
RK.threeAssets = {
pack: 'kenney-3d-characters',
models: {
'kenney-survivor-male': {
url: 'assets/generated/3d/kenney-animated-characters/survivorMaleB.glb',
clips: { idle: 'idle', run: 'run', jump: 'jump' }
},
'kenney-survivor-female': {
url: 'assets/generated/3d/kenney-animated-characters/survivorFemaleA.glb',
clips: { idle: 'idle', run: 'run', jump: 'jump' }
},
fallback: {
url: 'assets/generated/3d/kenney-animated-characters/survivorMaleB.glb',
clips: { idle: 'idle', run: 'run', jump: 'jump' }
}
}
};
RK.packForProjection = function (projection) {
for (var id in RK.PACKS) {
if (RK.PACKS[id].projection === projection) return RK.PACKS[id];
}
return null;
};
RK.resolveAppearance = function (entity, projection) {
entity = entity || {};
var baseAppearance = entity.appearance || { avatar: entity.avatar || 'default', tint: 'none', equipment: FREE_EQUIPMENT };
if (RK.avatarVisuals && RK.avatarVisuals.resolve) {
return RK.avatarVisuals.resolve(baseAppearance, {
projection: projection || 'topdown',
context: 'room-entity',
entity: entity
});
}
var appearance = entity.appearance || { avatar: entity.avatar || 'default', tint: 'none', equipment: FREE_EQUIPMENT };
var avatar = appearance.avatar || entity.avatar || 'default';
if (projection === 'iso') {
return {
projection: 'iso',
pack: 'iso-dungeon',
avatar: avatar,
tint: appearance.tint || 'none',
equipment: normalizeEquipment(appearance.equipment),
colors: normalizeColors(appearance.colors, appearance.tint),
character: RK.isoAssets.character
};
}
if (projection === '3d') {
return {
projection: '3d',
pack: 'kenney-3d-characters',
avatar: avatar,
model: RK.threeAssets.models[avatar] || RK.threeAssets.models.fallback,
tint: appearance.tint || 'none',
equipment: normalizeEquipment(appearance.equipment),
colors: normalizeColors(appearance.colors, appearance.tint)
};
}
return { projection: 'topdown', avatar: avatar, appearance: appearance };
};
})(window);