-
Notifications
You must be signed in to change notification settings - Fork 0
Implement basic card-based interception combat system #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,9 +12,12 @@ import EventModal from './components/EventModal'; | |
| import YearlyReviewModal from './components/YearlyReviewModal'; | ||
| import HangarModal from './components/HangarModal'; | ||
| import ConfirmationDialog from './components/ConfirmationDialog'; | ||
| import { GameState, Base, Personnel, ResearchProject, Transaction, UFO, ContinentSelection } from './types'; | ||
| import InterceptConfirmationModal from './components/InterceptConfirmationModal'; | ||
| import BattleScreen from './components/BattleScreen'; // Import the new BattleScreen component | ||
| import { GameState, Base, Personnel, ResearchProject, Transaction, UFO, ContinentSelection, Vehicle, BattleState, BattleCard } from './types'; | ||
| import { FACILITY_TYPES, upgradeFacility } from './data/facilities'; | ||
| import { BASE_SALARIES } from './data/personnel'; | ||
| import { getStarterDeck, shuffleDeck, drawCards as drawCardsUtil } from './data/cards'; // Card utilities | ||
| import { MONTHLY_EVENTS, GameEvent, evaluateYearlyPerformance, YearlyReview } from './data/events'; | ||
| import { hasCapacityForNewFacility, canAssignPersonnelToFacility } from './data/basePersonnel'; | ||
| import { doesTrajectoryIntersectRadar } from './utils/trajectory'; | ||
|
|
@@ -56,17 +59,21 @@ function App() { | |
| } | ||
| ] | ||
| }, | ||
| showRadarCoverage: false | ||
| showRadarCoverage: false, | ||
| activeBattle: null, | ||
| selectedVehicleForBattle: null, | ||
| selectedUFOForBattle: null, | ||
| }); | ||
|
|
||
| type ModalType = 'intro' | 'base' | 'personnel' | 'research' | 'financial' | 'event' | 'yearlyReview' | 'hangar' | null; | ||
| type ModalType = 'intro' | 'base' | 'personnel' | 'research' | 'financial' | 'event' | 'yearlyReview' | 'hangar' | 'interceptConfirmation' | null; | ||
| const [activeModal, setActiveModal] = useState<ModalType>('intro'); | ||
| const [selectedBase, setSelectedBase] = useState<Base | null>(null); | ||
| const [selectedContinent, setSelectedContinent] = useState<ContinentSelection | null>(null); | ||
| const [currentEvent, setCurrentEvent] = useState<{ event: GameEvent; message: string } | null>(null); | ||
| const [yearlyReview, setYearlyReview] = useState<YearlyReview | null>(null); | ||
| const [selectedHangarBase, setSelectedHangarBase] = useState<Base | null>(null); | ||
| const [researchNotification, setResearchNotification] = useState<string | null>(null); // State for research start notification | ||
| const [targetUFO, setTargetUFO] = useState<UFO | null>(null); // UFO targeted for interception | ||
|
|
||
| // State to track user actions during a turn | ||
| const [actionsPerformed, setActionsPerformed] = useState<boolean>(false); | ||
|
|
@@ -78,6 +85,160 @@ function App() { | |
| // Track transactions in the current turn | ||
| const [currentTurnTransactions, setCurrentTurnTransactions] = useState<number>(0); | ||
|
|
||
|
|
||
| const handleInitiateBattle = useCallback((vehicle: Vehicle, ufo: UFO) => { | ||
| console.log(`Initiating battle between ${vehicle.name} and ${ufo.name}`); | ||
|
|
||
| const initialDeck = shuffleDeck(getStarterDeck()); | ||
| const initialHandData = drawCardsUtil(initialDeck, [], [], 5); // Draw 5 cards initially | ||
|
|
||
| const initialBattleState: BattleState = { | ||
| id: crypto.randomUUID(), | ||
| stage: 'engagement', // Start directly in engagement for now | ||
| turn: 1, | ||
| initiative: { current: vehicle.id, order: [vehicle.id, ufo.id] }, // Placeholder | ||
| playerEnergy: vehicle.battleStats?.maxEnergy || 3, // Default to 3 energy | ||
| enemyEnergy: ufo.battleStats?.maxEnergy || 3, // Placeholder for UFO energy | ||
| playerHand: initialHandData.newHand, | ||
| playerDeck: initialHandData.newDeck, | ||
| playerDiscard: initialHandData.newDiscardPile, | ||
| activeEffects: [], | ||
| vehicleStatus: vehicle.battleStats || { // Use existing or provide default | ||
| maxHealth: 100, currentHealth: 100, energyPerTurn: 3, maxEnergy: 3, currentEnergy: 3, | ||
| accuracy: 75, evasion: 10, criticalChance: 5, cardSlots: 5, equipmentSlots: 2 | ||
| }, | ||
| ufoStatus: ufo.battleStats || { // Use existing or provide default | ||
| maxHealth: 50, currentHealth: 50, energyPerTurn: 3, maxEnergy: 3, currentEnergy: 3, | ||
| accuracy: 60, evasion: 5, criticalChance: 5, behaviorDeck: [], threatLevel: ufo.stealthRating // Placeholder | ||
| }, | ||
| stageObjectives: [], | ||
| environmentalConditions: [], | ||
| battleLog: [], | ||
| }; | ||
|
|
||
| setGameState(prev => ({ | ||
| ...prev, | ||
| activeBattle: initialBattleState, | ||
| selectedVehicleForBattle: vehicle, | ||
| selectedUFOForBattle: ufo, | ||
| })); | ||
| // Typically, you'd hide other modals or UI elements here | ||
| setActiveModal(null); | ||
| }, []); | ||
|
|
||
| const handlePlayCard = useCallback((cardId: string) => { | ||
| setGameState(prev => { | ||
| if (!prev.activeBattle) return prev; | ||
|
|
||
| const card = prev.activeBattle.playerHand.find(c => c.id === cardId); | ||
| if (!card) { | ||
| console.error("Card not found in hand:", cardId); | ||
| return prev; | ||
| } | ||
|
|
||
| if (prev.activeBattle.playerEnergy < card.cost) { | ||
| alert("Not enough energy to play this card!"); | ||
| // console.warn("Not enough energy for card:", card.name); | ||
| return prev; | ||
| } | ||
|
|
||
| console.log("Playing card:", card.name); | ||
| // TODO: Implement card effects | ||
| // For now, just remove from hand, add to discard, and deduct energy | ||
|
|
||
| const newHand = prev.activeBattle.playerHand.filter(c => c.id !== cardId); | ||
| const newDiscard = [...prev.activeBattle.playerDiscard, card]; | ||
| const newPlayerEnergy = prev.activeBattle.playerEnergy - card.cost; | ||
|
|
||
| // Example: Simple damage effect for first card in effects array | ||
| let newUFOStatus = { ...prev.activeBattle.ufoStatus }; | ||
| if (card.effects[0]?.type === 'damage' && typeof card.effects[0].value === 'string') { | ||
| // Basic damage range parsing (e.g., "25-35") | ||
| const parts = card.effects[0].value.split('-').map(Number); | ||
| const damage = parts.length === 2 ? Math.floor(Math.random() * (parts[1] - parts[0] + 1)) + parts[0] : parseInt(card.effects[0].value, 10) || 0; | ||
| newUFOStatus.currentHealth = Math.max(0, newUFOStatus.currentHealth - damage); | ||
| console.log(`${card.name} dealt ${damage} damage. UFO health: ${newUFOStatus.currentHealth}`); | ||
| } else if (card.effects[0]?.type === 'damage' && typeof card.effects[0].value === 'number') { | ||
| const damage = card.effects[0].value; | ||
| newUFOStatus.currentHealth = Math.max(0, newUFOStatus.currentHealth - damage); | ||
| console.log(`${card.name} dealt ${damage} damage. UFO health: ${newUFOStatus.currentHealth}`); | ||
| } | ||
|
|
||
|
|
||
| // Check for battle end | ||
| if (newUFOStatus.currentHealth <= 0) { | ||
| console.log("UFO Destroyed! Player wins!"); | ||
| alert("UFO Destroyed! Player wins!"); | ||
| // TODO: Handle victory rewards, cleanup, etc. | ||
| return { ...prev, activeBattle: null, selectedUFOForBattle: null, selectedVehicleForBattle: null }; | ||
| } | ||
|
|
||
| return { | ||
| ...prev, | ||
| activeBattle: { | ||
| ...prev.activeBattle, | ||
| playerHand: newHand, | ||
| playerDiscard: newDiscard, | ||
| playerEnergy: newPlayerEnergy, | ||
| ufoStatus: newUFOStatus, | ||
| // battleLog: [...prev.activeBattle.battleLog, { turn: prev.activeBattle.turn, stage: prev.activeBattle.stage, actorId: 'player', actionType: 'card', description: `Played ${card.name}`, timestamp: new Date() }] | ||
| } | ||
| }; | ||
| }); | ||
| }, []); | ||
|
|
||
| const handleEndPlayerTurn = useCallback(() => { | ||
| setGameState(prev => { | ||
| if (!prev.activeBattle) return prev; | ||
| console.log("Player ends turn."); | ||
| // TODO: Implement UFO turn logic here | ||
| // For now, UFO does nothing, player gets energy and draws a card. | ||
|
|
||
| let newPlayerDeck = [...prev.activeBattle.playerDeck]; | ||
| let newPlayerHand = [...prev.activeBattle.playerHand]; | ||
| let newPlayerDiscard = [...prev.activeBattle.playerDiscard]; | ||
|
|
||
| // Draw 1 card | ||
| const drawResult = drawCardsUtil(newPlayerDeck, newPlayerHand, newPlayerDiscard, 1); | ||
| newPlayerDeck = drawResult.newDeck; | ||
| newPlayerHand = drawResult.newHand; | ||
| newPlayerDiscard = drawResult.newDiscardPile; | ||
|
|
||
| // Replenish player energy (example: vehicle's energyPerTurn) | ||
| const playerMaxEnergy = prev.activeBattle.vehicleStatus.maxEnergy || 3; | ||
|
||
|
|
||
|
|
||
| // Placeholder UFO turn - simple attack | ||
| let newVehicleStatus = { ...prev.activeBattle.vehicleStatus }; | ||
| const ufoAttackDamage = 10; // Placeholder | ||
| newVehicleStatus.currentHealth = Math.max(0, newVehicleStatus.currentHealth - ufoAttackDamage); | ||
| console.log(`UFO attacks for ${ufoAttackDamage} damage! Player health: ${newVehicleStatus.currentHealth}`); | ||
|
|
||
| if (newVehicleStatus.currentHealth <= 0) { | ||
| console.log("Player Vehicle Destroyed! UFO wins!"); | ||
| alert("Player Vehicle Destroyed! UFO wins!"); | ||
| // TODO: Handle defeat, cleanup, etc. | ||
| return { ...prev, activeBattle: null, selectedUFOForBattle: null, selectedVehicleForBattle: null }; | ||
| } | ||
|
|
||
|
|
||
| return { | ||
| ...prev, | ||
| activeBattle: { | ||
| ...prev.activeBattle, | ||
| turn: prev.activeBattle.turn + 1, | ||
| playerEnergy: playerMaxEnergy, // Reset to max energy for simplicity | ||
| playerHand: newPlayerHand, | ||
| playerDeck: newPlayerDeck, | ||
| playerDiscard: newPlayerDiscard, | ||
| vehicleStatus: newVehicleStatus, | ||
| // battleLog: [...prev.activeBattle.battleLog, { turn: prev.activeBattle.turn, stage: prev.activeBattle.stage, actorId: 'player', actionType: 'status', description: `Ended turn.`, timestamp: new Date() }] | ||
| } | ||
| }; | ||
| }); | ||
| }, []); | ||
|
|
||
|
|
||
| // Reset action tracking when a new turn starts | ||
| const resetActionTracking = useCallback(() => { | ||
| setActionsPerformed(false); | ||
|
|
@@ -777,7 +938,11 @@ function App() { | |
| showRadarCoverage={gameState.showRadarCoverage} | ||
| activeUFOs={gameState.activeUFOs} | ||
| detectedUFOs={gameState.detectedUFOs} | ||
| onUFOClick={() => {}} // We'll implement this later | ||
| onUFOClick={(ufo) => { | ||
| if (gameState.activeBattle) return; // Don't allow new interception if battle is active | ||
| setTargetUFO(ufo); | ||
| setActiveModal('interceptConfirmation'); | ||
| }} | ||
| showAllUFOTrajectories={gameState.showAllUFOTrajectories} | ||
| completedResearch={gameState.completedResearch} | ||
| /> | ||
|
|
@@ -883,6 +1048,25 @@ function App() { | |
| cancelText="Go Back" | ||
| /> | ||
|
|
||
| {activeModal === 'interceptConfirmation' && targetUFO && ( | ||
| <InterceptConfirmationModal | ||
| isOpen={true} | ||
| onClose={() => { | ||
| setActiveModal(null); | ||
| setTargetUFO(null); | ||
| }} | ||
| onConfirm={(selectedVehicle) => { | ||
| if (targetUFO) { | ||
| handleInitiateBattle(selectedVehicle, targetUFO); | ||
| } | ||
| setActiveModal(null); | ||
| setTargetUFO(null); | ||
| }} | ||
| ufo={targetUFO} | ||
| availableVehicles={gameState.bases.flatMap(b => b.vehicles)} // Provide all vehicles from all bases | ||
| /> | ||
| )} | ||
|
|
||
| {/* Add debug panel */} | ||
| <DebugPanel | ||
| isVisible={gameState.debugPanelVisible ?? false} | ||
|
|
@@ -891,6 +1075,15 @@ function App() { | |
| onToggleUFOPaths={handleToggleUFODebug} | ||
| onToggleForceSpawn={handleToggleForceSpawn} | ||
| /> | ||
|
|
||
| {/* Render BattleScreen if a battle is active */} | ||
| {gameState.activeBattle && ( | ||
| <BattleScreen | ||
| gameState={gameState} | ||
| onPlayCard={handlePlayCard} | ||
| onEndTurn={handleEndPlayerTurn} | ||
| /> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This handler contains heavy battle-initialization logic. Consider extracting deck setup and initial
BattleStateconstruction into separate utility modules to keepApp.tsxlean and maintainable.