Skip to content
Permalink
a7f533512b
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
2078 lines (1798 sloc) 52.1 KB
//
// dgolf.c
// NESert Golfing, by Brad Smith 2019
// http://rainwarrior.ca
//
const char rom_version[] = " ################ "
"NESert Golfing version 1.3 by Brad Smith, 2019"
" ################ ";
// for debugging performance
// (turns screen greyscale/green at end of frame when SELECT is held)
//#define PROFILE() { if (gamepad & PAD_SELECT) ppu_profile(0x41); }
//#define PROFILE() { ppu_profile(0x41); }
#define PROFILE() {}
// a few checks for safety, triggers infinite loop
#define ASSERT(__c__) {}
//#define ASSERT(__c__) { debug_assert(__c__); }
// for debugging graphics in left column
//#define SHOW_LEFT_COLUMN 1
#define SHOW_LEFT_COLUMN 0
// for testing the last hole
//#define LAST_HOLE_TEST 3
#define LAST_HOLE_TEST 0
// for debug hole skip with START button
//#define HOLE_SKIP 1
#define HOLE_SKIP 0
//
// common library stuff
//
typedef unsigned char uint8;
typedef signed char sint8;
typedef unsigned short int uint16;
typedef signed short int sint16;
typedef unsigned long int uint32;
typedef signed long int sint32;
extern uint8* ptr;
extern uint32 seed; // 24-bit random seed, don't set to 0
extern uint8 i, j, k, l;
extern uint16 mx,nx,ox,px;
extern sint16 ux,vx;
#pragma zpsym("ptr")
#pragma zpsym("seed")
#pragma zpsym("i")
#pragma zpsym("j")
#pragma zpsym("k")
#pragma zpsym("l")
#pragma zpsym("mx")
#pragma zpsym("nx")
#pragma zpsym("ox")
#pragma zpsym("px")
#pragma zpsym("ux")
#pragma zpsym("vx")
extern uint8 input[16];
extern uint8 input_flags;
extern uint8 gamepad;
extern uint8 mouse1;
extern sint8 mouse2;
extern sint8 mouse3;
#pragma zpsym("input")
#pragma zpsym("input_flags")
#pragma zpsym("gamepad")
#pragma zpsym("mouse1")
#pragma zpsym("mouse2")
#pragma zpsym("mouse3")
extern uint8 prng(); // 8-bit random value
extern uint8 prng1(); // "fast" random value, only bit 0 is truly random (bits 1-7 have increasing entropy)
extern void mouse_sense(); // cycles sensitivity setting (doesn't work on Hyperkin clone)
extern void input_setup();
extern void input_poll();
extern void sound_play(const uint8* addr); // play a sound effect
extern void ppu_latch(uint16 addr); // write address to PPU latch
extern void ppu_direction(uint8 vertical); // set write increment direction
extern void ppu_write(uint8 value); // write value to $2007
extern void ppu_load(uint16 count); // uploads bytes from ptr to $2007 (clobbers ptr)
extern void ppu_fill(uint8 value, uint16 count); // uploads single value to $2007
extern void ppu_ctrl(uint8 v); // $2000, only bits 4-6 count (tile pages, sprite height), applies at next post
extern void ppu_mask(uint8 v); // $2001, applies at next post
extern void ppu_scroll_x(uint16 x);
extern void ppu_scroll_y(uint16 y);
extern void ppu_post(uint8 mode); // waits for next frame and posts PPU update
extern void ppu_profile(uint8 emphasis); // immediate $2001 write, OR with current mask (use bit 0 for greyscale)
extern void ppu_apply_direction(uint8 vertical); // immediately set write increment direction
extern void ppu_apply(); // immediately uploads ppu_send to $2007, resets ppu_send_count to 0
// POST_OFF turn off rendering
// POST_NONE turn on, no other updates
// POST_UPDATE turn on, palette, send
// POST_DOUBLE turn on, palette, send 64 bytes across 2 nametables
#define POST_OFF 1
#define POST_NONE 2
#define POST_UPDATE 3
#define POST_DOUBLE 4
#define PAD_A 0x80
#define PAD_B 0x40
#define PAD_SELECT 0x20
#define PAD_START 0x10
#define PAD_UP 0x08
#define PAD_DOWN 0x04
#define PAD_LEFT 0x02
#define PAD_RIGHT 0x01
#define MOUSE_L 0x40
#define MOUSE_R 0x80
extern uint16 ppu_send_addr;
extern uint8 ppu_send_count;
extern uint8 ppu_send[64];
extern uint8 palette[32];
extern uint8 oam[256];
//
// simple color blending library
//
extern uint8 blend50(uint8 a, uint8 b); // best palette match of 50% a, 50% b
extern uint8 blend25(uint8 a, uint8 b); // best palette match of 75% a, 25% b
//
// desert golfing stuff in assembly
//
extern uint8 layers_chr[];
extern uint8 sprite_chr[];
extern const uint16 LAYERS_CHR_SIZE;
extern const uint16 SPRITE_CHR_SIZE;
extern uint8 floor_column;
extern uint8 weather_tile;
extern uint8 weather_attribute;
extern sint8 weather_wind_dir;
extern uint8 weather_wind_p;
extern uint8 weather_rate_min;
extern uint8 weather_rate_mask;
extern uint8 hole;
extern uint8 ocean_attribute;
extern uint16 tx;
extern uint16 ty;
extern sint16 tsx;
extern sint16 tsy;
extern uint32 ball_x;
extern uint32 ball_y;
extern sint32 ball_vx;
extern sint32 ball_vy;
extern volatile uint32 balls_x[4]; // 16:8 fixed point + 8 bits of padding for convenience
extern volatile uint8 balls_fx[16]; // byte access to balls_x+0 (0,4,8,12), fixed 8
extern volatile uint8 balls_lx[15]; // byte access to balls_x+1 (0,4,8,12), low 8
extern volatile uint8 balls_hx[14]; // byte access to balls_x+2 (0,4,8,12), high 8
extern volatile uint16 balls_wx[7]; // word access to balls_x+1 (0,2,4,6), low,high 16
extern uint8 balls_y[4];
extern sint16 norm_x;
extern sint16 norm_y;
#pragma zpsym("floor_column")
#pragma zpsym("weather_tile")
#pragma zpsym("weather_attribute")
#pragma zpsym("weather_wind_dir")
#pragma zpsym("weather_wind_p")
#pragma zpsym("weather_rate_min")
#pragma zpsym("weather_rate_mask")
#pragma zpsym("hole")
#pragma zpsym("ocean_attribute")
#pragma zpsym("tx")
#pragma zpsym("ty")
#pragma zpsym("tsx")
#pragma zpsym("tsy")
#pragma zpsym("ball_x")
#pragma zpsym("ball_y")
#pragma zpsym("ball_vx")
#pragma zpsym("ball_vy")
#pragma zpsym("balls_x")
#pragma zpsym("balls_fx")
#pragma zpsym("balls_lx")
#pragma zpsym("balls_hx")
#pragma zpsym("balls_wx")
#pragma zpsym("balls_y")
#pragma zpsym("norm_x")
#pragma zpsym("norm_y")
extern uint8 floor_render[64];
extern uint8 floor_y[512];
extern uint8 floor_a[512];
extern uint16 read_slope(uint8 index);
extern sint16 read_norm_x(uint8 index);
extern sint16 read_norm_y(uint8 index);
extern void floor_render_prepare(uint8 phase); // 0 = build, 1-5 = send, 6+ nothing (~6500, 300 x 4, 600, 0... cy)
extern void weather_animate(); // <~4300 cy
extern void weather_shift(uint8 shift); // ~800 cy
// fixed point multiply: (a * b) / 256
extern sint16 fmult(sint16 a, sint16 b); // ~545-594 cy
// used this to replace basis transforms like: ((a*b)+(c*d))/256
// (they are about 4x as fast, the original C code is left in comments for comparison)
//
// allocations and other common stuff
//
// fixed point for swing_x/y
#define SWING_FIX 16
// minimum radius for swing (to allow cancel)
#define SWING_MIN (SWING_FIX*3)
// maximum radius of swing
#define SWING_MAX (SWING_FIX*256)
// GLOBAL_FLOOR must be at least 2 pixels above OCEAN_FLOOR
// OCEAN_FLOOR must match definition in dgolf.s
#define GLOBAL_FLOOR 222
#define OCEAN_FLOOR 224
#define WATER_COLOUR 0x11
const uint8 BALL_COLOUR[5] = { 0x06, 0x02, 0x08, 0x0A, 0x00 };
// conventions:
// sx/sy = screen x,y (s = sprite)
// cx = circular x (mod 512)
#pragma bss-name (push, "ZEROPAGE") // was out of BSS space, but had plenty of ZEROPAGE
uint16 tee_cx;
uint16 hole_cx;
uint16 scroll_cx;
// render positions of things to draw
uint16 tee_sx;
uint16 hole_sx;
uint16 status_sx;
uint8 tee_sy;
uint8 hole_sy;
uint8 tee_s;
uint8 ball_s;
uint8 splash_s;
uint8 balls_draw[4]; // which player is in which draw-slot
uint8 hole_digits[3];
uint8 flag_remove;
uint8 players;
uint8 player;
uint8 swinging;
sint16 swing_x;
sint16 swing_y;
uint8 strokes[5*4];
uint8 cleared; // bit 0,1,2,3 = player in hole
uint8 status_w;
uint8 ring_glow;
uint8 roll_sx, roll_sy; // last frame's position
uint8 soll_sx, soll_sy; // two frames ago position
uint16 timeout; // frame timeout
uint8 rollout; // visual timeout (if ball is in same place for too long)
uint8 valley;
uint8 old_lip[2];
uint8 first_stroke;
uint8 next_player;
#pragma bss-name (pop)
// sprite building
uint8 oam_pos;
// floor generation
// control parameters
uint16 fg_min; // floor should not go below min
uint16 fg_max; // floor should not go below max
uint8 fg_hold_mask; // power of 2 - 1, controls maximum length of slopes
uint8 fg_angle_mask; // $80 | power of 2 - 1, lower values favour shallower slopes
uint16 fg_cx; // next floor pixel index to build
uint16 fg_last; // last floor value
uint16 fg_min_soft; // used to guide more toward centre
uint16 fg_max_soft;
uint16 fg_next; // next floor value
uint8 fg_angle; // last floor angle
uint8 fg_hold; // pixels to continue current angle
// animation of scenery etc.
uint8 frame_count;
uint8 transition;
uint8 transition_time;
uint8 day;
sint8 weather_wind_fade_dir;
uint8 weather_wind_fade_p;
uint16 weather_wind_timeout;
// hole generation
uint8 pal_floor;
uint8 pal_sky;
uint8 pal_text;
uint8 course;
uint8 field_set;
// title menu
uint8 title_menu;
uint8 gamepad_last;
uint8 gamepad_new;
uint8 mouse_last;
uint8 mouse_new;
uint8 mouse_steady;
sint8 mouse_sx;
sint8 mouse_sy;
//
// sound effects
//
// 0xFF = end
// 0xFE = all notes off
// 0xFD = next frame
// 0xXX 0xYY = write YY to register $4000+XX
// - start all sounds with $FE
// - end all sounds with $FE $FF
#define SFX_BEGIN 0xFE
#define SFX_END 0xFE, 0xFF
#define SFX_FRAME 0xFD
#define SFX(a_,b_) a_, b_
const uint8 SFX_STROKE[] = { SFX_BEGIN,
SFX(0xC,0x3F), SFX(0xE,0x0C), SFX_FRAME,
SFX(0xC,0x3D), SFX(0xE,0x00), SFX_FRAME,
SFX(0xC,0x32), SFX_FRAME,
SFX(0xC,0x31), SFX_FRAME, SFX_END };
const uint8 SFX_FLAG[] = { SFX_BEGIN,
SFX(0x0,0xB2), SFX(0x2,0x93), SFX(0x3,0x01), SFX_FRAME, SFX_FRAME,
SFX(0x0,0xB3), SFX(0x2,0x3F), SFX_FRAME, SFX_FRAME,
SFX(0x0,0xB4), SFX(0x2,0xEF), SFX(0x3,0x00), SFX_FRAME, SFX_FRAME,
SFX(0x0,0xB5), SFX(0x2,0xBD), SFX_FRAME, SFX_FRAME,
SFX(0x0,0xB4), SFX(0x2,0x9F), SFX_FRAME, SFX_FRAME,
SFX(0x0,0xB3), SFX(0x2,0x7E), SFX_FRAME, SFX_FRAME,
SFX(0x0,0xB2), SFX(0x2,0x64), SFX_FRAME, SFX_FRAME,
SFX(0x0,0xB1), SFX(0x2,0x54), SFX_FRAME, SFX_FRAME,
SFX(0x0,0xB1), SFX(0x2,0x3F), SFX_FRAME, SFX_FRAME, SFX_END };
const uint8 SFX_RESPAWN[] = { SFX_BEGIN,
SFX(0x0,0x71), SFX(0x2,0x59), SFX_FRAME, SFX_FRAME,
SFX(0x0,0x72), SFX(0x2,0x54), SFX_FRAME, SFX_FRAME,
SFX(0x0,0x73), SFX(0x2,0x4F), SFX_FRAME, SFX_FRAME,
SFX(0x0,0x74), SFX(0x2,0x4B), SFX_FRAME, SFX_FRAME,
SFX(0x0,0x73), SFX(0x2,0x59), SFX_FRAME, SFX_FRAME,
SFX(0x2,0x54), SFX_FRAME, SFX_FRAME,
SFX(0x2,0x4F), SFX_FRAME, SFX_FRAME,
SFX(0x2,0x4B), SFX_FRAME, SFX_FRAME,
SFX(0x0,0x71), SFX(0x2,0x59), SFX_FRAME, SFX_FRAME,
SFX(0x2,0x54), SFX_FRAME, SFX_FRAME,
SFX(0x2,0x4F), SFX_FRAME, SFX_FRAME,
SFX(0x2,0x4B), SFX_FRAME, SFX_FRAME, SFX_END };
const uint8 SFX_BOUNCE0[] = { SFX_BEGIN,
SFX(0xC,0x31), SFX(0xE,0x0D), SFX_FRAME,
SFX(0xC,0x32), SFX_FRAME,
SFX(0xC,0x31), SFX_FRAME,
SFX_FRAME, SFX_FRAME, SFX_END };
const uint8 SFX_BOUNCE1[] = { SFX_BEGIN,
SFX(0xC,0x31), SFX(0xE,0x0E), SFX_FRAME,
SFX(0xC,0x35), SFX_FRAME,
SFX(0xC,0x33), SFX_FRAME,
SFX(0xC,0x32), SFX_FRAME,
SFX(0xC,0x31), SFX_FRAME, SFX_END };
const uint8 SFX_BOUNCE2[] = { SFX_BEGIN,
SFX(0xC,0x32), SFX(0xE,0x0C), SFX_FRAME,
SFX(0xC,0x35), SFX_FRAME,
SFX(0xC,0x34), SFX_FRAME,
SFX(0xC,0x32), SFX_FRAME,
SFX(0xC,0x31), SFX_FRAME, SFX_END };
const uint8 SFX_SPLASH[] = { SFX_BEGIN,
SFX(0xC,0x36), SFX(0xE,0x0F), SFX_FRAME,
SFX(0xC,0x36), SFX(0xE,0x09), SFX_FRAME, SFX_FRAME, SFX_FRAME,
SFX(0xC,0x38), SFX(0xE,0x06), SFX_FRAME,
SFX(0xC,0x3C), SFX_FRAME,
SFX(0xC,0x39), SFX_FRAME,
SFX(0xC,0x38), SFX_FRAME,
SFX(0xC,0x37), SFX_FRAME,
SFX(0xC,0x36), SFX_FRAME,
SFX(0xC,0x35), SFX_FRAME,
SFX(0xC,0x34), SFX_FRAME,
SFX(0xC,0x33), SFX_FRAME,
SFX(0xC,0x32), SFX_FRAME, SFX_FRAME, SFX_FRAME, SFX_FRAME,
SFX(0xC,0x31), SFX_FRAME, SFX_FRAME, SFX_FRAME, SFX_FRAME,
SFX_FRAME, SFX_FRAME, SFX_END };
const uint8 SFX_TEE[] = { SFX_BEGIN,
SFX(0x0,0xB2), SFX(0x2,0x9D), SFX(0x3,0x05), SFX_FRAME,
SFX(0x2,0xF9), SFX(0x3,0x02), SFX_FRAME,
SFX(0x2,0x9D), SFX(0x3,0x05), SFX_FRAME,
SFX(0x2,0x4C), SFX(0x3,0x05), SFX_FRAME,
SFX(0x0,0xB3), SFX(0x2,0x80), SFX(0x3,0x02), SFX_FRAME,
SFX(0x2,0xB8), SFX(0x3,0x04), SFX_FRAME,
SFX(0x2,0x00), SFX(0x3,0x05), SFX_FRAME,
SFX(0x2,0x5C), SFX(0x3,0x02), SFX_FRAME,
SFX(0x2,0x74), SFX(0x3,0x04), SFX_FRAME,
SFX(0x2,0x34), SFX(0x3,0x04), SFX_FRAME,
SFX(0x2,0xFB), SFX(0x3,0x01), SFX_FRAME,
SFX(0x2,0xBF), SFX(0x3,0x03), SFX_FRAME,
SFX(0x2,0x89), SFX(0x3,0x03), SFX_FRAME,
SFX(0x2,0xAB), SFX(0x3,0x01), SFX_FRAME,
SFX(0x2,0x89), SFX(0x3,0x03), SFX_FRAME,
SFX(0x2,0xCE), SFX(0x3,0x02), SFX_FRAME,
SFX(0x0,0xB1), SFX(0x2,0x89), SFX(0x3,0x03), SFX_FRAME,
SFX(0x2,0xAB), SFX(0x3,0x01), SFX_FRAME,
SFX(0x2,0x89), SFX(0x3,0x03), SFX_FRAME,
SFX(0x2,0xCE), SFX(0x3,0x02), SFX_FRAME, SFX_END };
const uint8 SFX_PROMPT0[] = { SFX_BEGIN,
SFX(0x0, 0xB3), SFX(0x2,0xFB), SFX(0x3,0x01), SFX_FRAME, SFX_FRAME,
SFX(0x2,0xC4), SFX(0x3,0x01), SFX_FRAME, SFX_FRAME,
SFX(0x2,0x52), SFX(0x3,0x01), SFX_FRAME, SFX_FRAME,
SFX(0x0, 0xB1), SFX_FRAME, SFX_FRAME,
SFX_FRAME, SFX_FRAME, SFX_END };
const uint8 SFX_PROMPT1[] = { SFX_BEGIN,
SFX(0x0, 0x73), SFX(0x2,0x7C), SFX(0x3,0x01), SFX_FRAME, SFX_FRAME,
SFX(0x2,0xAB), SFX(0x3,0x01), SFX_FRAME, SFX_FRAME,
SFX(0x2,0x3A), SFX(0x3,0x02), SFX_FRAME, SFX_FRAME,
SFX(0x0, 0x71), SFX_FRAME, SFX_FRAME,
SFX_FRAME, SFX_FRAME, SFX_END };
const uint8 SFX_PROMPT2[] = { SFX_BEGIN,
SFX(0x0, 0xB3), SFX(0x2,0xAB), SFX(0x3,0x01), SFX_FRAME, SFX_FRAME,
SFX(0x2,0x52), SFX(0x3,0x01), SFX_FRAME, SFX_FRAME,
SFX(0x2,0x1C), SFX(0x3,0x01), SFX_FRAME, SFX_FRAME,
SFX(0x0, 0xB1), SFX_FRAME, SFX_FRAME,
SFX_FRAME, SFX_FRAME, SFX_END };
const uint8 SFX_PROMPT3[] = { SFX_BEGIN,
SFX(0x0, 0x33), SFX(0x2,0xFD), SFX(0x3,0x00), SFX_FRAME, SFX_FRAME,
SFX(0x2,0x2D), SFX(0x3,0x01), SFX_FRAME, SFX_FRAME,
SFX(0x2,0x7C), SFX(0x3,0x01), SFX_FRAME, SFX_FRAME,
SFX(0x0, 0x31), SFX_FRAME, SFX_FRAME,
SFX_FRAME, SFX_FRAME, SFX_END };
const uint8* const SFX_PROMPTS[4] = {
SFX_PROMPT0,
SFX_PROMPT1,
SFX_PROMPT2,
SFX_PROMPT3,
};
//
// misc
//
void debug_assert(int v)
{
if (!v)
{
ppu_profile(0x81); // red tinted grey
while(1); // infinite loop
}
}
uint16 mag_squared_s8(sint8 x, sint8 y)
{
if (x < 0) x = -x;
if (y < 0) y = -y;
return ((uint16)x * x) + ((uint16)y * y);
}
//
// graphical stuff
//
void palette_generate(uint8 sky, uint8 ground, uint8 text)
{
palette[ 0] = palette[16] =
palette[ 2] = palette[10] =
palette[ 5] = palette[13] = sky;
palette[26] =
palette[ 9] = palette[11] =
palette[14] = palette[15] = ground;
palette[ 1] = palette[ 3] =
palette[ 6] = palette[ 7] = text;
//palette[26] = 0x21; // for debugging tee
if (ocean_attribute != 255)
{
palette[3] = WATER_COLOUR;
}
}
void ball_draw_setup()
{
// colour for current player
i = BALL_COLOUR[player];
palette[17] = i | 0x10;
palette[18] = i | 0x20;
palette[19] = i | 0x30;
// 3 remaining players packed into one palette
balls_draw[0] = player;
for (i=1; i<4; ++i)
{
if (i >= players) balls_draw[i] = 4; // hidden
else balls_draw[i] = (player + i) % players;
}
for (i=1; i<4; ++i)
{
palette[20+i] = BALL_COLOUR[balls_draw[i]] | 0x10;
}
}
uint8 attribute(tl,tr,bl,br)
{
return tl|(tr<<2)|(bl<<4)|(br<<6);
}
void ppu_text(const char* text, uint16 addr)
{
ptr = (uint8*)text;
nx = addr;
ppu_latch(nx);
i = *ptr;
while (i)
{
if (i == '\n')
{
nx += 32;
ppu_latch(nx);
}
else ppu_write(i);
++ptr;
i = *ptr;
}
}
void cls() // erase nametables
{
ppu_latch(0x2000);
ppu_fill(0x00,0x1000);
}
void sprite_begin()
{
oam_pos = 4;
// sprite 0 always untouched
}
void sprite_end()
{
//while (oam_pos != 0)
while (oam_pos < (32*4)) // leaves last 32 for weather
{
oam[oam_pos] = 0xFF;
oam_pos += 4;
}
}
void sprite_add(uint8 tile, uint8 x, uint8 y, uint8 attrib)
{
oam[(uint8)(oam_pos+2)] = attrib;
oam[(uint8)(oam_pos+0)] = (uint8)(y-1);
oam[(uint8)(oam_pos+3)] = x;
oam[(uint8)(oam_pos+1)] = tile;
oam_pos += 4;
}
//
// floor generation
//
void floor_build_pixel_calculate_range()
{
fg_min_soft = (fg_min/4) + (fg_max/4) + (fg_min/2);
fg_max_soft = (fg_min/4) + (fg_max/4) + (fg_max/2);
}
void floor_build_pixel_angle()
{
fg_hold = (prng() & fg_hold_mask) + 8;
fg_angle = prng() & fg_angle_mask;
// drive toward centre
if (fg_last < fg_min_soft) fg_angle &= 0x7F;
else if (fg_last > fg_max_soft) fg_angle |= 0x80;
}
void floor_build_pixel_next()
{
fg_next = fg_last + read_slope(fg_angle);
}
const uint8 HOLE_ANGLE[8] = { 4, 4, 0, 0, 0x80+0, 0x80+0, 0x80+4, 0x80+4 };
// builds one pixel worth of floor
// note that this should always be 8 pixels ahead of floor_column,
// so that floor_render_prepare has 8 pixels worth of stuff to build,
// (and floor_column should be another 8 pixels ahead of the scroll)
void floor_build_pixel()
{
uint16 h;
if (ocean_attribute != 255) goto build;
h = fg_cx - hole_cx;
if (h < 8)
{
if (hole == 0)
{
ocean_attribute = ((floor_column + 1) / 4) & 15;
fg_last = fg_next = OCEAN_FLOOR * 256;
fg_angle = 0;
palette[3] = WATER_COLOUR;
goto build;
}
fg_hold = 0;
fg_angle = HOLE_ANGLE[(uint8)h];
floor_build_pixel_next();
goto build;
}
if (fg_hold) --fg_hold;
else floor_build_pixel_angle();
floor_build_pixel_next();
if (fg_next < fg_min)
{
floor_build_pixel_angle();
fg_angle &= 0x7F; // force positive
floor_build_pixel_next();
}
else if (fg_next > fg_max)
{
floor_build_pixel_angle();
fg_angle |= 0x80; // force negative
floor_build_pixel_next();
}
build:
floor_y[fg_cx] = ((fg_last/2) + (fg_next/2)) / 256;
floor_a[fg_cx] = fg_angle;
fg_last = fg_next;
fg_cx = (fg_cx + 1) & 511;
}
//
// hole generation
//
// snow rain brwn pink dbrn gren purp yell, night...
const uint8 set_f[16] = { 0x30, 0x1A, 0x17, 0x25, 0x07, 0x19, 0x13, 0x27, 0x30, 0x1A, 0x17, 0x15, 0x07, 0x19, 0x13, 0x37 };
const uint8 set_s[16] = { 0x21, 0x2B, 0x27, 0x35, 0x17, 0x21, 0x23, 0x37, 0x0F, 0x0B, 0x0F, 0x05, 0x0F, 0x09, 0x03, 0x01 };
const uint8 set_t[16] = { 0x0F, 0x30, 0x0F, 0x0F, 0x30, 0x0F, 0x0F, 0x0F, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 };
const uint8 set_w[16] = { 0x39, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
// 18 hole repeating course terrain structure
// angle mask:
// 0 artificial angular, mostly flat
// 1 favour moderate slopes
// 2 all slopes
#define AM0 0x9F
#define AM1 0xBF
#define AM2 0xFF
// hold mask:
// 0 short segments
// 1 medium segments
// 2 long segments
#define HM0 15
#define HM1 31
#define HM2 63
const uint8 course_hm[18] = { HM1,HM0,HM1,HM0,HM1,HM1,HM2,HM1,HM1, HM1,HM1,HM2,HM1,HM0,HM2,HM1,HM2,HM1 };
const uint8 course_am[18] = { AM0,AM0,AM1,AM1,AM1,AM2,AM1,AM1,AM2, AM1,AM2,AM1,AM2,AM2,AM2,AM1,AM2,AM1 };
void hole_next()
{
++hole;
for (i=0;i<3;++i)
{
++hole_digits[i];
if (hole_digits[i] < 10) break;
hole_digits[i] = 0;
}
#if LAST_HOLE_TEST
if (hole == LAST_HOLE_TEST) hole = 0;
#endif
fg_hold_mask = course_hm[course];
fg_angle_mask = course_hm[course];
++course; if (course >= 18) course = 0;
mx = hole_cx - scroll_cx;
if (hole_cx < scroll_cx) mx += 512;
i = (mx / 8) + 7; // min screen column of next hole
j = i + 13; // max screen column of next hole
if (!hole) j = i+1; // "last hole" should be close
if (i < 34) i = 34; // must be at least 2 tiles offscreen
k = j - i; // allowed range
if (k == 0 || k >= 16) k = 1; // prevent invalid ranges (shouldn't occur?)
l = i + (prng() % k); // new hole column
ASSERT((scroll_cx & 7) == 0);
if (!hole)
{
// final hole must fall on 32-pixel attribute boundary
while (((scroll_cx + (l*8)) & 24) != 0) ++l;
// force terrain to go down toward ocean
fg_min = (GLOBAL_FLOOR - 8) * 256;
floor_build_pixel_calculate_range();
}
tee_cx = hole_cx; // last hole becomes new tee
hole_cx = (scroll_cx + (l * 8)) & 511; // place new hole
hole_sx = l * 8; // screen position of new hole
if (!hole) hole_sx = 2048; // place it offscreen for the ocean
}
//
// common animation
//
#define TRANSITION_TIME 16
#define DAY_TIME 8
void transition_animate()
{
uint8 s, f;
--transition;
if (transition == ((TRANSITION_TIME*3)/4))
{
s = blend25(pal_sky, set_s[field_set]);
f = blend25(pal_floor, set_f[field_set]);
palette_generate(s,f,s);
}
else if (transition == ((TRANSITION_TIME*2)/4))
{
s = blend50(pal_sky, set_s[field_set]);
f = blend50(pal_floor, set_f[field_set]);
palette_generate(s,f,s);
}
else if (transition == ((TRANSITION_TIME*1)/4))
{
s = blend25(set_s[field_set], pal_sky);
f = blend25(set_f[field_set], pal_floor);
palette_generate(s,f,s);
}
else if (transition == 0)
{
pal_sky = set_s[field_set];
pal_floor = set_f[field_set];
palette_generate(pal_sky, pal_floor, pal_sky);
}
}
void weather_fade_automask()
{
// scale back the randomness at each power of 2
if ((weather_rate_min & (weather_rate_min-1)) == 0)
{
weather_rate_mask = weather_rate_min-1;
}
}
void weather_wind_random()
{
weather_wind_fade_dir = 1 - ((prng1() & 1) * 2); // 1 or -1, wind direction
weather_wind_fade_p = prng(); // wind strength
weather_wind_timeout = ((prng() & 15) + 32) * 64; // 30-50 seconds before wind changes
}
void weather_fade()
{
if (weather_wind_timeout == 0)
weather_wind_random();
else
--weather_wind_timeout;
// smoothly change wind
if (weather_wind_dir != weather_wind_fade_dir)
{
if (weather_wind_p == 0) weather_wind_dir = weather_wind_fade_dir;
else --weather_wind_p;
}
else
{
if (weather_wind_p < weather_wind_fade_p) ++weather_wind_p;
else if (weather_wind_p > weather_wind_fade_p) --weather_wind_p;
}
// smoothly change particles drop rate
if (frame_count & 15) return;
if (set_w[field_set])
{
if (weather_rate_min == 0)
{
weather_rate_min = 64;
weather_fade_automask();
}
else if (weather_rate_min > 4)
{
--weather_rate_min;
weather_fade_automask();
}
}
else
{
if (weather_rate_min != 0)
{
++weather_rate_min;
weather_fade_automask();
if (weather_rate_min >= 65) weather_rate_min = 0;
}
}
}
void weather_attribute_set()
{
// sets speed and palette via whether tile is rain or snow
// the unused bits of the OAM attribute byte are used to control particle fall speed
weather_attribute = (weather_tile == 0x38) ? ((4<<2)|3) : ((1<<2)|2);
}
void flag_animate()
{
uint8 t;
if (flag_remove == 0) return;
t = hole_sy - 32;
if (t > hole_sy)
{
hole_sy = 255;
flag_remove = 0;
return;
}
hole_sy = t;
}
void frame()
{
if (transition) transition_animate();
weather_fade();
weather_animate();
flag_animate();
PROFILE();
ppu_post(POST_UPDATE);
++frame_count;
}
void frame_double()
{
weather_fade();
weather_animate();
flag_animate();
PROFILE();
ppu_post(POST_DOUBLE);
++frame_count;
}
void frames(uint8 count)
{
while (count--) frame();
}
void delay(uint8 frames)
{
while (frames--) frame();
}
//
// main menu
//
const char help_text[] =
" NESert Golfing\n"
" Brad Smith, 2019\n"
" http://rainwarrior.ca\n"
"\n"
"GAMEPAD CONTROL:\n"
" Hold A to begin swing,\n"
" use directions to aim,\n"
" release to stroke.\n"
" Hold B for fine control.\n"
"\n"
"MOUSE CONTROL:\n"
" Hold left button to begin\n"
" swing, drag to aim,\n"
" release to stroke.\n"
" Hold right button for\n"
" fine control.\n"
"\n"
" NESert Golfing was derived\n"
" from the original game\n"
" Desert Golfing\n"
" by Justin Smith, 2014.";
const char title_text[] =
" Play\n"
"\n"
"How many? 1 2 3 4\n"
"\n"
" Help";
#define MOUSE_STEADY_TIME 24
#define MOUSE_STEADY_MOVE 12
void new_poll()
{
input_poll();
gamepad_new = gamepad & (gamepad ^ gamepad_last);
mouse_new = mouse1 & (mouse1 ^ mouse_last);
gamepad_last = gamepad;
mouse_last = mouse1;
// filter out new mouse motions until it's been stationary for a few frames
mouse_sx = 0;
mouse_sy = 0;
if (mouse_steady)
{
--mouse_steady;
}
else
{
if (mouse3 >= MOUSE_STEADY_MOVE || mouse3 <= -MOUSE_STEADY_MOVE)
{
mouse_sx = mouse3;
mouse_steady = MOUSE_STEADY_TIME;
}
if (mouse2 >= MOUSE_STEADY_MOVE || mouse2 <= -MOUSE_STEADY_MOVE )
{
mouse_sy = mouse2;
mouse_steady = MOUSE_STEADY_TIME;
}
}
}
void help()
{
#define HELP_SCROLL_RATE 16
sprite_begin();
sprite_end();
// scroll in
nx = 512;
for (i = 0; i < (256 / HELP_SCROLL_RATE); ++i)
{
nx -= HELP_SCROLL_RATE;
ppu_scroll_x(nx);
weather_shift(HELP_SCROLL_RATE);
frame();
new_poll();
}
while (1) // wait for button
{
ppu_scroll_x(256);
frame();
new_poll();
if (gamepad_new || mouse_new) break;
}
// scroll out
nx = 256;
for (i=0; i < (256 / HELP_SCROLL_RATE); ++i)
{
nx += HELP_SCROLL_RATE;
ppu_scroll_x(nx);
weather_shift(-HELP_SCROLL_RATE);
frame();
new_poll();
}
return;
}
void title()
{
cls();
palette[25] = 0x2D; // grey tee / flagpole
// 26 // floor (palette_generate)
palette[27] = 0x30; // white snow
palette[29] = 0x00; // unused
palette[30] = 0x00; // unused
palette[31] = WATER_COLOUR; // rain
gamepad_last = gamepad;
mouse_last = mouse1;
mouse_steady = 0;
mouse_sx = 0;
mouse_sy = 0;
players = 1;
frame_count = 0;
ocean_attribute = 255;
flag_remove = 0;
course = 0;
field_set = prng() & 15;
pal_floor = set_f[field_set];
pal_sky = set_s[field_set];
pal_text = set_t[field_set];
weather_tile = set_w[field_set];
weather_rate_mask = 3;
weather_rate_min = weather_tile ? 4 : 0;
weather_wind_random();
weather_wind_p = weather_wind_fade_p;
weather_wind_dir = weather_wind_fade_dir;
weather_attribute_set();
transition = 0;
transition_time = (prng() & 7) + 3;
day = prng() & 7;
// keep hole and start off the field for menu
hole_cx = 512;
tee_cx = 512;
// build initial floor
fg_cx = 256;
fg_max = GLOBAL_FLOOR * 256; // global maximum
fg_min = (192+3) * 256; // 3 pixels below help text
fg_angle_mask = 0x80 | 0x3F; // shallow hills
floor_build_pixel_calculate_range();
fg_hold_mask = 7; // short hills
fg_last = fg_min + (prng() % (fg_max - fg_min));
fg_hold = 0;
for (mx = 0; mx < 256; ++mx) floor_build_pixel();
fg_min = (176+3) * 256; // 3 pixels below menu
floor_build_pixel_calculate_range();
for (mx = 0; mx < 256; ++mx) floor_build_pixel();
fg_min = 48 * 256; // global minimum
floor_build_pixel_calculate_range();
// render initial floor
floor_column = 32;
for (mx = 0; mx < 64; ++mx)
{
for (j=0;j<6;++j)
{
floor_render_prepare(j);
if (j>0) ppu_apply();
}
}
ppu_apply_direction(0);
// title
i = 16;
for (j=0; j<7; ++j)
{
ppu_latch(0x2000 + 8 + ((4+j) * 32));
for (k=0; k<16; ++k) { ppu_write(i); ++i; }
}
ppu_latch(0x2000 + (8+13) + ((4+7) * 32));
ppu_write(13);
ppu_write(14);
ppu_write(15);
// menu
ppu_latch(0x23C0 + 0 + (12*2));
ppu_fill(attribute(1,1,1,1),16);
ppu_fill(attribute(2,2,2,2),24);
ppu_text(title_text, 0x2000 + 5 + (15 * 32));
// help
ppu_latch(0x27C0 + 0 + (0*2));
ppu_fill(attribute(1,1,1,1),48);
ppu_fill(attribute(3,3,3,3),16);
ppu_text(help_text, 0x2400 + 2 + (3 * 32));
palette_generate(pal_sky, pal_floor, pal_text);
title_menu = 0;
ppu_scroll_x(0);
ppu_scroll_y(0);
palette[18] = 0x24;
while (1)
{
#define MENU_Y0 (15*8)
#define MENU_Y1 (17*8)
#define MENU_Y2 (19*8)
#define MENU_X0 (12*8)
#define MENU_X1 (19*8)
#define MENU_XP1 ((16-3)*8)
palette[22] = BALL_COLOUR[players-1];
i = (frame_count / 8) & 3;
if (i == 0) i = 2;
palette[22] |= i << 4;
sprite_begin();
if (title_menu != 1)
{
uint8 i = title_menu==0 ? MENU_Y0 : MENU_Y2;
sprite_add(0x3A, MENU_X0,i,0x00);
sprite_add(0x3A, MENU_X1,i,0x40);
palette[22] = (palette[22] & 0x0F) | 0x10; // darken, not selected
}
sprite_add(0x3A, MENU_XP1+(players*24), MENU_Y1, 0x01);
sprite_add(0x3A, MENU_XP1+16+(players*24), MENU_Y1, 0x41);
sprite_end();
frame();
new_poll();
if ((mouse_new & MOUSE_L) || (gamepad_new & PAD_START)) // START or LMB starts game unless help is selected
{
if (title_menu != 2) break;
else help();
}
else if (mouse_new & MOUSE_R) // RMB cycles players
{
players = (players & 3) + 1;
}
else if ((title_menu != 1) && (gamepad_new & (PAD_A | PAD_B))) // A/B can also start game or help
{
if (title_menu != 2) break;
else help();
}
else if ((gamepad_new & (PAD_DOWN | PAD_SELECT)) || mouse_sy > 0) // SELECT or down on pad/mouse to switch selection
{
++title_menu;
if (title_menu > 2) title_menu = 0;
}
else if ((gamepad_new & PAD_UP) || mouse_sy < 0) // up on pad/mouse to switch selection (reverse)
{
--title_menu;
if (title_menu > 2) title_menu = 2;
}
else if (title_menu == 1) // players selection
{
if ((gamepad_new & (PAD_A | PAD_RIGHT)) || mouse_sx > 0) // A or right on pad/mouse to increase players
{
players = (players & 3) + 1;
}
else if ((gamepad_new & PAD_LEFT) || mouse_sx < 0) // left on pad/mouse to decrease players
{
players = ((players + 2) & 3) + 1;
}
}
prng(); // build up entropy
++frame_count;
// colour cycle the cursor
if (!(frame_count & 7))
{
i = (palette[18] & 0x0F) + 1;
if (i >= 0xD) i = 1;
palette[18] = 0x20 | i;
}
}
// prepare for game
// clear sprites
sprite_begin();
sprite_end();
// fade out text
i = pal_text;
palette[1] = palette[3] = palette[6] = palette[7] = blend25(i,pal_sky); frames(2);
palette[1] = palette[3] = palette[6] = palette[7] = blend50(i,pal_sky); frames(2);
palette[1] = palette[3] = palette[6] = palette[7] = blend25(pal_sky,i); frames(2);
palette[1] = palette[3] = palette[6] = palette[7] = pal_sky;
// wipe existing text
for (i=0; i<64; ++i) ppu_send[i] = 0;
for (i=3; i<20; ++i)
{
ppu_send_addr = 0x2000 + (32 * i);
frame_double();
}
for (i=20; i<24; ++i)
{
ppu_send_addr = 0x2400 + (32 * i);
ppu_send_count = 32;
frame();
}
// replace attributes
j = attribute(1,1,1,1); for (i=0; i< 8; ++i) ppu_send[i] = j;
j = attribute(2,2,2,2); for (i=8; i<64; ++i) ppu_send[i] = j;
ppu_send_addr = 0x23C0;
ppu_send_count = 64;
frame();
j = attribute(3,3,3,3); for (i=8; i<64; ++i) ppu_send[i] = j;
ppu_send_addr = 0x27C0;
ppu_send_count = 64;
frame();
// create first tee (flat ground)
hole_cx = 256;
tee_sx = 256;
tee_sy = fg_last / 256;
tee_s = 7;
for (i=0; i<8; ++i)
{
floor_y[fg_cx+i] = tee_sy;
floor_a[fg_cx+i] = 0;
}
fg_cx += 8;
scroll_cx = 0;
hole = 0;
hole_digits[0] = hole_digits[1] = hole_digits[2] = 0;
hole_next(); // hole_cx converts to tee_cx, new hole_cx is created
// render the tee, and keep the floor build 8 pixels ahead of it
for (i=0; i<8; ++i)
{
floor_build_pixel();
floor_render_prepare(i);
frame();
}
// place players on tee, reset other variables
status_sx = 256;
balls_x[0] =
balls_x[1] =
balls_x[2] =
balls_x[3] = 256 * 256;
balls_y[0] =
balls_y[1] =
balls_y[2] =
balls_y[3] = tee_sy - 6;
ball_s = 0;
splash_s = 0;
player = 0;
next_player = 0;
swinging = 0;
status_w = 0;
ring_glow = 0;
first_stroke = (players > 1) ? 0 : 1;
status_w = (16 - (((7 * players) - 2) / 2)); // position to start the strokes display
ball_draw_setup();
return; // hole_play()
}
//
// play loop
//
void hole_shift()
{
--tee_sx;
--hole_sx;
balls_wx[0] -= 1;
balls_wx[2] -= 1;
balls_wx[4] -= 1;
balls_wx[6] -= 1;
}
void hole_draw_flag()
{
if (hole_sy == 255) return;
j = (uint8)hole_sx;
i = j + 2;
if (i < j) return;
sprite_add(0x3C, i, hole_sy- 1, 0x02); // flagpole bottom
sprite_add(0x2C, i, hole_sy- 9, 0x02); // flagpole mid
sprite_add(0x1C, i, hole_sy-17, 0x02); // flagpole top
i += 8;
k = hole_sy-16;
l = hole_sy-24;
if (i < j) return;
if (hole >= 100)
{
sprite_add(0x10 | hole_digits[2], i, k, 0x02);
sprite_add(0x1B , i, l, 0x02);
i += 8;
if (i < j) return;
}
if (hole >= 10)
{
sprite_add(0x10 | hole_digits[1], i, k, 0x02);
sprite_add(0x1B , i, l, 0x02);
i += 8;
if (i < j) return;
}
sprite_add(0x10 | hole_digits[0], i, k, 0x02); // numeral
sprite_add(0x1B , i, l, 0x02); // top line
i += 8;
if (i < j) return;
sprite_add(0x1A, i, k, 0x02); // flag tip
}
const uint8 RING_CYCLE[4] = { 0x00, 0x80, 0xC0, 0x40 }; // rotating ring for sprite 3B
const uint8 RING_OFFX [4] = { 0, 0, 1, 1 };
const uint8 RING_OFFY [4] = { 0, 1, 1, 0 };
const uint8 RING_GLOW [4] = { 0x3D, 0x3E, 0x3F, 0x3F }; // glowing sprites for motion indicator
// clobbers i,j,k,l,mx,nx,ox,px
void hole_draw()
{
static uint8 order; // needed internal value because i,j,k,l are clobbered by hole_draw_flag
ox = balls_wx[player*2];
px = balls_y[player];
sprite_begin();
if (swinging)
{
sprite_add(0x2A, ox, px, 0x00); // highlighted ball
if (
swing_x >= SWING_MIN ||
swing_x <= -SWING_MIN ||
swing_y >= SWING_MIN ||
swing_y <= -SWING_MIN )
{
// note: the +1/2 on swing offsets is a rounding adjustment to keep negative/positive visually symmetrical
tsx = (swing_x + (SWING_FIX/2)) / SWING_FIX;
tsy = (swing_y + (SWING_FIX/2)) / SWING_FIX;
// rotating ring in direction of shot
order = ((swing_x + swing_y) / (SWING_FIX / 4)) & 3;
tx = ox - tsx - RING_OFFX[order];
ty = px - tsy - RING_OFFY[order];
if (tx < 256 && ty < 256) sprite_add(0x2B, (uint8)tx, (uint8)ty, RING_CYCLE[order]);
// solid dark ring direction of pull-back for swing
tx = ox + tsx;
ty = px + tsy;
if (tx < 256 && ty < 256) sprite_add(0x29, (uint8)tx, (uint8)ty, 0x00);
// separated from frame count so it's not synchronized with it
// (want to avoid some bad colour against the background always
// appearing in the same phase in this visualization of direction)
ring_glow += 13;
#define SWING_FRAMES (1<<4)
order = frame_count & (SWING_FRAMES-1);
mx = (tsx * (sint16)order) / (SWING_FRAMES-1);
nx = (tsy * (sint16)order) / (SWING_FRAMES-1);
tx = ox - mx;
ty = px - nx;
if (tx < 256 && ty < 256) sprite_add(RING_GLOW[(ring_glow/32)&3], (uint8)tx, (uint8)ty, 0x00);
tx += tsx;
ty += tsy;
if (tx < 256 && ty < 256) sprite_add(RING_GLOW[(ring_glow/32)&3], (uint8)tx, (uint8)ty, 0x00);
}
}
else if (ox < 256)
{
if (splash_s) // ball splashing into water
{
sprite_add(0x1F + splash_s, ox, px, 0x03);
if (splash_s < 4) // ball behind splash
sprite_add(0x1D + (ball_s/2), ox, px + splash_s - 1, 0x00);
}
else // ball by itself
sprite_add(0x1D + (ball_s/2), ox, px, 0x00);
}
if (tee_sx < 256) sprite_add(0x30 | tee_s, (uint8)tee_sx, tee_sy, 0x02);
if (status_sx < 256) sprite_add(0x3A, (uint8)status_sx, 16, 0x00);
// cycling order of remaining sprites
for (order=4; order!=0; --order)
{
i = (frame_count + order) & 3;
switch(i)
{
case 0:
if (hole_sx < 256) hole_draw_flag();
break;
case 1:
case 2:
case 3:
j = balls_draw[i];
k = j * 4;
if (j < players && !balls_hx[k])
sprite_add(0x2C + i, balls_lx[k], balls_y[j], 0x01);
break;
}
}
sprite_end();
}
void stroke_add()
{
i = player * 5;
for (j=0; j<5; ++j)
{
++strokes[i];
if(strokes[i] < 10) return;
strokes[i] = 0;
++i;
}
// maxed out
i -= 5;
strokes[i] =
strokes[i+1] =
strokes[i+2] =
strokes[i+3] =
strokes[i+4] = 9;
}
void status_draw()
{
uint8 ws;
uint8 w;
for (i=0; i<64; ++i) ppu_send[i] = 0; // blank the line
ASSERT((scroll_cx & 7) == 0);
w = (status_w + (scroll_cx / 8)) & 63; // adjust for scroll
ppu_send_addr = 0x2000 + (2 * 32);
// draw stroke counter for each player
k = 4;
for (i=0; i<players; ++i)
{
ws = w;
for (j=0; j<4; ++j) // left justify by skipping leading 0s
{
if (strokes[k] != 0) break;
--k;
}
for ( ; j<5; ++j)
{
ppu_send[ws] = 0x30 | strokes[k];
--k;
ws = (ws + 1) & 63;
}
k += 10;
w = (w + 7) & 63;
}
}
void status_player()
{
if (players > 1)
status_sx = ((status_w + (player * 7)) - 1) * 8; // player indicator
}
#define status_player_hide() { status_sx = 256; }
void hole_splash()
{
uint8 t;
sound_play(SFX_SPLASH);
for (t=0; t<18; ++t)
{
splash_s = (t/2)+1;
balls_y[player] = OCEAN_FLOOR-6;
hole_draw();
frame();
}
}
// collision priority per-pixel overlapping the bottom half of the ball
// favours closeness to the centre, and right side before left
// (lowest wins, 13=miss)
const uint8 COLLIDE_PRIORITY[5*4] = {
8, 12, 13, 13, // left column
3, 5, 11, 13,
0, 1, 6, 13, // centre column
2, 4, 9, 13,
7, 10, 13, 13, // right column
};
void status_bar_fade_in()
{
pal_text = set_t[field_set];
status_draw();
hole_draw(); frame_double();
palette_generate(pal_sky, pal_floor, blend25(pal_sky, pal_text)); hole_draw(); frames(2);
palette_generate(pal_sky, pal_floor, blend50(pal_sky, pal_text)); hole_draw(); frames(2);
palette_generate(pal_sky, pal_floor, blend25(pal_text, pal_sky)); hole_draw(); frame();
input_poll(); hole_draw(); frame(); // clear pending input
palette_generate(pal_sky, pal_floor, pal_text);
}
void hole_play()
{
uint16 t = (tee_cx - (6*8)) & 511; // target scroll position
while (scroll_cx != t)
{
// build one more pixel of floor
floor_build_pixel();
floor_render_prepare(scroll_cx & 7);
// scroll one pixel to the right
weather_shift(-1);
hole_shift();
scroll_cx = (scroll_cx + 1) & 511;
ppu_scroll_x(scroll_cx);
if (hole_sx < 256) hole_sy = floor_y[hole_cx + 4] - 8;
hole_draw();
frame();
}
// 3. raise the tee
if (hole != 1)
{
sound_play(SFX_TEE);
tee_sy = floor_y[tee_cx + 4] - 8;
tee_sx = (tee_cx - scroll_cx) & 511;
for (t = 0; t < 8; ++t)
{
tee_s = t;
i = tee_sy + 1 - t;
for (j=0; j<4; ++j)
if (balls_y[j] > i) balls_y[j] = i;
hole_draw();
frames(2);
}
for (i = 0; i < 8; ++i)
{
floor_a[tee_cx+i] = 0; // flat
floor_y[tee_cx+i] = tee_sy;
}
// restore the lip
floor_a[(tee_cx-1)&511] = old_lip[0];
floor_a[(tee_cx+8)&511] = old_lip[1];
}
// add a little "lip" for the hole to help balls fall in
if (hole != 0)
{
old_lip[0] = floor_a[(hole_cx-1)&511];
old_lip[1] = floor_a[(hole_cx+8)&511];
floor_a[(hole_cx-1)&511] = 0x01;
floor_a[(hole_cx+8)&511] = 0x81;
}
while (transition) { hole_draw(); frame(); } // finish any pending colour transition
// 4. fade in status bar
if (!first_stroke) // delay this on first stroke in 1-player mode
status_bar_fade_in();
// 5. play hole
// find leading player (after the one who played first last)
for (t=1; t<4; ++t)
{
j = (next_player + t) & 3;
if (j >= players) continue;
for (i = 0; i<5; ++i)
{
k = strokes[(j*5)+4-i];
l = strokes[(next_player)*5+4-i];
if (k > l) break;
if (k < l)
{
next_player = j;
break;
}
}
}
player = next_player;
next_player = (next_player + 1) % players; // favour the next one if tied to keep it cycling
cleared = 0;
for (i=0; i<4; ++i)
if (i >= players) cleared |= (1 << i);
ball_draw_setup();
// play sound to indicate current player
if (players > 1)
{
hole_draw(); frames(2);
sound_play(SFX_PROMPTS[player]);
}
while (cleared < 0x10)
{
// if centre of ball is offscreen, put it back on the tee
if (balls_x[player] >= (253*256) || balls_x[player] < (5*256))
{
if (players > 1) // extra time to keep prompt jingle from cutting rudely
{
hole_draw(); frames(2);
hole_draw(); frames(2);
hole_draw(); frames(2);
hole_draw(); frames(2);
}
sound_play(SFX_RESPAWN);
balls_x[player] = tee_sx * 256;
balls_y[player] = tee_sy - 6;
}
stroke_wait:
status_player(); // indicate whose turn it is, and that input is now accepted
do
{
input_poll();
prng1(); // entropy
hole_draw();
frame();
#if HOLE_SKIP
if (gamepad & PAD_START) goto hole_skip;
#endif
}
while (!(gamepad & (PAD_B | PAD_A)) && !(mouse1 & (MOUSE_L | MOUSE_R)));
//stroke_swing:
swinging = 1;
swing_x = 0;
swing_y = 0;
do
{
input_poll();
#define PSWING_COARSE 32
#define PSWING_FINE 1
#define MSWING_COARSE 8
#define MSWING_FINE 1
if (gamepad & PAD_B)
{
if (gamepad & PAD_LEFT ) swing_x -= PSWING_FINE;
if (gamepad & PAD_RIGHT) swing_x += PSWING_FINE;
if (gamepad & PAD_UP ) swing_y -= PSWING_FINE;
if (gamepad & PAD_DOWN ) swing_y += PSWING_FINE;
}
else
{
if (gamepad & PAD_LEFT ) swing_x -= PSWING_COARSE;
if (gamepad & PAD_RIGHT) swing_x += PSWING_COARSE;
if (gamepad & PAD_UP ) swing_y -= PSWING_COARSE;
if (gamepad & PAD_DOWN ) swing_y += PSWING_COARSE;
}
if (mouse1 & MOUSE_R)
{
// note: preventing overflow by testing that direction of motion has same sign
swing_x += mouse3 * MSWING_FINE;
swing_y += mouse2 * MSWING_FINE;
}
else
{
swing_x += mouse3 * MSWING_COARSE;
swing_y += mouse2 * MSWING_COARSE;
}
if (swing_x > SWING_MAX) swing_x = SWING_MAX;
if (swing_x < -SWING_MAX) swing_x = -SWING_MAX;
if (swing_y > SWING_MAX) swing_y = SWING_MAX;
if (swing_y < -SWING_MAX) swing_y = -SWING_MAX;
prng1(); // entropy
hole_draw();
frame();
}
while ((gamepad & (PAD_B | PAD_A)) || (mouse1 & (MOUSE_L | MOUSE_R)));
swinging = 0;
if (
swing_x < SWING_MIN &&
swing_x > -SWING_MIN &&
swing_y < SWING_MIN &&
swing_y > -SWING_MIN )
goto stroke_wait;
stroke_add();
if (!first_stroke) status_draw();
status_player_hide(); // no longer taking input
hole_draw();
if (!first_stroke) frame_double(); // frame_double is for status_draw only
else frame();
//ball_fly:
sound_play(SFX_STROKE);
ball_x = balls_x[player];
ball_y = balls_y[player] * 256;
ball_vx = -swing_x / 2; // divider of swing controls max velocity
ball_vy = -swing_y / 2;
timeout = 0;
rollout = 0;
// some motion constants, higher gravity makes a "faster" game
// DRAG should be probably be greater than WIND so that the ball will stop?
// STICK is how fast a bounce is required to escape the floor
#define GRAVITY 12
#define WIND 3
#define DRAG 6
#define STICK 128
// radius should be at least 2 pixels:
// 2 pixels -> lifts ball completely off ground
// sqrt(4.5) pixels -> touches inner corners of all ball pixels
// 2.5 pixels -> touches outer 4 faces
// sqrt(8.5) pixels -> touches outer corners of all pixels
// chose the first, because smaller = easier to get in hole
#define BALL_RADIUS (2*256)
// limit amount of ejection per-frame to avoid "pop"
// (hard hits will tend to self-correct with the bounce anyway)
#define EJECT_MAX (BALL_RADIUS*2)
// to keep bad physics from lasting forever, just drop the ball straight down after this many frames
#define TIMEOUT (20*60)
// additional timeout: drop the ball after not visually moving for this many frames
#define ROLLOUT 16
do
{
soll_sx = roll_sx;
soll_sy = roll_sy;
roll_sx = balls_lx[player*4];
roll_sy = balls_y[player];
ball_x += ball_vx;
ball_y += ball_vy;
ball_vy += GRAVITY;
balls_x[player] = ball_x;
balls_y[player] = ball_y / 256;
if (ball_y >= (256*256))
{
balls_hx[player*4] = 1; // offscreen
goto collide_skip;
}
// final hole water test
if (hole == 0 && ball_y >= ((OCEAN_FLOOR-6)*256))
{
hole_splash(); // pause for a little splash animation
cleared |= (1 << player);
splash_s = 0; // clear splash
ball_x = 256*256;
balls_hx[player*4] = 1; // move ball offscreen
break;
}
// check 5 columns of the ball for any pixel overlap with the field
mx = (ball_x / 256) + 1; // start at left column of ball
l = (ball_y / 256) + 5; // bottom row of ball
px = 0; // best priority column
k = 13; // best priority
valley = 0;
for (i=0; i<(5*4); i+=4)
{
nx = mx;
if (mx >= 0x8000) nx = 0; // off left side, use leftmost column
else if (mx >= 256) nx = 255; // off right side, use rightmost column
nx = (nx + scroll_cx) & 511;
++mx;
j = floor_y[nx];
if (l >= j) // possible overlap
{
j = l-j;
if (j >= 2) { j = COLLIDE_PRIORITY[i]; } // centre row or above
else { j = COLLIDE_PRIORITY[i+(2-j)]; } // bottom two rows
if (j < 13) // detect simultaneous collision on both sides
{
if (i < (2*4)) valley |= 1;
else if (i >= (3*4)) valley |= 2;
}
// replace j with priority
if (j < k) // store best priority column
{
k = j;
px = nx;
}
}
}
if (k >= 13) goto collide_skip; // no pixel collision detected
// if ball lands offscreen, don't try to collide
// (this speeds up recovery for the next shot, but also avoids
// a problem calculating tsx/tsy below that causes it to slip past
// the radius of ejection and ending up in free fall)
if (ball_x >= (256*256)) break;
// px = index of floor
i = floor_a[px]; // i = angle lookup for floor slope
norm_x = read_norm_x(i);
norm_y = read_norm_y(i);
mx = (((px - scroll_cx) & 255) * 256) + 128; // midpoint of floor column
nx = floor_y[px] * 256;
// 1. eject ball from overlap with the floor plane
// tsx/tsy = vector pointing from midpoint of floor tile to ball centre (sprite corner + 3.5 pixels)
//tsx = (uint16)ball_x + 896 - mx; // 3.5 pixels seems to have horizontal bias on slopes
tsx = (uint16)ball_x + 768 - mx; // 3.0 pixels instead? maybe corrects the visual bias of rounding down? unsure
tsy = (uint16)ball_y + 896 - nx;
// ux = distance to eject from floor plane
//ux = BALL_RADIUS-(((norm_x * (sint32)tsx) + (norm_y * (sint32)tsy)) / 256);
ux = BALL_RADIUS-(fmult(norm_x,tsx) + fmult(norm_y,tsy));
if (ux > 0)
{
if (ux > EJECT_MAX) ux = EJECT_MAX;
tsx = (norm_x * (sint32)ux) / 256;
tsy = (norm_y * (sint32)ux) / 256;
ball_x += tsx;
ball_y += tsy;
}
balls_x[player] = ball_x;
balls_y[player] = ball_y / 256;
// 2. reflect ball velocity if against the floor plane, roll along plane if hit is too weak
//tsy = ((norm_x * (sint32)ball_vx) + (norm_y * (sint32)ball_vy)) / 256; // velocity against the normal
//tsx = ((norm_y * (sint32)ball_vx) - (norm_x * (sint32)ball_vy)) / 256; // velocity perpendicular to the normal
tsy = fmult(norm_x,ball_vx) + fmult(norm_y,ball_vy); // velocity against the normal
tsx = fmult(norm_y,ball_vx) - fmult(norm_x,ball_vy); // velocity perpendicular to the normal
// stick to floor if bounce isn't strong enough
if (tsy > -STICK && tsy < STICK )
{
if (tsy < 0 ) tsx -= tsx / 8; // milder attenuation of horizontal
if (tsx > DRAG) tsx -= DRAG;
else if (tsx < -DRAG) tsx += DRAG;
else tsx = 0;
tsy = 0;
}
else if (tsy < 0) // bounce if we're not already heading out
{
if (tsy < -400) sound_play(SFX_BOUNCE2);
else if (tsy < -200) sound_play(SFX_BOUNCE1);
else sound_play(SFX_BOUNCE0);
tsy /= -2;
tsx /= 2;
}
if (tsx == 0 && tsy == 0) break; // ball has stopped
//ball_vx = ((norm_x * (sint32)tsy) + (norm_y * (sint32)tsx)) / 256;
//ball_vy = ((norm_y * (sint32)tsy) - (norm_x * (sint32)tsx)) / 256;
ball_vx = fmult(norm_x,tsy) + fmult(norm_y,tsx);
ball_vy = fmult(norm_y,tsy) - fmult(norm_x,tsx);
// stop ball from gaining gravity indefinitely in a valley
if (valley >= 3)
{
if (ball_vy > 0) ball_vy = 0;
}
goto collide_done;
collide_skip:
// apply wind only if not colliding or on ground
if (weather_rate_min && (prng1() < weather_wind_p))
ball_vx += weather_wind_dir * WIND;
collide_done:
// rolling/wobble, tick it whenever the ball moves a pixel
if (balls_lx[player*4] > roll_sx) // -x = rolling backward
{
ball_s -= 1; if (ball_s >= 6) ball_s = 5;
if (balls_lx[player*4] != soll_sx || balls_y[player] != soll_sy) // secondary check for 2-frame cycle before clearing rollout
rollout = 0;
else
++rollout;
}
else if (balls_lx[player*4] != roll_sx || balls_y[player] != roll_sy) // moved and not -x = rolling forward
{
ball_s += 1; if (ball_s >= 6) ball_s = 0;
if (balls_lx[player*4] != soll_sx || balls_y[player] != soll_sy)
rollout = 0;
else
++rollout;
}
else
{
++rollout; // ball is visually still?
}
++timeout;
if (timeout >= TIMEOUT || rollout >= ROLLOUT)
{
if ((balls_hx[player*4] != 0) || (ball_y >= (256*256)))
{
balls_x[player] = 256*256; // force offscreen, will reload position next swing
break; // just immediately end
}
// onscreen: slide down 1 pixel per frame until we're done
t = (floor_y[(balls_wx[player*2] + scroll_cx + 3) & 511] - 6) & 255;
while (balls_y[player] < t)
{
ball_y += 256;
++balls_y[player];
hole_draw();
frame();
}
break;
}
hole_draw();
frame();
} while(1);
//ball_landed:
// stop the ball
ball_s = 0;
hole_draw();
frame();
hole_draw(); frames(2); // just a little extra time for last bounce sound to die down
if ((hole != 0) && ((ball_x/256) > (hole_sx-3)) && ((ball_x/256) < (hole_sx+5)))
{
cleared |= (1 << player); // landed in hole!
}
balls_x[player] = ball_x & 0x00FFFFFF; // sanitize the 4th "padding" byte
balls_y[player] = ball_y / 256;
// next player
for (i=0; i<4; ++i)
{
player = (player+1) & 3;
if (!(cleared & (1 << player))) break;
}
if (i >= 4) break; // should be same as cleared >= 0x10
// player prompt sound if mutliplayer
if (players > 1) sound_play(SFX_PROMPTS[player]);
if (first_stroke)
{
status_bar_fade_in();
first_stroke = 0;
}
ball_draw_setup();
hole_draw();
input_poll(); // clear pending input
frame();
}
#if HOLE_SKIP
hole_skip:
#endif
// end of game
while (hole == 0)
{
hole_draw();
frame();
}
// 6. fade out status bar, remove flag
sound_play(SFX_FLAG);
flag_remove = 1;
if (!first_stroke) // if they got a hole in 1, stroke status hasn't been faded in, so skip this fadeout
{
palette_generate(pal_sky, pal_floor, blend25(pal_text, pal_sky)); hole_draw(); frame(); hole_draw(); frame();
palette_generate(pal_sky, pal_floor, blend50(pal_sky, pal_text)); hole_draw(); frame(); hole_draw(); frame();
palette_generate(pal_sky, pal_floor, blend25(pal_sky, pal_text)); hole_draw(); frame(); hole_draw(); frame();
palette_generate(pal_sky, pal_floor, pal_sky );
}
else first_stroke = 0;
// wait for flag to finish leaving
while (flag_remove) { hole_draw(); frame(); }
// 7. prepare next hole
hole_next();
if (day) --day;
if (day == 0)
{
day = DAY_TIME;
field_set ^= 8; // day/night only
goto field_change;
}
else if (transition_time == 0)
{
field_set = (prng() & 7) | (field_set & 8); // random but don't switch day/night
transition_time = (prng() & 7) + 3;
field_change:
transition = TRANSITION_TIME;
i = set_w[field_set];
if (i)
{
weather_tile = i;
weather_attribute_set();
}
}
else
{
--transition_time;
}
weather_wind_random();
if (hole == 0 && weather_wind_fade_dir < 0) weather_wind_fade_dir = 1; // no left wind allowed on last hole
return; // hole_play() again
}
void fmult_test()
{
// needed this test to verify that fmult works
for (mx=1; mx!=0; ++mx)
{
tsx = (sint32)(prng() | ((uint16)prng()<<8));
tsy = (sint32)(prng() | ((uint16)prng()<<8));
ux = (sint32)(((sint32)tsx*tsy)/256);
vx = fmult(tsx,tsy);
ASSERT(ux==vx);
}
}
//
// main`
//
void main()
{
//fmult_test();
// replace the common "all 0s" or "all 1s" emulator RAM initialization seed
// with two hand-picked cases to make the title screen look nice.
// (further entropy for subsequent holes is gathered while waiting on the
// title screen, but I have to generate at least the title screen before user input.)
if (seed == 0x00800000) seed = 0x00654321; // field set 7 (yellow day), 4 holes to night
if (seed == 0x00FFFFFF) seed = 0x000D7755; // field set F (yellow night), 4 holes to day
if (seed == 0x00FF0000) seed = 0x00654399; // field set 7 (yellow day), 5 holes to night
input_setup();
ppu_latch(0x1000);
ppu_fill(0x55,8*1024);
ptr = layers_chr;
ppu_latch(0x0000);
ppu_load(LAYERS_CHR_SIZE);
ptr = sprite_chr;
ppu_latch(0x1000);
ppu_load(SPRITE_CHR_SIZE);
#if !SHOW_LEFT_COLUMN
ppu_mask(0x18); // hide left column
#endif
title();
while (1) hole_play();
return; // never reached
}
// end of file