From 4545c3f83a525661ebdda8d89c3417b6a2429081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=B1=E9=A2=A8=E8=B0=B7=E3=82=AA=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=A3=E3=83=B3?= Date: Sun, 9 Aug 2026 16:47:44 +0900 Subject: [PATCH] feat: show air raid in a collapsible panel Air raid info used to replace the sortie fleet display entirely, hiding the fleet for the duration of the raid. Extract the battle area render into renderArea(baseDefense) so the land base defense can be drawn in a collapsible panel (open by default) above the regular fleet view, which now stays visible throughout. The raid's air force counts and BattleInfo stay scoped to the panel; the sortie area keeps showing NextSpotInfo as it does on any other node. Co-Authored-By: Claude Opus 5 --- assets/i18n/en-US.json | 1 + assets/i18n/ko-KR.json | 1 + src/views/battle-view-area.tsx | 335 ++++++++++++++++++++------------- 3 files changed, 202 insertions(+), 135 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 254c051..c3124d8 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -38,6 +38,7 @@ "Boss Battle": "Boss Battle", "Battle Avoid": "Battle Avoid", "Air Strike": "Air Strike", + "Air Defense": "Air Defense", "Escort Success": "Escort Success", "Transport Munitions": "Transport Munitions", "Manual Selection": "Manual Selection", diff --git a/assets/i18n/ko-KR.json b/assets/i18n/ko-KR.json index 8943bfb..c7cc42d 100644 --- a/assets/i18n/ko-KR.json +++ b/assets/i18n/ko-KR.json @@ -37,6 +37,7 @@ "Boss Battle": "보스전", "Battle Avoid": "기분탓", "Air Strike": "항공전", + "Air Defense": "방공", "Escort Success": "호위 성공", "Transport Munitions": "수송 물자", "Manual Selection": "능동 분기", diff --git a/src/views/battle-view-area.tsx b/src/views/battle-view-area.tsx index 94829f5..0d78e1f 100644 --- a/src/views/battle-view-area.tsx +++ b/src/views/battle-view-area.tsx @@ -1,8 +1,9 @@ import FontAwesome from 'react-fontawesome' -import React, { FC } from 'react' +import React, { FC, useEffect, useState } from 'react' import { createSelector } from 'reselect' import _ from 'lodash' import { useSelector } from 'react-redux' +import { Collapse } from '@blueprintjs/core' import { Tooltip } from 'views/components/etc/overlay' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -96,6 +97,31 @@ const CombatVS = styled.div<{ visible?: boolean }>` opacity: ${({ visible }) => (visible ? 1 : 0)}; ` +const AirRaidPanel = styled.div` + margin-bottom: 6px; + border-bottom: 1px solid rgba(128, 128, 128, 0.4); +` + +const AirRaidHeader = styled.div<{ isOpen?: boolean }>` + display: flex; + align-items: center; + cursor: pointer; + user-select: none; + line-height: 24px; + opacity: 0.8; + + &:hover { + opacity: 1; + } + + /* one icon rotated, so the label does not shift when toggling */ + .svg-inline--fa { + margin-right: 1ex; + width: 1ex; + transform: rotate(${({ isOpen }) => (isOpen ? 90 : 0)}deg); + } +` + const inEventSelector = createSelector( [(state: PoiRootState) => state.const?.$maps], (maps = {}) => Object.keys(maps).some((mapId) => +mapId > 100), @@ -190,42 +216,58 @@ const BattleViewArea: FC = ({ const fleetName = useSelector((state: PoiRootState) => state.info?.fleets?.[fleetIds[0]]?.api_name ?? 'Sortie Fleet', ) - const friendTitle = buildFriendTitle({ - showEnemyTitle, - combinedFlag, - fleetName, - isBaseDefense, - }) - - const View = isBaseDefense ? SquadView : ShipView + const [airRaidOpen, setAirRaidOpen] = useState(true) + const toggleAirRaid = () => setAirRaidOpen((open) => !open) + + // every raid starts expanded, even if the previous one was collapsed by hand + useEffect(() => { + if (isBaseDefense) setAirRaidOpen(true) + }, [isBaseDefense, sortieMapId, currentNode]) + const times = !horizontalLayout ? 1 : 2 const fleetCount = _.sumBy([mainFleet, escortFleet], (fleet) => fleet != null ? 1 : 0) const enemyCount = _.sumBy([enemyFleet, enemyEscort], (fleet) => fleet != null ? 1 : 0) - const fleetWidth = escortFleet && !isBaseDefense ? 2 : 1 - const enemyWidth = enemyEscort && !isBaseDefense ? 2 : 1 const { getShip, getItem } = _.pick(result, ['getShip', 'getItem']) as { getShip?: number; getItem?: number } - const alliedForce = ( - - - - - ) + // `baseDefense` selects which battle is being drawn: the land base air defense + // (shown in the collapsible panel) or the sortie fleet (shown as usual below it). + // The air raid's own stats (air force, rank, formations) belong to the panel only; + // the sortie area relies on an air raid always leaving `sortieState` at Navigation, + // which keeps it on `NextSpotInfo` instead of the raid's `BattleInfo`. + const renderArea = (baseDefense: boolean): React.ReactElement => { + const friendTitle = buildFriendTitle({ + showEnemyTitle, + combinedFlag, + fleetName, + isBaseDefense: baseDefense, + }) + const View = baseDefense ? SquadView : ShipView + // air force / battle result belong to the air raid whenever there is one + const shownAirForce = baseDefense === Boolean(isBaseDefense) ? airForce : [] + const showEnemy = sortieState > SortieState.Navigation || baseDefense + const fleetWidth = escortFleet && !baseDefense ? 2 : 1 + const enemyWidth = enemyEscort && !baseDefense ? 2 : 1 + + const alliedForce = ( + + + + + ) - const enemyForce = - sortieState > SortieState.Navigation || isBaseDefense ? ( + const enemyForce = showEnemy ? ( = ({