Okay, let's start building this track and field game. We'll begin with the basic structure, the start screen, mode selection, and the first event: the 100 Meters Dash.
We'll use HTML for the structure, CSS for basic styling, and JavaScript with the Canvas API for the game logic and rendering.
This sets up the core game loop, state management, input handling, rendering, UI updates, and the first event (100 Meters Dash) with basic player/CPU logic and scoring.
To Play:
1
or 2
on your keyboard.Enter
for Normal Mode or Shift
for Practice Mode.Enter
or Shift
to start the race.Left Arrow
and Right Arrow
keys to run.A
and D
keys to run.★
indicates a score over 999.Enter
or Shift
to proceed (or retry/skip in practice mode).Currently, only the 100m Dash is implemented. We can add the other events following a similar pattern. The scoring for the 100m dash uses the provided table with linear interpolation. The CPU aims for a time that yields around 900 points.
Okay, let's add the Long Jump event. We'll create the event logic, scoring, and integrate it into the main game flow. We'll use the Space
bar as the jump key ('B').
Changes Made:
longJump.js
:
Jumper
class to handle individual player/CPU logic (running, angle setting, jumping physics, landing, fouling).Space
bar (JUMP_KEY
) for initiating the jump.LongJump
class manages the overall event state and the jumpers. Currently, it handles only one attempt per event.handleKeyPress
and handleInput
to the event class to manage jump key presses and potential continuous input like angle adjustment.canStart
method.scoring.js
:
scoreTableLongJump
with plausible distances and points.calculateScoreLongJump
using the calculateScoreValueBased
helper function. It handles fouls (0 distance) correctly.game.js
:
LongJump
and calculateScoreLongJump
.this.events
array.update
loop slightly to pass input
to the active event even before it officially starts (EVENT_START
) to allow for things like angle setting.handleInput
to allow the active event to potentially handle a key press first via activeEvent.handleKeyPress
.startEvent
to pass the ui
object to the event constructor.render
to pass the ui
object to the event's render method.input.js
:
Space
is mapped correctly and prevented default browser action.handleKeyPress
logic slightly to better integrate with the game state and event-specific key handling.Now, after the 100m Dash, the Long Jump event will load. Follow the on-screen instructions (run, adjust angle with Up/Down arrows if P1, press Space before the line to jump).*
You are absolutely right! My apologies. I added the call to this.activeEvent.handleInput(this.input)
in game.js
to allow events like Long Jump to react to continuous input (like holding arrow keys for angle), but I forgot to add the corresponding (even if empty) method to the HundredMeters
class.
Let's fix that by adding the missing handleInput
method to hundredMeters.js
.
This adds the empty handleInput
method to the HundredMeters
class. Now, the call this.activeEvent.handleInput(this.input)
in game.js
should work correctly for both the 100m Dash and the Long Jump events without causing a TypeError. I also adjusted the 100m finish time calculation slightly for better precision.
Okay, you're right. Simultaneous jumping isn't realistic for Long Jump, and it should definitely have multiple attempts. Let's refactor the LongJump
event to handle 3 attempts per player, taking turns.
This requires significant changes to the LongJump
class logic to manage turns, attempts, and individual player states.
Key Changes:
longJump.js
:
JUMPER_STATE
and EVENT_STATE
enums. The event now manages whose turn it is (currentPlayerIndex
) and the overall flow (STARTING_TURN
, PLAYER_ACTIVE
, TURN_OVER
, EVENT_OVER
).prepareCurrentTurn()
: Resets the current jumper and displays instructions. Auto-starts CPU run after a delay.startPlayerRun()
: Called when human player presses Enter/Shift to begin their run.update()
: Only updates the currentPlayerIndex
jumper. When their attempt finishes (jumper.attemptFinished
), it records the result and moves to TURN_OVER
.handleKeyPress()
: Handles starting the run (Enter/Shift in STARTING_TURN
), proceeding to the next turn (Enter/Shift in TURN_OVER
), and passing the jump key (Space
) to the active jumper.recordAttemptResult()
: Stores the numeric result of the attempt and updates the player's best distance.advanceTurn()
: Increments the player index, handles wrapping around, increments attempt counts, checks if the event is over, and calls prepareCurrentTurn()
for the next valid turn.jumpersData
array now tracks currentAttempt
, bestDistance
, and individual attempts
results for each jumper.Jumper
Class: Added resetForAttempt()
method. State transitions are more granular.getResults()
: Now iterates through jumpersData
to return the bestDistance
achieved by each player over their 3 attempts.game.js
:
EVENT_START
state is now more of a loading state. The active event manages its internal state transitions (like starting turns). The EVENT_RUNNING
state signifies the event is actively processing turns/actions.handleInput
now prioritizes letting the activeEvent.handleKeyPress
manage input first. This is crucial for the turn-based logic. It also checks if an event's internal state change necessitates a game state change (though this isn't strictly needed with the current structure).startEvent
loads the event. handleEventFinish
is called when activeEvent.isFinished()
returns true (i.e., when the event's internal state becomes EVENT_OVER
). It then uses activeEvent.getResults()
which provides the best jump from all attempts.Now, the Long Jump event should correctly implement 3 attempts per player, taking turns. The CPU will also take its turns automatically.
Okay, let's add the Shot Put event. This will involve setting power and angle using timed key presses, similar to some classic track & field games. We'll stick with the 3-attempt, turn-based structure.
Changes:
shotPut.js
:
Shot
class for the projectile physics.Thrower
class:
READY
, POWER_METER_UP/DOWN
, ANGLE_METER_UP/DOWN
, THROWING
, etc.).THROW_KEY
(Space
) presses to progress through states.Shot
throw when angle is set.Shot
object during THROWING
state.ShotPut
class:
LongJump
.Thrower
objects.scoring.js
:
scoreTableShotPut
with estimated distances and points.calculateScoreShotPut
function.game.js
:
ShotPut
and its scoring function.events
array.Now, after the Long Jump, the Shot Put event will load. Use the Space
bar three times: once to start the power meter, again to lock power and start the angle meter, and a final time to lock the angle and throw.*_
You've hit a classic scope issue! The EVENT_STATE
constant is defined inside shotPut.js
(and likely longJump.js
too) but isn't exported, so game.js
cannot access it directly.
The Game
class shouldn't need to know the specific internal states of an event anyway. It should rely on the event's public methods like isFinished()
and its ability to handle input.
Let's fix game.js
by removing the direct checks for the event's internal EVENT_STATE
and simplifying the game state transition. We'll transition from EVENT_START
to EVENT_RUNNING
when the event successfully handles its first relevant key press.
This adds the Shot Put event, following the same turn-based, 3-attempt structure as the updated Long Jump. It includes the power/angle meter mechanic using the Space
bar and basic CPU simulation for that mechanic.
Okay, it seems like the CPU logic in shotPut.js
might have a flaw preventing it from taking its turn correctly. The structure is similar to Long Jump, but the interaction with the timed key presses for power/angle adds complexity.
Let's refine the CPU activation and simulation logic within shotPut.js
. We'll add some logging to help diagnose if this doesn't fix it.
Key Changes in shotPut.js
:
Thrower.resetForAttempt
: Explicitly sets the initial cpuKeyPressTimer
for the first delay and resets cpuActionStep
to 0.Thrower.handleHumanKeyPress
: Created a separate method for human input to avoid confusion with CPU simulation.Thrower.update
(CPU Logic):
cpuKeyPressTimer
) now only runs when the CPU is in a state where it's waiting to perform the next action (READY
, POWER_METER_UP
, ANGLE_METER_UP
).simulateCpuAction
.Thrower.simulateCpuAction
: (Renamed from simulateCpuKeyPress
)
thrower.state
based on the cpuActionStep
.cpuKeyPressTimer
for the next action step.ShotPut.handleKeyPress
: Now calls thrower.handleHumanKeyPress
only if the thrower is not a CPU. It correctly handles starting the human player's turn and advancing turns after completion.ShotPut.update
:
eventState === STARTING_TURN && thrower.isCPU
), it immediately sets the eventState
to PLAYER_ACTIVE
. This allows the Thrower.update
method to run.Thrower.update
method itself now manages the CPU's internal timer and state changes via simulateCpuAction
.console.log
statements at key points in the CPU turn logic (resetForAttempt
, simulateCpuAction
, state transitions in ShotPut.update
, advanceTurn
) to help trace execution if problems persist.This revised logic separates human input handling from CPU action simulation more clearly and ensures the CPU's internal timer controls its actions after the initial turn setup. Give this a try; the CPU should now perform its actions based on the calculated timers.*
Okay, let's implement the High Jump event. This event introduces the concept of a progressively higher bar and elimination based on failed attempts. We'll maintain the turn-based structure with 3 attempts per height.
Changes:
highJump.js
:
Jumper
class with states for running, jumping, clearing, landing.HighJump
class:
currentHeightM
) and converts to pixels for rendering/physics.advanceTurn
logic checks if bar should be raised).scoring.js
:
scoreTableHighJump
based on the provided data.calculateScoreHighJump
function. Modified calculateScoreValueBased
slightly to allow linear extrapolation above the highest table value, as high jump scores often exceed listed benchmarks.game.js
:
HighJump
and its scoring function.events
array.The High Jump event now functions with the core mechanics: players take turns attempting the current height, the bar raises after a round is complete, and players are eliminated after 3 consecutive failures. The scoring reflects the highest successfully cleared height.*