mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 17:06:04 +00:00
289 lines
9.6 KiB
Text
289 lines
9.6 KiB
Text
|
|
// pong/ball.ne — multi-ball physics.
|
||
|
|
//
|
||
|
|
// Balls live in parallel arrays indexed by slot (ball_x[i], etc.)
|
||
|
|
// so swapping between single-ball M3 and multi-ball M4 is purely a
|
||
|
|
// matter of which slots are active — the physics code is the
|
||
|
|
// same. Velocity is stored as (magnitude, sign) per axis because
|
||
|
|
// NEScript's u8 arithmetic can't express negatives directly; sign
|
||
|
|
// 0 = positive (+x right / +y down), sign 1 = negative.
|
||
|
|
//
|
||
|
|
// Update order inside update_ball:
|
||
|
|
// 1. Move the ball by its velocity
|
||
|
|
// 2. Bounce off top / bottom walls
|
||
|
|
// 3. Test paddle collisions (left + right)
|
||
|
|
// 4. Test score-out on left / right exits
|
||
|
|
//
|
||
|
|
// The `i` parameter is the ball slot, 0..MAX_BALLS-1.
|
||
|
|
|
||
|
|
// ── Serve ────────────────────────────────────────────────
|
||
|
|
//
|
||
|
|
// Reset ball slot 0 to the centre of the playfield with a fresh
|
||
|
|
// random velocity, aimed toward the `target_side` that just lost
|
||
|
|
// a point (so the serve is into the loser's side, classic Pong).
|
||
|
|
// Every other ball slot is cleared. Called on game start and
|
||
|
|
// after every scored point.
|
||
|
|
fun serve_ball(target_side: u8) {
|
||
|
|
// Wipe every slot first so M4's multi-ball leftovers never
|
||
|
|
// carry through a score.
|
||
|
|
var i: u8 = 0
|
||
|
|
while i < MAX_BALLS {
|
||
|
|
ball_active[i] = 0
|
||
|
|
i += 1
|
||
|
|
}
|
||
|
|
|
||
|
|
// Centre the primary ball.
|
||
|
|
ball_x[0] = 124
|
||
|
|
ball_y[0] = 112
|
||
|
|
ball_dx[0] = BALL_BASE_DX
|
||
|
|
ball_dy[0] = BALL_BASE_DY
|
||
|
|
|
||
|
|
// Horizontal direction is toward target_side. SIDE_LEFT = 0
|
||
|
|
// means the ball travels left toward the left paddle, so x
|
||
|
|
// sign = 1 (negative). Right paddle = sign 0.
|
||
|
|
if target_side == SIDE_LEFT {
|
||
|
|
ball_dx_sign[0] = 1
|
||
|
|
} else {
|
||
|
|
ball_dx_sign[0] = 0
|
||
|
|
}
|
||
|
|
|
||
|
|
// Vertical direction picked by a random bit to avoid the
|
||
|
|
// same rally pattern every serve. RNG LSB = 0 → +y, 1 → -y.
|
||
|
|
var r: u8 = rng_next()
|
||
|
|
ball_dy_sign[0] = r & 1
|
||
|
|
|
||
|
|
ball_active[0] = 1
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Score handling ───────────────────────────────────────
|
||
|
|
//
|
||
|
|
// Called when a ball exits the playfield past a paddle. The
|
||
|
|
// scoring side gets +1. With multi-ball, the round continues
|
||
|
|
// until ALL active balls are out. Only when the last ball exits
|
||
|
|
// does the phase machine drop into P_POINT for the serve pause.
|
||
|
|
fun on_score(scoring_side: u8) {
|
||
|
|
score[scoring_side] += 1
|
||
|
|
play Score
|
||
|
|
|
||
|
|
// Count remaining active balls (the exiting ball has already
|
||
|
|
// been marked inactive before this call).
|
||
|
|
var remaining: u8 = 0
|
||
|
|
var j: u8 = 0
|
||
|
|
while j < MAX_BALLS {
|
||
|
|
if ball_active[j] == 1 {
|
||
|
|
remaining += 1
|
||
|
|
}
|
||
|
|
j += 1
|
||
|
|
}
|
||
|
|
if remaining == 0 {
|
||
|
|
// Last ball out — enter the P_POINT pause. The
|
||
|
|
// serving_side records the scorer; P_POINT flips it so
|
||
|
|
// the serve aims at the loser.
|
||
|
|
serving_side = scoring_side
|
||
|
|
phase = P_POINT
|
||
|
|
phase_timer = 0
|
||
|
|
// Clear the powerup so it doesn't linger during the
|
||
|
|
// inter-round pause.
|
||
|
|
powerup_kind = PWR_NONE
|
||
|
|
powerup_cooldown = 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Multi-ball spawn on paddle hit ───────────────────────
|
||
|
|
//
|
||
|
|
// When paddle_multi[side] is set, a paddle hit spawns two extra
|
||
|
|
// balls at the hit point with mirrored y velocities. This
|
||
|
|
// function is called from within check_paddle_hit's hit branch.
|
||
|
|
// Returns immediately if no multi flag is set. The flag clears
|
||
|
|
// on use.
|
||
|
|
fun spawn_multi_balls(i: u8, side: u8) {
|
||
|
|
if paddle_multi[side] == 0 {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
paddle_multi[side] = 0
|
||
|
|
|
||
|
|
// Find two free slots.
|
||
|
|
var slot1: u8 = 0
|
||
|
|
var slot2: u8 = 0
|
||
|
|
var found1: u8 = 0
|
||
|
|
var found2: u8 = 0
|
||
|
|
var k: u8 = 0
|
||
|
|
while k < MAX_BALLS {
|
||
|
|
if ball_active[k] == 0 {
|
||
|
|
if found1 == 0 {
|
||
|
|
slot1 = k
|
||
|
|
found1 = 1
|
||
|
|
} else {
|
||
|
|
if found2 == 0 {
|
||
|
|
slot2 = k
|
||
|
|
found2 = 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
k += 1
|
||
|
|
}
|
||
|
|
|
||
|
|
// Spawn each extra ball as a copy of the hit ball with a
|
||
|
|
// mirrored or offset y direction.
|
||
|
|
if found1 == 1 {
|
||
|
|
ball_active[slot1] = 1
|
||
|
|
ball_x[slot1] = ball_x[i]
|
||
|
|
ball_y[slot1] = ball_y[i]
|
||
|
|
ball_dx[slot1] = ball_dx[i]
|
||
|
|
ball_dy[slot1] = ball_dy[i]
|
||
|
|
ball_dx_sign[slot1] = ball_dx_sign[i]
|
||
|
|
// Mirror y sign from the source ball.
|
||
|
|
if ball_dy_sign[i] == 0 {
|
||
|
|
ball_dy_sign[slot1] = 1
|
||
|
|
} else {
|
||
|
|
ball_dy_sign[slot1] = 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if found2 == 1 {
|
||
|
|
ball_active[slot2] = 1
|
||
|
|
ball_x[slot2] = ball_x[i]
|
||
|
|
ball_y[slot2] = ball_y[i]
|
||
|
|
ball_dx[slot2] = ball_dx[i]
|
||
|
|
ball_dy[slot2] = ball_dy[i]
|
||
|
|
ball_dx_sign[slot2] = ball_dx_sign[i]
|
||
|
|
// Same y direction as the source (so we have 3 distinct
|
||
|
|
// trajectories: original, mirrored, and parallel).
|
||
|
|
ball_dy_sign[slot2] = ball_dy_sign[i]
|
||
|
|
// Offset the y slightly to avoid all three landing on
|
||
|
|
// the same pixel.
|
||
|
|
if ball_y[slot2] + 6 < PLAYFIELD_BOTTOM {
|
||
|
|
ball_y[slot2] += 6
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Paddle collision ─────────────────────────────────────
|
||
|
|
//
|
||
|
|
// Classic AABB overlap test against the paddle on `side`. On a
|
||
|
|
// hit we flip the ball's x velocity sign, push the ball out of
|
||
|
|
// the paddle to avoid double-bounces, and play the hit sfx.
|
||
|
|
//
|
||
|
|
// Powerup effects consumed on hit:
|
||
|
|
// - LONG: paddle_long already decrements
|
||
|
|
// - FAST: paddle_fast → doubles ball_dx for the rest of this ball's life
|
||
|
|
// - MULTI: paddle_multi → spawns 2 extra balls at the hit point
|
||
|
|
fun check_paddle_hit(i: u8, side: u8) {
|
||
|
|
var px: u8 = LEFT_PADDLE_X
|
||
|
|
if side == SIDE_RIGHT {
|
||
|
|
px = RIGHT_PADDLE_X
|
||
|
|
}
|
||
|
|
var py: u8 = paddle_y[side]
|
||
|
|
var ph: u8 = PADDLE_H
|
||
|
|
if paddle_long[side] > 0 {
|
||
|
|
ph = PADDLE_H_LONG
|
||
|
|
}
|
||
|
|
|
||
|
|
var bx: u8 = ball_x[i]
|
||
|
|
var by: u8 = ball_y[i]
|
||
|
|
|
||
|
|
// Four-way AABB overlap test. The operands are small u8
|
||
|
|
// values so the additions can't overflow past 255.
|
||
|
|
if bx + BALL_SIZE > px and bx < px + PADDLE_W and by + BALL_SIZE > py and by < py + ph {
|
||
|
|
if side == SIDE_LEFT {
|
||
|
|
ball_x[i] = px + PADDLE_W
|
||
|
|
ball_dx_sign[i] = 0
|
||
|
|
} else {
|
||
|
|
ball_x[i] = px - BALL_SIZE
|
||
|
|
ball_dx_sign[i] = 1
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Consume long-paddle hit ──────────────────
|
||
|
|
if paddle_long[side] > 0 {
|
||
|
|
paddle_long[side] -= 1
|
||
|
|
}
|
||
|
|
// ── Consume fast-ball flag ───────────────────
|
||
|
|
if paddle_fast[side] > 0 {
|
||
|
|
paddle_fast[side] = 0
|
||
|
|
ball_dx[i] = BALL_FAST_DX
|
||
|
|
}
|
||
|
|
// ── Consume multi-ball flag ──────────────────
|
||
|
|
spawn_multi_balls(i, side)
|
||
|
|
|
||
|
|
play PaddleHit
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Per-ball update ──────────────────────────────────────
|
||
|
|
fun update_ball(i: u8) {
|
||
|
|
if ball_active[i] == 0 {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 1. Move ──────────────────────────────────────
|
||
|
|
if ball_dx_sign[i] == 0 {
|
||
|
|
ball_x[i] += ball_dx[i]
|
||
|
|
} else {
|
||
|
|
ball_x[i] -= ball_dx[i]
|
||
|
|
}
|
||
|
|
if ball_dy_sign[i] == 0 {
|
||
|
|
ball_y[i] += ball_dy[i]
|
||
|
|
} else {
|
||
|
|
ball_y[i] -= ball_dy[i]
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 2. Wall bounce ───────────────────────────────
|
||
|
|
//
|
||
|
|
// Top: ball moving up and the top edge is now above the
|
||
|
|
// playfield (u8 comparison, safe because PLAYFIELD_TOP = 16
|
||
|
|
// and ball speeds are 1-2 so ball_y can't wrap around).
|
||
|
|
if ball_dy_sign[i] == 1 {
|
||
|
|
if ball_y[i] < PLAYFIELD_TOP {
|
||
|
|
ball_y[i] = PLAYFIELD_TOP
|
||
|
|
ball_dy_sign[i] = 0
|
||
|
|
play WallBounce
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Bottom: ball moving down and the bottom edge has crossed
|
||
|
|
// PLAYFIELD_BOTTOM.
|
||
|
|
if ball_dy_sign[i] == 0 {
|
||
|
|
if ball_y[i] + BALL_SIZE > PLAYFIELD_BOTTOM {
|
||
|
|
ball_y[i] = PLAYFIELD_BOTTOM - BALL_SIZE
|
||
|
|
ball_dy_sign[i] = 1
|
||
|
|
play WallBounce
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 3. Paddle collision ──────────────────────────
|
||
|
|
//
|
||
|
|
// Only check the paddle the ball is travelling toward.
|
||
|
|
// This is both faster and avoids the edge case of the
|
||
|
|
// ball hitting a paddle moving away from it.
|
||
|
|
if ball_dx_sign[i] == 1 {
|
||
|
|
check_paddle_hit(i, SIDE_LEFT)
|
||
|
|
} else {
|
||
|
|
check_paddle_hit(i, SIDE_RIGHT)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 4. Score-out ─────────────────────────────────
|
||
|
|
//
|
||
|
|
// "ball_x < 8" / "ball_x > 240" is safe because paddles
|
||
|
|
// live at x = 16 and x = 232 — a ball that has moved past
|
||
|
|
// the paddle edge with max speed 2 lands at ball_x ∈ [6, 14]
|
||
|
|
// or [234, 242] before the next frame, never close enough
|
||
|
|
// to 0 or 255 to wrap the u8.
|
||
|
|
if ball_dx_sign[i] == 1 {
|
||
|
|
if ball_x[i] < 8 {
|
||
|
|
ball_active[i] = 0
|
||
|
|
on_score(SIDE_RIGHT)
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
if ball_x[i] > 240 {
|
||
|
|
ball_active[i] = 0
|
||
|
|
on_score(SIDE_LEFT)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Sweep every active ball ──────────────────────────────
|
||
|
|
fun step_balls() {
|
||
|
|
var i: u8 = 0
|
||
|
|
while i < MAX_BALLS {
|
||
|
|
update_ball(i)
|
||
|
|
i += 1
|
||
|
|
}
|
||
|
|
}
|