diff --git a/Radiant/neverball.game/data/entities.ent b/Radiant/neverball.game/data/entities.ent
index b1d43c352..a98bc8de3 100644
--- a/Radiant/neverball.game/data/entities.ent
+++ b/Radiant/neverball.game/data/entities.ent
@@ -186,6 +186,8 @@ Used for most static level geometry.
Name of the author.
Marks this level as a bonus level.
Neverputt: time to wait after the ball has stopped before starting the next shot. Useful to ensure that moving objects won't hit the ball while the player is making the shot.
+Neverputt: time to wait for the ball to stop before the player can forfeit their turn for a penalty. -1.0 means never. 0.0 means always. The default is 30.0.
+Neverputt: time to wait for the ball to stop before the player automatically forfeits their turn for a penalty. -1.0 means never. The default is -1.0.
Neverputt: the number of strokes required to complete the hole.
diff --git a/TrenchBroom/entities.ent b/TrenchBroom/entities.ent
index 7ef1e2d01..ccd0f9790 100644
--- a/TrenchBroom/entities.ent
+++ b/TrenchBroom/entities.ent
@@ -187,6 +187,8 @@ Used for most static level geometry.
Name of the author.
Marks this level as a bonus level.
Neverputt: time to wait after the ball has stopped before starting the next shot. Useful to ensure that moving objects won't hit the ball while the player is making the shot.
+Neverputt: time to wait for the ball to stop before the player can forfeit their turn for a penalty. -1.0 means never. 0.0 means always. The default is 30.0.
+Neverputt: time to wait for the ball to stop before the player automatically forfeits their turn for a penalty. -1.0 means never. The default is -1.0.
Neverputt: the number of strokes required to complete the hole.
diff --git a/data/map-ckk/17.map b/data/map-ckk/17.map
index ce92f3b4d..6c5c2cd65 100644
--- a/data/map-ckk/17.map
+++ b/data/map-ckk/17.map
@@ -1,6 +1,7 @@
// entity 0
{
"classname" "worldspawn"
+"can_forfeit_after" "60"
// brush 0
{
( 72 640 264 ) ( 72 640 240 ) ( 72 656 264 ) mtrl/invisible 128 128 0 0.5 0.5 0 0 0
diff --git a/data/map-ckk/18.map b/data/map-ckk/18.map
index b4e609c34..4582922e5 100644
--- a/data/map-ckk/18.map
+++ b/data/map-ckk/18.map
@@ -1,9 +1,9 @@
-
// entity 0
{
"classname" "worldspawn"
"grad" "back/space.png"
"back" "map-back/jupitor.sol"
+"can_forfeit_after" "60"
// brush 0
{
( -384 6016 -160 ) ( -400 6032 -160 ) ( -384 6016 -184 ) mtrl/invisible 128 128 0 0.5 0.5 0 0 0
diff --git a/data/map-iCourse/iPutt14.map b/data/map-iCourse/iPutt14.map
index b4767cd8a..dc4875fd2 100644
--- a/data/map-iCourse/iPutt14.map
+++ b/data/map-iCourse/iPutt14.map
@@ -1,7 +1,7 @@
-
// entity 0
{
"classname" "worldspawn"
+"can_forfeit_after" "60"
// brush 0
{
( -66 -328 -128 ) ( -128 -352 -128 ) ( -70 -326 -128 ) mtrl/turf-green-small 0 0 0 0.5 0.5 0 0 0
diff --git a/data/map-iCourse/iPutt15.map b/data/map-iCourse/iPutt15.map
index 83b79c928..f43ccbe80 100644
--- a/data/map-iCourse/iPutt15.map
+++ b/data/map-iCourse/iPutt15.map
@@ -1,7 +1,7 @@
-
// entity 0
{
"classname" "worldspawn"
+"can_forfeit_after" "60"
// brush 0
{
( -352 288 104 ) ( -326 314 80 ) ( -322 312 80 ) mtrl/turf-green-dark 0 0 0 0.5 0.5 0 0 0
diff --git a/data/map-iCourse/iPutt16.map b/data/map-iCourse/iPutt16.map
index 26fd58260..842b56d45 100644
--- a/data/map-iCourse/iPutt16.map
+++ b/data/map-iCourse/iPutt16.map
@@ -1,7 +1,7 @@
-
// entity 0
{
"classname" "worldspawn"
+"can_forfeit_after" "60"
// brush 0
{
( -256 -288 -152 ) ( -230 -314 -152 ) ( -232 -318 -152 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
diff --git a/data/map-iCourse/iPutt18.map b/data/map-iCourse/iPutt18.map
index f9bfcf807..6b43ffac2 100644
--- a/data/map-iCourse/iPutt18.map
+++ b/data/map-iCourse/iPutt18.map
@@ -1,7 +1,7 @@
-
// entity 0
{
"classname" "worldspawn"
+"can_forfeit_after" "60"
// brush 0
{
( 128 768 -328 ) ( 128 768 -336 ) ( 186 826 -336 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
diff --git a/data/map-paxed3/ovalli.map b/data/map-paxed3/ovalli.map
index 740e2705d..28a9670cc 100644
--- a/data/map-paxed3/ovalli.map
+++ b/data/map-paxed3/ovalli.map
@@ -1,6 +1,7 @@
// entity 0
{
"classname" "worldspawn"
+"can_forfeit_after" "60"
// brush 0
{
( -64 64 8 ) ( -64 64 -8 ) ( -64 128 -8 ) mtrl/invisible 0 0 0 0.5 0.5 134217728 0 0
diff --git a/data/map-vidski/bigpipe.map b/data/map-vidski/bigpipe.map
index 062303ace..6e1c2a8b6 100644
--- a/data/map-vidski/bigpipe.map
+++ b/data/map-vidski/bigpipe.map
@@ -1,7 +1,7 @@
-
// entity 0
{
"classname" "worldspawn"
+"can_forfeit_after" "60"
// brush 0
{
( 0 -2304 296 ) ( 0 -2432 296 ) ( -96 -2304 296 ) mtrl/arrow-green-light 0 0 0 0.5 0.5 134217728 0 0
diff --git a/data/map-vidski/clovertop.map b/data/map-vidski/clovertop.map
index 528d97dee..4750964bd 100644
--- a/data/map-vidski/clovertop.map
+++ b/data/map-vidski/clovertop.map
@@ -1,6 +1,7 @@
// entity 0
{
"classname" "worldspawn"
+"can_forfeit_after" "60"
// brush 0
{
( 64 -176 -8 ) ( 64 -304 -8 ) ( -32 -176 -8 ) mtrl/arrow-green-light 128 160 0 0.5 0.5 134217728 0 0
@@ -4043,8 +4044,9 @@
}
// entity 1
{
-"classname" "func_train"
-"target" "t2"
+"classname" "info_player_start"
+"origin" "0.000000 16.000000 16.000000"
+"radius" "0.0625"
}
// entity 2
{
@@ -4072,37 +4074,31 @@
}
// entity 6
{
-"classname" "info_player_start"
-"origin" "0.000000 16.000000 16.000000"
-"radius" "0.0625"
-}
-// entity 7
-{
"classname" "target_position"
"origin" "-392.000000 376.000000 528.000000"
"targetname" "target_position1"
"angle" "-179.999985"
}
-// entity 8
+// entity 7
{
"classname" "info_player_intermission"
"origin" "1424.000000 -960.000000 1504.000000"
"target" "target_position1"
"angle" "-179.999985"
}
-// entity 9
+// entity 8
{
"classname" "info_player_deathmatch"
"origin" "0.000000 64.000000 1312.000000"
"radius" "0.1375"
}
-// entity 10
+// entity 9
{
"classname" "target_position"
"origin" "272.000000 -152.000000 288.000000"
"targetname" "target_position2"
}
-// entity 11
+// entity 10
{
"classname" "info_player_intermission"
"origin" "-2072.000000 1344.000000 2272.000000"
diff --git a/data/map-vidski/hourglass.map b/data/map-vidski/hourglass.map
index 0bccdd473..1aa3ff68f 100644
--- a/data/map-vidski/hourglass.map
+++ b/data/map-vidski/hourglass.map
@@ -1,7 +1,7 @@
-
// entity 0
{
"classname" "worldspawn"
+"can_forfeit_after" "60"
// brush 0
{
( -568 0 1066 ) ( -567 -18 1066 ) ( -568 0 1082 ) mtrl/edge-green-dark 240 4 0 -0.5 0.5 0 0 0
diff --git a/data/map-vidski/voodoo.map b/data/map-vidski/voodoo.map
index 2aaf2e40e..31c402c07 100644
--- a/data/map-vidski/voodoo.map
+++ b/data/map-vidski/voodoo.map
@@ -1,7 +1,7 @@
-
// entity 0
{
"classname" "worldspawn"
+"can_forfeit_after" "60"
// brush 0
{
( 0 -1312 16 ) ( 49 -1307 16 ) ( 0 -1312 32 ) mtrl/edge-green-dark 12 0 0 0.5 0.5 0 0 0
diff --git a/putt/game.c b/putt/game.c
index 654489503..99b4ef005 100644
--- a/putt/game.c
+++ b/putt/game.c
@@ -56,6 +56,12 @@ static float jump_dt; /* Jump duration */
static float jump_p[3]; /* Jump destination */
static float idle_t; /* Idling timeout */
+static float forf_t; /* Can forfeit timeout */
+static float auto_t; /* Automatic timeout */
+
+static float putt_t; /* Current player putt time elapsed */
+static int has_forfeited; /* Has the current player forfeited? */
+static int has_auto_forf; /* Was there an auto-forfeiture? */
/*---------------------------------------------------------------------------*/
@@ -93,7 +99,11 @@ int game_init(const char *s)
jump_e = 1;
jump_b = 0;
- idle_t = 1.0f;
+ idle_t = 1.0f;
+ forf_t = 30.0f;
+ auto_t = -1.0f;
+
+ putt_t = 0.0f;
view_init();
@@ -114,6 +124,10 @@ int game_init(const char *s)
if (idle_t < 1.0f)
idle_t = 1.0f;
}
+ else if (strcmp(k, "can_forfeit_after") == 0)
+ sscanf(v, "%f", &forf_t);
+ else if (strcmp(k, "auto_timeout") == 0)
+ sscanf(v, "%f", &auto_t);
}
return 1;
}
@@ -476,6 +490,18 @@ static int game_update_state(float dt)
return GAME_STOP;
}
+ /* Test for automatic forfeit time-outs. */
+
+ if (auto_t >= 0.f)
+ {
+ if (putt_t >= auto_t)
+ {
+ t = 0.f;
+ has_auto_forf = 1;
+ return GAME_TIME;
+ }
+ }
+
return GAME_NONE;
}
@@ -545,6 +571,8 @@ int game_step(const float g[3], float dt)
b = d;
if (m)
st += t;
+
+ putt_t += t;
}
/* Mix the sound of a ball bounce. */
@@ -570,6 +598,13 @@ void game_putt(void)
file.vary.uv[ball].v[2] = -4.f * view_e[2][2] * view_m;
view_m = 0.f;
+
+ /* Start forfeit timers. */
+
+ putt_t = 0.f;
+
+ has_forfeited = 0;
+ has_auto_forf = 0;
}
/*---------------------------------------------------------------------------*/
@@ -708,5 +743,55 @@ void game_set_pos(float p[3], float e[3][3])
v_cpy(file.vary.uv[ball].e[2], e[2]);
}
+int game_can_forfeit(void)
+{
+ if (forf_t < 0.f)
+ return 0;
+
+ if (has_forfeited)
+ return 0;
+
+ if (putt_t >= forf_t)
+ return 1;
+
+ return 0;
+}
+
+int game_has_forced(void)
+{
+ return has_auto_forf;
+}
+
+/*
+ * The ball is forfeited. If not already forfeited, call ‘hole_time’.
+ *
+ * The reason this isn't a direct wrapper around ‘hole_time’ is that once a
+ * forfeit is reached, the player can pause again, and then press escape to go
+ * back to the forfeiture screen, which upon entry re-executes this function.
+ *
+ * For st_fall, the logic is that if ‘hole_all’ is only called if the game
+ * is not currently paused, thus preventing multiple calls to ‘hole_fall’
+ * for a single fall, giving multiple penalties for a single event.
+ *
+ * This trick doesn't work for manual forfeits, since they are entered from
+ * a paused state, not from an unpaused state, so we need to track separate
+ * instances differently, so that pausing from a forfeit penalty and then
+ * pressing escape doesn't apply another forfeiture.
+ */
+void game_forfeit(void)
+{
+ if (!has_forfeited)
+ {
+ /* Reset timers. */
+
+ has_forfeited = 1;
+ putt_t = 0;
+
+ /* Forfeit the ball. */
+
+ hole_time();
+ }
+}
+
/*---------------------------------------------------------------------------*/
diff --git a/putt/game.h b/putt/game.h
index 0dfb62f69..9ef0b5fc1 100644
--- a/putt/game.h
+++ b/putt/game.h
@@ -31,6 +31,7 @@
#define GAME_STOP 1
#define GAME_GOAL 2
#define GAME_FALL 3
+#define GAME_TIME 4
/*---------------------------------------------------------------------------*/
@@ -52,6 +53,10 @@ void game_ball(int);
void game_set_pos(float[3], float[3][3]);
void game_get_pos(float[3], float[3][3]);
+int game_can_forfeit(void);
+int game_has_forced(void);
+void game_forfeit(void);
+
/*---------------------------------------------------------------------------*/
#endif
diff --git a/putt/hole.c b/putt/hole.c
index 3716105c3..247385ca3 100644
--- a/putt/hole.c
+++ b/putt/hole.c
@@ -389,6 +389,12 @@ void hole_fall(void)
}
}
+void hole_time(void)
+{
+ /* Just use the same logic as hole_fall. */
+ hole_fall();
+}
+
/*---------------------------------------------------------------------------*/
void hole_song(void)
diff --git a/putt/hole.h b/putt/hole.h
index aa106ac34..248309175 100644
--- a/putt/hole.h
+++ b/putt/hole.h
@@ -32,6 +32,7 @@ int hole_move(void);
void hole_goal(void);
void hole_stop(void);
void hole_fall(void);
+void hole_time(void);
void hole_song(void);
diff --git a/putt/main.c b/putt/main.c
index 9bc99f134..428e1e919 100644
--- a/putt/main.c
+++ b/putt/main.c
@@ -216,7 +216,7 @@ static int loop(void)
{
case SDL_WINDOWEVENT_FOCUS_LOST:
if (video_get_grab())
- goto_pause(&st_over);
+ goto_pause(&st_over, &st_time);
break;
case SDL_WINDOWEVENT_MOVED:
diff --git a/putt/st_all.c b/putt/st_all.c
index c6538532b..b105cd553 100644
--- a/putt/st_all.c
+++ b/putt/st_all.c
@@ -609,17 +609,20 @@ static int paused = 0;
static struct state *st_continue;
static struct state *st_quit;
+static struct state *st_forf;
#define PAUSE_CONTINUE 1
#define PAUSE_QUIT 2
+#define PAUSE_FORFEIT 3
-int goto_pause(struct state *s)
+int goto_pause(struct state *quit, struct state *forf)
{
if (curr_state() == &st_pause)
return 1;
st_continue = curr_state();
- st_quit = s;
+ st_quit = quit;
+ st_forf = forf;
paused = 1;
return goto_state(&st_pause);
@@ -636,6 +639,9 @@ static int pause_action(int i)
case PAUSE_QUIT:
return goto_state(st_quit);
+
+ case PAUSE_FORFEIT:
+ return goto_state(st_forf);
}
return 1;
}
@@ -654,6 +660,8 @@ static int pause_enter(struct state *st, struct state *prev, int intent)
if ((jd = gui_harray(id)))
{
gui_state(jd, _("Quit"), GUI_SML, PAUSE_QUIT, 0);
+ if (game_can_forfeit())
+ gui_state(jd, _("Forfeit"), GUI_SML, PAUSE_FORFEIT, 0);
gui_start(jd, _("Continue"), GUI_SML, PAUSE_CONTINUE, 1);
}
@@ -721,7 +729,7 @@ static int shared_keybd(int c, int d)
if (d)
{
if (c == KEY_EXIT)
- return goto_pause(&st_over);
+ return goto_pause(&st_over, &st_time);
}
return 1;
}
@@ -815,7 +823,7 @@ static int next_keybd(int c, int d)
if (c == KEY_POSE)
return goto_state(&st_poser);
if (c == KEY_EXIT)
- return goto_pause(&st_over);
+ return goto_pause(&st_over, &st_time);
if ('0' <= c && c <= '9')
num = num * 10 + c - '0';
}
@@ -844,7 +852,7 @@ static int next_buttn(int b, int d)
return goto_state(&st_flyby);
}
if (config_tst_d(CONFIG_JOYSTICK_BUTTON_B, b))
- return goto_pause(&st_over);
+ return goto_pause(&st_over, &st_time);
}
return 1;
}
@@ -934,7 +942,7 @@ static int flyby_buttn(int b, int d)
}
if (config_tst_d(CONFIG_JOYSTICK_BUTTON_B, b) ||
config_tst_d(CONFIG_JOYSTICK_BUTTON_START, b))
- return goto_pause(&st_over);
+ return goto_pause(&st_over, &st_time);
}
return 1;
}
@@ -1020,7 +1028,7 @@ static int stroke_buttn(int b, int d)
if (config_tst_d(CONFIG_JOYSTICK_BUTTON_A, b))
return goto_state(&st_roll);
if (config_tst_d(CONFIG_JOYSTICK_BUTTON_START, b))
- return goto_pause(&st_over);
+ return goto_pause(&st_over, &st_time);
}
else
{
@@ -1032,6 +1040,84 @@ static int stroke_buttn(int b, int d)
/*---------------------------------------------------------------------------*/
+static int time_enter(struct state *st, struct state *prev, int intent)
+{
+ int id;
+
+ const char *msg = (
+ game_has_forced()
+ ? _("1 Stroke Timeout Penalty")
+ : _("1 Stroke Penalty")
+ );
+
+ if ((id = gui_label(0, msg, GUI_MED, gui_blk, gui_red)))
+ gui_layout(id, 0, 0);
+
+ paused = 0;
+ game_forfeit();
+
+ hud_init();
+
+ return transition_slide(id, 1, intent);
+}
+
+static int time_leave(struct state *st, struct state *next, int id, int intent)
+{
+ hud_free();
+ return transition_slide(id, 0, intent);
+}
+
+static void time_paint(int id, float t)
+{
+ game_draw(0, t);
+ gui_paint(id);
+ hud_paint();
+}
+
+static void time_timer(int id, float dt)
+{
+ gui_timer(id, dt);
+
+ if (time_state() > 3)
+ {
+ if (hole_next())
+ goto_state(&st_next);
+ else
+ goto_state(&st_score);
+ }
+}
+
+static int time_click(int b, int d)
+{
+ if (b == SDL_BUTTON_LEFT && d == 1)
+ {
+ if (hole_next())
+ goto_state(&st_next);
+ else
+ goto_state(&st_score);
+ }
+ return 1;
+}
+
+static int time_buttn(int b, int d)
+{
+ if (d)
+ {
+ if (config_tst_d(CONFIG_JOYSTICK_BUTTON_A, b))
+ {
+ if (hole_next())
+ goto_state(&st_next);
+ else
+ goto_state(&st_score);
+ }
+ if (config_tst_d(CONFIG_JOYSTICK_BUTTON_B, b))
+ return goto_pause(&st_over, &st_time);
+ }
+ return 1;
+}
+
+/*---------------------------------------------------------------------------*/
+
static int roll_enter(struct state *st, struct state *prev, int intent)
{
video_hide_cursor();
@@ -1067,6 +1153,7 @@ static void roll_timer(int id, float dt)
case GAME_STOP: goto_state(&st_stop); break;
case GAME_GOAL: goto_state(&st_goal); break;
case GAME_FALL: goto_state(&st_fall); break;
+ case GAME_TIME: goto_state(&st_time); break;
}
}
@@ -1076,7 +1163,7 @@ static int roll_buttn(int b, int d)
{
if (config_tst_d(CONFIG_JOYSTICK_BUTTON_B, b) ||
config_tst_d(CONFIG_JOYSTICK_BUTTON_START, b))
- return goto_pause(&st_over);
+ return goto_pause(&st_over, &st_time);
}
return 1;
}
@@ -1150,7 +1237,7 @@ static int goal_buttn(int b, int d)
goto_state(&st_score);
}
if (config_tst_d(CONFIG_JOYSTICK_BUTTON_B, b))
- return goto_pause(&st_over);
+ return goto_pause(&st_over, &st_time);
}
return 1;
}
@@ -1222,7 +1309,7 @@ static int stop_buttn(int b, int d)
}
if (config_tst_d(CONFIG_JOYSTICK_BUTTON_B, b) ||
config_tst_d(CONFIG_JOYSTICK_BUTTON_START, b))
- return goto_pause(&st_over);
+ return goto_pause(&st_over, &st_time);
}
return 1;
}
@@ -1299,7 +1386,7 @@ static int fall_buttn(int b, int d)
goto_state(&st_score);
}
if (config_tst_d(CONFIG_JOYSTICK_BUTTON_B, b))
- return goto_pause(&st_over);
+ return goto_pause(&st_over, &st_time);
}
return 1;
}
@@ -1356,7 +1443,7 @@ static int score_buttn(int b, int d)
goto_state(&st_title);
}
if (config_tst_d(CONFIG_JOYSTICK_BUTTON_B, b))
- return goto_pause(&st_over);
+ return goto_pause(&st_over, &st_time);
}
return 1;
}
@@ -1547,6 +1634,19 @@ struct state st_fall = {
fall_buttn
};
+struct state st_time = {
+ time_enter,
+ time_leave,
+ time_paint,
+ time_timer,
+ NULL,
+ NULL,
+ NULL,
+ time_click,
+ shared_keybd,
+ time_buttn
+};
+
struct state st_score = {
score_enter,
score_leave,
diff --git a/putt/st_all.h b/putt/st_all.h
index 979f402f8..c4a154802 100644
--- a/putt/st_all.h
+++ b/putt/st_all.h
@@ -14,10 +14,11 @@ extern struct state st_roll;
extern struct state st_goal;
extern struct state st_stop;
extern struct state st_fall;
+extern struct state st_time;
extern struct state st_score;
extern struct state st_over;
extern struct state st_pause;
-int goto_pause(struct state *);
+int goto_pause(struct state *, struct state *);
#endif