diff --git a/docs/uml-diagrams/imgs/plantuml_class_diagram.png b/docs/uml-diagrams/imgs/plantuml_class_diagram.png index 15ed39a..4685b1a 100644 Binary files a/docs/uml-diagrams/imgs/plantuml_class_diagram.png and b/docs/uml-diagrams/imgs/plantuml_class_diagram.png differ diff --git a/docs/uml-diagrams/imgs/plantuml_class_diagram.svg b/docs/uml-diagrams/imgs/plantuml_class_diagram.svg index ca99eae..3e63618 100644 --- a/docs/uml-diagrams/imgs/plantuml_class_diagram.svg +++ b/docs/uml-diagrams/imgs/plantuml_class_diagram.svg @@ -1 +1 @@ -SC2002 Turn-Based Combat Arena - Layered Class DiagramSC2002 Turn-Based Combat Arena - Layered Class DiagramDOMAIN LAYER - domain entitiesDOMAIN LAYER - status effectsDOMAIN LAYER - action strategiesENGINE LAYER - orchestration and setupENGINE LAYER - factories/bootstrapENGINE LAYER - report eventsUI LAYER - CLI boundaryUI LAYER - Swing GUI boundary/control«abstract»«entity»Combatant-combatantId: CombatantId-name: String-baseStats: CombatStats-inventory: Inventory-hitPoints: HitPoints-statusEffectRegistry: StatusEffectRegistry+combatantId(): CombatantId+getName(): String+getCurrentHp(): int+getAttack(): int+getDefense(): int+getSpeed(): int+isAlive(): boolean+attack(target: Combatant): AttackResolution+receiveDamage(damage: int): void+heal(amount: int): void+addStatusEffect(statusEffect: StatusEffect): List<CombatantStatusOutcome>+completeRound(): List<CombatantStatusOutcome>«entity»PlayerCharacter-specialSkill: SpecialSkill+canUseSpecialSkill(): boolean+useSpecialSkill(context: ActionExecutionContext, target: Combatant): List<BattleEvent>+advanceRoundState(): void«entity»EnemyCombatant-turnAction: BattleAction+takeTurn(context: ActionExecutionContext, target: Combatant): List<BattleEvent>«entity»Inventory-itemCounts: Map<ItemType, Integer>+add(itemType: ItemType, count: int): void+countOf(itemType: ItemType): int+use(itemType: ItemType): void+snapshot(): Map<ItemType, Integer>«entity»SpecialSkill-action: BattleAction-cooldownTurns: int-cooldownRemaining: int+isAvailable(): boolean+use(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>+advanceCooldown(): void+triggerCooldown(): void«record»«entity»AttackResolution-attackUsed: int-targetDefense: int-hpBefore: int-hpAfter: int-damage: int-targetEliminated: boolean-statusEffectOutcomes: List<CombatantStatusOutcome>+appendStatusEffectOutcomes(additionalOutcomes: List<CombatantStatusOutcome>): AttackResolution«record»«entity»CombatantId-value: UUID+generate(): CombatantId«record»«entity»CombatStats-attack: Stat-defense: Stat-speed: Stat+valueOf(statType: StatType): int+builder(): CombatStats.Builder+addFlat(type: StatType, amount: int): CombatStats+multiplyBy(type: StatType, factor: double): CombatStats«factory»CombatStats.Builder-attack: Stat-defense: Stat-speed: Stat+attack(attack: int): CombatStats.Builder+attack(attack: Stat): CombatStats.Builder+defense(defense: int): CombatStats.Builder+defense(defense: Stat): CombatStats.Builder+speed(speed: int): CombatStats.Builder+speed(speed: Stat): CombatStats.Builder+build(): CombatStats«record»«entity»HitPoints-current: int-max: int+full(maxHp: int): HitPoints+takeDamage(damage: int): HitPoints+heal(amount: int): HitPoints+isDead(): boolean«record»«entity»Stat-value: int+addFlat(amount: int): Stat+multiplyBy(factor: double): Stat+clampMinimum(minimum: int): Stat«enumeration»ItemTypePOTIONPOWER_STONESMOKE_BOMB-displayName: String-description: String+getDisplayName(): String+getDescription(): String«enumeration»StatTypeATTACKDEFENSESPEED«interface»«strategy»StatusEffect+kind(): StatusEffectKind+description(): String+mergeWith(other: StatusEffect): Optional<StatusEffect>+onApply(owner: Combatant): List<StatusEffectOutcome>+onRoundEnd(owner: Combatant): List<StatusEffectOutcome>+onExpire(owner: Combatant): List<StatusEffectOutcome>+modifyStats(stats: CombatStats): CombatStats+modifyIncomingDamage(owner: Combatant, attacker: Combatant, damage: int): DamageAdjustment+getTurnBlockReason(owner: Combatant): Optional<String>+isExpired(): boolean«entity»StatusEffectRegistry-effects: List<StatusEffect>-pendingOutcomes: List<StatusEffectOutcome>+add(owner: Combatant, statusEffect: StatusEffect): List<StatusEffectOutcome>+apply(owner: Combatant, stats: CombatStats): CombatStats+adjustIncomingDamage(owner: Combatant, attacker: Combatant, damage: int): DamageAdjustment+getTurnBlockReason(owner: Combatant): Optional<String>+completeRound(owner: Combatant): List<StatusEffectOutcome>+activeStatuses(owner: Combatant): List<String>+consumeOutcomes(): List<StatusEffectOutcome>«entity»ArcanePowerStatusEffect-attackBonus: int+modifyStats(stats: CombatStats): CombatStats+mergeWith(other: StatusEffect): Optional<StatusEffect>«entity»DefendStatusEffect-roundsRemaining: int+modifyStats(stats: CombatStats): CombatStats+onRoundEnd(owner: Combatant): List<StatusEffectOutcome>«entity»StrengthBoostStatusEffect-attackBonus: int-roundsRemaining: int+modifyStats(stats: CombatStats): CombatStats+onRoundEnd(owner: Combatant): List<StatusEffectOutcome>«entity»SmokeBombStatusEffect-chargesRemaining: int+modifyIncomingDamage(owner: Combatant, attacker: Combatant, damage: int): DamageAdjustment«entity»StunStatusEffect-blockedTurnsRemaining: int+getTurnBlockReason(owner: Combatant): Optional<String>«interface»StatusEffectOutcome«record»«entity»CombatantStatusOutcome-combatantId: CombatantId-combatantName: String-outcome: StatusEffectOutcome«record»«entity»DamageAdjustment-damage: int-modifiers: List<DamageModifier>«record»«entity»DamageModifier-source: StatusEffectKind-type: DamageModifierType«record»«entity»StatusEffectChange-source: StatusEffectKind-type: StatusEffectChangeType-magnitude: int-duration: int«enumeration»StatusEffectKindARCANE_POWERDEFENDSMOKE_BOMBSTRENGTH_BOOSTSTUN«enumeration»DamageModifierTypeBLOCKEDREDUCEDINCREASED«enumeration»StatusEffectChangeTypeAPPLIEDEXPIRED«interface»ActionExecutionContext+getLivingEnemies(): List<Combatant>+getLivingEnemiesInTurnOrder(): List<Combatant>«interface»«strategy»BattleAction+getName(): String+advancesCooldown(): boolean+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»BasicAttackAction+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»DefendAction+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»ShieldBashAction+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»ArcaneBlastAction+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»UsePotionAction+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»UseSmokeBombAction+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»UseSpecialSkillAction+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»UsePowerStoneSkillAction+advancesCooldown(): boolean+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«enumeration»TargetingModeNONESINGLE_ENEMY«control»BattleEngine-battleState: BattleState-turnOrderStrategy: TurnOrderStrategy-turnProcessor: TurnProcessor-waveManager: WaveManager-roundLifecycle: RoundLifecycle-battleOutcomeReporter: BattleOutcomeReporter+runRounds(roundCount: int, playerDecisionProvider: PlayerDecisionProvider): List<BattleEvent>+runRounds(roundCount: int, playerDecisionProvider: PlayerDecisionProvider, battleEventListener: BattleEventListener): List<BattleEvent>+runUntilBattleEnds(playerDecisionProvider: PlayerDecisionProvider): List<BattleEvent>+runUntilBattleEnds(playerDecisionProvider: PlayerDecisionProvider, battleEventListener: BattleEventListener): List<BattleEvent>+getLivingEnemies(): List<Combatant>+getLivingEnemiesInTurnOrder(): List<Combatant>«control»BattleEventPublisher-events: List<BattleEvent>~emit(battleEvent: BattleEvent, battleEventListener: BattleEventListener): void~snapshot(): List<BattleEvent>«entity»BattleState-player: PlayerCharacter-initialEnemies: List<Combatant>-reserveEnemies: List<Combatant>-spawnedEnemies: List<Combatant>~player(): PlayerCharacter~initialEnemies(): List<Combatant>~reserveEnemies(): List<Combatant>~spawnedEnemies(): List<Combatant>~moveAllReserveToSpawned(): void~combatantsAliveAtRoundStart(): List<Combatant>~livingEnemies(): List<Combatant>~isBattleOver(): boolean«interface»«strategy»TurnProcessor~processTurn(roundNumber: int, actor: Combatant, playerDecisionProvider: PlayerDecisionProvider, actionExecutionContext: ActionExecutionContext, emit: Consumer<BattleEvent>): void«control»DefaultTurnProcessor-player: PlayerCharacter+processTurn(roundNumber: int, actor: Combatant, playerDecisionProvider: PlayerDecisionProvider, actionExecutionContext: ActionExecutionContext, emit: Consumer<BattleEvent>): void«interface»«strategy»WaveManager~spawnBackupIfNeeded(emit: Consumer<BattleEvent>): void«control»DefaultWaveManager-initialEnemies: List<Combatant>-reserveEnemies: List<Combatant>-moveReserveToSpawned: Runnable+spawnBackupIfNeeded(emit: Consumer<BattleEvent>): void«interface»«strategy»RoundLifecycle~createRoundSummary(roundNumber: int): RoundSummaryEvent~completeRound(emit: Consumer<BattleEvent>): void«control»DefaultRoundLifecycle-player: PlayerCharacter-spawnedEnemies: List<Combatant>+createRoundSummary(roundNumber: int): RoundSummaryEvent+completeRound(emit: Consumer<BattleEvent>): void«interface»«strategy»BattleOutcomeReporter~reportOutcome(roundsPlayed: int, livingEnemies: List<Combatant>, reserveEnemies: List<Combatant>, emit: Consumer<BattleEvent>): void«control»DefaultBattleOutcomeReporter-player: PlayerCharacter+reportOutcome(roundsPlayed: int, livingEnemies: List<Combatant>, reserveEnemies: List<Combatant>, emit: Consumer<BattleEvent>): void«interface»«strategy»TurnOrderStrategy+determineOrder(combatants: List<Combatant>): List<Combatant>«strategy»SpeedTurnOrderStrategy+determineOrder(combatants: List<Combatant>): List<Combatant>«interface»«strategy»PlayerDecisionProvider+decide(roundNumber: int, player: PlayerCharacter, livingEnemies: List<Combatant>): PlayerDecision«control»ScriptedDecisionProvider-decisionsByRound: Map<Integer, PlayerDecision>+addDecision(roundNumber: int, decision: PlayerDecision): ScriptedDecisionProvider+decide(roundNumber: int, player: PlayerCharacter, livingEnemies: List<Combatant>): PlayerDecision«interface»«observer»BattleEventListener+onEvent(event: BattleEvent): void«control»«factory»BattleSetupFactory-combatantFactory: CombatantFactory+create(configuration: GameConfiguration): BattleSetup+createCustom(config: CustomGameConfiguration): BattleSetup«factory»«control»EasyLevelSetup+createWarriorPotionSmokeBombSetup(): BattleSetup«factory»«control»MediumLevelSetup+createWarriorPowerStonePotionSetup(): BattleSetup+createWizardPowerStonePotionSetup(): BattleSetup«factory»«control»HardLevelSetup+create(player: PlayerCharacter, inventory: Inventory): BattleSetup«control»AppendixAScenarios+all(): List<ScenarioScript>+easyWarrior(): ScenarioScript+mediumWarrior(): ScenarioScript+mediumWizard(): ScenarioScript«entity»ScenarioScript-name: String-battleSetup: BattleSetup-decisionProvider: PlayerDecisionProvider-roundCount: int+getName(): String+getBattleSetup(): BattleSetup+getDecisionProvider(): PlayerDecisionProvider+getRoundCount(): int«entity»BattleSetup-player: PlayerCharacter-initialEnemies: List<Combatant>-backupEnemies: List<Combatant>+getPlayer(): PlayerCharacter+getInitialEnemies(): List<Combatant>+getBackupEnemies(): List<Combatant>«record»«entity»GameConfiguration-playerType: PlayerType-difficultyLevel: DifficultyLevel-selectedItems: List<ItemType>«record»«entity»CustomGameConfiguration-playerType: PlayerType-selectedItems: List<ItemType>-waves: List<WaveSpec> [1..2]«record»«entity»PlayerDecision-action: BattleAction-targetReference: TargetReference+targeted(action: BattleAction, target: Combatant): PlayerDecision+targeted(action: BattleAction, targetId: CombatantId): PlayerDecision+untargeted(action: BattleAction): PlayerDecision«record»«entity»TargetReference-type: TargetType-combatantId: CombatantId+none(): TargetReference+enemy(combatantId: CombatantId): TargetReference+enemy(combatant: Combatant): TargetReference+resolveFrom(livingEnemies: List<Combatant>): Combatant«enumeration»TargetReference.TargetTypeNONEENEMY«record»«entity»WaveSpec-enemyCounts: List<EnemyCount>+totalEnemies(): int«record»«entity»EnemyCount-enemyFactory: EnemyFactory-count: int«interface»«factory»EnemyFactory+create(name: String, combatantFactory: CombatantFactory): EnemyCombatant«enumeration»«factory»EnemyTypeGOBLINWOLF-displayName: String-maxPerWave: int-baseHitPoints: HitPoints-baseStats: CombatStats+create(name: String, combatantFactory: CombatantFactory): EnemyCombatant«enumeration»«factory»PlayerTypeWARRIORWIZARD-displayName: String-specialSkillName: String-baseHitPoints: HitPoints-baseStats: CombatStats+createPlayer(combatantFactory: CombatantFactory): PlayerCharacter«enumeration»DifficultyLevelEASYMEDIUMHARD-displayName: String-initialEnemyCount: int-backupEnemyCount: int+getTotalEnemyCount(): int«interface»«factory»CombatantFactory+createPlayer(playerType: PlayerType): PlayerCharacter+createEnemy(enemyType: EnemyType, name: String): EnemyCombatant«factory»«control»DefaultCombatantFactory-statusEffectRegistryFactory: StatusEffectRegistryFactory-playerCreators: Map<PlayerType, PlayerCharacterCreator>-enemyCreators: Map<EnemyType, EnemyCombatantCreator>+createPlayer(playerType: PlayerType): PlayerCharacter+createEnemy(enemyType: EnemyType, name: String): EnemyCombatant«interface»«factory»PlayerCharacterCreator+create(statusEffectRegistry: StatusEffectRegistry): PlayerCharacter«interface»«factory»EnemyCombatantCreator+create(name: String, statusEffectRegistry: StatusEffectRegistry): EnemyCombatant«interface»«factory»SpecialSkillFactory+create(): SpecialSkill«factory»«control»DefaultSpecialSkillFactory-action: BattleAction-cooldownTurns: int+create(): SpecialSkill«interface»«factory»StatusEffectRegistryFactory+create(): StatusEffectRegistry«factory»«control»DefaultStatusEffectRegistryFactory-statusEffectRegistrySupplier: Supplier<StatusEffectRegistry>+create(): StatusEffectRegistry«factory»«control»CombatantFactories+createDefault(...): CombatantFactory«interface»«event»BattleEvent«event»ActionEvent-actorId: CombatantId-actorName: String-actionName: String-targetId: CombatantId-targetName: String-damage: int-targetEliminated: boolean«event»RoundStartEvent-roundNumber: int+getRoundNumber(): int«event»RoundSummaryEvent-roundNumber: int-playerSummary: CombatantSummary-enemySummaries: List<CombatantSummary>-inventorySnapshot: Map<ItemType, Integer>«event»SkippedTurnEvent-combatantId: CombatantId-combatantName: String-reason: String-statusEffectNotes: List<String>«record»«event»StatusEffectReportEvent-statusEffectNotes: List<String>+fromStatusEffectOutcomes(outcomes: List<CombatantStatusOutcome>): StatusEffectReportEvent«control»StatusEffectReportMapper~toNotes(statusEffectOutcomes: List<CombatantStatusOutcome>): List<String>«event»NarrationEvent-text: String+getText(): String«record»«entity»CombatantSummary-combatantId: CombatantId-name: String-currentHp: int-maxHp: int-currentAttack: int-baseAttack: int-alive: boolean-activeStatuses: List<String>«boundary»TurnBasedArenaCli-ui: ConsoleBattleUi-battleSetupFactory: BattleSetupFactory-formatter: BattleConsoleFormatter-turnOrderStrategy: TurnOrderStrategy+run(): void«boundary»EasyRoundsDemo+main(args: String[]): void«boundary»ConsoleBattleUi+promptForPlayerType(): PlayerType+promptForDifficultyOrCustom(): DifficultyLevel+promptForWaveSpec(waveNumber: int): WaveSpec+promptForItems(itemCount: int): List<ItemType>+showBattleTranscript(lines: List<String>): void+promptPostGameChoice(): PostGameChoice«boundary»BattleConsoleFormatter+format(events: List<BattleEvent>): List<String>«boundary»CliPlayerDecisionProvider-ui: ConsoleBattleUi+decide(roundNumber: int, player: PlayerCharacter, livingEnemies: List<Combatant>): PlayerDecision«enumeration»PostGameChoiceREPLAYNEW_GAMEEXIT«boundary»TurnBasedArenaGui-controller: BattleController-setupPanel: BattleSetupPanel-arenaScene: ArenaScenePanel-commandPanel: BattleCommandPanel«interface»«boundary»BattleView+showSetupPreview(): void+showBattleLoaded(setup: BattleSetup): void+showBattleEvent(event: BattleEvent, message: String, transcriptLines: List<String>): void+showPlayerTurn(turn: PlayerTurnRequest): void+selectedEnemyId(): CombatantId«boundary»BattleSetupPanel-startListener: Consumer<BattleLaunchRequest>-layoutChangedListener: Runnable-battleRunning: boolean+setSetupControlsEnabled(enabled: boolean): void«boundary»ArenaScenePanel-model: ArenaSceneModel-renderer: ArenaSceneRenderer-sceneLoop: ArenaSceneLoop+showSetupPreview(): void+startBattle(setup: BattleSetup): void+showPlayerTurn(round: int, player: PlayerCharacter, livingEnemies: List<Combatant>): void+applyBattleEvent(event: BattleEvent): void«entity»ArenaSceneModel-sprites: Map<CombatantId, FighterSpriteDto>-enemyOrder: List<CombatantId>-pressedDirections: Set<String>-floatingTexts: List<FloatingText>-selectedEnemyId: CombatantId-acceptingPlayerTurn: boolean-battleActive: boolean~showSetupPreview(): void~startBattle(setup: BattleSetup, arenaWidth: int, arenaHeight: int): void~showPlayerTurn(currentRound: int, player: PlayerCharacter, livingEnemies: List<Combatant>, arenaWidth: int, arenaHeight: int): void~completePlayerTurn(actionName: String): void~selectNextEnemy(direction: int): void~selectEnemyAt(point: Point): boolean~setDirectionPressed(direction: String, pressed: boolean): void~applyBattleEvent(event: BattleEvent, arenaWidth: int, arenaHeight: int): void~tick(arenaWidth: int, arenaHeight: int): void~spritesByDrawOrder(): List<FighterSpriteDto>~floatingTexts(): List<FloatingText>~playerSprite(): FighterSpriteDto~currentSelectedEnemySprite(): FighterSpriteDto«boundary»ArenaSceneRenderer-fighterRenderer: FighterSpriteRenderer~render(g: Graphics2D, model: ArenaSceneModel, width: int, height: int, now: long): void«control»ArenaSceneLoop-tickCallback: Runnable-tickTimer: Timer~start(): void~stop(): void«boundary»FighterSpriteRenderer-bodyRenderers: Map<FighterType, FighterBodyRenderer>-effectRenderer: FighterSpriteEffectRenderer-hudRenderer: FighterSpriteHudRenderer+render(g: Graphics2D, sprite: FighterSpriteDto, selectedEnemyId: CombatantId): void~bodyRendererFor(sprite: FighterSpriteDto): FighterBodyRenderer«interface»«boundary»FighterBodyRenderer~renderBody(g: Graphics2D, sprite: FighterSpriteDto): void«boundary»FighterSpriteEffectRenderer~renderPulse(g: Graphics2D, sprite: FighterSpriteDto): void«boundary»FighterSpriteHudRenderer~render(g: Graphics2D, sprite: FighterSpriteDto, centerX: int, baseY: int): void~healthRatio(sprite: FighterSpriteDto): double«boundary»WarriorBodyRenderer«boundary»WizardBodyRenderer«boundary»GoblinBodyRenderer«boundary»WolfBodyRenderer«boundary»UnknownFighterBodyRenderer«entity»FighterSpriteDto-id: CombatantId-player: boolean-type: FighterType-name: String-hp: int-maxHp: int-statuses: List<String>+fromCombatant(combatant: Combatant, player: boolean): FighterSpriteDto+fromSummary(summary: CombatantSummary, player: boolean): FighterSpriteDto+updateFrom(combatant: Combatant): void+updateFrom(summary: CombatantSummary): void+bounds(): Shape«enumeration»FighterTypeWARRIORWIZARDGOBLINWOLFUNKNOWN+fromName(name: String, player: boolean): FighterType«record»«entity»ArenaSceneModel.FloatingText-text: String-x: double-y: double-createdAt: long-damage: boolean«boundary»BattleCommandPanel-promptLabel: JLabel-optionButtons: JButton[]-commandListener: Consumer<BattleCommandPanel.Command>+setCommandListener(commandListener: Consumer<BattleCommandPanel.Command>): void+setIdle(message: String): void+showTurn(round: int, player: PlayerCharacter, livingEnemies: List<Combatant>, selectedTargetLabel: String): void+updateTarget(selectedTargetLabel: String): void+setResolving(actionName: String): void+showBattleMessage(message: String): void+showBattleComplete(): void«enumeration»BattleCommandPanel.CommandBASIC_ATTACKDEFENDPOTIONSPECIAL_SKILLPOWER_STONESMOKE_BOMBPREVIOUS_TARGETNEXT_TARGET«enumeration»BattleCommandPanel.MenuStateIDLEROOTFIGHTBAGTARGETRESOLVING«enumeration»gui.PostGameChoiceREPLAYNEW_SETUPEXIT«control»BattleController-view: BattleView-model: BattleSessionModel-playbackController: BattlePlaybackController-commandResolver: PlayerCommandResolver-setupFactory: BattleSetupFactory+startBattle(request: BattleLaunchRequest): void+handleCommand(command: BattleCommandPanel.Command): void«control»GuiPlayerDecisionProvider-controller: BattleController+decide(roundNumber: int, player: PlayerCharacter, livingEnemies: List<Combatant>): PlayerDecision«control»PlayerCommandResolver+resolve(command: BattleCommandPanel.Command, turn: PlayerTurnRequest, selectedTarget: CombatantId): Optional<ResolvedPlayerCommand>«entity»BattleSessionModel-battleRunning: boolean-activePlayerTurn: PlayerTurnRequest-queuedPlayerTurn: PlayerTurnRequest-queuedPostGameConfig: PostGameConfig+beginBattle(): boolean+finishBattle(): void+stopBattle(): void+clearQueuedPlaybackState(): void+activePlayerTurn(): Optional<PlayerTurnRequest>+clearActivePlayerTurn(): void+queuePlayerTurn(turn: PlayerTurnRequest): void+takeQueuedPlayerTurn(): Optional<PlayerTurnRequest>+queuePostGame(configuration: PostGameConfig): void+takeQueuedPostGameConfig(): Optional<PostGameConfig>«control»BattlePlaybackController-events: Queue<BattleEvent>-dialogueFormatter: BattleDialogueFormatter-playbackTimer: Timer-active: boolean+enqueue(event: BattleEvent): void+playNextIfIdle(): void+reset(): void«control»BattleDialogueFormatter+format(event: BattleEvent): String+playbackDelayMillis(event: BattleEvent): int«entity»BattleLaunchRequest-setupCreator: Function<BattleSetupFactory, BattleSetup>-replayConfiguration: PostGameConfig-intro: String+preset(configuration: GameConfiguration): BattleLaunchRequest+custom(configuration: CustomGameConfiguration): BattleLaunchRequest+replay(configuration: PostGameConfig): BattleLaunchRequest+createSetup(setupFactory: BattleSetupFactory): BattleSetup+replayConfiguration(): PostGameConfig+intro(): String«interface»«entity»PostGameConfig+preset(configuration: GameConfiguration): PostGameConfig+custom(configuration: CustomGameConfiguration): PostGameConfig«record»«entity»PostGameConfig.Preset-configuration: GameConfiguration«record»«entity»PostGameConfig.Custom-configuration: CustomGameConfiguration«record»«entity»PlayerTurnRequest-roundNumber: int-player: PlayerCharacter-livingEnemies: List<Combatant>-responseQueue: BlockingQueue<PlayerDecision>«record»«entity»ResolvedPlayerCommand-decision: PlayerDecision-actionName: String-targetLabel: String«control»SwingThread+runAndWait(runnable: Runnable): voidEncapsulation:private state changes throughbehavior methods.Inheritance:PlayerCharacter and EnemyCombatantspecialize Combatant.Composition:Combatant owns HitPoints, Inventory,CombatStats, and StatusEffectRegistry.Polymorphism / OCP:actions share execute(); new actionsavoid BattleEngine changes.Polymorphism / OCP:StatusEffectRegistry applies effectsthrough StatusEffect hooks.Dependency inversion:BattleEngine coordinates throughstrategy interfaces.Observer:records events and notifiesBattleEventListener.Factory:setup and combatant constructionare centralized.SRP:panel handles Swing, model stores state,renderer draws, loop ticks.Boundary-Control:UI delegates battle flow toBattleController and BattleEngine.hp11inventory11baseStats11status effects11specialSkill11turnAction11action11stats13active effects10..*10..*111111111111events10..*notifieswaves11..211..*111111111111111111sprites10..*floatingTexts10..*11bodyRenderers11..*1111Focus: classes and interfaces grouped by layer:UI Boundary -> Engine Control -> Domain Entity.Includes all production classes, key attributes/methods,inheritance, ownership, and major architectural links. Type labels:«interface», «enumeration», and «record»identify non-standard class boxes explicitly.Plain rectangles are concrete classes. UML notation:<|-- inheritance<|.. interface implementation-- composition / strong ownershipo-- aggregation / collection reference--> association or major dependency To keep the generated image readable, local helper,enum/value-object, and method-parameter-only uses areshown through signatures instead of extra arrows. OO principle annotations:Encapsulation - CombatantInheritance - PlayerCharacter / EnemyCombatantComposition - Combatant owns HitPoints, Inventory,CombatStats, and StatusEffectRegistryPolymorphism / OCP - BattleAction and StatusEffectDependency inversion - BattleEngine depends on strategyinterfaces such as TurnProcessor and WaveManagerObserver - BattleEventPublisher + BattleEventListenerFactory - BattleSetupFactory and CombatantFactorySRP - ArenaScenePanel, ArenaSceneModel, ArenaSceneRendererBoundary-Control - UI classes delegate battle flow toBattleController and BattleEngine \ No newline at end of file +SC2002 Turn-Based Combat Arena - Layered Class DiagramSC2002 Turn-Based Combat Arena - Layered Class DiagramDOMAIN LAYER - domain entitiesDOMAIN LAYER - status effectsDOMAIN LAYER - action strategiesENGINE LAYER - orchestration and setupENGINE LAYER - factories/bootstrapENGINE LAYER - report eventsUI LAYER - CLI boundaryUI LAYER - Swing GUI boundary/control«abstract»«entity»Combatant-combatantId: CombatantId-name: String-baseStats: CombatStats-inventory: Inventory-hitPoints: HitPoints-statusEffectRegistry: StatusEffectRegistry+combatantId(): CombatantId+getName(): String+getCurrentHp(): int+getAttack(): int+getDefense(): int+getSpeed(): int+isAlive(): boolean+attack(target: Combatant): AttackResolution+receiveDamage(damage: int): void+heal(amount: int): void+addStatusEffect(statusEffect: StatusEffect): List<CombatantStatusOutcome>+completeRound(): List<CombatantStatusOutcome>«entity»PlayerCharacter-specialSkill: SpecialSkill+canUseSpecialSkill(): boolean+useSpecialSkill(context: ActionExecutionContext, target: Combatant): List<BattleEvent>+advanceRoundState(): void«entity»EnemyCombatant-turnAction: BattleAction+takeTurn(context: ActionExecutionContext, target: Combatant): List<BattleEvent>«entity»Inventory-itemCounts: Map<ItemType, Integer>+add(itemType: ItemType, count: int): void+countOf(itemType: ItemType): int+use(itemType: ItemType): void+snapshot(): Map<ItemType, Integer>«entity»SpecialSkill-action: BattleAction-cooldownTurns: int-cooldownRemaining: int+isAvailable(): boolean+use(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>+advanceCooldown(): void+triggerCooldown(): void«record»«entity»AttackResolution-attackUsed: int-targetDefense: int-hpBefore: int-hpAfter: int-damage: int-targetEliminated: boolean-statusEffectOutcomes: List<CombatantStatusOutcome>+appendStatusEffectOutcomes(additionalOutcomes: List<CombatantStatusOutcome>): AttackResolution«record»«entity»CombatantId-value: UUID+generate(): CombatantId«record»«entity»CombatStats-attack: Stat-defense: Stat-speed: Stat+valueOf(statType: StatType): int+builder(): CombatStats.Builder+addFlat(type: StatType, amount: int): CombatStats+multiplyBy(type: StatType, factor: double): CombatStats«factory»CombatStats.Builder-attack: Stat-defense: Stat-speed: Stat+attack(attack: int): CombatStats.Builder+attack(attack: Stat): CombatStats.Builder+defense(defense: int): CombatStats.Builder+defense(defense: Stat): CombatStats.Builder+speed(speed: int): CombatStats.Builder+speed(speed: Stat): CombatStats.Builder+build(): CombatStats«record»«entity»HitPoints-current: int-max: int+full(maxHp: int): HitPoints+takeDamage(damage: int): HitPoints+heal(amount: int): HitPoints+isDead(): boolean«record»«entity»Stat-value: int+addFlat(amount: int): Stat+multiplyBy(factor: double): Stat+clampMinimum(minimum: int): Stat«enumeration»ItemTypePOTIONPOWER_STONESMOKE_BOMB-displayName: String-description: String+getDisplayName(): String+getDescription(): String«enumeration»StatTypeATTACKDEFENSESPEED«interface»«strategy»StatusEffect+kind(): StatusEffectKind+description(): String+mergeWith(other: StatusEffect): Optional<StatusEffect>+onApply(owner: Combatant): List<StatusEffectOutcome>+onRoundEnd(owner: Combatant): List<StatusEffectOutcome>+onExpire(owner: Combatant): List<StatusEffectOutcome>+modifyStats(stats: CombatStats): CombatStats+modifyIncomingDamage(owner: Combatant, attacker: Combatant, damage: int): DamageAdjustment+getTurnBlockReason(owner: Combatant): Optional<String>+isExpired(): boolean«entity»StatusEffectRegistry-effects: List<StatusEffect>-pendingOutcomes: List<StatusEffectOutcome>+add(owner: Combatant, statusEffect: StatusEffect): List<StatusEffectOutcome>+apply(owner: Combatant, stats: CombatStats): CombatStats+adjustIncomingDamage(owner: Combatant, attacker: Combatant, damage: int): DamageAdjustment+getTurnBlockReason(owner: Combatant): Optional<String>+completeRound(owner: Combatant): List<StatusEffectOutcome>+activeStatuses(owner: Combatant): List<String>+consumeOutcomes(): List<StatusEffectOutcome>«entity»ArcanePowerStatusEffect-attackBonus: int+modifyStats(stats: CombatStats): CombatStats+mergeWith(other: StatusEffect): Optional<StatusEffect>«entity»DefendStatusEffect-roundsRemaining: int+modifyStats(stats: CombatStats): CombatStats+onRoundEnd(owner: Combatant): List<StatusEffectOutcome>«entity»StrengthBoostStatusEffect-attackBonus: int-roundsRemaining: int+modifyStats(stats: CombatStats): CombatStats+onRoundEnd(owner: Combatant): List<StatusEffectOutcome>«entity»SmokeBombStatusEffect-chargesRemaining: int+modifyIncomingDamage(owner: Combatant, attacker: Combatant, damage: int): DamageAdjustment«entity»StunStatusEffect-blockedTurnsRemaining: int+getTurnBlockReason(owner: Combatant): Optional<String>«interface»StatusEffectOutcome«record»«entity»CombatantStatusOutcome-combatantId: CombatantId-combatantName: String-outcome: StatusEffectOutcome«record»«entity»DamageAdjustment-damage: int-modifiers: List<DamageModifier>«record»«entity»DamageModifier-source: StatusEffectKind-type: DamageModifierType«record»«entity»StatusEffectChange-source: StatusEffectKind-type: StatusEffectChangeType-magnitude: int-duration: int«enumeration»StatusEffectKindARCANE_POWERDEFENDSMOKE_BOMBSTRENGTH_BOOSTSTUN«enumeration»DamageModifierTypeBLOCKEDREDUCEDINCREASED«enumeration»StatusEffectChangeTypeAPPLIEDEXPIRED«interface»ActionExecutionContext+getLivingEnemies(): List<Combatant>+getLivingEnemiesInTurnOrder(): List<Combatant>«interface»«strategy»BattleAction+getName(): String+advancesCooldown(): boolean+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»BasicAttackAction+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»DefendAction+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»ShieldBashAction+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»ArcaneBlastAction+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»UsePotionAction+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»UseSmokeBombAction+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»UseSpecialSkillAction+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«strategy»UsePowerStoneSkillAction+advancesCooldown(): boolean+targetingMode(actor: Combatant): TargetingMode+execute(context: ActionExecutionContext, actor: Combatant, target: Combatant): List<BattleEvent>«enumeration»TargetingModeNONESINGLE_ENEMY«control»BattleEngine-battleState: BattleState-turnOrderStrategy: TurnOrderStrategy-turnProcessor: TurnProcessor-waveManager: WaveManager-roundLifecycle: RoundLifecycle-battleOutcomeReporter: BattleOutcomeReporter+runRounds(roundCount: int, playerDecisionProvider: PlayerDecisionProvider): List<BattleEvent>+runRounds(roundCount: int, playerDecisionProvider: PlayerDecisionProvider, battleEventListener: BattleEventListener): List<BattleEvent>+runUntilBattleEnds(playerDecisionProvider: PlayerDecisionProvider): List<BattleEvent>+runUntilBattleEnds(playerDecisionProvider: PlayerDecisionProvider, battleEventListener: BattleEventListener): List<BattleEvent>+getLivingEnemies(): List<Combatant>+getLivingEnemiesInTurnOrder(): List<Combatant>«control»BattleEventPublisher-events: List<BattleEvent>~emit(battleEvent: BattleEvent, battleEventListener: BattleEventListener): void~snapshot(): List<BattleEvent>«entity»BattleState-player: PlayerCharacter-initialEnemies: List<Combatant>-reserveEnemies: List<Combatant>-spawnedEnemies: List<Combatant>~player(): PlayerCharacter~initialEnemies(): List<Combatant>~reserveEnemies(): List<Combatant>~spawnedEnemies(): List<Combatant>~moveAllReserveToSpawned(): void~combatantsAliveAtRoundStart(): List<Combatant>~livingEnemies(): List<Combatant>~isBattleOver(): boolean«interface»«strategy»TurnProcessor~processTurn(roundNumber: int, actor: Combatant, playerDecisionProvider: PlayerDecisionProvider, actionExecutionContext: ActionExecutionContext, emit: Consumer<BattleEvent>): void«control»DefaultTurnProcessor-player: PlayerCharacter+processTurn(roundNumber: int, actor: Combatant, playerDecisionProvider: PlayerDecisionProvider, actionExecutionContext: ActionExecutionContext, emit: Consumer<BattleEvent>): void«interface»«strategy»WaveManager~spawnBackupIfNeeded(emit: Consumer<BattleEvent>): void«control»DefaultWaveManager-initialEnemies: List<Combatant>-reserveEnemies: List<Combatant>-moveReserveToSpawned: Runnable+spawnBackupIfNeeded(emit: Consumer<BattleEvent>): void«interface»«strategy»RoundLifecycle~createRoundSummary(roundNumber: int): RoundSummaryEvent~completeRound(emit: Consumer<BattleEvent>): void«control»DefaultRoundLifecycle-player: PlayerCharacter-spawnedEnemies: List<Combatant>+createRoundSummary(roundNumber: int): RoundSummaryEvent+completeRound(emit: Consumer<BattleEvent>): void«interface»«strategy»BattleOutcomeReporter~reportOutcome(roundsPlayed: int, livingEnemies: List<Combatant>, reserveEnemies: List<Combatant>, emit: Consumer<BattleEvent>): void«control»DefaultBattleOutcomeReporter-player: PlayerCharacter+reportOutcome(roundsPlayed: int, livingEnemies: List<Combatant>, reserveEnemies: List<Combatant>, emit: Consumer<BattleEvent>): void«interface»«strategy»TurnOrderStrategy+determineOrder(combatants: List<Combatant>): List<Combatant>«strategy»SpeedTurnOrderStrategy+determineOrder(combatants: List<Combatant>): List<Combatant>«interface»«strategy»PlayerDecisionProvider+decide(roundNumber: int, player: PlayerCharacter, livingEnemies: List<Combatant>): PlayerDecision«control»ScriptedDecisionProvider-decisionsByRound: Map<Integer, PlayerDecision>+addDecision(roundNumber: int, decision: PlayerDecision): ScriptedDecisionProvider+decide(roundNumber: int, player: PlayerCharacter, livingEnemies: List<Combatant>): PlayerDecision«interface»«observer»BattleEventListener+onEvent(event: BattleEvent): void«control»«factory»BattleSetupFactory-combatantFactory: CombatantFactory+create(configuration: GameConfiguration): BattleSetup+createCustom(config: CustomGameConfiguration): BattleSetup«factory»«control»EasyLevelSetup+createWarriorPotionSmokeBombSetup(): BattleSetup«factory»«control»MediumLevelSetup+createWarriorPowerStonePotionSetup(): BattleSetup+createWizardPowerStonePotionSetup(): BattleSetup«factory»«control»HardLevelSetup+create(player: PlayerCharacter, inventory: Inventory): BattleSetup«control»AppendixAScenarios+all(): List<ScenarioScript>+easyWarrior(): ScenarioScript+mediumWarrior(): ScenarioScript+mediumWizard(): ScenarioScript«entity»ScenarioScript-name: String-battleSetup: BattleSetup-decisionProvider: PlayerDecisionProvider-roundCount: int+getName(): String+getBattleSetup(): BattleSetup+getDecisionProvider(): PlayerDecisionProvider+getRoundCount(): int«entity»BattleSetup-player: PlayerCharacter-initialEnemies: List<Combatant>-backupEnemies: List<Combatant>+getPlayer(): PlayerCharacter+getInitialEnemies(): List<Combatant>+getBackupEnemies(): List<Combatant>«record»«entity»GameConfiguration-playerType: PlayerType-difficultyLevel: DifficultyLevel-selectedItems: List<ItemType>«record»«entity»CustomGameConfiguration-playerType: PlayerType-selectedItems: List<ItemType>-waves: List<WaveSpec> [1..2]«record»«entity»PlayerDecision-action: BattleAction-targetReference: TargetReference+targeted(action: BattleAction, target: Combatant): PlayerDecision+targeted(action: BattleAction, targetId: CombatantId): PlayerDecision+untargeted(action: BattleAction): PlayerDecision«record»«entity»TargetReference-type: TargetType-combatantId: CombatantId+none(): TargetReference+enemy(combatantId: CombatantId): TargetReference+enemy(combatant: Combatant): TargetReference+resolveFrom(livingEnemies: List<Combatant>): Combatant«enumeration»TargetReference.TargetTypeNONEENEMY«record»«entity»WaveSpec-enemyCounts: List<EnemyCount>+totalEnemies(): int«record»«entity»EnemyCount-enemyFactory: EnemyFactory-count: int«interface»«factory»EnemyFactory+create(name: String, combatantFactory: CombatantFactory): EnemyCombatant«enumeration»«factory»EnemyTypeGOBLINWOLF-displayName: String-maxPerWave: int-baseHitPoints: HitPoints-baseStats: CombatStats+create(name: String, combatantFactory: CombatantFactory): EnemyCombatant«enumeration»«factory»PlayerTypeWARRIORWIZARD-displayName: String-specialSkillName: String-baseHitPoints: HitPoints-baseStats: CombatStats+createPlayer(combatantFactory: CombatantFactory): PlayerCharacter«enumeration»DifficultyLevelEASYMEDIUMHARD-displayName: String-initialEnemyCount: int-backupEnemyCount: int+getTotalEnemyCount(): int«interface»«factory»CombatantFactory+createPlayer(playerType: PlayerType): PlayerCharacter+createEnemy(enemyType: EnemyType, name: String): EnemyCombatant«factory»«control»DefaultCombatantFactory-statusEffectRegistryFactory: StatusEffectRegistryFactory-playerCreators: Map<PlayerType, PlayerCharacterCreator>-enemyCreators: Map<EnemyType, EnemyCombatantCreator>+createPlayer(playerType: PlayerType): PlayerCharacter+createEnemy(enemyType: EnemyType, name: String): EnemyCombatant«interface»«factory»PlayerCharacterCreator+create(statusEffectRegistry: StatusEffectRegistry): PlayerCharacter«interface»«factory»EnemyCombatantCreator+create(name: String, statusEffectRegistry: StatusEffectRegistry): EnemyCombatant«interface»«factory»SpecialSkillFactory+create(): SpecialSkill«factory»«control»DefaultSpecialSkillFactory-action: BattleAction-cooldownTurns: int+create(): SpecialSkill«interface»«factory»StatusEffectRegistryFactory+create(): StatusEffectRegistry«factory»«control»DefaultStatusEffectRegistryFactory-statusEffectRegistrySupplier: Supplier<StatusEffectRegistry>+create(): StatusEffectRegistry«factory»«control»CombatantFactories+createDefault(...): CombatantFactory«interface»«event»BattleEvent«event»ActionEvent-actorId: CombatantId-actorName: String-actionName: String-targetId: CombatantId-targetName: String-damage: int-targetEliminated: boolean«event»RoundStartEvent-roundNumber: int+getRoundNumber(): int«event»RoundSummaryEvent-roundNumber: int-playerSummary: CombatantSummary-enemySummaries: List<CombatantSummary>-inventorySnapshot: Map<ItemType, Integer>«event»SkippedTurnEvent-combatantId: CombatantId-combatantName: String-reason: String-statusEffectNotes: List<String>«record»«event»StatusEffectReportEvent-statusEffectNotes: List<String>+fromStatusEffectOutcomes(outcomes: List<CombatantStatusOutcome>): StatusEffectReportEvent«control»StatusEffectReportMapper~toNotes(statusEffectOutcomes: List<CombatantStatusOutcome>): List<String>«event»NarrationEvent-text: String+getText(): String«record»«entity»CombatantSummary-combatantId: CombatantId-name: String-currentHp: int-maxHp: int-currentAttack: int-baseAttack: int-alive: boolean-activeStatuses: List<String>«boundary»TurnBasedArenaCli-ui: ConsoleBattleUi-battleSetupFactory: BattleSetupFactory-formatter: BattleConsoleFormatter-turnOrderStrategy: TurnOrderStrategy+run(): void«boundary»EasyRoundsDemo+main(args: String[]): void«boundary»ConsoleBattleUi+promptForPlayerType(): PlayerType+promptForDifficultyOrCustom(): DifficultyLevel+promptForWaveSpec(waveNumber: int): WaveSpec+promptForItems(itemCount: int): List<ItemType>+showBattleTranscript(lines: List<String>): void+promptPostGameChoice(): PostGameChoice«boundary»BattleConsoleFormatter+format(events: List<BattleEvent>): List<String>«boundary»CliPlayerDecisionProvider-ui: ConsoleBattleUi+decide(roundNumber: int, player: PlayerCharacter, livingEnemies: List<Combatant>): PlayerDecision«enumeration»PostGameChoiceREPLAYNEW_GAMEEXIT«boundary»TurnBasedArenaGui-controller: BattleController-setupPanel: BattleSetupPanel-arenaScene: ArenaScenePanel-commandPanel: BattleCommandPanel«interface»«boundary»BattleView+showSetupPreview(): void+showBattleLoaded(setup: BattleSetup): void+showBattleEvent(event: BattleEvent, message: String, transcriptLines: List<String>): void+showPlayerTurn(turn: PlayerTurnRequest): void+selectedEnemyId(): CombatantId«boundary»BattleSetupPanel-startListener: Consumer<BattleLaunchRequest>-layoutChangedListener: Runnable-battleRunning: boolean+setSetupControlsEnabled(enabled: boolean): void«boundary»ArenaScenePanel-model: ArenaSceneModel-renderer: ArenaSceneRenderer-sceneLoop: ArenaSceneLoop+showSetupPreview(): void+startBattle(setup: BattleSetup): void+showPlayerTurn(round: int, player: PlayerCharacter, livingEnemies: List<Combatant>): void+applyBattleEvent(event: BattleEvent): void«entity»ArenaSceneModel-sprites: Map<CombatantId, FighterSpriteDto>-enemyOrder: List<CombatantId>-pressedDirections: Set<String>-floatingTexts: List<FloatingText>-selectedEnemyId: CombatantId-acceptingPlayerTurn: boolean-battleActive: boolean~showSetupPreview(): void~startBattle(setup: BattleSetup, arenaWidth: int, arenaHeight: int): void~showPlayerTurn(currentRound: int, player: PlayerCharacter, livingEnemies: List<Combatant>, arenaWidth: int, arenaHeight: int): void~completePlayerTurn(actionName: String): void~selectNextEnemy(direction: int): void~selectEnemyAt(point: Point): boolean~setDirectionPressed(direction: String, pressed: boolean): void~applyBattleEvent(event: BattleEvent, arenaWidth: int, arenaHeight: int): void~tick(arenaWidth: int, arenaHeight: int): void~spritesByDrawOrder(): List<FighterSpriteDto>~floatingTexts(): List<FloatingText>~playerSprite(): FighterSpriteDto~currentSelectedEnemySprite(): FighterSpriteDto«boundary»ArenaSceneRenderer-backgroundRenderer: ArenaBackgroundRenderer-fighterRenderer: FighterSpriteRenderer~render(g: Graphics2D, model: ArenaSceneModel, width: int, height: int, now: long): void«boundary»ArenaBackgroundRenderer~BACKGROUND_RESOURCE: String-source: BufferedImage-scaledBackground: BufferedImage~render(g: Graphics2D, width: int, height: int): void«control»ArenaSceneLoop-tickCallback: Runnable-tickTimer: Timer~start(): void~stop(): void«boundary»FighterSpriteRenderer-bodyRenderers: Map<FighterType, FighterBodyRenderer>-effectRenderer: FighterSpriteEffectRenderer-hudRenderer: FighterSpriteHudRenderer+render(g: Graphics2D, sprite: FighterSpriteDto, selectedEnemyId: CombatantId): void~bodyRendererFor(sprite: FighterSpriteDto): FighterBodyRenderer«interface»«boundary»FighterBodyRenderer~renderBody(g: Graphics2D, sprite: FighterSpriteDto): void«boundary»FighterSpriteEffectRenderer~renderPulse(g: Graphics2D, sprite: FighterSpriteDto): void«boundary»FighterSpriteHudRenderer~render(g: Graphics2D, sprite: FighterSpriteDto, centerX: int, baseY: int): void~healthRatio(sprite: FighterSpriteDto): double«boundary»WarriorBodyRenderer«boundary»WizardBodyRenderer«boundary»GoblinBodyRenderer«boundary»WolfBodyRenderer«boundary»UnknownFighterBodyRenderer«entity»FighterSpriteDto-id: CombatantId-player: boolean-type: FighterType-name: String-hp: int-maxHp: int-statuses: List<String>+fromCombatant(combatant: Combatant, player: boolean): FighterSpriteDto+fromSummary(summary: CombatantSummary, player: boolean): FighterSpriteDto+updateFrom(combatant: Combatant): void+updateFrom(summary: CombatantSummary): void+bounds(): Shape«enumeration»FighterTypeWARRIORWIZARDGOBLINWOLFUNKNOWN+fromName(name: String, player: boolean): FighterType«record»«entity»ArenaSceneModel.FloatingText-text: String-x: double-y: double-createdAt: long-damage: boolean«boundary»BattleCommandPanel-promptLabel: JLabel-optionButtons: JButton[]-commandListener: Consumer<BattleCommandPanel.Command>+setCommandListener(commandListener: Consumer<BattleCommandPanel.Command>): void+setIdle(message: String): void+showTurn(round: int, player: PlayerCharacter, livingEnemies: List<Combatant>, selectedTargetLabel: String): void+updateTarget(selectedTargetLabel: String): void+setResolving(actionName: String): void+showBattleMessage(message: String): void+showBattleComplete(): void«enumeration»BattleCommandPanel.CommandBASIC_ATTACKDEFENDPOTIONSPECIAL_SKILLPOWER_STONESMOKE_BOMBPREVIOUS_TARGETNEXT_TARGET«enumeration»BattleCommandPanel.MenuStateIDLEROOTFIGHTBAGTARGETRESOLVING«enumeration»gui.PostGameChoiceREPLAYNEW_SETUPEXIT«control»BattleController-view: BattleView-model: BattleSessionModel-playbackController: BattlePlaybackController-commandResolver: PlayerCommandResolver-setupFactory: BattleSetupFactory+startBattle(request: BattleLaunchRequest): void+handleCommand(command: BattleCommandPanel.Command): void«control»GuiPlayerDecisionProvider-controller: BattleController+decide(roundNumber: int, player: PlayerCharacter, livingEnemies: List<Combatant>): PlayerDecision«control»PlayerCommandResolver+resolve(command: BattleCommandPanel.Command, turn: PlayerTurnRequest, selectedTarget: CombatantId): Optional<ResolvedPlayerCommand>«entity»BattleSessionModel-battleRunning: boolean-activePlayerTurn: PlayerTurnRequest-queuedPlayerTurn: PlayerTurnRequest-queuedPostGameConfig: PostGameConfig+beginBattle(): boolean+finishBattle(): void+stopBattle(): void+clearQueuedPlaybackState(): void+activePlayerTurn(): Optional<PlayerTurnRequest>+clearActivePlayerTurn(): void+queuePlayerTurn(turn: PlayerTurnRequest): void+takeQueuedPlayerTurn(): Optional<PlayerTurnRequest>+queuePostGame(configuration: PostGameConfig): void+takeQueuedPostGameConfig(): Optional<PostGameConfig>«control»BattlePlaybackController-events: Queue<BattleEvent>-dialogueFormatter: BattleDialogueFormatter-playbackTimer: Timer-active: boolean+enqueue(event: BattleEvent): void+playNextIfIdle(): void+reset(): void«control»BattleDialogueFormatter+format(event: BattleEvent): String+playbackDelayMillis(event: BattleEvent): int«entity»BattleLaunchRequest-setupCreator: Function<BattleSetupFactory, BattleSetup>-replayConfiguration: PostGameConfig-intro: String+preset(configuration: GameConfiguration): BattleLaunchRequest+custom(configuration: CustomGameConfiguration): BattleLaunchRequest+replay(configuration: PostGameConfig): BattleLaunchRequest+createSetup(setupFactory: BattleSetupFactory): BattleSetup+replayConfiguration(): PostGameConfig+intro(): String«interface»«entity»PostGameConfig+preset(configuration: GameConfiguration): PostGameConfig+custom(configuration: CustomGameConfiguration): PostGameConfig«record»«entity»PostGameConfig.Preset-configuration: GameConfiguration«record»«entity»PostGameConfig.Custom-configuration: CustomGameConfiguration«record»«entity»PlayerTurnRequest-roundNumber: int-player: PlayerCharacter-livingEnemies: List<Combatant>-responseQueue: BlockingQueue<PlayerDecision>«record»«entity»ResolvedPlayerCommand-decision: PlayerDecision-actionName: String-targetLabel: String«control»SwingThread+runAndWait(runnable: Runnable): voidEncapsulation:private state changes throughbehavior methods.Inheritance:PlayerCharacter and EnemyCombatantspecialize Combatant.Composition:Combatant owns HitPoints, Inventory,CombatStats, and StatusEffectRegistry.Polymorphism / OCP:actions share execute(); new actionsavoid BattleEngine changes.Polymorphism / OCP:StatusEffectRegistry applies effectsthrough StatusEffect hooks.Dependency inversion:BattleEngine coordinates throughstrategy interfaces.Observer:records events and notifiesBattleEventListener.Factory:setup and combatant constructionare centralized.SRP:panel handles Swing, model stores state,renderer draws, loop ticks.Boundary-Control:UI delegates battle flow toBattleController and BattleEngine.hp11inventory11baseStats11status effects11specialSkill11turnAction11action11stats13active effects10..*10..*111111111111events10..*notifieswaves11..211..*111111111111111111sprites10..*floatingTexts10..*1111bodyRenderers11..*1111Focus: classes and interfaces grouped by layer:UI Boundary -> Engine Control -> Domain Entity.Includes all production classes, key attributes/methods,inheritance, ownership, and major architectural links. Type labels:«interface», «enumeration», and «record»identify non-standard class boxes explicitly.Plain rectangles are concrete classes. UML notation:<|-- inheritance<|.. interface implementation-- composition / strong ownershipo-- aggregation / collection reference--> association or major dependency To keep the generated image readable, local helper,enum/value-object, and method-parameter-only uses areshown through signatures instead of extra arrows. OO principle annotations:Encapsulation - CombatantInheritance - PlayerCharacter / EnemyCombatantComposition - Combatant owns HitPoints, Inventory,CombatStats, and StatusEffectRegistryPolymorphism / OCP - BattleAction and StatusEffectDependency inversion - BattleEngine depends on strategyinterfaces such as TurnProcessor and WaveManagerObserver - BattleEventPublisher + BattleEventListenerFactory - BattleSetupFactory and CombatantFactorySRP - ArenaScenePanel, ArenaSceneModel, ArenaSceneRenderer,ArenaBackgroundRendererBoundary-Control - UI classes delegate battle flow toBattleController and BattleEngine \ No newline at end of file diff --git a/docs/uml-diagrams/plantuml_class_diagram.puml b/docs/uml-diagrams/plantuml_class_diagram.puml index da49c71..eec6af7 100644 --- a/docs/uml-diagrams/plantuml_class_diagram.puml +++ b/docs/uml-diagrams/plantuml_class_diagram.puml @@ -50,7 +50,8 @@ legend right interfaces such as TurnProcessor and WaveManager Observer - BattleEventPublisher + BattleEventListener Factory - BattleSetupFactory and CombatantFactory - SRP - ArenaScenePanel, ArenaSceneModel, ArenaSceneRenderer + SRP - ArenaScenePanel, ArenaSceneModel, ArenaSceneRenderer, + ArenaBackgroundRenderer Boundary-Control - UI classes delegate battle flow to BattleController and BattleEngine endlegend @@ -760,10 +761,18 @@ package "UI LAYER - Swing GUI boundary/control" { } class ArenaSceneRenderer <> { + - backgroundRenderer: ArenaBackgroundRenderer - fighterRenderer: FighterSpriteRenderer ~ render(g: Graphics2D, model: ArenaSceneModel, width: int, height: int, now: long): void } + class ArenaBackgroundRenderer <> { + {static} ~ BACKGROUND_RESOURCE: String + - source: BufferedImage + - scaledBackground: BufferedImage + ~ render(g: Graphics2D, width: int, height: int): void + } + class ArenaSceneLoop <> { - tickCallback: Runnable - tickTimer: Timer @@ -1053,6 +1062,7 @@ ArenaScenePanel "1" *-- "1" ArenaSceneRenderer ArenaScenePanel "1" *-- "1" ArenaSceneLoop ArenaSceneModel "1" o-- "0..*" FighterSpriteDto : sprites ArenaSceneModel "1" *-- "0..*" FloatingText : floatingTexts +ArenaSceneRenderer "1" o-- "1" ArenaBackgroundRenderer ArenaSceneRenderer "1" *-- "1" FighterSpriteRenderer FighterBodyRenderer <|.. WarriorBodyRenderer FighterBodyRenderer <|.. WizardBodyRenderer diff --git a/src/main/java/sc2002/turnbased/ui/gui/view/ArenaBackgroundRenderer.java b/src/main/java/sc2002/turnbased/ui/gui/view/ArenaBackgroundRenderer.java new file mode 100644 index 0000000..81e1c29 --- /dev/null +++ b/src/main/java/sc2002/turnbased/ui/gui/view/ArenaBackgroundRenderer.java @@ -0,0 +1,109 @@ +package sc2002.turnbased.ui.gui.view; + +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.Objects; + +import javax.imageio.ImageIO; + +final class ArenaBackgroundRenderer { + static final String BACKGROUND_RESOURCE = "/sc2002/turnbased/ui/gui/assets/arena-background.png"; + + private final BufferedImage source; + private BufferedImage scaledBackground; + private int scaledWidth; + private int scaledHeight; + + ArenaBackgroundRenderer() { + this(loadResource(BACKGROUND_RESOURCE)); + } + + ArenaBackgroundRenderer(BufferedImage source) { + this.source = Objects.requireNonNull(source, "source"); + if (source.getWidth() <= 0 || source.getHeight() <= 0) { + throw new IllegalArgumentException("Background image must have positive dimensions."); + } + } + + void render(Graphics2D g, int width, int height) { + Objects.requireNonNull(g, "g"); + if (width <= 0 || height <= 0) { + return; + } + g.drawImage(scaledBackground(width, height), 0, 0, null); + } + + private BufferedImage scaledBackground(int width, int height) { + if (scaledBackground == null || width != scaledWidth || height != scaledHeight) { + scaledBackground = createScaledBackground(width, height); + scaledWidth = width; + scaledHeight = height; + } + return scaledBackground; + } + + private BufferedImage createScaledBackground(int width, int height) { + BufferedImage target = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D backgroundGraphics = target.createGraphics(); + try { + backgroundGraphics.setRenderingHint( + RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR + ); + backgroundGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + SourceCrop crop = coverCrop(width, height); + backgroundGraphics.drawImage( + source, + 0, + 0, + width, + height, + crop.x(), + crop.y(), + crop.x() + crop.width(), + crop.y() + crop.height(), + null + ); + } finally { + backgroundGraphics.dispose(); + } + return target; + } + + private SourceCrop coverCrop(int width, int height) { + int sourceWidth = source.getWidth(); + int sourceHeight = source.getHeight(); + double sourceRatio = sourceWidth / (double) sourceHeight; + double targetRatio = width / (double) height; + if (sourceRatio > targetRatio) { + int cropWidth = Math.max(1, (int) Math.round(sourceHeight * targetRatio)); + int cropX = (sourceWidth - cropWidth) / 2; + return new SourceCrop(cropX, 0, cropWidth, sourceHeight); + } + int cropHeight = Math.max(1, (int) Math.round(sourceWidth / targetRatio)); + int cropY = (sourceHeight - cropHeight) / 2; + return new SourceCrop(0, cropY, sourceWidth, cropHeight); + } + + private static BufferedImage loadResource(String resourceName) { + try (InputStream inputStream = ArenaBackgroundRenderer.class.getResourceAsStream(resourceName)) { + if (inputStream == null) { + throw new IllegalStateException("Missing arena background resource: " + resourceName); + } + BufferedImage image = ImageIO.read(inputStream); + if (image == null) { + throw new IllegalStateException("Unreadable arena background resource: " + resourceName); + } + return image; + } catch (IOException exception) { + throw new UncheckedIOException("Unable to load arena background resource: " + resourceName, exception); + } + } + + private record SourceCrop(int x, int y, int width, int height) { + } +} diff --git a/src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneRenderer.java b/src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneRenderer.java index 6e7e17c..2c3faec 100644 --- a/src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneRenderer.java +++ b/src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneRenderer.java @@ -5,103 +5,34 @@ import java.awt.Color; import java.awt.Composite; import java.awt.Font; -import java.awt.GradientPaint; import java.awt.Graphics2D; -import java.awt.Polygon; import java.awt.RenderingHints; -import java.awt.geom.Path2D; +import java.util.Objects; final class ArenaSceneRenderer { private static final long FLOATING_TEXT_NANOS = 1_000_000_000L; + private final ArenaBackgroundRenderer backgroundRenderer; private final FighterSpriteRenderer fighterRenderer = new FighterSpriteRenderer(); + ArenaSceneRenderer() { + this(new ArenaBackgroundRenderer()); + } + + ArenaSceneRenderer(ArenaBackgroundRenderer backgroundRenderer) { + this.backgroundRenderer = Objects.requireNonNull(backgroundRenderer, "backgroundRenderer"); + } + void render(Graphics2D g, ArenaSceneModel model, int width, int height, long now) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - drawBackground(g, width, height); + backgroundRenderer.render(g, width, height); drawTargetPath(g, model); drawSprites(g, model); drawFloatingTexts(g, model, now); drawOverlay(g, model, width); } - private void drawBackground(Graphics2D g, int width, int height) { - int floorTop = floorTop(height); - g.setPaint(new GradientPaint(0, 0, new Color(34, 107, 124), 0, height, new Color(232, 86, 76))); - g.fillRect(0, 0, width, height); - - g.setColor(new Color(255, 210, 99, 185)); - g.fillOval(width - 178, 48, 86, 86); - - drawMountain(g, -40, floorTop - 160, 270, new Color(60, 102, 91)); - drawMountain(g, 190, floorTop - 145, 270, new Color(55, 84, 93)); - drawMountain(g, 470, floorTop - 152, 300, new Color(63, 105, 81)); - drawForest(g, width, floorTop); - drawRuins(g, width, floorTop); - - g.setPaint(new GradientPaint(0, floorTop, new Color(58, 69, 58), 0, height, new Color(34, 48, 45))); - g.fillRect(0, floorTop, width, height - floorTop); - - g.setColor(new Color(88, 116, 91, 140)); - for (int x = -40; x < width + 90; x += 86) { - g.fillRoundRect(x, floorTop + 22, 58, 18, 8, 8); - g.fillRoundRect(x + 28, floorTop + 116, 74, 22, 8, 8); - } - g.setColor(new Color(18, 29, 31, 90)); - for (int y = floorTop + 30; y < height; y += 48) { - g.drawLine(0, y, width, y - 26); - } - } - - private void drawMountain(Graphics2D g, int x, int baseY, int size, Color color) { - Polygon mountain = new Polygon(); - mountain.addPoint(x, baseY + size); - mountain.addPoint(x + size / 2, baseY); - mountain.addPoint(x + size, baseY + size); - g.setColor(color); - g.fillPolygon(mountain); - g.setColor(new Color(235, 238, 214, 88)); - Polygon cap = new Polygon(); - cap.addPoint(x + size / 2, baseY); - cap.addPoint(x + size / 2 - 32, baseY + 70); - cap.addPoint(x + size / 2 + 12, baseY + 48); - cap.addPoint(x + size / 2 + 42, baseY + 86); - g.fillPolygon(cap); - } - - private void drawForest(Graphics2D g, int width, int floorTop) { - int horizon = floorTop - 40; - for (int x = -20; x < width + 60; x += 42) { - int height = 54 + Math.floorMod(x * 13, 48); - g.setColor(new Color(34, 78, 57, 180)); - Path2D tree = new Path2D.Double(); - tree.moveTo(x, horizon); - tree.lineTo(x + 20, horizon - height); - tree.lineTo(x + 42, horizon); - tree.closePath(); - g.fill(tree); - g.setColor(new Color(50, 63, 48, 190)); - g.fillRect(x + 18, horizon - 10, 8, 18); - } - } - - private void drawRuins(Graphics2D g, int width, int floorTop) { - int base = floorTop - 26; - Color stoneColor = new Color(74, 77, 72, 155); - Color stripeColor = new Color(120, 58, 58, 150); - for (int x = 44; x < width; x += 245) { - g.setColor(stoneColor); - g.fillRect(x, base - 94, 26, 94); - g.setColor(stoneColor); - g.fillRect(x + 76, base - 118, 28, 118); - g.setColor(stoneColor); - g.fillRect(x - 10, base - 120, 128, 18); - g.setColor(stripeColor); - g.fillRect(x + 24, base - 108, 50, 12); - } - } - private void drawTargetPath(Graphics2D g, ArenaSceneModel model) { FighterSpriteDto player = model.playerSprite(); FighterSpriteDto target = model.currentSelectedEnemySprite(); @@ -159,10 +90,6 @@ private void drawOverlay(Graphics2D g, ArenaSceneModel model, int width) { } } - private static int floorTop(int height) { - return (int) (height * 0.55); - } - private static String fitText(Graphics2D g, String text, int maxWidth) { if (g.getFontMetrics().stringWidth(text) <= maxWidth) { return text; diff --git a/src/main/resources/sc2002/turnbased/ui/gui/assets/arena-background.png b/src/main/resources/sc2002/turnbased/ui/gui/assets/arena-background.png new file mode 100644 index 0000000..015dbce Binary files /dev/null and b/src/main/resources/sc2002/turnbased/ui/gui/assets/arena-background.png differ diff --git a/src/test/java/sc2002/turnbased/ui/gui/view/ArenaBackgroundRendererTest.java b/src/test/java/sc2002/turnbased/ui/gui/view/ArenaBackgroundRendererTest.java new file mode 100644 index 0000000..482a7c3 --- /dev/null +++ b/src/test/java/sc2002/turnbased/ui/gui/view/ArenaBackgroundRendererTest.java @@ -0,0 +1,50 @@ +package sc2002.turnbased.ui.gui.view; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; + +import org.junit.jupiter.api.Test; + +class ArenaBackgroundRendererTest { + private static final byte[] PNG_SIGNATURE = new byte[] { + (byte) 0x89, + 'P', + 'N', + 'G', + '\r', + '\n', + 0x1A, + '\n' + }; + + @Test + void packagesBackgroundAsPngResource() throws IOException { + try (InputStream inputStream = ArenaBackgroundRenderer.class.getResourceAsStream( + ArenaBackgroundRenderer.BACKGROUND_RESOURCE + )) { + assertNotNull(inputStream); + assertArrayEquals(PNG_SIGNATURE, inputStream.readNBytes(PNG_SIGNATURE.length)); + } + } + + @Test + void decodesPackagedBackgroundResource() throws IOException { + try (InputStream inputStream = ArenaBackgroundRenderer.class.getResourceAsStream( + ArenaBackgroundRenderer.BACKGROUND_RESOURCE + )) { + assertNotNull(inputStream); + BufferedImage image = ImageIO.read(inputStream); + + assertNotNull(image); + assertTrue(image.getWidth() > 0); + assertTrue(image.getHeight() > 0); + } + } +}