fix: Object.values null safety + Docker dev build
Build and Publish Mana Loop Docker Image / build-and-publish (push) Has been cancelled

- Change Dockerfile to use development build (better error messages)
- Add || {} fallbacks to all Object.values() calls accessing state
- Fixes "Cannot convert undefined or null to object" browser error during SSR/hydration
- Verified TypeScript compilation and Next.js build successful

Files modified:
- Dockerfile
- src/app/page.tsx
- src/components/game/tabs/StatsTab.tsx
- src/components/game/StatsTab/LoopStatsSection.tsx
- src/components/game/StatsTab/ElementStatsSection.tsx
- src/components/game/tabs/AttunementsTab.tsx
- src/components/game/tabs/GolemancyTab.tsx
- src/lib/game/effects.ts
- src/lib/game/utils/combat-utils.ts
- src/lib/game/crafting-loot.ts
- src/components/game/LootInventory/LootInventoryDisplay.tsx
- src/components/game/LootInventory/index.tsx
- src/components/game/crafting/EnchantmentDesigner/utils.ts
This commit is contained in:
Refactoring Agent
2026-05-04 11:03:11 +02:00
parent 98ab975fb9
commit 0eabd604b0
18 changed files with 46 additions and 60 deletions
@@ -55,7 +55,7 @@ export function LootInventoryDisplay({
const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'material' | 'equipment'; id: string; name: string } | null>(null);
// Count items
const materialCount = Object.values(inventory.materials).reduce((a, b) => a + b, 0);
const materialCount = Object.values(inventory.materials || {}).reduce((a, b) => a + b, 0);
const essenceCount = elements ? Object.entries(elements).reduce((a, [id, e]) => id === 'transference' ? a : a + e.current, 0) : 0;
const blueprintCount = inventory.blueprints.length;
const equipmentCount = Object.keys(equipmentInstances).length;
+1 -1
View File
@@ -55,7 +55,7 @@ export function LootInventoryDisplay({
const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'material' | 'equipment'; id: string; name: string } | null>(null);
// Count items
const materialCount = Object.values(inventory.materials).reduce((a: number, b: number) => a + b, 0);
const materialCount = Object.values(inventory.materials || {}).reduce((a: number, b: number) => a + b, 0);
// Calculate essence count
let essenceCount = 0;
@@ -48,7 +48,7 @@ export function ElementStatsSection({ store, elemMax }: ElementStatsSectionProps
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Unlocked Elements:</span>
<span className="text-green-300">{Object.values(store.elements).filter((e: any) => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
<span className="text-green-300">{Object.values(store.elements || {}).filter((e: any) => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Elem. Crafting Bonus:</span>
@@ -10,8 +10,8 @@ interface LoopStatsSectionProps {
}
export function LoopStatsSection({ store }: LoopStatsSectionProps) {
const spellsLearned = Object.values(store.spells as Record<string, { learned: boolean }>).filter((s) => s.learned).length;
const totalSkillLevels = Object.values(store.skills as Record<string, number>).reduce((a: number, b: number) => a + b, 0);
const spellsLearned = Object.values(store.spells || {}).filter((s) => s.learned).length;
const totalSkillLevels = Object.values(store.skills || {}).reduce((a: number, b: number) => a + b, 0);
return (
<Card className="bg-gray-900/80 border-gray-700">
@@ -49,7 +49,7 @@ export function getOwnedEquipmentTypes(store: GameStore) {
const ownedEquipmentTypeIds = new Set<string>();
// Check all equipment instances the player owns
for (const instance of Object.values(store.equipmentInstances)) {
for (const instance of Object.values(store.equipmentInstances || {})) {
ownedEquipmentTypeIds.add(instance.typeId);
}
+1 -1
View File
@@ -232,7 +232,7 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
</p>
<div className="flex flex-wrap gap-2">
{availableCategories.map(cat => {
const attunement = Object.values(ATTUNEMENTS_DEF).find(a =>
const attunement = Object.values(ATTUNEMENTS_DEF || {}).find(a =>
a.skillCategories.includes(cat) && attunements[a.id]?.active
);
return (
+4 -4
View File
@@ -202,7 +202,7 @@ export function EquipmentTab() {
// Check if an instance is currently equipped
const isEquipped = (instanceId: string): boolean =>
Object.values(equippedInstances).includes(instanceId);
Object.values(equippedInstances || {}).includes(instanceId);
// Get all slots an item type can be equipped to
const getEquippableSlots = (typeId: string): EquipmentSlot[] => {
@@ -243,7 +243,7 @@ export function EquipmentTab() {
title="Equipped Gear"
action={
<span className="text-xs text-[var(--text-muted)]">
{Object.values(equippedInstances).filter(Boolean).length} / {EQUIPMENT_SLOTS.length} slots filled
{Object.values(equippedInstances || {}).filter(Boolean).length} / {EQUIPMENT_SLOTS.length} slots filled
</span>
}
/>
@@ -288,7 +288,7 @@ export function EquipmentTab() {
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
<div className="text-2xl font-bold text-[var(--mana-light)] font-[var(--font-mono)]">
{Object.values(equipmentInstances).length}
{Object.values(equipmentInstances || {}).length}
</div>
<div className="text-xs text-[var(--text-muted)]">Total Items</div>
</div>
@@ -306,7 +306,7 @@ export function EquipmentTab() {
</div>
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
<div className="text-2xl font-bold text-[var(--mana-stellar)] font-[var(--font-mono)]">
{Object.values(equipmentInstances).reduce(
{Object.values(equipmentInstances || {}).reduce(
(sum, inst) => sum + inst.enchantments.length,
0
)}
+2 -2
View File
@@ -35,7 +35,7 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
.map(([id]) => id);
// Get all unlocked golems
const unlockedGolems = Object.values(GOLEMS_DEF).filter(golem =>
const unlockedGolems = Object.values(GOLEMS_DEF || {}).filter(golem =>
isGolemUnlocked(golem.id, attunements, unlockedElements)
);
@@ -293,7 +293,7 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
{unlockedGolems.map(golem => renderGolemCard(golem.id, true))}
{/* Locked Golems */}
{Object.values(GOLEMS_DEF)
{Object.values(GOLEMS_DEF || {})
.filter(g => !isGolemUnlocked(g.id, attunements, unlockedElements))
.map(golem => renderGolemCard(golem.id, false))}
</div>
+1 -1
View File
@@ -36,7 +36,7 @@ export function SpellsTab() {
);
}
for (const instanceId of Object.values(equippedInstances)) {
for (const instanceId of Object.values(equippedInstances || {})) {
if (!instanceId) continue;
const instance = equipmentInstances[instanceId];
if (!instance) continue;
+2 -2
View File
@@ -176,7 +176,7 @@ export function StatsTab() {
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Unlocked Elements:</span>
<span className="text-green-300">{Object.values(elements).filter(e => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
<span className="text-green-300">{Object.values(elements || {}).filter((e: any) => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Elem. Crafting Bonus:</span>
@@ -298,7 +298,7 @@ export function StatsTab() {
<Separator className="bg-gray-700 my-3" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="p-3 bg-gray-800/50 rounded text-center">
<div className="text-xl font-bold text-gray-300 game-mono">{Object.values(spells).filter(s => s.learned).length}</div>
<div className="text-xl font-bold text-gray-300 game-mono">{Object.values(spells || {}).filter((s: any) => s.learned).length}</div>
<div className="text-xs text-gray-400">Spells Learned</div>
</div>
<div className="p-3 bg-gray-800/50 rounded text-center">