From 59bcd96e617f251d9aa7c01c355343b02e4e0a85 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 4 Jul 2026 02:14:45 +0900 Subject: [PATCH] Add first battle deployment phase --- src/game/scenes/BattleScene.ts | 537 ++++++++++++++++++++++++++++++++- 1 file changed, 533 insertions(+), 4 deletions(-) diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 4476ef4..709f22d 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -1108,7 +1108,7 @@ type UnitActionPose = 'attack' | 'strategy' | 'item' | 'hurt' | 'celebrate'; const levelUpCelebrationStepOffsets = [0, -7, -2, -9, 0, -6, -3, -8]; -type BattlePhase = 'idle' | 'moving' | 'command' | 'targeting' | 'animating' | 'resolved'; +type BattlePhase = 'deployment' | 'idle' | 'moving' | 'command' | 'targeting' | 'animating' | 'resolved'; type BattleCommand = 'attack' | 'strategy' | 'item' | 'wait'; type DamageCommand = Exclude; type UsableCommand = Extract; @@ -1146,6 +1146,17 @@ type TurnEndPromptOptions = { secondaryAction?: () => void; }; +type DeploymentSlot = { + x: number; + y: number; + role: SortieFormationRole; + label: string; + note: string; + unitId?: string; + icon: BattleUiIconKey; + tone: number; +}; + type BattleSceneData = { battleId?: string; selectedSortieUnitIds?: string[]; @@ -3026,6 +3037,9 @@ export class BattleScene extends Phaser.Scene { private sidePanelObjects: Phaser.GameObjects.GameObject[] = []; private commandButtons: CommandButton[] = []; private commandMenuObjects: Phaser.GameObjects.GameObject[] = []; + private deploymentObjects: Phaser.GameObjects.GameObject[] = []; + private deploymentSelectedUnitId?: string; + private deploymentNotice = '추천 배치는 이미 적용되어 있습니다. 필요하면 장수를 선택해 시작 구역 안에서 위치를 바꾸세요.'; private mapMenuObjects: Array = []; private mapMenuLayout?: MapMenuLayout; private battleSpeed: BattleSpeed = 'normal'; @@ -3189,9 +3203,13 @@ export class BattleScene extends Phaser.Scene { this.drawSidePanel(); this.drawDebugSpritePreview(); this.updateTurnText(); - this.time.delayedCall(180, () => this.showOpeningBattleEvent()); + if (this.shouldShowPreBattleDeployment()) { + this.showPreBattleDeployment(); + } else { + this.time.delayedCall(180, () => this.showOpeningBattleEvent()); + this.renderRosterPanel('ally', '행동할 장수를 선택하세요.'); + } this.scheduleDebugForcedBattleOutcome(); - this.renderRosterPanel('ally', '행동할 장수를 선택하세요.'); } private ensureScenarioAssets(onReady: () => void) { @@ -3558,6 +3576,11 @@ export class BattleScene extends Phaser.Scene { hitZone.setInteractive({ useHandCursor: true }); hitZone.on('pointerdown', (pointer: Phaser.Input.Pointer) => { if (pointer.leftButtonDown()) { + if (this.phase === 'deployment') { + this.suppressNextLeftClick = true; + this.handleDeploymentUnitClick(unit); + return; + } this.selectUnit(unit); } }); @@ -3949,6 +3972,7 @@ export class BattleScene extends Phaser.Scene { }); battleUnits.forEach((unit) => this.positionUnitView(unit)); + this.positionDeploymentOverlayObjects(); this.updateMiniMap(); this.refreshFacingIndicator(); this.updatePointerFeedback(this.input.activePointer, true); @@ -4075,6 +4099,11 @@ export class BattleScene extends Phaser.Scene { return; } + if (this.phase === 'deployment') { + this.updateDeploymentPointerFeedback(tile, force); + return; + } + if (this.phase === 'moving' && this.selectedUnit) { this.updateMovePointerFeedback(this.selectedUnit, tile, force); return; @@ -4308,6 +4337,11 @@ export class BattleScene extends Phaser.Scene { } private selectUnit(unit: UnitData) { + if (this.phase === 'deployment') { + this.handleDeploymentUnitClick(unit); + return; + } + this.hideMapMenu(); this.hideSaveSlotPanel(); this.hideTurnEndPrompt(); @@ -4608,6 +4642,484 @@ export class BattleScene extends Phaser.Scene { this.clearPointerFeedback(); } + private shouldShowPreBattleDeployment() { + return battleScenario.id === 'first-battle-zhuo-commandery' && battleUnits.some((unit) => unit.faction === 'ally' && unit.hp > 0); + } + + private showPreBattleDeployment() { + this.phase = 'deployment'; + this.activeFaction = 'ally'; + this.selectedUnit = undefined; + this.pendingMove = undefined; + this.targetingAction = undefined; + this.selectedUsable = undefined; + this.lockedTargetPreview = undefined; + this.deploymentSelectedUnitId = battleUnits.find((unit) => unit.id === 'guan-yu' && unit.hp > 0)?.id; + this.deploymentNotice = '추천 배치는 이미 적용되어 있습니다. 필요하면 장수를 선택해 시작 구역 안에서 위치를 바꾸세요.'; + this.clearMarkers(); + this.hideCommandMenu(); + this.hideMapMenu(); + this.hideTurnEndPrompt(); + this.hideBattleEventBanner(); + this.centerCameraOnTile(2, 16); + this.renderDeploymentMap(); + this.renderDeploymentPanel(); + this.refreshUnitLegibilityStyles(); + this.updatePointerFeedback(this.input.activePointer, true); + } + + private firstBattleDeploymentSlots(): DeploymentSlot[] { + const slots: DeploymentSlot[] = [ + { + x: 2, + y: 15, + role: 'front', + unitId: 'guan-yu', + label: '정면 돌파', + note: '길목 보병을 가장 먼저 받습니다.', + icon: 'spear', + tone: 0xd8b15f + }, + { + x: 1, + y: 16, + role: 'flank', + unitId: 'zhang-fei', + label: '측면 압박', + note: '아래쪽 길로 궁병 엄호를 흔듭니다.', + icon: 'attack', + tone: 0xc76542 + }, + { + x: 1, + y: 15, + role: 'support', + unitId: 'liu-bei', + label: '후방 지휘', + note: '유비 생존과 회복 여지를 지킵니다.', + icon: 'leadership', + tone: 0x5f95d8 + }, + { x: 2, y: 16, role: 'reserve', label: '예비 자리', note: '전열을 좁게 묶습니다.', icon: 'move', tone: 0x7d8a96 }, + { x: 1, y: 17, role: 'reserve', label: '예비 자리', note: '후방을 더 안전하게 둡니다.', icon: 'move', tone: 0x7d8a96 }, + { x: 2, y: 17, role: 'reserve', label: '예비 자리', note: '측면 출발을 늦춥니다.', icon: 'move', tone: 0x7d8a96 } + ]; + + return slots.filter((slot) => { + if (!this.isInBounds(slot.x, slot.y)) { + return false; + } + return getTerrainRule(battleMap.terrain[slot.y][slot.x]).passable !== false; + }); + } + + private isDeploymentTile(x: number, y: number) { + return this.firstBattleDeploymentSlots().some((slot) => slot.x === x && slot.y === y); + } + + private deploymentSlotAt(x: number, y: number) { + return this.firstBattleDeploymentSlots().find((slot) => slot.x === x && slot.y === y); + } + + private renderDeploymentMap() { + this.clearDeploymentObjects(); + const selected = this.deploymentSelectedUnit(); + + this.firstBattleDeploymentSlots().forEach((slot) => { + const occupant = this.unitAtTile(slot.x, slot.y); + const recommendedUnit = slot.unitId ? battleUnits.find((unit) => unit.id === slot.unitId) : undefined; + const selectedTile = selected?.x === slot.x && selected?.y === slot.y; + const recommended = Boolean(slot.unitId); + const fillAlpha = selectedTile ? 0.34 : recommended ? 0.2 : 0.12; + const strokeAlpha = selectedTile ? 1 : recommended ? 0.86 : 0.46; + const strokeWidth = selectedTile ? 3 : recommended ? 2 : 1; + + const marker = this.trackDeploymentObject( + this.add.rectangle(this.tileTopLeftX(slot.x), this.tileTopLeftY(slot.y), this.layout.tileSize, this.layout.tileSize, slot.tone, fillAlpha) + ); + marker.setName(`deployment-slot-${slot.x}-${slot.y}`); + marker.setData('tileX', slot.x); + marker.setData('tileY', slot.y); + marker.setOrigin(0); + marker.setDepth(4.75); + marker.setStrokeStyle(strokeWidth, slot.tone, strokeAlpha); + if (this.mapMask) { + marker.setMask(this.mapMask); + } + marker.setInteractive({ useHandCursor: true }); + marker.on('pointerover', () => { + marker.setFillStyle(slot.tone, Math.min(fillAlpha + 0.14, 0.44)); + this.deploymentNotice = occupant + ? `${occupant.name}: ${slot.label}. 다른 장수를 선택한 뒤 누르면 위치를 교환합니다.` + : `${slot.label}: ${slot.note}`; + this.renderDeploymentPanel(); + }); + marker.on('pointerout', () => { + marker.setFillStyle(slot.tone, fillAlpha); + }); + marker.on('pointerdown', (pointer: Phaser.Input.Pointer) => { + if (pointer.leftButtonDown()) { + this.suppressNextLeftClick = true; + this.handleDeploymentTileClick(slot.x, slot.y); + } + }); + + if (recommendedUnit) { + const label = this.trackDeploymentObject( + this.add.text(this.tileCenterX(slot.x), this.tileTopLeftY(slot.y) + 3, slot.label, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '10px', + color: '#fff1c8', + fontStyle: '700', + stroke: '#05070a', + strokeThickness: 3 + }) + ); + label.setName(`deployment-label-${slot.x}-${slot.y}`); + label.setData('tileX', slot.x); + label.setData('tileY', slot.y); + label.setData('tileOffsetX', this.layout.tileSize / 2); + label.setData('tileOffsetY', 3); + label.setOrigin(0.5, 0); + label.setDepth(11.2); + if (this.mapMask) { + label.setMask(this.mapMask); + } + } + }); + + this.positionDeploymentOverlayObjects(); + } + + private clearDeploymentObjects() { + this.deploymentObjects.forEach((object) => object.destroy()); + this.deploymentObjects = []; + } + + private trackDeploymentObject(object: T) { + this.deploymentObjects.push(object); + return object; + } + + private positionDeploymentOverlayObjects() { + this.deploymentObjects.forEach((object) => { + const tileX = object.getData('tileX') as number | undefined; + const tileY = object.getData('tileY') as number | undefined; + if (tileX === undefined || tileY === undefined) { + return; + } + + const offsetX = (object.getData('tileOffsetX') as number | undefined) ?? 0; + const offsetY = (object.getData('tileOffsetY') as number | undefined) ?? 0; + const tileObject = object as Phaser.GameObjects.GameObject & { + setPosition: (x: number, y: number) => Phaser.GameObjects.GameObject; + setVisible: (value: boolean) => Phaser.GameObjects.GameObject; + input?: { enabled: boolean }; + }; + const visible = this.isTileVisible(tileX, tileY); + tileObject.setPosition(this.tileTopLeftX(tileX) + offsetX, this.tileTopLeftY(tileY) + offsetY); + tileObject.setVisible(visible); + if (tileObject.input) { + tileObject.input.enabled = visible; + } + }); + } + + private renderDeploymentPanel() { + this.clearSidePanelContent(); + this.setMiniMapVisible(false); + + const { panelX, panelY, panelWidth, panelHeight } = this.layout; + const left = panelX + 24; + const width = panelWidth - 48; + const top = this.sideContentTop(); + const selected = this.deploymentSelectedUnit(); + + const header = this.trackSideObject(this.add.rectangle(left, top, width, 92, 0x101820, 0.96)); + header.setOrigin(0); + header.setStrokeStyle(1, palette.gold, 0.78); + this.trackSideObject(this.add.text(left + 16, top + 13, '전열 배치', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '24px', + color: '#f4dfad', + fontStyle: '700' + })); + this.trackSideObject(this.add.text(left + 16, top + 48, '황건 잔당의 보병 전열을 보고 시작 위치를 정합니다.', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '13px', + color: '#c8d2dd', + wordWrap: { width: width - 32, useAdvancedWrap: true } + })); + + const noticeTop = top + 104; + const notice = this.trackSideObject(this.add.rectangle(left, noticeTop, width, 54, 0x0b1118, 0.94)); + notice.setOrigin(0); + notice.setStrokeStyle(1, selected ? palette.blue : 0x647485, selected ? 0.74 : 0.56); + this.trackSideObject(this.add.text(left + 14, noticeTop + 10, selected ? `선택: ${selected.name}` : '장수를 선택하세요.', { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '14px', + color: selected ? '#f2e3bf' : '#d4dce6', + fontStyle: '700' + })); + this.trackSideObject(this.add.text(left + 14, noticeTop + 30, this.deploymentNotice, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#9fb0bf', + fixedWidth: width - 28 + })); + + const roleTop = noticeTop + 72; + this.deploymentRoleSummaries().forEach((summary, index) => { + const rowTop = roleTop + index * 72; + const active = selected?.id === summary.unitId; + const row = this.trackSideObject(this.add.rectangle(left, rowTop, width, 62, active ? 0x21364a : 0x101820, active ? 0.98 : 0.92)); + row.setOrigin(0); + row.setStrokeStyle(1, active ? summary.tone : 0x53606c, active ? 0.86 : 0.52); + row.setInteractive({ useHandCursor: true }); + row.on('pointerdown', () => { + const unit = battleUnits.find((candidate) => candidate.id === summary.unitId && candidate.hp > 0); + if (unit) { + this.handleDeploymentUnitClick(unit); + } + }); + const icon = this.trackSideIcon(left + 31, rowTop + 31, summary.icon, 42); + icon.setDepth(34); + this.trackSideObject(this.add.text(left + 58, rowTop + 9, `${summary.name} · ${summary.role}`, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '15px', + color: active ? '#f4dfad' : '#e7edf7', + fontStyle: '700' + })); + this.trackSideObject(this.add.text(left + 58, rowTop + 32, summary.text, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '12px', + color: '#aeb7c2', + fixedWidth: width - 70 + })); + }); + + this.renderDeploymentButton(left, panelY + panelHeight - 76, width, '전투 시작', palette.gold, () => this.confirmPreBattleDeployment()); + this.renderDeploymentButton(left, panelY + panelHeight - 116, width, '추천 배치 복원', 0x647485, () => this.restoreRecommendedDeployment()); + } + + private deploymentRoleSummaries() { + return [ + { + unitId: 'guan-yu', + name: this.unitName('guan-yu'), + role: '정면 돌파', + text: '길목의 황건 보병을 먼저 열어 전열을 만듭니다.', + icon: 'spear' as BattleUiIconKey, + tone: 0xd8b15f + }, + { + unitId: 'zhang-fei', + name: this.unitName('zhang-fei'), + role: '측면 압박', + text: '아래쪽에서 궁병 엄호와 잔당을 흔듭니다.', + icon: 'attack' as BattleUiIconKey, + tone: 0xc76542 + }, + { + unitId: 'liu-bei', + name: this.unitName('liu-bei'), + role: '후방 지휘', + text: '유비를 무리시키지 않고 생존과 회복 여지를 남깁니다.', + icon: 'leadership' as BattleUiIconKey, + tone: 0x5f95d8 + } + ]; + } + + private renderDeploymentButton( + x: number, + y: number, + width: number, + label: string, + tone: number, + action: () => void + ) { + const button = this.trackSideObject(this.add.rectangle(x, y, width, 34, 0x17232e, 0.96)); + button.setOrigin(0); + button.setStrokeStyle(1, tone, 0.86); + button.setInteractive({ useHandCursor: true }); + button.on('pointerover', () => button.setFillStyle(0x243746, 0.98)); + button.on('pointerout', () => button.setFillStyle(0x17232e, 0.96)); + button.on('pointerdown', () => { + this.suppressNextLeftClick = true; + action(); + }); + + const text = this.trackSideObject(this.add.text(x + width / 2, y + 17, label, { + fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', + fontSize: '15px', + color: '#f2e3bf', + fontStyle: '700' + })); + text.setOrigin(0.5); + text.setInteractive({ useHandCursor: true }); + text.on('pointerdown', () => { + this.suppressNextLeftClick = true; + action(); + }); + } + + private deploymentSelectedUnit() { + return this.deploymentSelectedUnitId + ? battleUnits.find((unit) => unit.id === this.deploymentSelectedUnitId && unit.faction === 'ally' && unit.hp > 0) + : undefined; + } + + private handleDeploymentUnitClick(unit: UnitData) { + if (this.phase !== 'deployment') { + return; + } + + if (unit.faction !== 'ally' || unit.hp <= 0) { + this.deploymentNotice = '전열 배치 중에는 아군 시작 위치만 조정할 수 있습니다.'; + this.renderDeploymentPanel(); + return; + } + + const selected = this.deploymentSelectedUnit(); + if (!selected || selected.id === unit.id) { + this.deploymentSelectedUnitId = unit.id; + this.deploymentNotice = `${unit.name}을 선택했습니다. 빈 시작 칸을 누르면 이동하고, 다른 장수를 누르면 위치를 바꿉니다.`; + soundDirector.playSelect(); + this.renderDeploymentMap(); + this.renderDeploymentPanel(); + return; + } + + this.swapDeploymentUnits(selected, unit); + } + + private handleDeploymentTileClick(x: number, y: number) { + if (this.phase !== 'deployment') { + return; + } + + const selected = this.deploymentSelectedUnit(); + const occupant = this.unitAtTile(x, y); + if (occupant?.faction === 'ally') { + this.handleDeploymentUnitClick(occupant); + return; + } + + if (!selected) { + this.deploymentNotice = '먼저 유비, 관우, 장비 중 한 명을 선택하세요.'; + this.renderDeploymentPanel(); + return; + } + + if (!this.isDeploymentTile(x, y)) { + this.deploymentNotice = '첫 전투에서는 표시된 시작 구역 안에서만 전열을 바꿀 수 있습니다.'; + this.renderDeploymentPanel(); + return; + } + + if (occupant) { + this.deploymentNotice = `${occupant.name}이 있는 칸입니다. 시작 구역 안의 빈 칸을 선택하세요.`; + this.renderDeploymentPanel(); + return; + } + + const fromX = selected.x; + const fromY = selected.y; + selected.x = x; + selected.y = y; + this.moveUnitView(selected, x, y, this.unitViews.get(selected.id), 150, fromX, fromY); + this.deploymentNotice = `${selected.name}을 ${this.deploymentSlotAt(x, y)?.label ?? '시작 위치'}에 배치했습니다.`; + this.updateMiniMap(); + this.renderDeploymentMap(); + this.renderDeploymentPanel(); + soundDirector.playSelect(); + } + + private swapDeploymentUnits(selected: UnitData, target: UnitData) { + const selectedFrom = { x: selected.x, y: selected.y }; + const targetFrom = { x: target.x, y: target.y }; + selected.x = targetFrom.x; + selected.y = targetFrom.y; + target.x = selectedFrom.x; + target.y = selectedFrom.y; + this.moveUnitView(selected, selected.x, selected.y, this.unitViews.get(selected.id), 150, selectedFrom.x, selectedFrom.y); + this.moveUnitView(target, target.x, target.y, this.unitViews.get(target.id), 150, targetFrom.x, targetFrom.y); + this.deploymentNotice = `${selected.name}과 ${target.name}의 위치를 바꿨습니다.`; + this.updateMiniMap(); + this.renderDeploymentMap(); + this.renderDeploymentPanel(); + soundDirector.playSelect(); + } + + private restoreRecommendedDeployment() { + const recommended = this.firstBattleDeploymentSlots().filter((slot) => slot.unitId); + recommended.forEach((slot) => { + const unit = battleUnits.find((candidate) => candidate.id === slot.unitId && candidate.hp > 0); + if (!unit) { + return; + } + const fromX = unit.x; + const fromY = unit.y; + unit.x = slot.x; + unit.y = slot.y; + this.moveUnitView(unit, slot.x, slot.y, this.unitViews.get(unit.id), 130, fromX, fromY); + }); + this.deploymentSelectedUnitId = 'guan-yu'; + this.deploymentNotice = '추천 배치로 되돌렸습니다. 관우는 정면, 장비는 측면, 유비는 후방에 섭니다.'; + this.updateMiniMap(); + this.renderDeploymentMap(); + this.renderDeploymentPanel(); + soundDirector.playSelect(); + } + + private confirmPreBattleDeployment() { + if (this.phase !== 'deployment') { + return; + } + + this.clearDeploymentObjects(); + this.clearPointerFeedback(); + this.deploymentSelectedUnitId = undefined; + this.selectedUnit = undefined; + this.pendingMove = undefined; + this.phase = 'idle'; + this.refreshUnitLegibilityStyles(); + this.renderRosterPanel('ally', '전열 배치가 끝났습니다. 관우로 길목을 열고, 장비로 측면을 압박하세요.'); + this.showOpeningBattleEvent(); + soundDirector.playSelect(); + } + + private updateDeploymentPointerFeedback(tile: { x: number; y: number }, force: boolean) { + const selected = this.deploymentSelectedUnit(); + const occupant = this.unitAtTile(tile.x, tile.y); + const slot = this.deploymentSlotAt(tile.x, tile.y); + const key = `deployment:${tile.x},${tile.y}:${selected?.id ?? 'none'}:${occupant?.id ?? 'empty'}:${slot?.label ?? 'out'}`; + if (!force && this.lastPointerFeedbackKey === key) { + return; + } + + if (occupant?.faction === 'ally') { + const text = selected && selected.id !== occupant.id + ? `${occupant.name}: 클릭하면 ${selected.name}과 위치 교환` + : `${occupant.name}: 선택 후 시작 구역 칸으로 이동`; + this.showPointerFeedback(tile, palette.blue, text, key, 0x1c7ed6, 0.12, 0.9); + return; + } + + if (slot && selected) { + this.showPointerFeedback(tile, slot.tone, `${slot.label}: ${selected.name} 배치 가능`, key, slot.tone, 0.16, 0.92); + return; + } + + if (slot) { + this.showPointerFeedback(tile, slot.tone, `${slot.label}: 먼저 장수를 선택하세요`, key, slot.tone, 0.12, 0.7); + return; + } + + this.showPointerFeedback(tile, 0x9b4d45, '표시된 시작 구역 안에서만 배치할 수 있습니다.', key, 0x9b4d45, 0.08, 0.66); + } + private showEnemyThreatRange() { this.clearMarkers(); const threatTiles = this.enemyThreatTiles(); @@ -9517,6 +10029,15 @@ export class BattleScene extends Phaser.Scene { return; } + if (this.phase === 'deployment') { + this.deploymentSelectedUnitId = undefined; + this.deploymentNotice = '선택을 해제했습니다. 배치할 장수를 다시 선택하세요.'; + this.renderDeploymentMap(); + this.renderDeploymentPanel(); + this.clearPointerFeedback(); + return; + } + if (this.cancelAttackTargeting()) { return; } @@ -9543,6 +10064,14 @@ export class BattleScene extends Phaser.Scene { return; } + if (this.phase === 'deployment') { + const tile = this.pointerToTile(pointer); + if (tile) { + this.handleDeploymentTileClick(tile.x, tile.y); + } + return; + } + const mapMenuAction = this.mapMenuActionAt(pointer.x, pointer.y); if (mapMenuAction) { this.runMapMenuAction(mapMenuAction); @@ -9623,7 +10152,7 @@ export class BattleScene extends Phaser.Scene { } private showMapMenu(pointerX: number, pointerY: number) { - if (this.phase === 'command' || this.phase === 'targeting' || this.phase === 'animating') { + if (this.phase === 'deployment' || this.phase === 'command' || this.phase === 'targeting' || this.phase === 'animating') { return; }