feat: New theme + wire price adjustement
This commit is contained in:
@@ -59,6 +59,7 @@
|
||||
@media (min-width: 1200px) {
|
||||
.game {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, 1.2fr);
|
||||
grid-template-rows: auto auto 1fr; /* spare height goes below the left column, not between its panels */
|
||||
grid-template-areas:
|
||||
'stats stats stats'
|
||||
'production generators upgrades'
|
||||
|
||||
+14
-7
@@ -11,13 +11,20 @@
|
||||
"priceStep": 0.01
|
||||
},
|
||||
"materialMarket": {
|
||||
"batchSize": 100,
|
||||
"basePrice": 2,
|
||||
"minFactor": 0.5,
|
||||
"maxFactor": 2,
|
||||
"volatility": 0.12,
|
||||
"reversion": 0.05,
|
||||
"updateEverySeconds": 2
|
||||
"minBatch": 100,
|
||||
"batchShareOfTotal": 0.1,
|
||||
"batchSecondsOfProduction": 60,
|
||||
"startUnitPrice": 0.08,
|
||||
"floorUnitPrice": 0.01,
|
||||
"scale": 3000,
|
||||
"minFactor": 0.4,
|
||||
"maxFactor": 3,
|
||||
"volatility": 0.3,
|
||||
"reversion": 0.08,
|
||||
"spikeChance": 0.06,
|
||||
"spikeSize": 1.9,
|
||||
"updateEverySeconds": 1.5,
|
||||
"autoBuyBelowFactor": 1.2
|
||||
},
|
||||
"marketing": { "baseCost": 40, "costGrowth": 1.45, "demandPerLevel": 1.4 },
|
||||
"offline": { "maxSeconds": 28800, "efficiency": 0.5, "minSeconds": 10 },
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// browser and in Node (tests, balance simulations).
|
||||
|
||||
import { computeModifiers } from './effects.js';
|
||||
import { bulkCost, clamp, demandPerSecond, maxAffordable } from './formulas.js';
|
||||
import { bulkCost, clamp, demandPerSecond, materialBatch, maxAffordable } from './formulas.js';
|
||||
|
||||
const MAX_STEP_SECONDS = 1;
|
||||
|
||||
@@ -38,6 +38,13 @@ export function derive(state, ctx) {
|
||||
}
|
||||
|
||||
const demandMultiplier = mods.demand * rules.marketing.demandPerLevel ** state.marketing;
|
||||
const batch = materialBatch(
|
||||
rules.materialMarket,
|
||||
state.totalProduced,
|
||||
rate,
|
||||
state.materialFactor,
|
||||
mods.batchDiscount,
|
||||
);
|
||||
return {
|
||||
clickYield: rules.click.yield * mods.click,
|
||||
perGenerator,
|
||||
@@ -46,8 +53,8 @@ export function derive(state, ctx) {
|
||||
demand: demandPerSecond(rules.market, state.price, demandMultiplier),
|
||||
valueMultiplier: mods.value,
|
||||
materialPerUnit: rules.unit.materialPerUnit / mods.materialEfficiency,
|
||||
batchSize: rules.materialMarket.batchSize,
|
||||
batchCost: Math.ceil(rules.materialMarket.basePrice * state.materialFactor * mods.batchDiscount),
|
||||
batchSize: batch.size,
|
||||
batchCost: batch.cost,
|
||||
autoBuyMaterial: mods.autoBuyMaterial,
|
||||
marketingCost: Math.ceil(rules.marketing.baseCost * rules.marketing.costGrowth ** state.marketing),
|
||||
};
|
||||
@@ -121,9 +128,12 @@ function stepMaterialMarket(state, ctx, dt, rng) {
|
||||
state.materialTimer += dt;
|
||||
while (state.materialTimer >= m.updateEverySeconds) {
|
||||
state.materialTimer -= m.updateEverySeconds;
|
||||
const shock = 1 + (rng() - 0.5) * 2 * m.volatility;
|
||||
let shock = 1 + (rng() - 0.5) * 2 * m.volatility;
|
||||
if (rng() < m.spikeChance) shock *= rng() < 0.5 ? m.spikeSize : 1 / m.spikeSize; // shortage or glut
|
||||
const pulled = state.materialFactor + (1 - state.materialFactor) * m.reversion;
|
||||
state.materialFactor = clamp(pulled * shock, m.minFactor, m.maxFactor);
|
||||
const next = clamp(pulled * shock, m.minFactor, m.maxFactor);
|
||||
state.materialTrend = Math.sign(next - state.materialFactor);
|
||||
state.materialFactor = next;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,9 +147,11 @@ function step(state, ctx, dt, rng, totals) {
|
||||
state.units += made;
|
||||
state.totalProduced += made;
|
||||
|
||||
// Automatic restock when the stock runs low.
|
||||
if (d.autoBuyMaterial && state.material < d.batchSize / 2 && state.funds >= d.batchCost) {
|
||||
buyMaterial(state, ctx);
|
||||
// Automatic restock: patient while the price is high, urgent when nearly out.
|
||||
if (d.autoBuyMaterial && state.funds >= d.batchCost) {
|
||||
const { autoBuyBelowFactor } = ctx.rules.materialMarket;
|
||||
const low = state.material < d.batchSize / 2 && state.materialFactor <= autoBuyBelowFactor;
|
||||
if (low || state.material < d.batchSize / 10) buyMaterial(state, ctx);
|
||||
}
|
||||
|
||||
// Customers buy what they want, up to what is in stock.
|
||||
|
||||
@@ -25,6 +25,30 @@ export function maxAffordable(base, growth, owned, budget) {
|
||||
return Math.max(n, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw-material price per unit of material, before market noise. Expensive at
|
||||
* first, then cheaper relative to revenue as the operation scales
|
||||
* (economies of scale): it falls from `startUnitPrice` towards `floorUnitPrice`.
|
||||
*/
|
||||
export function materialUnitPrice(mm, totalProduced) {
|
||||
const scale = 1 / Math.sqrt(1 + totalProduced / mm.scale);
|
||||
return mm.floorUnitPrice + (mm.startUnitPrice - mm.floorUnitPrice) * scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* What one purchase of material looks like. The lot grows with the operation
|
||||
* (a share of everything produced so far, or `batchSecondsOfProduction` of
|
||||
* current output), in doubling steps so the price stays readable.
|
||||
* `factor` is the market's current noise, `discount` comes from upgrades.
|
||||
*/
|
||||
export function materialBatch(mm, totalProduced, rate, factor, discount) {
|
||||
const basis = Math.max(totalProduced * mm.batchShareOfTotal, rate * mm.batchSecondsOfProduction);
|
||||
const doublings = Math.max(0, Math.floor(Math.log2(Math.max(1, basis / mm.minBatch))));
|
||||
const size = mm.minBatch * 2 ** doublings;
|
||||
const cost = Math.max(1, Math.ceil(size * materialUnitPrice(mm, totalProduced) * factor * discount));
|
||||
return { size, cost };
|
||||
}
|
||||
|
||||
/**
|
||||
* Units customers want to buy per second at `price`.
|
||||
* Demand is maxed at or below the fair price and drops steeply above it, so
|
||||
|
||||
@@ -22,6 +22,7 @@ export function createState(ctx, now = Date.now()) {
|
||||
upgrades: [],
|
||||
savedAt: now,
|
||||
materialTimer: 0, // transient, never saved
|
||||
materialTrend: 0, // transient: -1 falling, 0 steady, 1 rising
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ export function sanitizeState(raw, ctx, now = Date.now()) {
|
||||
|
||||
/** Compact JSON (numbers trimmed to 10 significant digits) for cookies and export. */
|
||||
export function serializeState(state) {
|
||||
const { materialTimer, ...persisted } = state;
|
||||
const { materialTimer, materialTrend, ...persisted } = state;
|
||||
return JSON.stringify(persisted, (_key, value) =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? Number(value.toPrecision(10)) : value,
|
||||
);
|
||||
|
||||
+40
-20
@@ -16,7 +16,15 @@ import { fetchLocales, fetchThemes, loadGameContext } from './services/data.js';
|
||||
import { createFormatter } from './services/format.js';
|
||||
import { applyTranslations, fetchThemeName, FALLBACK_LOCALE, loadDictionary, t } from './services/i18n.js';
|
||||
import { buildExport, parseImport } from './services/saveio.js';
|
||||
import { cookiesAvailable, loadGameText, loadPrefs, saveGameText, savePrefs, deleteGame } from './services/storage.js';
|
||||
import {
|
||||
cookiesAvailable,
|
||||
deleteGame,
|
||||
loadGameText,
|
||||
loadPrefs,
|
||||
saveGameText,
|
||||
savePrefs,
|
||||
takeLegacySaves,
|
||||
} from './services/storage.js';
|
||||
import { applyThemeStyles } from './services/themes.js';
|
||||
import { $, refreshStaticIcons, setThemeAssets } from './ui/dom.js';
|
||||
import { createSettings } from './ui/settings.js';
|
||||
@@ -44,7 +52,7 @@ function persist() {
|
||||
if (!state) return;
|
||||
state.savedAt = Date.now();
|
||||
try {
|
||||
saveGameText(themeId, serializeState(state));
|
||||
saveGameText(serializeState(state));
|
||||
} catch {
|
||||
if (Date.now() - lastSaveError > SAVE_ERROR_COOLDOWN_MS) {
|
||||
lastSaveError = Date.now();
|
||||
@@ -53,9 +61,7 @@ function persist() {
|
||||
}
|
||||
}
|
||||
|
||||
function loadSavedState() {
|
||||
const text = loadGameText(themeId);
|
||||
if (!text) return null;
|
||||
function parseSaved(text) {
|
||||
try {
|
||||
return sanitizeState(JSON.parse(text), ctx);
|
||||
} catch {
|
||||
@@ -63,6 +69,19 @@ function loadSavedState() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The shared game from cookies. Falls back to the per-theme saves of the first
|
||||
* version: the most advanced one becomes the shared game, the others are dropped.
|
||||
*/
|
||||
function loadSavedState() {
|
||||
const text = loadGameText();
|
||||
if (text) return parseSaved(text);
|
||||
|
||||
const legacy = takeLegacySaves(themes.map((th) => th.id)).map(parseSaved).filter(Boolean);
|
||||
legacy.sort((a, b) => b.totalProduced - a.totalProduced);
|
||||
return legacy[0] ?? null;
|
||||
}
|
||||
|
||||
function persistPrefs() {
|
||||
savePrefs({ theme: themeId, lang: locale });
|
||||
}
|
||||
@@ -107,16 +126,13 @@ async function refreshTexts() {
|
||||
await fillSelects();
|
||||
}
|
||||
|
||||
async function enterTheme(id) {
|
||||
persist(); // keep the theme we are leaving
|
||||
/** Change the skin only: the game state is shared by every theme. */
|
||||
async function setTheme(id) {
|
||||
themeId = id;
|
||||
await applyThemeStyles(id);
|
||||
await refreshTexts();
|
||||
state = loadSavedState() ?? createState(ctx);
|
||||
income = 0;
|
||||
catchUp((Date.now() - state.savedAt) / 1000);
|
||||
persistPrefs();
|
||||
render();
|
||||
if (state) render();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ the loop
|
||||
@@ -181,18 +197,17 @@ const view = createView({
|
||||
|
||||
createSettings({
|
||||
getExport: () => ({
|
||||
text: buildExport(themeId, state),
|
||||
name: `idle-clicker-${themeId}-${new Date().toISOString().slice(0, 10)}`,
|
||||
text: buildExport(state),
|
||||
name: `idle-clicker-${new Date().toISOString().slice(0, 10)}`,
|
||||
}),
|
||||
async importText(text) {
|
||||
const imported = parseImport(text, ctx, themes.map((th) => th.id)); // throws before any change
|
||||
if (imported.themeId !== themeId) await enterTheme(imported.themeId);
|
||||
state = imported.state;
|
||||
importText(text) {
|
||||
state = parseImport(text, ctx); // throws before anything changes
|
||||
income = 0;
|
||||
persist();
|
||||
render();
|
||||
},
|
||||
reset() {
|
||||
deleteGame(themeId);
|
||||
deleteGame();
|
||||
state = createState(ctx);
|
||||
income = 0;
|
||||
persist();
|
||||
@@ -216,7 +231,7 @@ async function boot() {
|
||||
const initialTheme = themes.find((th) => th.id === prefs.theme) ?? themes[0];
|
||||
|
||||
$('#cookie-warning').hidden = cookiesAvailable();
|
||||
$('#theme-select').addEventListener('change', (e) => enterTheme(e.target.value));
|
||||
$('#theme-select').addEventListener('change', (e) => setTheme(e.target.value));
|
||||
$('#locale-select').addEventListener('change', async (e) => {
|
||||
locale = e.target.value;
|
||||
persistPrefs();
|
||||
@@ -224,9 +239,14 @@ async function boot() {
|
||||
render();
|
||||
});
|
||||
|
||||
await enterTheme(initialTheme.id);
|
||||
await setTheme(initialTheme.id);
|
||||
refreshStaticIcons();
|
||||
|
||||
state = loadSavedState() ?? createState(ctx);
|
||||
catchUp((Date.now() - state.savedAt) / 1000);
|
||||
persist();
|
||||
render();
|
||||
|
||||
lastFrame = Date.now();
|
||||
setInterval(frame, TICK_MS);
|
||||
setInterval(persist, ctx.rules.autosaveSeconds * 1000);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Export / import of a save as a JSON or TXT file (or pasted text).
|
||||
// Export / import of the save as a JSON or TXT file (or pasted text).
|
||||
// Works on every device: download via a Blob link, import via file input or paste.
|
||||
// The save is theme-independent: any export can be imported under any theme.
|
||||
|
||||
import { sanitizeState, serializeState } from '../engine/state.js';
|
||||
|
||||
@@ -7,7 +8,7 @@ const GAME_ID = 'idle-clicker';
|
||||
const FORMAT = 1;
|
||||
|
||||
export class ImportError extends Error {
|
||||
/** @param {'empty'|'invalid'|'unknown_theme'|'wrong_game'} code */
|
||||
/** @param {'empty'|'invalid'|'wrong_game'} code */
|
||||
constructor(code) {
|
||||
super(code);
|
||||
this.code = code;
|
||||
@@ -15,12 +16,11 @@ export class ImportError extends Error {
|
||||
}
|
||||
|
||||
/** The text a player downloads, copies or pastes back. */
|
||||
export function buildExport(themeId, state, now = Date.now()) {
|
||||
export function buildExport(state, now = Date.now()) {
|
||||
return JSON.stringify(
|
||||
{
|
||||
game: GAME_ID,
|
||||
format: FORMAT,
|
||||
theme: themeId,
|
||||
exportedAt: new Date(now).toISOString(),
|
||||
state: JSON.parse(serializeState(state)),
|
||||
},
|
||||
@@ -29,8 +29,8 @@ export function buildExport(themeId, state, now = Date.now()) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Validate imported text. Throws ImportError; never trusts the content. */
|
||||
export function parseImport(text, ctx, knownThemeIds) {
|
||||
/** Validate imported text and return a clean state. Throws ImportError; never trusts the content. */
|
||||
export function parseImport(text, ctx) {
|
||||
const trimmed = String(text ?? '').trim().replace(/^/, '');
|
||||
if (!trimmed) throw new ImportError('empty');
|
||||
|
||||
@@ -41,11 +41,10 @@ export function parseImport(text, ctx, knownThemeIds) {
|
||||
throw new ImportError('invalid');
|
||||
}
|
||||
if (data?.game !== GAME_ID || data.format !== FORMAT) throw new ImportError('wrong_game');
|
||||
if (!knownThemeIds.includes(data.theme)) throw new ImportError('unknown_theme');
|
||||
|
||||
const state = sanitizeState(data.state, ctx);
|
||||
if (!state) throw new ImportError('invalid');
|
||||
return { themeId: data.theme, state };
|
||||
return state;
|
||||
}
|
||||
|
||||
export function downloadText(filename, text, mime = 'application/json') {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
// Persistence in browser cookies.
|
||||
//
|
||||
// A cookie holds ~4 KB, so a save is base64url-encoded and split in chunks:
|
||||
// ic_save_<theme>_n -> number of chunks
|
||||
// ic_save_<theme>_0.. -> the chunks
|
||||
// Each theme has its own save slot. Preferences (theme, language) are stored
|
||||
// in a single small cookie.
|
||||
// ic_game_n -> number of chunks
|
||||
// ic_game_0.. -> the chunks
|
||||
// There is ONE game, shared by every theme (a theme is only a skin).
|
||||
// Preferences (theme, language) are stored in a single small cookie.
|
||||
|
||||
import { readCookie, removeCookie, writeCookie } from './cookies.js';
|
||||
|
||||
const CHUNK_SIZE = 3000;
|
||||
const MAX_CHUNKS = 6;
|
||||
const PREFS_KEY = 'ic_prefs';
|
||||
const saveKey = (themeId) => `ic_save_${themeId}`;
|
||||
const GAME_KEY = 'ic_game';
|
||||
const legacyKey = (themeId) => `ic_save_${themeId}`; // v1: one save per theme
|
||||
|
||||
// ------------------------------------------------------------------ encoding
|
||||
|
||||
@@ -79,16 +80,30 @@ export function cookiesAvailable() {
|
||||
}
|
||||
|
||||
/** Throws if the save does not fit in cookies. */
|
||||
export function saveGameText(themeId, text) {
|
||||
writeChunked(saveKey(themeId), text);
|
||||
export function saveGameText(text) {
|
||||
writeChunked(GAME_KEY, text);
|
||||
}
|
||||
|
||||
export function loadGameText(themeId) {
|
||||
return readChunked(saveKey(themeId));
|
||||
export function loadGameText() {
|
||||
return readChunked(GAME_KEY);
|
||||
}
|
||||
|
||||
export function deleteGame(themeId) {
|
||||
removeChunked(saveKey(themeId));
|
||||
export function deleteGame() {
|
||||
removeChunked(GAME_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves from the first version (one per theme). Returns their texts and
|
||||
* removes those cookies, so the caller can merge them into the shared game.
|
||||
*/
|
||||
export function takeLegacySaves(themeIds) {
|
||||
const texts = [];
|
||||
for (const id of themeIds) {
|
||||
const text = readChunked(legacyKey(id));
|
||||
if (text) texts.push(text);
|
||||
removeChunked(legacyKey(id));
|
||||
}
|
||||
return texts;
|
||||
}
|
||||
|
||||
export function savePrefs(prefs) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { t } from '../services/i18n.js';
|
||||
import { $, h, icon, refreshStaticIcons } from './dom.js';
|
||||
|
||||
const SLIDER_STEPS = 1000;
|
||||
const TREND = { '-1': '▼', 1: '▲' }; // last move of the material price
|
||||
|
||||
function setText(node, text) {
|
||||
if (node.textContent !== text) node.textContent = text;
|
||||
@@ -218,7 +219,11 @@ export function createView(actions) {
|
||||
el.make.disabled = state.material < d.materialPerUnit;
|
||||
setText(
|
||||
el.buyMaterialInfo,
|
||||
t('ui.batchInfo', { amount: fmt.number(d.batchSize), cost: money(d.batchCost) }),
|
||||
t('ui.batchInfo', {
|
||||
amount: fmt.number(d.batchSize),
|
||||
cost: money(d.batchCost),
|
||||
trend: TREND[state.materialTrend] ?? '',
|
||||
}),
|
||||
);
|
||||
el.buyMaterial.disabled = state.funds < d.batchCost;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"noUpgrades": "Keep producing to unlock upgrades.",
|
||||
"perSecond": "{value}/s",
|
||||
"makeYield": "+{amount} per click",
|
||||
"batchInfo": "+{amount} {@theme.res.material} · {cost}",
|
||||
"batchInfo": "+{amount} {@theme.res.material} · {trend} {cost}",
|
||||
"marketingInfo": "Level {level} → demand ×{multiplier} · {cost}",
|
||||
"hintFair": "At or below the fair price ({fair}) demand is at its maximum: lowering the price sells nothing more.",
|
||||
"hintHigh": "Customers want {demand}/s at this price. Above the fair price ({fair}) demand drops fast.",
|
||||
@@ -55,13 +55,13 @@
|
||||
"downloadTxt": "Download .txt",
|
||||
"copy": "Copy text",
|
||||
"import": "Import a save",
|
||||
"importHelp": "Choose a .json or .txt file, or paste the text below. It replaces the progress of the theme it was saved with.",
|
||||
"importHelp": "Choose a .json or .txt file, or paste the text below. It replaces your current progress, whatever the theme.",
|
||||
"importFile": "Choose a file…",
|
||||
"pastePlaceholder": "Paste the exported text here",
|
||||
"importText": "Import pasted text",
|
||||
"danger": "Danger zone",
|
||||
"reset": "Reset this theme's progress",
|
||||
"resetConfirm": "Erase all progress for this theme? This cannot be undone."
|
||||
"reset": "Reset all progress",
|
||||
"resetConfirm": "Erase all progress? This cannot be undone."
|
||||
},
|
||||
"toast": {
|
||||
"cookiesBlocked": "Cookies are blocked, so progress cannot be saved in this browser. Use Save & settings to export it.",
|
||||
@@ -76,8 +76,7 @@
|
||||
"import": {
|
||||
"empty": "Nothing to import.",
|
||||
"invalid": "This is not a valid save.",
|
||||
"wrong_game": "This file is not an Idle Clicker save.",
|
||||
"unknown_theme": "This save belongs to a theme that is not installed."
|
||||
"wrong_game": "This file is not an Idle Clicker save."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"noUpgrades": "Continuez à produire pour débloquer des améliorations.",
|
||||
"perSecond": "{value}/s",
|
||||
"makeYield": "+{amount} par clic",
|
||||
"batchInfo": "+{amount} {@theme.res.material} · {cost}",
|
||||
"batchInfo": "+{amount} {@theme.res.material} · {trend} {cost}",
|
||||
"marketingInfo": "Niveau {level} → demande ×{multiplier} · {cost}",
|
||||
"hintFair": "Au prix juste ({fair}) ou en dessous, la demande est maximale : baisser le prix ne vend rien de plus.",
|
||||
"hintHigh": "Les clients veulent {demand}/s à ce prix. Au-dessus du prix juste ({fair}), la demande chute vite.",
|
||||
@@ -55,13 +55,13 @@
|
||||
"downloadTxt": "Télécharger en .txt",
|
||||
"copy": "Copier le texte",
|
||||
"import": "Importer une sauvegarde",
|
||||
"importHelp": "Choisissez un fichier .json ou .txt, ou collez le texte ci-dessous. Cela remplace la progression du thème avec lequel il a été sauvegardé.",
|
||||
"importHelp": "Choisissez un fichier .json ou .txt, ou collez le texte ci-dessous. Cela remplace votre progression actuelle, quel que soit le thème.",
|
||||
"importFile": "Choisir un fichier…",
|
||||
"pastePlaceholder": "Collez ici le texte exporté",
|
||||
"importText": "Importer le texte collé",
|
||||
"danger": "Zone dangereuse",
|
||||
"reset": "Réinitialiser la progression de ce thème",
|
||||
"resetConfirm": "Effacer toute la progression de ce thème ? Cette action est irréversible."
|
||||
"reset": "Réinitialiser toute la progression",
|
||||
"resetConfirm": "Effacer toute la progression ? Cette action est irréversible."
|
||||
},
|
||||
"toast": {
|
||||
"cookiesBlocked": "Les cookies sont bloqués : la progression ne peut pas être sauvegardée dans ce navigateur. Exportez-la depuis Sauvegarde et options.",
|
||||
@@ -76,8 +76,7 @@
|
||||
"import": {
|
||||
"empty": "Rien à importer.",
|
||||
"invalid": "Ceci n'est pas une sauvegarde valide.",
|
||||
"wrong_game": "Ce fichier n'est pas une sauvegarde d'Idle Clicker.",
|
||||
"unknown_theme": "Cette sauvegarde appartient à un thème qui n'est pas installé."
|
||||
"wrong_game": "Ce fichier n'est pas une sauvegarde d'Idle Clicker."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"theme": {
|
||||
"name": "Leaf-Piling Insects",
|
||||
"title": "Leaf Pile Colony",
|
||||
"res": { "units": "Leaves", "unit": "leaf", "funds": "Honeydew", "material": "Silk" },
|
||||
"fmt": { "funds": "{value} drops" },
|
||||
"action": { "make": "Cut a leaf", "buyMaterial": "Trade for silk" },
|
||||
"gen": {
|
||||
"gen1": { "name": "Worker Ant", "desc": "Carries a leaf many times its own size." },
|
||||
"gen2": { "name": "Leafcutter Squad", "desc": "Slices, hauls and stacks in perfect formation." },
|
||||
"gen3": { "name": "Beetle Hauler", "desc": "Armoured carrier that shifts whole piles." },
|
||||
"gen4": { "name": "Termite Mound", "desc": "A towering colony that never stops building." },
|
||||
"gen5": { "name": "Locust Swarm", "desc": "Strips whole trees in an afternoon." },
|
||||
"gen6": { "name": "Hive Queen", "desc": "Her pheromones coordinate millions of workers." }
|
||||
},
|
||||
"upg": {
|
||||
"click_1": "Sharper Mandibles",
|
||||
"click_2": "Pheromone Trail",
|
||||
"click_3": "Double-Grip Legs",
|
||||
"g1_a": "Ant Drill Sergeant",
|
||||
"g1_b": "Marching Songs",
|
||||
"g2_a": "Wider Leaf Slices",
|
||||
"g2_b": "Leafcutter Guild",
|
||||
"g3_a": "Reinforced Shells",
|
||||
"g3_b": "Dung Beetle Logistics",
|
||||
"g4_a": "Air-Conditioned Chambers",
|
||||
"g4_b": "Deep Tunnel Network",
|
||||
"g5_a": "Riding the Wind",
|
||||
"g5_b": "Migration Instinct",
|
||||
"g6_a": "Royal Jelly",
|
||||
"g6_b": "Queen's Decree",
|
||||
"global_1": "Dewdrop Hydration",
|
||||
"global_2": "Seasonal Rhythm",
|
||||
"global_3": "Hive Mind",
|
||||
"global_4": "Perfect Spring",
|
||||
"demand_1": "Scent Markers",
|
||||
"demand_2": "Cricket Courier",
|
||||
"demand_3": "Forest Market",
|
||||
"demand_4": "Famous Pile",
|
||||
"value_1": "Dewy Gloss",
|
||||
"value_2": "Autumn Colours",
|
||||
"value_3": "Golden Leaf Grade",
|
||||
"mat_eff_1": "Tighter Weaving",
|
||||
"mat_eff_2": "Silk Recycling",
|
||||
"mat_disc_1": "Spider Contract",
|
||||
"mat_disc_2": "Silkworm Farm",
|
||||
"auto_buy": "Spider Delivery Service"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"theme": {
|
||||
"name": "Insectes et tas de feuilles",
|
||||
"title": "Colonie des tas de feuilles",
|
||||
"res": { "units": "Feuilles", "unit": "feuille", "funds": "Miellat", "material": "Soie" },
|
||||
"fmt": { "funds": "{value} gouttes" },
|
||||
"action": { "make": "Couper une feuille", "buyMaterial": "Échanger contre de la soie" },
|
||||
"gen": {
|
||||
"gen1": { "name": "Fourmi ouvrière", "desc": "Porte une feuille bien plus grosse qu'elle." },
|
||||
"gen2": { "name": "Escouade coupeuse", "desc": "Coupe, transporte et empile en formation parfaite." },
|
||||
"gen3": { "name": "Scarabée porteur", "desc": "Porteur blindé qui déplace des tas entiers." },
|
||||
"gen4": { "name": "Termitière", "desc": "Une colonie immense qui ne cesse de bâtir." },
|
||||
"gen5": { "name": "Nuée de criquets", "desc": "Dépouille des arbres entiers en un après-midi." },
|
||||
"gen6": { "name": "Reine de la ruche", "desc": "Ses phéromones coordonnent des millions d'ouvrières." }
|
||||
},
|
||||
"upg": {
|
||||
"click_1": "Mandibules aiguisées",
|
||||
"click_2": "Piste de phéromones",
|
||||
"click_3": "Pattes à double prise",
|
||||
"g1_a": "Fourmi sergent-instructeur",
|
||||
"g1_b": "Chants de marche",
|
||||
"g2_a": "Tranches de feuille plus larges",
|
||||
"g2_b": "Guilde des coupeuses",
|
||||
"g3_a": "Carapaces renforcées",
|
||||
"g3_b": "Logistique des bousiers",
|
||||
"g4_a": "Chambres climatisées",
|
||||
"g4_b": "Réseau de galeries profondes",
|
||||
"g5_a": "Chevaucher le vent",
|
||||
"g5_b": "Instinct de migration",
|
||||
"g6_a": "Gelée royale",
|
||||
"g6_b": "Décret de la reine",
|
||||
"global_1": "Hydratation à la rosée",
|
||||
"global_2": "Rythme des saisons",
|
||||
"global_3": "Esprit de ruche",
|
||||
"global_4": "Printemps parfait",
|
||||
"demand_1": "Marqueurs odorants",
|
||||
"demand_2": "Courrier-criquet",
|
||||
"demand_3": "Marché de la forêt",
|
||||
"demand_4": "Tas célèbre",
|
||||
"value_1": "Lustre de rosée",
|
||||
"value_2": "Couleurs d'automne",
|
||||
"value_3": "Qualité feuille d'or",
|
||||
"mat_eff_1": "Tissage plus serré",
|
||||
"mat_eff_2": "Recyclage de la soie",
|
||||
"mat_disc_1": "Contrat avec l'araignée",
|
||||
"mat_disc_2": "Ferme de vers à soie",
|
||||
"auto_buy": "Livraison par araignée"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* Insects piling up leaves: a sunlit forest floor. */
|
||||
:root[data-theme='insects'] {
|
||||
color-scheme: light;
|
||||
--bg: #e9efd8;
|
||||
--surface: #f9fbf1;
|
||||
--surface-2: #e2ebcb;
|
||||
--text: #2a3321;
|
||||
--text-muted: #5d6b49;
|
||||
--border: #c4d3a2;
|
||||
--accent: #2f7d32;
|
||||
--accent-contrast: #ffffff;
|
||||
--danger: #b3412a;
|
||||
--radius: 12px;
|
||||
--font: 'Trebuchet MS', 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"order": 4,
|
||||
"assets": {}
|
||||
}
|
||||
Reference in New Issue
Block a user