feat(admin-configurator): add web configurator, visual level editor, audit logs and session management
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* PvZ Dynamic Game Configuration & Balance System (PvZConfig)
|
||||
* Fetches active balance multipliers from /api/config with fallback defaults and offline localStorage cache.
|
||||
*/
|
||||
|
||||
window.PvZConfig = (function () {
|
||||
const DEFAULT_CONFIG = {
|
||||
preset: 'Classic 2009',
|
||||
economy: {
|
||||
sunMultiplier: 1.0,
|
||||
startingSun: 50,
|
||||
coinDropRate: 1.0,
|
||||
sunDropRate: 1.0,
|
||||
plantCostMultiplier: 1.0,
|
||||
upgradeCostMultiplier: 1.0,
|
||||
plantPrices: {
|
||||
sunflower: 0,
|
||||
peashooter: 0,
|
||||
wallnut: 0,
|
||||
potatomine: 150,
|
||||
snowpea: 350,
|
||||
repeater: 500,
|
||||
chomper: 450,
|
||||
cherrybomb: 400,
|
||||
jalapeno: 450,
|
||||
sunshroom: 250,
|
||||
fumeshroom: 300,
|
||||
lilypad: 200,
|
||||
gravebuster: 250,
|
||||
flowerpot: 200,
|
||||
cabbagepult: 400,
|
||||
melonpult: 750
|
||||
},
|
||||
plantSunCosts: {
|
||||
sunflower: 50,
|
||||
peashooter: 100,
|
||||
wallnut: 50,
|
||||
potatomine: 25,
|
||||
snowpea: 175,
|
||||
repeater: 200,
|
||||
chomper: 150,
|
||||
cherrybomb: 150,
|
||||
jalapeno: 125,
|
||||
sunshroom: 25,
|
||||
fumeshroom: 75,
|
||||
lilypad: 25,
|
||||
gravebuster: 75,
|
||||
flowerpot: 25,
|
||||
cabbagepult: 100,
|
||||
melonpult: 300
|
||||
}
|
||||
},
|
||||
zombies: {
|
||||
hpMultiplier: 1.0,
|
||||
speedMultiplier: 1.0,
|
||||
damageMultiplier: 1.0,
|
||||
waveDensityMultiplier: 1.0,
|
||||
types: {
|
||||
normal: { hp: 200, speed: 22, damage: 30 },
|
||||
conehead: { hp: 300, speed: 22, damage: 30 },
|
||||
buckethead: { hp: 600, speed: 22, damage: 30 },
|
||||
newspaper: { hp: 300, speed: 18, damage: 30 },
|
||||
screendoor: { hp: 650, speed: 20, damage: 30 },
|
||||
football: { hp: 600, speed: 42, damage: 35 },
|
||||
dancing: { hp: 350, speed: 25, damage: 30 },
|
||||
gargantuar: { hp: 3000, speed: 14, damage: 300 },
|
||||
zomboss: { hp: 10000, speed: 0, damage: 500 },
|
||||
polevaulter: { hp: 180, speed: 44, damage: 30 },
|
||||
duckytube: { hp: 120, speed: 24, damage: 30 },
|
||||
flag: { hp: 200, speed: 28, damage: 30 }
|
||||
}
|
||||
},
|
||||
plants: {
|
||||
damageMultiplier: 1.0,
|
||||
hpMultiplier: 1.0,
|
||||
cooldownMultiplier: 1.0,
|
||||
sunProduceIntervalMultiplier: 1.0
|
||||
},
|
||||
zenGarden: {
|
||||
dropRateMultiplier: 1.0,
|
||||
growthRateMultiplier: 1.0,
|
||||
fertilizerMultiplier: 1.0,
|
||||
waterIntervalMinutes: 30
|
||||
},
|
||||
events: {
|
||||
doubleGold: false,
|
||||
fastCooldowns: false,
|
||||
infiniteSun: false,
|
||||
turboZombies: false,
|
||||
activeEventName: 'none'
|
||||
}
|
||||
};
|
||||
|
||||
let activeConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
||||
|
||||
// Try loading cached config from localStorage
|
||||
try {
|
||||
const cached = localStorage.getItem('pvz_game_config');
|
||||
if (cached) {
|
||||
const parsed = JSON.parse(cached);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
activeConfig = Object.assign({}, DEFAULT_CONFIG, parsed);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
async function fetchConfig() {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 2500);
|
||||
const res = await fetch('/api/config', { signal: controller.signal });
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data && typeof data === 'object') {
|
||||
activeConfig = Object.assign({}, DEFAULT_CONFIG, data);
|
||||
try {
|
||||
localStorage.setItem('pvz_game_config', JSON.stringify(activeConfig));
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[PvZConfig] Using cached/default balance config:', err.message);
|
||||
}
|
||||
return activeConfig;
|
||||
}
|
||||
|
||||
// Auto-fetch on script load
|
||||
if (typeof window !== 'undefined') {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
return {
|
||||
fetchConfig,
|
||||
getConfig: () => activeConfig,
|
||||
getPreset: () => activeConfig.preset || 'Classic 2009',
|
||||
|
||||
// Economy
|
||||
getEconomy: () => activeConfig.economy || DEFAULT_CONFIG.economy,
|
||||
getStartingSun: () => {
|
||||
const eco = activeConfig.economy || {};
|
||||
const base = typeof eco.startingSun === 'number' ? eco.startingSun : 50;
|
||||
return activeConfig.events?.infiniteSun ? Math.max(base, 250) : base;
|
||||
},
|
||||
getSunMultiplier: () => {
|
||||
const eco = activeConfig.economy || {};
|
||||
const mult = typeof eco.sunMultiplier === 'number' ? eco.sunMultiplier : 1.0;
|
||||
return activeConfig.events?.infiniteSun ? mult * 1.5 : mult;
|
||||
},
|
||||
getCoinDropRate: () => {
|
||||
const eco = activeConfig.economy || {};
|
||||
const mult = typeof eco.coinDropRate === 'number' ? eco.coinDropRate : 1.0;
|
||||
return activeConfig.events?.doubleGold ? mult * 2.0 : mult;
|
||||
},
|
||||
getSunDropRate: () => activeConfig.economy?.sunDropRate ?? 1.0,
|
||||
getPlantShopPrice: (plantId) => {
|
||||
const eco = activeConfig.economy || {};
|
||||
const prices = eco.plantPrices || {};
|
||||
const base = prices[plantId] ?? (plantId === 'sunflower' || plantId === 'peashooter' || plantId === 'wallnut' ? 0 : 250);
|
||||
const mult = eco.plantCostMultiplier ?? 1.0;
|
||||
return Math.round(base * mult);
|
||||
},
|
||||
getPlantSunCost: (plantId, defaultCost = 50) => {
|
||||
const eco = activeConfig.economy || {};
|
||||
const sunCosts = eco.plantSunCosts || {};
|
||||
return sunCosts[plantId] !== undefined ? sunCosts[plantId] : defaultCost;
|
||||
},
|
||||
|
||||
// Zombies & Combat
|
||||
getZombieGlobal: () => activeConfig.zombies || DEFAULT_CONFIG.zombies,
|
||||
getZombieHpMult: () => activeConfig.zombies?.hpMultiplier ?? 1.0,
|
||||
getZombieSpeedMult: () => {
|
||||
const mult = activeConfig.zombies?.speedMultiplier ?? 1.0;
|
||||
return activeConfig.events?.turboZombies ? mult * 1.3 : mult;
|
||||
},
|
||||
getZombieDamageMult: () => activeConfig.zombies?.damageMultiplier ?? 1.0,
|
||||
getWaveDensityMult: () => activeConfig.zombies?.waveDensityMultiplier ?? 1.0,
|
||||
getZombieTypeConfig: (type) => {
|
||||
const types = activeConfig.zombies?.types || {};
|
||||
return types[type] || DEFAULT_CONFIG.zombies.types[type] || { hp: 200, speed: 22, damage: 30 };
|
||||
},
|
||||
|
||||
// Plants
|
||||
getPlantGlobal: () => activeConfig.plants || DEFAULT_CONFIG.plants,
|
||||
getPlantDamageMult: () => activeConfig.plants?.damageMultiplier ?? 1.0,
|
||||
getPlantHpMult: () => activeConfig.plants?.hpMultiplier ?? 1.0,
|
||||
getPlantCooldownMult: () => {
|
||||
const mult = activeConfig.plants?.cooldownMultiplier ?? 1.0;
|
||||
return activeConfig.events?.fastCooldowns ? mult * 0.4 : mult;
|
||||
},
|
||||
getSunIntervalMult: () => activeConfig.plants?.sunProduceIntervalMultiplier ?? 1.0,
|
||||
|
||||
// Zen Garden
|
||||
getZenGarden: () => activeConfig.zenGarden || DEFAULT_CONFIG.zenGarden,
|
||||
getZenDropRateMult: () => {
|
||||
const mult = activeConfig.zenGarden?.dropRateMultiplier ?? 1.0;
|
||||
return activeConfig.events?.doubleGold ? mult * 2.0 : mult;
|
||||
},
|
||||
getZenGrowthRateMult: () => activeConfig.zenGarden?.growthRateMultiplier ?? 1.0,
|
||||
getZenFertilizerMult: () => activeConfig.zenGarden?.fertilizerMultiplier ?? 1.0,
|
||||
getZenWaterIntervalMinutes: () => activeConfig.zenGarden?.waterIntervalMinutes ?? 30,
|
||||
|
||||
// Events
|
||||
getEvents: () => activeConfig.events || DEFAULT_CONFIG.events
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user