Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
453b1c7
improve automation UI/UX
djizus Oct 29, 2025
8ae7b8d
production overview panel revamp
djizus Oct 29, 2025
dc6c46c
production overview panel revamp 2
djizus Oct 29, 2025
c8c6435
production overview panel revamp 3
djizus Oct 29, 2025
edfd5da
production advanced panel revamp
djizus Oct 29, 2025
bd3f7f5
fix custom preset
djizus Oct 29, 2025
524ddd0
add net
djizus Oct 29, 2025
5d22b83
add net 2
djizus Oct 29, 2025
35598f7
cleanup css
djizus Oct 29, 2025
9dbed86
sync prod with block timestamp
djizus Oct 30, 2025
064a85a
sync transfers with block timestamp
djizus Oct 30, 2025
6ef62b7
prune old transfers
djizus Oct 30, 2025
8529fb1
fix build
djizus Oct 30, 2025
7d250a9
Merge remote-tracking branch 'origin/blitz' into djizus-blitz
djizus Oct 30, 2025
d1bcfce
more advanced auto panel changes
djizus Oct 30, 2025
3c3e322
add labor in net
djizus Oct 30, 2025
8a2facf
fix navigation when in prod
djizus Oct 30, 2025
44714ab
Merge remote-tracking branch 'origin/blitz' into djizus-blitz
djizus Oct 30, 2025
f54a1d7
fix lint
djizus Oct 30, 2025
d9d9c57
fix text and css
djizus Oct 30, 2025
6b5e2a4
fix text and css
djizus Oct 30, 2025
372c6f9
prettier
djizus Oct 30, 2025
029d05f
Merge remote-tracking branch 'origin/blitz' into djizus-blitz
djizus Oct 31, 2025
18b30cb
better structure filter selection
djizus Oct 31, 2025
d1ee488
better navigation between realms
djizus Oct 31, 2025
a79b749
better navigation between realms 2
djizus Oct 31, 2025
5dc40c3
better advanced automation panel
djizus Oct 31, 2025
667e752
change sort button order
djizus Oct 31, 2025
2ad115d
better transfer automation UI
djizus Oct 31, 2025
98d0458
Merge remote-tracking branch 'origin/blitz' into djizus-blitz
djizus Oct 31, 2025
9abe1dc
Merge remote-tracking branch 'origin/blitz' into djizus-blitz
djizus Nov 1, 2025
f6d53fa
fix transfer panel for camps
djizus Nov 1, 2025
8d395b2
Merge remote-tracking branch 'origin/blitz' into djizus-blitz
djizus Nov 2, 2025
991fd19
small fixes
djizus Nov 2, 2025
f4ddb4d
swap relics and rss in army inventory
djizus Nov 2, 2025
8d28514
better tabbing
djizus Nov 2, 2025
ede290c
Merge remote-tracking branch 'origin/blitz' into djizus-blitz
djizus Nov 2, 2025
e2f07e5
Merge remote-tracking branch 'origin/blitz' into djizus-blitz
djizus Nov 4, 2025
22fdd8d
fix old prod automation showing up
djizus Nov 4, 2025
34bd465
fix lint
djizus Nov 4, 2025
11bac9b
Merge remote-tracking branch 'origin/blitz' into djizus-blitz
djizus Nov 5, 2025
3c1535d
Merge remote-tracking branch 'origin/blitz' into djizus-blitz
djizus Nov 8, 2025
71b9cc4
Merge remote-tracking branch 'origin/blitz' into djizus-blitz
djizus Nov 11, 2025
c4d5462
fix transfers from/to rifts and villages
djizus Nov 12, 2025
1d4e42e
docs update + auto claim transfers
djizus Nov 12, 2025
9117119
transfer window fixes
djizus Nov 12, 2025
d646bd8
better transfer, autoclaim
djizus Nov 12, 2025
c4fe782
format fix
djizus Nov 12, 2025
b68f292
schedule transfer fix
djizus Nov 12, 2025
f1aebc6
schedule transfer fix
djizus Nov 12, 2025
6462168
fix build - better transfer + autoclaim deposits
djizus Nov 12, 2025
f3acc11
prod automation bugfix 1
djizus Nov 12, 2025
49bf31c
prod automation bugfix 2
djizus Nov 12, 2025
4f0a65f
fix docs
djizus Nov 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,401 changes: 1,611 additions & 1,790 deletions client/apps/bot/llm.txt

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions client/apps/game-docs/docs/components/DefendingArmies.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { DEFENSE_NAMES } from "@bibliothecadao/types";
import { GUARD_SLOT_NAMES } from "@bibliothecadao/types";
import { section, table } from "./styles";

// Component 2: Defense Slots by Realm Level
export const RealmDefenseSlotsTable = () => {
const defenseSlots = [
{ level: "Settlement", slots: 1, slotName: DEFENSE_NAMES[0] },
{ level: "City", slots: 2, slotName: DEFENSE_NAMES[1] },
{ level: "Kingdom", slots: 3, slotName: DEFENSE_NAMES[2] },
{ level: "Empire", slots: 4, slotName: DEFENSE_NAMES[3] },
{ level: "Settlement", slots: 1, slotName: GUARD_SLOT_NAMES[0] },
{ level: "City", slots: 2, slotName: GUARD_SLOT_NAMES[1] },
{ level: "Kingdom", slots: 3, slotName: GUARD_SLOT_NAMES[2] },
{ level: "Empire", slots: 4, slotName: GUARD_SLOT_NAMES[3] },
];

return (
Expand Down Expand Up @@ -44,10 +44,10 @@ export const RealmDefenseSlotsTable = () => {
// Component 2b: Blitz Defense Slots by Realm Level
export const BlitzRealmDefenseSlotsTable = () => {
const defenseSlots = [
{ level: "Settlement", slots: 1, slotName: DEFENSE_NAMES[0] },
{ level: "City", slots: 2, slotName: DEFENSE_NAMES[1] },
{ level: "Kingdom", slots: 3, slotName: DEFENSE_NAMES[2] },
{ level: "Empire", slots: 4, slotName: DEFENSE_NAMES[3] },
{ level: "Settlement", slots: 1, slotName: GUARD_SLOT_NAMES[0] },
{ level: "City", slots: 2, slotName: GUARD_SLOT_NAMES[1] },
{ level: "Kingdom", slots: 3, slotName: GUARD_SLOT_NAMES[2] },
{ level: "Empire", slots: 4, slotName: GUARD_SLOT_NAMES[3] },
];

return (
Expand Down
402 changes: 198 additions & 204 deletions client/apps/game-docs/docs/pages/blitz/materials/automation.mdx

Large diffs are not rendered by default.

38 changes: 36 additions & 2 deletions client/apps/game/src/hooks/store/use-automation-store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { calculatePresetAllocations, RealmPresetId } from "@/utils/automation-presets";
import {
calculatePresetAllocations,
calculateResourceBootstrapAllocation,
RealmPresetId,
} from "@/utils/automation-presets";
import { configManager } from "@bibliothecadao/eternum";
import { ResourcesIds } from "@bibliothecadao/types";
import { create } from "zustand";
Expand Down Expand Up @@ -382,12 +386,33 @@ export const useAutomationStore = create<ProductionAutomationState>()(
return existing;
}

const newConfig = createDefaultResourceSettings(
const baseConfig = createDefaultResourceSettings(
resourceId,
{ autoManaged: options?.autoManaged ?? true, label: options?.label },
options?.defaults,
);

let newConfig = baseConfig;
if (realm && realm.presetId === null) {
const configWithResource: RealmAutomationConfig = {
...realm,
resources: {
...realm.resources,
[resourceId]: baseConfig,
},
};
const bootstrapAllocation = calculateResourceBootstrapAllocation(configWithResource, resourceId);
if (bootstrapAllocation && bootstrapAllocation.resourceToResource > 0) {
newConfig = {
...baseConfig,
percentages: {
resourceToResource: bootstrapAllocation.resourceToResource,
laborToResource: bootstrapAllocation.laborToResource,
},
};
}
}

set((state) => {
const realmConfig = state.realms[realmId];
if (!realmConfig) return state;
Expand Down Expand Up @@ -429,11 +454,20 @@ export const useAutomationStore = create<ProductionAutomationState>()(

const current = realm.resources[resourceId] ?? createDefaultResourceSettings(resourceId);
const updatedPercentages = normalizePercentages({ ...current.percentages, ...percentages }, resourceId);
const changed =
updatedPercentages.resourceToResource !== current.percentages.resourceToResource ||
updatedPercentages.laborToResource !== current.percentages.laborToResource;

if (!changed && realm.resources[resourceId]) {
return state;
}

const nextState = {
realms: {
...state.realms,
[realmId]: {
...realm,
presetId: realm.presetId !== null ? null : realm.presetId,
resources: sanitizeRealmResources(
{
...realm.resources,
Expand Down
182 changes: 98 additions & 84 deletions client/apps/game/src/hooks/store/use-transfer-automation-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ const resolveCurrentGameId = (): string => {
try {
const season = configManager.getSeasonConfig();
return `${season.startSettlingAt}-${season.startMainAt}-${season.endAt}`;
} catch (_error) {
} catch {
return "unknown";
}
};

export interface TransferAutomationResourceConfig {
resourceId: ResourcesIds;
amount: number;
}

export interface TransferAutomationEntry {
id: string;
active: boolean;
Expand All @@ -22,24 +27,16 @@ export interface TransferAutomationEntry {
destinationEntityId: string;
destinationName?: string;
resourceIds: ResourcesIds[];
resourceConfigs?: Array<{
resourceId: ResourcesIds;
mode: "percent" | "flat";
percent?: number; // 5..90 when mode = percent
flatPercent?: number; // 1..90 when mode = flat (percent of current balance at execution time)
flatAmount?: number; // explicit human amount when mode = flat
}>;
amountMode?: "percent" | "flat"; // default percent for v1
percent: number; // 5..90 when amountMode = percent
flatAmount?: number; // human units per resource when amountMode = flat
resourceConfigs?: TransferAutomationResourceConfig[];
intervalMinutes: number; // 5..60 (used when active)
lastRunAt?: number;
nextRunAt?: number | null;
}

type NewEntry = Omit<TransferAutomationEntry, "id" | "createdAt" | "lastRunAt" | "nextRunAt" | "active" | "gameId"> & {
type NewEntry = Omit<TransferAutomationEntry, "id" | "createdAt" | "nextRunAt" | "active" | "gameId"> & {
active?: boolean;
gameId?: string;
lastRunAt?: number;
};

interface TransferAutomationState {
Expand All @@ -53,16 +50,6 @@ interface TransferAutomationState {
pruneForGame: (gameId: string) => void;
}

const clampPercent = (value: number): number => {
if (!Number.isFinite(value)) return 5;
return Math.min(90, Math.max(5, Math.round(value)));
};

const clampInterval = (minutes: number): number => {
if (!Number.isFinite(minutes)) return 5;
return Math.min(60, Math.max(5, Math.round(minutes)));
};

const getBlockNowMs = (): number => {
const { currentBlockTimestamp } = getBlockTimestamp();
return currentBlockTimestamp * 1000;
Expand All @@ -71,21 +58,57 @@ const getBlockNowMs = (): number => {
const generateId = () => {
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
try {
return (crypto as any).randomUUID();
} catch (_) {}
return (crypto as Crypto & { randomUUID: () => string }).randomUUID();
} catch {
// ignore and fall through to timestamp-based id
}
}
return `ta_${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
};

const sanitizeResourceConfigs = (raw: unknown, fallbackAmount?: number): TransferAutomationEntry["resourceConfigs"] => {
if (!Array.isArray(raw) || raw.length === 0) return undefined;
const sanitized: TransferAutomationResourceConfig[] = [];
raw.forEach((item) => {
if (!item || typeof item !== "object") return;
const candidate = item as { resourceId?: unknown; amount?: unknown };
if (typeof candidate.resourceId !== "number") return;
const sourceAmount =
typeof candidate.amount === "number" && Number.isFinite(candidate.amount) ? candidate.amount : fallbackAmount;
const normalizedAmount =
typeof sourceAmount === "number" && Number.isFinite(sourceAmount) ? Math.max(0, Math.floor(sourceAmount)) : 0;
sanitized.push({
resourceId: candidate.resourceId,
amount: normalizedAmount,
});
});
return sanitized.length > 0 ? sanitized : undefined;
};

const buildConfigsFromResources = (
resourceIds: ResourcesIds[],
fallbackAmount?: number,
): TransferAutomationEntry["resourceConfigs"] => {
const fallback = Number.isFinite(fallbackAmount) ? Math.max(0, Math.floor(fallbackAmount as number)) : 0;
if (resourceIds.length === 0) return undefined;
return resourceIds.map((rid) => ({
resourceId: rid,
amount: fallback,
}));
};

export const useTransferAutomationStore = create<TransferAutomationState>()(
persist(
(set, get) => ({
(set) => ({
entries: {},
add: (raw) => {
const id = generateId();
const now = getBlockNowMs();
const active = raw.active ?? true;
const gameId = raw.gameId ?? resolveCurrentGameId();
const resourceIdsArray = Array.from(new Set(raw.resourceIds || [])).filter(
(r) => typeof r === "number",
) as ResourcesIds[];
const entry: TransferAutomationEntry = {
id,
createdAt: now,
Expand All @@ -95,15 +118,12 @@ export const useTransferAutomationStore = create<TransferAutomationState>()(
sourceName: raw.sourceName,
destinationEntityId: String(raw.destinationEntityId),
destinationName: raw.destinationName,
resourceIds: Array.from(new Set(raw.resourceIds || [])).filter(
(r) => typeof r === "number",
) as ResourcesIds[],
amountMode: raw.amountMode ?? "percent",
percent: clampPercent(raw.percent),
flatAmount: typeof raw.flatAmount === "number" ? Math.max(0, Math.floor(raw.flatAmount)) : undefined,
intervalMinutes: clampInterval(raw.intervalMinutes),
lastRunAt: undefined,
nextRunAt: active ? now + clampInterval(raw.intervalMinutes) * 60_000 : null,
resourceIds: resourceIdsArray,
resourceConfigs:
sanitizeResourceConfigs(raw.resourceConfigs) ?? buildConfigsFromResources(resourceIdsArray, undefined),
intervalMinutes: Math.max(1, Math.round(raw.intervalMinutes ?? 1)),
lastRunAt: raw.lastRunAt,
nextRunAt: active ? now + Math.max(1, Math.round(raw.intervalMinutes ?? 1)) * 60_000 : null,
};
set((state) => ({ entries: { ...state.entries, [id]: entry } }));
return id;
Expand All @@ -112,15 +132,18 @@ export const useTransferAutomationStore = create<TransferAutomationState>()(
set((state) => {
const prev = state.entries[id];
if (!prev) return state;
const updatedInterval =
patch.intervalMinutes !== undefined
? Math.max(1, Math.round(patch.intervalMinutes))
: Math.max(1, Math.round(prev.intervalMinutes || 1));
const next: TransferAutomationEntry = {
...prev,
...patch,
amountMode: patch.amountMode ?? prev.amountMode ?? "percent",
percent: patch.percent !== undefined ? clampPercent(patch.percent) : prev.percent,
flatAmount:
patch.flatAmount !== undefined ? Math.max(0, Math.floor(patch.flatAmount as number)) : prev.flatAmount,
intervalMinutes:
patch.intervalMinutes !== undefined ? clampInterval(patch.intervalMinutes) : prev.intervalMinutes,
intervalMinutes: updatedInterval,
resourceConfigs:
patch.resourceConfigs !== undefined
? sanitizeResourceConfigs(patch.resourceConfigs)
: prev.resourceConfigs,
};

return { entries: { ...state.entries, [id]: next } };
Expand All @@ -138,10 +161,11 @@ export const useTransferAutomationStore = create<TransferAutomationState>()(
if (!prev) return state;
const isActive = active ?? !prev.active;
const now = getBlockNowMs();
const intervalMinutes = Math.max(1, Math.round(prev.intervalMinutes || 1));
const next: TransferAutomationEntry = {
...prev,
active: isActive,
nextRunAt: isActive ? now + clampInterval(prev.intervalMinutes) * 60_000 : null,
nextRunAt: isActive ? now + intervalMinutes * 60_000 : null,
};
return { entries: { ...state.entries, [id]: next } };
}),
Expand All @@ -150,9 +174,10 @@ export const useTransferAutomationStore = create<TransferAutomationState>()(
const prev = state.entries[id];
if (!prev) return state;
const now = base ?? getBlockNowMs();
const intervalMinutes = Math.max(1, Math.round(prev.intervalMinutes || 1));
const next: TransferAutomationEntry = {
...prev,
nextRunAt: now + clampInterval(prev.intervalMinutes) * 60_000,
nextRunAt: now + intervalMinutes * 60_000,
};
return { entries: { ...state.entries, [id]: next } };
}),
Expand All @@ -170,56 +195,45 @@ export const useTransferAutomationStore = create<TransferAutomationState>()(
version: 3,
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ entries: state.entries }),
migrate: (persisted: any, version) => {
migrate: (persisted: unknown) => {
if (!persisted || typeof persisted !== "object") return { entries: {} };
const rawEntries = (persisted as any).entries ?? {};
const rawEntries = (persisted as { entries?: unknown }).entries;
if (!rawEntries || typeof rawEntries !== "object") return { entries: {} };
const entries: Record<string, TransferAutomationEntry> = {};
Object.entries(rawEntries).forEach(([id, value]) => {
const v = value as TransferAutomationEntry;
if (!v || typeof v !== "object") return;
const resourceIds = Array.isArray(v.resourceIds)
? (v.resourceIds.filter((r) => typeof r === "number") as ResourcesIds[])
: [];
const percent = clampPercent((v as any).percent ?? 5);
const intervalMinutes = clampInterval((v as any).intervalMinutes ?? 5);
let resourceConfigs: TransferAutomationEntry["resourceConfigs"] | undefined;
if (Array.isArray((v as any).resourceConfigs)) {
resourceConfigs = ((v as any).resourceConfigs as any[])
.map((c) => ({
resourceId: c.resourceId,
mode: c.mode === "flat" ? "flat" : "percent",
percent: typeof c.percent === "number" ? clampPercent(c.percent) : undefined,
flatPercent:
typeof c.flatPercent === "number" ? Math.min(90, Math.max(1, Math.floor(c.flatPercent))) : undefined,
flatAmount: typeof c.flatAmount === "number" ? Math.max(0, Math.floor(c.flatAmount)) : undefined,
}))
.map((c) => ({
resourceId: c.resourceId,
mode: c.mode,
percent: c.percent,
flatPercent: (c as any).flatPercent,
flatAmount: (c as any).flatAmount,
})) as any;
}
Object.entries(rawEntries as Record<string, unknown>).forEach(([id, value]) => {
if (!value || typeof value !== "object") return;
const persistedEntry = value as Record<string, unknown>;
const resourceIdsRaw = Array.isArray(persistedEntry.resourceIds) ? persistedEntry.resourceIds : [];
const resourceIds = resourceIdsRaw.filter((r): r is ResourcesIds => typeof r === "number");
const intervalMinutes =
typeof persistedEntry.intervalMinutes === "number"
? Math.max(1, Math.round(persistedEntry.intervalMinutes))
: 5;
const fallbackAmount =
typeof persistedEntry.flatAmount === "number"
? Math.max(0, Math.floor(persistedEntry.flatAmount))
: undefined;
const resourceConfigs =
sanitizeResourceConfigs(persistedEntry.resourceConfigs, fallbackAmount) ??
buildConfigsFromResources(resourceIds, fallbackAmount);
entries[id] = {
id,
createdAt: typeof v.createdAt === "number" ? v.createdAt : Date.now(),
active: Boolean((v as any).active),
gameId: typeof (v as any).gameId === "string" ? (v as any).gameId : resolveCurrentGameId(),
sourceEntityId: String((v as any).sourceEntityId ?? "0"),
sourceName: (v as any).sourceName,
destinationEntityId: String((v as any).destinationEntityId ?? "0"),
destinationName: (v as any).destinationName,
createdAt: typeof persistedEntry.createdAt === "number" ? persistedEntry.createdAt : Date.now(),
active: Boolean(persistedEntry.active),
gameId: typeof persistedEntry.gameId === "string" ? persistedEntry.gameId : resolveCurrentGameId(),
sourceEntityId: String(persistedEntry.sourceEntityId ?? "0"),
sourceName: typeof persistedEntry.sourceName === "string" ? persistedEntry.sourceName : undefined,
destinationEntityId: String(persistedEntry.destinationEntityId ?? "0"),
destinationName:
typeof persistedEntry.destinationName === "string" ? persistedEntry.destinationName : undefined,
resourceIds,
resourceConfigs,
amountMode: (v as any).amountMode ?? "percent",
percent,
flatAmount:
typeof (v as any).flatAmount === "number" ? Math.max(0, Math.floor((v as any).flatAmount)) : undefined,
intervalMinutes,
lastRunAt: typeof (v as any).lastRunAt === "number" ? (v as any).lastRunAt : undefined,
lastRunAt: typeof persistedEntry.lastRunAt === "number" ? persistedEntry.lastRunAt : undefined,
nextRunAt:
(v as any).nextRunAt === null || typeof (v as any).nextRunAt === "number" ? (v as any).nextRunAt : null,
persistedEntry.nextRunAt === null || typeof persistedEntry.nextRunAt === "number"
? (persistedEntry.nextRunAt as number | null)
: null,
};
});
return { entries };
Expand Down
Loading
Loading