Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
56bdd00
refactor(state): remove unused UPDATE_VOID_STRESS action scaffolding
claude May 15, 2026
ce7d127
refactor: delete unused randomUtils module
claude May 15, 2026
fd0c414
refactor: remove unused VisualPrototypes scaffolding
claude May 15, 2026
3bdc1d8
refactor: drop export keyword on module-private utilities
claude May 15, 2026
a6f1bfa
refactor: remove unused storage helpers
claude May 15, 2026
f94bf37
refactor: remove unused error and image-fetch helpers
claude May 15, 2026
fca32fb
refactor: remove deprecated re-export shims
claude May 15, 2026
ef21a92
fix(stage): update PixiStageController to import withTimeout from sta…
claude May 15, 2026
d8c9e35
docs(audits): add 2026-05-15 codebase audit report
claude May 15, 2026
3fd1233
refactor(state): use canonical clamp helpers for 0..100 fields
claude May 15, 2026
d741c81
fix(state): clamp relationship scores in reducer sanitization
claude May 15, 2026
37ce7f7
refactor: use isEmptyObject for emptiness checks
claude May 15, 2026
9e6e461
refactor: prefer nullish coalescing for valid-zero numeric fallbacks
claude May 15, 2026
d04b373
refactor(images): extract resolveGenImageUrl helper
claude May 15, 2026
e3a8ea9
refactor: slim MINIGAME_REGISTRY to actively-used fields
claude May 15, 2026
afd942f
test: add resolveGenImageUrl to imageGen mocks
claude May 15, 2026
b85df0b
fix: resolve PR feedback related to clampNonNegative missing import a…
DaFum May 16, 2026
5fb0327
fix: restore generated image error fallback and zealotry template string
DaFum May 16, 2026
f1671a1
Merge pull request #1648 from DaFum/jules-13654275781429783194-ca113761
DaFum May 16, 2026
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
2 changes: 1 addition & 1 deletion .agents/skills/writing-skills/anthropic-best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -1104,7 +1104,7 @@ Don't assume packages are available:

Then use it:

```python
````python
from pypdf import PdfReader
reader = PdfReader("file.pdf")
```"
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/writing-skills/anthropic-best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -1104,7 +1104,7 @@ Don't assume packages are available:

Then use it:

```python
````python
from pypdf import PdfReader
reader = PdfReader("file.pdf")
```"
Expand Down
295 changes: 295 additions & 0 deletions docs/audits/2026-05-15-codebase-audit.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/components/PixiStageController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { LaneManager } from './stage/LaneManager'
import { EffectManager } from './stage/EffectManager'
import { NoteManager } from './stage/NoteManager'
import { getGigTimeMs } from '../utils/audio/audioEngine'
import { withTimeout } from './stage/utils'
import { withTimeout } from './stage/stageRenderUtils'
import type { StageControllerOptions } from '../types/components'
import type { RhythmGameRefState } from '../types/rhythmGame'

Expand Down
11 changes: 3 additions & 8 deletions src/components/postGig/CompletePhase.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { motion } from 'framer-motion'
import { useTranslation } from 'react-i18next'
import { ActionButton } from '../../ui/shared'
import {
getGenImageUrl,
IMG_PROMPTS,
isImageGenerationAvailable,
getGeneratedImageFallbackUrl
} from '../../utils/imageGen'
import { IMG_PROMPTS, resolveGenImageUrl } from '../../utils/imageGen'
import { SideEffectsSummary } from './SideEffectsSummary'
import type { CompletePhaseProps } from '../../types/components'

Expand All @@ -20,7 +15,7 @@ export const CompletePhase = ({
}: CompletePhaseProps) => {
const { t, i18n } = useTranslation()
const hasPR = player?.hqUpgrades?.includes('pr_manager_contract')
const isHighControversy = (social?.controversyLevel || 0) > 50
const isHighControversy = (social?.controversyLevel ?? 0) > 50

const getOutcomeImagePrompt = () => {
if (result.success === true) {
Expand All @@ -38,7 +33,7 @@ export const CompletePhase = ({
<div
className='absolute inset-0 opacity-20 bg-cover bg-center mix-blend-screen pointer-events-none z-0'
style={{
backgroundImage: `url("${isImageGenerationAvailable() ? getGenImageUrl(getOutcomeImagePrompt()) : getGeneratedImageFallbackUrl()}")`
backgroundImage: `url("${resolveGenImageUrl(getOutcomeImagePrompt())}")`
}}
/>
<motion.div
Expand Down
13 changes: 2 additions & 11 deletions src/components/postGig/DealCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { ActionButton } from '../../ui/shared'
import { BRAND_ALIGNMENTS } from '../../context/initialState'
import {
IMG_PROMPTS,
getGenImageUrl,
isImageGenerationAvailable,
getGeneratedImageFallbackUrl
} from '../../utils/imageGen'
import { IMG_PROMPTS, resolveGenImageUrl } from '../../utils/imageGen'
import type {
DealImageProps,
DealInfoProps,
Expand Down Expand Up @@ -141,11 +136,7 @@ const getAlignmentColor = (alignment?: string) => {
const DealImage = memo(({ alignment, name }: DealImageProps) => (
<div className='shrink-0 w-24 h-24 border border-current opacity-80 overflow-hidden'>
<img
src={
isImageGenerationAvailable()
? getGenImageUrl(getAlignmentImagePrompt(alignment))
: getGeneratedImageFallbackUrl()
}
src={resolveGenImageUrl(getAlignmentImagePrompt(alignment))}
alt={name}
className='w-full h-full object-cover object-center grayscale hover:grayscale-0 transition-all duration-300'
loading='lazy'
Expand Down
9 changes: 2 additions & 7 deletions src/components/postGig/SocialOptionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@ import { useTranslation } from 'react-i18next'
import { motion } from 'framer-motion'
import { ActionButton } from '../../ui/shared'
import { SideEffectsPreview } from './SideEffectsPreview'
import {
getGenImageUrl,
IMG_PROMPTS,
isImageGenerationAvailable,
getGeneratedImageFallbackUrl
} from '../../utils/imageGen'
import { IMG_PROMPTS, resolveGenImageUrl } from '../../utils/imageGen'
import type { SocialOptionButtonProps } from '../../types/components'

const CATEGORY_PROMPTS = {
Expand Down Expand Up @@ -49,7 +44,7 @@ export const SocialOptionButton = memo(function SocialOptionButton({
<div
className='absolute inset-0 opacity-80 group-hover:opacity-20 transition-opacity bg-cover bg-center pointer-events-none'
style={{
backgroundImage: `url("${isImageGenerationAvailable() ? getGenImageUrl(getImagePromptForCategory(opt.category, opt.badges)) : getGeneratedImageFallbackUrl()}")`
backgroundImage: `url("${resolveGenImageUrl(getImagePromptForCategory(opt.category, opt.badges))}")`
}}
/>

Expand Down
16 changes: 4 additions & 12 deletions src/components/postGig/ZealotryGauge.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { ZEALOTRY_PROMO_THRESHOLD } from '../../utils/economyEngine'
import {
getGenImageUrl,
IMG_PROMPTS,
isImageGenerationAvailable,
getGeneratedImageFallbackUrl
} from '../../utils/imageGen'
import { clampZealotry } from '../../utils/gameStateUtils'
import { IMG_PROMPTS, resolveGenImageUrl } from '../../utils/imageGen'

type ZealotryGaugeProps = { zealotryLevel?: number }

Expand All @@ -20,11 +16,7 @@ export const ZealotryGauge = memo(
<div className='flex flex-row items-center gap-4 mb-4 p-3 bg-blood-red/10 border border-blood-red/30 rounded relative overflow-hidden'>
<div className='w-12 h-12 shrink-0 border border-blood-red/50 rounded overflow-hidden'>
<img
src={
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.ZEALOTRY_CULT)
: getGeneratedImageFallbackUrl()
}
src={resolveGenImageUrl(IMG_PROMPTS.ZEALOTRY_CULT)}
alt={t('ui:postGig.socialPhase.altZealotryCult', {
defaultValue: 'Zealotry Cult'
})}
Expand All @@ -46,7 +38,7 @@ export const ZealotryGauge = memo(
<div
className='bg-blood-red h-full transition-all duration-500'
style={{
width: `${Math.min(100, Math.max(0, zealotryLevel))}%`
width: `${clampZealotry(zealotryLevel)}%`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since 'zealotryLevel' is an optional prop (number | undefined), passing it directly to 'clampZealotry' might cause issues if the helper expects a strict 'number'. Providing an explicit fallback ensures the UI renders correctly (e.g., 0%) when the value is missing.

Suggested change
width: `${clampZealotry(zealotryLevel)}%`
width: clampZealotry(zealotryLevel ?? 0) + '%'

}}
/>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/components/stage/AmpStageController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as PIXI from 'pixi.js'
import { getPixiColorFromToken } from './stageRenderUtils'
import { BaseStageController } from './BaseStageController'
import { getSafeRandom } from '../../utils/crypto'
import { clamp0to100 } from '../../utils/gameStateUtils'
import type {
StageControllerOptions,
AmpStageOptions
Expand Down Expand Up @@ -88,7 +89,7 @@ export class AmpStageController extends BaseStageController<AmpStageOptions> {
if (Object.hasOwn(state, 'interference')) {
const sanitizedInterference = Number(state.interference)
if (Number.isFinite(sanitizedInterference)) {
this.interference = Math.max(0, Math.min(100, sanitizedInterference))
this.interference = clamp0to100(sanitizedInterference)
}
}
if (Object.hasOwn(state, 'isHijackActive')) {
Expand Down
15 changes: 3 additions & 12 deletions src/components/stage/CrowdTextureManager.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import type { Texture } from 'pixi.js'
import {
getGenImageUrl,
IMG_PROMPTS,
isImageGenerationAvailable,
getGeneratedImageFallbackUrl
} from '../../utils/imageGen'
import { IMG_PROMPTS, resolveGenImageUrl } from '../../utils/imageGen'
import { handleError } from '../../utils/errorHandler'
import { loadTextures } from './stageRenderUtils'

Expand All @@ -23,12 +18,8 @@ export class CrowdTextureManager {
async loadAssets(): Promise<void> {
try {
const urls = {
idle: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.CROWD_IDLE)
: getGeneratedImageFallbackUrl(),
mosh: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.CROWD_MOSH)
: getGeneratedImageFallbackUrl()
idle: resolveGenImageUrl(IMG_PROMPTS.CROWD_IDLE),
mosh: resolveGenImageUrl(IMG_PROMPTS.CROWD_MOSH)
}

const loadedTextures = await loadTextures(
Expand Down
15 changes: 3 additions & 12 deletions src/components/stage/EffectTextureManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@
*/
import { Graphics, Texture } from 'pixi.js'
import type { Application } from 'pixi.js'
import {
getGenImageUrl,
IMG_PROMPTS,
isImageGenerationAvailable,
getGeneratedImageFallbackUrl
} from '../../utils/imageGen'
import { IMG_PROMPTS, resolveGenImageUrl } from '../../utils/imageGen'
import { logger } from '../../utils/logger'
import {
loadTextures,
Expand Down Expand Up @@ -106,12 +101,8 @@ export class EffectTextureManager {
async loadAssets(): Promise<void> {
try {
const urls = {
blood: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.HIT_BLOOD)
: getGeneratedImageFallbackUrl(),
toxic: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.HIT_TOXIC)
: getGeneratedImageFallbackUrl()
blood: resolveGenImageUrl(IMG_PROMPTS.HIT_BLOOD),
toxic: resolveGenImageUrl(IMG_PROMPTS.HIT_TOXIC)
}

const loadedTextures = await loadTextures(
Expand Down
15 changes: 3 additions & 12 deletions src/components/stage/NoteTextureManager.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { Texture } from 'pixi.js'
import { handleError } from '../../utils/errorHandler'
import {
getGenImageUrl,
IMG_PROMPTS,
isImageGenerationAvailable,
getGeneratedImageFallbackUrl
} from '../../utils/imageGen'
import { IMG_PROMPTS, resolveGenImageUrl } from '../../utils/imageGen'
import { loadTextures } from './stageRenderUtils'

export type NoteTextures = { skull: Texture | null; lightning: Texture | null }
Expand All @@ -20,12 +15,8 @@ export class NoteTextureManager {
async loadAssets(): Promise<void> {
try {
const urls = {
skull: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.NOTE_SKULL)
: getGeneratedImageFallbackUrl(),
lightning: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.NOTE_LIGHTNING)
: getGeneratedImageFallbackUrl()
skull: resolveGenImageUrl(IMG_PROMPTS.NOTE_SKULL),
lightning: resolveGenImageUrl(IMG_PROMPTS.NOTE_LIGHTNING)
}

const loadedTextures = await loadTextures(
Expand Down
2 changes: 0 additions & 2 deletions src/components/stage/utils.ts

This file was deleted.

23 changes: 4 additions & 19 deletions src/context/actionCreators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
clampPlayerFame,
calculateFameLevel,
clampBandHarmony,
clampNonNegative
clampNonNegative,
clamp0to100
} from '../utils/gameStateUtils'
import type { RhythmSetlistEntry } from '../types/rhythmGame'
import type {
Expand Down Expand Up @@ -96,22 +97,6 @@ export const createUpdatePlayerAction = (
}
}

/**
* Creates a band update action
* @param {Object} updates - Band state updates
* @returns {Object} Action object
*/
export const createUpdateVoidStressAction = (
delta: number
): Extract<GameAction, { type: typeof ActionTypes.UPDATE_VOID_STRESS }> => {
return {
type: ActionTypes.UPDATE_VOID_STRESS,
payload: {
delta: Number.isFinite(delta) ? delta : 0
}
}
}

export const createUpdateBandAction = (
updates: UpdateBandPayload
): Extract<GameAction, { type: typeof ActionTypes.UPDATE_BAND }> => {
Expand Down Expand Up @@ -448,8 +433,8 @@ export const createCompleteRoadieMinigameAction = (
> => ({
type: ActionTypes.COMPLETE_ROADIE_MINIGAME,
payload: {
equipmentDamage: Math.max(0, Math.min(100, Number(equipmentDamage) || 0)),
contrabandDelivered: Math.max(0, Number(contrabandDelivered) || 0)
equipmentDamage: clamp0to100(Number(equipmentDamage) || 0),
contrabandDelivered: clampNonNegative(Number(contrabandDelivered) || 0)
}
})

Expand Down
1 change: 0 additions & 1 deletion src/context/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export const ActionTypes = {
CHANGE_SCENE: 'CHANGE_SCENE',
UPDATE_PLAYER: 'UPDATE_PLAYER',
UPDATE_BAND: 'UPDATE_BAND',
UPDATE_VOID_STRESS: 'UPDATE_VOID_STRESS',
TOGGLE_NEURO_DECIMATOR: 'TOGGLE_NEURO_DECIMATOR',
UPDATE_SOCIAL: 'UPDATE_SOCIAL',
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
Expand Down
27 changes: 15 additions & 12 deletions src/context/reducers/systemReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
calculateFameLevel,
isForbiddenKey,
clampVanFuel,
clampRelationship,
isPlainObject,
isEmptyObject
} from '../../utils/gameStateUtils'
Expand Down Expand Up @@ -564,7 +565,7 @@ const normalizeLoadedGameMap = (gameMap: unknown): GameMap | null => {
}
}

if (Object.keys(sanitizedCityStates).length > 0) {
if (!isEmptyObject(sanitizedCityStates)) {
sanitizedMap.cityStates = sanitizedCityStates
}
}
Expand Down Expand Up @@ -905,16 +906,18 @@ const sanitizeBand = (loadedBand: unknown): BandState => {
equipment: copySafePrimitiveObject(m.equipment) ?? {},
relationships: isPlainObject(m.relationships)
? Object.fromEntries(
Object.entries(m.relationships).filter(([key, value]) => {
const normalizedKey = key.toLowerCase()
if (
selfRelationshipKeys.has(key) ||
selfRelationshipKeys.has(normalizedKey)
) {
return false
}
return typeof value === 'number' && Number.isFinite(value)
}) as Array<[string, number]>
(
Object.entries(m.relationships).filter(([key, value]) => {
const normalizedKey = key.toLowerCase()
if (
selfRelationshipKeys.has(key) ||
selfRelationshipKeys.has(normalizedKey)
) {
return false
}
return typeof value === 'number' && Number.isFinite(value)
}) as Array<[string, number]>
).map(([key, value]) => [key, clampRelationship(value)])
)
: {}
}
Expand Down Expand Up @@ -1269,7 +1272,7 @@ const sanitizeNpcs = (value: unknown): GameState['npcs'] => {
: {}),
...(typeof npc.relationship === 'number' &&
Number.isFinite(npc.relationship)
? { relationship: npc.relationship }
? { relationship: clampRelationship(npc.relationship) }
: {})
}
}
Expand Down
2 changes: 0 additions & 2 deletions src/hooks/minigames/constants.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/hooks/minigames/useRoadieLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useRef, useCallback, useEffect, useState } from 'react'
import { useGameState } from '../../context/GameState'
import { GAME_PHASES } from '../../context/gameConstants'
import { audioManager } from '../../utils/audio/audioEngine'
import { isEmptyObject } from '../../utils/gameStateUtils'
import { isEmptyObject, clamp0to100 } from '../../utils/gameStateUtils'
import {
ROADIE_GRID_WIDTH,
ROADIE_GRID_HEIGHT,
Expand Down Expand Up @@ -79,7 +79,7 @@ export function handleCrash(
audioManager.playSFX('crash')

if (game.carrying) {
game.equipmentDamage = Math.max(0, Math.min(100, game.equipmentDamage + 10))
game.equipmentDamage = clamp0to100(game.equipmentDamage + 10)

if (game.equipmentDamage >= 100) {
game.isGameOver = true
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/minigames/useTourbusLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
TOURBUS_TARGET_DISTANCE
} from './minigameConstants'
import { getSafeRandom } from '../../utils/crypto'
import { clamp0to100 } from '../../utils/gameStateUtils'

type TourbusObstacleType = 'FUEL' | 'OBSTACLE' | 'VOID_HAZARD'

Expand Down Expand Up @@ -187,7 +188,7 @@ export const useTourbusLogic = () => {
// Damage Mitigation
const hitDamage = getHitDamage(upgradesRef.current)

game.damage = Math.max(0, Math.min(100, game.damage + hitDamage))
game.damage = clamp0to100(game.damage + hitDamage)
audioManager.playSFX('crash') // Play SFX immediately on collision
} else if (obs.type === 'FUEL') {
game.itemsCollected.push('FUEL')
Expand Down
Loading
Loading