From 85ef3dbe75c342efafa00e57e4c5d1d6a20dede3 Mon Sep 17 00:00:00 2001 From: chiba-bot Date: Sat, 21 Feb 2026 19:16:58 +0000 Subject: [PATCH 1/9] feat: migrate views/components/ship to TypeScript --- .../{aaci-indicator.es => aaci-indicator.tsx} | 96 +++--- views/components/ship/aapb-indicator.es | 44 --- views/components/ship/aapb-indicator.tsx | 66 +++++ views/components/ship/{index.es => index.tsx} | 221 ++++++++++---- .../ship/{lbac-view.es => lbac-view.tsx} | 71 ++++- .../{oasw-indicator.es => oasw-indicator.tsx} | 36 ++- .../ship/{ship-item.es => ship-item.tsx} | 133 ++++++--- .../{slotitems-data.es => slotitems-data.ts} | 40 ++- views/components/ship/slotitems.es | 196 ------------- views/components/ship/slotitems.tsx | 276 ++++++++++++++++++ 10 files changed, 767 insertions(+), 412 deletions(-) rename views/components/ship/{aaci-indicator.es => aaci-indicator.tsx} (50%) delete mode 100644 views/components/ship/aapb-indicator.es create mode 100644 views/components/ship/aapb-indicator.tsx rename views/components/ship/{index.es => index.tsx} (64%) rename views/components/ship/{lbac-view.es => lbac-view.tsx} (72%) rename views/components/ship/{oasw-indicator.es => oasw-indicator.tsx} (60%) rename views/components/ship/{ship-item.es => ship-item.tsx} (73%) rename views/components/ship/{slotitems-data.es => slotitems-data.ts} (52%) delete mode 100644 views/components/ship/slotitems.es create mode 100644 views/components/ship/slotitems.tsx diff --git a/views/components/ship/aaci-indicator.es b/views/components/ship/aaci-indicator.tsx similarity index 50% rename from views/components/ship/aaci-indicator.es rename to views/components/ship/aaci-indicator.tsx index 66c112652..085e50229 100644 --- a/views/components/ship/aaci-indicator.es +++ b/views/components/ship/aaci-indicator.tsx @@ -1,7 +1,7 @@ import { Tooltip, Tag, Position, Intent } from '@blueprintjs/core' import { memoize, get } from 'lodash' import React from 'react' -import { withNamespaces, Trans } from 'react-i18next' +import { useTranslation, Trans } from 'react-i18next' import { connect } from 'react-redux' import { createSelector } from 'reselect' import { @@ -14,7 +14,7 @@ import i18next from 'views/env-parts/i18next' import { getShipAACIs, getShipAllAACIs, AACITable } from 'views/utils/aaci' import { shipDataSelectorFactory, shipEquipDataSelectorFactory } from 'views/utils/selectors' -const getAvailableTranslation = memoize((str) => +const getAvailableTranslation = memoize((str: string) => i18next.translator.exists(`main:${str}`) ? ( main:{str} ) : i18next.translator.exists(`resources:${str}`) ? ( @@ -24,14 +24,29 @@ const getAvailableTranslation = memoize((str) => ), ) -const __t = (name) => +const __t = (name: string[]) => name.map((n, i) => ( {getAvailableTranslation(n)} )) -const AACISelectorFactory = memoize((shipId) => +interface AACIInfo { + fixed: number + modifier: number + name: string[] +} + +interface AACISelectorResult { + AACIs: number[] + maxShotdown: number +} + +interface AACIIndicatorProps extends AACISelectorResult { + shipId: number +} + +const AACISelectorFactory = memoize((shipId: number) => createSelector( [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], ([_ship = {}, $ship = {}] = [], _equips = []) => { @@ -45,42 +60,46 @@ const AACISelectorFactory = memoize((shipId) => ), ) -const maxAACIShotdownSelectorFactory = memoize((shipId) => +const maxAACIShotdownSelectorFactory = memoize((shipId: number) => createSelector([shipDataSelectorFactory(shipId)], ([_ship = {}, $ship = {}] = []) => { const AACIs = getShipAllAACIs({ ...$ship, ..._ship }) return Math.max(...AACIs.map((id) => AACITable[id].fixed || 0)) }), ) -export const AACIIndicator = withNamespaces(['main'])( - connect((state, { shipId }) => ({ - AACIs: AACISelectorFactory(shipId)(state) || [], - maxShotdown: maxAACIShotdownSelectorFactory(shipId)(state), - }))(({ AACIs, maxShotdown, shipId, t }) => { - const currentMax = Math.max(...AACIs.map((id) => AACITable[id].fixed || 0)) +const AACIIndicatorComponent: React.FC = ({ + AACIs, + maxShotdown, + shipId, +}) => { + const { t } = useTranslation(['main']) + const currentMax = Math.max(...AACIs.map((id) => AACITable[id].fixed || 0)) - const tooltip = AACIs.length && ( - - {AACIs.map((id) => ( - - - {t('main:AACIType', { count: id })} - - {get(AACITable, `${id}.name.length`, 0) > 0 ? __t(AACITable[id].name) : ''} - - - {t('main:Shot down', { count: AACITable[id].fixed })} - - {t('main:Modifier', { count: AACITable[id].modifier })} + const tooltip = AACIs.length > 0 && ( + + {AACIs.map((id) => ( + + + {t('main:AACIType', { count: id })} + + {get(AACITable, `${id}.name.length`, 0) > 0 + ? __t(AACITable[id].name) + : ''} - - ))} - {currentMax < maxShotdown && {t('main:Max shot down not reached')}} - - ) + + {t('main:Shot down', { count: AACITable[id].fixed })} + + {t('main:Modifier', { count: AACITable[id].modifier })} + + + ))} + {currentMax < maxShotdown && {t('main:Max shot down not reached')}} + + ) - return ( - !!AACIs.length && ( + return ( + <> + {AACIs.length > 0 && ( @@ -88,7 +107,14 @@ export const AACIIndicator = withNamespaces(['main'])( - ) - ) - }), -) + )} + + ) +} + +const mapStateToProps = (state: unknown, { shipId }: { shipId: number }): AACISelectorResult => ({ + AACIs: AACISelectorFactory(shipId)(state) || [], + maxShotdown: maxAACIShotdownSelectorFactory(shipId)(state), +}) + +export const AACIIndicator = connect(mapStateToProps)(AACIIndicatorComponent) diff --git a/views/components/ship/aapb-indicator.es b/views/components/ship/aapb-indicator.es deleted file mode 100644 index dea1c5543..000000000 --- a/views/components/ship/aapb-indicator.es +++ /dev/null @@ -1,44 +0,0 @@ -import { Tooltip, Tag, Position, Intent } from '@blueprintjs/core' -import { memoize, compact, isFinite } from 'lodash' -import React from 'react' -import { withNamespaces } from 'react-i18next' -import { connect } from 'react-redux' -import { compose } from 'redux' -import { createSelector } from 'reselect' -import { ShipLabel } from 'views/components/ship-parts/styled-components' -import { getShipAAPB } from 'views/utils/aapb' -import { shipDataSelectorFactory, shipEquipDataSelectorFactory } from 'views/utils/selectors' - -const AAPBSelectorFactory = memoize((shipId) => - createSelector( - [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], - (shipInfo, equipsInfo) => { - if (!shipInfo || !equipsInfo) return 0 - /* - equipment position is irrelevant with regard to AAPB trigger rate, - so we might as well remove all `undefined` for getShipAAPB to - have a uniform structure to work with. - */ - return getShipAAPB(shipInfo, compact(equipsInfo)) - }, - ), -) - -export const AAPBIndicator = compose( - withNamespaces(['main']), - connect((state, { shipId }) => ({ - AAPB: AAPBSelectorFactory(shipId)(state) || 0, - })), -)( - ({ AAPB, shipId, t }) => - isFinite(AAPB) && - AAPB > 0 && ( - - {`${AAPB.toFixed(2)}%`}}> - - {t('main:AAPB')} - - - - ), -) diff --git a/views/components/ship/aapb-indicator.tsx b/views/components/ship/aapb-indicator.tsx new file mode 100644 index 000000000..a101c8337 --- /dev/null +++ b/views/components/ship/aapb-indicator.tsx @@ -0,0 +1,66 @@ +import { Tag, Intent, Position, Tooltip } from '@blueprintjs/core' +import { compact, isFinite, memoize } from 'lodash' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { connect } from 'react-redux' +import { createSelector } from 'reselect' +import { ShipLabel } from 'views/components/ship-parts/styled-components' +import { getShipAAPB } from 'views/utils/aapb' +import { shipDataSelectorFactory, shipEquipDataSelectorFactory } from 'views/utils/selectors' + +interface ShipInfo { + [key: string]: unknown +} + +interface EquipInfo { + [key: string]: unknown +} + +interface AAPBSelectorProps { + shipId: number +} + +interface AAPBStateProps { + AAPB: number +} + +interface AAPBIndicatorProps extends AAPBSelectorProps, AAPBStateProps {} + +const AAPBSelectorFactory = memoize((shipId: number) => + createSelector( + [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], + (shipInfo: [ShipInfo, ShipInfo] | undefined, equipsInfo: (EquipInfo[] | undefined)[]) => { + if (!shipInfo || !equipsInfo) return 0 + /* + equipment position is irrelevant with regard to AAPB trigger rate, + so we might as well remove all `undefined` for getShipAAPB to + have a uniform structure to work with. + */ + return getShipAAPB(shipInfo, compact(equipsInfo)) + }, + ), +) + +const AAPBIndicatorComponent: React.FC = ({ AAPB }) => { + const { t } = useTranslation(['main']) + + if (!isFinite(AAPB) || AAPB <= 0) { + return null + } + + return ( + + {`${AAPB.toFixed(2)}%`}}> + + {t('main:AAPB')} + + + + ) +} + +const mapStateToProps = (state: unknown, ownProps: AAPBSelectorProps): AAPBStateProps => ({ + AAPB: AAPBSelectorFactory(ownProps.shipId)(state) || 0, +}) + +export const AAPBIndicator = connect(mapStateToProps)(AAPBIndicatorComponent) diff --git a/views/components/ship/index.es b/views/components/ship/index.tsx similarity index 64% rename from views/components/ship/index.es rename to views/components/ship/index.tsx index c637317e7..b53a639be 100644 --- a/views/components/ship/index.es +++ b/views/components/ship/index.tsx @@ -1,10 +1,8 @@ import { Button, ResizeSensor } from '@blueprintjs/core' import { get, memoize, times } from 'lodash' -import PropTypes from 'prop-types' import React, { Component } from 'react' import FontAwesome from 'react-fontawesome' -import { withNamespaces, Trans } from 'react-i18next' -/* global getStore */ +import { Trans, withTranslation, type WithTranslation } from 'react-i18next' import { connect } from 'react-redux' import { compose } from 'redux' import { createSelector } from 'reselect' @@ -33,9 +31,85 @@ import { LandbaseButton } from '../ship-parts/landbase-button' import { SquardRow } from './lbac-view' import { ShipRow } from './ship-item' -const shipRowWidthSelector = (state) => get(state, 'layout.shippane.width', 450) +interface StateData { + const?: { + $mapareas?: Record + } + info?: { + fleets?: unknown[] + airbase?: unknown[] + } + sortie?: { + spAttackCount?: number + combinedFlag?: unknown + } + layout?: { + shippane?: { + width?: number + height?: number + } + } + ui?: { + activeFleetId?: number + } + config?: { + poi?: { + transition?: { + enable?: boolean + } + appearance?: { + avatar?: boolean + } + } + } + [key: string]: unknown +} + +interface ShipViewSwitchButtonProps { + fleetId: number + activeFleetId: number + fleetName: string + fleetState: number + onClick: (e: React.MouseEvent) => void + disabled: boolean +} + +interface FleetShipViewProps { + fleetId: number + shipsId: number[] + enableAvatar: boolean + width: number + isSpAttack: boolean +} -const shipViewSwitchButtonDataSelectorFactory = memoize((fleetId) => +interface LBViewProps extends WithTranslation { + areaIds: number[] + mapareas: Record + enableAvatar: boolean + width: number +} + +interface ReactClassProps extends WithTranslation { + enableTransition: boolean + fleetCount: number + activeFleetId: number + airBaseCnt: number + enableAvatar: boolean + width: number + dispatch: (action: { type: string; [key: string]: unknown }) => void +} + +interface ReactClassState { + activeFleetId: number + prevFleetId: number | null +} + +/* global getStore */ +declare function getStore(key: string): unknown + +const shipRowWidthSelector = (state: StateData) => get(state, 'layout.shippane.width', 450) + +const shipViewSwitchButtonDataSelectorFactory = memoize((fleetId: number) => createSelector( [fleetNameSelectorFactory(fleetId), fleetStateSelectorFactory(fleetId)], (fleetName, fleetState) => ({ @@ -45,27 +119,36 @@ const shipViewSwitchButtonDataSelectorFactory = memoize((fleetId) => ), ) -const ShipViewSwitchButton = connect((state, { fleetId }) => - shipViewSwitchButtonDataSelectorFactory(fleetId)(state), -)(({ fleetId, activeFleetId, fleetName, fleetState, onClick, disabled }) => ( +const ShipViewSwitchButton: React.FC = ({ + fleetId, + activeFleetId, + fleetName, + fleetState, + onClick, + disabled, +}) => ( -)) +) -const fleetShipViewDataSelectorFactory = memoize((fleetId) => +const ConnectedShipViewSwitchButton = connect((state: StateData, { fleetId }: { fleetId: number }) => + shipViewSwitchButtonDataSelectorFactory(fleetId)(state), +)(ShipViewSwitchButton) + +const fleetShipViewDataSelectorFactory = memoize((fleetId: number) => createSelector( [ fleetShipsIdSelectorFactory(fleetId), fleetShipsDataWithEscapeSelectorFactory(fleetId), - (state) => get(state.sortie, 'spAttackCount'), - (state) => get(state.info, 'useitems.95.api_count'), - (state) => get(state.sortie, 'combinedFlag'), + (state: StateData) => get(state, 'sortie.spAttackCount'), + (state: StateData) => get(state, 'info.useitems.95.api_count'), + (state: StateData) => get(state, 'sortie.combinedFlag'), ], (shipsId, shipsData, spAttackCount, submarineSupplyCount, combinedFlag) => ({ shipsId, @@ -79,9 +162,13 @@ const fleetShipViewDataSelectorFactory = memoize((fleetId) => ), ) -const FleetShipView = connect((state, { fleetId }) => - fleetShipViewDataSelectorFactory(fleetId)(state), -)(({ fleetId, shipsId, enableAvatar, width, isSpAttack }) => ( +const FleetShipView: React.FC = ({ + fleetId, + shipsId, + enableAvatar, + width, + isSpAttack, +}) => ( <>
@@ -98,15 +185,13 @@ const FleetShipView = connect((state, { fleetId }) => ))} -)) +) -const LBView = compose( - withNamespaces(['resources']), - connect((state) => ({ - areaIds: get(state, 'info.airbase', []).map((a) => a.api_area_id), - mapareas: get(state, 'const.$mapareas', {}), - })), -)(({ areaIds, mapareas, t, enableAvatar, width }) => ( +const ConnectedFleetShipView = connect((state: StateData, { fleetId }: { fleetId: number }) => + fleetShipViewDataSelectorFactory(fleetId)(state), +)(FleetShipView) + +const LBView: React.FC = ({ areaIds, mapareas, t, enableAvatar, width }) => ( {areaIds.map( (id, i) => @@ -123,27 +208,31 @@ const LBView = compose( )), )} -)) +) -@connect((state, props) => ({ - enableTransition: get(state, 'config.poi.transition.enable', true), - fleetCount: get(state, 'info.fleets.length', 4), - activeFleetId: get(state, 'ui.activeFleetId', 0), - airBaseCnt: get(state, 'info.airbase.length', 0), - enableAvatar: get(state, 'config.poi.appearance.avatar', true), - width: shipRowWidthSelector(state), -})) -export class reactClass extends Component { - static propTypes = { - enableTransition: PropTypes.bool.isRequired, - fleetCount: PropTypes.number.isRequired, - activeFleetId: PropTypes.number.isRequired, - airBaseCnt: PropTypes.number.isRequired, - enableAvatar: PropTypes.bool, - width: PropTypes.number, +const ConnectedLBView = compose( + withTranslation(['resources']), + connect((state: StateData) => ({ + areaIds: get(state, 'info.airbase', []).map((a: { api_area_id: number }) => a.api_area_id), + mapareas: get(state, 'const.$mapareas', {}), + })), +)(LBView) + +class ReactClassComponent extends Component { + static displayName = 'ShipView' + + constructor(props: ReactClassProps) { + super(props) + this.state = { + activeFleetId: props.activeFleetId, + prevFleetId: null, + } } - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps( + props: ReactClassProps, + state: ReactClassState, + ): Partial | null { if (props.activeFleetId !== state.activeFleetId) { return { prevFleetId: state.activeFleetId, @@ -153,24 +242,14 @@ export class reactClass extends Component { return null } - constructor(props) { - super(props) - this.nowTime = 0 - } - - state = { - activeFleetId: this.props.activeFleetId, - prevFleetId: null, - } - - handleTransitionEnd = (i) => { + handleTransitionEnd = (i: number) => { if (i === this.state.prevFleetId) { this.setState({ prevFleetId: null }) } } - handleClick = (idx) => { - if (idx != this.state.activeFleetId) { + handleClick = (idx: number) => { + if (idx !== this.state.activeFleetId) { this.props.dispatch({ type: '@@TabSwitch', tabInfo: { @@ -189,14 +268,14 @@ export class reactClass extends Component { }) } - handleResize = (entries) => { + handleResize = (entries: ResizeObserverEntry[]) => { entries.forEach((entry) => { const { width, height } = entry.contentRect if ( width !== 0 && height !== 0 && - (width !== getStore('layout.shippane.width') || - height !== getStore('layout.shippane.height')) + (width !== (getStore('layout.shippane.width') as number) || + height !== (getStore('layout.shippane.height') as number)) ) { this.props.dispatch({ type: '@@LayoutUpdate', @@ -219,11 +298,11 @@ export class reactClass extends Component { {times(4).map((i) => ( - this.props.fleetCount} - onClick={(e) => this.handleClick(i)} + onClick={() => this.handleClick(i)} activeFleetId={activeFleetId} /> ))} @@ -232,7 +311,7 @@ export class reactClass extends Component { key={4} fleetId={4} disabled={this.props.airBaseCnt === 0} - onClick={(e) => this.handleClick(4)} + onClick={() => this.handleClick(4)} activeFleetId={activeFleetId} isMini={false} /> @@ -252,7 +331,7 @@ export class reactClass extends Component { left={activeFleetId > i} right={activeFleetId < i} > - 4} right={activeFleetId < 4} > - + @@ -281,6 +363,17 @@ export class reactClass extends Component { } } +const mapStateToProps = (state: StateData) => ({ + enableTransition: get(state, 'config.poi.transition.enable', true), + fleetCount: get(state, 'info.fleets.length', 4), + activeFleetId: get(state, 'ui.activeFleetId', 0), + airBaseCnt: get(state, 'info.airbase.length', 0), + enableAvatar: get(state, 'config.poi.appearance.avatar', true), + width: shipRowWidthSelector(state), +}) + +export const reactClass = connect(mapStateToProps)(ReactClassComponent) + export const displayName = ( main:Fleet diff --git a/views/components/ship/lbac-view.es b/views/components/ship/lbac-view.tsx similarity index 72% rename from views/components/ship/lbac-view.es rename to views/components/ship/lbac-view.tsx index d9da34a70..20ee49903 100644 --- a/views/components/ship/lbac-view.es +++ b/views/components/ship/lbac-view.tsx @@ -2,9 +2,8 @@ import { Tag, ProgressBar, Tooltip, Position } from '@blueprintjs/core' import memoize from 'fast-memoize' import { get } from 'lodash' import React from 'react' -import { withNamespaces } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { connect } from 'react-redux' -import { compose } from 'redux' import { createSelector } from 'reselect' import { ShipItem, @@ -29,10 +28,43 @@ import { landbaseSelectorFactory, landbaseEquipDataSelectorFactory } from 'views import { LandbaseSlotitems } from './slotitems' -const SquadSelectorFactory = memoize((squardId) => +interface EquipData { + [key: string]: unknown +} + +interface Landbase { + api_action_kind: number + api_distance: { + api_base: number + api_bonus: number + } + api_name: string + api_nowhp?: number + api_maxhp?: number + [key: string]: unknown +} + +interface SquadSelectorProps { + squardId: number +} + +interface SquadStateProps { + landbase: Landbase + equipsData: EquipData[] + squardId: number +} + +interface SquadRowProps extends SquadSelectorProps { + enableAvatar: boolean + compact: boolean +} + +type SquadRowStateProps = SquadStateProps + +const SquadSelectorFactory = memoize((squardId: number) => createSelector( [landbaseSelectorFactory(squardId), landbaseEquipDataSelectorFactory(squardId)], - (landbase, equipsData) => ({ + (landbase: Landbase, equipsData: EquipData[]) => ({ landbase, equipsData, squardId, @@ -40,15 +72,28 @@ const SquadSelectorFactory = memoize((squardId) => ), ) -export const SquardRow = compose( - withNamespaces(['main']), - connect((state, { squardId }) => SquadSelectorFactory(squardId)), -)(({ landbase, equipsData, squardId, t, enableAvatar, compact }) => { - const { api_action_kind, api_distance, api_name, api_nowhp = 200, api_maxhp = 200 } = landbase - const { api_base, api_bonus } = api_distance +const SquadRowComponent: React.FC = ({ + landbase, + equipsData, + squardId, + enableAvatar, + compact, +}) => { + const { t } = useTranslation(['main']) + + const { + api_action_kind, + api_distance, + api_name, + api_nowhp = 200, + api_maxhp = 200, + } = landbase + + const { api_base = 0, api_bonus = 0 } = api_distance || {} const tyku = getTyku([equipsData], api_action_kind) const hpPercentage = (api_nowhp / api_maxhp) * 100 const hideLBACName = enableAvatar && compact + return ( ) +} + +const mapStateToProps = (state: unknown, ownProps: SquadSelectorProps): SquadStateProps => ({ + ...SquadSelectorFactory(ownProps.squardId)(state), }) + +export const SquardRow = connect(mapStateToProps)(SquadRowComponent) diff --git a/views/components/ship/oasw-indicator.es b/views/components/ship/oasw-indicator.tsx similarity index 60% rename from views/components/ship/oasw-indicator.es rename to views/components/ship/oasw-indicator.tsx index 3110d93ab..3493f6b39 100644 --- a/views/components/ship/oasw-indicator.es +++ b/views/components/ship/oasw-indicator.tsx @@ -1,14 +1,22 @@ import { Tag, Intent } from '@blueprintjs/core' import { memoize } from 'lodash' import React from 'react' -import { withNamespaces } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { connect } from 'react-redux' import { createSelector } from 'reselect' import { ShipLabel } from 'views/components/ship-parts/styled-components' import { isOASW } from 'views/utils/oasw' import { shipDataSelectorFactory, shipEquipDataSelectorFactory } from 'views/utils/selectors' -const OASWSelectorFactory = memoize((shipId) => +interface OASWSelectorResult { + isOASW: boolean +} + +interface OASWIndicatorProps extends OASWSelectorResult { + shipId: number +} + +const OASWSelectorFactory = memoize((shipId: number) => createSelector( [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], ([_ship = {}, $ship = {}] = [], _equips = []) => { @@ -22,17 +30,23 @@ const OASWSelectorFactory = memoize((shipId) => ), ) -export const OASWIndicator = withNamespaces(['main'])( - connect((state, { shipId }) => ({ - isOASW: OASWSelectorFactory(shipId)(state), - }))( - ({ isOASW, shipId, t }) => - isOASW && ( +const OASWIndicatorComponent: React.FC = ({ isOASW, shipId }) => { + const { t } = useTranslation(['main']) + return ( + <> + {isOASW && ( {t('main:OASW')} - ), - ), -) + )} + + ) +} + +const mapStateToProps = (state: unknown, { shipId }: { shipId: number }): OASWSelectorResult => ({ + isOASW: OASWSelectorFactory(shipId)(state), +}) + +export const OASWIndicator = connect(mapStateToProps)(OASWIndicatorComponent) diff --git a/views/components/ship/ship-item.es b/views/components/ship/ship-item.tsx similarity index 73% rename from views/components/ship/ship-item.es rename to views/components/ship/ship-item.tsx index 9b9369e8c..bc1e9e2ab 100644 --- a/views/components/ship/ship-item.es +++ b/views/components/ship/ship-item.tsx @@ -1,9 +1,8 @@ import { ProgressBar, Tooltip, Position, Tag, Intent } from '@blueprintjs/core' import shallowEqual from 'fbjs/lib/shallowEqual' import { isEqual, pick, omit, memoize, get } from 'lodash' -import PropTypes from 'prop-types' import React, { Component } from 'react' -import { withNamespaces } from 'react-i18next' +import { type WithTranslation, withTranslation } from 'react-i18next' import { connect } from 'react-redux' import { createSelector } from 'reselect' import { MaterialIcon } from 'views/components/etc/icon' @@ -49,7 +48,56 @@ import { AAPBIndicator } from './aapb-indicator' import { OASWIndicator } from './oasw-indicator' import { Slotitems } from './slotitems' -const shipRowDataSelectorFactory = memoize((shipId) => +interface Ship { + api_lv?: number + api_exp?: [number, number, number] + api_id: number + api_ship_id?: number + api_nowhp: number + api_maxhp: number + api_cond: number + api_fuel: number + api_bull: number + api_soku: number + api_ndock_time: number + [key: string]: unknown +} + +interface ShipType { + api_name?: string + [key: string]: unknown +} + +interface ConstData { + $shipTypes: Record + [key: string]: unknown +} + +interface RepairDock { + [key: string]: unknown +} + +interface ShipRowData { + ship: Ship + $ship: Ship + $shipTypes: Record + labelStatus: number + shipAvatarColor: string +} + +interface ShipRowProps extends WithTranslation { + shipId: number + ship?: Ship + $ship?: Ship + $shipTypes?: Record + labelStatus?: number + enableAvatar?: boolean + compact?: boolean + shipAvatarColor?: string + showSpAttackLabel?: boolean +} + +const shipRowDataSelectorFactory = memoize((shipId: number) => createSelector( [ shipDataSelectorFactory(shipId), @@ -57,11 +105,18 @@ const shipRowDataSelectorFactory = memoize((shipId) => constSelector, escapeStatusSelectorFactory(shipId), fcdShipTagColorSelector, - (state) => get(state, 'config.poi.appearance.avatarType'), + (state: Record) => get(state, 'config.poi.appearance.avatarType'), ], - ([ship, $ship] = [], repairDock, { $shipTypes }, escaped, shipTagColor, avatarType) => ({ - ship: ship || {}, - $ship: $ship || {}, + ( + [ship, $ship] = [{}, {}] as [Ship, Ship], + repairDock: RepairDock | undefined, + { $shipTypes }: ConstData, + escaped: boolean, + shipTagColor: string[], + avatarType: string, + ): ShipRowData => ({ + ship: ship || ({} as Ship), + $ship: $ship || ({} as Ship), $shipTypes, labelStatus: getShipLabelStatus(ship, $ship, repairDock, escaped), shipAvatarColor: selectShipAvatarColor(ship, $ship, shipTagColor, avatarType), @@ -69,20 +124,8 @@ const shipRowDataSelectorFactory = memoize((shipId) => ), ) -@withNamespaces(['main', 'resources']) -@connect((state, { shipId }) => shipRowDataSelectorFactory(shipId)(state)) -export class ShipRow extends Component { - static propTypes = { - ship: PropTypes.object, - $ship: PropTypes.object, - $shipTypes: PropTypes.object, - labelStatus: PropTypes.number, - enableAvatar: PropTypes.bool, - compact: PropTypes.bool, - shipAvatarColor: PropTypes.string, - } - - shouldComponentUpdate(nextProps) { +class ShipRowComponent extends Component { + shouldComponentUpdate(nextProps: ShipRowProps): boolean { // Remember to expand the list in case you add new properties to display const shipPickProps = [ 'api_lv', @@ -101,55 +144,64 @@ export class ShipRow extends Component { ) } - render() { + render(): React.ReactNode { const { - ship, - $ship, - $shipTypes, - labelStatus, + ship = {} as Ship, + $ship = {} as Ship, + $shipTypes = {}, + labelStatus = 0, enableAvatar, shipAvatarColor, showSpAttackLabel, compact, t, } = this.props + const hideShipName = enableAvatar && compact const labelStatusStyle = getStatusStyle(labelStatus) const hpPercentage = (ship.api_nowhp / ship.api_maxhp) * 100 - const fuelPercentage = (ship.api_fuel / $ship.api_fuel_max) * 100 - const ammoPercentage = (ship.api_bull / $ship.api_bull_max) * 100 + const fuelPercentage = (ship.api_fuel / ($ship?.api_fuel_max || 1)) * 100 + const ammoPercentage = (ship.api_bull / ($ship?.api_bull_max || 1)) * 100 + const fuelTip = ( - {ship.api_fuel} / {$ship.api_fuel_max} + {ship.api_fuel} / {$ship?.api_fuel_max} {fuelPercentage < 100 && ` (-${Math.max( 1, - Math.floor(($ship.api_fuel_max - ship.api_fuel) * (ship.api_lv > 99 ? 0.85 : 1)), + Math.floor( + (($ship?.api_fuel_max || 0) - ship.api_fuel) * (ship.api_lv && ship.api_lv > 99 ? 0.85 : 1), + ), )})`} ) + const ammoTip = ( - {ship.api_bull} / {$ship.api_bull_max} + {ship.api_bull} / {$ship?.api_bull_max} {ammoPercentage < 100 && ` (-${Math.max( 1, - Math.floor(($ship.api_bull_max - ship.api_bull) * (ship.api_lv > 99 ? 0.85 : 1)), + Math.floor( + (($ship?.api_bull_max || 0) - ship.api_bull) * (ship.api_lv && ship.api_lv > 99 ? 0.85 : 1), + ), )})`} ) + const shipBasicContent = ( <> Lv. {ship.api_lv || '??'} - {$shipTypes[$ship.api_stype] && $shipTypes[$ship.api_stype].api_name - ? t(`resources:${$shipTypes[$ship.api_stype].api_name}`) + {$shipTypes[$ship?.api_stype as number]?.api_name + ? t(`resources:${$shipTypes[$ship?.api_stype as number].api_name}`) : '??'} ) + const shipIndicatorsContent = ( <> {t(`main:${getSpeedLabel(ship.api_soku)}`)} @@ -165,6 +217,7 @@ export class ShipRow extends Component { )} ) + return ( -
{$ship.api_name ? t(`resources:${$ship.api_name}`) : '??'}
+
{$ship?.api_name ? t(`resources:${$ship.api_name}`) : '??'}
Lv. {ship.api_lv || '??'} Next. {(ship.api_exp || [])[1]}
@@ -190,7 +243,7 @@ export class ShipRow extends Component { {enableAvatar && ( <> - {$ship.api_name + {$ship?.api_name ? t(`resources:${$ship.api_name}`, { keySeparator: 'chiba' }) : '??'} @@ -297,3 +350,9 @@ export class ShipRow extends Component { ) } } + +const mapStateToProps = (state: unknown, ownProps: { shipId: number }): Partial => ({ + ...shipRowDataSelectorFactory(ownProps.shipId)(state), +}) + +export const ShipRow = connect(mapStateToProps)(withTranslation(['main', 'resources'])(ShipRowComponent)) diff --git a/views/components/ship/slotitems-data.es b/views/components/ship/slotitems-data.ts similarity index 52% rename from views/components/ship/slotitems-data.es rename to views/components/ship/slotitems-data.ts index 978b98e71..a9494767a 100644 --- a/views/components/ship/slotitems-data.es +++ b/views/components/ship/slotitems-data.ts @@ -1,6 +1,10 @@ import i18next from 'views/env-parts/i18next' -const types = { +interface TypesMap { + [key: string]: string +} + +const types: TypesMap = { api_taik: 'HP', api_souk: 'Armor', api_houg: 'Firepower', @@ -10,41 +14,47 @@ const types = { api_tyku: 'AA', api_tais: 'ASW', api_houm: 'Accuracy', - //"api_raim": "Torpedo Accuracy", + // "api_raim": "Torpedo Accuracy", api_houk: 'Evasion', - //"api_raik": "Torpedo Evasion", - //"api_bakk": "Bombing Evasion", + // "api_raik": "Torpedo Evasion", + // "api_bakk": "Bombing Evasion", api_saku: 'LOS', - //"api_sakb": "Anti-LOS", + // "api_sakb": "Anti-LOS", api_luck: 'Luck', api_leng: 'Range', } -const landbaseFighterTypes = { +const landbaseFighterTypes: TypesMap = { api_houm: 'Anti-Bomber', api_houk: 'Interception', } const range = ['Short', 'Medium', 'Long', 'Very Long'] -export function getItemData(slotitem) { - const data = [] +interface SlotItem { + api_type: [number, number, number, number] + [key: string]: unknown +} + +export function getItemData(slotitem: SlotItem): string[] { + const data: string[] = [] for (const type in types) { - if (slotitem[type] && slotitem[type] != 0) { + const value = slotitem[type] as number | undefined + if (value && value != 0) { if (type == 'api_leng') { data.push( - `${i18next.t('data:' + types[type])} ${i18next.t('data:' + range[slotitem[type] - 1])}`, + `${i18next.t('data:' + types[type])} ${i18next.t('data:' + range[value - 1])}`, ) } else if ( [48].includes(slotitem.api_type[2]) && ['api_houk', 'api_houm'].includes(type) && - slotitem[type] > 0 + value > 0 ) { - data.push(`${i18next.t('data:' + landbaseFighterTypes[type])} +${slotitem[type]}`) - } else if (slotitem[type] > 0) { - data.push(`${i18next.t('data:' + types[type])} +${slotitem[type]}`) + data.push(`${i18next.t('data:' + landbaseFighterTypes[type])} +${value}`) + } else if (value > 0) { + data.push(`${i18next.t('data:' + types[type])} +${value}`) } else { - data.push(`${i18next.t('data:' + types[type])} ${slotitem[type]}`) + data.push(`${i18next.t('data:' + types[type])} ${value}`) } } } diff --git a/views/components/ship/slotitems.es b/views/components/ship/slotitems.es deleted file mode 100644 index 0c1f59e07..000000000 --- a/views/components/ship/slotitems.es +++ /dev/null @@ -1,196 +0,0 @@ -import { Tooltip, Intent, Position } from '@blueprintjs/core' -import classNames from 'classnames' -import { memoize } from 'lodash' -import { join } from 'path-extra' -import React from 'react' -import FontAwesome from 'react-fontawesome' -import { withNamespaces } from 'react-i18next' -import { connect } from 'react-redux' -import { compose } from 'redux' -import { createSelector } from 'reselect' -import { SlotitemIcon } from 'views/components/etc/icon' -import { - SlotItems, - SlotItemContainer, - OnSlotMini, - ALevel, -} from 'views/components/ship-parts/styled-components' -import { equipIsAircraft } from 'views/utils/game-utils' -import { - shipDataSelectorFactory, - shipEquipDataSelectorFactory, - landbaseSelectorFactory, - landbaseEquipDataSelectorFactory, -} from 'views/utils/selectors' - -import { getItemData } from './slotitems-data' - -const slotitemsDataSelectorFactory = memoize((shipId) => - createSelector( - [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], - ([ship, $ship] = [{}, {}], equipsData) => ({ - api_maxeq: $ship.api_maxeq, - equipsData, - exslotUnlocked: ship.api_slot_ex !== 0, - }), - ), -) - -const landbaseSlotitemsDataSelectorFactory = memoize((landbaseId) => - createSelector( - [landbaseSelectorFactory(landbaseId), landbaseEquipDataSelectorFactory(landbaseId)], - (landbase = {}, equipsData) => ({ - api_maxeq: (landbase.api_plane_info || []).map((l) => l.api_max_count), - api_cond: (landbase.api_plane_info || []).map((l) => l.api_cond), - api_state: (landbase.api_plane_info || []).map((l) => l.api_state), - equipsData, - }), - ), -) - -export const Slotitems = compose( - withNamespaces(['resources']), - connect((state, { shipId }) => slotitemsDataSelectorFactory(shipId)(state)), -)(({ api_maxeq, equipsData, exslotUnlocked, t }) => ( - - {equipsData && - equipsData.map((equipData, equipIdx) => { - const isExslot = equipIdx === equipsData.length - 1 - if (isExslot && !equipData && !exslotUnlocked) { - return
- } - const [equip, $equip, onslot] = equipData || [] - const itemOverlay = equipData && ( -
-
-
- {$equip.api_name - ? t(`resources:${$equip.api_name}`, { keySeparator: '%%%%' }) - : '??'} - {equip.api_level == null || equip.api_level == 0 ? undefined : ( - - {' '} - - {equip.api_level} - - )} - {equip.api_alv && equip.api_alv >= 1 && equip.api_alv <= 7 && ( - - )} -
- {$equip && getItemData($equip).map((data, propId) =>
{data}
)} -
-
- ) - - const equipIconId = equipData ? $equip.api_type[3] : 0 - const showOnslot = !equipData || isExslot || equipIsAircraft($equip) - const maxOnslot = isExslot ? 0 : api_maxeq[equipIdx] - const onslotText = isExslot ? '+' : equipData ? `${onslot}` : `${maxOnslot}` - const onslotWarning = equipData && onslot < maxOnslot - - return ( - - - - - - ) - })} - -)) - -export const LandbaseSlotitems = compose( - withNamespaces(['resources']), - connect((state, { landbaseId }) => landbaseSlotitemsDataSelectorFactory(landbaseId)(state)), -)(({ api_maxeq, api_cond, api_state, equipsData, isMini, t, className }) => ( - - {equipsData && - equipsData.map((equipData, equipIdx) => { - const [equip, $equip, onslot] = equipData || [] - const equipIconId = equipData ? $equip.api_type[3] : 0 - const showOnslot = !equipData || equipIsAircraft($equip) - const maxOnslot = api_maxeq[equipIdx] - const onslotWarning = equipData && onslot < maxOnslot - const onslotText = equipData ? onslot : maxOnslot - const iconStyle = { - opacity: api_state[equipIdx] === 2 ? 0.5 : null, - filter: - api_cond[equipIdx] > 1 - ? `drop-shadow(0px 0px 4px ${api_cond[equipIdx] === 2 ? '#FB8C00' : '#E53935'})` - : null, - } - const itemOverlay = equipData && ( -
-
-
- {$equip.api_name - ? t(`resources:${$equip.api_name}`, { keySeparator: 'chiba' }) - : '??'} - {equip.api_level > 0 && ( - - {' '} - - {equip.api_level} - - )} - {equip.api_alv && equip.api_alv >= 1 && equip.api_alv <= 7 && ( - - )} - {isMini && ( - - {onslotText} - - )} - {$equip.api_distance} -
- {$equip && getItemData($equip).map((data, propId) =>
{data}
)} -
-
- ) - - return ( - - - - - - ) - })} -
-)) diff --git a/views/components/ship/slotitems.tsx b/views/components/ship/slotitems.tsx new file mode 100644 index 000000000..79cc46faa --- /dev/null +++ b/views/components/ship/slotitems.tsx @@ -0,0 +1,276 @@ +import { Tooltip, Intent, Position } from '@blueprintjs/core' +import classNames from 'classnames' +import { memoize } from 'lodash' +import { join } from 'path-extra' +import React from 'react' +import FontAwesome from 'react-fontawesome' +import { useTranslation } from 'react-i18next' +import { connect } from 'react-redux' +import { createSelector } from 'reselect' +import { SlotitemIcon } from 'views/components/etc/icon' +import { + SlotItems, + SlotItemContainer, + OnSlotMini, + ALevel, +} from 'views/components/ship-parts/styled-components' +import { equipIsAircraft } from 'views/utils/game-utils' +import { + shipDataSelectorFactory, + shipEquipDataSelectorFactory, + landbaseSelectorFactory, + landbaseEquipDataSelectorFactory, +} from 'views/utils/selectors' + +import { getItemData } from './slotitems-data' + +interface Equip { + api_slotitem_id?: number + api_name?: string + api_type: [number, number, number, number] + api_level?: number + api_alv?: number + api_distance?: number + [key: string]: unknown +} + +interface Ship { + api_slot_ex: number + [key: string]: unknown +} + +interface Landbase { + api_plane_info: Array<{ + api_max_count: number + api_cond: number + api_state: number + }> + [key: string]: unknown +} + +interface SlotitemsData { + api_maxeq: number[] + equipsData: ([Equip, Equip, number] | undefined)[] + exslotUnlocked: boolean +} + +interface LandbaseSlotitemsData { + api_maxeq: number[] + api_cond: number[] + api_state: number[] + equipsData: ([Equip, Equip, number] | undefined)[] +} + +interface SlotitemsProps { + shipId: number +} + +type SlotitemsStateProps = SlotitemsData + +interface SlotitemsComponentProps extends SlotitemsProps, SlotitemsStateProps {} + +interface LandbaseSlotitemsProps { + landbaseId: number + isMini?: boolean + className?: string +} + +type LandbaseSlotitemsStateProps = LandbaseSlotitemsData + +interface LandbaseSlotitemsComponentProps extends LandbaseSlotitemsProps, LandbaseSlotitemsStateProps {} + +const slotitemsDataSelectorFactory = memoize((shipId: number) => + createSelector( + [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], + ([ship, $ship] = [{}, {}] as [Ship, Ship], equipsData) => ({ + api_maxeq: $ship.api_maxeq, + equipsData, + exslotUnlocked: ship.api_slot_ex !== 0, + }), + ), +) + +const landbaseSlotitemsDataSelectorFactory = memoize((landbaseId: number) => + createSelector( + [landbaseSelectorFactory(landbaseId), landbaseEquipDataSelectorFactory(landbaseId)], + (landbase: Landbase = {} as Landbase, equipsData) => ({ + api_maxeq: (landbase.api_plane_info || []).map((l) => l.api_max_count), + api_cond: (landbase.api_plane_info || []).map((l) => l.api_cond), + api_state: (landbase.api_plane_info || []).map((l) => l.api_state), + equipsData, + }), + ), +) + +const SlotitemsComponent: React.FC = ({ + api_maxeq, + equipsData, + exslotUnlocked, +}) => { + const { t } = useTranslation(['resources']) + + return ( + + {equipsData && + equipsData.map((equipData, equipIdx) => { + const isExslot = equipIdx === equipsData.length - 1 + if (isExslot && !equipData && !exslotUnlocked) { + return
+ } + const [equip, $equip, onslot] = equipData || [] + const itemOverlay = equipData && ( +
+
+
+ {$equip?.api_name + ? t(`resources:${$equip.api_name}`, { keySeparator: '%%%%' }) + : '??'} + {equip?.api_level == null || equip?.api_level === 0 ? undefined : ( + + {' '} + + {equip.api_level} + + )} + {equip?.api_alv && equip.api_alv >= 1 && equip.api_alv <= 7 && ( + + )} +
+ {$equip && getItemData($equip as Equip).map((data, propId) =>
{data}
)} +
+
+ ) + + const equipIconId = equipData ? $equip?.api_type?.[3] : 0 + const showOnslot = !equipData || isExslot || equipIsAircraft($equip) + const maxOnslot = isExslot ? 0 : api_maxeq?.[equipIdx] ?? 0 + const onslotText = isExslot ? '+' : equipData ? `${onslot}` : `${maxOnslot}` + const onslotWarning = !!equipData && !!onslot && onslot < maxOnslot + + return ( + + + + + + ) + })} + + ) +} + +const LandbaseSlotitemsComponent: React.FC = ({ + api_maxeq, + api_cond, + api_state, + equipsData, + isMini, + className, +}) => { + const { t } = useTranslation(['resources']) + + return ( + + {equipsData && + equipsData.map((equipData, equipIdx) => { + const [equip, $equip, onslot] = equipData || [] + const equipIconId = equipData ? $equip?.api_type?.[3] : 0 + const showOnslot = !equipData || equipIsAircraft($equip) + const maxOnslot = api_maxeq?.[equipIdx] ?? 0 + const onslotWarning = !!equipData && !!onslot && onslot < maxOnslot + const onslotText = equipData ? onslot : maxOnslot + const iconStyle: React.CSSProperties = { + opacity: api_state?.[equipIdx] === 2 ? 0.5 : undefined, + filter: + api_cond?.[equipIdx] > 1 + ? `drop-shadow(0px 0px 4px ${api_cond[equipIdx] === 2 ? '#FB8C00' : '#E53935'})` + : undefined, + } + const itemOverlay = equipData && ( +
+
+
+ {$equip?.api_name + ? t(`resources:${$equip.api_name}`, { keySeparator: 'chiba' }) + : '??'} + {equip && (equip as Equip).api_level && (equip as Equip).api_level! > 0 && ( + + {' '} + + {(equip as Equip).api_level} + + )} + {(equip as Equip)?.api_alv && + (equip as Equip).api_alv! >= 1 && + (equip as Equip).api_alv! <= 7 && ( + + )} + {isMini && ( + + {onslotText} + + )} + {(equip as Equip)?.api_distance} +
+ {$equip && getItemData($equip as Equip).map((data, propId) =>
{data}
)} +
+
+ ) + + return ( + + + + + + ) + })} +
+ ) +} + +const mapStateToPropsSlotitems = (state: unknown, ownProps: SlotitemsProps): SlotitemsStateProps => ({ + ...slotitemsDataSelectorFactory(ownProps.shipId)(state), +}) + +const mapStateToPropsLandbase = (state: unknown, ownProps: LandbaseSlotitemsProps): LandbaseSlotitemsStateProps => ({ + ...landbaseSlotitemsDataSelectorFactory(ownProps.landbaseId)(state), +}) + +export const Slotitems = connect(mapStateToPropsSlotitems)(SlotitemsComponent) +export const LandbaseSlotitems = connect(mapStateToPropsLandbase)(LandbaseSlotitemsComponent) From a2ebb02dad52ccde7fe751ae545d8fedd574cfbb Mon Sep 17 00:00:00 2001 From: chiba-bot Date: Sat, 21 Feb 2026 22:14:43 +0000 Subject: [PATCH 2/9] fix: address Copilot review comments from PR #2642 - Fix shipId unused variable issue in shipRowDataSelectorFactory - Fix Selector output type mismatches: - shipDataSelectorFactory in ship-item.tsx - slotitemsDataSelectorFactory and landbaseSlotitemsDataSelectorFactory in slotitems.tsx - SquadSelectorFactory in lbac-view.tsx - Fix onslotWarning logic error (use typeof check instead of !!onslot) - Fix Interface definitions to match actual selector return types - Add missing api_maxeq property to Ship interface - Add eslint-disable comments for necessary type assertions --- views/components/ship/lbac-view.tsx | 17 ++--- views/components/ship/ship-item.tsx | 39 +++++++--- views/components/ship/slotitems.tsx | 109 +++++++++++++++++++--------- 3 files changed, 108 insertions(+), 57 deletions(-) diff --git a/views/components/ship/lbac-view.tsx b/views/components/ship/lbac-view.tsx index 20ee49903..14d7c6e85 100644 --- a/views/components/ship/lbac-view.tsx +++ b/views/components/ship/lbac-view.tsx @@ -59,14 +59,13 @@ interface SquadRowProps extends SquadSelectorProps { compact: boolean } -type SquadRowStateProps = SquadStateProps - const SquadSelectorFactory = memoize((squardId: number) => createSelector( [landbaseSelectorFactory(squardId), landbaseEquipDataSelectorFactory(squardId)], - (landbase: Landbase, equipsData: EquipData[]) => ({ - landbase, - equipsData, + (landbase: Landbase | undefined, equipsData: EquipData[] | undefined): SquadStateProps => ({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + landbase: landbase ?? ({} as Landbase), + equipsData: equipsData ?? [], squardId, }), ), @@ -81,13 +80,7 @@ const SquadRowComponent: React.FC = ({ }) => { const { t } = useTranslation(['main']) - const { - api_action_kind, - api_distance, - api_name, - api_nowhp = 200, - api_maxhp = 200, - } = landbase + const { api_action_kind, api_distance, api_name, api_nowhp = 200, api_maxhp = 200 } = landbase const { api_base = 0, api_bonus = 0 } = api_distance || {} const tyku = getTyku([equipsData], api_action_kind) diff --git a/views/components/ship/ship-item.tsx b/views/components/ship/ship-item.tsx index bc1e9e2ab..4f895c213 100644 --- a/views/components/ship/ship-item.tsx +++ b/views/components/ship/ship-item.tsx @@ -108,19 +108,25 @@ const shipRowDataSelectorFactory = memoize((shipId: number) => (state: Record) => get(state, 'config.poi.appearance.avatarType'), ], ( - [ship, $ship] = [{}, {}] as [Ship, Ship], + shipData: [Ship, Ship] | undefined, repairDock: RepairDock | undefined, { $shipTypes }: ConstData, escaped: boolean, shipTagColor: string[], avatarType: string, - ): ShipRowData => ({ - ship: ship || ({} as Ship), - $ship: $ship || ({} as Ship), - $shipTypes, - labelStatus: getShipLabelStatus(ship, $ship, repairDock, escaped), - shipAvatarColor: selectShipAvatarColor(ship, $ship, shipTagColor, avatarType), - }), + ): ShipRowData => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const [ship, $ship] = shipData ?? [{} as Ship, {} as Ship] + return { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + ship: ship || ({} as Ship), + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + $ship: $ship || ({} as Ship), + $shipTypes, + labelStatus: getShipLabelStatus(ship, $ship, repairDock, escaped), + shipAvatarColor: selectShipAvatarColor(ship, $ship, shipTagColor, avatarType), + } + }, ), ) @@ -146,7 +152,9 @@ class ShipRowComponent extends Component { render(): React.ReactNode { const { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion ship = {} as Ship, + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion $ship = {} as Ship, $shipTypes = {}, labelStatus = 0, @@ -171,7 +179,8 @@ class ShipRowComponent extends Component { ` (-${Math.max( 1, Math.floor( - (($ship?.api_fuel_max || 0) - ship.api_fuel) * (ship.api_lv && ship.api_lv > 99 ? 0.85 : 1), + (($ship?.api_fuel_max || 0) - ship.api_fuel) * + (ship.api_lv && ship.api_lv > 99 ? 0.85 : 1), ), )})`} @@ -185,7 +194,8 @@ class ShipRowComponent extends Component { ` (-${Math.max( 1, Math.floor( - (($ship?.api_bull_max || 0) - ship.api_bull) * (ship.api_lv && ship.api_lv > 99 ? 0.85 : 1), + (($ship?.api_bull_max || 0) - ship.api_bull) * + (ship.api_lv && ship.api_lv > 99 ? 0.85 : 1), ), )})`} @@ -195,8 +205,10 @@ class ShipRowComponent extends Component { <> Lv. {ship.api_lv || '??'} + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} {$shipTypes[$ship?.api_stype as number]?.api_name - ? t(`resources:${$shipTypes[$ship?.api_stype as number].api_name}`) + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + t(`resources:${$shipTypes[$ship?.api_stype as number].api_name}`) : '??'} @@ -243,6 +255,7 @@ class ShipRowComponent extends Component { {enableAvatar && ( <> createSelector( [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], - ([ship, $ship] = [{}, {}] as [Ship, Ship], equipsData) => ({ - api_maxeq: $ship.api_maxeq, - equipsData, - exslotUnlocked: ship.api_slot_ex !== 0, - }), + ( + shipData: [Ship, Ship] | undefined, + equipsData: ([Equip, Equip, number] | undefined)[] | undefined, + ): SlotitemsData => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const [ship, $ship] = shipData ?? [{} as Ship, {} as Ship] + return { + api_maxeq: $ship?.api_maxeq ?? [], + equipsData: equipsData ?? [], + exslotUnlocked: ship?.api_slot_ex !== 0, + } + }, ), ) const landbaseSlotitemsDataSelectorFactory = memoize((landbaseId: number) => createSelector( [landbaseSelectorFactory(landbaseId), landbaseEquipDataSelectorFactory(landbaseId)], - (landbase: Landbase = {} as Landbase, equipsData) => ({ - api_maxeq: (landbase.api_plane_info || []).map((l) => l.api_max_count), - api_cond: (landbase.api_plane_info || []).map((l) => l.api_cond), - api_state: (landbase.api_plane_info || []).map((l) => l.api_state), - equipsData, + ( + landbase: Landbase | undefined, + equipsData: ([Equip, Equip, number] | undefined)[] | undefined, + ): LandbaseSlotitemsData => ({ + api_maxeq: (landbase?.api_plane_info || []).map((l) => l.api_max_count), + api_cond: (landbase?.api_plane_info || []).map((l) => l.api_cond), + api_state: (landbase?.api_plane_info || []).map((l) => l.api_state), + equipsData: equipsData ?? [], }), ), ) @@ -139,16 +151,20 @@ const SlotitemsComponent: React.FC = ({ /> )}
- {$equip && getItemData($equip as Equip).map((data, propId) =>
{data}
)} + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} + {$equip && + getItemData($equip as Equip).map((data, propId) => ( +
{data}
+ ))}
) const equipIconId = equipData ? $equip?.api_type?.[3] : 0 const showOnslot = !equipData || isExslot || equipIsAircraft($equip) - const maxOnslot = isExslot ? 0 : api_maxeq?.[equipIdx] ?? 0 + const maxOnslot = isExslot ? 0 : (api_maxeq?.[equipIdx] ?? 0) const onslotText = isExslot ? '+' : equipData ? `${onslot}` : `${maxOnslot}` - const onslotWarning = !!equipData && !!onslot && onslot < maxOnslot + const onslotWarning = !!equipData && typeof onslot === 'number' && onslot < maxOnslot return ( = ({ const equipIconId = equipData ? $equip?.api_type?.[3] : 0 const showOnslot = !equipData || equipIsAircraft($equip) const maxOnslot = api_maxeq?.[equipIdx] ?? 0 - const onslotWarning = !!equipData && !!onslot && onslot < maxOnslot + const onslotWarning = !!equipData && typeof onslot === 'number' && onslot < maxOnslot const onslotText = equipData ? onslot : maxOnslot const iconStyle: React.CSSProperties = { opacity: api_state?.[equipIdx] === 2 ? 0.5 : undefined, @@ -209,21 +225,36 @@ const LandbaseSlotitemsComponent: React.FC = ({ {$equip?.api_name ? t(`resources:${$equip.api_name}`, { keySeparator: 'chiba' }) : '??'} - {equip && (equip as Equip).api_level && (equip as Equip).api_level! > 0 && ( - - {' '} - - {(equip as Equip).api_level} - + {equip && ( + <> + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} + {(equip as Equip).api_level && (equip as Equip).api_level! > 0 && ( + + {' '} + + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} + {(equip as Equip).api_level} + + )} + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} + {(equip as Equip)?.api_alv && + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (equip as Equip).api_alv! >= 1 && + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (equip as Equip).api_alv! <= 7 && ( + + )} + )} - {(equip as Equip)?.api_alv && - (equip as Equip).api_alv! >= 1 && - (equip as Equip).api_alv! <= 7 && ( - - )} {isMini && ( = ({ {onslotText} )} - {(equip as Equip)?.api_distance} + {' '} + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} + {(equip as Equip)?.api_distance} - {$equip && getItemData($equip as Equip).map((data, propId) =>
{data}
)} + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} + {$equip && + getItemData($equip as Equip).map((data, propId) => ( +
{data}
+ ))} ) @@ -264,11 +301,17 @@ const LandbaseSlotitemsComponent: React.FC = ({ ) } -const mapStateToPropsSlotitems = (state: unknown, ownProps: SlotitemsProps): SlotitemsStateProps => ({ +const mapStateToPropsSlotitems = ( + state: unknown, + ownProps: SlotitemsProps, +): SlotitemsStateProps => ({ ...slotitemsDataSelectorFactory(ownProps.shipId)(state), }) -const mapStateToPropsLandbase = (state: unknown, ownProps: LandbaseSlotitemsProps): LandbaseSlotitemsStateProps => ({ +const mapStateToPropsLandbase = ( + state: unknown, + ownProps: LandbaseSlotitemsProps, +): LandbaseSlotitemsStateProps => ({ ...landbaseSlotitemsDataSelectorFactory(ownProps.landbaseId)(state), }) From 1928a445a2a5511e768bb927663d392aa8dbd8e7 Mon Sep 17 00:00:00 2001 From: chiba-bot Date: Sun, 22 Feb 2026 05:40:47 +0000 Subject: [PATCH 3/9] fix: resolve ESLint errors --- views/components/ship/aaci-indicator.tsx | 18 ++---------------- views/components/ship/index.tsx | 10 +++++++--- views/components/ship/slotitems-data.ts | 6 ++---- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/views/components/ship/aaci-indicator.tsx b/views/components/ship/aaci-indicator.tsx index 085e50229..241b2cf45 100644 --- a/views/components/ship/aaci-indicator.tsx +++ b/views/components/ship/aaci-indicator.tsx @@ -31,12 +31,6 @@ const __t = (name: string[]) => )) -interface AACIInfo { - fixed: number - modifier: number - name: string[] -} - interface AACISelectorResult { AACIs: number[] maxShotdown: number @@ -67,11 +61,7 @@ const maxAACIShotdownSelectorFactory = memoize((shipId: number) => }), ) -const AACIIndicatorComponent: React.FC = ({ - AACIs, - maxShotdown, - shipId, -}) => { +const AACIIndicatorComponent: React.FC = ({ AACIs, maxShotdown, shipId }) => { const { t } = useTranslation(['main']) const currentMax = Math.max(...AACIs.map((id) => AACITable[id].fixed || 0)) @@ -81,11 +71,7 @@ const AACIIndicatorComponent: React.FC = ({ {t('main:AACIType', { count: id })} - - {get(AACITable, `${id}.name.length`, 0) > 0 - ? __t(AACITable[id].name) - : ''} - + {get(AACITable, `${id}.name.length`, 0) > 0 ? __t(AACITable[id].name) : ''} {t('main:Shot down', { count: AACITable[id].fixed })} diff --git a/views/components/ship/index.tsx b/views/components/ship/index.tsx index b53a639be..deb814c90 100644 --- a/views/components/ship/index.tsx +++ b/views/components/ship/index.tsx @@ -105,7 +105,6 @@ interface ReactClassState { } /* global getStore */ -declare function getStore(key: string): unknown const shipRowWidthSelector = (state: StateData) => get(state, 'layout.shippane.width', 450) @@ -137,8 +136,9 @@ const ShipViewSwitchButton: React.FC = ({ ) -const ConnectedShipViewSwitchButton = connect((state: StateData, { fleetId }: { fleetId: number }) => - shipViewSwitchButtonDataSelectorFactory(fleetId)(state), +const ConnectedShipViewSwitchButton = connect( + (state: StateData, { fleetId }: { fleetId: number }) => + shipViewSwitchButtonDataSelectorFactory(fleetId)(state), )(ShipViewSwitchButton) const fleetShipViewDataSelectorFactory = memoize((fleetId: number) => @@ -274,7 +274,9 @@ class ReactClassComponent extends Component { if ( width !== 0 && height !== 0 && + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion (width !== (getStore('layout.shippane.width') as number) || + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion height !== (getStore('layout.shippane.height') as number)) ) { this.props.dispatch({ @@ -381,3 +383,5 @@ export const displayName = ( ) export const icon = + +export const name = 'ship-view' diff --git a/views/components/ship/slotitems-data.ts b/views/components/ship/slotitems-data.ts index a9494767a..95242db2c 100644 --- a/views/components/ship/slotitems-data.ts +++ b/views/components/ship/slotitems-data.ts @@ -39,12 +39,10 @@ interface SlotItem { export function getItemData(slotitem: SlotItem): string[] { const data: string[] = [] for (const type in types) { - const value = slotitem[type] as number | undefined + const value = slotitem[type] as number | undefined // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion if (value && value != 0) { if (type == 'api_leng') { - data.push( - `${i18next.t('data:' + types[type])} ${i18next.t('data:' + range[value - 1])}`, - ) + data.push(`${i18next.t('data:' + types[type])} ${i18next.t('data:' + range[value - 1])}`) } else if ( [48].includes(slotitem.api_type[2]) && ['api_houk', 'api_houm'].includes(type) && From 020cf58292b53eeace902e6b9f7930fb9c4b4261 Mon Sep 17 00:00:00 2001 From: chiba-bot Date: Sun, 22 Feb 2026 05:48:02 +0000 Subject: [PATCH 4/9] fix: remove unreachable code in ExtraDebugger.getLogFunc --- lib/debug.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/debug.ts b/lib/debug.ts index 8f39b1573..9d93d234a 100644 --- a/lib/debug.ts +++ b/lib/debug.ts @@ -143,11 +143,12 @@ class ExtraDebugger extends BaseDebugger { } protected getLogFunc(level: LogType = 'log') { - if (this.prefix != null && isConsoleLogMethod(level)) { + // 'assert' and 'table' are handled in getLeveledLog, so this only receives ConsoleLogMethod levels + if (isConsoleLogMethod(level)) { return console[level].bind(console, ...getLogformatArgs(level, this.prefix)) - } else { - return console[level].bind(console) } + // Type-safe fallback - unreachable due to getLeveledLog handling but satisfies TypeScript + return console.log.bind(console) } } From 42833346a86325ea4926eea1171b4be01647b7c0 Mon Sep 17 00:00:00 2001 From: chiba-bot Date: Sun, 22 Feb 2026 07:34:16 +0000 Subject: [PATCH 5/9] chore: remove unrelated debug.ts changes from this PR --- lib/debug.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/debug.ts b/lib/debug.ts index 9d93d234a..8f39b1573 100644 --- a/lib/debug.ts +++ b/lib/debug.ts @@ -143,12 +143,11 @@ class ExtraDebugger extends BaseDebugger { } protected getLogFunc(level: LogType = 'log') { - // 'assert' and 'table' are handled in getLeveledLog, so this only receives ConsoleLogMethod levels - if (isConsoleLogMethod(level)) { + if (this.prefix != null && isConsoleLogMethod(level)) { return console[level].bind(console, ...getLogformatArgs(level, this.prefix)) + } else { + return console[level].bind(console) } - // Type-safe fallback - unreachable due to getLeveledLog handling but satisfies TypeScript - return console.log.bind(console) } } From 373998c51335867cb5dd29987f3672fee20bf52f Mon Sep 17 00:00:00 2001 From: chiba-bot Date: Sun, 22 Feb 2026 12:34:38 +0000 Subject: [PATCH 6/9] fix: address Copilot review feedback - Add explanatory text to all ESLint disable comments for @typescript-eslint/no-unsafe-type-assertion - Refactor repeated equip as Equip type assertions in LandbaseSlotitemsComponent to assert once - Fix value && value != 0 to value !== undefined && value !== 0 to not skip zero values - Change loose equality == to strict === for api_leng comparison --- views/components/etc/webview.tsx | 2 +- views/components/ship/index.tsx | 4 +- views/components/ship/lbac-view.tsx | 2 +- views/components/ship/ship-item.tsx | 16 +++--- views/components/ship/slotitems-data.ts | 6 +-- views/components/ship/slotitems.tsx | 65 ++++++++++++------------- 6 files changed, 47 insertions(+), 48 deletions(-) diff --git a/views/components/etc/webview.tsx b/views/components/etc/webview.tsx index 7200fe1d8..044f0657e 100644 --- a/views/components/etc/webview.tsx +++ b/views/components/etc/webview.tsx @@ -127,7 +127,7 @@ const ElectronWebView = forwardRef( const errorScript = `document.write('
Webview load error
Error Code: ${e.errorCode}
Description: ${e.errorDescription}
URL: ${e.validatedURL}')\ndocument.body.style.backgroundColor = "white"` const target = e.target if (target && 'executeJavaScript' in target) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- target is narrowed by 'executeJavaScript' in check above const webviewTarget = target as WebviewTag webviewTarget.executeJavaScript(errorScript) } diff --git a/views/components/ship/index.tsx b/views/components/ship/index.tsx index deb814c90..2ed596a83 100644 --- a/views/components/ship/index.tsx +++ b/views/components/ship/index.tsx @@ -274,9 +274,9 @@ class ReactClassComponent extends Component { if ( width !== 0 && height !== 0 && - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- getStore returns unknown, layout values are known to be numbers (width !== (getStore('layout.shippane.width') as number) || - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- getStore returns unknown, layout values are known to be numbers height !== (getStore('layout.shippane.height') as number)) ) { this.props.dispatch({ diff --git a/views/components/ship/lbac-view.tsx b/views/components/ship/lbac-view.tsx index 14d7c6e85..d5ed37308 100644 --- a/views/components/ship/lbac-view.tsx +++ b/views/components/ship/lbac-view.tsx @@ -63,7 +63,7 @@ const SquadSelectorFactory = memoize((squardId: number) => createSelector( [landbaseSelectorFactory(squardId), landbaseEquipDataSelectorFactory(squardId)], (landbase: Landbase | undefined, equipsData: EquipData[] | undefined): SquadStateProps => ({ - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- default empty Landbase when selector returns undefined landbase: landbase ?? ({} as Landbase), equipsData: equipsData ?? [], squardId, diff --git a/views/components/ship/ship-item.tsx b/views/components/ship/ship-item.tsx index 4f895c213..0483d7e3a 100644 --- a/views/components/ship/ship-item.tsx +++ b/views/components/ship/ship-item.tsx @@ -115,12 +115,12 @@ const shipRowDataSelectorFactory = memoize((shipId: number) => shipTagColor: string[], avatarType: string, ): ShipRowData => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- default empty Ship when shipData is undefined const [ship, $ship] = shipData ?? [{} as Ship, {} as Ship] return { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- fallback empty Ship when ship is falsy ship: ship || ({} as Ship), - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- fallback empty Ship when $ship is falsy $ship: $ship || ({} as Ship), $shipTypes, labelStatus: getShipLabelStatus(ship, $ship, repairDock, escaped), @@ -152,9 +152,9 @@ class ShipRowComponent extends Component { render(): React.ReactNode { const { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- default empty Ship for destructuring default value ship = {} as Ship, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- default empty Ship for destructuring default value $ship = {} as Ship, $shipTypes = {}, labelStatus = 0, @@ -205,9 +205,9 @@ class ShipRowComponent extends Component { <> Lv. {ship.api_lv || '??'} - {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- api_stype is a number key used for $shipTypes lookup */} {$shipTypes[$ship?.api_stype as number]?.api_name - ? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- same api_stype assertion for translation key t(`resources:${$shipTypes[$ship?.api_stype as number].api_name}`) : '??'} @@ -255,7 +255,7 @@ class ShipRowComponent extends Component { {enableAvatar && ( <> shipData: [Ship, Ship] | undefined, equipsData: ([Equip, Equip, number] | undefined)[] | undefined, ): SlotitemsData => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- provide default empty Ship objects when shipData is undefined const [ship, $ship] = shipData ?? [{} as Ship, {} as Ship] return { api_maxeq: $ship?.api_maxeq ?? [], @@ -151,7 +151,7 @@ const SlotitemsComponent: React.FC = ({ /> )} - {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- $equip from selector conforms to Equip shape when truthy */} {$equip && getItemData($equip as Equip).map((data, propId) => (
{data}
@@ -225,36 +225,35 @@ const LandbaseSlotitemsComponent: React.FC = ({ {$equip?.api_name ? t(`resources:${$equip.api_name}`, { keySeparator: 'chiba' }) : '??'} - {equip && ( - <> - {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} - {(equip as Equip).api_level && (equip as Equip).api_level! > 0 && ( - - {' '} - - {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} - {(equip as Equip).api_level} - - )} - {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} - {(equip as Equip)?.api_alv && - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - (equip as Equip).api_alv! >= 1 && - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - (equip as Equip).api_alv! <= 7 && ( - { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- equip from equipData tuple is typed loosely, assert once for property access + const typedEquip = equip as Equip + return ( + <> + {typedEquip.api_level && typedEquip.api_level > 0 && ( + + {' '} + + {typedEquip.api_level} + + )} + {typedEquip?.api_alv && + typedEquip.api_alv >= 1 && + typedEquip.api_alv <= 7 && ( + )} - /> - )} - - )} + + ) + })()} {isMini && ( = ({ )} {' '} - {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- equip from equipData tuple needs assertion for api_distance access */} {(equip as Equip)?.api_distance} - {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- $equip from selector conforms to Equip shape when truthy */} {$equip && getItemData($equip as Equip).map((data, propId) => (
{data}
From 9a3ed144159958935cbc87599d31fa2d82dd438e Mon Sep 17 00:00:00 2001 From: chiba-bot Date: Sun, 22 Feb 2026 15:34:13 +0000 Subject: [PATCH 7/9] fix: address Kimi review findings - null safety and bounds checks - ship-item.tsx: Guard against division by zero in HP percentage calculation - ship-item.tsx: Add nullish coalescing for api_fuel and api_bull access - slotitems.tsx: Fix exslotUnlocked false positive when ship is empty object - slotitems-data.ts: Add nullish coalescing for range array bounds access - aaci-indicator.tsx: Use get() consistently for AACITable property access --- views/components/ship/aaci-indicator.tsx | 4 ++-- views/components/ship/ship-item.tsx | 14 +++++++------- views/components/ship/slotitems-data.ts | 4 +++- views/components/ship/slotitems.tsx | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/views/components/ship/aaci-indicator.tsx b/views/components/ship/aaci-indicator.tsx index 241b2cf45..b41d39cff 100644 --- a/views/components/ship/aaci-indicator.tsx +++ b/views/components/ship/aaci-indicator.tsx @@ -73,9 +73,9 @@ const AACIIndicatorComponent: React.FC = ({ AACIs, maxShotdo {t('main:AACIType', { count: id })} {get(AACITable, `${id}.name.length`, 0) > 0 ? __t(AACITable[id].name) : ''} - {t('main:Shot down', { count: AACITable[id].fixed })} + {t('main:Shot down', { count: get(AACITable, `${id}.fixed`, 0) })} - {t('main:Modifier', { count: AACITable[id].modifier })} + {t('main:Modifier', { count: get(AACITable, `${id}.modifier`, 0) })}
))} diff --git a/views/components/ship/ship-item.tsx b/views/components/ship/ship-item.tsx index 0483d7e3a..2ffa7a5ab 100644 --- a/views/components/ship/ship-item.tsx +++ b/views/components/ship/ship-item.tsx @@ -167,19 +167,19 @@ class ShipRowComponent extends Component { const hideShipName = enableAvatar && compact const labelStatusStyle = getStatusStyle(labelStatus) - const hpPercentage = (ship.api_nowhp / ship.api_maxhp) * 100 - const fuelPercentage = (ship.api_fuel / ($ship?.api_fuel_max || 1)) * 100 - const ammoPercentage = (ship.api_bull / ($ship?.api_bull_max || 1)) * 100 + const hpPercentage = ship.api_maxhp ? (ship.api_nowhp / ship.api_maxhp) * 100 : 0 + const fuelPercentage = ((ship.api_fuel ?? 0) / ($ship?.api_fuel_max || 1)) * 100 + const ammoPercentage = ((ship.api_bull ?? 0) / ($ship?.api_bull_max || 1)) * 100 const fuelTip = ( - {ship.api_fuel} / {$ship?.api_fuel_max} + {ship.api_fuel ?? 0} / {$ship?.api_fuel_max} {fuelPercentage < 100 && ` (-${Math.max( 1, Math.floor( - (($ship?.api_fuel_max || 0) - ship.api_fuel) * + (($ship?.api_fuel_max || 0) - (ship.api_fuel ?? 0)) * (ship.api_lv && ship.api_lv > 99 ? 0.85 : 1), ), )})`} @@ -189,12 +189,12 @@ class ShipRowComponent extends Component { const ammoTip = ( - {ship.api_bull} / {$ship?.api_bull_max} + {ship.api_bull ?? 0} / {$ship?.api_bull_max} {ammoPercentage < 100 && ` (-${Math.max( 1, Math.floor( - (($ship?.api_bull_max || 0) - ship.api_bull) * + (($ship?.api_bull_max || 0) - (ship.api_bull ?? 0)) * (ship.api_lv && ship.api_lv > 99 ? 0.85 : 1), ), )})`} diff --git a/views/components/ship/slotitems-data.ts b/views/components/ship/slotitems-data.ts index cbd20518c..6511d0eb0 100644 --- a/views/components/ship/slotitems-data.ts +++ b/views/components/ship/slotitems-data.ts @@ -42,7 +42,9 @@ export function getItemData(slotitem: SlotItem): string[] { const value = slotitem[type] as number | undefined // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion -- dynamic property access on SlotItem requires numeric assertion if (value !== undefined && value !== 0) { if (type === 'api_leng') { - data.push(`${i18next.t('data:' + types[type])} ${i18next.t('data:' + range[value - 1])}`) + data.push( + `${i18next.t('data:' + types[type])} ${i18next.t('data:' + (range[value - 1] ?? ''))}`, + ) } else if ( [48].includes(slotitem.api_type[2]) && ['api_houk', 'api_houm'].includes(type) && diff --git a/views/components/ship/slotitems.tsx b/views/components/ship/slotitems.tsx index 0d2407b6f..d7ded5035 100644 --- a/views/components/ship/slotitems.tsx +++ b/views/components/ship/slotitems.tsx @@ -93,7 +93,7 @@ const slotitemsDataSelectorFactory = memoize((shipId: number) => return { api_maxeq: $ship?.api_maxeq ?? [], equipsData: equipsData ?? [], - exslotUnlocked: ship?.api_slot_ex !== 0, + exslotUnlocked: (ship?.api_slot_ex ?? 0) !== 0, } }, ), From 8063a5f68ae0917b6fb2afe7f0c65a894c912dda Mon Sep 17 00:00:00 2001 From: chiba-bot Date: Sun, 22 Feb 2026 18:37:48 +0000 Subject: [PATCH 8/9] fix: address Copilot review - unused vars and optional chaining - Remove unused shipId destructuring in OASWIndicator and AACIIndicator - Use consistent optional chaining for api_cond in slotitems.tsx --- views/components/ship/aaci-indicator.tsx | 2 +- views/components/ship/oasw-indicator.tsx | 2 +- views/components/ship/slotitems.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/views/components/ship/aaci-indicator.tsx b/views/components/ship/aaci-indicator.tsx index b41d39cff..728d3cd37 100644 --- a/views/components/ship/aaci-indicator.tsx +++ b/views/components/ship/aaci-indicator.tsx @@ -61,7 +61,7 @@ const maxAACIShotdownSelectorFactory = memoize((shipId: number) => }), ) -const AACIIndicatorComponent: React.FC = ({ AACIs, maxShotdown, shipId }) => { +const AACIIndicatorComponent: React.FC = ({ AACIs, maxShotdown }) => { const { t } = useTranslation(['main']) const currentMax = Math.max(...AACIs.map((id) => AACITable[id].fixed || 0)) diff --git a/views/components/ship/oasw-indicator.tsx b/views/components/ship/oasw-indicator.tsx index 3493f6b39..3d84a02de 100644 --- a/views/components/ship/oasw-indicator.tsx +++ b/views/components/ship/oasw-indicator.tsx @@ -30,7 +30,7 @@ const OASWSelectorFactory = memoize((shipId: number) => ), ) -const OASWIndicatorComponent: React.FC = ({ isOASW, shipId }) => { +const OASWIndicatorComponent: React.FC = ({ isOASW }) => { const { t } = useTranslation(['main']) return ( <> diff --git a/views/components/ship/slotitems.tsx b/views/components/ship/slotitems.tsx index d7ded5035..e395b67a2 100644 --- a/views/components/ship/slotitems.tsx +++ b/views/components/ship/slotitems.tsx @@ -215,7 +215,7 @@ const LandbaseSlotitemsComponent: React.FC = ({ opacity: api_state?.[equipIdx] === 2 ? 0.5 : undefined, filter: api_cond?.[equipIdx] > 1 - ? `drop-shadow(0px 0px 4px ${api_cond[equipIdx] === 2 ? '#FB8C00' : '#E53935'})` + ? `drop-shadow(0px 0px 4px ${api_cond?.[equipIdx] === 2 ? '#FB8C00' : '#E53935'})` : undefined, } const itemOverlay = equipData && ( From 424595ce01d7ef89ecb173abc81aafa95cec2749 Mon Sep 17 00:00:00 2001 From: chiba-bot Date: Sun, 1 Mar 2026 19:08:38 +0000 Subject: [PATCH 9/9] refactor: remove unused shipId from component props types --- views/components/ship/aaci-indicator.tsx | 5 +---- views/components/ship/oasw-indicator.tsx | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/views/components/ship/aaci-indicator.tsx b/views/components/ship/aaci-indicator.tsx index 728d3cd37..d57ffb9af 100644 --- a/views/components/ship/aaci-indicator.tsx +++ b/views/components/ship/aaci-indicator.tsx @@ -36,9 +36,6 @@ interface AACISelectorResult { maxShotdown: number } -interface AACIIndicatorProps extends AACISelectorResult { - shipId: number -} const AACISelectorFactory = memoize((shipId: number) => createSelector( @@ -61,7 +58,7 @@ const maxAACIShotdownSelectorFactory = memoize((shipId: number) => }), ) -const AACIIndicatorComponent: React.FC = ({ AACIs, maxShotdown }) => { +const AACIIndicatorComponent: React.FC = ({ AACIs, maxShotdown }) => { const { t } = useTranslation(['main']) const currentMax = Math.max(...AACIs.map((id) => AACITable[id].fixed || 0)) diff --git a/views/components/ship/oasw-indicator.tsx b/views/components/ship/oasw-indicator.tsx index 3d84a02de..829c33f7e 100644 --- a/views/components/ship/oasw-indicator.tsx +++ b/views/components/ship/oasw-indicator.tsx @@ -12,9 +12,6 @@ interface OASWSelectorResult { isOASW: boolean } -interface OASWIndicatorProps extends OASWSelectorResult { - shipId: number -} const OASWSelectorFactory = memoize((shipId: number) => createSelector( @@ -30,7 +27,7 @@ const OASWSelectorFactory = memoize((shipId: number) => ), ) -const OASWIndicatorComponent: React.FC = ({ isOASW }) => { +const OASWIndicatorComponent: React.FC = ({ isOASW }) => { const { t } = useTranslation(['main']) return ( <>