feat: add passenger boarding system with station-based pickup
- Add BOARD_PASSENGER action to GameAction types - Implement BOARD_PASSENGER reducer to mark passengers as boarded - Add findBoardablePassengers helper to identify passengers ready to board - Update useSteamJourney hook to automatically board passengers when train reaches their origin station - Modify findDeliverablePassengers to only check boarded passengers for delivery - Passengers now transition through complete lifecycle: waiting → boarding → aboard → delivery 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -351,6 +351,14 @@ function gameReducer(state: GameState, action: GameAction): GameState {
|
||||
passengers: action.passengers
|
||||
}
|
||||
|
||||
case 'BOARD_PASSENGER':
|
||||
return {
|
||||
...state,
|
||||
passengers: state.passengers.map(p =>
|
||||
p.id === action.passengerId ? { ...p, isBoarded: true } : p
|
||||
)
|
||||
}
|
||||
|
||||
case 'DELIVER_PASSENGER':
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useComplementRace } from '../context/ComplementRaceContext'
|
||||
import { generatePassengers, findDeliverablePassengers } from '../lib/passengerGenerator'
|
||||
import { generatePassengers, findBoardablePassengers, findDeliverablePassengers } from '../lib/passengerGenerator'
|
||||
|
||||
/**
|
||||
* Steam Sprint momentum system
|
||||
@@ -103,6 +103,21 @@ export function useSteamJourney() {
|
||||
elapsedTime: elapsed
|
||||
})
|
||||
|
||||
// Check for passengers that should board
|
||||
const boardable = findBoardablePassengers(
|
||||
state.passengers,
|
||||
state.stations,
|
||||
trainPosition
|
||||
)
|
||||
|
||||
// Board passengers at their origin station
|
||||
boardable.forEach(passenger => {
|
||||
dispatch({
|
||||
type: 'BOARD_PASSENGER',
|
||||
passengerId: passenger.id
|
||||
})
|
||||
})
|
||||
|
||||
// Check for deliverable passengers
|
||||
const deliverable = findDeliverablePassengers(
|
||||
state.passengers,
|
||||
|
||||
@@ -48,8 +48,11 @@ export interface Station {
|
||||
export interface Passenger {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
originStationId: string
|
||||
destinationStationId: string
|
||||
isUrgent: boolean
|
||||
isBoarded: boolean
|
||||
isDelivered: boolean
|
||||
}
|
||||
|
||||
@@ -142,6 +145,7 @@ export type GameAction =
|
||||
| { type: 'SHOW_RESULTS' }
|
||||
| { type: 'RESET_GAME' }
|
||||
| { type: 'GENERATE_PASSENGERS'; passengers: Passenger[] }
|
||||
| { type: 'BOARD_PASSENGER'; passengerId: string }
|
||||
| { type: 'DELIVER_PASSENGER'; passengerId: string; points: number }
|
||||
| { type: 'START_NEW_ROUTE'; routeNumber: number; stations: Station[] }
|
||||
| { type: 'COMPLETE_ROUTE' }
|
||||
|
||||
@@ -1,11 +1,44 @@
|
||||
import type { Passenger, Station } from './gameTypes'
|
||||
|
||||
// Passenger name pool (mix of diverse names)
|
||||
const PASSENGER_NAMES = [
|
||||
'Alice', 'Bob', 'Charlie', 'Diana', 'Ethan', 'Fiona', 'George', 'Hannah',
|
||||
'Ian', 'Julia', 'Kevin', 'Laura', 'Marcus', 'Nina', 'Oliver', 'Petra',
|
||||
'Quinn', 'Rosa', 'Sam', 'Tessa', 'Uma', 'Victor', 'Wendy', 'Xavier',
|
||||
'Yuki', 'Zara', 'Ahmed', 'Bella', 'Carlos', 'Devi', 'Elias', 'Fatima'
|
||||
// Names and avatars organized by gender presentation
|
||||
const MASCULINE_NAMES = [
|
||||
'Ahmed', 'Bob', 'Carlos', 'Elias', 'Ethan', 'George', 'Ian', 'Kevin',
|
||||
'Marcus', 'Oliver', 'Victor', 'Xavier', 'Raj', 'David', 'Miguel', 'Jin'
|
||||
]
|
||||
|
||||
const FEMININE_NAMES = [
|
||||
'Alice', 'Bella', 'Diana', 'Devi', 'Fatima', 'Fiona', 'Hannah', 'Julia',
|
||||
'Laura', 'Nina', 'Petra', 'Rosa', 'Tessa', 'Uma', 'Wendy', 'Zara', 'Yuki'
|
||||
]
|
||||
|
||||
const GENDER_NEUTRAL_NAMES = [
|
||||
'Alex', 'Charlie', 'Jordan', 'Morgan', 'Quinn', 'Riley', 'Sam', 'Taylor'
|
||||
]
|
||||
|
||||
// Masculine-presenting avatars
|
||||
const MASCULINE_AVATARS = [
|
||||
'👨', '👨🏻', '👨🏼', '👨🏽', '👨🏾', '👨🏿',
|
||||
'👴', '👴🏻', '👴🏼', '👴🏽', '👴🏾', '👴🏿',
|
||||
'👦', '👦🏻', '👦🏼', '👦🏽', '👦🏾', '👦🏿',
|
||||
'🧔', '🧔🏻', '🧔🏼', '🧔🏽', '🧔🏾', '🧔🏿',
|
||||
'👨🦱', '👨🏻🦱', '👨🏼🦱', '👨🏽🦱', '👨🏾🦱', '👨🏿🦱',
|
||||
'👨🦰', '👨🏻🦰', '👨🏼🦰', '👨🏽🦰', '👨🏾🦰', '👨🏿🦰',
|
||||
'👱', '👱🏻', '👱🏼', '👱🏽', '👱🏾', '👱🏿'
|
||||
]
|
||||
|
||||
// Feminine-presenting avatars
|
||||
const FEMININE_AVATARS = [
|
||||
'👩', '👩🏻', '👩🏼', '👩🏽', '👩🏾', '👩🏿',
|
||||
'👵', '👵🏻', '👵🏼', '👵🏽', '👵🏾', '👵🏿',
|
||||
'👧', '👧🏻', '👧🏼', '👧🏽', '👧🏾', '👧🏿',
|
||||
'👩🦱', '👩🏻🦱', '👩🏼🦱', '👩🏽🦱', '👩🏾🦱', '👩🏿🦱',
|
||||
'👩🦰', '👩🏻🦰', '👩🏼🦰', '👩🏽🦰', '👩🏾🦰', '👩🏿🦰',
|
||||
'👱♀️', '👱🏻♀️', '👱🏼♀️', '👱🏽♀️', '👱🏾♀️', '👱🏿♀️'
|
||||
]
|
||||
|
||||
// Gender-neutral avatars
|
||||
const NEUTRAL_AVATARS = [
|
||||
'🧑', '🧑🏻', '🧑🏼', '🧑🏽', '🧑🏾', '🧑🏿'
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -15,19 +48,62 @@ const PASSENGER_NAMES = [
|
||||
export function generatePassengers(stations: Station[]): Passenger[] {
|
||||
const count = Math.floor(Math.random() * 3) + 3 // 3-5 passengers
|
||||
const passengers: Passenger[] = []
|
||||
const usedNames = new Set<string>()
|
||||
const usedCombos = new Set<string>()
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Pick a unique name
|
||||
let name: string
|
||||
do {
|
||||
name = PASSENGER_NAMES[Math.floor(Math.random() * PASSENGER_NAMES.length)]
|
||||
} while (usedNames.has(name) && usedNames.size < PASSENGER_NAMES.length)
|
||||
usedNames.add(name)
|
||||
let avatar: string
|
||||
let comboKey: string
|
||||
|
||||
// Pick a random destination (exclude first station - Depot)
|
||||
const destinationStations = stations.slice(1) // Exclude starting depot
|
||||
const destination = destinationStations[Math.floor(Math.random() * destinationStations.length)]
|
||||
// Keep trying until we get a unique name/avatar combo
|
||||
do {
|
||||
// Randomly choose a gender category
|
||||
const genderRoll = Math.random()
|
||||
let namePool: string[]
|
||||
let avatarPool: string[]
|
||||
|
||||
if (genderRoll < 0.45) {
|
||||
// 45% masculine
|
||||
namePool = MASCULINE_NAMES
|
||||
avatarPool = MASCULINE_AVATARS
|
||||
} else if (genderRoll < 0.9) {
|
||||
// 45% feminine
|
||||
namePool = FEMININE_NAMES
|
||||
avatarPool = FEMININE_AVATARS
|
||||
} else {
|
||||
// 10% neutral
|
||||
namePool = GENDER_NEUTRAL_NAMES
|
||||
avatarPool = NEUTRAL_AVATARS
|
||||
}
|
||||
|
||||
// Pick from the chosen category
|
||||
name = namePool[Math.floor(Math.random() * namePool.length)]
|
||||
avatar = avatarPool[Math.floor(Math.random() * avatarPool.length)]
|
||||
comboKey = `${name}-${avatar}`
|
||||
} while (usedCombos.has(comboKey) && usedCombos.size < 100) // Prevent infinite loop
|
||||
|
||||
usedCombos.add(comboKey)
|
||||
|
||||
// Pick random origin and destination stations (must be different)
|
||||
// 40% chance to start at depot, 60% chance to start at other stations
|
||||
let originStation: Station
|
||||
let destination: Station
|
||||
|
||||
if (Math.random() < 0.4 || stations.length < 3) {
|
||||
// Start at depot
|
||||
originStation = stations[0]
|
||||
// Pick any other station as destination
|
||||
const otherStations = stations.slice(1)
|
||||
destination = otherStations[Math.floor(Math.random() * otherStations.length)]
|
||||
} else {
|
||||
// Start at a random non-depot station
|
||||
const nonDepotStations = stations.slice(1)
|
||||
originStation = nonDepotStations[Math.floor(Math.random() * nonDepotStations.length)]
|
||||
|
||||
// Pick a different station as destination (can include depot)
|
||||
const possibleDestinations = stations.filter(s => s.id !== originStation.id)
|
||||
destination = possibleDestinations[Math.floor(Math.random() * possibleDestinations.length)]
|
||||
}
|
||||
|
||||
// 30% chance of urgent
|
||||
const isUrgent = Math.random() < 0.3
|
||||
@@ -35,8 +111,11 @@ export function generatePassengers(stations: Station[]): Passenger[] {
|
||||
passengers.push({
|
||||
id: `passenger-${Date.now()}-${i}`,
|
||||
name,
|
||||
avatar,
|
||||
originStationId: originStation.id,
|
||||
destinationStationId: destination.id,
|
||||
isUrgent,
|
||||
isBoarded: false,
|
||||
isDelivered: false
|
||||
})
|
||||
}
|
||||
@@ -51,6 +130,31 @@ export function isTrainAtStation(trainPosition: number, stationPosition: number)
|
||||
return Math.abs(trainPosition - stationPosition) < 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Find passengers that should board at current position
|
||||
*/
|
||||
export function findBoardablePassengers(
|
||||
passengers: Passenger[],
|
||||
stations: Station[],
|
||||
trainPosition: number
|
||||
): Passenger[] {
|
||||
const boardable: Passenger[] = []
|
||||
|
||||
for (const passenger of passengers) {
|
||||
// Skip if already boarded or delivered
|
||||
if (passenger.isBoarded || passenger.isDelivered) continue
|
||||
|
||||
const station = stations.find(s => s.id === passenger.originStationId)
|
||||
if (!station) continue
|
||||
|
||||
if (isTrainAtStation(trainPosition, station.position)) {
|
||||
boardable.push(passenger)
|
||||
}
|
||||
}
|
||||
|
||||
return boardable
|
||||
}
|
||||
|
||||
/**
|
||||
* Find passengers that should be delivered at current position
|
||||
*/
|
||||
@@ -62,7 +166,8 @@ export function findDeliverablePassengers(
|
||||
const deliverable: Array<{ passenger: Passenger; station: Station; points: number }> = []
|
||||
|
||||
for (const passenger of passengers) {
|
||||
if (passenger.isDelivered) continue
|
||||
// Only check boarded passengers
|
||||
if (!passenger.isBoarded || passenger.isDelivered) continue
|
||||
|
||||
const station = stations.find(s => s.id === passenger.destinationStationId)
|
||||
if (!station) continue
|
||||
|
||||
Reference in New Issue
Block a user