feat(admin-configurator): add web configurator, visual level editor, audit logs and session management
This commit is contained in:
@@ -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