From 7fb08abe58b2c84e36a5ed247d2103e08ae98aa4 Mon Sep 17 00:00:00 2001 From: Holger Schemel Date: Sun, 11 Feb 2024 12:37:35 +0100 Subject: [PATCH] added GDash source files prepared for game engine integration into R'n'D --- src/game.c | 11 + src/game.h | 1 + src/game_bd/Makefile | 24 +- src/game_bd/bd_bdcff.c | 1218 ++++++++++++ src/game_bd/bd_bdcff.h | 25 + src/game_bd/bd_c64import.c | 2549 +++++++++++++++++++++++++ src/game_bd/bd_c64import.h | 70 + src/game_bd/bd_cave.c | 1590 ++++++++++++++++ src/game_bd/bd_cavedb.c | 1097 +++++++++++ src/game_bd/bd_cavedb.h | 36 + src/game_bd/bd_caveengine.c | 3596 +++++++++++++++++++++++++++++++++++ src/game_bd/bd_caveengine.h | 32 + src/game_bd/bd_caveobject.c | 1608 ++++++++++++++++ src/game_bd/bd_caveobject.h | 102 + src/game_bd/bd_caveset.c | 684 +++++++ src/game_bd/bd_caveset.h | 92 + src/game_bd/bd_gameplay.c | 704 +++++++ src/game_bd/bd_graphics.c | 373 ++++ src/game_bd/bd_graphics.h | 46 + src/game_bd/bd_sound.c | 418 ++++ src/game_bd/bd_sound.h | 30 + src/game_bd/export_bd.h | 11 + src/game_bd/import_bd.h | 3 + src/game_bd/main_bd.c | 16 + src/game_bd/main_bd.h | 29 + src/libgame/system.h | 2 + src/tape.c | 25 +- src/tape.h | 3 + 28 files changed, 14391 insertions(+), 4 deletions(-) create mode 100644 src/game_bd/bd_bdcff.c create mode 100644 src/game_bd/bd_bdcff.h create mode 100644 src/game_bd/bd_c64import.c create mode 100644 src/game_bd/bd_c64import.h create mode 100644 src/game_bd/bd_cave.c create mode 100644 src/game_bd/bd_cavedb.c create mode 100644 src/game_bd/bd_cavedb.h create mode 100644 src/game_bd/bd_caveengine.c create mode 100644 src/game_bd/bd_caveengine.h create mode 100644 src/game_bd/bd_caveobject.c create mode 100644 src/game_bd/bd_caveobject.h create mode 100644 src/game_bd/bd_caveset.c create mode 100644 src/game_bd/bd_caveset.h create mode 100644 src/game_bd/bd_gameplay.c create mode 100644 src/game_bd/bd_graphics.c create mode 100644 src/game_bd/bd_graphics.h create mode 100644 src/game_bd/bd_sound.c create mode 100644 src/game_bd/bd_sound.h diff --git a/src/game.c b/src/game.c index 9fe1fed1..2bdd28e5 100644 --- a/src/game.c +++ b/src/game.c @@ -16141,6 +16141,17 @@ boolean CheckRestartGame(void) return TRUE; } +boolean checkGameRunning(void) +{ + if (game_status != GAME_MODE_PLAYING) + return FALSE; + + if (level.game_engine_type == GAME_ENGINE_TYPE_BD && !checkGameRunning_BD()) + return FALSE; + + return TRUE; +} + boolean checkGameSolved(void) { // set for all game engines if level was solved diff --git a/src/game.h b/src/game.h index 28b222ed..4141b3bc 100644 --- a/src/game.h +++ b/src/game.h @@ -471,6 +471,7 @@ void RequestQuitGameExt(boolean, boolean, char *); void RequestQuitGame(boolean); boolean CheckRestartGame(void); +boolean checkGameRunning(void); boolean checkGameSolved(void); boolean checkGameFailed(void); boolean checkGameEnded(void); diff --git a/src/game_bd/Makefile b/src/game_bd/Makefile index 68a6d10e..b3be1068 100644 --- a/src/game_bd/Makefile +++ b/src/game_bd/Makefile @@ -16,9 +16,29 @@ # configuration # ----------------------------------------------------------------------------- -SRCS = main_bd.c +SRCS = main_bd.c \ + bd_cave.c \ + bd_cavedb.c \ + bd_caveengine.c \ + bd_caveobject.c \ + bd_bdcff.c \ + bd_caveset.c \ + bd_c64import.c \ + bd_gameplay.c \ + bd_graphics.c \ + bd_sound.c -OBJS = main_bd.o +OBJS = main_bd.o \ + bd_cave.o \ + bd_cavedb.o \ + bd_caveengine.o \ + bd_caveobject.o \ + bd_bdcff.o \ + bd_caveset.o \ + bd_c64import.o \ + bd_gameplay.o \ + bd_graphics.o \ + bd_sound.o GAME_BD = game_bd.a diff --git a/src/game_bd/bd_bdcff.c b/src/game_bd/bd_bdcff.c new file mode 100644 index 00000000..19cfc2d7 --- /dev/null +++ b/src/game_bd/bd_bdcff.c @@ -0,0 +1,1218 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include + +#include "main_bd.h" + + +#define BDCFF_VERSION "0.5" + +/* these are used for bdcff loading, storing the sizes of caves */ +static int cavesize[6], intermissionsize[6]; + +static boolean replay_store_from_bdcff(GdReplay *replay, const char *str) +{ + GdDirection dir; + boolean up, down, left, right; + boolean fire, suicide; + const char *num = NULL; + int count, i; + + fire = suicide = up = down = left = right = FALSE; + + for (i = 0; str[i] != 0; i++) + { + switch (str[i]) + { + case 'U': + fire = TRUE; + case 'u': + up = TRUE; + break; + + case 'D': + fire = TRUE; + case 'd': + down = TRUE; + break; + + case 'L': + fire = TRUE; + case 'l': + left = TRUE; + break; + + case 'R': + fire = TRUE; + case 'r': + right = TRUE; + break; + + case 'F': + fire = TRUE; + break; + case 'k': + suicide = TRUE; + break; + + case '.': + /* do nothing, as all other movements are false */ + break; + + case 'c': + case 'C': + /* bdcff 'combined' flags. do nothing. */ + break; + + default: + if (g_ascii_isdigit(str[i])) + { + if (!num) + num = str + i; + } + } + } + + dir = gd_direction_from_keypress(up, down, left, right); + count = 1; + + if (num) + sscanf(num, "%d", &count); + + for (i = 0; i < count; i++) + gd_replay_store_movement(replay, dir, fire, suicide); + + return TRUE; +} + +static boolean attrib_is_valid_for_cave(const char *attrib) +{ + int i; + + /* bdcff engine flag............ */ + if (strcasecmp(attrib, "Engine")==0) + return TRUE; + + /* old flags - for compatibility */ + if (strcasecmp(attrib, "BD1Scheduling")==0) + return TRUE; + + if (strcasecmp(attrib, "SnapExplosions")==0) + return TRUE; + + if (strcasecmp(attrib, "AmoebaProperties")==0) + return TRUE; + + /* search in property database */ + for (i = 0; gd_cave_properties[i].identifier != NULL; i++) + if (strcasecmp(gd_cave_properties[i].identifier, attrib) == 0) + return TRUE; + + return FALSE; +} + +static boolean attrib_is_valid_for_caveset(const char *attrib) +{ + int i; + + /* search in property database */ + for (i = 0; gd_caveset_properties[i].identifier != NULL; i++) + if (strcasecmp(gd_caveset_properties[i].identifier, attrib) == 0) + return TRUE; + + return FALSE; +} + +static boolean struct_set_property(gpointer str, const GdStructDescriptor *prop_desc, + const char *attrib, const char *param, int ratio) +{ + char **params; + int paramcount; + boolean identifier_found; + int paramindex = 0; + int i; + boolean was_string; + + params = g_strsplit_set(param, " ", -1); + paramcount = g_strv_length(params); + identifier_found = FALSE; + + /* check all known tags. do not exit this loop if identifier_found == true... + as there are more lines in the array which have the same identifier. */ + was_string = FALSE; + + for (i = 0; prop_desc[i].identifier != NULL; i++) + { + if (strcasecmp(prop_desc[i].identifier, attrib) == 0) + { + /* found the identifier */ + gpointer value = G_STRUCT_MEMBER_P(str, prop_desc[i].offset); + + /* these point to the same, but to avoid the awkward cast syntax */ + int *ivalue = value; + GdElement *evalue = value; + GdDirection *dvalue = value; + GdScheduling *svalue = value; + boolean *bvalue = value; + int j, k; + + identifier_found = TRUE; + + if (prop_desc[i].type == GD_TYPE_STRING) + { + /* strings are treated different, as occupy the whole length of the line */ + gd_strcpy(value, param); + + /* remember this to skip checking the number of parameters at the end of the function */ + was_string = TRUE; + + continue; + } + + if (prop_desc[i].type == GD_TYPE_LONGSTRING) + { + GString *str = *(GString **)value; + char *compressed; + + compressed = g_strcompress(param); + g_string_assign(str, compressed); + free(compressed); + + /* remember this to skip checking the number of parameters at the end of the function */ + was_string = TRUE; + + continue; + } + + /* not a string, so use scanf calls */ + /* ALSO, if no more parameters to process, exit loop */ + for (j = 0; j < prop_desc[i].count && params[paramindex] != NULL; j++) + { + boolean success = FALSE; + gdouble res; + + switch (prop_desc[i].type) + { + case GD_TYPE_LONGSTRING: + case GD_TYPE_STRING: + /* handled above */ + case GD_TAB: + case GD_LABEL: + /* do nothing */ + break; + + case GD_TYPE_BOOLEAN: + success = sscanf(params[paramindex], "%d", &bvalue[j]) == 1; + if (!success) + { + if (strcasecmp(params[paramindex], "true") == 0 || + strcasecmp(params[paramindex], "on") == 0 || + strcasecmp(params[paramindex], "yes") == 0) + { + bvalue[j] = TRUE; + success = TRUE; + } + else if (strcasecmp(params[paramindex], "false") == 0 || + strcasecmp(params[paramindex], "off") == 0 || + strcasecmp(params[paramindex], "no") == 0) + { + bvalue[j] = FALSE; + success = TRUE; + } + } + + /* if we are processing an array, fill other values with these. + if there are other values specified, those will be overwritten. */ + if (success) + for (k = j + 1; k < prop_desc[i].count; k++) + bvalue[k] = bvalue[j]; + + break; + + case GD_TYPE_INT: + success = sscanf(params[paramindex], "%d", &ivalue[j]) == 1; + if (success) + /* copy to other if array */ + for (k = j + 1; k < prop_desc[i].count; k++) + ivalue[k] = ivalue[j]; + + break; + + case GD_TYPE_PROBABILITY: + res = g_ascii_strtod(params[paramindex], NULL); + if (errno == 0 && res >= 0 && res <= 1) + { + /* fill all remaining items in array - may be only one */ + for (k = j; k < prop_desc[i].count; k++) + /* probabilities are stored inside as ppm (1E6) */ + ivalue[k] = res * 1E6 + 0.5; + + success = TRUE; + } + + break; + + case GD_TYPE_RATIO: + res = g_ascii_strtod (params[paramindex], NULL); + if (errno == 0 && res >= 0 && res <= 1) + { + for (k = j; k < prop_desc[i].count; k++) + ivalue[k] = (int)(res * ratio + 0.5); + + success = TRUE; + } + + break; + + case GD_TYPE_ELEMENT: + evalue[j] = gd_get_element_from_string(params[paramindex]); + + /* copy to all remaining elements in array */ + for (k = j + 1; k < prop_desc[i].count; k++) + evalue[k] = evalue[j]; + + /* this shows error message on its own, do treat as always succeeded */ + success = TRUE; + break; + + case GD_TYPE_DIRECTION: + dvalue[j] = gd_direction_from_string(params[paramindex]); + /* copy to all remaining items in array */ + for (k = j + 1; k < prop_desc[i].count; k++) + dvalue[k] = dvalue[j]; + + success = TRUE; + break; + + case GD_TYPE_SCHEDULING: + svalue[j] = gd_scheduling_from_string(params[paramindex]); + /* copy to all remaining items in array */ + for (k = j + 1; k < prop_desc[i].count; k++) + svalue[k] = svalue[j]; + + /* if there was an error, already reported by gd_scheduling_from_string */ + success = TRUE; + break; + + case GD_TYPE_COLOR: + case GD_TYPE_EFFECT: + /* shoud have handled this elsewhere */ + break; + } + + if (success) + paramindex++; /* go to next parameter to process */ + else + Warn("invalid parameter '%s' for attribute %s", params[paramindex], attrib); + } + } + } + + /* if we found the identifier, but still could not process all parameters... */ + /* of course, not for strings, as the whole line is the string */ + if (identifier_found && !was_string && paramindex < paramcount) + Warn("excess parameters for attribute '%s': '%s'", attrib, params[paramindex]); + + g_strfreev(params); + + return identifier_found; +} + +/******************************************************************************** + * + * BDCFF LOADING + * + */ + +static boolean replay_store_more_from_bdcff(GdReplay *replay, const char *param) +{ + char **split; + int i; + boolean result = TRUE; + + split = g_strsplit_set(param, " ", -1); + + for (i = 0; split[i] != 0; i++) + result = result && replay_store_from_bdcff(replay, split[i]); + + g_strfreev(split); + + return result; +} + +/* report all remaining tags; called after the above function. */ +static void replay_report_unknown_tags_func(const char *attrib, const char *param, gpointer data) +{ + Warn("unknown replay tag '%s'", attrib); +} + +/* a GHashTable foreach func. + keys are attribs; values are params; + the user data is the cave the hash table belongs to. */ +static boolean replay_process_tags_func(const char *attrib, const char *param, GdReplay *replay) +{ + boolean identifier_found = FALSE; + + /* movements */ + if (strcasecmp(attrib, "Movements") == 0) + { + identifier_found = TRUE; + replay_store_more_from_bdcff(replay, param); + } + else + { + /* any other tag */ + /* 0: for ratio types; not used */ + identifier_found = struct_set_property(replay, gd_replay_properties, + attrib, param, 0); + } + + /* a ghrfunc should return true if the identifier is to be removed */ + return identifier_found; +} + +/* ... */ +static void replay_process_tags(GdReplay *replay, GHashTable *tags) +{ + /* process all tags */ + g_hash_table_foreach_remove(tags, (GHRFunc) replay_process_tags_func, replay); +} + +/* a GHashTable foreach func. + keys are attribs; values are params; + the user data is the cave the hash table belongs to. */ +static boolean cave_process_tags_func(const char *attrib, const char *param, GdCave *cave) +{ + char **params; + boolean identifier_found; + + params = g_strsplit_set(param, " ", -1); + identifier_found = FALSE; + + if (strcasecmp(attrib, "SnapExplosions") == 0) + { + /* handle compatibility with old snapexplosions flag */ + + identifier_found = TRUE; + + if (strcasecmp(param, "true") == 0) + { + cave->snap_element = O_EXPLODE_1; + } + else if (strcasecmp(param, "false") == 0) + { + cave->snap_element = O_SPACE; + } + else + { + Warn("invalid param for '%s': '%s'", attrib, param); + } + } + else if (strcasecmp(attrib, "BD1Scheduling") == 0) + { + /* handle compatibility with old bd1scheduling flag */ + + identifier_found = TRUE; + + if (strcasecmp(param, "true") == 0) + { + if (cave->scheduling == GD_SCHEDULING_PLCK) + cave->scheduling = GD_SCHEDULING_BD1; + } + } + else if (strcasecmp(attrib, "Engine") == 0) + { + /* handle bdcff engine flag */ + + identifier_found = TRUE; + + GdEngine engine = gd_cave_get_engine_from_string(param); + + if (engine == GD_ENGINE_INVALID) + Warn(_("invalid parameter \"%s\" for attribute %s"), param, attrib); + else + gd_cave_set_engine_defaults(cave, engine); + } + else if (strcasecmp(attrib, "AmoebaProperties") == 0) + { + /* handle compatibility with old AmoebaProperties flag */ + + GdElement elem1 = O_STONE, elem2 = O_DIAMOND; + + identifier_found = TRUE; + elem1 = gd_get_element_from_string(params[0]); + elem2 = gd_get_element_from_string(params[1]); + cave->amoeba_too_big_effect = elem1; + cave->amoeba_enclosed_effect = elem2; + } + else if (strcasecmp(attrib, "Colors") == 0) + { + /* colors attribute is a mess, have to process explicitly */ + + /* Colors = [border background] foreground1 foreground2 foreground3 [amoeba slime] */ + identifier_found = TRUE; + + cave->colorb = GD_GDASH_BLACK; /* border - black */ + cave->color0 = GD_GDASH_BLACK; /* background - black */ + cave->color1 = GD_GDASH_RED; + cave->color2 = GD_GDASH_PURPLE; + cave->color3 = GD_GDASH_YELLOW; + cave->color4 = cave->color3; /* amoeba */ + cave->color5 = cave->color1; /* slime */ + } + else + { + identifier_found = struct_set_property(cave, gd_cave_properties, attrib, param, cave->w * cave->h); + } + + g_strfreev(params); + + /* a ghrfunc should return true if the identifier is to be removed */ + return identifier_found; +} + +/* report all remaining tags; called after the above function. */ +static void cave_report_and_copy_unknown_tags_func(char *attrib, char *param, gpointer data) +{ + GdCave *cave = (GdCave *)data; + + Warn("unknown tag '%s'", attrib); + + g_hash_table_insert(cave->tags, g_strdup(attrib), g_strdup(param)); +} + +/* having read all strings belonging to the cave, process it. */ +static void cave_process_tags(GdCave *cave, GHashTable *tags, GList *maplines) +{ + char *value; + + /* first check cave name, so we can report errors correctly (saying that GdCave xy: error foobar) */ + value = g_hash_table_lookup(tags, "Name"); + if (value) + cave_process_tags_func("Name", value, cave); + + /* process lame engine tag first so its settings may be overwritten later */ + value = g_hash_table_lookup(tags, "Engine"); + if (value) + { + cave_process_tags_func("Engine", value, cave); + g_hash_table_remove(tags, "Engine"); + } + + /* check if this is an intermission, so we can set to cavesize or intermissionsize */ + value = g_hash_table_lookup(tags, "Intermission"); + if (value) + { + cave_process_tags_func("Intermission", value, cave); + g_hash_table_remove(tags, "Intermission"); + } + + if (cave->intermission) + { + /* set to IntermissionSize */ + cave->w = intermissionsize[0]; + cave->h = intermissionsize[1]; + cave->x1 = intermissionsize[2]; + cave->y1 = intermissionsize[3]; + cave->x2 = intermissionsize[4]; + cave->y2 = intermissionsize[5]; + } + else + { + /* set to CaveSize */ + cave->w = cavesize[0]; + cave->h = cavesize[1]; + cave->x1 = cavesize[2]; + cave->y1 = cavesize[3]; + cave->x2 = cavesize[4]; + cave->y2 = cavesize[5]; + } + + /* process size at the beginning... as ratio types depend on this. */ + value = g_hash_table_lookup(tags, "Size"); + if (value) + { + cave_process_tags_func("Size", value, cave); + g_hash_table_remove(tags, "Size"); + } + + /* these are read from the hash table, but also have some implications */ + /* we do not delete them from the hash table here; as _their values will be processed later_. */ + /* here we only set their implicite meanings. */ + /* these also set predictability */ + if (g_hash_table_lookup(tags, "SlimePermeability")) + cave->slime_predictable = FALSE; + + if (g_hash_table_lookup(tags, "SlimePermeabilityC64")) + cave->slime_predictable = TRUE; + + /* these set scheduling type. framedelay takes precedence, if there are both; so we check it later. */ + if (g_hash_table_lookup(tags, "CaveDelay")) + { + /* only set scheduling type, when it is not the gdash-default. */ + /* this allows settings cavescheduling = bd1 in the [game] section, for example. */ + /* in that case, this one will not overwrite it. */ + if (cave->scheduling == GD_SCHEDULING_MILLISECONDS) + cave->scheduling = GD_SCHEDULING_PLCK; + } + + if (g_hash_table_lookup(tags, "FrameTime")) + /* but if the cave has a frametime setting, always switch to milliseconds. */ + cave->scheduling = GD_SCHEDULING_MILLISECONDS; + + /* process all tags */ + g_hash_table_foreach_remove(tags, (GHRFunc) cave_process_tags_func, cave); + + /* and at the end, when read all tags (especially the size= tag) */ + /* process map, if any. */ + /* only report if map read is bigger than size= specified. */ + /* some old bdcff files use smaller intermissions than the one specified. */ + if (maplines) + { + int x, y, length = g_list_length(maplines); + GList *iter; + + /* create map and fill with initial border, in case that map strings are shorter or somewhat */ + cave->map = gd_cave_map_new(cave, GdElement); + + for (y = 0; y < cave->h; y++) + for (x = 0; x < cave->w; x++) + cave->map[y][x] = cave->initial_border; + + if (length != cave->h && length != (cave->y2-cave->y1 + 1)) + Warn("map error: cave height = %d (%d visible), map height = %d", + cave->h, cave->y2 - cave->y1 + 1, length); + + for (iter = maplines, y = 0; y < length && iter != NULL; iter = iter->next, y++) + { + const char *line = iter->data; + int slen = strlen(line); + + if (slen != cave->w && slen != (cave->x2 - cave->x1 + 1)) + Warn("map error in row %d: cave width = %d (%d visible), map width = %d", + y, cave->w, cave->x2 - cave->x1 + 1, slen); + + /* use number of cells from cave or string, whichever is smaller. + so will not overwrite array! */ + for (x = 0; x < MIN(cave->w, slen); x++) + cave->map[y][x] = gd_get_element_from_character (line[x]); + } + } +} + +/* sets the cavesize array to default values */ +static void set_cavesize_defaults(void) +{ + cavesize[0] = 40; + cavesize[1] = 22; + cavesize[2] = 0; + cavesize[3] = 0; + cavesize[4] = 39; + cavesize[5] = 21; +} + +/* sets the cavesize array to default values */ +static void set_intermissionsize_defaults(void) +{ + intermissionsize[0] = 40; + intermissionsize[1] = 22; + intermissionsize[2] = 0; + intermissionsize[3] = 0; + intermissionsize[4] = 19; + intermissionsize[5] = 11; +} + +boolean gd_caveset_load_from_bdcff(const char *contents) +{ + char **lines; + int lineno; + GdCave *cave; + GList *iter; + boolean reading_replay = FALSE; + boolean reading_map = FALSE; + boolean reading_mapcodes = FALSE; + boolean reading_highscore = FALSE; + boolean reading_objects = FALSE; + boolean reading_bdcff_demo = FALSE; + /* assume version to be 0.32, also when the file does not specify it explicitly */ + GdString version_read = "0.32"; + GList *mapstrings = NULL; + int linenum; + GHashTable *tags, *replay_tags; + GdObjectLevels levels = GD_OBJECT_LEVEL_ALL; + GdCave *default_cave; + + gd_caveset_clear(); + + set_cavesize_defaults(); + set_intermissionsize_defaults(); + gd_create_char_to_element_table(); + + tags = g_hash_table_new_full(gd_str_case_hash, gd_str_case_equal, free, free); + replay_tags = g_hash_table_new_full(gd_str_case_hash, gd_str_case_equal, free, free); + + /* split into lines */ + lines = g_strsplit_set (contents, "\n", 0); + + /* attributes read will be set in cave. if no [cave]; they are stored + in the default cave; like in a [game] */ + default_cave = gd_cave_new(); + cave = default_cave; + + linenum = g_strv_length(lines); + + for (lineno = 0; lineno < linenum; lineno++) + { + char *line = lines[lineno]; + char *r; + + /* remove windows-nightmare \r-s */ + while((r = strchr(line, '\r'))) + strcpy(r, r + 1); + + if (strlen (line) == 0) + continue; /* skip empty lines */ + + /* just skip comments. be aware that map lines may start with a semicolon... */ + if (!reading_map && line[0] == ';') + continue; + + /* STARTING WITH A BRACKET [ IS A SECTION */ + if (line[0] == '[') + { + if (strcasecmp(line, "[cave]") == 0) + { + /* new cave */ + if (mapstrings) + { + Warn("incorrect file format: new [cave] section, but already read some map lines"); + g_list_free(mapstrings); + mapstrings = NULL; + } + + /* process any pending tags for game ... */ + cave_process_tags(default_cave, tags, NULL); + + /* ... to be able to create a copy for a new cave. */ + cave = gd_cave_new_from_cave(default_cave); + gd_caveset = g_list_append (gd_caveset, cave); + } + else if (strcasecmp(line, "[/cave]") == 0) + { + cave_process_tags(cave, tags, mapstrings); + g_list_free(mapstrings); + mapstrings = NULL; + + if (g_hash_table_size(tags) != 0) + g_hash_table_foreach(tags, (GHFunc) cave_report_and_copy_unknown_tags_func, cave); + g_hash_table_remove_all(tags); + /* set this to point the pseudo-cave which holds default values */ + cave = default_cave; + } + else if (strcasecmp(line, "[map]") == 0) + { + reading_map = TRUE; + if (mapstrings != NULL) + { + Warn("incorrect file format: new [map] section, but already read some map lines"); + g_list_free(mapstrings); + mapstrings = NULL; + } + } + else if (strcasecmp(line, "[/map]") == 0) + { + reading_map = FALSE; + } + else if (strcasecmp(line, "[mapcodes]") == 0) + { + reading_mapcodes = TRUE; + } + else if (strcasecmp(line, "[/mapcodes]") == 0) + { + reading_mapcodes = FALSE; + } + else if (strcasecmp(line, "[highscore]") == 0) + { + reading_highscore = TRUE; + } + else if (strcasecmp(line, "[/highscore]") == 0) + { + reading_highscore = FALSE; + } + else if (strcasecmp(line, "[objects]") == 0) + { + reading_objects = TRUE; + } + else if (strcasecmp(line, "[/objects]") == 0) + { + reading_objects = FALSE; + } + else if (strcasecmp(line, "[demo]") == 0) + { + GdReplay *replay; + + reading_bdcff_demo = TRUE; + + if (cave != default_cave) + { + replay = gd_replay_new(); + replay->saved = TRUE; + replay->success = TRUE; /* we think that it is a successful demo */ + cave->replays = g_list_append(cave->replays, replay); + gd_strcpy(replay->player_name, "???"); /* name not saved */ + } + else + { + Warn("[demo] section must be in [cave] section!"); + } + } + else if (strcasecmp(line, "[/demo]") == 0) + { + reading_bdcff_demo = FALSE; + } + else if (strcasecmp(line, "[replay]") == 0) + { + reading_replay = TRUE; + } + else if (strcasecmp(line, "[/replay]") == 0) + { + GdReplay *replay; + + reading_replay = FALSE; + replay = gd_replay_new(); + + /* set "saved" flag, so this replay will be written when the caveset is saved again */ + replay->saved = TRUE; + replay_process_tags(replay, replay_tags); + +#if 1 + /* BDCFF numbers levels from 1 to 5, but internally we number levels from 0 to 4 */ + if (replay->level > 0) + replay->level--; +#endif + + /* report any remaining unknown tags */ + g_hash_table_foreach(replay_tags, (GHFunc) replay_report_unknown_tags_func, NULL); + g_hash_table_remove_all(replay_tags); + + if (replay->movements->len != 0) + { + cave->replays = g_list_append(cave->replays, replay); + } + else + { + Warn("no movements in replay!"); + gd_replay_free(replay); + } + } + /* GOSH i hate bdcff */ + else if (strncasecmp(line, "[level=", strlen("[level=")) == 0) + { + int l[5]; + int num; + char *nums; + + /* there IS an equal sign, and we also skip that, so this points to the numbers */ + nums = strchr(line, '=') + 1; + num = sscanf(nums, "%d,%d,%d,%d,%d", l + 0, l + 1, l + 2, l + 3, l + 4); + levels = 0; + + if (num == 0) + { + Warn("invalid Levels tag: %s", line); + levels = GD_OBJECT_LEVEL_ALL; + } + else + { + int n; + + for (n = 0; n < num; n++) + { + if (l[n] <= 5 && l[n] >= 1) + levels |= gd_levels_mask[l[n] - 1]; + else + Warn("invalid level number %d", l[n]); + } + } + } + else if (strcasecmp(line, "[/level]") == 0) + { + levels = GD_OBJECT_LEVEL_ALL; + } + else if (strcasecmp(line, "[game]") == 0) + { + } + else if (strcasecmp(line, "[/game]") == 0) + { + } + else if (strcasecmp(line, "[BDCFF]") == 0) + { + } + else if (strcasecmp(line, "[/BDCFF]") == 0) + { + } + else + { + Warn("unknown section: \"%s\"", line); + } + + continue; + } + + if (reading_map) + { + /* just append to the mapstrings list. we will process it later */ + mapstrings = g_list_append(mapstrings, line); + + continue; + } + + /* strip leading and trailing spaces AFTER checking if we are reading a map. + map lines might begin or end with spaces */ + g_strstrip(line); + + if (reading_highscore) + { + int score; + + if (sscanf(line, "%d", &score) != 1 || strchr(line, ' ') == NULL) + { /* first word is the score */ + Warn("highscore format incorrect"); + } + else + { + if (cave == default_cave) + /* if we are reading the [game], add highscore to that one. */ + /* from first space: the name */ + gd_add_highscore(gd_caveset_data->highscore, strchr(line, ' ') + 1, score); + else + /* if a cave, add highscore to that. */ + gd_add_highscore(cave->highscore, strchr(line, ' ') + 1, score); + } + + continue; + } + + /* read bdcff-style [demo], similar to a complete replay but cannot store like anything */ + if (reading_bdcff_demo) + { + GdReplay *replay; + GList *iter; + + /* demo must be in [cave] section. we already showed an error message for this. */ + if (cave == default_cave) + continue; + + iter = g_list_last(cave->replays); + + replay = (GdReplay *)iter->data; + replay_store_more_from_bdcff(replay, line); + + continue; + } + + if (reading_objects) + { + GdObject *new_object; + + new_object = gd_object_new_from_string(line); + if (new_object) + { + new_object->levels = levels; /* apply levels to new object */ + cave->objects = g_list_append(cave->objects, new_object); + } + else + { + Error("invalid object specification: %s", line); + } + + continue; + } + + /* has an equal sign -> some_attrib = parameters type line. */ + if (strchr (line, '=') != NULL) + { + char *attrib, *param; + + attrib = line; /* attrib is from the first char */ + param = strchr(line, '=') + 1; /* param is after equal sign */ + *strchr (line, '=') = 0; /* delete equal sign - line is therefore splitted */ + + /* own tag: not too much thinking :P */ + if (reading_replay) + { + g_hash_table_insert(replay_tags, g_strdup(attrib), g_strdup(param)); + } + else if (reading_mapcodes) + { + if (strcasecmp("Length", attrib) == 0) + { + /* we do not support map code width != 1 */ + if (strcmp(param, "1") != 0) + Warn(_("Only one-character map codes are currently supported!")); + } + else + { + /* the first character of the attribute is the element code itself */ + gd_char_to_element[(int)attrib[0]] = gd_get_element_from_string(param); + } + } + /* BDCFF version */ + else if (strcasecmp("Version", attrib) == 0) + { + gd_strcpy(version_read, param); + } + /* CAVES = x */ + else if (strcasecmp(attrib, "Caves") == 0) + { + /* BDCFF files sometimes state how many caves they have */ + /* we ignore this field. */ + } + /* LEVELS = x */ + else if (strcasecmp(attrib, "Levels") == 0) + { + /* BDCFF files sometimes state how many levels they have */ + /* we ignore this field. */ + } + else if (strcasecmp(attrib, "CaveSize") == 0) + { + int i; + + i = sscanf(param, "%d %d %d %d %d %d", + cavesize + 0, + cavesize + 1, + cavesize + 2, + cavesize + 3, + cavesize + 4, + cavesize + 5); + + /* allowed: 2 or 6 numbers */ + if (i == 2) + { + cavesize[2] = 0; + cavesize[3] = 0; + cavesize[4] = cavesize[0]-1; + cavesize[5] = cavesize[1]-1; + } + else if (i != 6) + { + set_cavesize_defaults(); + Warn("invalid CaveSize tag: %s", line); + } + } + else if (strcasecmp(attrib, "IntermissionSize") == 0) + { + int i; + + i = sscanf(param, "%d %d %d %d %d %d", + intermissionsize + 0, + intermissionsize + 1, + intermissionsize + 2, + intermissionsize + 3, + intermissionsize + 4, + intermissionsize + 5); + + /* allowed: 2 or 6 numbers */ + if (i == 2) + { + intermissionsize[2] = 0; + intermissionsize[3] = 0; + intermissionsize[4] = intermissionsize[0]-1; + intermissionsize[5] = intermissionsize[1]-1; + } + else if (i != 6) + { + set_intermissionsize_defaults(); + Warn("invalid IntermissionSize tag: '%s'", line); + } + } + else if (strcasecmp(attrib, "Effect") == 0) + { + /* CHECK IF IT IS AN EFFECT */ + char **params; + + params = g_strsplit_set(param, " ", -1); + + /* an effect command has two parameters */ + if (g_strv_length(params) == 2) + { + int i; + + for (i = 0; gd_cave_properties[i].identifier != NULL; i++) + { + /* we have to search for this effect */ + if (gd_cave_properties[i].type == GD_TYPE_EFFECT && + strcasecmp(params[0], gd_cave_properties[i].identifier) == 0) + { + /* found identifier */ + gpointer value = G_STRUCT_MEMBER_P (cave, gd_cave_properties[i].offset); + + *((GdElement *) value) = gd_get_element_from_string (params[1]); + break; + } + } + + /* if we didn't find first element name */ + if (gd_cave_properties[i].identifier == NULL) + { + /* for compatibility with tim stridmann's memorydump->bdcff converter... .... ... */ + if (strcasecmp(params[0], "BOUNCING_BOULDER") == 0) + cave->stone_bouncing_effect = gd_get_element_from_string (params[1]); + else if (strcasecmp(params[0], "EXPLOSION3S") == 0) + cave->explosion_effect = gd_get_element_from_string(params[1]); + /* falling with one l... */ + else if (strcasecmp(params[0], "STARTING_FALING_DIAMOND") == 0) + cave->diamond_falling_effect = gd_get_element_from_string (params[1]); + /* dirt lookslike */ + else if (strcasecmp(params[0], "DIRT") == 0) + cave->dirt_looks_like = gd_get_element_from_string (params[1]); + else if (strcasecmp(params[0], "HEXPANDING_WALL") == 0 && strcasecmp(params[1], "STEEL_HEXPANDING_WALL") == 0) + { + cave->expanding_wall_looks_like = O_STEEL; + } + else + /* didn't find at all */ + Warn("invalid effect name '%s'", params[0]); + } + } + else + Warn("invalid effect specification '%s'", param); + + g_strfreev(params); + } + else + { + /* no special handling: this is a normal attribute. */ + + if (cave == default_cave) + { + /* we are reading the [game] */ + if (attrib_is_valid_for_caveset(attrib)) + { + /* if it is a caveset attrib, process it for the caveset. */ + struct_set_property(gd_caveset_data, gd_caveset_properties, attrib, param, 0); + } + else if (attrib_is_valid_for_cave(attrib)) + { + /* it must be a default setting for all caves. is it a valid identifier? */ + /* yes, it is. add to the hash table, which will be copied for all caves. */ + g_hash_table_insert(tags, g_strdup(attrib), g_strdup(param)); + } + else + /* unknown setting - report. */ + Warn("invalid attribute for [game] '%s'", attrib); + } + else + { + /* we are reading a [cave] */ + /* cave settings are immediately added to cave hash table. */ + /* if it is unknown, we have to remember it, and save it again. */ + g_hash_table_insert(tags, g_strdup(attrib), g_strdup(param)); + } + } + + continue; + } + + Error("cannot parse line: %s", line); + } + + if (mapstrings) + { + Warn("incorrect file format: end of file, but still have some map lines read"); + g_list_free(mapstrings); + mapstrings = NULL; + } + + /* the [game] section had some values which are default if not specified in [cave] sections. */ + /* these are used only for loading, so forget them now */ + if (default_cave->map) + Warn(_("Invalid BDCFF: [game] section has a map")); + if (default_cave->objects) + Warn(_("Invalid BDCFF: [game] section has drawing objects defined")); + + /* cleanup */ + g_strfreev (lines); + g_hash_table_destroy(tags); + g_hash_table_destroy(replay_tags); + gd_cave_free(default_cave); + + /* old bdcff files hack. explanation follows. */ + /* there were 40x22 caves in c64 bd, intermissions were also 40x22, but the visible */ + /* part was the upper left corner, 20x12. 40x22 caves are needed, as 20x12 caves would */ + /* look different (random cave elements needs the correct size.) */ + /* also, in older bdcff files, there is no size= tag. caves default to 40x22 and 20x12. */ + /* even the explicit drawrect and other drawing instructions, which did set up intermissions */ + /* to be 20x12, are deleted. very very bad decision. */ + /* here we try to detect and correct this. */ + + if (strEqual(version_read, "0.32")) + { + GList *iter; + + Warn("No BDCFF version, or 0.32. Using unspecified-intermission-size hack."); + + for (iter = gd_caveset; iter != NULL; iter = iter->next) + { + GdCave *cave = (GdCave *)iter->data; + + /* only applies to intermissions */ + /* not applied to mapped caves, as maps are filled with initial border, if the map read is smaller */ + if (cave->intermission && !cave->map) + { + /* we do not set the cave to 20x12, rather to 40x22 with 20x12 visible. */ + GdObject object; + + cave->w = 40; + cave->h = 22; + cave->x1 = 0; + cave->y1 = 0; + cave->x2 = 19; + cave->y2 = 11; + + /* and cover the invisible area */ + object.type = GD_FILLED_RECTANGLE; + object.x1 = 0; + object.y1 = 11; /* 11, because this will also be the border */ + object.x2 = 39; + object.y2 = 21; + object.element = cave->initial_border; + object.fill_element = cave->initial_border; + + cave->objects = g_list_prepend(cave->objects, g_memdup(&object, sizeof(object))); + + object.x1 = 19; + object.y1 = 0; /* 19, as it is also the border */ + + cave->objects = g_list_prepend(cave->objects, g_memdup(&object, sizeof(object))); /* another */ + } + } + } + + if (!strEqual(version_read, BDCFF_VERSION)) + Warn("BDCFF version %s, loaded caveset may have errors.", version_read); + + /* check for replays which are problematic */ + for (iter = gd_caveset; iter != NULL; iter = iter->next) + gd_cave_check_replays((GdCave *)iter->data, TRUE, FALSE, FALSE); + + /* if there was some error message - return fail XXX */ + return TRUE; +} diff --git a/src/game_bd/bd_bdcff.h b/src/game_bd/bd_bdcff.h new file mode 100644 index 00000000..54fff642 --- /dev/null +++ b/src/game_bd/bd_bdcff.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef BD_BDCFF_H +#define BD_BDCFF_H + +#include + +boolean gd_caveset_load_from_bdcff(const char *contents); +void gd_caveset_save_to_bdcff(GPtrArray *out); + +#endif // BD_BDCFF_H diff --git a/src/game_bd/bd_c64import.c b/src/game_bd/bd_c64import.c new file mode 100644 index 00000000..40764a4b --- /dev/null +++ b/src/game_bd/bd_c64import.c @@ -0,0 +1,2549 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include "main_bd.h" + + +/* make all caves selectable. */ +static boolean gd_import_as_all_caves_selectable = TRUE; + +/* conversion table for imported bd1 caves. */ +static const GdElement bd1_import_table[] = +{ + /* 0 */ O_SPACE, O_DIRT, O_BRICK, O_MAGIC_WALL, + /* 4 */ O_PRE_OUTBOX, O_OUTBOX, O_STEEL_EXPLODABLE, O_STEEL, + /* 8 */ O_FIREFLY_1, O_FIREFLY_2, O_FIREFLY_3, O_FIREFLY_4, + /* c */ O_FIREFLY_1, O_FIREFLY_2, O_FIREFLY_3, O_FIREFLY_4, + /* 10 */ O_STONE, O_STONE, O_STONE_F, O_STONE_F, + /* 14 */ O_DIAMOND, O_DIAMOND, O_DIAMOND_F, O_DIAMOND_F, + /* 18 */ O_ACID, O_ACID, O_EXPLODE_1, O_EXPLODE_2, /* ACID: marek roth extension in crazy dream 3 */ + /* 1c */ O_EXPLODE_3, O_EXPLODE_4, O_EXPLODE_5, O_PRE_DIA_1, + /* 20 */ O_PRE_DIA_2, O_PRE_DIA_3, O_PRE_DIA_4, O_PRE_DIA_5, + /* 24 */ O_PRE_DIA_5, O_INBOX, O_PRE_PL_1, O_PRE_PL_2, + /* 28 */ O_PRE_PL_3, O_PRE_PL_3, O_H_EXPANDING_WALL, O_H_EXPANDING_WALL, + /* 2c */ O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, + /* 30 */ O_BUTTER_4, O_BUTTER_1, O_BUTTER_2, O_BUTTER_3, + /* 34 */ O_BUTTER_4, O_BUTTER_1, O_BUTTER_2, O_BUTTER_3, + /* 38 */ O_PLAYER, O_PLAYER, O_AMOEBA, O_AMOEBA, + /* 3c */ O_VOODOO, O_INVIS_OUTBOX, O_SLIME, O_UNKNOWN +}; + +/* conversion table for imported plck caves. */ +static const GdElement plck_import_nybble[] = +{ + /* 0 */ O_STONE, O_DIAMOND, O_MAGIC_WALL, O_BRICK, + /* 4 */ O_STEEL, O_H_EXPANDING_WALL, O_VOODOO, O_DIRT, + /* 8 */ O_FIREFLY_1, O_BUTTER_4, O_AMOEBA, O_SLIME, + /* 12 */ O_PRE_INVIS_OUTBOX, O_PRE_OUTBOX, O_INBOX, O_SPACE +}; + +/* conversion table for imported 1stb caves. */ +static const GdElement firstboulder_import_table[] = +{ + /* 0 */ O_SPACE, O_DIRT, O_BRICK, O_MAGIC_WALL, + /* 4 */ O_PRE_OUTBOX, O_OUTBOX, O_PRE_INVIS_OUTBOX, O_INVIS_OUTBOX, + /* 8 */ O_FIREFLY_1, O_FIREFLY_2, O_FIREFLY_3, O_FIREFLY_4, + /* c */ O_FIREFLY_1, O_FIREFLY_2, O_FIREFLY_3, O_FIREFLY_4, + /* 10 */ O_STONE, O_STONE, O_STONE_F, O_STONE_F, + /* 14 */ O_DIAMOND, O_DIAMOND, O_DIAMOND_F, O_DIAMOND_F, + /* 18 */ O_PRE_CLOCK_1, O_PRE_CLOCK_2, O_PRE_CLOCK_3, O_PRE_CLOCK_4, + /* 1c */ O_BITER_SWITCH, O_BITER_SWITCH, O_BLADDER_SPENDER, O_PRE_DIA_1, + /* 20 */ O_PRE_DIA_1, O_PRE_DIA_2, O_PRE_DIA_3, O_PRE_DIA_4, + /* 24 */ O_PRE_DIA_5, O_INBOX, O_PRE_PL_1, O_PRE_PL_2, + /* 28 */ O_PRE_PL_3, O_CLOCK, O_H_EXPANDING_WALL, O_H_EXPANDING_WALL, /* CLOCK: not mentioned in marek's bd inside faq */ + /* 2c */ O_CREATURE_SWITCH, O_CREATURE_SWITCH, O_EXPANDING_WALL_SWITCH, O_EXPANDING_WALL_SWITCH, + /* 30 */ O_BUTTER_3, O_BUTTER_4, O_BUTTER_1, O_BUTTER_2, + /* 34 */ O_BUTTER_3, O_BUTTER_4, O_BUTTER_1, O_BUTTER_2, + /* 38 */ O_STEEL, O_SLIME, O_BOMB, O_SWEET, + /* 3c */ O_PRE_STONE_1, O_PRE_STONE_2, O_PRE_STONE_3, O_PRE_STONE_4, + /* 40 */ O_BLADDER, O_BLADDER_1, O_BLADDER_2, O_BLADDER_3, + /* 44 */ O_BLADDER_4, O_BLADDER_5, O_BLADDER_6, O_BLADDER_7, + /* 48 */ O_BLADDER_8, O_BLADDER_8, O_EXPLODE_1, O_EXPLODE_1, + /* 4c */ O_EXPLODE_2, O_EXPLODE_3, O_EXPLODE_4, O_EXPLODE_5, + /* 50 */ O_PLAYER, O_PLAYER, O_PLAYER_BOMB, O_PLAYER_BOMB, + /* 54 */ O_PLAYER_GLUED, O_PLAYER_GLUED, O_VOODOO, O_AMOEBA, + /* 58 */ O_AMOEBA, O_BOMB_TICK_1, O_BOMB_TICK_2, O_BOMB_TICK_3, + /* 5c */ O_BOMB_TICK_4, O_BOMB_TICK_5, O_BOMB_TICK_6, O_BOMB_TICK_7, + /* 60 */ O_BOMB_EXPL_1, O_BOMB_EXPL_2, O_BOMB_EXPL_3, O_BOMB_EXPL_4, + /* 64 */ O_GHOST, O_GHOST, O_GHOST_EXPL_1, O_GHOST_EXPL_2, + /* 68 */ O_GHOST_EXPL_3, O_GHOST_EXPL_4, O_GRAVESTONE, O_STONE_GLUED, + /* 6c */ O_DIAMOND_GLUED, O_DIAMOND_KEY, O_TRAPPED_DIAMOND, O_GRAVESTONE, + /* 70 */ O_WAITING_STONE, O_WAITING_STONE, O_CHASING_STONE, O_CHASING_STONE, + /* 74 */ O_PRE_STEEL_1, O_PRE_STEEL_2, O_PRE_STEEL_3, O_PRE_STEEL_4, + /* 78 */ O_BITER_1, O_BITER_2, O_BITER_3, O_BITER_4, + /* 7c */ O_BITER_1, O_BITER_2, O_BITER_3, O_BITER_4, +}; + +/* conversion table for imported crazy dream caves. */ +static const GdElement crazydream_import_table[] = +{ + /* 0 */ O_SPACE, O_DIRT, O_BRICK, O_MAGIC_WALL, + /* 4 */ O_PRE_OUTBOX, O_OUTBOX, O_PRE_INVIS_OUTBOX, O_INVIS_OUTBOX, + /* 8 */ O_FIREFLY_1, O_FIREFLY_2, O_FIREFLY_3, O_FIREFLY_4, + /* c */ O_FIREFLY_1, O_FIREFLY_2, O_FIREFLY_3, O_FIREFLY_4, + /* 10 */ O_STONE, O_STONE, O_STONE_F, O_STONE_F, + /* 14 */ O_DIAMOND, O_DIAMOND, O_DIAMOND_F, O_DIAMOND_F, + /* 18 */ O_PRE_CLOCK_1, O_PRE_CLOCK_2, O_PRE_CLOCK_3, O_PRE_CLOCK_4, + /* 1c */ O_BITER_SWITCH, O_BITER_SWITCH, O_BLADDER_SPENDER, O_PRE_DIA_1, /* 6 different stages */ + /* 20 */ O_PRE_DIA_1, O_PRE_DIA_2, O_PRE_DIA_3, O_PRE_DIA_4, + /* 24 */ O_PRE_DIA_5, O_INBOX, O_PRE_PL_1, O_PRE_PL_2, + /* 28 */ O_PRE_PL_3, O_CLOCK, O_H_EXPANDING_WALL, O_H_EXPANDING_WALL, /* CLOCK: not mentioned in marek's bd inside faq */ + /* 2c */ O_CREATURE_SWITCH, O_CREATURE_SWITCH, O_EXPANDING_WALL_SWITCH, O_EXPANDING_WALL_SWITCH, + /* 30 */ O_BUTTER_3, O_BUTTER_4, O_BUTTER_1, O_BUTTER_2, + /* 34 */ O_BUTTER_3, O_BUTTER_4, O_BUTTER_1, O_BUTTER_2, + /* 38 */ O_STEEL, O_SLIME, O_BOMB, O_SWEET, + /* 3c */ O_PRE_STONE_1, O_PRE_STONE_2, O_PRE_STONE_3, O_PRE_STONE_4, + /* 40 */ O_BLADDER, O_BLADDER_1, O_BLADDER_2, O_BLADDER_3, + /* 44 */ O_BLADDER_4, O_BLADDER_5, O_BLADDER_6, O_BLADDER_7, + /* 48 */ O_BLADDER_8, O_BLADDER_8|SCANNED, O_EXPLODE_1, O_EXPLODE_1, + /* 4c */ O_EXPLODE_2, O_EXPLODE_3, O_EXPLODE_4, O_EXPLODE_5, + /* 50 */ O_PLAYER, O_PLAYER, O_PLAYER_BOMB, O_PLAYER_BOMB, + /* 54 */ O_PLAYER_GLUED, O_PLAYER_GLUED, O_VOODOO, O_AMOEBA, + /* 58 */ O_AMOEBA, O_BOMB_TICK_1, O_BOMB_TICK_2, O_BOMB_TICK_3, + /* 5c */ O_BOMB_TICK_4, O_BOMB_TICK_5, O_BOMB_TICK_6, O_BOMB_TICK_7, + /* 60 */ O_BOMB_EXPL_1, O_BOMB_EXPL_2, O_BOMB_EXPL_3, O_BOMB_EXPL_4, + /* 64 */ O_GHOST, O_GHOST, O_GHOST_EXPL_1, O_GHOST_EXPL_2, + /* 68 */ O_GHOST_EXPL_3, O_GHOST_EXPL_4, O_GRAVESTONE, O_STONE_GLUED, + /* 6c */ O_DIAMOND_GLUED, O_DIAMOND_KEY, O_TRAPPED_DIAMOND, O_GRAVESTONE, + /* 70 */ O_WAITING_STONE, O_WAITING_STONE, O_CHASING_STONE, O_CHASING_STONE, + /* 74 */ O_PRE_STEEL_1, O_PRE_STEEL_2, O_PRE_STEEL_3, O_PRE_STEEL_4, + /* 78 */ O_BITER_1, O_BITER_2, O_BITER_3, O_BITER_4, + /* 7c */ O_BITER_1, O_BITER_2, O_BITER_3, O_BITER_4, + + /* 80 */ O_POT, O_PLAYER_STIRRING, O_GRAVITY_SWITCH, O_GRAVITY_SWITCH, + /* 84 */ O_PNEUMATIC_HAMMER, O_PNEUMATIC_HAMMER, O_BOX, O_BOX, + /* 88 */ O_UNKNOWN, O_UNKNOWN, O_ACID, O_ACID, + /* 8c */ O_KEY_1, O_KEY_2, O_KEY_3, O_UNKNOWN, + /* 90 */ O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, + /* 94 */ O_UNKNOWN, O_TELEPORTER, O_UNKNOWN, O_SKELETON, + /* 98 */ O_WATER, O_WATER_16, O_WATER_15, O_WATER_14, + /* 9c */ O_WATER_13, O_WATER_12, O_WATER_11, O_WATER_10, + /* a0 */ O_WATER_9, O_WATER_8, O_WATER_7, O_WATER_6, + /* a4 */ O_WATER_5, O_WATER_4, O_WATER_3, O_WATER_2, + /* a8 */ O_WATER_1, O_COW_ENCLOSED_1, O_COW_ENCLOSED_2, O_COW_ENCLOSED_3, + /* ac */ O_COW_ENCLOSED_4, O_COW_ENCLOSED_5, O_COW_ENCLOSED_6, O_COW_ENCLOSED_7, + /* b0 */ O_COW_1, O_COW_2, O_COW_3, O_COW_4, + /* b4 */ O_COW_1, O_COW_2, O_COW_3, O_COW_4, + /* b8 */ O_DIRT_GLUED, O_STEEL_EXPLODABLE, O_DOOR_1, O_DOOR_2, + /* bc */ O_DOOR_3, O_FALLING_WALL, O_FALLING_WALL_F, O_FALLING_WALL_F, + /* c0 */ O_WALLED_DIAMOND, O_UNKNOWN, O_WALLED_KEY_1, O_WALLED_KEY_2, + /* c5 = brick?! (vital key), c7 = dirt?! (think twice) */ + /* c7 = dirt, as it has a code which will change it to dirt. */ + /* c4 */ O_WALLED_KEY_3, O_BRICK, O_UNKNOWN, O_DIRT, + /* c8 */ O_DIRT2, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, + /* cc */ O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, + /* d0 */ O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, + /* d4 */ O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, + /* d8 */ O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, + /* dc */ O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, + /* e0 */ O_ALT_FIREFLY_1, O_ALT_FIREFLY_2, O_ALT_FIREFLY_3, O_ALT_FIREFLY_4, + /* e4 */ O_ALT_FIREFLY_1, O_ALT_FIREFLY_2, O_ALT_FIREFLY_3, O_ALT_FIREFLY_4, + /* e8 */ O_ALT_BUTTER_3, O_ALT_BUTTER_4, O_ALT_BUTTER_1, O_ALT_BUTTER_2, + /* ec */ O_ALT_BUTTER_3, O_ALT_BUTTER_4, O_ALT_BUTTER_1, O_ALT_BUTTER_2, + /* f0 */ O_WATER, O_WATER, O_WATER, O_WATER, + /* f4 */ O_WATER, O_WATER, O_WATER, O_WATER, + /* f8 */ O_WATER, O_WATER, O_WATER, O_WATER, + /* fc */ O_WATER, O_WATER, O_WATER, O_WATER, +}; + +/* conversion table for imported 1stb caves. */ +const GdElement gd_crazylight_import_table[] = +{ + /* 0 */ O_SPACE, O_DIRT, O_BRICK, O_MAGIC_WALL, + /* 4 */ O_PRE_OUTBOX, O_OUTBOX, O_PRE_INVIS_OUTBOX, O_INVIS_OUTBOX, + /* 8 */ O_FIREFLY_1, O_FIREFLY_2, O_FIREFLY_3, O_FIREFLY_4, + /* c */ O_FIREFLY_1|SCANNED, O_FIREFLY_2|SCANNED, O_FIREFLY_3|SCANNED, O_FIREFLY_4|SCANNED, + /* 10 */ O_STONE, O_STONE|SCANNED, O_STONE_F, O_STONE_F|SCANNED, + /* 14 */ O_DIAMOND, O_DIAMOND|SCANNED, O_DIAMOND_F, O_DIAMOND_F|SCANNED, + /* 18 */ O_PRE_CLOCK_1, O_PRE_CLOCK_2, O_PRE_CLOCK_3, O_PRE_CLOCK_4, + /* 1c */ O_BITER_SWITCH, O_BITER_SWITCH, O_BLADDER_SPENDER, O_PRE_DIA_1, /* 6 different stages, the first is the pre_dia_0 */ + /* 20 */ O_PRE_DIA_1, O_PRE_DIA_2, O_PRE_DIA_3, O_PRE_DIA_4, + /* 24 */ O_PRE_DIA_5, O_INBOX, O_PRE_PL_1, O_PRE_PL_2, + /* 28 */ O_PRE_PL_3, O_CLOCK, O_H_EXPANDING_WALL, O_H_EXPANDING_WALL|SCANNED, /* CLOCK: not mentioned in marek's bd inside faq */ + /* 2c */ O_CREATURE_SWITCH, O_CREATURE_SWITCH, O_EXPANDING_WALL_SWITCH, O_EXPANDING_WALL_SWITCH, + /* 30 */ O_BUTTER_3, O_BUTTER_4, O_BUTTER_1, O_BUTTER_2, + /* 34 */ O_BUTTER_3|SCANNED, O_BUTTER_4|SCANNED, O_BUTTER_1|SCANNED, O_BUTTER_2|SCANNED, + /* 38 */ O_STEEL, O_SLIME, O_BOMB, O_SWEET, + /* 3c */ O_PRE_STONE_1, O_PRE_STONE_2, O_PRE_STONE_3, O_PRE_STONE_4, + /* 40 */ O_BLADDER, O_BLADDER_1, O_BLADDER_2, O_BLADDER_3, + /* 44 */ O_BLADDER_4, O_BLADDER_5, O_BLADDER_6, O_BLADDER_7, + /* 48 */ O_BLADDER_8, O_BLADDER_8|SCANNED, O_EXPLODE_1, O_EXPLODE_1, + /* 4c */ O_EXPLODE_2, O_EXPLODE_3, O_EXPLODE_4, O_EXPLODE_5, + /* 50 */ O_PLAYER, O_PLAYER|SCANNED, O_PLAYER_BOMB, O_PLAYER_BOMB|SCANNED, + /* 54 */ O_PLAYER_GLUED, O_PLAYER_GLUED|SCANNED, O_VOODOO, O_AMOEBA, + /* 58 */ O_AMOEBA|SCANNED, O_BOMB_TICK_1, O_BOMB_TICK_2, O_BOMB_TICK_3, + /* 5c */ O_BOMB_TICK_4, O_BOMB_TICK_5, O_BOMB_TICK_6, O_BOMB_TICK_7, + /* 60 */ O_BOMB_EXPL_1, O_BOMB_EXPL_2, O_BOMB_EXPL_3, O_BOMB_EXPL_4, + /* 64 */ O_ACID, O_ACID, O_FALLING_WALL, O_FALLING_WALL_F, + /* 68 */ O_FALLING_WALL_F|SCANNED, O_BOX, O_GRAVESTONE, O_STONE_GLUED, + /* 6c */ O_DIAMOND_GLUED, O_DIAMOND_KEY, O_TRAPPED_DIAMOND, O_GRAVESTONE, + /* 70 */ O_WAITING_STONE, O_WAITING_STONE|SCANNED, O_CHASING_STONE, O_CHASING_STONE|SCANNED, + /* 74 */ O_PRE_STEEL_1, O_PRE_STEEL_2, O_PRE_STEEL_3, O_PRE_STEEL_4, + /* 78 */ O_BITER_1, O_BITER_2, O_BITER_3, O_BITER_4, + /* 7c */ O_BITER_1|SCANNED, O_BITER_2|SCANNED, O_BITER_3|SCANNED, O_BITER_4|SCANNED, +}; + +GdPropertyDefault gd_defaults_bd1[] = +{ + {CAVE_OFFSET(level_amoeba_threshold), 200}, + {CAVE_OFFSET(amoeba_growth_prob), 31250}, + {CAVE_OFFSET(amoeba_fast_growth_prob), 250000}, + {CAVE_OFFSET(amoeba_timer_started_immediately), TRUE}, + {CAVE_OFFSET(amoeba_timer_wait_for_hatching), FALSE}, + {CAVE_OFFSET(lineshift), TRUE}, + {CAVE_OFFSET(wraparound_objects), TRUE}, + {CAVE_OFFSET(diagonal_movements), FALSE}, + {CAVE_OFFSET(voodoo_collects_diamonds), FALSE}, + {CAVE_OFFSET(voodoo_dies_by_stone), FALSE}, + {CAVE_OFFSET(voodoo_disappear_in_explosion), TRUE}, + {CAVE_OFFSET(voodoo_any_hurt_kills_player), FALSE}, + {CAVE_OFFSET(creatures_backwards), FALSE}, + {CAVE_OFFSET(creatures_direction_auto_change_on_start), FALSE}, + {CAVE_OFFSET(creatures_direction_auto_change_time), 0}, + {CAVE_OFFSET(level_hatching_delay_time[0]), 2}, + {CAVE_OFFSET(intermission_instantlife), TRUE}, + {CAVE_OFFSET(intermission_rewardlife), FALSE}, + {CAVE_OFFSET(magic_wall_stops_amoeba), TRUE}, + {CAVE_OFFSET(magic_timer_wait_for_hatching), FALSE}, + {CAVE_OFFSET(pushing_stone_prob), 250000}, + {CAVE_OFFSET(pushing_stone_prob_sweet), 1000000}, + {CAVE_OFFSET(active_is_first_found), FALSE}, + {CAVE_OFFSET(short_explosions), TRUE}, + {CAVE_OFFSET(slime_predictable), TRUE}, + {CAVE_OFFSET(snap_element), O_SPACE}, + {CAVE_OFFSET(max_time), 999}, + + {CAVE_OFFSET(scheduling), GD_SCHEDULING_BD1}, + {CAVE_OFFSET(pal_timing), TRUE}, + + {-1}, +}; + +GdPropertyDefault gd_defaults_bd2[] = +{ + {CAVE_OFFSET(level_amoeba_threshold), 200}, + {CAVE_OFFSET(amoeba_growth_prob), 31250}, + {CAVE_OFFSET(amoeba_fast_growth_prob), 250000}, + {CAVE_OFFSET(amoeba_timer_started_immediately), FALSE}, + {CAVE_OFFSET(amoeba_timer_wait_for_hatching), FALSE}, + {CAVE_OFFSET(lineshift), TRUE}, + {CAVE_OFFSET(wraparound_objects), TRUE}, + {CAVE_OFFSET(diagonal_movements), FALSE}, + {CAVE_OFFSET(voodoo_collects_diamonds), FALSE}, + {CAVE_OFFSET(voodoo_dies_by_stone), FALSE}, + {CAVE_OFFSET(voodoo_disappear_in_explosion), TRUE}, + {CAVE_OFFSET(voodoo_any_hurt_kills_player), FALSE}, + {CAVE_OFFSET(creatures_backwards), FALSE}, + {CAVE_OFFSET(creatures_direction_auto_change_on_start), FALSE}, + {CAVE_OFFSET(creatures_direction_auto_change_time), 0}, + {CAVE_OFFSET(level_hatching_delay_time[0]), 2}, + {CAVE_OFFSET(intermission_instantlife), TRUE}, + {CAVE_OFFSET(intermission_rewardlife), FALSE}, + {CAVE_OFFSET(magic_wall_stops_amoeba), FALSE}, /* marek roth bd inside faq 3.0 */ + {CAVE_OFFSET(magic_timer_wait_for_hatching), FALSE}, + {CAVE_OFFSET(pushing_stone_prob), 250000}, + {CAVE_OFFSET(pushing_stone_prob_sweet), 1000000}, + {CAVE_OFFSET(active_is_first_found), FALSE}, + {CAVE_OFFSET(short_explosions), TRUE}, + {CAVE_OFFSET(slime_predictable), TRUE}, + {CAVE_OFFSET(snap_element), O_SPACE}, + {CAVE_OFFSET(max_time), 999}, + + {CAVE_OFFSET(pal_timing), TRUE}, + {CAVE_OFFSET(scheduling), GD_SCHEDULING_BD2}, + + {-1}, +}; + +GdPropertyDefault gd_defaults_plck[] = +{ + {CAVE_OFFSET(amoeba_growth_prob), 31250}, + {CAVE_OFFSET(amoeba_fast_growth_prob), 250000}, + {CAVE_OFFSET(amoeba_timer_started_immediately), FALSE}, + {CAVE_OFFSET(amoeba_timer_wait_for_hatching), FALSE}, + {CAVE_OFFSET(lineshift), TRUE}, + {CAVE_OFFSET(wraparound_objects), TRUE}, + {CAVE_OFFSET(border_scan_first_and_last), FALSE}, + {CAVE_OFFSET(diagonal_movements), FALSE}, + {CAVE_OFFSET(voodoo_collects_diamonds), FALSE}, + {CAVE_OFFSET(voodoo_dies_by_stone), FALSE}, + {CAVE_OFFSET(voodoo_disappear_in_explosion), TRUE}, + {CAVE_OFFSET(voodoo_any_hurt_kills_player), FALSE}, + {CAVE_OFFSET(creatures_backwards), FALSE}, + {CAVE_OFFSET(creatures_direction_auto_change_on_start), FALSE}, + {CAVE_OFFSET(creatures_direction_auto_change_time), 0}, + {CAVE_OFFSET(level_hatching_delay_time[0]), 2}, + {CAVE_OFFSET(intermission_instantlife), TRUE}, + {CAVE_OFFSET(intermission_rewardlife), FALSE}, + {CAVE_OFFSET(magic_wall_stops_amoeba), FALSE}, + {CAVE_OFFSET(magic_timer_wait_for_hatching), FALSE}, + {CAVE_OFFSET(pushing_stone_prob), 250000}, + {CAVE_OFFSET(pushing_stone_prob_sweet), 1000000}, + {CAVE_OFFSET(active_is_first_found), FALSE}, + {CAVE_OFFSET(short_explosions), TRUE}, + {CAVE_OFFSET(snap_element), O_SPACE}, + {CAVE_OFFSET(max_time), 999}, + + {CAVE_OFFSET(pal_timing), TRUE}, + {CAVE_OFFSET(scheduling), GD_SCHEDULING_PLCK}, + + {-1}, +}; + +GdPropertyDefault gd_defaults_1stb[] = +{ + {CAVE_OFFSET(amoeba_growth_prob), 31250}, + {CAVE_OFFSET(amoeba_fast_growth_prob), 250000}, + {CAVE_OFFSET(amoeba_timer_started_immediately), FALSE}, + {CAVE_OFFSET(amoeba_timer_wait_for_hatching), TRUE}, + {CAVE_OFFSET(lineshift), TRUE}, + {CAVE_OFFSET(wraparound_objects), TRUE}, + {CAVE_OFFSET(voodoo_collects_diamonds), TRUE}, + {CAVE_OFFSET(voodoo_dies_by_stone), TRUE}, + {CAVE_OFFSET(voodoo_disappear_in_explosion), FALSE}, + {CAVE_OFFSET(voodoo_any_hurt_kills_player), FALSE}, + {CAVE_OFFSET(creatures_direction_auto_change_on_start), TRUE}, + {CAVE_OFFSET(level_hatching_delay_time[0]), 2}, + {CAVE_OFFSET(intermission_instantlife), FALSE}, + {CAVE_OFFSET(intermission_rewardlife), TRUE}, + {CAVE_OFFSET(magic_timer_wait_for_hatching), TRUE}, + {CAVE_OFFSET(pushing_stone_prob), 250000}, + {CAVE_OFFSET(pushing_stone_prob_sweet), 1000000}, + {CAVE_OFFSET(active_is_first_found), TRUE}, + {CAVE_OFFSET(short_explosions), FALSE}, + {CAVE_OFFSET(slime_predictable), TRUE}, + {CAVE_OFFSET(snap_element), O_SPACE}, + {CAVE_OFFSET(max_time), 999}, + + {CAVE_OFFSET(pal_timing), TRUE}, + {CAVE_OFFSET(scheduling), GD_SCHEDULING_PLCK}, + {CAVE_OFFSET(amoeba_enclosed_effect), O_PRE_DIA_1}, /* not immediately to diamond, but with animation */ + {CAVE_OFFSET(dirt_looks_like), O_DIRT2}, + + {-1}, +}; + +GdPropertyDefault gd_defaults_crdr_7[] = +{ + {CAVE_OFFSET(amoeba_growth_prob), 31250}, + {CAVE_OFFSET(amoeba_fast_growth_prob), 250000}, + {CAVE_OFFSET(amoeba_timer_started_immediately), FALSE}, + {CAVE_OFFSET(amoeba_timer_wait_for_hatching), TRUE}, + {CAVE_OFFSET(lineshift), TRUE}, + {CAVE_OFFSET(wraparound_objects), TRUE}, + {CAVE_OFFSET(voodoo_collects_diamonds), TRUE}, + {CAVE_OFFSET(voodoo_dies_by_stone), TRUE}, + {CAVE_OFFSET(voodoo_disappear_in_explosion), FALSE}, + {CAVE_OFFSET(voodoo_any_hurt_kills_player), FALSE}, + {CAVE_OFFSET(creatures_direction_auto_change_on_start), FALSE}, + {CAVE_OFFSET(level_hatching_delay_time[0]), 2}, + {CAVE_OFFSET(intermission_instantlife), FALSE}, + {CAVE_OFFSET(intermission_rewardlife), TRUE}, + {CAVE_OFFSET(magic_timer_wait_for_hatching), TRUE}, + {CAVE_OFFSET(pushing_stone_prob), 250000}, + {CAVE_OFFSET(pushing_stone_prob_sweet), 1000000}, + {CAVE_OFFSET(active_is_first_found), TRUE}, + {CAVE_OFFSET(short_explosions), FALSE}, + {CAVE_OFFSET(slime_predictable), TRUE}, + {CAVE_OFFSET(snap_element), O_SPACE}, + {CAVE_OFFSET(max_time), 999}, + + {CAVE_OFFSET(pal_timing), TRUE}, + {CAVE_OFFSET(scheduling), GD_SCHEDULING_CRDR}, + {CAVE_OFFSET(amoeba_enclosed_effect), O_PRE_DIA_1}, /* not immediately to diamond, but with animation */ + {CAVE_OFFSET(water_does_not_flow_down), TRUE}, + {CAVE_OFFSET(skeletons_worth_diamonds), 1}, /* in crdr, skeletons can also be used to open the gate */ + {CAVE_OFFSET(gravity_affects_all), FALSE}, /* the intermission "survive" needs this flag */ + + {-1}, +}; + +GdPropertyDefault gd_defaults_crli[] = +{ + {CAVE_OFFSET(amoeba_growth_prob), 31250}, + {CAVE_OFFSET(amoeba_fast_growth_prob), 250000}, + {CAVE_OFFSET(amoeba_timer_started_immediately), FALSE}, + {CAVE_OFFSET(amoeba_timer_wait_for_hatching), TRUE}, + {CAVE_OFFSET(lineshift), TRUE}, + {CAVE_OFFSET(wraparound_objects), TRUE}, + {CAVE_OFFSET(voodoo_collects_diamonds), TRUE}, + {CAVE_OFFSET(voodoo_dies_by_stone), TRUE}, + {CAVE_OFFSET(voodoo_disappear_in_explosion), FALSE}, + {CAVE_OFFSET(voodoo_any_hurt_kills_player), FALSE}, + {CAVE_OFFSET(creatures_direction_auto_change_on_start), FALSE}, + {CAVE_OFFSET(level_hatching_delay_time[0]), 2}, + {CAVE_OFFSET(intermission_instantlife), FALSE}, + {CAVE_OFFSET(intermission_rewardlife), TRUE}, + {CAVE_OFFSET(magic_timer_wait_for_hatching), TRUE}, + {CAVE_OFFSET(pushing_stone_prob), 250000}, + {CAVE_OFFSET(pushing_stone_prob_sweet), 1000000}, + {CAVE_OFFSET(active_is_first_found), TRUE}, + {CAVE_OFFSET(short_explosions), FALSE}, + {CAVE_OFFSET(slime_predictable), TRUE}, + {CAVE_OFFSET(max_time), 999}, + + {CAVE_OFFSET(pal_timing), TRUE}, + {CAVE_OFFSET(scheduling), GD_SCHEDULING_PLCK}, + {CAVE_OFFSET(amoeba_enclosed_effect), O_PRE_DIA_1}, /* not immediately to diamond, but with animation */ + + {-1}, +}; + +/* internal character (letter) codes in c64 games. + missing: "triple line" after >, diamond between ()s, player's head after ) + used for converting names of caves imported from crli and other types of binary data */ +const char gd_bd_internal_chars[] = " ,!./0123456789:*<=> ABCDEFGHIJKLMNOPQRSTUVWXYZ( ) _"; + +/* used for bdcff engine flag. */ +const char *gd_engines[] = +{ + "BD1", + "BD2", + "PLCK", + "1stB", + "CrDr", + "CrLi" +}; + +/* to convert predictable slime values to bit masks */ +static int slime_shift_msb(int c64_data) +{ + int i, perm; + + perm = 0; + + for (i = 0; i < c64_data; i++) + /* shift in this many msb 1's */ + perm = (0x100|perm) >> 1; + + return perm; +} + +static GdElement bd1_import(guint8 c, int i) +{ + if (c < G_N_ELEMENTS(bd1_import_table)) + return bd1_import_table[c]; + + Warn("Invalid BD1 element in imported file at cave data %d: %d", i, c); + + return O_UNKNOWN; +} + +/* deluxe caves 1 contained a special element, non-sloped brick. */ +static GdElement deluxecaves_1_import(guint8 c, int i) +{ + GdElement e = bd1_import(c, i); + + if (e == O_H_EXPANDING_WALL) + e = O_BRICK_NON_SLOPED; + + return e; +} + +static GdElement firstboulder_import(guint8 c, int i) +{ + if (c < G_N_ELEMENTS(firstboulder_import_table)) + return firstboulder_import_table[c]; + + Warn("Invalid 1stB element in imported file at cave data %d: %d", i, c); + + return O_UNKNOWN; +} + +static GdElement crazylight_import(guint8 c, int i) +{ + if (c < G_N_ELEMENTS(gd_crazylight_import_table)) + return gd_crazylight_import_table[c] & O_MASK; /* & O_MASK: do not import "scanned" flag */ + + Warn("Invalid CrLi element in imported file at cave data %d: %d", i, c); + + return O_UNKNOWN; +} + +GdPropertyDefault *gd_get_engine_default_array(GdEngine engine) +{ + switch(engine) + { + case GD_ENGINE_BD1: + return gd_defaults_bd1; + break; + + case GD_ENGINE_BD2: + return gd_defaults_bd2; + break; + + case GD_ENGINE_PLCK: + return gd_defaults_plck; + break; + + case GD_ENGINE_1STB: + return gd_defaults_1stb; + break; + + case GD_ENGINE_CRDR7: + return gd_defaults_crdr_7; + break; + + case GD_ENGINE_CRLI: + return gd_defaults_crli; + break; + + /* to avoid compiler warning */ + case GD_ENGINE_INVALID: + break; + } + + return gd_defaults_bd1; +} + +void gd_cave_set_engine_defaults(GdCave *cave, GdEngine engine) +{ + gd_cave_set_defaults_from_array(cave, gd_get_engine_default_array(engine)); + + /* these have hardcoded ckdelay. */ + /* setting this ckdelay array does not fit into the gd_struct_default scheme. */ + if (engine == GD_ENGINE_BD1) + { + cave->level_ckdelay[0] = 12; + cave->level_ckdelay[1] = 6; + cave->level_ckdelay[2] = 3; + cave->level_ckdelay[3] = 1; + cave->level_ckdelay[4] = 0; + } + + if (engine == GD_ENGINE_BD2) + { + cave->level_ckdelay[0] = 9; /* 180ms */ + cave->level_ckdelay[1] = 8; /* 160ms */ + cave->level_ckdelay[2] = 7; /* 140ms */ + cave->level_ckdelay[3] = 6; /* 120ms */ + cave->level_ckdelay[4] = 6; /* 120ms (!) not faster than level4 */ + } +} + +GdEngine gd_cave_get_engine_from_string(const char *param) +{ + int i; + + for (i = 0; i < GD_ENGINE_INVALID; i++) + if (strcasecmp(param, gd_engines[i]) == 0) + return (GdEngine)i; + + return GD_ENGINE_INVALID; +} + +/**************************************************************************** + * + * cave import routines. + * take a cave, data, and maybe remaining bytes. + * return the number of bytes read, -1 if error. + * + ****************************************************************************/ + +/* + take care of required diamonds values == 0 or > 100. + in original bd, the counter was only two-digit. so bd3 cave f + says 150 diamonds required, but you only had to collect 50. + also, gate opening is triggered by incrementing diamond + count and THEN checking if more required; so if required was + 0, you had to collect 100. (also check crazy light 8 cave "1000") + + http://www.boulder-dash.nl/forum/viewtopic.php?t=88 +*/ + +/* import bd1 cave data into our format. */ +static int cave_copy_from_bd1(GdCave *cave, const guint8 *data, int remaining_bytes, + GdCavefileFormat format) +{ + int length, direction; + int index; + int level; + int x1, y1, x2, y2; + guint8 code; + GdElement elem; + GdElement (* import_func) (guint8 c, int i); + int i; + + /* cant be shorted than this: header + no objects + delimiter */ + if (remaining_bytes < 33) + { + Error("truncated BD1 cave data, %d bytes", remaining_bytes); + + return -1; + } + + gd_cave_set_engine_defaults(cave, GD_ENGINE_BD1); + + if (format == GD_FORMAT_BD1_ATARI) + cave->scheduling = GD_SCHEDULING_BD1_ATARI; + + if (format == GD_FORMAT_DC1) + import_func = deluxecaves_1_import; + else + import_func = bd1_import; + + /* set visible size for intermission */ + if (cave->intermission) + { + cave->x2 = 19; + cave->y2 = 11; + } + + /* cave number data[0] */ + cave->diamond_value = data[2]; + cave->extra_diamond_value = data[3]; + + for (level = 0; level < 5; level++) + { + cave->level_amoeba_time[level] = data[1]; + + /* 0 immediately underflowed to 999, so we use 999. example: sendydash 3, cave 02. */ + if (cave->level_amoeba_time[level] == 0) + cave->level_amoeba_time[level] = 999; + + cave->level_magic_wall_time[level] = data[1]; + cave->level_rand[level] = data[4 + level]; + cave->level_diamonds[level] = data[9 + level] % 100; /* check comment above */ + + /* gate opening is checked AFTER adding to diamonds collected, so 0 here means 100 to collect */ + if (cave->level_diamonds[level] == 0) + cave->level_diamonds[level] = 100; + cave->level_time[level] = data[14 + level]; + } + + /* LogicDeLuxe extension: acid + $16 Acid speed (unused in the original BD1) + $17 Bit 2: if set, Acid's original position converts to explosion puff during spreading. + Otherwise, Acid remains intact, ie. it's just growing. (unused in the original BD1) + $1C Acid eats this element. (also Probability of element 1) + + there is no problem importing these; as other bd1 caves did not contain acid at all, + so it does not matter how we set the values. + */ + + /* 0x1c index: same as probability1 !!!!! don't be surprised. we do a &0x3f because of this */ + cave->acid_eats_this = import_func(data[0x1c] & 0x3F, 0x1c); + + /* acid speed, *1e6 as probabilities are stored in int */ + cave->acid_spread_ratio = data[0x16] / 255.0 * 1E6 + 0.5; + + cave->acid_turns_to = (data[0x17] & (1 << 2)) ? O_EXPLODE_3 : O_ACID; + + cave->colorb = GD_GDASH_BLACK; /* border - black */ + cave->color0 = GD_GDASH_BLACK; /* background - black */ + cave->color1= GD_GDASH_RED; + cave->color2 = GD_GDASH_PURPLE; + cave->color3 = GD_GDASH_YELLOW; + cave->color4 = cave->color3; /* in bd1, amoeba was color3 */ + cave->color5 = cave->color3; /* no slime, but let it be color 3 */ + + /* random fill */ + for (i = 0; i < 4; i++) + { + cave->random_fill[i] = import_func(data[24 + i], 24 + i); + cave->random_fill_probability[i] = data[28 + i]; + } + + /* + * Decode the explicit cave data + */ + index = 32; + + while (data[index] != 0xFF && index < remaining_bytes && index < 255) + { + code = data[index]; + + /* crazy dream 3 extension: */ + if (code == 0x0f) + { + int x1, y1, nx, ny, dx, dy; + int x, y; + + /* as this one uses nonstandard dx dy values, create points instead */ + elem = import_func(data[index + 1], index + 1); + x1 = data[index + 2]; + y1 = data[index + 3] - 2; + nx = data[index + 4]; + ny = data[index + 5]; + dx = data[index + 6]; + dy = data[index + 7] + 1; + + for (y = 0; y < ny; y++) + { + for (x = 0; x < nx; x++) + { + int pos = x1 + y1 * 40 + y * dy * 40 + x * dx; + + cave->objects = g_list_append(cave->objects, gd_object_new_point(GD_OBJECT_LEVEL_ALL, pos % 40, pos / 40, elem)); + } + } + + index += 8; + } + else + { + /* object is code&3f, object type is upper 2 bits */ + elem = import_func(code & 0x3F, index); + + switch ((code >> 6) & 3) + { + case 0: /* 00: POINT */ + x1 = data[index + 1]; + y1 = data[index + 2] - 2; + + if (x1 >= cave->w || y1 >= cave->h) + Warn("invalid point coordinates %d,%d at byte %d", x1, y1, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_point(GD_OBJECT_LEVEL_ALL, x1, y1, elem)); + + index += 3; + break; + + case 1: /* 01: LINE */ + x1 = data[index + 1]; + y1 = data[index + 2] - 2; + length = (gint8)data[index + 3] - 1; + direction = data[index + 4]; + + if (length < 0) + { + Warn("line length negative, not displaying line at all, at byte %d", index); + } + else + { + if (direction > GD_MV_UP_LEFT) + { + Warn("invalid line direction %d at byte %d", direction, index); + direction = GD_MV_STILL; + } + + x2 = x1 + length * gd_dx[direction + 1]; + y2 = y1 + length * gd_dy[direction + 1]; + + if (x1 >= cave->w || + y1 >= cave->h || + x2 >= cave->w || + y2 >= cave->h) + Warn("invalid line coordinates %d,%d %d,%d at byte %d", x1, y1, x2, y2, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_line(GD_OBJECT_LEVEL_ALL, x1, y1, x2, y2, elem)); + } + + index += 5; + break; + + case 2: /* 10: FILLED RECTANGLE */ + x1 = data[index + 1]; + y1 = data[index + 2] - 2; + x2 = x1 + data[index + 3] - 1; /* width */ + y2 = y1 + data[index + 4] - 1; /* height */ + + if (x1 >= cave->w || + y1 >= cave->h || + x2 >= cave->w || + y2 >= cave->h) + Warn("invalid filled rectangle coordinates %d,%d %d,%d at byte %d", x1, y1, x2, y2, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_filled_rectangle(GD_OBJECT_LEVEL_ALL, x1, y1, x2, y2, elem, import_func(data[index + 5], index + 5))); + + index += 6; + break; + + case 3: /* 11: OPEN RECTANGLE (OUTLINE) */ + x1 = data[index + 1]; + y1 = data[index + 2] - 2; + x2 = x1 + data[index + 3] - 1; + y2 = y1 + data[index + 4] - 1; + + if (x1 >= cave->w || + y1 >= cave->h || + x2 >= cave->w || + y2 >= cave->h) + Warn("invalid rectangle coordinates %d,%d %d,%d at byte %d", x1, y1, x2, y2, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_rectangle(GD_OBJECT_LEVEL_ALL, x1, y1, x2, y2, elem)); + + index += 5; + break; + } + } + } + + if (data[index] != 0xFF) + { + Error("import error, cave not delimited with 0xFF"); + return -1; + } + + return index + 1; +} + +/* import bd2 cave data into our format. return number of bytes if pointer passed. + this is pretty much the same as above, only the encoding was different. */ +static int cave_copy_from_bd2(GdCave *cave, const guint8 *data, int remaining_bytes, + GdCavefileFormat format) +{ + int index; + int i; + int x, y, rx, ry; + int x1, y1, x2, y2, dx, dy; + GdElement elem; + + if (remaining_bytes < 0x1A + 5) + { + Error("truncated BD2 cave data, %d bytes", remaining_bytes); + return -1; + } + + gd_cave_set_engine_defaults(cave, GD_ENGINE_BD2); + + if (format==GD_FORMAT_BD2_ATARI) + cave->scheduling = GD_SCHEDULING_BD2_PLCK_ATARI; + + /* set visible size for intermission */ + if (cave->intermission) + { + cave->x2 = 19; + cave->y2 = 11; + } + + cave->diamond_value = data[1]; + cave->extra_diamond_value = data[2]; + + for (i = 0; i < 5; i++) + { + /* 0 immediately underflowed to 999, so we use 999. example: sendydash 3, cave 02. */ + cave->level_amoeba_time[i] = data[0]==0 ? 999 : data[0]; + cave->level_rand[i] = data[13 + i]; + + /* gate opening is checked AFTER adding to diamonds collected, so 0 here is 1000 needed */ + cave->level_diamonds[i] = data[8 + i]==0 ? 1000 : data[8 + i]; + cave->level_time[i] = data[3 + i]; + cave->level_magic_wall_time[i] = data[0]; + } + + for (i = 0; i < 4; i++) + { + cave->random_fill[i] = bd1_import(data[0x16 + i], 0x16 + i); + cave->random_fill_probability[i] = data[0x12 + i]; + } + + /* + * Decode the explicit cave data + */ + index = 0x1A; + + while (data[index] != 0xFF && index < remaining_bytes) + { + int nx, ny; + unsigned int addr; + int val, n, bytes; + int length, direction; + + switch (data[index]) + { + case 0: /* LINE */ + elem = bd1_import(data[index + 1], index + 1); + y1 = data[index + 2]; + x1 = data[index + 3]; + + /* they are multiplied by two - 0 is up, 2 is upright, 4 is right... */ + direction = data[index + 4] / 2; + length = data[index + 5] - 1; + + if (direction > GD_MV_UP_LEFT) + { + Warn("invalid line direction %d at byte %d", direction, index); + direction = GD_MV_STILL; + } + + x2 = x1 + length * gd_dx[direction + 1]; + y2 = y1 + length * gd_dy[direction + 1]; + + if (x1 >= cave->w || + y1 >= cave->h || + x2 >= cave->w || + y2 >= cave->h) + Warn("invalid line coordinates %d,%d %d,%d at byte %d", x1, y1, x2, y2, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_line(GD_OBJECT_LEVEL_ALL, x1, y1, x2, y2, elem)); + + index += 6; + break; + + case 1: /* OPEN RECTANGLE */ + elem = bd1_import(data[index + 1], index + 1); + y1 = data[index + 2]; + x1 = data[index + 3]; + y2 = y1 + data[index + 4] - 1; /* height */ + x2 = x1 + data[index + 5] - 1; + + if (x1 >= cave->w || + y1 >= cave->h || + x2 >= cave->w || + y2 >= cave->h) + Warn("invalid rectangle coordinates %d,%d %d,%d at byte %d", x1, y1, x2, y2, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_rectangle(GD_OBJECT_LEVEL_ALL, x1, y1, x2, y2, elem)); + + index += 6; + break; + + case 2: /* FILLED RECTANGLE */ + elem = bd1_import(data[index + 1], index + 1); + y1 = data[index + 2]; + x1 = data[index + 3]; + y2 = y1 + data[index + 4] - 1; + x2 = x1 + data[index + 5] - 1; + + if (x1 >= cave->w || + y1 >= cave->h || + x2 >= cave->w || + y2 >= cave->h) + Warn("invalid filled rectangle coordinates %d,%d %d,%d at byte %d", x1, y1, x2, y2, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_filled_rectangle(GD_OBJECT_LEVEL_ALL, x1, y1, x2, y2, elem, bd1_import(data[index+6], index+6))); + + index += 7; + break; + + case 3: /* POINT */ + elem = bd1_import(data[index + 1], index + 1); + y1 = data[index + 2]; + x1 = data[index + 3]; + + if (x1 >= cave->w || + y1 >= cave->h) + Warn("invalid point coordinates %d,%d at byte %d", x1, y1, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_point(GD_OBJECT_LEVEL_ALL, x1, y1, elem)); + + index += 4; + break; + + case 4: /* RASTER */ + elem = bd1_import(data[index + 1], index + 1); + y1 = data[index + 2]; /* starting pos */ + x1 = data[index + 3]; + ny = data[index + 4] - 1; /* number of elements */ + nx = data[index + 5] - 1; + dy = data[index + 6]; /* displacement */ + dx = data[index + 7]; + y2 = y1 + dy * ny; /* calculate rectangle */ + x2 = x1 + dx * nx; + + /* guess this has to be here, after x2,y2 calculation, because of some bugs in imported data */ + if (dy < 1) + dy = 1; + if (dx < 1) + dx = 1; + + if (x1 >= cave->w || + y1 >= cave->h || + x2 >= cave->w || + y2 >= cave->h) + Warn("invalid raster coordinates %d,%d %d,%d at byte %d", x1, y1, x2, y2, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_raster(GD_OBJECT_LEVEL_ALL, x1, y1, x2, y2, dx, dy, elem)); + + index += 8; + break; + + case 5: + /* profi boulder extension: bitmap */ + elem = bd1_import(data[index + 1], index + 1); + bytes = data[index + 2]; /* number of bytes in bitmap */ + + if (bytes >= cave->w * cave->h / 8) + Warn("invalid bitmap length at byte %d", index - 4); + + addr = 0; + addr += data[index + 3]; /*msb */ + addr += data[index + 4] << 8; /*lsb */ + + /* this was a pointer to the cave work memory (used during game). */ + addr -= 0x0850; + + if (addr >= cave->w * cave->h) + Warn("invalid bitmap start address at byte %d", index - 4); + + x1 = addr % 40; + y1 = addr / 40; + + for (i = 0; i < bytes; i++) + { + /* for ("bytes" number of bytes) */ + val = data[index + 5 + i]; + + for (n = 0; n < 8; n++) + { + /* for (8 bits in a byte) */ + if ((val & 1) != 0) /* convert to single points... */ + cave->objects = g_list_append(cave->objects, gd_object_new_point(GD_OBJECT_LEVEL_ALL, x1, y1, elem)); + + val >>= 1; + x1++; /* next cave pos */ + + if (x1 >= cave->w) + { + /* maybe next line in map */ + x1 = 0; + y1++; + } + } + } + + index += 5 + bytes; /* 5 description bytes and "bytes" data bytes */ + break; + + case 6: /* JOIN */ + dy = data[index + 3] / 40; + dx = data[index + 3] % 40; /* same byte!!! */ + cave->objects = g_list_append(cave->objects, gd_object_new_join(GD_OBJECT_LEVEL_ALL, dx, dy, bd1_import(data[index+1], index+1), bd1_import(data[index+2], index+2))); + + index += 4; + break; + + case 7: /* SLIME PERMEABILITY */ + /* interesting this is set here, and not in the cave header */ + for (i = 0; i < 5; i++) + cave->level_slime_permeability_c64[i] = data[index + 1]; + + index += 2; + break; + + case 9: + /* profi boulder extension by player: plck-like cave map. the import + routine (any2gdash) inserts it here. */ + if (cave->map != NULL) + { + Error("contains more than one PLCK map"); + gd_cave_map_free(cave->map); + } + + cave->map = gd_cave_map_new(cave, GdElement); + + for (x = 0; x < cave->w; x++) + { + /* fill the first and the last row with steel wall. */ + cave->map[0][x] = O_STEEL; + cave->map[cave->h - 1][x] = O_STEEL; + } + + n = 0; /* number of bytes read from map */ + + /* the first and the last rows are not stored. */ + for (y = 1; y < cave->h - 1; y++) + { + for (x = 0; x < cave->w; x += 2) + { + cave->map[y][x] = plck_import_nybble[data[index + 3 + n] >> 4]; /* msb 4 bits */ + cave->map[y][x + 1] = plck_import_nybble[data[index + 3 + n] % 16]; /* lsb 4 bits */ + n++; + } + } + + /* the position of inbox is stored. this is to check the cave */ + ry = data[index + 1] - 2; + rx = data[index + 2]; + + /* at the start of the cave, bd scrolled to the last player placed during the drawing + (setup) of the cave. + i think this is why a map also stored the coordinates of the player - we can use + this to check its integrity */ + if (rx >= cave->w || ry < 0 || + ry >= cave->h || cave->map[ry][rx] != O_INBOX) + Warn ("embedded PLCK map may be corrupted, player coordinates %d,%d", rx, rx); + + index += 3 + n; + break; + + default: + Warn ("unknown bd2 extension no. %02x at byte %d", data[index], index); + + index += 1; /* skip that byte */ + } + } + + if (data[index] != 0xFF) + { + Error("import error, cave not delimited with 0xFF"); + return -1; + } + + /* skip delimiter */ + index++; + + /* animation byte - told the engine which objects to animate - to make game faster */ + index++; + + /* the colors from the memory dump are appended here by any2gdash */ + cave->colorb = GD_GDASH_BLACK; /* border - black */ + cave->color0 = GD_GDASH_BLACK; /* background - black */ + cave->color1 = GD_GDASH_RED; + cave->color2 = GD_GDASH_PURPLE; + cave->color3 = GD_GDASH_YELLOW; + cave->color4 = cave->color3; /* in bd1, amoeba was color3 */ + cave->color5 = cave->color3; /* no slime, but let it be color 3 */ + + return index; +} + +/* import plck cave data into our format. + length is always 512 bytes, and contains if it is an intermission cave. */ +static int cave_copy_from_plck(GdCave *cave, const guint8 *data, + int remaining_bytes, GdCavefileFormat format) +{ + /* i don't really think that all this table is needed, but included to be complete. */ + /* this is for the dirt and expanding wall looks like effect. */ + /* it also contains the individual frames */ + static GdElement plck_graphic_table[] = + { + /* 3000 */ O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, + /* 3100 */ O_BUTTER_1, O_MAGIC_WALL, O_PRE_DIA_1, O_PRE_DIA_2, O_PRE_DIA_3, O_PRE_DIA_4, O_PRE_DIA_5, O_OUTBOX_CLOSED, + /* 3200 */ O_AMOEBA, O_VOODOO, O_STONE, O_DIRT, O_DIAMOND, O_STEEL, O_PLAYER, O_BRICK, + /* 3300 */ O_SPACE, O_OUTBOX_OPEN, O_FIREFLY_1, O_EXPLODE_1, O_EXPLODE_2, O_EXPLODE_3, O_MAGIC_WALL, O_MAGIC_WALL, + /* 3400 */ O_PLAYER_TAP_BLINK, O_PLAYER_TAP_BLINK, O_PLAYER_TAP_BLINK, O_PLAYER_TAP_BLINK, O_PLAYER_TAP_BLINK, O_PLAYER_TAP_BLINK, O_PLAYER_TAP_BLINK, O_PLAYER_TAP_BLINK, + /* 3500 */ O_PLAYER_LEFT, O_PLAYER_LEFT, O_PLAYER_LEFT, O_PLAYER_LEFT, O_PLAYER_LEFT, O_PLAYER_LEFT, O_PLAYER_LEFT, O_PLAYER_LEFT, + /* 3600 */ O_PLAYER_RIGHT, O_PLAYER_RIGHT, O_PLAYER_RIGHT, O_PLAYER_RIGHT, O_PLAYER_RIGHT, O_PLAYER_RIGHT, O_PLAYER_RIGHT, O_PLAYER_RIGHT, + /* 3700 */ O_BUTTER_1, O_BUTTER_1, O_BUTTER_1, O_BUTTER_1, O_BUTTER_1, O_BUTTER_1, O_BUTTER_1, O_BUTTER_1, + /* 3800 */ O_AMOEBA, O_AMOEBA, O_AMOEBA, O_AMOEBA, O_AMOEBA, O_AMOEBA, O_AMOEBA, O_AMOEBA, + }; + + int i; + int x, y; + + if (remaining_bytes < 512) + { + Error("truncated plck cave data!"); + return -1; + } + + gd_cave_set_engine_defaults(cave, GD_ENGINE_PLCK); + + if (format == GD_FORMAT_PLC_ATARI) + cave->scheduling = GD_SCHEDULING_BD2_PLCK_ATARI; + + cave->intermission = data[0x1da] != 0; + + if (cave->intermission) + { + /* set visible size for intermission */ + cave->x2 = 19; + cave->y2 = 11; + } + + /* cave selection table, was not part of cave data, rather given in game packers. + * if a new enough version of any2gdash is used, it will put information after the cave. + * detect this here and act accordingly */ + if ((data[0x1f0] == data[0x1f1] - 1) && + (data[0x1f0] == 0x19 || + data[0x1f0] == 0x0e)) + { + int j; + + /* found selection table */ + cave->selectable = data[0x1f0] == 0x19; + gd_strcpy(cave->name, " "); + + for (j = 0; j < 12; j++) + cave->name[j] = data[0x1f2 + j]; + + g_strchomp(cave->name); /* remove spaces */ + } + else + { + /* no selection info found, let intermissions be unselectable */ + cave->selectable = !cave->intermission; + } + + cave->diamond_value = data[0x1be]; + cave->extra_diamond_value = data[0x1c0]; + + for (i = 0; i < 5; i++) + { + /* plck doesnot really have levels, so just duplicate data five times */ + cave->level_amoeba_time[i] = data[0x1c4]; + + /* immediately underflowed to 999, so we use 999. example: sendydash 3, cave 02. */ + if (cave->level_amoeba_time[i] == 0) + cave->level_amoeba_time[i] = 999; + + cave->level_time[i] = data[0x1ba]; + cave->level_diamonds[i] = data[0x1bc]; + + /* gate opening is checked AFTER adding to diamonds collected, so 0 here is 1000 needed */ + if (cave->level_diamonds[i] == 0) + cave->level_diamonds[i] = 1000; + + cave->level_ckdelay[i] = data[0x1b8]; + cave->level_magic_wall_time[i] = data[0x1c6]; + cave->level_slime_permeability_c64[i] = slime_shift_msb(data[0x1c2]); + } + + cave->colorb = GD_GDASH_BLACK; /* border - black */ + cave->color0 = GD_GDASH_BLACK; /* background - black */ + cave->color1 = GD_GDASH_RED; + cave->color2 = GD_GDASH_PURPLE; + cave->color3 = GD_GDASH_YELLOW; + cave->color4 = cave->color3; /* in bd1, amoeba was color3 */ + cave->color5 = cave->color3; /* no slime, but let it be color 3 */ + + /* ... the cave is stored like a map. */ + cave->map = gd_cave_map_new(cave, GdElement); + + /* cave map looked like this. */ + /* two rows of steel wall ($44's), then cave description, 20 bytes (40 nybbles) for each line. */ + /* the bottom and top lines were not stored... originally. */ + /* some games write to the top line; so we import that, too. */ + /* also dlp 155 allowed writing to the bottom line; the first 20 $44-s now store the bottom line. */ + /* so the cave is essentially shifted one row down in the file: cave->map[y][x] = data[... y+1 mod height ][x] */ + for (y = 0; y < cave->h; y++) + { + for (x = 0; x < cave->w; x += 2) + { + /* msb 4 bits: we do not check index ranges, as >>4 and %16 will result in 0..15 */ + cave->map[y][x] = plck_import_nybble[data[((y + 1) % cave->h) * 20 + x / 2] >> 4]; + + /* lsb 4 bits */ + cave->map[y][x + 1] = plck_import_nybble[data[((y + 1) % cave->h) * 20 + x / 2] % 16]; + } + } + + /* FOR NOW, WE DO NOT IMPORT THE BOTTOM BORDER */ + for (x = 0; x < cave->w; x++) + cave->map[cave->h - 1][x] = O_STEEL; + + /* + if (steels && data[0]==0x55) + cave->map[cave->h - 1][0] = cave->map[cave->h - 1][1] = O_STEEL; + */ + + /* check for diego-effects */ + /* c64 magic values (byte sequences) 0x20 0x90 0x46, also 0xa9 0x1c 0x85 */ + if ((data[0x1e5] == 0x20 && + data[0x1e6] == 0x90 && + data[0x1e7] == 0x46) || + (data[0x1e5] == 0xa9 && + data[0x1e6] == 0x1c && + data[0x1e7] == 0x85)) + { + /* diego effects enabled. */ + cave->stone_bouncing_effect = bd1_import(data[0x1ea], 0x1ea); + cave->diamond_falling_effect = bd1_import(data[0x1eb], 0x1eb); + + /* explosions: 0x1e was explosion 5, if this is set to default, we also do not read it, + as in our engine this would cause an O_EXPLODE_5 to stay there. */ + if (data[0x1ec] != 0x1e) + cave->explosion_effect = bd1_import(data[0x1ec], 0x1ec); + + /* pointer to element graphic. + two bytes/column (one element), that is data[xxx] % 16 / 2. + also there are 16bytes/row. + that is, 0x44 = stone, upper left character. 0x45 = upper right, + 0x54 = lower right, 0x55 = lower right. + so high nybble must be shifted right twice -> data[xxx]/16*4. */ + cave->dirt_looks_like = plck_graphic_table[(data[0x1ed] / 16) * 4 + (data[0x1ed] % 16) / 2]; + cave->expanding_wall_looks_like = plck_graphic_table[(data[0x1ee] / 16) * 4 + (data[0x1ee] % 16) / 2]; + + for (i = 0; i < 5; i++) + cave->level_amoeba_threshold[i] = data[0x1ef]; + } + + return 512; +} + +/* no one's delight boulder dash essentially: rle compressed plck maps. */ +static int cave_copy_from_dlb(GdCave *cave, const guint8 *data, int remaining_bytes) +{ + guint8 decomp[512]; + enum + { + START, /* initial state */ + SEPARATOR, /* got a separator */ + RLE, /* after a separator, got the byte to duplicate */ + NORMAL /* normal, copy bytes till separator */ + } state; + int pos, cavepos, i, x, y; + guint8 byte, separator; + + gd_cave_set_engine_defaults(cave, GD_ENGINE_PLCK); /* essentially the plck engine */ + + for (i = 0; i < 5; i++) + { + /* does not really have levels, so just duplicate data five times */ + cave->level_time[i] = data[1]; + cave->level_diamonds[i] = data[2]; + + /* gate opening is checked AFTER adding to diamonds collected, so 0 here is 1000 needed */ + if (cave->level_diamonds[i] == 0) + cave->level_diamonds[i] = 1000; + + cave->level_ckdelay[i] = data[0]; + cave->level_amoeba_time[i] = data[6]; + + /* 0 immediately underflowed to 999, so we use 999. example: sendydash 3, cave 02. */ + if (cave->level_amoeba_time[i] == 0) + cave->level_amoeba_time[i] = 999; + + cave->level_magic_wall_time[i] = data[7]; + cave->level_slime_permeability_c64[i] = slime_shift_msb(data[5]); + } + + cave->diamond_value = data[3]; + cave->extra_diamond_value = data[4]; + + /* then 5 color bytes follow */ + cave->colorb = gd_c64_color(data[8] & 0xf); /* border */ + cave->color0 = gd_c64_color(data[9] & 0xf); + cave->color1 = gd_c64_color(data[10] & 0xf); + cave->color2 = gd_c64_color(data[11] & 0xf); + cave->color3 = gd_c64_color(data[12] & 0x7); /* lower 3 bits only! */ + cave->color4 = cave->color3; /* in plck, amoeba was color3 */ + cave->color5 = cave->color3; /* same for slime */ + + /* cave map */ + pos = 13; /* those 13 bytes were the cave values above */ + cavepos = 0; + byte = 0; /* just to get rid of compiler warning */ + separator = 0; /* just to get rid of compiler warning */ + + /* employ a state machine. */ + state = START; + + while (cavepos < 400 && pos < remaining_bytes) + { + switch (state) + { + case START: + /* first byte is a separator. remember it */ + separator = data[pos]; + + /* after the first separator, no rle data, just copy. */ + state = NORMAL; + break; + + case SEPARATOR: + /* we had a separator. remember this byte, as this will be duplicated (or more) */ + byte = data[pos]; + state = RLE; + break; + + case RLE: + /* we had the first byte, duplicate this n times. */ + if (data[pos] == 0xff) + { + /* if it is a 0xff, we will have another byte, which is also a length specifier. */ + /* and for this one, duplicate only 254 times */ + if (cavepos + 254 > 400) + { + Error("DLB import error: RLE data overflows buffer"); + return -1; + } + + for (i = 0; i < 254; i++) + decomp[cavepos++] = byte; + } + else + { + /* if not 0xff, duplicate n times and back to copy mode */ + if (cavepos + data[pos] > 400) + { + Error("DLB import error: RLE data overflows buffer"); + return -1; + } + + for (i = 0; i < data[pos]; i++) + decomp[cavepos++] = byte; + + state = NORMAL; + } + break; + + case NORMAL: + /* bytes duplicated; now only copy the remaining, till the next separator. */ + if (data[pos] == separator) + state = SEPARATOR; + else + decomp[cavepos++] = data[pos]; /* copy this byte and state is still NORMAL */ + break; + } + + pos++; + } + + if (cavepos != 400) + { + Error("DLB import error: RLE processing, cave length %d, should be 400", cavepos); + return -1; + } + + /* process uncompressed map */ + cave->map = gd_cave_map_new(cave, GdElement); + + for (x = 0; x < cave->w; x++) + { + /* fill the first and the last row with steel wall. */ + cave->map[0][x] = O_STEEL; + cave->map[cave->h - 1][x] = O_STEEL; + } + + for (y = 1; y < cave->h - 1; y++) + { + for (x = 0; x < cave->w; x += 2) + { + /* msb 4 bits */ + cave->map[y][x] = plck_import_nybble[decomp[((y - 1) * cave->w + x) / 2] >> 4]; + /* lsb 4 bits */ + cave->map[y][x + 1] = plck_import_nybble[decomp[((y - 1) * cave->w + x) / 2] % 16]; + } + } + + /* return number of bytes read from buffer */ + return pos; +} + +/* import plck cave data into our format. */ +static int cave_copy_from_1stb(GdCave *cave, const guint8 *data, int remaining_bytes) +{ + int i; + int x, y; + + if (remaining_bytes < 1024) + { + Error("truncated 1stb cave data!"); + + return -1; + } + + gd_cave_set_engine_defaults(cave, GD_ENGINE_1STB); + + /* copy name */ + gd_strcpy(cave->name, " "); + + for (i = 0; i < 14; i++) + { + int c = data[0x3a0 + i]; + + /* import cave name; a conversion table is used for each character */ + if (c < 0x40) + c = gd_bd_internal_chars[c]; + else if (c == 0x74) + c = ' '; + else if (c == 0x76) + c = '?'; + else + c = ' '; /* don't know this, so change to space */ + + if (i > 0) + c = g_ascii_tolower(c); + + cave->name[i] = c; + } + + g_strchomp(cave->name); + + cave->intermission = data[0x389] != 0; + + /* if it is intermission but not scrollable */ + if (cave->intermission && !data[0x38c]) + { + cave->x2 = 19; + cave->y2 = 11; + } + + cave->diamond_value = 100 * data[0x379] + 10 * data[0x379 + 1] + data[0x379 + 2]; + cave->extra_diamond_value = 100 * data[0x376] + 10 * data[0x376 + 1] + data[0x376 + 2]; + + for (i = 0; i < 5; i++) + { + /* plck doesnot really have levels, so just duplicate data five times */ + cave->level_time[i] = 100 * data[0x370] + 10 * data[0x370+1] + data[0x370 + 2]; + + /* same as gate opening after 0 diamonds */ + if (cave->level_time[i] == 0) + cave->level_time[i] = 1000; + + cave->level_diamonds[i] = 100 * data[0x373] + 10 * data[0x373 + 1] + data[0x373 + 2]; + + /* gate opening is checked AFTER adding to diamonds collected, so 0 here is 1000 (!) needed */ + if (cave->level_diamonds[i] == 0) + cave->level_diamonds[i] = 1000; + + cave->level_ckdelay[i] = data[0x38a]; + cave->level_amoeba_time[i] = 256 * (int)data[0x37c] + data[0x37d]; + + /* 0 immediately underflowed to 999, so we use 999. example: sendydash 3, cave 02. */ + if (cave->level_amoeba_time[i] == 0) + cave->level_amoeba_time[i] = 999; + + cave->level_magic_wall_time[i] = 256 * (int)data[0x37e] + data[0x37f]; + cave->level_slime_permeability_c64[i] = data[0x38b]; + cave->level_bonus_time[i] = data[0x392]; + cave->level_penalty_time[i] = data[0x393]; + cave->level_amoeba_threshold[i] = 256 * (int)data[0x390] + data[0x390 + 1]; + } + + /* also has no random data... */ + cave->colorb = gd_c64_color(data[0x384] & 0xf); /* border */ + cave->color0 = gd_c64_color(data[0x385] & 0xf); + cave->color1 = gd_c64_color(data[0x386] & 0xf); + cave->color2 = gd_c64_color(data[0x387] & 0xf); + cave->color3 = gd_c64_color(data[0x388] & 0x7); /* lower 3 bits only! */ + cave->color4 = cave->color1; + cave->color5 = cave->color1; + + cave->amoeba_growth_prob = (4.0 * 1E6 / (data[0x382] + 1)) + 0.5; /* probabilities store *1M */ + if (cave->amoeba_growth_prob > 1000000) + cave->amoeba_growth_prob = 1000000; + + cave->amoeba_fast_growth_prob = (4.0 * 1E6 / (data[0x383] + 1)) + 0.5; + if (cave->amoeba_fast_growth_prob > 1000000) + cave->amoeba_fast_growth_prob = 1000000; + + if (data[0x380] != 0) + cave->creatures_direction_auto_change_time = data[0x381]; + else + cave->diagonal_movements = data[0x381] != 0; + + /* ... the cave is stored like a map. */ + cave->map = gd_cave_map_new(cave, GdElement); + for (y = 0; y < cave->h; y++) + for (x = 0; x < cave->w; x++) + cave->map[y][x] = firstboulder_import(data[y * 40 + x], y * 40 + x); + + cave->magic_wall_sound = data[0x38d] == 0xf1; + + /* 2d was a normal switch, 2e a changed one. */ + cave->creatures_backwards = data[0x38f] == 0x2d; + + /* 2e horizontal, 2f vertical. */ + cave->expanding_wall_changed = data[0x38e] == 0x2f; + + cave->biter_delay_frame = data[0x394]; + cave->magic_wall_stops_amoeba = data[0x395] == 0; /* negated!! */ + + cave->bomb_explosion_effect = firstboulder_import(data[0x396], 0x396); + cave->explosion_effect = firstboulder_import(data[0x397], 0x397); + cave->stone_bouncing_effect = firstboulder_import(data[0x398], 0x398); + cave->diamond_birth_effect = firstboulder_import(data[0x399], 0x399); + cave->magic_diamond_to = firstboulder_import(data[0x39a], 0x39a); + + cave->bladder_converts_by = firstboulder_import(data[0x39b], 0x39b); + cave->diamond_falling_effect = firstboulder_import(data[0x39c], 0x39c); + cave->biter_eat = firstboulder_import(data[0x39d], 0x39d); + cave->slime_eats_1 = firstboulder_import(data[0x39e], 0x39e); + cave->slime_converts_1 = firstboulder_import(data[0x39e] + 3, 0x39e); + cave->slime_eats_2 = firstboulder_import(data[0x39f], 0x39f); + cave->slime_converts_2 = firstboulder_import(data[0x39f] + 3, 0x39f); + cave->magic_diamond_to = firstboulder_import(data[0x39a], 0x39a); + + /* length is always 1024 bytes */ + return 1024; +} + +/* crazy dream 7 */ +static int cave_copy_from_crdr_7(GdCave *cave, const guint8 *data, int remaining_bytes) +{ + int i, index; + guint8 checksum; + + /* if we have name, convert */ + gd_strcpy(cave->name, " "); + + for (i = 0; i < 14; i++) + { + int c = data[i]; + + /* import cave name; a conversion table is used for each character */ + if (c < 0x40) + c = gd_bd_internal_chars[c]; + else if (c == 0x74) + c = ' '; + else if (c == 0x76) + c = '?'; + else + c = ' '; + if (i > 0) + c = g_ascii_tolower(c); + + cave->name[i] = c; + } + + g_strchomp(cave->name); /* remove trailing and leading spaces */ + + cave->selectable = data[14] != 0; + + /* jump 15 bytes, 14 was the name and 15 selectability */ + data += 15; + + if (memcmp((char *)data + 0x30, "V4\0020", 4) != 0) + Warn("unknown crdr version %c%c%c%c", data[0x30], data[0x31], data[0x32], data[0x33]); + + gd_cave_set_engine_defaults(cave, GD_ENGINE_CRDR7); + + for (i = 0; i < 5; i++) + { + cave->level_time[i] = (int)data[0x0] * 100 + data[0x1] * 10 + data[0x2]; + + /* same as gate opening after 0 diamonds */ + if (cave->level_time[i] == 0) + cave->level_time[i] = 1000; + + cave->level_diamonds[i] = (int)data[0x3] * 100 + data[0x4] * 10 + data[0x5]; + + /* gate opening is checked AFTER adding to diamonds collected, so 0 here is 1000 (!) needed */ + if (cave->level_diamonds[i] == 0) + cave->level_diamonds[i] = 1000; + + cave->level_ckdelay[i] = data[0x1A]; + cave->level_rand[i] = data[0x40]; + cave->level_amoeba_time[i] = (int)data[0xC] * 256 + data[0xD]; + + /* 0 immediately underflowed to 999, so we use 999. example: sendydash 3, cave 02. */ + if (cave->level_amoeba_time[i] == 0) + cave->level_amoeba_time[i] = 999; + + cave->level_magic_wall_time[i] = (int)data[0xE] * 256 + data[0xF]; + cave->level_slime_permeability_c64[i] = data[0x1B]; + cave->level_bonus_time[i] = data[0x22]; + cave->level_penalty_time[i] = data[0x23]; + cave->level_bonus_time[i] = data[0x22]; + cave->level_penalty_time[i] = data[0x23]; + cave->level_amoeba_threshold[i] = 256 * (int)data[0x20] + data[0x21]; + } + + cave->extra_diamond_value = (int)data[0x6] * 100 + data[0x7] * 10 + data[0x8]; + cave->diamond_value = (int)data[0x9] * 100 + data[0xA] * 10 + data[0xB]; + + if (data[0x10]) + cave->creatures_direction_auto_change_time = data[0x11]; + + cave->colorb = gd_c64_color(data[0x14] & 0xf); /* border */ + cave->color0 = gd_c64_color(data[0x15] & 0xf); + cave->color1 = gd_c64_color(data[0x16] & 0xf); + cave->color2 = gd_c64_color(data[0x17] & 0xf); + cave->color3 = gd_c64_color(data[0x18] & 0x7); /* lower 3 bits only! */ + cave->color4 = cave->color3; + cave->color5 = cave->color1; + cave->intermission = data[0x19] != 0; + + /* if it is intermission but not scrollable */ + if (cave->intermission && !data[0x1c]) + { + cave->x2 = 19; + cave->y2 = 11; + } + + /* AMOEBA in crazy dash 8: + jsr $2500 ; generate true random + and $94 ; binary and the current "probability" + cmp #$04 ; compare to 4 + bcs out ; jump out (do not expand) if carry set, ie. result was less than 4. + + prob values can be like num = 3, 7, 15, 31, 63, ... n lsb bits count. + 0..3>=4? 0..7>=4? 0..15>=4? and similar. + this way, probability of growing is 4/(num+1) + */ + + cave->amoeba_growth_prob = (4.0 * 1E6 / (data[0x12] + 1)) + 0.5; /* probabilities store * 1M */ + if (cave->amoeba_growth_prob > 1000000) + cave->amoeba_growth_prob = 1000000; + + cave->amoeba_fast_growth_prob = (4.0 * 1E6 / (data[0x13] + 1)) + 0.5; + if (cave->amoeba_fast_growth_prob > 1000000) + cave->amoeba_fast_growth_prob = 1000000; + + /* expanding wall direction change - 2e horizontal, 2f vertical */ + cave->expanding_wall_changed = data[0x1e] == 0x2f; + + /* 2c was a normal switch, 2d a changed one. */ + cave->creatures_backwards = data[0x1f] == 0x2d; + cave->biter_delay_frame = data[0x24]; + cave->magic_wall_stops_amoeba = data[0x25] == 0; /* negated!! */ + + cave->bomb_explosion_effect = crazydream_import_table[data[0x26]]; + cave->explosion_effect = crazydream_import_table[data[0x27]]; + cave->stone_bouncing_effect = crazydream_import_table[data[0x28]]; + cave->diamond_birth_effect = crazydream_import_table[data[0x29]]; + cave->magic_diamond_to = crazydream_import_table[data[0x2a]]; + + cave->bladder_converts_by = crazydream_import_table[data[0x2b]]; + cave->diamond_falling_effect = crazydream_import_table[data[0x2c]]; + cave->biter_eat = crazydream_import_table[data[0x2d]]; + cave->slime_eats_1 = crazydream_import_table[data[0x2e]]; + cave->slime_converts_1 = crazydream_import_table[data[0x2e] + 3]; + cave->slime_eats_2 = crazydream_import_table[data[0x2f]]; + cave->slime_converts_2 = crazydream_import_table[data[0x2f] + 3]; + + cave->diagonal_movements = (data[0x34] & 1) != 0; + cave->gravity_change_time = data[0x35]; + cave->pneumatic_hammer_frame = data[0x36]; + cave->hammered_wall_reappear_frame = data[0x37]; + cave->hammered_walls_reappear = data[0x3f] != 0; + + /* + acid in crazy dream 8: + jsr $2500 ; true random + cmp $03a8 ; compare to ratio + bcs out ; if it was smaller, forget it for now. + + ie. random<=ratio, then acid grows. + */ + + /* 1e6, probabilities are stored as int */ + cave->acid_spread_ratio = data[0x38] / 255.0 * 1E6 + 0.5; + + cave->acid_eats_this = crazydream_import_table[data[0x39]]; + switch(data[0x3a] & 3) + { + case 0: cave->gravity = GD_MV_UP; break; + case 1: cave->gravity = GD_MV_DOWN; break; + case 2: cave->gravity = GD_MV_LEFT; break; + case 3: cave->gravity = GD_MV_RIGHT; break; + } + + cave->snap_element = ((data[0x3a] & 4) != 0) ? O_EXPLODE_1 : O_SPACE; + + /* we do not know the values for these, so do not import */ + // cave->dirt_looks_like... data[0x3c] + // cave->expanding_wall_looks_like... data[0x3b] + for (i = 0; i < 4; i++) + { + cave->random_fill[i] = crazydream_import_table[data[0x41 + i]]; + cave->random_fill_probability[i] = data[0x45 + i]; + } + + data += 0x49; + index = 0; + + while (data[index] != 0xff) + { + GdElement elem; + int x1, y1, x2, y2, dx, dy; + int nx, ny; + int length, direction; + + /* for copy&paste; copy&paste are different objects, static = ugly solution :) */ + static int cx1, cy1, cw, ch; + + switch (data[index]) + { + case 1: /* point */ + elem = crazydream_import_table[data[index + 1]]; + x1 = data[index + 2]; + y1 = data[index + 3]; + if (x1 >= cave->w || y1 >= cave->h) + Warn("invalid point coordinates %d,%d at byte %d", x1, y1, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_point(GD_OBJECT_LEVEL_ALL, x1, y1, elem)); + + index += 4; + break; + + case 2: /* rectangle */ + elem = crazydream_import_table[data[index + 1]]; + x1 = data[index + 2]; + y1 = data[index + 3]; + x2 = x1 + data[index + 4] - 1; + y2 = y1 + data[index + 5] - 1; /* height */ + + if (x1 >= cave->w || + y1 >= cave->h || + x2 >= cave->w || + y2 >= cave->h) + Warn("invalid rectangle coordinates %d,%d %d,%d at byte %d", x1, y1, x2, y2, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_rectangle(GD_OBJECT_LEVEL_ALL, x1, y1, x2, y2, elem)); + + index += 6; + break; + + case 3: /* fillrect */ + x1 = data[index + 2]; + y1 = data[index + 3]; + x2 = x1 + data[index + 4] - 1; + y2 = y1 + data[index + 5] - 1; + + if (x1 >= cave->w || + y1 >= cave->h || + x2 >= cave->w || + y2 >= cave->h) + Warn("invalid filled rectangle coordinates %d,%d %d,%d at byte %d", x1, y1, x2, y2, index); + + /* border and inside of fill is the same element. */ + cave->objects = g_list_append(cave->objects, gd_object_new_filled_rectangle(GD_OBJECT_LEVEL_ALL, x1, y1, x2, y2, crazydream_import_table[data[index + 1]], crazydream_import_table[data[index + 1]])); + + index += 6; + break; + + case 4: /* line */ + elem = crazydream_import_table[data[index + 1]]; + if (elem == O_UNKNOWN) + Warn("unknown element at %d: %x", index + 1, data[index + 1]); + + x1 = data[index + 2]; + y1 = data[index + 3]; + length = data[index + 4]; + direction = data[index + 5]; + nx = ((signed)direction - 128) % 40; + ny = ((signed)direction - 128) / 40; + x2 = x1 + (length - 1) * nx; + y2 = y1 + (length - 1) * ny; + + /* if either is bigger than one, we cannot treat this as a line. create points instead */ + if (ABS(nx) >= 2 || ABS(ny) >= 2) + { + for (i = 0; i < length; i++) + { + cave->objects = g_list_append(cave->objects, gd_object_new_point(GD_OBJECT_LEVEL_ALL, x1, y1, elem)); + x1 += nx; + y1 += ny; + } + } + else + { + /* this is a normal line, and will be appended. only do the checking here */ + if (x1 >= cave->w || + y1 >= cave->h || + x2 >= cave->w || + y2 >= cave->h) + Warn("invalid line coordinates %d,%d %d,%d at byte %d", x1, y1, x2, y2, index - 5); + + cave->objects = g_list_append(cave->objects, gd_object_new_line(GD_OBJECT_LEVEL_ALL, x1, y1, x2, y2, elem)); + } + + index += 6; + break; + + case 6: /* copy */ + cx1 = data[index + 1]; + cy1 = data[index + 2]; + cw = data[index + 3]; + ch = data[index + 4]; + + if (cx1 >= cave->w || + cy1 >= cave->h || + cx1 + cw > cave->w || + cy1 + ch > cave->h) + Warn("invalid copy coordinates %d,%d or size %d,%d at byte %d", cx1, cy1, cw, ch, index); + + index += 5; + break; + + case 7: /* paste */ + x1 = cx1; + y1 = cy1; + + /* original stored width and height, we store the coordinates of the source area */ + x2 = cx1 + cw - 1; + y2 = cy1 + ch - 1; + dx = data[index + 1]; /* new pos */ + dy = data[index + 2]; + + if (dx >= cave->w || + dy >= cave->h || + dx + cw > cave->w || + dy + ch > cave->h) + Warn("invalid paste coordinates %d,%d at byte %d", dx, dy, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_copy_paste(GD_OBJECT_LEVEL_ALL, x1, y1, x2, y2, dx, dy, FALSE, FALSE)); + + index += 3; + break; + + case 11: /* raster */ + elem = crazydream_import_table[data[index + 1]]; + x1 = data[index + 2]; + y1 = data[index + 3]; + dx = data[index + 4]; + dy = data[index + 5]; + nx = data[index + 6] - 1; + ny = data[index + 7] - 1; + x2 = x1 + dx * nx; /* calculate rectangle we use */ + y2 = y1 + dy * ny; + + if (dx < 1) + dx = 1; + if (dy < 1) + dy = 1; + + if (x1 >= cave->w || + y1 >= cave->h || + x2 >= cave->w || + y2 >= cave->h) + Warn("invalid raster coordinates %d,%d %d,%d at byte %d", x1, y1, x2, y2, index); + + cave->objects = g_list_append(cave->objects, gd_object_new_raster(GD_OBJECT_LEVEL_ALL, x1, y1, x2, y2, dx, dy, elem)); + + index += 8; + break; + + default: + Warn ("unknown crdr extension no. %02x at byte %d", data[index], index); + index += 1; /* skip that byte */ + break; + } + } + + index++; /* skip $ff */ + + /* crazy dream 7 hack */ + checksum = 0; + + for (i = 0; i < 0x3b0; i++) + checksum = checksum^data[i]; + + if (strEqual(cave->name, "Crazy maze") && checksum == 195) + cave->skeletons_needed_for_pot = 0; + + return 15 + 0x49 + index; +} + +static void crazy_dream_9_add_specials(GdCave *cave, const guint8 *buf, const int length) +{ + guint8 checksum; + int i; + + /* crazy dream 9 hack */ + checksum = 0; + for (i = 0; i < length; i++) + checksum = checksum^buf[i]; + + /* check cave name and the checksum. both are hardcoded here */ + if (strEqual(cave->name, "Rockfall") && checksum == 134) + { + GdElement rand[4] = { O_DIAMOND, O_STONE, O_ACID, O_DIRT }; + gint32 prob[4] = { 37, 32, 2, 0 }; + gint32 seeds[5] = { -1, -1, -1, -1, -1 }; + + cave->objects = g_list_append(cave->objects, gd_object_new_random_fill(GD_OBJECT_LEVEL_ALL, 0, 0, 39, 21, seeds, O_DIRT, rand, prob, O_BLADDER_SPENDER, FALSE)); + } + + if (strEqual(cave->name, "Roll dice now!") && checksum == 235) + { + GdElement rand[4] = { O_STONE, O_BUTTER_3, O_DIRT, O_DIRT }; + gint32 prob[4] = { 0x18, 0x08, 0, 0 }; + gint32 seeds[5] = { -1, -1, -1, -1, -1 }; + + cave->objects = g_list_append(cave->objects, gd_object_new_random_fill(GD_OBJECT_LEVEL_ALL, 0, 0, 39, 21, seeds, O_DIRT, rand, prob, O_BLADDER_SPENDER, FALSE)); + } + + if (strEqual(cave->name, "Random maze") && checksum == 24) + { + gint32 seeds[5] = { -1, -1, -1, -1, -1 }; + cave->objects = g_list_append(cave->objects, gd_object_new_maze(GD_OBJECT_LEVEL_ALL, 1, 4, 35, 20, 1, 1, O_NONE, O_DIRT, 50, seeds)); + } + + if (strEqual(cave->name, "Metamorphosis") && checksum == 53) + { + gint32 seeds[5] = { -1, -1, -1, -1, -1 }; + GdElement rand[4] = { O_STONE, O_DIRT, O_DIRT, O_DIRT }; + gint32 prob[4] = { 0x18, 0, 0, 0 }; + + cave->objects = g_list_append(cave->objects, gd_object_new_maze(GD_OBJECT_LEVEL_ALL, 4, 1, 38, 19, 1, 3, O_NONE, O_BLADDER_SPENDER, 50, seeds)); + cave->objects = g_list_append(cave->objects, gd_object_new_random_fill(GD_OBJECT_LEVEL_ALL, 4, 1, 38, 19, seeds, O_DIRT, rand, prob, O_BLADDER_SPENDER, FALSE)); + cave->creatures_backwards = TRUE; /* for some reason, this level worked like that */ + } + + if (strEqual(cave->name, "All the way") && checksum == 33) + { + gint32 seeds[5] = { -1, -1, -1, -1, -1 }; + + cave->objects = g_list_append(cave->objects, gd_object_new_maze_unicursal(GD_OBJECT_LEVEL_ALL, 1, 1, 35, 19, 1, 1, O_BRICK, O_PRE_DIA_1, 50, seeds)); + + /* a point which "breaks" the unicursal maze, making it one very long path */ + cave->objects = g_list_append(cave->objects, gd_object_new_point(GD_OBJECT_LEVEL_ALL, 35, 18, O_BRICK)); + } +} + +/* crazy light contruction kit */ +static int cave_copy_from_crli(GdCave *cave, const guint8 *data, int remaining_bytes) +{ + guint8 uncompressed[1024]; + int datapos, cavepos, i, x, y; + boolean cavefile; + const char *versions[] = { "V2.2", "V2.6", "V3.0" }; + enum + { + none, + V2_2, /* XXX whats the difference between 2.2 and 2.6?*/ + V2_6, + V3_0 + } version = none; + GdElement (*import) (guint8 c, int i) = NULL; /* import function */ + + gd_cave_set_engine_defaults(cave, GD_ENGINE_CRLI); + + /* detect if this is a cavefile */ + if (data[0] == 0 && + data[1] == 0xc4 && + data[2] == 'D' && + data[3] == 'L' && + data[4] == 'P') + { + datapos = 5; /* cavefile, skipping 0x00 0xc4 D L P */ + cavefile = TRUE; + } + else + { + /* converted from snapshot, skip "selectable" and 14byte name */ + datapos = 15; + cavefile = FALSE; + } + + /* if we have name, convert */ + if (!cavefile) + { + gd_strcpy(cave->name, " "); + + for (i = 0; i < 14; i++) + { + int c = data[i + 1]; + + /* import cave name; a conversion table is used for each character */ + if (c < 0x40) + c = gd_bd_internal_chars[c]; + else if (c == 0x74) + c = ' '; + else if (c == 0x76) + c = '?'; + else + c = ' '; + + if (i > 0) + c = g_ascii_tolower(c); + + cave->name[i] = c; + } + + g_strchomp(cave->name); /* remove trailing and leading spaces */ + } + + /* uncompress rle data */ + cavepos = 0; + + while (cavepos < 0x3b0) + { + /* <- loop until the uncompressed reaches its size */ + if (datapos >= remaining_bytes) + { + Error("truncated crli cave data"); + return -1; + } + + if (data[datapos] == 0xbf) + { + /* magic value 0xbf is the escape byte */ + if (datapos + 2 >= remaining_bytes) + { + Error("truncated crli cave data"); + return -1; + } + + if (data[datapos + 2] + datapos >= sizeof(uncompressed)) + { + /* we would run out of buffer, this must be some error */ + Error("invalid crli cave data - RLE length value is too big"); + return -1; + } + + /* 0xbf, number, byte to dup */ + for (i = 0; i < data[datapos + 2]; i++) + uncompressed[cavepos++] = data[datapos + 1]; + + datapos += 3; + } + else + { + uncompressed[cavepos++] = data[datapos++]; + } + } + + /* check crli version */ + for (i = 0; i < G_N_ELEMENTS(versions); i++) + if (memcmp((char *)uncompressed + 0x3a0, versions[i], 4) == 0) + version = i + 1; + + /* v3.0 has falling wall and box, and no ghost. */ + import = version >= V3_0 ? crazylight_import : firstboulder_import; + + if (version == none) + { + Warn("unknown crli version %c%c%c%c", uncompressed[0x3a0], uncompressed[0x3a1], uncompressed[0x3a2], uncompressed[0x3a3]); + import = crazylight_import; + } + + /* process map */ + cave->map = gd_cave_map_new(cave, GdElement); + + for (y = 0; y < cave->h; y++) + { + for (x = 0; x < cave->w; x++) + { + int index = y * cave->w + x; + + cave->map[y][x] = import(uncompressed[index], index); + } + } + + /* crli has no levels */ + for (i = 0; i < 5; i++) + { + cave->level_time[i] = (int)uncompressed[0x370] * 100 + uncompressed[0x371] * 10 + uncompressed[0x372]; + + /* same as gate opening after 0 diamonds */ + if (cave->level_time[i] == 0) + cave->level_time[i] = 1000; + + cave->level_diamonds[i] = (int)uncompressed[0x373] * 100 + uncompressed[0x374] * 10 + uncompressed[0x375]; + + /* gate opening is checked AFTER adding to diamonds collected, so 0 here is 1000 (!) needed */ + if (cave->level_diamonds[i] == 0) + cave->level_diamonds[i] = 1000; + + cave->level_ckdelay[i] = uncompressed[0x38A]; + cave->level_amoeba_time[i] = (int)uncompressed[0x37C] * 256 + uncompressed[0x37D]; + + /* 0 immediately underflowed to 999, so we use 999. example: sendydash 3, cave 02. */ + if (cave->level_amoeba_time[i] == 0) + cave->level_amoeba_time[i] = 999; + + cave->level_magic_wall_time[i] = (int)uncompressed[0x37E] * 256 + uncompressed[0x37F]; + cave->level_slime_permeability_c64[i] = uncompressed[0x38B]; + cave->level_bonus_time[i] = uncompressed[0x392]; + cave->level_penalty_time[i] = uncompressed[0x393]; + cave->level_amoeba_threshold[i] = 256 * (int)uncompressed[0x390] + uncompressed[0x390 + 1]; + } + + cave->extra_diamond_value = (int)uncompressed[0x376] * 100 + uncompressed[0x377] * 10 + uncompressed[0x378]; + cave->diamond_value = (int)uncompressed[0x379] * 100 + uncompressed[0x37A] * 10 + uncompressed[0x37B]; + + if (uncompressed[0x380]) + cave->creatures_direction_auto_change_time = uncompressed[0x381]; + + cave->colorb = gd_c64_color(uncompressed[0x384]&0xf); /* border */ + cave->color0 = gd_c64_color(uncompressed[0x385]&0xf); + cave->color1 = gd_c64_color(uncompressed[0x386]&0xf); + cave->color2 = gd_c64_color(uncompressed[0x387]&0xf); + cave->color3 = gd_c64_color(uncompressed[0x388]&0x7); /* lower 3 bits only! */ + cave->color4 = cave->color3; + cave->color5 = cave->color1; + cave->intermission = uncompressed[0x389]!=0; + + /* if it is intermission but not scrollable */ + if (cave->intermission && !uncompressed[0x38c]) + { + cave->x2 = 19; + cave->y2 = 11; + } + + /* AMOEBA in crazy dash 8: + jsr $2500 ; generate true random + and $94 ; binary and the current "probability" + cmp #$04 ; compare to 4 + bcs out ; jump out (do not expand) if carry set, ie. result was less than 4. + + prob values can be like num = 3, 7, 15, 31, 63, ... n lsb bits count. + 0..3>=4? 0..7>=4? 0..15>=4? and similar. + this way, probability of growing is 4/(num+1) + */ + + /* probabilities store * 1M */ + cave->amoeba_growth_prob = (1E6 * 4.0 / (uncompressed[0x382] + 1)) + 0.5; + + if (cave->amoeba_growth_prob > 1000000) + cave->amoeba_growth_prob = 1000000; + + cave->amoeba_fast_growth_prob = (1E6*4.0/(uncompressed[0x383] + 1)) + 0.5; + + if (cave->amoeba_fast_growth_prob > 1000000) + cave->amoeba_fast_growth_prob = 1000000; + + /* 2c was a normal switch, 2d a changed one. */ + cave->creatures_backwards = uncompressed[0x38f] == 0x2d; + cave->magic_wall_sound = uncompressed[0x38d] == 0xf1; + + /* 2e horizontal, 2f vertical. we implement this by changing them */ + if (uncompressed[0x38e] == 0x2f) + { + for (y = 0; y < cave->h; y++) + { + for (x = 0; x < cave->w; x++) + { + if (cave->map[y][x] == O_H_EXPANDING_WALL) + cave->map[y][x] = O_V_EXPANDING_WALL; + } + } + } + + cave->biter_delay_frame = uncompressed[0x394]; + cave->magic_wall_stops_amoeba = uncompressed[0x395]==0; /* negated!! */ + cave->bomb_explosion_effect = import(uncompressed[0x396], 0x396); + cave->explosion_effect = import(uncompressed[0x397], 0x397); + cave->stone_bouncing_effect = import(uncompressed[0x398], 0x398); + cave->diamond_birth_effect = import(uncompressed[0x399], 0x399); + cave->magic_diamond_to = import(uncompressed[0x39a], 0x39a); + + cave->bladder_converts_by = import(uncompressed[0x39b], 0x39b); + cave->diamond_falling_effect = import(uncompressed[0x39c], 0x39c); + cave->biter_eat = import(uncompressed[0x39d], 0x39d); + cave->slime_eats_1 = import(uncompressed[0x39e], 0x39e); + cave->slime_converts_1 = import(uncompressed[0x39e] + 3, 0x39e); + cave->slime_eats_2 = import(uncompressed[0x39f], 0x39f); + cave->slime_converts_2 = import(uncompressed[0x39f] + 3, 0x39f); + + /* v3.0 has some new properties. */ + if (version >= V3_0) + { + cave->diagonal_movements = uncompressed[0x3a4] != 0; + cave->amoeba_too_big_effect = import(uncompressed[0x3a6], 0x3a6); + cave->amoeba_enclosed_effect = import(uncompressed[0x3a7], 0x3a7); + + /* + acid in crazy dream 8: + jsr $2500 ; true random + cmp $03a8 ; compare to ratio + bcs out ; if it was smaller, forget it for now. + + ie. random<=ratio, then acid grows. + */ + + /* * 1e6, probabilities are stored as int */ + cave->acid_spread_ratio = uncompressed[0x3a8] / 255.0 * 1E6; + cave->acid_eats_this = import(uncompressed[0x3a9], 0x3a9); + cave->expanding_wall_looks_like = import(uncompressed[0x3ab], 0x3ab); + cave->dirt_looks_like = import(uncompressed[0x3ac], 0x3ac); + } + else + { + /* version is <= 3.0, so this is a 1stb cave. */ + /* the only parameters, for which this matters, are these: */ + if (uncompressed[0x380] != 0) + cave->creatures_direction_auto_change_time = uncompressed[0x381]; + else + cave->diagonal_movements = uncompressed[0x381] != 0; + } + + if (cavefile) + cave->selectable = !cave->intermission; /* best we can do */ + else + cave->selectable = !data[0]; /* given by converter */ + + return datapos; +} + +GdCavefileFormat gd_caveset_imported_get_format(const guint8 *buf) +{ + const char *s_bd1 = "GDashBD1"; + const char *s_bd1_atari = "GDashB1A"; + const char *s_dc1 = "GDashDC1"; + const char *s_bd2 = "GDashBD2"; + const char *s_bd2_atari = "GDashB2A"; + const char *s_plc = "GDashPLC"; + const char *s_plc_atari = "GDashPCA"; + const char *s_dlb = "GDashDLB"; + const char *s_crl = "GDashCRL"; + const char *s_cd7 = "GDashCD7"; + const char *s_cd9 = "GDashCD9"; + const char *s_1st = "GDash1ST"; + + if (memcmp((char *)buf, s_bd1, strlen(s_bd1)) == 0) + return GD_FORMAT_BD1; + if (memcmp((char *)buf, s_bd1_atari, strlen(s_bd1_atari)) == 0) + return GD_FORMAT_BD1_ATARI; + if (memcmp((char *)buf, s_dc1, strlen(s_dc1)) == 0) + return GD_FORMAT_DC1; + if (memcmp((char *)buf, s_bd2, strlen(s_bd2)) == 0) + return GD_FORMAT_BD2; + if (memcmp((char *)buf, s_bd2_atari, strlen(s_bd2_atari)) == 0) + return GD_FORMAT_BD2_ATARI; + if (memcmp((char *)buf, s_plc, strlen(s_plc)) == 0) + return GD_FORMAT_PLC; + if (memcmp((char *)buf, s_plc_atari, strlen(s_plc_atari)) == 0) + return GD_FORMAT_PLC_ATARI; + if (memcmp((char *)buf, s_dlb, strlen(s_dlb)) == 0) + return GD_FORMAT_DLB; + if (memcmp((char *)buf, s_crl, strlen(s_crl)) == 0) + return GD_FORMAT_CRLI; + if (memcmp((char *)buf, s_cd7, strlen(s_cd7)) == 0) + return GD_FORMAT_CRDR_7; + if (memcmp((char *)buf, s_cd9, strlen(s_cd9)) == 0) + return GD_FORMAT_CRDR_9; + if (memcmp((char *)buf, s_1st, strlen(s_1st)) == 0) + return GD_FORMAT_FIRSTB; + + return GD_FORMAT_UNKNOWN; +} + +/* + Load caveset from memory buffer. + Loads the caveset from a memory buffer. + returns: GList * of caves. +*/ +GList *gd_caveset_import_from_buffer (const guint8 *buf, gsize length) +{ + boolean numbering; + int cavenum, intermissionnum, num; + int cavelength, bufp; + GList *caveset = NULL, *iter; + guint32 encodedlength; + GdCavefileFormat format; + + if (length != -1 && length < 12) + { + Warn("buffer too short to be a GDash datafile"); + return NULL; + } + + encodedlength = GUINT32_FROM_LE(*((guint32 *)(buf + 8))); + if (length != -1 && encodedlength != length - 12) + { + Warn("file length and data size mismatch in GDash datafile"); + return NULL; + } + + format = gd_caveset_imported_get_format(buf); + if (format==GD_FORMAT_UNKNOWN) + { + Warn("buffer does not contain a GDash datafile"); + return NULL; + } + + buf += 12; + length = encodedlength; + + bufp = 0; + cavenum = 0; + + while (bufp < length) + { + GdCave *newcave; + /* default is to append cave to caveset; g_list_insert appends when pos = -1 */ + int insertpos = -1; + + newcave = gd_cave_new(); + + cavelength = 0; /* to avoid compiler warning */ + + switch (format) + { + case GD_FORMAT_BD1: /* boulder dash 1 */ + case GD_FORMAT_BD1_ATARI: /* boulder dash 1, atari version */ + case GD_FORMAT_DC1: /* deluxe caves 1 */ + case GD_FORMAT_BD2: /* boulder dash 2 */ + case GD_FORMAT_BD2_ATARI: /* boulder dash 2 */ + /* these are not in the data so we guess */ + newcave->selectable = (cavenum < 16) && (cavenum % 4 == 0); + newcave->intermission = cavenum > 15; + + /* no name, so we make up one */ + if (newcave->intermission) + g_snprintf(newcave->name, sizeof(newcave->name), _("Intermission %d"), cavenum - 15); + else + g_snprintf(newcave->name, sizeof(newcave->name), _("Cave %c"), 'A' + cavenum); + + switch(format) + { + case GD_FORMAT_BD1: + case GD_FORMAT_BD1_ATARI: + case GD_FORMAT_DC1: + cavelength = cave_copy_from_bd1(newcave, buf + bufp, length - bufp, format); + break; + case GD_FORMAT_BD2: + case GD_FORMAT_BD2_ATARI: + cavelength = cave_copy_from_bd2(newcave, buf + bufp, length - bufp, format); + break; + + default: + break; + }; + + /* original bd1 had level order ABCDEFGH... and then the last four were the intermissions. + * those should be inserted between D-E, H-I... caves. */ + if (cavenum > 15) + insertpos = (cavenum - 15) * 5 - 1; + break; + + case GD_FORMAT_FIRSTB: + cavelength = cave_copy_from_1stb(newcave, buf + bufp, length - bufp); + + /* every fifth cave (4+1 intermission) is selectable. */ + newcave->selectable = cavenum % 5 == 0; + break; + + case GD_FORMAT_PLC: /* peter liepa construction kit */ + case GD_FORMAT_PLC_ATARI: /* peter liepa construction kit, atari version */ + cavelength = cave_copy_from_plck(newcave, buf + bufp, length - bufp, format); + break; + + case GD_FORMAT_DLB: + /* no one's delight boulder dash, something like rle compressed plck caves */ + /* but there are 20 of them, as if it was a bd1 or bd2 game. also num%5 = 4 is intermission. */ + /* we have to set intermission flag on our own, as the file did not contain the info explicitly */ + newcave->intermission = (cavenum % 5) == 4; + if (newcave->intermission) + { + /* also set visible size */ + newcave->x2 = 19; + newcave->y2 = 11; + } + + newcave->selectable = cavenum % 5 == 0; /* original selection scheme */ + if (newcave->intermission) + g_snprintf(newcave->name, sizeof(newcave->name), _("Intermission %d"), cavenum / 5 + 1); + else + g_snprintf(newcave->name, sizeof(newcave->name), _("Cave %c"), 'A'+(cavenum % 5 + cavenum / 5 * 4)); + + cavelength = cave_copy_from_dlb (newcave, buf + bufp, length - bufp); + break; + + case GD_FORMAT_CRLI: + cavelength = cave_copy_from_crli (newcave, buf + bufp, length - bufp); + break; + + case GD_FORMAT_CRDR_7: + cavelength = cave_copy_from_crdr_7 (newcave, buf + bufp, length - bufp); + break; + + case GD_FORMAT_CRDR_9: + cavelength = cave_copy_from_crli (newcave, buf + bufp, length - bufp); + if (cavelength != -1) + crazy_dream_9_add_specials(newcave, buf, cavelength); + break; + + case GD_FORMAT_UNKNOWN: + break; + } + + if (cavelength == -1) + { + gd_cave_free(newcave); + + Error("Aborting cave import."); + break; + } + else + { + caveset = g_list_insert(caveset, newcave, insertpos); + } + + cavenum++; + bufp += cavelength; + + /* hack: some dlb files contain junk data after 20 caves. */ + if (format == GD_FORMAT_DLB && cavenum == 20) + { + if (bufp < length) + Warn("excess data in dlb file, %d bytes", (int)(length-bufp)); + break; + } + } + + /* try to detect if plc caves are in standard layout. */ + /* that is, caveset looks like an original, (4 cave,1 intermission)+ */ + if (format == GD_FORMAT_PLC) + /* if no selection table stored by any2gdash */ + if ((buf[2 + 0x1f0] != buf[2 + 0x1f1] - 1) || + (buf[2 + 0x1f0] != 0x19 && buf[2 + 0x1f0] != 0x0e)) + { + GList *iter; + int n; + boolean standard; + + standard = (g_list_length(caveset)%5) == 0; /* cave count % 5 != 0 -> nonstandard */ + + for (n = 0, iter = caveset; iter != NULL; n++, iter = iter->next) + { + GdCave *cave = iter->data; + + if ((n % 5 == 4 && !cave->intermission) || + (n % 5 != 4 && cave->intermission)) + standard = FALSE; /* 4 cave, 1 intermission */ + } + + /* if test passed, update selectability */ + if (standard) + for (n = 0, iter = caveset; iter != NULL; n++, iter = iter->next) + { + GdCave *cave = iter->data; + + /* update "selectable" */ + cave->selectable = (n % 5) == 0; + } + } + + /* try to give some names for the caves */ + cavenum = 1; + intermissionnum = 1; + num = 1; + + /* use numbering instead of letters, if following formats or too many caves + (as we would run out of letters) */ + numbering = format == GD_FORMAT_PLC || format==GD_FORMAT_CRLI || g_list_length(caveset) > 26; + + for (iter = caveset; iter != NULL; iter = iter->next) + { + GdCave *cave = (GdCave *)iter->data; + + if (!strEqual(cave->name, "")) /* if it already has a name, skip */ + continue; + + if (cave->intermission) + { + /* intermission */ + if (numbering) + g_snprintf(cave->name, sizeof(cave->name), _("Intermission %02d"), num); + else + g_snprintf(cave->name, sizeof(cave->name), _("Intermission %d"), intermissionnum); + } else { + if (numbering) + g_snprintf(cave->name, sizeof(cave->name), _("Cave %02d"), num); + else + g_snprintf(cave->name, sizeof(cave->name), _("Cave %c"), 'A' - 1 + cavenum); + } + + num++; + if (cave->intermission) + intermissionnum++; + else + cavenum++; + } + + /* if the user requests, we make all caves selectable. intermissions not. */ + if (gd_import_as_all_caves_selectable) + { + for (iter = caveset; iter != NULL; iter = iter->next) + { + GdCave *cave = (GdCave *)iter->data; + + /* make selectable if not an intermission. */ + /* also selectable, if it was selectable originally, for some reason. */ + cave->selectable = cave->selectable || !cave->intermission; + } + } + + return caveset; +} + +/* to be called at program start. */ +void +gd_c64_import_init_tables(void) +{ +} diff --git a/src/game_bd/bd_c64import.h b/src/game_bd/bd_c64import.h new file mode 100644 index 00000000..936f2127 --- /dev/null +++ b/src/game_bd/bd_c64import.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef BD_CAVEIMPORT_H +#define BD_CAVEIMPORT_H + +#include + +#include "bd_cave.h" + + +extern const char gd_bd_internal_chars[]; +extern const GdElement gd_crazylight_import_table[]; + +/* file formats */ +typedef enum _gd_cavefile_format +{ + GD_FORMAT_UNKNOWN, /* unknown format */ + GD_FORMAT_BD1, /* boulder dash 1 */ + GD_FORMAT_BD1_ATARI, /* boulder dash 1 atari version */ + GD_FORMAT_DC1, /* boulder dash 1, deluxe caves 1 extension - + non-sloped brick wall. */ + GD_FORMAT_BD2, /* boulder dash 2 with rockford's extensions */ + GD_FORMAT_BD2_ATARI, /* boulder dash 2, atari version */ + GD_FORMAT_PLC, /* peter liepa construction kit */ + GD_FORMAT_PLC_ATARI, /* peter liepa construction kit, atari version */ + GD_FORMAT_DLB, /* no one's delight boulder dash */ + GD_FORMAT_CRLI, /* crazy light construction kit */ + GD_FORMAT_CRDR_7, /* crazy dream 7 */ + GD_FORMAT_CRDR_9, /* crazy dream 9 - is a crli caveset with hardcoded mazes */ + GD_FORMAT_FIRSTB, /* first boulder */ +} GdCavefileFormat; + +/* engines */ +typedef enum _gd_engine +{ + GD_ENGINE_BD1, + GD_ENGINE_BD2, + GD_ENGINE_PLCK, + GD_ENGINE_1STB, + GD_ENGINE_CRDR7, + GD_ENGINE_CRLI, + GD_ENGINE_INVALID, /* fake */ +} GdEngine; + +extern const char *gd_engines[]; + +GdCavefileFormat gd_caveset_imported_get_format(const guint8 *buf); +GList* gd_caveset_import_from_buffer (const guint8 *buf, gsize length); + +void gd_cave_set_engine_defaults(GdCave *cave, GdEngine engine); +GdEngine gd_cave_get_engine_from_string(const char *param); +GdPropertyDefault *gd_get_engine_default_array(GdEngine engine); + +void gd_c64_import_init_tables(void); + +#endif // BD_CAVEIMPORT_H diff --git a/src/game_bd/bd_cave.c b/src/game_bd/bd_cave.c new file mode 100644 index 00000000..2ffb071e --- /dev/null +++ b/src/game_bd/bd_cave.c @@ -0,0 +1,1590 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include "main_bd.h" + + +/* arrays for movements */ +/* also no1 and bd2 cave data import helpers; line direction coordinates */ +const int gd_dx[] = +{ + 0, 0, 1, 1, 1, 0, -1, -1, -1, 0, 2, 2, 2, 0, -2, -2, -2 +}; +const int gd_dy[] = +{ + 0, -1, -1, 0, 1, 1, 1, 0, -1, -2, -2, 0, 2, 2, 2, 0, -2 +}; + +/* TRANSLATORS: + None here means "no direction to move"; when there is no gravity while stirring the pot. */ +static const char* direction_name[] = +{ + N_("None"), + N_("Up"), + N_("Up+right"), + N_("Right"), + N_("Down+right"), + N_("Down"), + N_("Down+left"), + N_("Left"), + N_("Up+left") +}; + +static const char* direction_filename[] = +{ + "none", + "up", + "upright", + "right", + "downright", + "down", + "downleft", + "left", + "upleft" +}; + +static const char* scheduling_name[] = +{ + N_("Milliseconds"), + "BD1", + "BD2", + "Construction Kit", + "Crazy Dream 7", + "Atari BD1", + "Atari BD2/Construction Kit" +}; + +static const char* scheduling_filename[] = +{ + "ms", + "bd1", + "bd2", + "plck", + "crdr7", + "bd1atari", + "bd2ckatari" +}; + +static GHashTable *name_to_element; +GdElement gd_char_to_element[256]; + +/* color of flashing the screen, gate opening to exit */ +const GdColor gd_flash_color = 0xFFFFC0; + +/* selected object in editor */ +const GdColor gd_select_color = 0x8080FF; + +/* direction to string and vice versa */ +const char *gd_direction_get_visible_name(GdDirection dir) +{ + return direction_name[dir]; +} + +const char *gd_direction_get_filename(GdDirection dir) +{ + return direction_filename[dir]; +} + +GdDirection gd_direction_from_string(const char *str) +{ + int i; + + for (i = 1; i properties[n].max) + Warn("integer property %s out of range", properties[n].identifier); + ivalue[j] = defaults[i].defval; + break; + + case GD_TYPE_PROBABILITY: + /* floats are stored as integer, /million; but are integers */ + if (defaults[i].defval < 0 || + defaults[i].defval > 1000000) + Warn("integer property %s out of range", properties[n].identifier); + ivalue[j] = defaults[i].defval; + break; + + case GD_TYPE_BOOLEAN: + bvalue[j] = defaults[i].defval != 0; + break; + + case GD_TYPE_ELEMENT: + case GD_TYPE_EFFECT: + evalue[j] = (GdElement) defaults[i].defval; + break; + + case GD_TYPE_COLOR: + cvalue[j] = gd_c64_color(defaults[i].defval); + break; + + case GD_TYPE_DIRECTION: + dvalue[j] = (GdDirection) defaults[i].defval; + break; + + case GD_TYPE_SCHEDULING: + svalue[j] = (GdScheduling) defaults[i].defval; + break; + } + } + } +} + +/* creates the character->element conversion table; using + the fixed-in-the-bdcff characters. later, this table + may be filled with more elements. +*/ +void gd_create_char_to_element_table(void) +{ + int i; + + /* fill all with unknown */ + for (i = 0; i < G_N_ELEMENTS(gd_char_to_element); i++) + gd_char_to_element[i] = O_UNKNOWN; + + /* then set fixed characters */ + for (i = 0; i < O_MAX; i++) + { + int c = gd_elements[i].character; + + if (c) + { + if (gd_char_to_element[c]!=O_UNKNOWN) + Warn("Character %c already used for element %x", c, gd_char_to_element[c]); + + gd_char_to_element[c] = i; + } + } +} + +/* search the element database for the specified character, and return the element. */ +GdElement gd_get_element_from_character (guint8 character) +{ + if (gd_char_to_element[character] != O_UNKNOWN) + return gd_char_to_element[character]; + + Warn ("Invalid character representing element: %c", character); + + return O_UNKNOWN; +} + +/* + do some init; this function is to be called at the start of the application +*/ +void gd_cave_init(void) +{ + int i; + + /* put names to a hash table */ + /* this is a helper for file read operations */ + /* maps g_strdupped strings to elemenets (integers) */ + name_to_element = g_hash_table_new_full(gd_str_case_hash, gd_str_case_equal, + free, NULL); + + for (i = 0; i < O_MAX; i++) + { + char *key; + + key = g_ascii_strup(gd_elements[i].filename, -1); + + if (g_hash_table_lookup_extended(name_to_element, key, NULL, NULL)) + Warn("Name %s already used for element %x", key, i); + + g_hash_table_insert(name_to_element, key, GINT_TO_POINTER(i)); + /* ^^^ do not free "key", as hash table needs it during the whole time! */ + + key = g_strdup_printf("SCANNED_%s", key); /* new string */ + + g_hash_table_insert(name_to_element, key, GINT_TO_POINTER(i)); + /* once again, do not free "key" ^^^ */ + } + + /* for compatibility with tim stridmann's memorydump->bdcff converter... .... ... */ + g_hash_table_insert(name_to_element, "HEXPANDING_WALL", GINT_TO_POINTER(O_H_EXPANDING_WALL)); + g_hash_table_insert(name_to_element, "FALLING_DIAMOND", GINT_TO_POINTER(O_DIAMOND_F)); + g_hash_table_insert(name_to_element, "FALLING_BOULDER", GINT_TO_POINTER(O_STONE_F)); + g_hash_table_insert(name_to_element, "EXPLOSION1S", GINT_TO_POINTER(O_EXPLODE_1)); + g_hash_table_insert(name_to_element, "EXPLOSION2S", GINT_TO_POINTER(O_EXPLODE_2)); + g_hash_table_insert(name_to_element, "EXPLOSION3S", GINT_TO_POINTER(O_EXPLODE_3)); + g_hash_table_insert(name_to_element, "EXPLOSION4S", GINT_TO_POINTER(O_EXPLODE_4)); + g_hash_table_insert(name_to_element, "EXPLOSION5S", GINT_TO_POINTER(O_EXPLODE_5)); + g_hash_table_insert(name_to_element, "EXPLOSION1D", GINT_TO_POINTER(O_PRE_DIA_1)); + g_hash_table_insert(name_to_element, "EXPLOSION2D", GINT_TO_POINTER(O_PRE_DIA_2)); + g_hash_table_insert(name_to_element, "EXPLOSION3D", GINT_TO_POINTER(O_PRE_DIA_3)); + g_hash_table_insert(name_to_element, "EXPLOSION4D", GINT_TO_POINTER(O_PRE_DIA_4)); + g_hash_table_insert(name_to_element, "EXPLOSION5D", GINT_TO_POINTER(O_PRE_DIA_5)); + g_hash_table_insert(name_to_element, "WALL2", GINT_TO_POINTER(O_STEEL_EXPLODABLE)); + + /* compatibility with old bd-faq (pre disassembly of bladder) */ + g_hash_table_insert(name_to_element, "BLADDERd9", GINT_TO_POINTER(O_BLADDER_8)); + + /* create table to show errors at the start of the application */ + gd_create_char_to_element_table(); +} + +/* search the element database for the specified name, and return the element */ +GdElement gd_get_element_from_string (const char *string) +{ + char *upper = g_ascii_strup(string, -1); + gpointer value; + boolean found; + + if (!string) + { + Warn("Invalid string representing element: (null)"); + return O_UNKNOWN; + } + + found = g_hash_table_lookup_extended(name_to_element, upper, NULL, &value); + free(upper); + if (found) + return (GdElement) (GPOINTER_TO_INT(value)); + + Warn("Invalid string representing element: %s", string); + return O_UNKNOWN; +} + +void gd_cave_set_defaults_from_array(GdCave* cave, GdPropertyDefault *defaults) +{ + gd_struct_set_defaults_from_array(cave, gd_cave_properties, defaults); +} + +/* + load default values from description array + these are default for gdash and bdcff. +*/ +void gd_cave_set_gdash_defaults(GdCave* cave) +{ + int i; + + gd_cave_set_defaults_from_array(cave, gd_cave_defaults_gdash); + + /* these did not fit into the descriptor array */ + for (i = 0; i < 5; i++) + { + cave->level_rand[i] = i; + cave->level_timevalue[i] = i + 1; + } +} + +/* for quicksort. compares two highscores. */ +int gd_highscore_compare(gconstpointer a, gconstpointer b) +{ + const GdHighScore *ha = a; + const GdHighScore *hb = b; + return hb->score - ha->score; +} + +void gd_clear_highscore(GdHighScore *hs) +{ + int i; + + for (i = 0; i < GD_HIGHSCORE_NUM; i++) + { + strcpy(hs[i].name, ""); + hs[i].score = 0; + } +} + +boolean gd_has_highscore(GdHighScore *hs) +{ + return hs[0].score > 0; +} + +/* return true if score achieved is a highscore */ +boolean gd_is_highscore(GdHighScore *scores, int score) +{ + /* if score is above zero AND bigger than the last one */ + if (score > 0 && score > scores[GD_HIGHSCORE_NUM-1].score) + return TRUE; + + return FALSE; +} + +int gd_add_highscore(GdHighScore *highscores, const char *name, int score) +{ + int i; + + if (!gd_is_highscore(highscores, score)) + return -1; + + /* overwrite the last one */ + gd_strcpy(highscores[GD_HIGHSCORE_NUM-1].name, name); + highscores[GD_HIGHSCORE_NUM-1].score = score; + + /* and sort */ + qsort(highscores, GD_HIGHSCORE_NUM, sizeof(GdHighScore), gd_highscore_compare); + + for (i = 0; i < GD_HIGHSCORE_NUM; i++) + if (g_str_equal(highscores[i].name, name) && highscores[i].score == score) + return i; + + return -1; +} + +/* for the case-insensitive hash keys */ +boolean gd_str_case_equal(gconstpointer s1, gconstpointer s2) +{ + return strcasecmp(s1, s2) == 0; +} + +guint gd_str_case_hash(gconstpointer v) +{ + char *upper; + guint hash; + + upper = g_ascii_strup(v, -1); + hash = g_str_hash(v); + free(upper); + return hash; +} + +/* + create new cave with default values. + sets every value, also default size, diamond value etc. +*/ +GdCave *gd_cave_new(void) +{ + int i; + GdCave *cave; + + cave = checked_calloc(sizeof(GdCave)); + + /* hash table which stores unknown tags as strings. */ + cave->tags = g_hash_table_new_full(gd_str_case_hash, gd_str_case_equal, free, free); + + /* for strings */ + for (i = 0; gd_cave_properties[i].identifier != NULL; i++) + if (gd_cave_properties[i].type == GD_TYPE_LONGSTRING) + G_STRUCT_MEMBER(GString *, cave, gd_cave_properties[i].offset) = g_string_new(NULL); + + gd_cave_set_gdash_defaults(cave); + + return cave; +} + +/* cave maps. + cave maps are continuous areas in memory. the allocated memory + is width * height * bytes_per_cell long. + the cave map[0] stores the pointer given by g_malloc(). + the map itself is also an allocated array of pointers to the + beginning of rows. + therefore: + rows = new (pointers to rows); + rows[0] = new map + rows[1..h-1] = rows[0] + width * bytes + + freeing this: + free(rows[0]) + free(rows) +*/ + +/* + allocate a cave map-like array, and initialize to zero. + one cell is cell_size bytes long. +*/ +gpointer gd_cave_map_new_for_cave(const GdCave *cave, const int cell_size) +{ + gpointer *rows; /* this is void**, pointer to array of ... */ + int y; + + rows = checked_malloc((cave->h) * sizeof(gpointer)); + rows[0] = checked_calloc(cell_size * cave->w * cave->h); + + for (y = 1; y < cave->h; y++) + /* base pointer + num_of_bytes_per_element * width * number_of_row; as sizeof(char) = 1 */ + rows[y] = (char *)rows[0] + cell_size * cave->w * y; + + return rows; +} + +/* + duplicate map + + if map is null, this also returns null. +*/ +gpointer gd_cave_map_dup_size(const GdCave *cave, const gpointer map, const int cell_size) +{ + gpointer *rows; + gpointer *maplines = (gpointer *)map; + int y; + + if (!map) + return NULL; + + rows = checked_malloc((cave->h) * sizeof(gpointer)); + rows[0] = g_memdup (maplines[0], cell_size * cave->w * cave->h); + + for (y = 1; y < cave->h; y++) + rows[y] = (char *)rows[0] + cell_size * cave->w * y; + + return rows; +} + +void gd_cave_map_free(gpointer map) +{ + gpointer *maplines = (gpointer *) map; + + if (!map) + return; + + free(maplines[0]); + free(map); +} + +/* + frees memory associated to cave +*/ +void gd_cave_free(GdCave *cave) +{ + int i; + + if (!cave) + return; + + if (cave->tags) + g_hash_table_destroy(cave->tags); + + if (cave->random) /* random generator is a GRand * */ + g_rand_free(cave->random); + + /* free GStrings */ + for (i = 0; gd_cave_properties[i].identifier != NULL; i++) + if (gd_cave_properties[i].type==GD_TYPE_LONGSTRING) + g_string_free(G_STRUCT_MEMBER(GString *, cave, gd_cave_properties[i].offset), TRUE); + + /* map */ + gd_cave_map_free(cave->map); + + /* rendered data */ + gd_cave_map_free(cave->objects_order); + + /* hammered walls to reappear data */ + gd_cave_map_free(cave->hammered_reappear); + + /* free objects */ + g_list_foreach(cave->objects, (GFunc) free, NULL); + g_list_free (cave->objects); + + /* free replays */ + g_list_foreach(cave->replays, (GFunc) gd_replay_free, NULL); + g_list_free(cave->replays); + + /* freeing main pointer */ + free (cave); +} + +static void hash_copy_foreach(const char *key, const char *value, GHashTable *dest) +{ + g_hash_table_insert(dest, g_strdup(key), g_strdup(value)); +} + +/* copy cave from src to destination, with duplicating dynamically allocated data */ +void gd_cave_copy(GdCave *dest, const GdCave *src) +{ + int i; + + /* copy entire data */ + g_memmove(dest, src, sizeof(GdCave)); + + /* but duplicate dynamic data */ + dest->tags = g_hash_table_new_full(gd_str_case_hash, gd_str_case_equal, + free, free); + if (src->tags) + g_hash_table_foreach(src->tags, (GHFunc) hash_copy_foreach, dest->tags); + + dest->map = gd_cave_map_dup(src, map); + dest->hammered_reappear = gd_cave_map_dup(src, hammered_reappear); + + /* for longstrings */ + for (i = 0; gd_cave_properties[i].identifier != NULL; i++) + if (gd_cave_properties[i].type==GD_TYPE_LONGSTRING) + G_STRUCT_MEMBER(GString *, dest, gd_cave_properties[i].offset) = + g_string_new(G_STRUCT_MEMBER(GString *, src, gd_cave_properties[i].offset)->str); + + /* no reason to copy this */ + dest->objects_order = NULL; + + /* copy objects list */ + if (src->objects) + { + GList *iter; + + dest->objects = NULL; /* new empty list */ + for (iter = src->objects; iter != NULL; iter = iter->next) /* do a deep copy */ + dest->objects = g_list_append(dest->objects, g_memdup (iter->data, sizeof (GdObject))); + } + + /* copy replays */ + if (src->replays) + { + GList *iter; + + dest->replays = NULL; + for (iter = src->replays; iter != NULL; iter = iter->next) /* do a deep copy */ + dest->replays = g_list_append(dest->replays, gd_replay_new_from_replay(iter->data)); + } + + /* copy random number generator */ + if (src->random) + dest->random = g_rand_copy(src->random); +} + +/* create new cave, which is a copy of the cave given. */ +GdCave *gd_cave_new_from_cave(const GdCave *orig) +{ + GdCave *cave; + + cave = gd_cave_new(); + gd_cave_copy(cave, orig); + + return cave; +} + +/* + Put an object to the specified position. + Performs range checking. + If wraparound objects are selected, wraps around x coordinates, with or without lineshift. + (The y coordinate is not wrapped, as it did not work like that on the c64) + order is a pointer to the GdObject describing this object. Thus the editor can identify which cell was created by which object. +*/ +void gd_cave_store_rc(GdCave *cave, int x, int y, const GdElement element, const void *order) +{ + /* if we do not need to draw, exit now */ + if (element == O_NONE) + return; + + /* check bounds */ + if (cave->wraparound_objects) + { + if (cave->lineshift) + { + /* fit x coordinate within range, with correcting y at the same time */ + while (x < 0) + { + x += cave->w; /* out of bounds on the left... */ + y--; /* previous row */ + } + + while (x >= cave->w) + { + x -= cave->w; + y++; + } + + /* lineshifting does not fix the y coordinates. + if out of bounds, element will not be displayed. */ + /* if such an object appeared in the c64 game, well, it was a buffer overrun. */ + } + else + { + /* non lineshifting: changing x does not change y coordinate. */ + while (x < 0) + x += cave->w; + + while (x >= cave->w) + x -= cave->w; + + /* after that, fix y coordinate */ + while (y < 0) + y += cave->h; + + while (y >= cave->h) + y -= cave->h; + } + } + + /* if the above wraparound code fixed the coordinates, this will always be true. */ + /* but see the above comment for lineshifting y coordinate */ + if (x >= 0 && x < cave->w && y >= 0 && y < cave->h) + { + cave->map[y][x] = element; + cave->objects_order[y][x] = (void *)order; + } +} + +GdElement gd_cave_get_rc(const GdCave *cave, int x, int y) +{ + /* always fix coordinates as if cave was wraparound. */ + + /* fix x coordinate */ + if (cave->lineshift) + { + /* fit x coordinate within range, with correcting y at the same time */ + while (x < 0) + { + x += cave->w; /* out of bounds on the left... */ + y--; /* previous row */ + } + while (x >= cave->w) + { + x -= cave->w; + y++; + } + } + else + { + /* non lineshifting: changing x does not change y coordinate. */ + while (x < 0) + x += cave->w; + + while (x >= cave->w) + x -= cave->w; + } + + /* after that, fix y coordinate */ + while (y < 0) + y += cave->h; + + while (y >= cave->h) + y -= cave->h; + + return cave->map[y][x]; +} + +unsigned int gd_c64_random(GdC64RandomGenerator *rand) +{ + unsigned int temp_rand_1, temp_rand_2, carry, result; + + temp_rand_1 = (rand->rand_seed_1 & 0x0001) << 7; + temp_rand_2 = (rand->rand_seed_2 >> 1) & 0x007F; + result = (rand->rand_seed_2) + ((rand->rand_seed_2 & 0x0001) << 7); + carry = (result >> 8); + result = result & 0x00FF; + result = result + carry + 0x13; + carry = (result >> 8); + rand->rand_seed_2 = result & 0x00FF; + result = rand->rand_seed_1 + carry + temp_rand_1; + carry = (result >> 8); + result = result & 0x00FF; + result = result + carry + temp_rand_2; + rand->rand_seed_1 = result & 0x00FF; + + return rand->rand_seed_1; +} + +/* + C64 BD predictable random number generator. + Used to load the original caves imported from c64 files. + Also by the predictable slime. +*/ +unsigned int gd_cave_c64_random(GdCave *cave) +{ + return gd_c64_random(&cave->c64_rand); +} + +void gd_c64_random_set_seed(GdC64RandomGenerator *rand, int seed1, int seed2) +{ + rand->rand_seed_1 = seed1; + rand->rand_seed_2 = seed2; +} + +void gd_cave_c64_random_set_seed(GdCave *cave, int seed1, int seed2) +{ + gd_c64_random_set_seed(&cave->c64_rand, seed1, seed2); +} + +/* + select random colors for a given cave. + this function will select colors so that they should look somewhat nice; for example + brick walls won't be the darkest color, for example. +*/ +static inline void swap(int *i1, int *i2) +{ + int t = *i1; + *i1 = *i2; + *i2 = t; +} + +/* + shrink cave + if last line or last row is just steel wall (or (invisible) outbox). + used after loading a game for playing. + after this, ew and eh will contain the effective width and height. +*/ +void gd_cave_auto_shrink(GdCave *cave) +{ + + int x, y; + enum + { + STEEL_ONLY, + STEEL_OR_OTHER, + NO_SHRINK + } + empty; + + /* set to maximum size, then try to shrink */ + cave->x1 = 0; + cave->y1 = 0; + cave->x2 = cave->w - 1; + cave->y2 = cave->h - 1; + + /* search for empty, steel-wall-only last rows. */ + /* clear all lines, which are only steel wall. + * and clear only one line, which is steel wall, but also has a player or an outbox. */ + empty = STEEL_ONLY; + + do + { + for (y = cave->y2 - 1; y <= cave->y2; y++) + { + for (x = cave->x1; x <= cave->x2; x++) + { + switch (gd_cave_get_rc (cave, x, y)) + { + /* if steels only, this is to be deleted. */ + case O_STEEL: + break; + + case O_PRE_OUTBOX: + case O_PRE_INVIS_OUTBOX: + case O_INBOX: + if (empty == STEEL_OR_OTHER) + empty = NO_SHRINK; + + /* if this, delete only this one, and exit. */ + if (empty == STEEL_ONLY) + empty = STEEL_OR_OTHER; + break; + + default: + /* anything else, that should be left in the cave. */ + empty = NO_SHRINK; + break; + } + } + } + + /* shrink if full steel or steel and player/outbox. */ + if (empty != NO_SHRINK) + cave->y2--; /* one row shorter */ + } + while (empty == STEEL_ONLY); /* if found just steels, repeat. */ + + /* search for empty, steel-wall-only first rows. */ + empty = STEEL_ONLY; + + do + { + for (y = cave->y1; y <= cave->y1 + 1; y++) + { + for (x = cave->x1; x <= cave->x2; x++) + { + switch (gd_cave_get_rc (cave, x, y)) + { + case O_STEEL: + break; + + case O_PRE_OUTBOX: + case O_PRE_INVIS_OUTBOX: + case O_INBOX: + /* shrink only lines, which have only ONE player or outbox. + this is for bd4 intermission 2, for example. */ + if (empty==STEEL_OR_OTHER) + empty = NO_SHRINK; + if (empty==STEEL_ONLY) + empty = STEEL_OR_OTHER; + break; + + default: + empty = NO_SHRINK; + break; + } + } + } + + if (empty != NO_SHRINK) + cave->y1++; + } + while (empty == STEEL_ONLY); /* if found one, repeat. */ + + /* empty last columns. */ + empty = STEEL_ONLY; + + do + { + for (y = cave->y1; y <= cave->y2; y++) + { + for (x = cave->x2 - 1; x <= cave->x2; x++) + { + switch (gd_cave_get_rc (cave, x, y)) + { + case O_STEEL: + break; + + case O_PRE_OUTBOX: + case O_PRE_INVIS_OUTBOX: + case O_INBOX: + if (empty==STEEL_OR_OTHER) + empty = NO_SHRINK; + if (empty==STEEL_ONLY) + empty = STEEL_OR_OTHER; + break; + + default: + empty = NO_SHRINK; + break; + } + } + } + + /* just remember that one column shorter. + free will know the size of memchunk, no need to realloc! */ + if (empty != NO_SHRINK) + cave->x2--; + } + while (empty == STEEL_ONLY); /* if found one, repeat. */ + + /* empty first columns. */ + empty = STEEL_ONLY; + + do + { + for (y = cave->y1; y <= cave->y2; y++) + { + for (x = cave->x1; x <= cave->x1 + 1; x++) + { + switch (gd_cave_get_rc (cave, x, y)) + { + case O_STEEL: + break; + + case O_PRE_OUTBOX: + case O_PRE_INVIS_OUTBOX: + case O_INBOX: + if (empty==STEEL_OR_OTHER) + empty = NO_SHRINK; + if (empty==STEEL_ONLY) + empty = STEEL_OR_OTHER; + break; + + default: + empty = NO_SHRINK; + break; + } + } + } + + if (empty != NO_SHRINK) + cave->x1++; + } + while (empty == STEEL_ONLY); /* if found one, repeat. */ +} + +/* check if cave visible part coordinates + are outside cave sizes, or not in the right order. + correct them if needed. +*/ +void gd_cave_correct_visible_size(GdCave *cave) +{ + /* change visible coordinates if they do not point to upperleft and lowerright */ + if (cave->x2 < cave->x1) + { + int t = cave->x2; + cave->x2 = cave->x1; + cave->x1 = t; + } + + if (cave->y2 < cave->y1) + { + int t = cave->y2; + cave->y2 = cave->y1; + cave->y1 = t; + } + + if (cave->x1 < 0) + cave->x1 = 0; + + if (cave->y1 < 0) + cave->y1 = 0; + + if (cave->x2 > cave->w - 1) + cave->x2 = cave->w - 1; + + if (cave->y2 > cave->h - 1) + cave->y2 = cave->h - 1; +} + +/* + bd1 and similar engines had animation bits in cave data, to set which elements to animate + (firefly, butterfly, amoeba). + animating an element also caused some delay each frame; according to my measurements, + around 2.6 ms/element. +*/ +static void cave_set_ckdelay_extra_for_animation(GdCave *cave) +{ + int x, y; + boolean has_amoeba = FALSE, has_firefly = FALSE, has_butterfly = FALSE; + + for (y = 0; y < cave->h; y++) + { + for (x = 0; x < cave->w; x++) + { + switch (cave->map[y][x] & ~SCANNED) + { + case O_FIREFLY_1: + case O_FIREFLY_2: + case O_FIREFLY_3: + case O_FIREFLY_4: + has_firefly = TRUE; + break; + + case O_BUTTER_1: + case O_BUTTER_2: + case O_BUTTER_3: + case O_BUTTER_4: + has_butterfly = TRUE; + break; + + case O_AMOEBA: + has_amoeba = TRUE; + break; + } + } + } + + cave->ckdelay_extra_for_animation = 0; + if (has_amoeba) + cave->ckdelay_extra_for_animation += 2600; + if (has_firefly) + cave->ckdelay_extra_for_animation += 2600; + if (has_butterfly) + cave->ckdelay_extra_for_animation += 2600; + if (has_amoeba) + cave->ckdelay_extra_for_animation += 2600; +} + +/* do some init - setup some cave variables before the game. */ +void gd_cave_setup_for_game(GdCave *cave) +{ + int x, y; + + cave_set_ckdelay_extra_for_animation(cave); + + /* find the player which will be the one to scroll to at the beginning of the game + (before the player's birth) */ + if (cave->active_is_first_found) + { + /* uppermost player is active */ + for (y = cave->h - 1; y >= 0; y--) + { + for (x = cave->w - 1; x >= 0; x--) + { + if (cave->map[y][x] == O_INBOX) + { + cave->player_x = x; + cave->player_y = y; + } + } + } + } + else + { + /* lowermost player is active */ + for (y = 0; y < cave->h; y++) + { + for (x = 0; x < cave->w; x++) + { + if (cave->map[y][x] == O_INBOX) + { + cave->player_x = x; + cave->player_y = y; + } + } + } + } + + /* select number of milliseconds (for pal and ntsc) */ + cave->timing_factor = cave->pal_timing ? 1200 : 1000; + + cave->time *= cave->timing_factor; + cave->magic_wall_time *= cave->timing_factor; + cave->amoeba_time *= cave->timing_factor; + cave->amoeba_2_time *= cave->timing_factor; + cave->hatching_delay_time *= cave->timing_factor; + + if (cave->hammered_walls_reappear) + cave->hammered_reappear = gd_cave_map_new(cave, int); +} + +/* cave diamonds needed can be set to n<=0. */ +/* if so, count the diamonds at the time of the hatching, and decrement that value from */ +/* the number of diamonds found. */ +/* of course, this function is to be called from the cave engine, at the exact time of hatching. */ +void gd_cave_count_diamonds(GdCave *cave) +{ + int x, y; + + /* if automatically counting diamonds. if this was negative, + * the sum will be this less than the number of all the diamonds in the cave */ + if (cave->diamonds_needed <= 0) + { + for (y = 0; y < cave->h; y++) + for (x = 0; x < cave->w; x++) + if (cave->map[y][x] == O_DIAMOND) + cave->diamonds_needed++; + + /* if still below zero, let this be 0, so gate will be open immediately */ + if (cave->diamonds_needed < 0) + cave->diamonds_needed = 0; + } +} + +/* takes a cave and a gfx buffer, and fills the buffer with cell indexes. + the indexes might change if bonus life flash is active (small lines in + "SPACE" cells), + for the paused state (which is used in gdash but not in sdash) - yellowish + color. + also one can select the animation frame (0..7) to draw the cave on. so the + caller manages + increasing that. + + if a cell is changed, it is flagged with GD_REDRAW; the flag can be cleared + by the caller. +*/ +void gd_drawcave_game(const GdCave *cave, int **element_buffer, int **gfx_buffer, + boolean bonus_life_flash, int animcycle, boolean hate_invisible_outbox) +{ + static int player_blinking = 0; + static int player_tapping = 0; + int elemmapping[O_MAX]; + int elemdrawing[O_MAX]; + int x, y, map, draw; + + if (cave->last_direction) + { + /* he is moving, so stop blinking and tapping. */ + player_blinking = 0; + player_tapping = 0; + } + else + { + /* he is idle, so animations can be done. */ + if (animcycle == 0) + { + /* blinking and tapping is started at the beginning of animation sequences. */ + /* 1/4 chance of blinking, every sequence. */ + player_blinking = g_random_int_range(0, 4) == 0; + + /* 1/16 chance of starting or stopping tapping. */ + if (g_random_int_range(0, 16) == 0) + player_tapping = !player_tapping; + } + } + + for (x = 0; x < O_MAX; x++) + { + elemmapping[x] = x; + elemdrawing[x] = gd_elements[x].image_game; + } + + if (bonus_life_flash) + { + elemmapping[O_SPACE] = O_FAKE_BONUS; + elemdrawing[O_SPACE] = gd_elements[O_FAKE_BONUS].image_game; + } + + elemmapping[O_MAGIC_WALL] = (cave->magic_wall_state == GD_MW_ACTIVE ? O_MAGIC_WALL : O_BRICK); + elemdrawing[O_MAGIC_WALL] = gd_elements[cave->magic_wall_state == GD_MW_ACTIVE ? O_MAGIC_WALL : O_BRICK].image_game; + + elemmapping[O_CREATURE_SWITCH] = (cave->creatures_backwards ? O_CREATURE_SWITCH_ON : O_CREATURE_SWITCH); + elemdrawing[O_CREATURE_SWITCH] = gd_elements[cave->creatures_backwards ? O_CREATURE_SWITCH_ON : O_CREATURE_SWITCH].image_game; + + elemmapping[O_EXPANDING_WALL_SWITCH] = (cave->expanding_wall_changed ? O_EXPANDING_WALL_SWITCH_VERT : O_EXPANDING_WALL_SWITCH_HORIZ); + elemdrawing[O_EXPANDING_WALL_SWITCH] = gd_elements[cave->expanding_wall_changed ? O_EXPANDING_WALL_SWITCH_VERT : O_EXPANDING_WALL_SWITCH_HORIZ].image_game; + + elemmapping[O_GRAVITY_SWITCH] = (cave->gravity_switch_active ? O_GRAVITY_SWITCH_ACTIVE : O_GRAVITY_SWITCH); + elemdrawing[O_GRAVITY_SWITCH] = gd_elements[cave->gravity_switch_active ? O_GRAVITY_SWITCH_ACTIVE : O_GRAVITY_SWITCH].image_game; + + elemmapping[O_REPLICATOR_SWITCH] = (cave->replicators_active ? O_REPLICATOR_SWITCH_ON : O_REPLICATOR_SWITCH_OFF); + elemdrawing[O_REPLICATOR_SWITCH] = gd_elements[cave->replicators_active ? O_REPLICATOR_SWITCH_ON : O_REPLICATOR_SWITCH_OFF].image_game; + + if (cave->replicators_active) + /* if the replicators are active, animate them. */ + elemmapping[O_REPLICATOR] = O_REPLICATOR_ACTIVE; + + if (!cave->replicators_active) + /* if the replicators are inactive, do not animate them. */ + elemdrawing[O_REPLICATOR] = ABS(elemdrawing[O_REPLICATOR]); + + elemmapping[O_CONVEYOR_SWITCH] = (cave->conveyor_belts_active ? O_CONVEYOR_SWITCH_ON : O_CONVEYOR_SWITCH_OFF); + elemdrawing[O_CONVEYOR_SWITCH] = gd_elements[cave->conveyor_belts_active ? O_CONVEYOR_SWITCH_ON : O_CONVEYOR_SWITCH_OFF].image_game; + + if (cave->conveyor_belts_direction_changed) + { + /* if direction is changed, animation is changed. */ + int temp; + + elemmapping[O_CONVEYOR_LEFT] = O_CONVEYOR_RIGHT; + elemmapping[O_CONVEYOR_RIGHT] = O_CONVEYOR_LEFT; + + temp = elemdrawing[O_CONVEYOR_LEFT]; + elemdrawing[O_CONVEYOR_LEFT] = elemdrawing[O_CONVEYOR_RIGHT]; + elemdrawing[O_CONVEYOR_RIGHT] = temp; + + elemmapping[O_CONVEYOR_DIR_SWITCH] = O_CONVEYOR_DIR_CHANGED; + elemdrawing[O_CONVEYOR_DIR_SWITCH] = gd_elements[O_CONVEYOR_DIR_CHANGED].image_game; + } + else + { + elemmapping[O_CONVEYOR_DIR_SWITCH] = O_CONVEYOR_DIR_NORMAL; + elemdrawing[O_CONVEYOR_DIR_SWITCH] = gd_elements[O_CONVEYOR_DIR_NORMAL].image_game; + } + + if (cave->conveyor_belts_active) + { + /* keep potentially changed direction */ + int offset = (O_CONVEYOR_LEFT_ACTIVE - O_CONVEYOR_LEFT); + + /* if they are running, animate them. */ + elemmapping[O_CONVEYOR_LEFT] += offset; + elemmapping[O_CONVEYOR_RIGHT] += offset; + } + if (!cave->conveyor_belts_active) + { + /* if they are not running, do not animate them. */ + elemdrawing[O_CONVEYOR_LEFT] = ABS(elemdrawing[O_CONVEYOR_LEFT]); + elemdrawing[O_CONVEYOR_RIGHT] = ABS(elemdrawing[O_CONVEYOR_RIGHT]); + } + + if (animcycle & 2) + { + /* also a hack, like biter_switch */ + elemdrawing[O_PNEUMATIC_ACTIVE_LEFT] += 2; + elemdrawing[O_PNEUMATIC_ACTIVE_RIGHT] += 2; + elemdrawing[O_PLAYER_PNEUMATIC_LEFT] += 2; + elemdrawing[O_PLAYER_PNEUMATIC_RIGHT] += 2; + } + + if ((cave->last_direction) == GD_MV_STILL) + { + /* player is idle. */ + if (player_blinking && player_tapping) + { + map = O_PLAYER_TAP_BLINK; + draw = gd_elements[O_PLAYER_TAP_BLINK].image_game; + } + else if (player_blinking) + { + map = O_PLAYER_BLINK; + draw = gd_elements[O_PLAYER_BLINK].image_game; + } + else if (player_tapping) + { + map = O_PLAYER_TAP; + draw = gd_elements[O_PLAYER_TAP].image_game; + } + else + { + map = O_PLAYER; + draw = gd_elements[O_PLAYER].image_game; + } + } + else if (cave->last_horizontal_direction == GD_MV_LEFT) + { + map = O_PLAYER_LEFT; + draw = gd_elements[O_PLAYER_LEFT].image_game; + } + else + { + /* of course this is GD_MV_RIGHT. */ + map = O_PLAYER_RIGHT; + draw = gd_elements[O_PLAYER_RIGHT].image_game; + } + + elemmapping[O_PLAYER] = map; + elemmapping[O_PLAYER_GLUED] = map; + + elemdrawing[O_PLAYER] = draw; + elemdrawing[O_PLAYER_GLUED] = draw; + + /* player with bomb does not blink or tap - no graphics drawn for that. + running is drawn using w/o bomb cells */ + if (cave->last_direction!=GD_MV_STILL) + { + elemmapping[O_PLAYER_BOMB] = map; + elemdrawing[O_PLAYER_BOMB] = draw; + } + + elemmapping[O_INBOX] = (cave->inbox_flash_toggle ? O_INBOX_OPEN : O_INBOX_CLOSED); + elemdrawing[O_INBOX] = gd_elements[cave->inbox_flash_toggle ? O_OUTBOX_OPEN : O_OUTBOX_CLOSED].image_game; + + elemmapping[O_OUTBOX] = (cave->inbox_flash_toggle ? O_OUTBOX_OPEN : O_OUTBOX_CLOSED); + elemdrawing[O_OUTBOX] = gd_elements[cave->inbox_flash_toggle ? O_OUTBOX_OPEN : O_OUTBOX_CLOSED].image_game; + + /* hack, not fit into gd_elements */ + elemmapping[O_BITER_SWITCH] = O_BITER_SWITCH_1 + cave->biter_delay_frame; + /* hack, not fit into gd_elements */ + elemdrawing[O_BITER_SWITCH] = gd_elements[O_BITER_SWITCH].image_game + cave->biter_delay_frame; + + /* visual effects */ + elemmapping[O_DIRT] = cave->dirt_looks_like; + elemmapping[O_EXPANDING_WALL] = cave->expanding_wall_looks_like; + elemmapping[O_V_EXPANDING_WALL] = cave->expanding_wall_looks_like; + elemmapping[O_H_EXPANDING_WALL] = cave->expanding_wall_looks_like; + elemmapping[O_AMOEBA_2] = cave->amoeba_2_looks_like; + + /* visual effects */ + elemdrawing[O_DIRT] = elemdrawing[cave->dirt_looks_like]; + elemdrawing[O_EXPANDING_WALL] = elemdrawing[cave->expanding_wall_looks_like]; + elemdrawing[O_V_EXPANDING_WALL] = elemdrawing[cave->expanding_wall_looks_like]; + elemdrawing[O_H_EXPANDING_WALL] = elemdrawing[cave->expanding_wall_looks_like]; + elemdrawing[O_AMOEBA_2] = elemdrawing[cave->amoeba_2_looks_like]; + + /* change only graphically */ + if (hate_invisible_outbox) + { + elemmapping[O_PRE_INVIS_OUTBOX] = O_PRE_OUTBOX; + elemmapping[O_INVIS_OUTBOX] = O_OUTBOX; + } + + if (hate_invisible_outbox) + { + elemdrawing[O_PRE_INVIS_OUTBOX] = elemdrawing[O_PRE_OUTBOX]; + elemdrawing[O_INVIS_OUTBOX] = elemdrawing[O_OUTBOX]; + } + + for (y = cave->y1; y <= cave->y2; y++) + { + for (x = cave->x1; x <= cave->x2; x++) + { + GdElement actual = cave->map[y][x]; + + /* if covered, real element is not important */ + if (actual & COVERED) + map = O_COVERED; + else + map = elemmapping[actual]; + + /* if covered, real element is not important */ + if (actual & COVERED) + draw = gd_elements[O_COVERED].image_game; + else + draw = elemdrawing[actual]; + + /* if negative, animated. */ + if (draw < 0) + draw = -draw + animcycle; + + /* flash */ + if (cave->gate_open_flash) + draw += GD_NUM_OF_CELLS; + + /* set to buffer, with caching */ + if (element_buffer[y][x] != map) + element_buffer[y][x] = map; + + if (gfx_buffer[y][x] != draw) + gfx_buffer[y][x] = draw | GD_REDRAW; + } + } +} + +/* cave time is rounded _UP_ to seconds. so at the exact moment when it + changes from + 2sec remaining to 1sec remaining, the player has exactly one second. + when it changes + to zero, it is the exact moment of timeout. */ +/* internal time is milliseconds (or 1200 milliseconds for pal timing). */ +int gd_cave_time_show(const GdCave *cave, int internal_time) +{ + return (internal_time + cave->timing_factor - 1) / cave->timing_factor; +} + +GdReplay *gd_replay_new(void) +{ + GdReplay *rep; + + rep = checked_calloc(sizeof(GdReplay)); + + /* create dynamic objects */ + rep->comment = g_string_new(NULL); + rep->movements = g_byte_array_new(); + + return rep; +} + +GdReplay *gd_replay_new_from_replay(GdReplay *orig) +{ + GdReplay *rep; + + rep = g_memdup(orig, sizeof(GdReplay)); + + /* replicate dynamic data */ + rep->comment = g_string_new(orig->comment->str); + rep->movements = g_byte_array_new(); + g_byte_array_append(rep->movements, orig->movements->data, orig->movements->len); + + return rep; +} + +void gd_replay_free(GdReplay *replay) +{ + g_byte_array_free(replay->movements, TRUE); + g_string_free(replay->comment, TRUE); + free(replay); +} + +/* store movement in a replay */ +void gd_replay_store_movement(GdReplay *replay, GdDirection player_move, + boolean player_fire, boolean suicide) +{ + guint8 data[1]; + + data[0] = ((player_move) | + (player_fire ? GD_REPLAY_FIRE_MASK : 0) | + (suicide ? GD_REPLAY_SUICIDE_MASK : 0)); + + g_byte_array_append(replay->movements, data, 1); +} + +/* get next available movement from a replay; store variables to player_move, + player_fire, suicide */ +/* return true if successful */ +boolean gd_replay_get_next_movement(GdReplay *replay, GdDirection *player_move, + boolean *player_fire, boolean *suicide) +{ + guint8 data; + + /* if no more available movements */ + if (replay->current_playing_pos >= replay->movements->len) + return FALSE; + + data = replay->movements->data[replay->current_playing_pos++]; + *suicide = (data & GD_REPLAY_SUICIDE_MASK) != 0; + *player_fire = (data & GD_REPLAY_FIRE_MASK) != 0; + *player_move = (data & GD_REPLAY_MOVE_MASK); + + return TRUE; +} + +void gd_replay_rewind(GdReplay *replay) +{ + replay->current_playing_pos = 0; +} + +#define REPLAY_BDCFF_UP "u" +#define REPLAY_BDCFF_UP_RIGHT "ur" +#define REPLAY_BDCFF_RIGHT "r" +#define REPLAY_BDCFF_DOWN_RIGHT "dr" +#define REPLAY_BDCFF_DOWN "d" +#define REPLAY_BDCFF_DOWN_LEFT "dl" +#define REPLAY_BDCFF_LEFT "l" +#define REPLAY_BDCFF_UP_LEFT "ul" +/* when not moving */ +#define REPLAY_BDCFF_STILL "." +/* when the fire is pressed */ +#define REPLAY_BDCFF_FIRE "F" +#define REPLAY_BDCFF_SUICIDE "k" + +static char *direction_to_bdcff(GdDirection mov) +{ + switch (mov) + { + /* not moving */ + case GD_MV_STILL: return REPLAY_BDCFF_STILL; + + /* directions */ + case GD_MV_UP: return REPLAY_BDCFF_UP; + case GD_MV_UP_RIGHT: return REPLAY_BDCFF_UP_RIGHT; + case GD_MV_RIGHT: return REPLAY_BDCFF_RIGHT; + case GD_MV_DOWN_RIGHT: return REPLAY_BDCFF_DOWN_RIGHT; + case GD_MV_DOWN: return REPLAY_BDCFF_DOWN; + case GD_MV_DOWN_LEFT: return REPLAY_BDCFF_DOWN_LEFT; + case GD_MV_LEFT: return REPLAY_BDCFF_LEFT; + case GD_MV_UP_LEFT: return REPLAY_BDCFF_UP_LEFT; + + default: + return REPLAY_BDCFF_STILL; + } +} + +/* same as above; pressing fire will be a capital letter. */ +static char *direction_fire_to_bdcff(GdDirection dir, boolean fire) +{ + static char mov[10]; + + strcpy(mov, direction_to_bdcff(dir)); + + if (fire) + { + int i; + + for (i = 0; mov[i] != 0; i++) + mov[i] = g_ascii_toupper(mov[i]); + } + + return mov; +} + +char *gd_replay_movements_to_bdcff(GdReplay *replay) +{ + int pos; + GString *str; + + str = g_string_new(NULL); + + for (pos = 0; pos < replay->movements->len; pos++) + { + int num = 1; + guint8 data; + + /* if this is not the first movement, append a space. */ + if (str->len != 0) + g_string_append_c(str, ' '); + + /* if same byte appears, count number of occurrences - something like an rle compression. */ + /* be sure not to cross the array boundaries */ + while (pos < replay->movements->len - 1 && + replay->movements->data[pos] == replay->movements->data[pos + 1]) + { + pos++; + num++; + } + + data = replay->movements->data[pos]; + + if (data & GD_REPLAY_SUICIDE_MASK) + g_string_append(str, REPLAY_BDCFF_SUICIDE); + + g_string_append(str, direction_fire_to_bdcff(data & GD_REPLAY_MOVE_MASK, + data & GD_REPLAY_FIRE_MASK)); + + if (num != 1) + g_string_append_printf(str, "%d", num); + } + + return g_string_free(str, FALSE); +} + +/* calculate adler checksum for a rendered cave; this can be used for more caves. */ +void gd_cave_adler_checksum_more(GdCave *cave, guint32 *a, guint32 *b) +{ + int x, y; + + for (y = 0; y < cave->h; y++) + for (x = 0; x < cave->w; x++) + { + *a += gd_elements[cave->map[y][x]].character; + *b += *a; + + *a %= 65521; + *b %= 65521; + } +} + +/* calculate adler checksum for a single rendered cave. */ +guint32 +gd_cave_adler_checksum(GdCave *cave) +{ + guint32 a = 1; + guint32 b = 0; + + gd_cave_adler_checksum_more(cave, &a, &b); + return (b << 16) + a; +} + +/* return c64 color with index. */ +GdColor gd_c64_color(int index) +{ + return (GD_COLOR_TYPE_C64 << 24) + index; +} diff --git a/src/game_bd/bd_cavedb.c b/src/game_bd/bd_cavedb.c new file mode 100644 index 00000000..d9399300 --- /dev/null +++ b/src/game_bd/bd_cavedb.c @@ -0,0 +1,1097 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include "main_bd.h" + + +/* some cells are created inside the game (by merging two cells from the png etc); + those are used only in the editor. + this enum is used to give each and every one a different index automatically. */ +enum _generated_cells_indexes +{ + /* the first one gets the first available index. */ + /* the following ones will be generated by the compiler automatically. */ + i_stonefly_1 = GD_NUM_OF_CELLS_X * GD_NUM_OF_CELLS_Y, + i_stonefly_2, + i_stonefly_3, + i_stonefly_4, + i_alt_guard_1, + i_alt_guard_2, + i_alt_guard_3, + i_alt_guard_4, + i_steel_eatable, + i_brick_eatable, + i_dirt_glued, + i_diamond_glued, + i_stone_glued, + i_falling_wall, + i_falling_wall_falling, + i_expanding_wall, + i_h_expanding_wall, + i_v_expanding_wall, + i_expanding_steel_wall, + i_h_expanding_steel_wall, + i_v_expanding_steel_wall, + i_mega_stone_falling, + i_time_penalty, + i_biter_1, + i_biter_2, + i_biter_3, + i_biter_4, + i_cow_1, + i_cow_2, + i_cow_3, + i_cow_4, + i_guard_1, + i_guard_2, + i_guard_3, + i_guard_4, + i_butter_1, + i_butter_2, + i_butter_3, + i_butter_4, + i_dragonfly_1, + i_dragonfly_2, + i_dragonfly_3, + i_dragonfly_4, + i_cow_enclosed, + i_pre_outbox_nonblink, + i_invis_outbox, + i_brick_non_sloped, + i_outbox, + i_stone_f, + i_flying_stone_f, + i_diamond_f, + i_flying_diamond_f, + i_pre_invis_outbox, + i_unknown, + i_waiting_stone, + i_pre_outbox_frame_1, /* this will have 8 frames. */ + i_pre_outbox_frame_2, + i_pre_outbox_frame_3, + i_pre_outbox_frame_4, + i_pre_outbox_frame_5, + i_pre_outbox_frame_6, + i_pre_outbox_frame_7, + i_pre_outbox_frame_8, + i_nitro_pack_f, + i_alt_butter_1, + i_alt_butter_2, + i_alt_butter_3, + i_alt_butter_4, + i_conveyor_left, + i_conveyor_right, + i_nitro_explode, + i_walled_diamond, + i_walled_key_1, + i_walled_key_2, + i_walled_key_3, + i_player, + i_player_glued, + i_nut_f, + + i_max_cell_num +}; + +/* elements description array. do not miss an index! + the game will check if one is missing and stop the game. + the identifier in the saved file might also not match, reading an "outbox" from + the file should store an O_PRE_OUTBOX. + + images are: image in editor, image in editor - animated, game image + indexes which are in the png have to be given as numeric constants. + for generated cells (ie. guard + an arrow), use the above enum +*/ +GdElements gd_elements[] = +{ + { O_SPACE, N_("Space"), P_AMOEBA_CONSUMES, "SPACE", ' ', 0, 0, 0 }, + { O_DIRT, N_("Dirt"), P_AMOEBA_CONSUMES|P_VISUAL_EFFECT|P_DIRT, "DIRT", '.', 2, 2, 2 }, + { O_DIRT_SLOPED_UP_RIGHT, N_("Sloped dirt (up & right)"), P_DIRT|P_SLOPED_UP|P_SLOPED_RIGHT|P_AMOEBA_CONSUMES, "DIRTSLOPEDUPRIGHT", 0, 280, 280, 280 }, + { O_DIRT_SLOPED_UP_LEFT, N_("Sloped dirt (up & left)"), P_DIRT|P_SLOPED_UP|P_SLOPED_LEFT|P_AMOEBA_CONSUMES, "DIRTSLOPEDUPLEFT", 0, 281, 281, 281 }, + { O_DIRT_SLOPED_DOWN_LEFT, N_("Sloped dirt (down & left)"), P_DIRT|P_SLOPED_DOWN|P_SLOPED_LEFT|P_AMOEBA_CONSUMES, "DIRTSLOPEDDOWNLEFT", 0, 282, 282, 282 }, + { O_DIRT_SLOPED_DOWN_RIGHT, N_("Sloped dirt (down & right)"), P_DIRT|P_SLOPED_DOWN|P_SLOPED_RIGHT|P_AMOEBA_CONSUMES, "DIRTSLOPEDDOWNRIGHT", 0, 283, 283, 283 }, + { O_DIRT_BALL, N_("Dirt ball"), P_DIRT|P_SLOPED|P_AMOEBA_CONSUMES|P_MOVED_BY_CONVEYOR_TOP, "DIRTBALL", 0, 289, 289, 289, 120 }, /* has ckdelay */ + { O_DIRT_BALL_F, N_("Dirt ball (falling)"), 0, "DIRTBALLf", 0, 289, 289, 289, 120 }, /* has ckdelay */ + { O_DIRT_LOOSE, N_("Loose dirt"), P_DIRT|P_AMOEBA_CONSUMES|P_MOVED_BY_CONVEYOR_TOP, "DIRTLOOSE", 0, 352, 352, 352, 60 }, /* has ckdelay */ + { O_DIRT_LOOSE_F, N_("Loose dirt (falling)"), 0, "DIRTLOOSEf", 0, 352, 352, 352, 60 }, /* has ckdelay */ + { O_DIRT2, N_("Dirt 2"), P_DIRT|P_AMOEBA_CONSUMES, "DIRT2", 0, 3, 3, 3 }, + { O_BRICK, N_("Brick wall"), P_SLOPED|P_BLADDER_SLOPED|P_CAN_BE_HAMMERED, "WALL", 'w', 5, 5, 5 }, + { O_BRICK_SLOPED_UP_RIGHT, N_("Sloped brick wall (up & right)"), P_SLOPED_UP|P_SLOPED_RIGHT|P_BLADDER_SLOPED|P_CAN_BE_HAMMERED, "WALLSLOPEDUPRIGHT", 0, 276, 276, 276 }, + { O_BRICK_SLOPED_UP_LEFT, N_("Sloped brick wall (up & left)"), P_SLOPED_UP|P_SLOPED_LEFT|P_BLADDER_SLOPED|P_CAN_BE_HAMMERED, "WALLSLOPEDUPLEFT", 0, 277, 277, 277 }, + { O_BRICK_SLOPED_DOWN_LEFT, N_("Sloped brick wall (down & left)"), P_SLOPED_DOWN|P_SLOPED_LEFT|P_BLADDER_SLOPED|P_CAN_BE_HAMMERED, "WALLSLOPEDDOWNLEFT", 0, 278, 278, 278 }, + { O_BRICK_SLOPED_DOWN_RIGHT, N_("Sloped brick wall (down & right)"), P_SLOPED_DOWN|P_SLOPED_RIGHT|P_BLADDER_SLOPED|P_CAN_BE_HAMMERED, "WALLSLOPEDDOWNRIGHT", 0, 279, 279, 279 }, + { O_BRICK_NON_SLOPED, N_("Non-sloped brick wall"), P_CAN_BE_HAMMERED, "WALLNONSLOPED", 0, i_brick_non_sloped, i_brick_non_sloped, 5 }, + { O_MAGIC_WALL, N_("Magic wall"), P_CAN_BE_HAMMERED, "MAGICWALL", 'M', 184, -184, -184 }, + { O_PRE_OUTBOX, N_("Outbox"), 0, "OUTBOX", 'X', i_pre_outbox_nonblink, -i_pre_outbox_frame_1, 22 }, + { O_OUTBOX, N_("Outbox (open)"), 0, "OUTBOXopen", 0, i_outbox, i_outbox, 22 }, + { O_PRE_INVIS_OUTBOX, N_("Invisible outbox"), 0, "HIDDENOUTBOX", 'H', i_pre_invis_outbox, i_pre_invis_outbox, 22 }, + { O_INVIS_OUTBOX, N_("Invisible outbox (open)"), 0, "HIDDENOUTBOXopen", 0, i_invis_outbox, i_invis_outbox, 22 }, + { O_STEEL, N_("Steel wall"), P_NON_EXPLODABLE, "STEELWALL", 'W', 4, 4, 4 }, + { O_STEEL_SLOPED_UP_RIGHT, N_("Sloped steel wall (up & right)"), P_SLOPED_UP|P_SLOPED_RIGHT|P_NON_EXPLODABLE, "STEELWALLSLOPEDUPRIGHT", 0, 284, 284, 284 }, + { O_STEEL_SLOPED_UP_LEFT, N_("Sloped steel wall (up & left)"), P_SLOPED_UP|P_SLOPED_LEFT|P_NON_EXPLODABLE, "STEELWALLSLOPEDUPLEFT", 0, 285, 285, 285 }, + { O_STEEL_SLOPED_DOWN_LEFT, N_("Sloped steel wall (down & left)"), P_SLOPED_DOWN|P_SLOPED_LEFT|P_NON_EXPLODABLE, "STEELWALLSLOPEDDOWNLEFT", 0, 286, 286, 286 }, + { O_STEEL_SLOPED_DOWN_RIGHT, N_("Sloped steel wall (down & right)"), P_SLOPED_DOWN|P_SLOPED_RIGHT|P_NON_EXPLODABLE, "STEELWALLSLOPEDDOWNRIGHT", 0, 287, 287, 287 }, + { O_STEEL_EXPLODABLE, N_("Explodable steel wall"), P_CAN_BE_HAMMERED, "STEELWALLDESTRUCTABLE", 'E', 72, 72, 4 }, + { O_STEEL_EATABLE, N_("Eatable steel wall"), 0, "STEELWALLEATABLE", 0, i_steel_eatable, i_steel_eatable, 4 }, + { O_BRICK_EATABLE, N_("Eatable brick wall"), 0, "WALLEATABLE", 0, i_brick_eatable, i_brick_eatable, 5 }, + { O_STONE, N_("Stone"), P_SLOPED|P_MOVED_BY_CONVEYOR_TOP, "BOULDER", 'r', 1, 1, 1, 156 }, /* has ckdelay */ + { O_STONE_F, N_("Stone, falling"), 0, "BOULDERf", 'R', i_stone_f, i_stone_f, 1, 156 }, /* has ckdelay */ + { O_FLYING_STONE, N_("Flying stone"), P_SLOPED|P_MOVED_BY_CONVEYOR_BOTTOM, "FLYINGBOULDER", 0, 357, 357, 357, 156 }, /* has ckdelay */ + { O_FLYING_STONE_F, N_("Flying stone, flying"), 0, "FLYINGBOULDERf", 0, i_flying_stone_f, i_flying_stone_f, 357, 156 }, /* has ckdelay */ + { O_MEGA_STONE, N_("Mega stone"), P_SLOPED|P_MOVED_BY_CONVEYOR_TOP, "MEGABOULDER", 0, 272, 272, 272, 156 }, /* has ckdelay */ + { O_MEGA_STONE_F, N_("Mega stone, falling"), 0, "MEGABOULDERf", 0, i_mega_stone_falling, i_mega_stone_falling, 272, 156 }, /* has ckdelay */ + { O_DIAMOND, N_("Diamond"), P_SLOPED|P_MOVED_BY_CONVEYOR_TOP, "DIAMOND", 'd', 248, -248, -248, 156 }, /* has ckdelay */ + { O_DIAMOND_F, N_("Diamond, falling"), 0, "DIAMONDf", 'D', i_diamond_f, i_diamond_f, -248, 156 }, /* has ckdelay */ + { O_FLYING_DIAMOND, N_("Flying diamond"), P_SLOPED|P_MOVED_BY_CONVEYOR_BOTTOM, "FLYINGDIAMOND", 0, 344, -344, -344, 156 }, /* has ckdelay */ + { O_FLYING_DIAMOND_F, N_("Flying diamond, flying"), 0, "FLYINGDIAMONDf", 0, i_flying_diamond_f, i_flying_diamond_f, -344, 156 }, /* has ckdelay */ + { O_NUT, N_("Nut"), P_SLOPED|P_MOVED_BY_CONVEYOR_TOP, "NUT", 0, 358, 358, 358, 156 }, /* has ckdelay */ + { O_NUT_F, N_("Nut, falling"), 0, "NUTf", 0, i_nut_f, i_nut_f, 358, 156 }, /* has ckdelay */ + { O_BLADDER_SPENDER, N_("Bladder Spender"), 0, "BLADDERSPENDER", 0, 6, 6, 6, 20 }, /* has ckdelay */ + { O_INBOX, N_("Inbox"), 0, "INBOX", 'P', 35, 35, 22 }, + { O_H_EXPANDING_WALL, N_("Expanding wall, horizontal"), P_VISUAL_EFFECT | P_CAN_BE_HAMMERED, "HEXPANDINGWALL", 'x', i_h_expanding_wall, i_h_expanding_wall, 5, 111 }, /* has ckdelay */ + { O_V_EXPANDING_WALL, N_("Expanding wall, vertical"), P_VISUAL_EFFECT | P_CAN_BE_HAMMERED, "VEXPANDINGWALL", 'v', i_v_expanding_wall, i_v_expanding_wall, 5, 111 }, /* has ckdelay */ + { O_EXPANDING_WALL, N_("Expanding wall"), P_VISUAL_EFFECT | P_CAN_BE_HAMMERED, "EXPANDINGWALL", 'e', i_expanding_wall, i_expanding_wall, 5, 111 }, /* has ckdelay */ + { O_H_EXPANDING_STEEL_WALL, N_("Expanding steel wall, horizontal"), P_NON_EXPLODABLE, "HEXPANDINGSTEELWALL", 0, i_h_expanding_steel_wall, i_h_expanding_steel_wall, 4, 111 }, /* has ckdelay */ + { O_V_EXPANDING_STEEL_WALL, N_("Expanding steel wall, vertical"), P_NON_EXPLODABLE, "VEXPANDINGSTEELWALL", 0, i_v_expanding_steel_wall, i_v_expanding_steel_wall, 4, 111 }, /* has ckdelay */ + { O_EXPANDING_STEEL_WALL, N_("Expanding steel wall"), P_NON_EXPLODABLE, "EXPANDINGSTEELWALL", 0, i_expanding_steel_wall, i_expanding_steel_wall, 4, 111 }, /* has ckdelay */ + { O_EXPANDING_WALL_SWITCH, N_("Expanding wall switch"), 0, "EXPANDINGWALLSWITCH", 0, 40, 40, 40 }, + { O_CREATURE_SWITCH, N_("Creature direction switch"), 0, "FIREFLYBUTTERFLYSWITCH", 0, 18, 18, 18 }, + { O_BITER_SWITCH, N_("Biter switch"), 0, "BITERSWITCH", 0, 12, 12, 12 }, + { O_REPLICATOR_SWITCH, N_("Replicator switch"), 0, "REPLICATORSWITCH", 0, 290, 290, 290 }, + { O_CONVEYOR_SWITCH, N_("Conveyor belt power switch"), 0, "CONVEYORSWITCH", 0, 356, 356, 356 }, + { O_CONVEYOR_DIR_SWITCH, N_("Conveyor belt direction switch"), 0, "CONVEYORDIRECTIONSWITCH", 0, 353, 353, 353 }, + { O_ACID, N_("Acid"), 0, "ACID", 0, 20, 20, 20, 128 }, /* has ckdelay */ + { O_FALLING_WALL, N_("Falling wall"), P_CAN_BE_HAMMERED, "FALLINGWALL", 0, i_falling_wall, i_falling_wall, 5, 80 }, /* has ckdelay */ + { O_FALLING_WALL_F, N_("Falling wall, falling"), P_CAN_BE_HAMMERED, "FALLINGWALLf", 0, i_falling_wall_falling, i_falling_wall_falling, 5, 80 }, /* has ckdelay */ + { O_BOX, N_("Box"), 0, "SOKOBANBOX", 0, 21, 21, 21 }, + { O_TIME_PENALTY, N_("Time penalty"), P_NON_EXPLODABLE, "TIMEPENALTY", 0, i_time_penalty, i_time_penalty, 9 }, + { O_GRAVESTONE, N_("Gravestone"), P_NON_EXPLODABLE, "GRAVESTONE", 'G', 9, 9, 9 }, + { O_STONE_GLUED, N_("Glued stone"), P_SLOPED, "GLUEDBOULDER", 0, i_stone_glued, i_stone_glued, 1 }, + { O_DIAMOND_GLUED, N_("Glued diamond"), P_SLOPED, "GLUEDDIAMOND", 0, i_diamond_glued, i_diamond_glued, -248 }, + { O_DIAMOND_KEY, N_("Diamond key"), 0, "DIAMONDRELEASEKEY", 0, 11, 11, 11 }, + { O_TRAPPED_DIAMOND, N_("Trapped diamond"), P_NON_EXPLODABLE, "TRAPPEDDIAMOND", 0, 10, 10, 10 }, + { O_CLOCK, N_("Clock"), 0, "CLOCK", 0, 16, 16, 16 }, + { O_DIRT_GLUED, N_("Glued dirt"), 0, "GLUEDDIRT", 0, i_dirt_glued, i_dirt_glued, 2 }, + { O_KEY_1, N_("Key 1"), 0, "KEY1", 0, 67, 67, 67 }, + { O_KEY_2, N_("Key 2"), 0, "KEY2", 0, 68, 68, 68 }, + { O_KEY_3, N_("Key 3"), 0, "KEY3", 0, 69, 69, 69 }, + { O_DOOR_1, N_("Door 1"), 0, "DOOR1", 0, 64, 64, 64 }, + { O_DOOR_2, N_("Door 2"), 0, "DOOR2", 0, 65, 65, 65 }, + { O_DOOR_3, N_("Door 3"), 0, "DOOR3", 0, 66, 66, 66 }, + + { O_POT, N_("Pot"), 0, "POT", 0, 63, 63, 63 }, + { O_GRAVITY_SWITCH, N_("Gravity switch"), 0, "GRAVITY_SWITCH", 0, 274, 274, 274 }, + { O_PNEUMATIC_HAMMER, N_("Pneumatic hammer"), 0, "PNEUMATIC_HAMMER", 0, 62, 62, 62 }, + { O_TELEPORTER, N_("Teleporter"), 0, "TELEPORTER", 0, 61, 61, 61 }, + { O_SKELETON, N_("Skeleton"), 0, "SKELETON", 0, 273, 273, 273 }, + { O_WATER, N_("Water"), 0, "WATER", 0, 96, -96, -96, 100 }, /* has ckdelay */ + { O_WATER_1, N_("Water (1)"), 0, "WATER1", 0, 96, -96, -96 }, + { O_WATER_2, N_("Water (2)"), 0, "WATER2", 0, 96, -96, -96 }, + { O_WATER_3, N_("Water (3)"), 0, "WATER3", 0, 96, -96, -96 }, + { O_WATER_4, N_("Water (4)"), 0, "WATER4", 0, 96, -96, -96 }, + { O_WATER_5, N_("Water (5)"), 0, "WATER5", 0, 96, -96, -96 }, + { O_WATER_6, N_("Water (6)"), 0, "WATER6", 0, 96, -96, -96 }, + { O_WATER_7, N_("Water (7)"), 0, "WATER7", 0, 96, -96, -96 }, + { O_WATER_8, N_("Water (8)"), 0, "WATER8", 0, 96, -96, -96 }, + { O_WATER_9, N_("Water (9)"), 0, "WATER9", 0, 96, -96, -96 }, + { O_WATER_10, N_("Water (10)"), 0, "WATER10", 0, 96, -96, -96 }, + { O_WATER_11, N_("Water (11)"), 0, "WATER11", 0, 96, -96, -96 }, + { O_WATER_12, N_("Water (12)"), 0, "WATER12", 0, 96, -96, -96 }, + { O_WATER_13, N_("Water (13)"), 0, "WATER13", 0, 96, -96, -96 }, + { O_WATER_14, N_("Water (14)"), 0, "WATER14", 0, 96, -96, -96 }, + { O_WATER_15, N_("Water (15)"), 0, "WATER15", 0, 96, -96, -96 }, + { O_WATER_16, N_("Water (16)"), 0, "WATER16", 0, 96, -96, -96 }, + { O_COW_1, N_("Cow (left)"), P_CCW, "COWl", 0, i_cow_1, -88, -88, 384 }, /* has ckdelay */ + { O_COW_2, N_("Cow (up)"), P_CCW, "COWu", 0, i_cow_2, -88, -88, 384 }, /* has ckdelay */ + { O_COW_3, N_("Cow (right)"), P_CCW, "COWr", 0, i_cow_3, -88, -88, 384 }, /* has ckdelay */ + { O_COW_4, N_("Cow (down)"), P_CCW, "COWd", 0, i_cow_4, -88, -88, 384 }, /* has ckdelay */ + { O_COW_ENCLOSED_1, N_("Cow (enclosed, 1)"), 0, "COW_ENCLOSED1", 0, i_cow_enclosed, -88, -88, 120 }, /* has ckdelay */ + { O_COW_ENCLOSED_2, N_("Cow (enclosed, 2)"), 0, "COW_ENCLOSED2", 0, i_cow_enclosed, -88, -88, 120 }, /* has ckdelay */ + { O_COW_ENCLOSED_3, N_("Cow (enclosed, 3)"), 0, "COW_ENCLOSED3", 0, i_cow_enclosed, -88, -88, 120 }, /* has ckdelay */ + { O_COW_ENCLOSED_4, N_("Cow (enclosed, 4)"), 0, "COW_ENCLOSED4", 0, i_cow_enclosed, -88, -88, 120 }, /* has ckdelay */ + { O_COW_ENCLOSED_5, N_("Cow (enclosed, 5)"), 0, "COW_ENCLOSED5", 0, i_cow_enclosed, -88, -88, 120 }, /* has ckdelay */ + { O_COW_ENCLOSED_6, N_("Cow (enclosed, 6)"), 0, "COW_ENCLOSED6", 0, i_cow_enclosed, -88, -88, 120 }, /* has ckdelay */ + { O_COW_ENCLOSED_7, N_("Cow (enclosed, 7)"), 0, "COW_ENCLOSED7", 0, i_cow_enclosed, -88, -88, 120 }, /* has ckdelay */ + { O_WALLED_DIAMOND, N_("Walled diamond"), P_CAN_BE_HAMMERED, "WALLED_DIAMOND", 0, i_walled_diamond, i_walled_diamond, 5 }, + { O_WALLED_KEY_1, N_("Walled key 1"), P_CAN_BE_HAMMERED, "WALLED_KEY1", 0, i_walled_key_1, i_walled_key_1, 5 }, + { O_WALLED_KEY_2, N_("Walled key 2"), P_CAN_BE_HAMMERED, "WALLED_KEY2", 0, i_walled_key_2, i_walled_key_2, 5 }, + { O_WALLED_KEY_3, N_("Walled key 3"), P_CAN_BE_HAMMERED, "WALLED_KEY3", 0, i_walled_key_3, i_walled_key_3, 5 }, + + { O_AMOEBA, N_("Amoeba"), P_BLOWS_UP_FLIES, "AMOEBA", 'a', 192, -192, -192, 260 }, /* has ckdelay */ + { O_AMOEBA_2, N_("Amoeba 2"), P_BLOWS_UP_FLIES|P_VISUAL_EFFECT, "AMOEBA2", 0, 296, -296, -296, 260 }, /* has ckdelay */ + { O_REPLICATOR, N_("Replicator"), P_NON_EXPLODABLE, "REPLICATOR", 0, 304, 304, 304, 210 }, /* has ckdelay */ + { O_CONVEYOR_LEFT, N_("Conveyor belt (left)"), P_NON_EXPLODABLE, "CONVEYORLEFT", 0, i_conveyor_left, -328, -328, 256 }, /* has ckdelay */ + { O_CONVEYOR_RIGHT, N_("Conveyor belt (right)"), P_NON_EXPLODABLE, "CONVEYORRIGHT", 0, i_conveyor_right, -320, -320 }, + { O_LAVA, N_("Lava"), P_NON_EXPLODABLE, "LAVA", 0, 312, -312, -312 }, + { O_SWEET, N_("Sweet"), 0, "SWEET", 0, 8, 8, 8 }, + { O_VOODOO, N_("Voodoo doll"), P_BLOWS_UP_FLIES, "DUMMY", 'F', 7, 7, 7 }, + { O_SLIME, N_("Slime"), 0, "SLIME", 's', 200, -200, -200, 211 }, /* has ckdelay */ + { O_BLADDER, N_("Bladder"), 0, "BLADDER", 0, 176, -176, -176, 267 }, /* has ckdelay */ + { O_BLADDER_1, N_("Bladder (1)"), 0, "BLADDERd1", 0, 176, -176, -176 }, + { O_BLADDER_2, N_("Bladder (2)"), 0, "BLADDERd2", 0, 176, -176, -176 }, + { O_BLADDER_3, N_("Bladder (3)"), 0, "BLADDERd3", 0, 176, -176, -176 }, + { O_BLADDER_4, N_("Bladder (4)"), 0, "BLADDERd4", 0, 176, -176, -176 }, + { O_BLADDER_5, N_("Bladder (5)"), 0, "BLADDERd5", 0, 176, -176, -176 }, + { O_BLADDER_6, N_("Bladder (6)"), 0, "BLADDERd6", 0, 176, -176, -176 }, + { O_BLADDER_7, N_("Bladder (7)"), 0, "BLADDERd7", 0, 176, -176, -176 }, + { O_BLADDER_8, N_("Bladder (8)"), 0, "BLADDERd8", 0, 176, -176, -176 }, + + { O_WAITING_STONE, N_("Waiting stone"), P_SLOPED, "WAITINGBOULDER", 0, i_waiting_stone, i_waiting_stone, 1, 176 }, /* has ckdelay */ + { O_CHASING_STONE, N_("Chasing stone"), P_SLOPED, "CHASINGBOULDER", 0, 17, 17, 17, 269 }, /* has ckdelay */ + { O_GHOST, N_("Ghost"), 0, "GHOST", 'g', 160, -160, -160, 50 }, /* has ckdelay */ + { O_FIREFLY_1, N_("Guard, left"), P_EXPLODES_BY_HIT | P_CCW, "FIREFLYl", 'Q', i_guard_1, -136, -136, 384 }, /* has ckdelay */ + { O_FIREFLY_2, N_("Guard, up"), P_EXPLODES_BY_HIT | P_CCW, "FIREFLYu", 'o', i_guard_2, -136, -136, 384 }, /* has ckdelay */ + { O_FIREFLY_3, N_("Guard, right"), P_EXPLODES_BY_HIT | P_CCW, "FIREFLYr", 'O', i_guard_3, -136, -136, 384 }, /* has ckdelay */ + { O_FIREFLY_4, N_("Guard, down"), P_EXPLODES_BY_HIT | P_CCW, "FIREFLYd", 'q', i_guard_4, -136, -136, 384 }, /* has ckdelay */ + { O_ALT_FIREFLY_1, N_("Alternative guard, left"), P_EXPLODES_BY_HIT, "A_FIREFLYl", 0, i_alt_guard_1, -104, -104, 384 }, /* has ckdelay */ + { O_ALT_FIREFLY_2, N_("Alternative guard, up"), P_EXPLODES_BY_HIT, "A_FIREFLYu", 0, i_alt_guard_2, -104, -104, 384 }, /* has ckdelay */ + { O_ALT_FIREFLY_3, N_("Alternative guard, right"), P_EXPLODES_BY_HIT, "A_FIREFLYr", 0, i_alt_guard_3, -104, -104, 384 }, /* has ckdelay */ + { O_ALT_FIREFLY_4, N_("Alternative guard, down"), P_EXPLODES_BY_HIT, "A_FIREFLYd", 0, i_alt_guard_4, -104, -104, 384 }, /* has ckdelay */ + { O_BUTTER_1, N_("Butterfly, left"), P_EXPLODES_BY_HIT, "BUTTERFLYl", 'C', i_butter_1, -144, -144, 384 }, /* has ckdelay */ + { O_BUTTER_2, N_("Butterfly, up"), P_EXPLODES_BY_HIT, "BUTTERFLYu", 'b', i_butter_2, -144, -144, 384 }, /* has ckdelay */ + { O_BUTTER_3, N_("Butterfly, right"), P_EXPLODES_BY_HIT, "BUTTERFLYr", 'B', i_butter_3, -144, -144, 384 }, /* has ckdelay */ + { O_BUTTER_4, N_("Butterfly, down"), P_EXPLODES_BY_HIT, "BUTTERFLYd", 'c', i_butter_4, -144, -144, 384 }, /* has ckdelay */ + { O_ALT_BUTTER_1, N_("Alternative butterfly, left"), P_EXPLODES_BY_HIT | P_CCW, "A_BUTTERFLYl", 0, i_alt_butter_1, -112, -112, 384 }, /* has ckdelay */ + { O_ALT_BUTTER_2, N_("Alternative butterfly, up"), P_EXPLODES_BY_HIT | P_CCW, "A_BUTTERFLYu", 0, i_alt_butter_2, -112, -112, 384 }, /* has ckdelay */ + { O_ALT_BUTTER_3, N_("Alternative butterfly, right"), P_EXPLODES_BY_HIT | P_CCW, "A_BUTTERFLYr", 0, i_alt_butter_3, -112, -112, 384 }, /* has ckdelay */ + { O_ALT_BUTTER_4, N_("Alternative butterfly, down"), P_EXPLODES_BY_HIT | P_CCW, "A_BUTTERFLYd", 0, i_alt_butter_4, -112, -112, 384 }, /* has ckdelay */ + { O_STONEFLY_1, N_("Stonefly, left"), P_EXPLODES_BY_HIT, "STONEFLYl", 0, i_stonefly_1, -152, -152, 384 }, /* has ckdelay */ + { O_STONEFLY_2, N_("Stonefly, up"), P_EXPLODES_BY_HIT, "STONEFLYu", 0, i_stonefly_2, -152, -152, 384 }, /* has ckdelay */ + { O_STONEFLY_3, N_("Stonefly, right"), P_EXPLODES_BY_HIT, "STONEFLYr", 0, i_stonefly_3, -152, -152, 384 }, /* has ckdelay */ + { O_STONEFLY_4, N_("Stonefly, down"), P_EXPLODES_BY_HIT, "STONEFLYd", 0, i_stonefly_4, -152, -152, 384 }, /* has ckdelay */ + { O_BITER_1, N_("Biter, up"), 0, "BITERu", 0, i_biter_1, -168, -168, 518 }, /* has ckdelay */ + { O_BITER_2, N_("Biter, right"), 0, "BITERr", 0, i_biter_2, -168, -168, 518 }, /* has ckdelay */ + { O_BITER_3, N_("Biter, down"), 0, "BITERd", 0, i_biter_3, -168, -168, 518 }, /* has ckdelay */ + { O_BITER_4, N_("Biter, left"), 0, "BITERl", 0, i_biter_4, -168, -168, 518 }, /* has ckdelay */ + { O_DRAGONFLY_1, N_("Dragonfly, left"), P_EXPLODES_BY_HIT|P_CCW, "DRAGONFLYl", 0, i_dragonfly_1, -336, -336, 256 }, /* has ckdelay */ + { O_DRAGONFLY_2, N_("Dragonfly, up"), P_EXPLODES_BY_HIT|P_CCW, "DRAGONFLYu", 0, i_dragonfly_2, -336, -336, 256 }, /* has ckdelay */ + { O_DRAGONFLY_3, N_("Dragonfly, right"), P_EXPLODES_BY_HIT|P_CCW, "DRAGONFLYr", 0, i_dragonfly_3, -336, -336, 256 }, /* has ckdelay */ + { O_DRAGONFLY_4, N_("Dragonfly, down"), P_EXPLODES_BY_HIT|P_CCW, "DRAGONFLYd", 0, i_dragonfly_4, -336, -336, 256 }, /* has ckdelay */ + + { O_PRE_PL_1, N_("Player birth (1)"), 0, "GUYBIRTH1", 0, 32, 32, 32 }, + { O_PRE_PL_2, N_("Player birth (2)"), 0, "GUYBIRTH2", 0, 33, 33, 33 }, + { O_PRE_PL_3, N_("Player birth (3)"), 0, "GUYBIRTH3", 0, 34, 34, 34 }, + { O_PLAYER, N_("Player"), P_BLOWS_UP_FLIES | P_EXPLODES_BY_HIT | P_PLAYER, "GUY", 0, i_player, i_player, 35, 32 }, /* has ckdelay */ + { O_PLAYER_BOMB, N_("Player with bomb"), P_BLOWS_UP_FLIES | P_EXPLODES_BY_HIT | P_PLAYER, "GUYBOMB", 0, 42, 42, 42, 25 }, /* has ckdelay */ + { O_PLAYER_GLUED, N_("Glued player"), P_BLOWS_UP_FLIES | P_EXPLODES_BY_HIT, "GUYGLUED", 0, i_player_glued, i_player_glued, 35 }, /* is not a real player! so active x, y will not find it. no P_PLAYER bit! */ + { O_PLAYER_STIRRING, N_("Player stirring"), P_BLOWS_UP_FLIES | P_EXPLODES_BY_HIT | P_PLAYER, "GUYSTIRRING", 0, 256, -256, -256 }, + + { O_BOMB, N_("Bomb"), 0, "BOMB", 0, 48, 48, 48 }, + { O_BOMB_TICK_1, N_("Ticking bomb (1)"), P_EXPLOSION_FIRST_STAGE, "IGNITEDBOMB1", 0, 49, 49, 49 }, + { O_BOMB_TICK_2, N_("Ticking bomb (2)"), 0, "IGNITEDBOMB2", 0, 50, 50, 50 }, + { O_BOMB_TICK_3, N_("Ticking bomb (3)"), 0, "IGNITEDBOMB3", 0, 51, 51, 51 }, + { O_BOMB_TICK_4, N_("Ticking bomb (4)"), 0, "IGNITEDBOMB4", 0, 52, 52, 52 }, + { O_BOMB_TICK_5, N_("Ticking bomb (5)"), 0, "IGNITEDBOMB5", 0, 53, 53, 53 }, + { O_BOMB_TICK_6, N_("Ticking bomb (6)"), 0, "IGNITEDBOMB6", 0, 54, 54, 54 }, + { O_BOMB_TICK_7, N_("Ticking bomb (7)"), 0, "IGNITEDBOMB7", 0, 55, 55, 55 }, + + { O_NITRO_PACK, N_("Nitro pack"), P_SLOPED|P_EXPLODES_BY_HIT|P_MOVED_BY_CONVEYOR_TOP, "NITRO", 0, 288, 288, 288 }, + { O_NITRO_PACK_F, N_("Nitro pack, falling"), P_EXPLODES_BY_HIT, "NITROf", 0, i_nitro_pack_f, i_nitro_pack_f, 288 }, + { O_NITRO_PACK_EXPLODE, N_("Nitro pack, triggered"), P_EXPLODES_BY_HIT, "NITROtriggered", 0, i_nitro_explode, i_nitro_explode, 288 }, + + { O_PRE_CLOCK_1, N_("Clock birth (1)"), P_EXPLOSION_FIRST_STAGE, "CLOCKBIRTH1", 0, 28, 28, 28, 280 }, /* has ckdelay */ + { O_PRE_CLOCK_2, N_("Clock birth (2)"), 0, "CLOCKBIRTH2", 0, 29, 29, 29, 280 }, /* has ckdelay */ + { O_PRE_CLOCK_3, N_("Clock birth (3)"), 0, "CLOCKBIRTH3", 0, 30, 30, 30, 280 }, /* has ckdelay */ + { O_PRE_CLOCK_4, N_("Clock birth (4)"), 0, "CLOCKBIRTH4", 0, 31, 31, 31, 280 }, /* has ckdelay */ + { O_PRE_DIA_1, N_("Diamond birth (1)"), P_EXPLOSION_FIRST_STAGE, "DIAMONDBIRTH1", 0, 56, 56, 56, 280 }, /* has ckdelay */ + { O_PRE_DIA_2, N_("Diamond birth (2)"), 0, "DIAMONDBIRTH2", 0, 57, 57, 57, 280 }, /* has ckdelay */ + { O_PRE_DIA_3, N_("Diamond birth (3)"), 0, "DIAMONDBIRTH3", 0, 58, 58, 58, 280 }, /* has ckdelay */ + { O_PRE_DIA_4, N_("Diamond birth (4)"), 0, "DIAMONDBIRTH4", 0, 59, 59, 59, 280 }, /* has ckdelay */ + { O_PRE_DIA_5, N_("Diamond birth (5)"), 0, "DIAMONDBIRTH5", 0, 60, 60, 60, 280 }, /* has ckdelay */ + { O_EXPLODE_1, N_("Explosion (1)"), P_EXPLOSION_FIRST_STAGE, "EXPLOSION1", 0, 43, 43, 43, 280 }, /* has ckdelay */ + { O_EXPLODE_2, N_("Explosion (2)"), 0, "EXPLOSION2", 0, 44, 44, 44, 280 }, /* has ckdelay */ + { O_EXPLODE_3, N_("Explosion (3)"), 0, "EXPLOSION3", 0, 45, 45, 45, 280 }, /* has ckdelay */ + { O_EXPLODE_4, N_("Explosion (4)"), 0, "EXPLOSION4", 0, 46, 46, 46, 280 }, /* has ckdelay */ + { O_EXPLODE_5, N_("Explosion (5)"), 0, "EXPLOSION5", 0, 47, 47, 47, 280 }, /* has ckdelay */ + { O_PRE_STONE_1, N_("Stone birth (1)"), P_EXPLOSION_FIRST_STAGE, "BOULDERBIRTH1", 0, 36, 36, 36, 280 }, /* has ckdelay */ + { O_PRE_STONE_2, N_("Stone birth (2)"), 0, "BOULDERBIRTH2", 0, 37, 37, 37, 280 }, /* has ckdelay */ + { O_PRE_STONE_3, N_("Stone birth (3)"), 0, "BOULDERBIRTH3", 0, 38, 38, 38, 280 }, /* has ckdelay */ + { O_PRE_STONE_4, N_("Stone birth (4)"), 0, "BOULDERBIRTH4", 0, 39, 39, 39, 280 }, /* has ckdelay */ + { O_PRE_STEEL_1, N_("Steel birth (1)"), P_EXPLOSION_FIRST_STAGE, "STEELWALLBIRTH1", 0, 24, 24, 24, 280 }, /* has ckdelay */ + { O_PRE_STEEL_2, N_("Steel birth (2)"), 0, "STEELWALLBIRTH2", 0, 25, 25, 25, 280 }, /* has ckdelay */ + { O_PRE_STEEL_3, N_("Steel birth (3)"), 0, "STEELWALLBIRTH3", 0, 26, 26, 26, 280 }, /* has ckdelay */ + { O_PRE_STEEL_4, N_("Steel birth (4)"), 0, "STEELWALLBIRTH4", 0, 27, 27, 27, 280 }, /* has ckdelay */ + { O_GHOST_EXPL_1, N_("Ghost explosion (1)"), P_EXPLOSION_FIRST_STAGE, "GHOSTEXPLOSION1", 0, 80, 80, 80, 280 }, /* has ckdelay */ + { O_GHOST_EXPL_2, N_("Ghost explosion (2)"), 0, "GHOSTEXPLOSION2", 0, 81, 81, 81, 280 }, /* has ckdelay */ + { O_GHOST_EXPL_3, N_("Ghost explosion (3)"), 0, "GHOSTEXPLOSION3", 0, 82, 82, 82, 280 }, /* has ckdelay */ + { O_GHOST_EXPL_4, N_("Ghost explosion (4)"), 0, "GHOSTEXPLOSION4", 0, 83, 83, 83, 280 }, /* has ckdelay */ + { O_BOMB_EXPL_1, N_("Bomb explosion (1)"), P_EXPLOSION_FIRST_STAGE, "BOMBEXPLOSION1", 0, 84, 84, 84, 280 }, /* has ckdelay */ + { O_BOMB_EXPL_2, N_("Bomb explosion (2)"), 0, "BOMBEXPLOSION2", 0, 85, 85, 85, 280 }, /* has ckdelay */ + { O_BOMB_EXPL_3, N_("Bomb explosion (3)"), 0, "BOMBEXPLOSION3", 0, 86, 86, 86, 280 }, /* has ckdelay */ + { O_BOMB_EXPL_4, N_("Bomb explosion (4)"), 0, "BOMBEXPLOSION4", 0, 87, 87, 87, 280 }, /* has ckdelay */ + { O_NITRO_EXPL_1, N_("Nitro pack explosion (1)"), P_EXPLOSION_FIRST_STAGE, "NITROEXPLOSION1", 0, 44, 44, 44, 280 }, /* has ckdelay */ + { O_NITRO_EXPL_2, N_("Nitro pack explosion (2)"), 0, "NITROEXPLOSION2", 0, 45, 45, 45, 280 }, /* has ckdelay */ + { O_NITRO_EXPL_3, N_("Nitro pack explosion (3)"), 0, "NITROEXPLOSION3", 0, 46, 46, 46, 280 }, /* has ckdelay */ + { O_NITRO_EXPL_4, N_("Nitro pack explosion (4)"), 0, "NITROEXPLOSION4", 0, 47, 47, 47, 280 }, /* has ckdelay */ + { O_AMOEBA_2_EXPL_1, N_("Amoeba 2 explosion (1)"), P_EXPLOSION_FIRST_STAGE, "AMOEBA2EXPLOSION1", 0, 292, 292, 292, 280 }, /* has ckdelay */ + { O_AMOEBA_2_EXPL_2, N_("Amoeba 2 explosion (2)"), 0, "AMOEBA2EXPLOSION2", 0, 293, 293, 293, 280 }, /* has ckdelay */ + { O_AMOEBA_2_EXPL_3, N_("Amoeba 2 explosion (3)"), 0, "AMOEBA2EXPLOSION3", 0, 294, 294, 294, 280 }, /* has ckdelay */ + { O_AMOEBA_2_EXPL_4, N_("Amoeba 2 explosion (4)"), 0, "AMOEBA2EXPLOSION4", 0, 295, 295, 295, 280 }, /* has ckdelay */ + { O_NUT_EXPL_1, N_("Nut explosion (1)"), P_SLOPED | P_EXPLOSION_FIRST_STAGE, "NUTEXPLOSION1", 0, 360, 360, 360, 280 }, /* has ckdelay */ + { O_NUT_EXPL_2, N_("Nut explosion (2)"), P_SLOPED, "NUTEXPLOSION2", 0, 361, 361, 361, 280 }, /* has ckdelay */ /* these are rounded!! */ + { O_NUT_EXPL_3, N_("Nut explosion (3)"), P_SLOPED, "NUTEXPLOSION3", 0, 362, 362, 362, 280 }, /* has ckdelay */ + { O_NUT_EXPL_4, N_("Nut explosion (4)"), P_SLOPED, "NUTEXPLOSION4", 0, 363, 363, 363, 280 }, /* has ckdelay */ + + { O_PLAYER_PNEUMATIC_LEFT, NULL /* Player using hammer, left */, P_BLOWS_UP_FLIES | P_EXPLODES_BY_HIT | P_PLAYER, "GUYHAMMERl", 0, 265, 265, 265 }, + { O_PLAYER_PNEUMATIC_RIGHT, NULL /* Player using hammer, right */, P_BLOWS_UP_FLIES | P_EXPLODES_BY_HIT | P_PLAYER, "GUYHAMMERr", 0, 268, 268, 268 }, + { O_PNEUMATIC_ACTIVE_LEFT, NULL /* Active hammer, left */, 0, "HAMMERACTIVEl", 0, 264, 264, 264 }, + { O_PNEUMATIC_ACTIVE_RIGHT, NULL /* Active hammer, right */, 0, "HAMMERACTIVEr", 0, 269, 269, 269 }, + + { O_UNKNOWN, N_("Unknown element"), P_NON_EXPLODABLE, "UNKNOWN", 0, i_unknown, i_unknown, 4 }, + { O_NONE, N_("No element"), 0, "NONE", 0, 79, 79, 79 }, + { O_MAX }, + + /* these are just helpers, for all the element -> image index information to be in this array */ + { O_FAKE_BONUS, NULL, 0, NULL, 0, 120, -120, -120 }, + { O_INBOX_CLOSED, NULL, 0, NULL, 0, 22, 22, 22 }, + { O_INBOX_OPEN, NULL, 0, NULL, 0, 23, 23, 23 }, + { O_OUTBOX_CLOSED, NULL, 0, NULL, 0, 22, 22, 22 }, /* game graphics - also for imported diego effects, but don't know if it is used anywhere in original games */ + { O_OUTBOX_OPEN, NULL, 0, NULL, 0, 23, 23, 23 }, + { O_COVERED, NULL, 0, NULL, 0, 128, -128, -128 }, + { O_PLAYER_LEFT, NULL, 0, NULL, 0, 232, -232, -232 }, + { O_PLAYER_RIGHT, NULL, 0, NULL, 0, 240, -240, -240 }, + { O_PLAYER_TAP, NULL, 0, NULL, 0, 216, -216, -216 }, + { O_PLAYER_BLINK, NULL, 0, NULL, 0, 208, -208, -208 }, + { O_PLAYER_TAP_BLINK, NULL, 0, NULL, 0, 224, -224, -224 }, + { O_CREATURE_SWITCH_ON, NULL, 0, NULL, 0, 19, 19, 19 }, + { O_EXPANDING_WALL_SWITCH_HORIZ, NULL, 0, NULL, 0, 40, 40, 40 }, + { O_EXPANDING_WALL_SWITCH_VERT, NULL, 0, NULL, 0, 41, 41, 41 }, + { O_GRAVITY_SWITCH_ACTIVE, NULL, 0, NULL, 0, 275, 275, 275 }, + { O_REPLICATOR_SWITCH_ON, NULL, 0, NULL, 0, 290, 290, 290 }, + { O_REPLICATOR_SWITCH_OFF, NULL, 0, NULL, 0, 291, 291, 291 }, + { O_CONVEYOR_DIR_NORMAL, NULL, 0, NULL, 0, 353, 353, 353 }, + { O_CONVEYOR_DIR_CHANGED, NULL, 0, NULL, 0, 354, 354, 354 }, + { O_CONVEYOR_SWITCH_OFF, NULL, 0, NULL, 0, 355, 355, 355 }, + { O_CONVEYOR_SWITCH_ON, NULL, 0, NULL, 0, 356, 356, 356 }, + + { O_MAGIC_WALL_ACTIVE, NULL, 0, NULL, 0, 184, -184, -184 }, + { O_REPLICATOR_ACTIVE, NULL, 0, NULL, 0, 304, -304, -304 }, + { O_CONVEYOR_LEFT_ACTIVE, NULL, 0, NULL, 0, i_conveyor_left, -328, -328 }, + { O_CONVEYOR_RIGHT_ACTIVE, NULL, 0, NULL, 0, i_conveyor_right, -320, -320 }, + { O_BITER_SWITCH_1, NULL, 0, NULL, 0, 12, 12, 12 }, + { O_BITER_SWITCH_2, NULL, 0, NULL, 0, 13, 13, 13 }, + { O_BITER_SWITCH_3, NULL, 0, NULL, 0, 14, 14, 14 }, + { O_BITER_SWITCH_4, NULL, 0, NULL, 0, 15, 15, 15 }, + + { O_QUESTION_MARK, NULL, 0, NULL, 0, 70, 70, 70 }, + { O_EATABLE, NULL, 0, NULL, 0, 71, 71, 71 }, + { O_DOWN_ARROW, NULL, 0, NULL, 0, 73, 73, 73 }, + { O_LEFTRIGHT_ARROW, NULL, 0, NULL, 0, 74, 74, 74 }, + { O_EVERYDIR_ARROW, NULL, 0, NULL, 0, 75, 75, 75 }, + { O_GLUED, NULL, 0, NULL, 0, 76, 76, 76 }, + { O_OUT, NULL, 0, NULL, 0, 77, 77, 77 }, + { O_EXCLAMATION_MARK, NULL, 0, NULL, 0, 78, 78, 78 }, + + { -1 }, +}; + +/* entries. */ +/* type given for each element; + * GD_TYPE_ELEMENT represents a combo box of gdash objects. + * GD_TAB&LABEL represents a notebook tab or a label. + * others are self-explanatory. */ +const GdStructDescriptor gd_cave_properties[] = +{ + /* default data */ + {"", GD_TAB, 0, N_("Cave data")}, + {"Name", GD_TYPE_STRING, 0, N_("Name"), G_STRUCT_OFFSET(GdCave, name), 1, N_("Name of game")}, + {"Description", GD_TYPE_STRING, 0, N_("Description"), G_STRUCT_OFFSET(GdCave, description), 1, N_("Some words about the game")}, + {"Author", GD_TYPE_STRING, 0, N_("Author"), G_STRUCT_OFFSET(GdCave, author), 1, N_("Name of author")}, + {"Date", GD_TYPE_STRING, 0, N_("Date"), G_STRUCT_OFFSET(GdCave, date), 1, N_("Date of creation")}, + {"WWW", GD_TYPE_STRING, 0, N_("WWW"), G_STRUCT_OFFSET(GdCave, www), 1, N_("Web page or e-mail address")}, + {"Difficulty", GD_TYPE_STRING, 0, N_("Difficulty"), G_STRUCT_OFFSET(GdCave, difficulty), 1, N_("Difficulty (informative)")}, + + {"Selectable", GD_TYPE_BOOLEAN, 0, N_("Selectable as start"), G_STRUCT_OFFSET(GdCave, selectable), 1, N_("This sets whether the game can be started at this cave.")}, + {"Intermission", GD_TYPE_BOOLEAN, GD_ALWAYS_SAVE, N_("Intermission"), G_STRUCT_OFFSET(GdCave, intermission), 1, N_("Intermission caves are usually small and fast caves, which are not required to be solved. The player will not lose a life if he is not successful. The game always proceeds to the next cave.")}, + {"IntermissionProperties.instantlife", GD_TYPE_BOOLEAN, 0, N_(" Instant life"), G_STRUCT_OFFSET(GdCave, intermission_instantlife), 1, N_("If true, an extra life is given to the player, when the intermission cave is reached.")}, + {"IntermissionProperties.rewardlife", GD_TYPE_BOOLEAN, 0, N_(" Reward life"), G_STRUCT_OFFSET(GdCave, intermission_rewardlife), 1, N_("If true, an extra life is given to the player, when the intermission cave is successfully finished.")}, + {"Size", GD_TYPE_INT, GD_ALWAYS_SAVE, N_("Width"), G_STRUCT_OFFSET(GdCave, w), 1, N_("Width of cave. The standard size for a cave is 40x22, and 20x12 for an intermission."), 12, 128}, + {"Size", GD_TYPE_INT, GD_ALWAYS_SAVE, N_("Height"), G_STRUCT_OFFSET(GdCave, h), 1, N_("Height of cave. The standard size for a cave is 40x22, and 20x12 for an intermission."), 12, 128}, + {"Size", GD_TYPE_INT, GD_ALWAYS_SAVE|GD_DONT_SHOW_IN_EDITOR, N_("Visible, left"), G_STRUCT_OFFSET(GdCave, x1), 1, N_("Visible parts of the cave, upper left and lower right corner."), 0, 127}, + {"Size", GD_TYPE_INT, GD_ALWAYS_SAVE|GD_DONT_SHOW_IN_EDITOR, N_("Visible, upper"), G_STRUCT_OFFSET(GdCave, y1), 1, N_("Visible parts of the cave, upper left and lower right corner."), 0, 127}, + {"Size", GD_TYPE_INT, GD_ALWAYS_SAVE|GD_DONT_SHOW_IN_EDITOR, N_("Visible, right"), G_STRUCT_OFFSET(GdCave, x2), 1, N_("Visible parts of the cave, upper left and lower right corner."), 0, 127}, + {"Size", GD_TYPE_INT, GD_ALWAYS_SAVE|GD_DONT_SHOW_IN_EDITOR, N_("Visible, lower"), G_STRUCT_OFFSET(GdCave, y2), 1, N_("Visible parts of the cave, upper left and lower right corner."), 0, 127}, + {"Charset", GD_TYPE_STRING, 0, N_("Character set"), G_STRUCT_OFFSET(GdCave, charset), 1, N_("Theme used for displaying the game. Not used by GDash.")}, + {"Fontset", GD_TYPE_STRING, 0, N_("Font set"), G_STRUCT_OFFSET(GdCave, fontset), 1, N_("Font used during the game. Not used by GDash.")}, + + /* notes - a tab on its own */ + {"Story", GD_TYPE_LONGSTRING, 0, N_("Story"), G_STRUCT_OFFSET(GdCave, story), 1, N_("Story for the cave. It will be shown when the cave is played.")}, + + /* remark - also a tab on its own */ + {"Remark", GD_TYPE_LONGSTRING, 0, N_("Remark"), G_STRUCT_OFFSET(GdCave, remark), 1, N_("Remark (informative). Can contain supplementary information about the design of the cave. It is not shown during the game, only when the user requests the cave info dialog, so can also contain spoilers and hints.")}, + + {"", GD_TAB, 0, N_("Colors")}, + {"Colors", GD_TYPE_COLOR, GD_ALWAYS_SAVE, N_("Border color"), G_STRUCT_OFFSET(GdCave, colorb), 1, N_("Border color for C64 graphics. Only for compatibility, not used by GDash.")}, + {"Colors", GD_TYPE_COLOR, GD_ALWAYS_SAVE, N_("Background color"), G_STRUCT_OFFSET(GdCave, color0), 1, N_("Background color for C64 graphics")}, + {"Colors", GD_TYPE_COLOR, GD_ALWAYS_SAVE, N_("Color 1 (dirt)"), G_STRUCT_OFFSET(GdCave, color1), 1, N_("Foreground color 1 for C64 graphics")}, + {"Colors", GD_TYPE_COLOR, GD_ALWAYS_SAVE, N_("Color 2 (steel wall)"), G_STRUCT_OFFSET(GdCave, color2), 1, N_("Foreground color 2 for C64 graphics")}, + {"Colors", GD_TYPE_COLOR, GD_ALWAYS_SAVE, N_("Color 3 (brick wall)"), G_STRUCT_OFFSET(GdCave, color3), 1, N_("Foreground color 3 for C64 graphics")}, + {"Colors", GD_TYPE_COLOR, GD_ALWAYS_SAVE, N_("Amoeba color"), G_STRUCT_OFFSET(GdCave, color4), 1, N_("Amoeba color for C64 graphics")}, + {"Colors", GD_TYPE_COLOR, GD_ALWAYS_SAVE, N_("Slime color"), G_STRUCT_OFFSET(GdCave, color5), 1, N_("Slime color for C64 graphics")}, + + /* difficulty */ + {"", GD_TAB, 0, N_("Difficulty")}, + {"", GD_LABEL, GD_SHOW_LEVEL_LABEL, N_("Diamonds")}, + {"DiamondsRequired", GD_TYPE_INT, GD_ALWAYS_SAVE, N_("Diamonds needed"), CAVE_OFFSET(level_diamonds[0]), 5, N_("Here zero means automatically count diamonds before level start. If negative, the value is subtracted from that. This is useful for totally random caves."), -100, 999}, + {"DiamondValue", GD_TYPE_INT, GD_ALWAYS_SAVE, N_("Score for diamonds"), CAVE_OFFSET(diamond_value), 1, N_("Number of points per diamond collected, before opening the exit."), 0, 100}, + {"DiamondValue", GD_TYPE_INT, GD_ALWAYS_SAVE, N_("Score for extra diamonds"), CAVE_OFFSET(extra_diamond_value), 1, N_("Number of points per diamond collected, after opening the exit."), 0, 100}, + {"", GD_LABEL, 0, N_("Time")}, + {"CaveTime", GD_TYPE_INT, GD_ALWAYS_SAVE, N_("Time (s)"), CAVE_OFFSET(level_time[0]), 5, N_("Time available to solve cave, in seconds."), 1, 999}, + {"CaveMaxTime", GD_TYPE_INT, 0, N_("Maximum time (s)"), CAVE_OFFSET(max_time), 1, N_("If you reach this time by collecting too many clocks, the timer will overflow."), 60, 999}, + {"TimeValue", GD_TYPE_INT, 0, N_("Score for time"), CAVE_OFFSET(level_timevalue[0]), 5, N_("Points for each seconds remaining, when the player exits the level."), 0, 50}, + {"CaveScheduling", GD_TYPE_SCHEDULING, GD_ALWAYS_SAVE, N_("Scheduling type"), CAVE_OFFSET(scheduling), 1, N_("This flag sets whether the game uses an emulation of the original timing (c64-style), or a more modern milliseconds-based timing. The original game used a delay (empty loop) based timing of caves; this is selected by setting this to BD1, BD2, Construction Kit or Crazy Dream 7. This is a compatibility setting only; milliseconds-based timing is recommended for every new cave.")}, + {"PALTiming", GD_TYPE_BOOLEAN, 0, N_("PAL timing"), CAVE_OFFSET(pal_timing), 1, N_("On the PAL version of the C64 computer, the timer was actually slower than normal seconds. This flag is used to compensate for this. If enabled, one game second will last 1.2 real seconds. Most original games were authored for the PAL version. This is a compatibility setting for imported caves; it is not recommended to enable it for newly authored ones.")}, + {"FrameTime", GD_TYPE_INT, GD_ALWAYS_SAVE, N_(" Speed (ms)"), CAVE_OFFSET(level_speed[0]), 5, N_("Number of milliseconds between game frames. Used when milliseconds-based timing is active, ie. C64 scheduling is off."), 50, 500}, + {"HatchingDelay", GD_TYPE_INT, 0, N_(" Hatching delay (frames)"), CAVE_OFFSET(level_hatching_delay_frame[0]), 5, N_("This value sets how much the cave will move until the player enters the cave, and is expressed in frames. This is used for the milliseconds-based scheduling."), 1, 40}, + {"CaveDelay", GD_TYPE_INT, GD_ALWAYS_SAVE, N_(" Delay (C64-style)"), CAVE_OFFSET(level_ckdelay[0]), 5, N_("The length of the delay loop between game frames. Used when milliseconds-based timing is inactive, ie. some kind of C64 or Atari scheduling is selected."), 0, 32}, + {"HatchingTime", GD_TYPE_INT, 0, N_(" Hatching time (seconds)"), CAVE_OFFSET(level_hatching_delay_time[0]), 5, N_("This value sets how much the cave will move until the player enters the cave. This is used for the C64-like schedulings."), 1, 40}, + + /* initial fill */ + {"RandSeed", GD_TYPE_INT, GD_DONT_SHOW_IN_EDITOR, NULL /* random seed value */, CAVE_OFFSET(level_rand[0]), 5, NULL, -1, 255}, + {"InitialBorder", GD_TYPE_ELEMENT, GD_DONT_SHOW_IN_EDITOR, NULL /* Initial border */, CAVE_OFFSET(initial_border), 1, NULL}, + {"InitialFill", GD_TYPE_ELEMENT, GD_DONT_SHOW_IN_EDITOR, NULL /* Initial fill */, CAVE_OFFSET(initial_fill), 1, NULL}, + {"RandomFill", GD_TYPE_ELEMENT, GD_DONT_SHOW_IN_EDITOR, NULL /* Random fill 1 */, CAVE_OFFSET(random_fill[0]), 1, NULL}, + {"RandomFill", GD_TYPE_INT, GD_DONT_SHOW_IN_EDITOR, NULL /* Probability 1 */, CAVE_OFFSET(random_fill_probability[0]), 1, NULL, 0, 255}, + {"RandomFill", GD_TYPE_ELEMENT, GD_DONT_SHOW_IN_EDITOR, NULL /* Random fill 2 */, CAVE_OFFSET(random_fill[1]), 1, NULL}, + {"RandomFill", GD_TYPE_INT, GD_DONT_SHOW_IN_EDITOR, NULL /* Probability 2 */, CAVE_OFFSET(random_fill_probability[1]), 1, NULL, 0, 255}, + {"RandomFill", GD_TYPE_ELEMENT, GD_DONT_SHOW_IN_EDITOR, NULL /* Random fill 3 */, CAVE_OFFSET(random_fill[2]), 1, NULL}, + {"RandomFill", GD_TYPE_INT, GD_DONT_SHOW_IN_EDITOR, NULL /* Probability 3 */, CAVE_OFFSET(random_fill_probability[2]), 1, NULL, 0, 255}, + {"RandomFill", GD_TYPE_ELEMENT, GD_DONT_SHOW_IN_EDITOR, NULL /* Random fill 4 */, CAVE_OFFSET(random_fill[3]), 1, NULL}, + {"RandomFill", GD_TYPE_INT, GD_DONT_SHOW_IN_EDITOR, NULL /* Probability 4 */, CAVE_OFFSET(random_fill_probability[3]), 1, NULL, 0, 255}, + + /* PLAYER */ + {"", GD_TAB, 0, N_("Player")}, + /* player */ + {"", GD_LABEL, 0, N_("Player movements")}, + {"DiagonalMovement", GD_TYPE_BOOLEAN, 0, N_("Diagonal movements"), CAVE_OFFSET(diagonal_movements), 1, N_("Controls if the player can move diagonally.")}, + {"ActiveGuyIsFirst", GD_TYPE_BOOLEAN, 0, N_("Uppermost player active"), CAVE_OFFSET(active_is_first_found), 1, N_("In 1stB, cave is scrolled to the uppermost and leftmost player found, whereas in the original game to the last one. Chasing stones also follow the active player.")}, + {"SnapEffect", GD_TYPE_ELEMENT, 0, N_("Snap element"), CAVE_OFFSET(snap_element), 1, N_("Snapping (pressing fire while moving) usually creates space, but it can create any other element.")}, + {"PushingBoulderProb", GD_TYPE_PROBABILITY, 0, N_("Probability of pushing (%)"), CAVE_OFFSET(pushing_stone_prob), 1, N_("Chance of player managing to push a stone, every game cycle he tries. This is the normal probability.")}, + {"", GD_LABEL, 0, N_("Sweet")}, + {"PushingBoulderProb", GD_TYPE_PROBABILITY, 0, N_("Probability of pushing (%)"), CAVE_OFFSET(pushing_stone_prob_sweet), 1, N_("Chance of player managing to push a stone, every game cycle he tries. This is used after eating sweet.")}, + {"PushingMegaStonesAfterSweet", GD_TYPE_BOOLEAN, 0, N_("Mega stones pushable"), CAVE_OFFSET(mega_stones_pushable_with_sweet), 1, N_("If it is true, mega stones can be pushed after eating sweet.")}, + /* pneumatic hammer */ + {"", GD_LABEL, 0, N_("Pneumatic hammer")}, + {"PneumaticHammer.frames", GD_TYPE_INT, 0, N_("Time for hammer (frames)"), CAVE_OFFSET(pneumatic_hammer_frame), 1, N_("This is the number of game frames, a pneumatic hammer is required to break a wall."), 1, 100}, + {"PneumaticHammer.wallsreappear", GD_TYPE_BOOLEAN, 0, N_("Hammered walls reappear"), CAVE_OFFSET(hammered_walls_reappear), 1, N_("If this is set to true, walls broken with a pneumatic hammer will reappear later.")}, + {"PneumaticHammer.wallsreappearframes", GD_TYPE_INT, 0, N_(" Timer for reappear (frames)"), CAVE_OFFSET(hammered_wall_reappear_frame), 1, N_("This sets the number of game frames, after hammered walls reappear, when the above setting is true."), 1, 200}, + /* clock */ + {"", GD_LABEL, GD_SHOW_LEVEL_LABEL, N_("Clock")}, + {"BonusTime", GD_TYPE_INT, 0, N_("Time bonus (s)"), CAVE_OFFSET(level_bonus_time), 5, N_("Bonus time when a clock is collected."), -100, 100}, + /* voodoo */ + {"", GD_LABEL, 0, N_("Voodoo Doll")}, + {"DummyProperties.diamondcollector", GD_TYPE_BOOLEAN, 0, N_("Can collect diamonds"), CAVE_OFFSET(voodoo_collects_diamonds), 1, N_("Controls if a voodoo doll can collect diamonds for the player.")}, + {"DummyProperties.penalty", GD_TYPE_BOOLEAN, 0, N_("Dies if hit by a stone"), CAVE_OFFSET(voodoo_dies_by_stone), 1, N_("Controls if the voodoo doll dies if it is hit by a stone. Then the player gets a time penalty, and it is turned to a gravestone surrounded by steel wall.")}, + {"DummyProperties.destructable", GD_TYPE_BOOLEAN, 0, N_("Disappear in explosion"), CAVE_OFFSET(voodoo_disappear_in_explosion), 1, N_("Controls if the voodoo can be destroyed by an explosion nearby. If not, it is converted to a gravestone, and you get a time penalty. If yes, the voodoo simply disappears.")}, + {"DummyProperties.alwayskillsplayer", GD_TYPE_BOOLEAN, 0, N_("Any way hurt, player explodes"), CAVE_OFFSET(voodoo_any_hurt_kills_player), 1, N_("If this setting is enabled, the player will explode if the voodoo is hurt in any possible way, ie. touched by a firefly, hit by a stone or an explosion.")}, + {"PenaltyTime", GD_TYPE_INT, 0, N_("Time penalty (s)"), CAVE_OFFSET(level_penalty_time), 5, N_("Penalty time when the voodoo is destroyed by a stone."), 0, 100}, + + /* AMOEBA */ + {"", GD_TAB, 0, N_("Amoeba")}, + {"AmoebaProperties.immediately", GD_TYPE_BOOLEAN, 0, N_("Timer started immediately"), CAVE_OFFSET(amoeba_timer_started_immediately), 1, N_("If this flag is enabled, the amoeba slow growth timer will start at the beginning of the cave, regardless of the amoeba being let free or not. This can make a big difference when playing the cave!")}, + {"AmoebaProperties.waitforhatching", GD_TYPE_BOOLEAN, 0, N_("Timer waits for hatching"), CAVE_OFFSET(amoeba_timer_wait_for_hatching), 1, N_("This determines if the amoeba timer starts before the player appearing. Amoeba can always be activated before that; but if this is set to true, the timer will not start. This setting is for compatiblity for some old imported caves. As the player is usually born within a few seconds, changing this setting makes not much difference. It is not advised to change it, set the slow growth time to fit your needs instead.")}, + /* amoeba */ + {"", GD_LABEL, GD_SHOW_LEVEL_LABEL, N_("Amoeba")}, + {"AmoebaThreshold", GD_TYPE_RATIO, 0, N_("Threshold (cells)"), CAVE_OFFSET(level_amoeba_threshold), 5, N_("If the amoeba grows more than this fraction of the cave, it is considered too big and it converts to the element specified below."), 0, 16383}, + {"AmoebaTime", GD_TYPE_INT, 0, N_("Slow growth time (s)"), CAVE_OFFSET(level_amoeba_time), 5, N_("After this time, amoeba will grow very quickly."), 0, 999}, + {"AmoebaGrowthProb", GD_TYPE_PROBABILITY, 0, N_("Growth ratio, slow (%)"), CAVE_OFFSET(amoeba_growth_prob), 1, N_("This sets the speed at which a slow amoeba grows.")}, + {"AmoebaGrowthProb", GD_TYPE_PROBABILITY, 0, N_("Growth ratio, fast (%)"), CAVE_OFFSET(amoeba_fast_growth_prob), 1, N_("This sets the speed at which a fast amoeba grows.")}, + {"AMOEBABOULDEReffect", GD_TYPE_EFFECT, 0, N_("If too big, converts to"), CAVE_OFFSET(amoeba_too_big_effect), 1, N_("Controls which element an overgrown amoeba converts to.")}, + {"AMOEBADIAMONDeffect", GD_TYPE_EFFECT, 0, N_("If enclosed, converts to"), CAVE_OFFSET(amoeba_enclosed_effect), 1, N_("Controls which element an enclosed amoeba converts to.")}, + {"", GD_LABEL, GD_SHOW_LEVEL_LABEL, N_("Amoeba 2")}, + {"Amoeba2Threshold", GD_TYPE_RATIO, 0, N_("Threshold (cells)"), CAVE_OFFSET(level_amoeba_2_threshold), 5, N_("If the amoeba grows more than this fraction of the cave, it is considered too big and it converts to the element specified below."), 0, 16383}, + {"Amoeba2Time", GD_TYPE_INT, 0, N_("Slow growth time (s)"), CAVE_OFFSET(level_amoeba_2_time), 5, N_("After this time, amoeba will grow very quickly."), 0, 999}, + {"Amoeba2GrowthProb", GD_TYPE_PROBABILITY, 0, N_("Growth ratio, slow (%)"), CAVE_OFFSET(amoeba_2_growth_prob), 1, N_("This sets the speed at which a slow amoeba grows.")}, + {"Amoeba2GrowthProb", GD_TYPE_PROBABILITY, 0, N_("Growth ratio, fast (%)"), CAVE_OFFSET(amoeba_2_fast_growth_prob), 1, N_("This sets the speed at which a fast amoeba grows.")}, + {"Amoeba2Properties.explode", GD_TYPE_BOOLEAN, 0, N_("Explodes by amoeba"), CAVE_OFFSET(amoeba_2_explodes_by_amoeba), 1, N_("If this setting is enabled, an amoeba 2 will explode if it is touched by a normal amoeba.")}, + {"AMOEBA2EXPLOSIONeffect", GD_TYPE_EFFECT, 0, N_(" Explosion ends in"), CAVE_OFFSET(amoeba_2_explosion_effect), 1, N_("An amoeba 2 explodes to this element, when touched by the original amoeba.")}, + {"AMOEBA2BOULDEReffect", GD_TYPE_EFFECT, 0, N_("If too big, converts to"), CAVE_OFFSET(amoeba_2_too_big_effect), 1, N_("Controls which element an overgrown amoeba converts to.")}, + {"AMOEBA2DIAMONDeffect", GD_TYPE_EFFECT, 0, N_("If enclosed, converts to"), CAVE_OFFSET(amoeba_2_enclosed_effect), 1, N_("Controls which element an enclosed amoeba converts to.")}, + {"AMOEBA2LOOKSLIKEeffect", GD_TYPE_EFFECT, 0, N_("Looks like"), CAVE_OFFSET(amoeba_2_looks_like), 1, N_("Amoeba 2 can look like any other element. Hint: it can also look like a normal amoeba. Or it can look like slime, and then you have two different colored amoebas!")}, + + /* magic wall */ + {"", GD_TAB, 0, N_("Magic Wall")}, + {"", GD_LABEL, GD_SHOW_LEVEL_LABEL, N_("Timing")}, + {"MagicWallTime", GD_TYPE_INT, 0, N_("Milling time (s)"), CAVE_OFFSET(level_magic_wall_time), 5, N_("Magic wall will stop after this time, and it cannot be activated again."), 0, 999}, + {"MagicWallProperties.waitforhatching", GD_TYPE_BOOLEAN, 0, N_("Timer waits for hatching"), CAVE_OFFSET(magic_timer_wait_for_hatching), 1, N_("This determines if the magic wall timer starts before the player appearing. Magic can always be activated before that; but if this is set to true, the timer will not start.")}, + {"MagicWallProperties.convertamoeba", GD_TYPE_BOOLEAN, 0, N_("Stops amoeba"), CAVE_OFFSET(magic_wall_stops_amoeba), 1, N_("When the magic wall is activated, it can convert amoeba into diamonds.")}, + {"", GD_LABEL, 0, N_("Conversions")}, + {"MagicWallProperties", GD_TYPE_ELEMENT, 0, N_("Diamond to"), CAVE_OFFSET(magic_diamond_to), 1, N_("As a special effect, magic walls can convert diamonds to any other element.")}, + {"MagicWallProperties", GD_TYPE_ELEMENT, 0, N_("Stone to"), CAVE_OFFSET(magic_stone_to), 1, N_("As a special effect, magic walls can convert stones to any other element.")}, + {"MagicWallProperties.megastoneto", GD_TYPE_ELEMENT, 0, N_("Mega stone to"), CAVE_OFFSET(magic_mega_stone_to), 1, N_("If a mega stone falls into the magic wall, it will drop this element.")}, + {"MagicWallProperties.nitropackto", GD_TYPE_ELEMENT, 0, N_("Nitro pack to"), CAVE_OFFSET(magic_nitro_pack_to), 1, N_("If a nitro pack falls into the magic wall, it will be turned to this element.")}, + {"MagicWallProperties.nutto", GD_TYPE_ELEMENT, 0, N_("Nut to"), CAVE_OFFSET(magic_nut_to), 1, N_("As a special effect, magic walls can convert nuts to any other element.")}, + {"MagicWallProperties.flyingstoneto", GD_TYPE_ELEMENT, 0, N_("Flying stone to"), CAVE_OFFSET(magic_flying_stone_to), 1, N_("If a flying stone climbs up into the magic wall, it will be turned to this element. Remember that flying stones enter the magic wall from its bottom, not from the top!")}, + {"MagicWallProperties.flyingdiamondto", GD_TYPE_ELEMENT, 0, N_("Flying diamonds to"), CAVE_OFFSET(magic_flying_diamond_to), 1, N_("If a flying diamond enters the magic wall, it will be turned to this element. Remember that flying diamonds enter the magic wall from its bottom, not from the top!")}, + + /* slime */ + {"", GD_TAB, 0, N_("Slime")}, + {"", GD_LABEL, GD_SHOW_LEVEL_LABEL, N_("Permeability")}, + {"", GD_TYPE_BOOLEAN, GD_DONT_SAVE, N_("Predictable"), CAVE_OFFSET(slime_predictable), 1, N_("Controls if the predictable random generator is used for slime. It is required for compatibility with some older caves.")}, + /* permeabilities are "always" saved; and according to the predictability, one of them is removed. */ + {"SlimePermeability", GD_TYPE_PROBABILITY, GD_ALWAYS_SAVE, N_("Permeability (unpredictable, %)"), CAVE_OFFSET(level_slime_permeability[0]), 5, N_("This controls the rate at which elements go through the slime. Higher values represent higher probability of passing. This one is for unpredictable slime.")}, + {"SlimePermeabilityC64", GD_TYPE_INT, GD_ALWAYS_SAVE, N_("Permeability (predictable, bits)"), CAVE_OFFSET(level_slime_permeability_c64[0]), 5, N_("This controls the rate at which elements go through the slime. This one is for predictable slime, and the value is used for a bitwise AND function. The values used by the C64 engines are 0, 128, 192, 224, 240, 248, 252, 254 and 255."), 0, 255}, + {"SlimePredictableC64.seed", GD_TYPE_INT, 0, N_("Random seed (predictable)"), CAVE_OFFSET(level_slime_seed_c64), 5, N_("The random number seed for predictable slime. Use -1 to leave on its default. Not recommended to change. Does not affect unpredictable slime."), -1, 65535}, + {"", GD_LABEL, 0, N_("Passing elements")}, + {"SlimeProperties", GD_TYPE_ELEMENT, 0, N_("Eats this..."), CAVE_OFFSET(slime_eats_1), 1, N_("Slime can let other elements than stone and diamond go through. It always lets a waiting or a chasing stone pass, though. Also, flying diamonds and stones, as well as bladders are always passed.")}, + {"SlimeProperties", GD_TYPE_ELEMENT, 0, N_(" ... and converts to"), CAVE_OFFSET(slime_converts_1), 1, N_("Slime can let other elements than stone and diamond go through. It always lets a waiting or a chasing stone pass, though. Also, flying diamonds and stones, as well as bladders are always passed.")}, + {"SlimeProperties", GD_TYPE_ELEMENT, 0, N_("Eats this..."), CAVE_OFFSET(slime_eats_2), 1, N_("Slime can let other elements than stone and diamond go through. It always lets a waiting or a chasing stone pass, though. Also, flying diamonds and stones, as well as bladders are always passed.")}, + {"SlimeProperties", GD_TYPE_ELEMENT, 0, N_(" ... and converts to"), CAVE_OFFSET(slime_converts_2), 1, N_("Slime can let other elements than stone and diamond go through. It always lets a waiting or a chasing stone pass, though. Also, flying diamonds and stones, as well as bladders are always passed.")}, + {"SlimeProperties", GD_TYPE_ELEMENT, 0, N_("Eats this..."), CAVE_OFFSET(slime_eats_3), 1, N_("Slime can let other elements than stone and diamond go through. It always lets a waiting or a chasing stone pass, though. Also, flying diamonds and stones, as well as bladders are always passed.")}, + {"SlimeProperties", GD_TYPE_ELEMENT, 0, N_(" ... and converts to"), CAVE_OFFSET(slime_converts_3), 1, N_("Slime can let other elements than stone and diamond go through. It always lets a waiting or a chasing stone pass, though. Also, flying diamonds and stones, as well as bladders are always passed.")}, + + /* ACTIVE 2 */ + {"", GD_TAB, 0, N_("Other elements")}, + /* acid */ + {"", GD_LABEL, 0, N_("Acid")}, + {"AcidProperties", GD_TYPE_ELEMENT, 0, N_("Eats this element"), CAVE_OFFSET(acid_eats_this), 1, N_("The element which acid eats. If it cannot find any, it simply disappears.")}, + {"AcidProperties", GD_TYPE_PROBABILITY, 0, N_("Spread ratio (%)"), CAVE_OFFSET(acid_spread_ratio), 1, N_("The probability at which an acid will explode and eat neighbouring elements.")}, + {"ACIDEffect", GD_TYPE_EFFECT, 0, N_("Leaves this behind"), CAVE_OFFSET(acid_turns_to), 1, N_("If acid converts to an explosion puff on spreading or any other element.")}, + /* biter */ + {"", GD_LABEL, 0, N_("Biter")}, + {"BiterProperties", GD_TYPE_INT, 0, N_("Delay (frame)"), CAVE_OFFSET(biter_delay_frame), 1, N_("Number of frames biters wait between movements."), 0, 3}, + {"BiterProperties", GD_TYPE_ELEMENT, 0, N_("Eats this"), CAVE_OFFSET(biter_eat), 1, N_("Biters eat this element. (They always eat dirt.)")}, + /* bladder */ + {"", GD_LABEL, 0, N_("Bladder")}, + {"BladderProperties", GD_TYPE_ELEMENT, 0, N_("Converts to clock by touching"), CAVE_OFFSET(bladder_converts_by), 1, NULL}, + /* expanding wall */ + {"", GD_LABEL, 0, N_("Expanding wall")}, + {"ExpandingWallDirection.changed", GD_TYPE_BOOLEAN, 0, N_("Direction changed"), CAVE_OFFSET(expanding_wall_changed), 1, N_("If this option is enabled, the direction of growing for the horizontal and vertical expanding wall is switched. As you can use both horizontal and vertical expanding walls in a cave, it is not recommended to change this setting, as it might be confusing. You should rather select the type with the correct direction from the element box when drawing the cave.")}, + /* replicator */ + {"", GD_LABEL, 0, N_("Replicator")}, + {"ReplicatorActive", GD_TYPE_BOOLEAN, 0, N_("Active at start"), CAVE_OFFSET(replicators_active), 1, N_("Whether the replicators are turned on or off at the cave start.")}, + {"ReplicatorDelayFrame", GD_TYPE_INT, 0, N_("Delay (frame)"), CAVE_OFFSET(replicator_delay_frame), 1, N_("Number of frames to wait between replicating elements."), 0, 100}, + /* conveyor belt */ + {"", GD_LABEL, 0, N_("Conveyor belt")}, + {"ConveyorBeltActive", GD_TYPE_BOOLEAN, 0, N_("Active at start"), CAVE_OFFSET(conveyor_belts_active), 1, N_("Whether the conveyor belts are moving when the cave starts.")}, + {"ConveyorBeltDirection.changed", GD_TYPE_BOOLEAN, 0, N_("Direction changed"), CAVE_OFFSET(conveyor_belts_direction_changed), 1, N_("If the conveyor belts' movement is changed, ie. they are running in the opposite direction. As you can freely use left and right going versions of the conveyor belt in a cave, it is not recommended to change this setting, rather you should select the correct one from the element box when drawing.")}, + /* water */ + {"", GD_LABEL, 0, N_("Water")}, + {"WaterProperties.doesnotflowdown", GD_TYPE_BOOLEAN, 0, N_("Does not flow downwards"), CAVE_OFFSET(water_does_not_flow_down), 1, N_("In CrDr, the water element had the odd property that it did not flow downwards, only in other directions. This flag emulates this behaviour.")}, + /* nut */ + {"", GD_LABEL, 0, N_("Nut")}, + {"Nut.whencrushed", GD_TYPE_ELEMENT, 0, N_("Turns to when crushed"), CAVE_OFFSET(nut_turns_to_when_crushed), 1, N_("Normally, a nut contains a diamond. If you crush it with a stone, the diamond will appear after the usual nut explosion sequence. This setting can be used to change the element the nut contains.")}, + + /* EFFECTS 1 */ + {"", GD_TAB, 0, N_("Effects")}, + /* cave effects */ + {"", GD_LABEL, 0, N_("Stone and diamond effects")}, + {"BOULDERfallingeffect", GD_TYPE_EFFECT, 0, N_("Falling stones convert to"), CAVE_OFFSET(stone_falling_effect), 1, N_("When a stone begins falling, it converts to this element.")}, + {"BOULDERbouncingeffect", GD_TYPE_EFFECT, 0, N_("Bouncing stones convert to"), CAVE_OFFSET(stone_bouncing_effect), 1, N_("When a stone stops falling and rolling, it converts to this element.")}, + {"DIAMONDfallingeffect", GD_TYPE_EFFECT, 0, N_("Falling diamonds convert to"), CAVE_OFFSET(diamond_falling_effect), 1, N_("When a diamond begins falling, it converts to this element.")}, + {"DIAMONDbouncingeffect", GD_TYPE_EFFECT, 0, N_("Bouncing diamonds convert to"), CAVE_OFFSET(diamond_bouncing_effect), 1, N_("When a diamond stops falling and rolling, it converts to this element.")}, + + {"", GD_LABEL, 0, N_("Creature explosion effects")}, + {"FireflyExplodeTo", GD_TYPE_ELEMENT, 0, N_("Fireflies explode to"), CAVE_OFFSET(firefly_explode_to), 1, N_("When a firefly explodes, it will create this element. Change this setting wisely. The firefly is a traditional element which is expected to explode to empty space.")}, + {"AltFireflyExplodeTo", GD_TYPE_ELEMENT, 0, N_("Alt. fireflies explode to"), CAVE_OFFSET(alt_firefly_explode_to), 1, N_("When an alternative firefly explodes, it will create this element. Use this setting wisely. Do not create a firefly which explodes to stones, for example: use the stonefly instead.")}, + {"ButterflyExplodeTo", GD_TYPE_ELEMENT, 0, N_("Butterflies explode to"), CAVE_OFFSET(butterfly_explode_to), 1, N_("When a butterfly explodes, it will create this element. Use this setting wisely. Butterflies should explode to diamonds. If you need a creature which explodes to space, use the firefly instead.")}, + {"AltButterflyExplodeTo", GD_TYPE_ELEMENT, 0, N_("Alt. butterflies explode to"), CAVE_OFFSET(alt_butterfly_explode_to), 1, N_("When an alternative butterfly explodes, it will create this element. Use this setting wisely.")}, + {"StoneflyExplodeTo", GD_TYPE_ELEMENT, 0, N_("Stoneflies explode to"), CAVE_OFFSET(stonefly_explode_to), 1, N_("When a stonefly explodes, it will create this element.")}, + {"DragonflyExplodeTo", GD_TYPE_ELEMENT, 0, N_("Dragonflies explode to"), CAVE_OFFSET(dragonfly_explode_to), 1, N_("When a dragonfly explodes, it will create this element.")}, + + {"", GD_LABEL, 0, N_("Explosion effects")}, + {"EXPLOSIONEffect", GD_TYPE_EFFECT, 0, N_("Explosions end in"), CAVE_OFFSET(explosion_effect), 1, N_("This element appears in places where an explosion finishes.")}, + {"DIAMONDBIRTHEffect", GD_TYPE_EFFECT, 0, N_("Diamond births end in"), CAVE_OFFSET(diamond_birth_effect), 1, N_("When a diamond birth animation reaches its end, it will leave this element there. This can be used to change the element butterflies explode to.")}, + {"BOMBEXPLOSIONeffect", GD_TYPE_EFFECT, 0, N_("Bombs explosions end in"), CAVE_OFFSET(bomb_explosion_effect), 1, N_("Use this setting to select the element the exploding bomb creates.")}, + {"NITROEXPLOSIONeffect", GD_TYPE_EFFECT, 0, N_("Nitro explosions end in"), CAVE_OFFSET(nitro_explosion_effect), 1, N_("The nitro explosions can create some element other than space.")}, + + /* EFFECTS 2 */ + {"", GD_TAB, 0, N_("More effects")}, + /* visual effects */ + {"", GD_LABEL, 0, N_("Visual effects")}, + {"EXPANDINGWALLLOOKSLIKEeffect", GD_TYPE_EFFECT, 0, N_("Expanding wall looks like"), CAVE_OFFSET(expanding_wall_looks_like), 1, N_("This is a compatibility setting for old caves. If you need an expanding wall which looks like steel, you should rather choose the expanding steel wall from the element box.")}, + {"DIRTLOOKSLIKEeffect", GD_TYPE_EFFECT, 0, N_("Dirt looks like"), CAVE_OFFSET(dirt_looks_like), 1, N_("Compatibility setting. Use it wisely! Anything other than Dirt 2 (which can be used to emulate the Dirt Mod) is not recommended.")}, + + /* creature effects */ + {"", GD_LABEL, 0, N_("Creature movement")}, + {"EnemyDirectionProperties.startbackwards", GD_TYPE_BOOLEAN, 0, N_("Start backwards"), CAVE_OFFSET(creatures_backwards), 1, N_("Whether the direction creatures travel will already be switched at the cave start.")}, + {"EnemyDirectionProperties.time", GD_TYPE_INT, 0, N_("Automatically turn (s)"), CAVE_OFFSET(creatures_direction_auto_change_time), 1, N_("If this is greater than zero, creatures will automatically change direction in every x seconds."), 0, 999}, + {"EnemyDirectionProperties.changeathatching", GD_TYPE_BOOLEAN, 0, N_("Auto turn on hatching"), CAVE_OFFSET(creatures_direction_auto_change_on_start), 1, N_("If this is set to true, creatures also turn at the start signal. If false, the first change in direction occurs only later.")}, + /* gravity */ + {"", GD_LABEL, 0, N_("Gravitation change")}, + {"Gravitation", GD_TYPE_DIRECTION, 0, N_("Direction"), CAVE_OFFSET(gravity), 1, N_("The direction where stones and diamonds fall.")}, + {"GravitationSwitchActive", GD_TYPE_BOOLEAN, 0, N_("Switch active at start"), CAVE_OFFSET(gravity_switch_active), 1, N_("If set to true, the gravitation switch will be already activated, when the cave is started, as if a pot has already been collected.")}, + {"SkeletonsForPot", GD_TYPE_INT, 0, N_("Skeletons needed for pot"), CAVE_OFFSET(skeletons_needed_for_pot), 1, N_("The number of skeletons to be collected to be able to use a pot."), 0, 50}, + {"GravitationChangeDelay", GD_TYPE_INT, 0, N_("Gravitation switch delay"), CAVE_OFFSET(gravity_change_time), 1, N_("The gravitation changes after a while using the gravitation switch. This option sets the number of seconds to wait."), 1, 60}, + + /* SOUND */ + {"", GD_TAB, 0, N_("Sound")}, + {"", GD_LABEL, 0, N_("Sound for elements")}, + {"Diamond.sound", GD_TYPE_BOOLEAN, 0, N_("Diamond"), CAVE_OFFSET(diamond_sound), 1, N_("If true, falling diamonds will have sound.")}, + {"Stone.sound", GD_TYPE_BOOLEAN, 0, N_("Stone"), CAVE_OFFSET(stone_sound), 1, N_("If true, falling and pushed stones will have sound.")}, + {"Nut.sound", GD_TYPE_BOOLEAN, 0, N_("Nut"), CAVE_OFFSET(nut_sound), 1, N_("If true, falling and cracked nuts have sound.")}, + {"NitroPack.sound", GD_TYPE_BOOLEAN, 0, N_("Nitro pack"), CAVE_OFFSET(nitro_sound), 1, N_("If true, falling and pushed nitro packs will have sound.")}, + {"ExpandingWall.sound", GD_TYPE_BOOLEAN, 0, N_("Expanding wall"), CAVE_OFFSET(expanding_wall_sound), 1, N_("If true, expanding wall will have sound.")}, + {"FallingWall.sound", GD_TYPE_BOOLEAN, 0, N_("Falling wall"), CAVE_OFFSET(falling_wall_sound), 1, N_("If true, falling wall will have sound.")}, + {"AmoebaProperties.sound", GD_TYPE_BOOLEAN, 0, N_("Amoeba"), CAVE_OFFSET(amoeba_sound), 1, N_("Controls if the living amoeba has sound or not.")}, + {"MagicWallProperties.sound", GD_TYPE_BOOLEAN, 0, N_("Magic wall"), CAVE_OFFSET(magic_wall_sound), 1, N_("If true, the activated magic wall will have sound.")}, + {"SlimeProperties.sound", GD_TYPE_BOOLEAN, 0, N_("Slime"), CAVE_OFFSET(slime_sound), 1, N_("If true, the elements passing slime will have sound.")}, + {"LavaProperties.sound", GD_TYPE_BOOLEAN, 0, N_("Lava"), CAVE_OFFSET(lava_sound), 1, N_("If true, the elements sinking in lava will have sound.")}, + {"ReplicatorProperties.sound", GD_TYPE_BOOLEAN, 0, N_("Replicator"), CAVE_OFFSET(replicator_sound), 1, N_("If true, the new element appearing under the replicator will make sound.")}, + {"AcidProperties.sound", GD_TYPE_BOOLEAN, 0, N_("Acid"), CAVE_OFFSET(acid_spread_sound), 1, N_("If true, the acid spreading will have sound.")}, + {"BiterProperties.sound", GD_TYPE_BOOLEAN, 0, N_("Biter"), CAVE_OFFSET(biter_sound), 1, N_("Biters eating something or pushing a stone will have sound.")}, + {"BladderProperties.sound", GD_TYPE_BOOLEAN, 0, N_("Bladder"), CAVE_OFFSET(bladder_sound), 1, N_("Bladders moving and being pushed can have sound.")}, + {"WaterProperties.sound", GD_TYPE_BOOLEAN, 0, N_("Water"), CAVE_OFFSET(water_sound), 1, N_("If true, the cave containing water will have sound.")}, + {"PneumaticHammer.sound", GD_TYPE_BOOLEAN, 0, N_("Pneumatic hammer"), CAVE_OFFSET(pneumatic_hammer_sound), 1, N_("If true, using the pneumatic hammer will have sound.")}, + {"BladderSpender.sound", GD_TYPE_BOOLEAN, 0, N_("Bladder spender"), CAVE_OFFSET(bladder_spender_sound), 1, N_("If true, the bladder spender will make sound, when the bladder appears.")}, + {"BladderConvert.sound", GD_TYPE_BOOLEAN, 0, N_("Bladder convert"), CAVE_OFFSET(bladder_convert_sound), 1, N_("If true, the bladder converting to a clock will make sound.")}, + {"", GD_LABEL, 0, N_("Event sounds")}, + {"GravityChange.sound", GD_TYPE_BOOLEAN, 0, N_("Gravity change"), CAVE_OFFSET(gravity_change_sound), 1, N_("If true, the gravity changing will make sound.")}, + {"EnemyDirectionProperties.sound", GD_TYPE_BOOLEAN, 0, N_("Creature direction change"), CAVE_OFFSET(creature_direction_auto_change_sound), 1, N_("If this is set to true, creatures changing direction will be signaled by a sound.")}, + + /* COMPATIBILITY */ + {"", GD_TAB, 0, N_("Compatibility")}, + {"", GD_LABEL, 0, N_("Skeleton")}, + {"SkeletonsWorthDiamonds", GD_TYPE_INT, GD_COMPATIBILITY_SETTING, N_("Skeletons worth diamonds"), CAVE_OFFSET(skeletons_worth_diamonds), 1, N_("The number of diamonds each skeleton is worth. Normally skeletons are used for letting the player use the pot! They are not intended to be used as a second kind of diamond."), 0, 10}, + {"", GD_LABEL, 0, N_("Borders")}, + {"BorderProperties.lineshift", GD_TYPE_BOOLEAN, 0, N_("Line shifting border"), CAVE_OFFSET(lineshift), 1, N_("If this is set to true, the player exiting on either side will appear one row lower or upper on the other side.")}, + {"BorderProperties.objectwraparound", GD_TYPE_BOOLEAN, 0, N_("Objects wrap around"), CAVE_OFFSET(wraparound_objects), 1, N_("If true, objects will wrap around the cave borders as well, ie. if you drag a line to the left, part of it will appear on the right hand side of the cave. The drawing in this case is also affected by the line shifting border property. If that one is enabled, too, crossing the left hand side or right hand side boundary will decrement or increment the row, and crossing the top or the bottom boundary will have no effect at all.")}, + {"BorderProperties.scan", GD_TYPE_BOOLEAN, 0, N_("Scan first and last row"), CAVE_OFFSET(border_scan_first_and_last), 1, N_("Elements move on first and last row, too. Usually those rows are the border. The games created by the original editor were not allowed to put anything but steel wall there, so it was not apparent that the borders were not processed by the engine. Some old caves need this for compatibility; it is not recommended to change this setting for newly designed caves, though.")}, + {"", GD_LABEL, 0, N_("Other")}, + {"ShortExplosions", GD_TYPE_BOOLEAN, 0, N_("Short explosions"), CAVE_OFFSET(short_explosions), 1, N_("In 1stB and newer engines, explosions were longer, they took five cave frames to complete, as opposed to four frames in the original.")}, + {"GravityAffectsAll", GD_TYPE_BOOLEAN, 0, N_("Gravity change affects everything"), CAVE_OFFSET(gravity_affects_all), 1, N_("If this is enabled, changing the gravity will also affect bladders (moving and pushing), bladder spenders, falling walls and waiting stones. Otherwise, those elements behave as gravity was always pointing downwards. This is a compatibility setting which is not recommended to change. It is intended for imported caves.")}, + + {NULL} /* end of array */ +}; + +/* entries. */ +/* type given for each element */ +const GdStructDescriptor gd_replay_properties[] = +{ + /* default data */ + {"", GD_TAB, 0, N_("Replay")}, + {"Level", GD_TYPE_INT, 0, NULL, G_STRUCT_OFFSET(GdReplay, level), 1, NULL}, + {"RandomSeed", GD_TYPE_INT, 0, NULL, G_STRUCT_OFFSET(GdReplay, seed), 1, NULL}, + // {"Saved", GD_TYPE_BOOLEAN, 0, NULL, G_STRUCT_OFFSET(GdReplay, saved), 1, NULL}, /* no need to state in bdcff, as saved replays are saved ones :) */ + {"Player", GD_TYPE_STRING, 0, NULL, G_STRUCT_OFFSET(GdReplay, player_name), 1, NULL}, + {"Date", GD_TYPE_STRING, 0, NULL, G_STRUCT_OFFSET(GdReplay, date), 1, NULL}, + {"Comment", GD_TYPE_LONGSTRING, 0, NULL, G_STRUCT_OFFSET(GdReplay, comment), 1, NULL}, + {"RecordedWith", GD_TYPE_STRING, 0, NULL, G_STRUCT_OFFSET(GdReplay, recorded_with), 1, NULL}, + {"Score", GD_TYPE_INT, 0, NULL, G_STRUCT_OFFSET(GdReplay, score), 1, NULL}, + {"Duration", GD_TYPE_INT, 0, NULL, G_STRUCT_OFFSET(GdReplay, duration), 1, NULL}, + {"Success", GD_TYPE_BOOLEAN, 0, NULL, G_STRUCT_OFFSET(GdReplay, success), 1, NULL}, + {"Checksum", GD_TYPE_INT, 0, NULL, G_STRUCT_OFFSET(GdReplay, checksum), 1, NULL}, + + {NULL} /* end of array */ +}; + +GdPropertyDefault gd_cave_defaults_gdash[] = +{ + /* default data */ + {CAVE_OFFSET(selectable), TRUE}, + {CAVE_OFFSET(intermission), FALSE}, + {CAVE_OFFSET(intermission_instantlife), FALSE}, + {CAVE_OFFSET(intermission_rewardlife), TRUE}, + {CAVE_OFFSET(w), 40}, + {CAVE_OFFSET(h), 22}, + {CAVE_OFFSET(x1), 0}, + {CAVE_OFFSET(y1), 0}, + {CAVE_OFFSET(x2), 39}, + {CAVE_OFFSET(y2), 21}, + {CAVE_OFFSET(colorb), 0}, + {CAVE_OFFSET(color0), 0}, + {CAVE_OFFSET(color1), 8}, + {CAVE_OFFSET(color2), 11}, + {CAVE_OFFSET(color3), 1}, + {CAVE_OFFSET(color4), 5}, + {CAVE_OFFSET(color5), 6}, + + /* difficulty */ + {CAVE_OFFSET(level_diamonds[0]), 10}, + {CAVE_OFFSET(diamond_value), 0}, + {CAVE_OFFSET(extra_diamond_value), 0}, + {CAVE_OFFSET(level_time[0]), 999}, + {CAVE_OFFSET(max_time), 999}, + {CAVE_OFFSET(pal_timing), FALSE}, + {CAVE_OFFSET(level_timevalue[0]), 1}, + {CAVE_OFFSET(scheduling), GD_SCHEDULING_MILLISECONDS}, + {CAVE_OFFSET(level_ckdelay[0]), 0}, + {CAVE_OFFSET(level_hatching_delay_time[0]), 2}, + {CAVE_OFFSET(level_speed[0]), 200}, + {CAVE_OFFSET(level_hatching_delay_frame[0]), 21}, + {CAVE_OFFSET(level_rand[0]), 0}, + + /* initial fill */ + {CAVE_OFFSET(initial_border), O_STEEL}, + {CAVE_OFFSET(initial_fill), O_DIRT}, + {CAVE_OFFSET(random_fill[0]), O_DIRT}, + {CAVE_OFFSET(random_fill_probability[0]), 0}, + {CAVE_OFFSET(random_fill[1]), O_DIRT}, + {CAVE_OFFSET(random_fill_probability[1]), 0}, + {CAVE_OFFSET(random_fill[2]), O_DIRT}, + {CAVE_OFFSET(random_fill_probability[2]), 0}, + {CAVE_OFFSET(random_fill[3]), O_DIRT}, + {CAVE_OFFSET(random_fill_probability[3]), 0}, + + /* PLAYER */ + {CAVE_OFFSET(diagonal_movements), FALSE}, + {CAVE_OFFSET(active_is_first_found), TRUE}, + {CAVE_OFFSET(snap_element), O_SPACE}, + {CAVE_OFFSET(pushing_stone_prob), 250000}, + {CAVE_OFFSET(pushing_stone_prob_sweet), 1000000}, + {CAVE_OFFSET(level_bonus_time), 30}, + {CAVE_OFFSET(pneumatic_hammer_frame), 5}, + {CAVE_OFFSET(hammered_walls_reappear), FALSE}, + {CAVE_OFFSET(hammered_wall_reappear_frame), 100}, + {CAVE_OFFSET(voodoo_collects_diamonds), FALSE}, + {CAVE_OFFSET(voodoo_disappear_in_explosion), TRUE}, + {CAVE_OFFSET(voodoo_dies_by_stone), FALSE}, + {CAVE_OFFSET(voodoo_any_hurt_kills_player), FALSE}, + {CAVE_OFFSET(level_penalty_time), 30}, + + /* magic wall */ + {CAVE_OFFSET(level_magic_wall_time), 999}, + {CAVE_OFFSET(magic_diamond_to), O_STONE_F}, + {CAVE_OFFSET(magic_stone_to), O_DIAMOND_F}, + {CAVE_OFFSET(magic_mega_stone_to), O_NITRO_PACK_F}, + {CAVE_OFFSET(magic_nitro_pack_to), O_MEGA_STONE_F}, + {CAVE_OFFSET(magic_nut_to), O_NUT_F}, + {CAVE_OFFSET(magic_flying_stone_to), O_FLYING_DIAMOND_F}, + {CAVE_OFFSET(magic_flying_diamond_to), O_FLYING_STONE_F}, + {CAVE_OFFSET(magic_wall_stops_amoeba), TRUE}, + {CAVE_OFFSET(magic_timer_wait_for_hatching), FALSE}, + /* amoeba */ + {CAVE_OFFSET(amoeba_timer_started_immediately), TRUE}, + {CAVE_OFFSET(amoeba_timer_wait_for_hatching), FALSE}, + + {CAVE_OFFSET(level_amoeba_threshold), 200}, + {CAVE_OFFSET(amoeba_growth_prob), 31250}, + {CAVE_OFFSET(amoeba_fast_growth_prob), 250000}, + {CAVE_OFFSET(level_amoeba_time), 999}, + {CAVE_OFFSET(amoeba_timer_started_immediately), TRUE}, + {CAVE_OFFSET(amoeba_timer_wait_for_hatching), FALSE}, + {CAVE_OFFSET(amoeba_too_big_effect), O_STONE}, + {CAVE_OFFSET(amoeba_enclosed_effect), O_DIAMOND}, + /* amoeba */ + {CAVE_OFFSET(level_amoeba_2_threshold), 200}, + {CAVE_OFFSET(amoeba_2_growth_prob), 31250}, + {CAVE_OFFSET(amoeba_2_fast_growth_prob), 250000}, + {CAVE_OFFSET(level_amoeba_2_time), 999}, + {CAVE_OFFSET(amoeba_2_too_big_effect), O_STONE}, + {CAVE_OFFSET(amoeba_2_enclosed_effect), O_DIAMOND}, + {CAVE_OFFSET(amoeba_2_explodes_by_amoeba), TRUE}, + {CAVE_OFFSET(amoeba_2_looks_like), O_AMOEBA_2}, + {CAVE_OFFSET(amoeba_2_explosion_effect), O_SPACE}, + + /* water */ + {CAVE_OFFSET(water_does_not_flow_down), FALSE}, + + /* nut */ + {CAVE_OFFSET(nut_turns_to_when_crushed), O_NUT_EXPL_1}, + /* replicator */ + {CAVE_OFFSET(replicator_delay_frame), 4}, + {CAVE_OFFSET(replicators_active), TRUE}, + + /* conveyor belt */ + {CAVE_OFFSET(conveyor_belts_active), TRUE}, + {CAVE_OFFSET(conveyor_belts_direction_changed), FALSE}, + + /* slime */ + {CAVE_OFFSET(slime_predictable), TRUE}, + {CAVE_OFFSET(level_slime_seed_c64), -1}, + {CAVE_OFFSET(level_slime_permeability_c64), 0}, + {CAVE_OFFSET(level_slime_permeability), 1000000}, + {CAVE_OFFSET(slime_eats_1), O_DIAMOND}, + {CAVE_OFFSET(slime_converts_1), O_DIAMOND_F}, + {CAVE_OFFSET(slime_eats_2), O_STONE}, + {CAVE_OFFSET(slime_converts_2), O_STONE_F}, + {CAVE_OFFSET(slime_eats_3), O_NUT}, + {CAVE_OFFSET(slime_converts_3), O_NUT_F}, + + /* acid */ + {CAVE_OFFSET(acid_eats_this), O_DIRT}, + {CAVE_OFFSET(acid_spread_ratio), 31250}, + {CAVE_OFFSET(acid_turns_to), O_EXPLODE_3}, + + /* biter */ + {CAVE_OFFSET(biter_delay_frame), 0}, + {CAVE_OFFSET(biter_eat), O_DIAMOND}, + + /* bladder */ + {CAVE_OFFSET(bladder_converts_by), O_VOODOO}, + + /* SOUND */ + {CAVE_OFFSET(amoeba_sound), TRUE}, + {CAVE_OFFSET(magic_wall_sound), TRUE}, + {CAVE_OFFSET(slime_sound), TRUE}, + {CAVE_OFFSET(lava_sound), TRUE}, + {CAVE_OFFSET(replicator_sound), TRUE}, + {CAVE_OFFSET(acid_spread_sound), TRUE}, + {CAVE_OFFSET(biter_sound), TRUE}, + {CAVE_OFFSET(bladder_sound), TRUE}, + {CAVE_OFFSET(water_sound), TRUE}, + {CAVE_OFFSET(stone_sound), TRUE}, + {CAVE_OFFSET(nut_sound), TRUE}, + {CAVE_OFFSET(diamond_sound), TRUE}, + {CAVE_OFFSET(falling_wall_sound), TRUE}, + {CAVE_OFFSET(expanding_wall_sound), TRUE}, + {CAVE_OFFSET(nitro_sound), TRUE}, + {CAVE_OFFSET(pneumatic_hammer_sound), TRUE}, + {CAVE_OFFSET(bladder_spender_sound), TRUE}, + {CAVE_OFFSET(bladder_convert_sound), TRUE}, + {CAVE_OFFSET(gravity_change_sound), TRUE}, + {CAVE_OFFSET(creature_direction_auto_change_sound), TRUE}, + + /* creature effects */ + {CAVE_OFFSET(creatures_backwards), FALSE}, + {CAVE_OFFSET(creatures_direction_auto_change_time), 0}, + {CAVE_OFFSET(creatures_direction_auto_change_on_start), FALSE}, + /* cave effects */ + {CAVE_OFFSET(explosion_effect), O_SPACE}, + {CAVE_OFFSET(diamond_birth_effect), O_DIAMOND}, + {CAVE_OFFSET(bomb_explosion_effect), O_BRICK}, + {CAVE_OFFSET(nitro_explosion_effect), O_SPACE}, + {CAVE_OFFSET(firefly_explode_to), O_EXPLODE_1}, + {CAVE_OFFSET(alt_firefly_explode_to), O_EXPLODE_1}, + {CAVE_OFFSET(butterfly_explode_to), O_PRE_DIA_1}, + {CAVE_OFFSET(alt_butterfly_explode_to), O_PRE_DIA_1}, + {CAVE_OFFSET(stonefly_explode_to), O_PRE_STONE_1}, + {CAVE_OFFSET(dragonfly_explode_to), O_EXPLODE_1}, + + {CAVE_OFFSET(stone_falling_effect), O_STONE_F}, + {CAVE_OFFSET(stone_bouncing_effect), O_STONE}, + {CAVE_OFFSET(diamond_falling_effect), O_DIAMOND_F}, + {CAVE_OFFSET(diamond_bouncing_effect), O_DIAMOND}, + /* visual effects */ + {CAVE_OFFSET(expanding_wall_looks_like), O_BRICK}, + {CAVE_OFFSET(dirt_looks_like), O_DIRT}, + /* gravity */ + {CAVE_OFFSET(gravity), GD_MV_DOWN}, + {CAVE_OFFSET(gravity_switch_active), FALSE}, + {CAVE_OFFSET(skeletons_needed_for_pot), 5}, + {CAVE_OFFSET(gravity_change_time), 10}, + + /* COMPATIBILITY */ + {CAVE_OFFSET(border_scan_first_and_last), TRUE}, + {CAVE_OFFSET(lineshift), FALSE}, + {CAVE_OFFSET(wraparound_objects), FALSE}, + {CAVE_OFFSET(short_explosions), TRUE}, + {CAVE_OFFSET(skeletons_worth_diamonds), 0}, + {CAVE_OFFSET(gravity_affects_all), TRUE}, + + {-1}, +}; + +/* return new element, which appears after elem is hammered. */ +/* returns o_none, if elem is invalid for hammering. */ +GdElement gd_element_get_hammered(GdElement elem) +{ + switch (elem) + { + /* what is under the pneumatic hammer? */ + case O_WALLED_KEY_1: + return O_KEY_1; + + case O_WALLED_KEY_2: + return O_KEY_2; + + case O_WALLED_KEY_3: + return O_KEY_3; + + case O_WALLED_DIAMOND: + return O_DIAMOND; + + case O_BRICK: + case O_BRICK_SLOPED_UP_RIGHT: + case O_BRICK_SLOPED_UP_LEFT: + case O_BRICK_SLOPED_DOWN_RIGHT: + case O_BRICK_SLOPED_DOWN_LEFT: + case O_BRICK_NON_SLOPED: + case O_MAGIC_WALL: + case O_STEEL_EXPLODABLE: + case O_EXPANDING_WALL: + case O_V_EXPANDING_WALL: + case O_H_EXPANDING_WALL: + case O_FALLING_WALL: + case O_FALLING_WALL_F: + return O_SPACE; + + default: + return O_NONE; + } +} + +void gd_cave_db_init(void) +{ + int i; + GHashTable *pointers; + boolean lowercase_names = TRUE; + + /* TRANSLATORS: some languages (for example, german) do not have lowercase nouns. */ + /* When gdash generates the list of lowercase element names, this has to be */ + /* taken into account. Therefore we have a string, which must be changed */ + /* by the translator to select the behavior. */ + /* For example, the name of the element is "Brick wall", as in a button, it has to be */ + /* written with an uppercase initial. But if "Line of brick wall", the B is changed to b. */ + /* However, this is not allowed in some languages, for example, German. */ + /* So one writes "Ziegelmauer", and "Linie aus Ziegelmauer", the Z is not changed to z. */ + /* Set the translated string to "lowercase-element-names-yes", if your language */ + /* allows writing nouns with lowercase initials. Set it to "lowercase-element-names-no", */ + /* if not: for example, german. Do not translate the string, but set the behavior! */ + + if (strEqual(_("lowercase-element-names-yes"), "lowercase-element-names-no")) + lowercase_names = FALSE; + + /* check element database for faults. */ + for (i = 0; gd_elements[i].element!=-1; i++) + { + if (gd_elements[i].element != i) + Error("element: i:0x%x!=0x%x", i, gd_elements[i].element); + + /* if it has a name, create a lowercase name (of the translated one). + will be used by the editor */ + if (gd_elements[i].name) + { + if (lowercase_names) + /* the function allocates a new string, but it is needed as long as the app is running */ + gd_elements[i].lowercase_name = g_utf8_strdown(gettext(gd_elements[i].name), -1); + else + /* only translate, no lowercase. */ + gd_elements[i].lowercase_name = gettext(gd_elements[i].name); + } + + /* we do not like generated pixbufs for games. only those that are in the png. */ + if (ABS(gd_elements[i].image_game)>GD_NUM_OF_CELLS_X*GD_NUM_OF_CELLS_Y) + Error("game pixbuf for element %x (%s) bigger than png size", i, gd_elements[i].name); + + if (gd_elements[i].image < 0) + Error("editor pixbuf for element %x (%s) should not be animated", i, gd_elements[i].name); + + if (gd_elements[i].properties&P_CAN_BE_HAMMERED && gd_element_get_hammered((GdElement) i) == O_NONE) + Error("element %x (%s) can be hammered, but get_hammered_element does not define another one", i, gd_elements[i].name); + } + + /* NOT REALLY NEEDED ANYMORE, as the enum takes care of it. + maybe to show that there is an unnecessary one. */ + /* + g_print("Free pixbuf indexes: "); + for (i = GD_NUM_OF_CELLS_X*GD_NUM_OF_CELLS_Y; icharacter characters. */ + /* + gd_create_char_to_element_table(); + g_print("Free characters: "); + for (i = 32; i < 128; i++) + if (gd_char_to_element[i]==O_UNKNOWN) + g_print("%c", i); + g_print("\n"); + */ + + /* check the cave property database for faults. */ + pointers = g_hash_table_new(g_direct_hash, g_direct_equal); + + for (i = 0; gd_cave_properties[i].identifier != NULL; i++) + { + GdType type = gd_cave_properties[i].type; + + switch (type) + { + case GD_LABEL: + case GD_TAB: + /* some lines are used for the user interface. these should not have + an identifier. */ + if (strcmp(gd_cave_properties[i].identifier, "") != 0) + { + Error ("ui lines in cave properties should not have identifiers: %s", + gd_cave_properties[i].identifier); + } + break; + + case GD_TYPE_STRING: + /* check if any of the properties are designated as string arrays. + they are not supported in + * file read/write and operations, also they do not even make any sense! */ + if (gd_cave_properties[i].count != 1) + { + Error ("string arrays have no sense in cave properties: %s", + gd_cave_properties[i].identifier); + } + break; + + case GD_TYPE_LONGSTRING: + if (gd_cave_properties[i].count != 1) + { + Error ("longstring arrays have no sense cave properties: %s", + gd_cave_properties[i].identifier); + } + break; + + case GD_TYPE_EFFECT: + /* the same applies for effects. */ + if (gd_cave_properties[i].count != 1) + { + Error ("effect arrays not supported in cave properties: %s", + gd_cave_properties[i].identifier); + } + break; + + case GD_TYPE_COLOR: + /* the same applies for effects. */ + if (gd_cave_properties[i].count != 1) + { + Error ("color arrays not supported in cave properties: %s", + gd_cave_properties[i].identifier); + } + break; + + default: + break; + } + + if (type != GD_LABEL && (gd_cave_properties[i].flags & GD_SHOW_LEVEL_LABEL)) + { + Error ("show_level_label only for labels: line %d", i); + } + + if (type != GD_LABEL && type != GD_TAB) + { + const char *another_prop; + + /* other types */ + /* check if its pointer is not the same as another one's */ + /* +1 is added so it is never zero */ + if (!(gd_cave_properties[i].flags&GD_DONT_SAVE) && strcmp(gd_cave_properties[i].identifier, "") == 0) + { + Error ("property should have a bdcff identifier: line %d, name %s", + i, gd_cave_properties[i].name); + } + + another_prop = g_hash_table_lookup(pointers, GINT_TO_POINTER(gd_cave_properties[i].offset + 1)); + if (another_prop!=NULL) + { + Error("property %s has the same pointer as property %s", + gd_cave_properties[i].identifier, another_prop); + } + else + { + /* value is the identifier, so we can report the OLD one if the check fails */ + g_hash_table_insert(pointers, GINT_TO_POINTER(gd_cave_properties[i].offset + 1), + gd_cave_properties[i].identifier); + } + } + } + + g_hash_table_destroy(pointers); +} diff --git a/src/game_bd/bd_cavedb.h b/src/game_bd/bd_cavedb.h new file mode 100644 index 00000000..31c54753 --- /dev/null +++ b/src/game_bd/bd_cavedb.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef BD_CAVEDB_H +#define BD_CAVEDB_H + +#include + +#include "bd_cave.h" + + +extern GdElements gd_elements[]; + +extern const GdStructDescriptor gd_cave_properties[]; +extern const GdStructDescriptor gd_replay_properties[]; + +extern GdPropertyDefault gd_cave_defaults_gdash[]; + +/* do some checks on the cave db */ +void gd_cave_db_init(void); +GdElement gd_element_get_hammered(GdElement elem); + +#endif // BD_CAVEDB_H diff --git a/src/game_bd/bd_caveengine.c b/src/game_bd/bd_caveengine.c new file mode 100644 index 00000000..5788933b --- /dev/null +++ b/src/game_bd/bd_caveengine.c @@ -0,0 +1,3596 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* IMPORTANT NOTES */ + +/* + * LAVA. + * + * Lava absorbs everything going into it. Everything. + * But it does not "pull" elements; only the things disappear which + * _do_ go directly into it. So if the player steps into the lava, + * he will die. If a dragonfly flies over it, it will not. + * + * This behavior is implemented in the is_space_dir and the store + * functions. is_space_dir returns true for the lava, too. The store + * function ignores any store requests into the lava. + * The player_get function will also behave for lava as it does for space. + */ + +#include + +#include "main_bd.h" + + +/* for gravity */ +static const GdDirection ccw_eighth[] = +{ + GD_MV_STILL, + GD_MV_UP_LEFT, + GD_MV_UP, + GD_MV_UP_RIGHT, + GD_MV_RIGHT, + GD_MV_DOWN_RIGHT, + GD_MV_DOWN, + GD_MV_DOWN_LEFT +}; + +static const GdDirection ccw_fourth[] = +{ + GD_MV_STILL, + GD_MV_LEFT, + GD_MV_UP_LEFT, + GD_MV_UP, + GD_MV_UP_RIGHT, + GD_MV_RIGHT, + GD_MV_DOWN_RIGHT, + GD_MV_DOWN, + GD_MV_DOWN_LEFT, + GD_MV_LEFT +}; + +static const GdDirection cw_eighth[] = +{ + GD_MV_STILL, + GD_MV_UP_RIGHT, + GD_MV_RIGHT, + GD_MV_DOWN_RIGHT, + GD_MV_DOWN, + GD_MV_DOWN_LEFT, + GD_MV_LEFT, + GD_MV_UP_LEFT, + GD_MV_UP +}; + +static const GdDirection cw_fourth[] = +{ + GD_MV_STILL, + GD_MV_RIGHT, + GD_MV_DOWN_RIGHT, + GD_MV_DOWN, + GD_MV_DOWN_LEFT, + GD_MV_LEFT, + GD_MV_UP_LEFT, + GD_MV_UP, + GD_MV_UP_RIGHT +}; + +static const GdDirection opposite[] = +{ + GD_MV_STILL, + GD_MV_DOWN, + GD_MV_DOWN_LEFT, + GD_MV_LEFT, + GD_MV_UP_LEFT, + GD_MV_UP, + GD_MV_UP_RIGHT, + GD_MV_RIGHT, + GD_MV_DOWN_RIGHT +}; + +/* sets timeout sound. */ +void gd_cave_set_seconds_sound(GdCave *cave) +{ + // when not counting bonus time, timeout sounds will be played by main game engine; + // also skip timeout sounds when not using native sound engine + if (game_bd.game == NULL || game_bd.game->state_counter != GAME_INT_CHECK_BONUS_TIME || + !game.use_native_bd_sound_engine) + return; + + /* this is an integer division, so 0 seconds can be 0.5 seconds... */ + /* also, when this reaches 8, the player still has 8.9999 seconds. + so the sound is played at almost t = 9s. */ + switch (cave->time / cave->timing_factor) + { + case 9: gd_sound_play(cave, GD_S_TIMEOUT_10, O_NONE, -1, -1); break; + case 8: gd_sound_play(cave, GD_S_TIMEOUT_9, O_NONE, -1, -1); break; + case 7: gd_sound_play(cave, GD_S_TIMEOUT_8, O_NONE, -1, -1); break; + case 6: gd_sound_play(cave, GD_S_TIMEOUT_7, O_NONE, -1, -1); break; + case 5: gd_sound_play(cave, GD_S_TIMEOUT_6, O_NONE, -1, -1); break; + case 4: gd_sound_play(cave, GD_S_TIMEOUT_5, O_NONE, -1, -1); break; + case 3: gd_sound_play(cave, GD_S_TIMEOUT_4, O_NONE, -1, -1); break; + case 2: gd_sound_play(cave, GD_S_TIMEOUT_3, O_NONE, -1, -1); break; + case 1: gd_sound_play(cave, GD_S_TIMEOUT_2, O_NONE, -1, -1); break; + case 0: gd_sound_play(cave, GD_S_TIMEOUT_1, O_NONE, -1, -1); break; + } +} + +/* play diamond or stone sound of given element. */ +static void play_sound_of_element(GdCave *cave, GdElement element, int x, int y) +{ + /* stone and diamond fall sounds. */ + switch (element) + { + case O_NUT: + case O_NUT_F: + if (cave->nut_sound) + gd_sound_play(cave, GD_S_NUT, element, x, y); + break; + + case O_STONE: + case O_STONE_F: + case O_FLYING_STONE: + case O_FLYING_STONE_F: + case O_MEGA_STONE: + case O_MEGA_STONE_F: + case O_WAITING_STONE: + case O_CHASING_STONE: + if (cave->stone_sound) + gd_sound_play(cave, GD_S_STONE, element, x, y); + break; + + case O_NITRO_PACK: + case O_NITRO_PACK_F: + if (cave->nitro_sound) + gd_sound_play(cave, GD_S_NITRO, element, x, y); + break; + + case O_FALLING_WALL: + case O_FALLING_WALL_F: + if (cave->falling_wall_sound) + gd_sound_play(cave, GD_S_FALLING_WALL, element, x, y); + break; + + case O_H_EXPANDING_WALL: + case O_V_EXPANDING_WALL: + case O_EXPANDING_WALL: + case O_H_EXPANDING_STEEL_WALL: + case O_V_EXPANDING_STEEL_WALL: + case O_EXPANDING_STEEL_WALL: + if (cave->expanding_wall_sound) + gd_sound_play(cave, GD_S_EXPANDING_WALL, element, x, y); + break; + + case O_DIAMOND: + case O_DIAMOND_F: + case O_FLYING_DIAMOND: + case O_FLYING_DIAMOND_F: + if (cave->diamond_sound) + gd_sound_play(cave, GD_S_DIAMOND_RANDOM, element, x, y); + break; + + case O_BLADDER_SPENDER: + if (cave->bladder_spender_sound) + gd_sound_play(cave, GD_S_BLADDER_SPENDER, element, x, y); + break; + + case O_PRE_CLOCK_1: + if (cave->bladder_convert_sound) + gd_sound_play(cave, GD_S_BLADDER_CONVERT, element, x, y); + break; + + case O_SLIME: + if (cave->slime_sound) + gd_sound_play(cave, GD_S_SLIME, element, x, y); + break; + + case O_LAVA: + if (cave->lava_sound) + gd_sound_play(cave, GD_S_LAVA, element, x, y); + break; + + case O_ACID: + if (cave->acid_spread_sound) + gd_sound_play(cave, GD_S_ACID_SPREAD, element, x, y); + break; + + case O_BLADDER: + if (cave->bladder_sound) + gd_sound_play(cave, GD_S_BLADDER_MOVE, element, x, y); + break; + + case O_BITER_1: + case O_BITER_2: + case O_BITER_3: + case O_BITER_4: + if (cave->biter_sound) + gd_sound_play(cave, GD_S_BITER_EAT, element, x, y); + break; + + case O_DIRT_BALL: + case O_DIRT_BALL_F: + case O_DIRT_LOOSE: + case O_DIRT_LOOSE_F: + gd_sound_play(cave, GD_S_DIRT_BALL, element, x, y); + break; + + default: + /* do nothing. */ + break; + } +} + +static inline GdElement *getp(const GdCave *cave, const int x, const int y) +{ + return cave->getp(cave, x, y); +} + +/* + perfect (non-lineshifting) GET function. + returns a pointer to a selected cave element by its coordinates. +*/ +static inline GdElement *getp_perfect(const GdCave *cave, const int x, const int y) +{ + /* (x + n) mod n: this works also for x >= n and -n + 1 < x < 0 */ + return &(cave->map[(y + cave->h) % cave->h][(x + cave->w) % cave->w]); +} + +/* + line shifting GET function; returns a pointer to the selected cave element. + this is used to emulate the line-shifting behaviour of original games, so that + the player entering one side will appear one row above or below on the other. +*/ +static inline GdElement *getp_shift(const GdCave *cave, int x, int y) +{ + if (x >= cave->w) + { + y++; + x -= cave->w; + } + else if (x < 0) + { + y--; + x += cave->w; + } + + y = (y + cave->h) % cave->h; + + return &(cave->map[y][x]); +} + +static inline GdElement get(const GdCave *cave, const int x, const int y) +{ + return *getp(cave, x, y); +} + +/* returns an element which is somewhere near x,y */ +static inline GdElement get_dir(const GdCave *cave, const int x, const int y, + const GdDirection dir) +{ + return get(cave, x + gd_dx[dir], y + gd_dy[dir]); +} + +static inline boolean explodes_by_hit_dir(const GdCave *cave, const int x, + const int y, GdDirection dir) +{ + return (gd_elements[get_dir(cave, x, y, dir) & O_MASK].properties & P_EXPLODES_BY_HIT) != 0; +} + +/* returns true if the element is not explodable, for example the steel wall */ +static inline boolean non_explodable(const GdCave *cave, const int x, const int y) +{ + return (gd_elements[get(cave, x,y) & O_MASK].properties & P_NON_EXPLODABLE) != 0; +} + +/* returns true if the element can be eaten by the amoeba, eg. space and dirt. */ +static inline boolean amoeba_eats_dir(const GdCave *cave, const int x, const int y, + const GdDirection dir) +{ + return (gd_elements[get_dir(cave, x, y, dir) & O_MASK].properties & P_AMOEBA_CONSUMES) != 0; +} + +/* returns true if the element is sloped, so stones and diamonds roll down on it. + for example a stone or brick wall */ +static inline boolean sloped_dir(const GdCave *cave, const int x, const int y, + const GdDirection dir, const GdDirection slop) +{ + switch (slop) + { + case GD_MV_LEFT: + return (gd_elements[get_dir(cave, x, y, dir) & O_MASK].properties & P_SLOPED_LEFT) != 0; + + case GD_MV_RIGHT: + return (gd_elements[get_dir(cave, x, y, dir) & O_MASK].properties & P_SLOPED_RIGHT) != 0; + + case GD_MV_UP: + return (gd_elements[get_dir(cave, x, y, dir) & O_MASK].properties & P_SLOPED_UP) != 0; + + case GD_MV_DOWN: + return (gd_elements[get_dir(cave, x, y, dir) & O_MASK].properties & P_SLOPED_DOWN) != 0; + + default: + break; + } + + return FALSE; +} + +/* returns true if the element is sloped for bladder movement + (brick = yes, diamond = no, for example) */ +static inline boolean sloped_for_bladder_dir (const GdCave *cave, const int x, const int y, + const GdDirection dir) +{ + return (gd_elements[get_dir(cave, x, y, dir) & O_MASK].properties & P_BLADDER_SLOPED) != 0; +} + +static inline boolean blows_up_flies_dir(const GdCave *cave, const int x, const int y, + const GdDirection dir) +{ + return (gd_elements[get_dir(cave, x, y, dir) & O_MASK].properties & P_BLOWS_UP_FLIES) != 0; +} + +/* returns true if the element is a counter-clockwise creature */ +static inline boolean rotates_ccw (const GdCave *cave, const int x, const int y) +{ + return (gd_elements[get(cave, x, y) & O_MASK].properties & P_CCW) != 0; +} + +/* returns true if the element is a player */ +static inline boolean is_player(const GdCave *cave, const int x, const int y) +{ + return (gd_elements[get(cave, x, y) & O_MASK].properties & P_PLAYER) != 0; +} + +/* returns true if the element is a player */ +static inline boolean is_player_dir(const GdCave *cave, const int x, const int y, + const GdDirection dir) +{ + return (gd_elements[get_dir(cave, x, y, dir) & O_MASK].properties & P_PLAYER) != 0; +} + +static inline boolean can_be_hammered_dir(const GdCave *cave, const int x, const int y, + const GdDirection dir) +{ + return (gd_elements[get_dir(cave, x, y, dir) & O_MASK].properties & P_CAN_BE_HAMMERED) != 0; +} + +/* returns true if the element is explodable and explodes to space, for example the player */ +static inline boolean is_first_stage_of_explosion(const GdCave *cave, const int x, const int y) +{ + return (gd_elements[get(cave, x, y) & O_MASK].properties & P_EXPLOSION_FIRST_STAGE) != 0; +} + +/* returns true if the element is moved by the conveyor belt */ +static inline boolean moved_by_conveyor_top_dir(const GdCave *cave, const int x, const int y, + const GdDirection dir) +{ + return (gd_elements[get_dir(cave, x, y, dir) & O_MASK].properties & P_MOVED_BY_CONVEYOR_TOP) != 0; +} + +/* returns true if the element is moved by the conveyor belt */ +static inline boolean moved_by_conveyor_bottom_dir(const GdCave *cave, const int x, const int y, + const GdDirection dir) +{ + return (gd_elements[get_dir(cave, x, y, dir) & O_MASK].properties & P_MOVED_BY_CONVEYOR_BOTTOM) != 0; +} + +static inline boolean is_scanned_dir(const GdCave *cave, const int x, const int y, + const GdDirection dir) +{ + return (get_dir(cave, x, y, dir) & SCANNED) != 0; +} + +/* returns true if neighbouring element is "e" */ +/* treats dirt specially */ +/* treats lava specially */ +static inline boolean is_element_dir(const GdCave *cave, const int x, const int y, + const GdDirection dir, GdElement e) +{ + GdElement examined = get_dir(cave, x, y, dir); + + /* if it is a dirt-like, change to dirt, so equality will evaluate to true */ + if (gd_elements[examined & O_MASK].properties & P_DIRT) + examined = O_DIRT; + + if (gd_elements[e & O_MASK].properties & P_DIRT) + e = O_DIRT; + + /* if the element on the map is a lava, it should be like space */ + if (examined == O_LAVA) + examined = O_SPACE; + + return (e == examined); +} + +/* returns true if neighbouring element is space */ +static inline boolean is_space_dir(const GdCave *cave, const int x, const int y, + const GdDirection dir) +{ + GdElement e = get_dir(cave, x, y, dir)&O_MASK; + + return (e == O_SPACE || e == O_LAVA); +} + +/* store an element at the given position */ +static inline void store(GdCave *cave, const int x, const int y, const GdElement element) +{ + GdElement *e = getp(cave, x, y); + + if (*e == O_LAVA) + { + play_sound_of_element(cave, O_LAVA, x, y); + + return; + } + + *e = element; +} + +/* store an element with SCANNED flag turned on */ +static inline void store_sc(GdCave *cave, const int x, const int y, const GdElement element) +{ + store(cave, x, y, element | SCANNED); +} + +/* store an element to a neighbouring cell */ +static inline void store_dir(GdCave *cave, const int x, const int y, + const GdDirection dir, const GdElement element) +{ + store(cave, x + gd_dx[dir], y + gd_dy[dir], element|SCANNED); +} + +/* store an element to a neighbouring cell */ +static inline void store_dir_no_scanned(GdCave *cave, const int x, const int y, + const GdDirection dir, const GdElement element) +{ + store(cave, x + gd_dx[dir], y + gd_dy[dir], element); +} + +/* move element to direction; then place space at x, y */ +static inline void move(GdCave *cave, const int x, const int y, + const GdDirection dir, const GdElement e) +{ + store_dir(cave, x, y, dir, e); + store(cave, x, y, O_SPACE); +} + +/* increment a cave element; can be used for elements which are one after + the other, for example bladder1, bladder2, bladder3... */ +static inline void next(GdCave *cave, const int x, const int y) +{ + (*getp(cave, x, y))++; +} + +static void cell_explode(GdCave *cave, int x, int y, GdElement explode_to) +{ + if (non_explodable (cave, x, y)) + return; + + if (cave->voodoo_any_hurt_kills_player && get(cave, x, y) == O_VOODOO) + cave->voodoo_touched = TRUE; + + if (get(cave, x, y) == O_VOODOO && !cave->voodoo_disappear_in_explosion) + /* voodoo turns into a time penalty */ + store_sc(cave, x, y, O_TIME_PENALTY); + else if (get(cave, x, y) == O_NITRO_PACK || + get(cave, x, y) == O_NITRO_PACK_F) + /* nitro pack inside an explosion - it is now triggered */ + store_sc(cave, x, y, O_NITRO_PACK_EXPLODE); + else + /* for everything else */ + store_sc(cave, x, y, explode_to); +} + +/* a creature explodes to a 3x3 something. */ +static void creature_explode(GdCave *cave, int x, int y, GdElement explode_to) +{ + int xx, yy; + + /* the processing of an explosion took pretty much time: processing 3x3 = 9 elements */ + cave->ckdelay += 1200; + gd_sound_play(cave, GD_S_EXPLOSION, get(cave, x, y), x, y); + + for (yy = y - 1; yy <= y + 1; yy++) + for (xx = x - 1; xx <= x + 1; xx++) + cell_explode(cave, xx, yy, explode_to); +} + +static void nitro_explode(GdCave *cave, int x, int y) +{ + int xx, yy; + + /* the processing of an explosion took pretty much time: processing 3x3 = 9 elements */ + cave->ckdelay += 1200; + gd_sound_play(cave, GD_S_NITRO_EXPLOSION, get(cave, x, y), x, y); + + for (yy = y - 1; yy <= y + 1; yy++) + for (xx = x - 1; xx <= x + 1; xx++) + cell_explode(cave, xx, yy, O_NITRO_EXPL_1); + + /* the current cell is explicitly changed into a nitro expl, + as cell_explode changes it to a triggered nitro pack */ + store_sc(cave, x, y, O_NITRO_EXPL_1); +} + +/* a voodoo explodes, leaving a 3x3 steel and a time penalty behind. */ +static void voodoo_explode(GdCave *cave, int x, int y) +{ + int xx, yy; + + /* the processing of an explosion took pretty much time: processing 3x3 = 9 elements */ + cave->ckdelay += 1000; + + gd_sound_play(cave, GD_S_VOODOO_EXPLOSION, get(cave, x, y), x, y); + if (cave->voodoo_any_hurt_kills_player) + cave->voodoo_touched = TRUE; + + /* voodoo explodes to 3x3 steel */ + for (yy = y - 1; yy <= y + 1; yy++) + for (xx = x - 1; xx <= x + 1; xx++) + store_sc(cave, xx, yy, O_PRE_STEEL_1); + + /* middle is a time penalty (which will be turned into a gravestone) */ + store_sc(cave, x, y, O_TIME_PENALTY); +} + +/* a bomb does not explode the voodoo, neither does the ghost. + this function check this, and stores the new element or not. + destroying the voodoo is also controlled by the + voodoo_disappear_in_explosion flag. */ +static void explode_try_skip_voodoo(GdCave *cave, const int x, const int y, const GdElement expl) +{ + if (non_explodable (cave, x, y)) + return; + + /* bomb does not explode voodoo */ + if (!cave->voodoo_disappear_in_explosion && get(cave, x, y) == O_VOODOO) + return; + + if (cave->voodoo_any_hurt_kills_player && get(cave, x, y) == O_VOODOO) + cave->voodoo_touched = TRUE; + + store_sc (cave, x, y, expl); +} + +/* X shaped ghost explosion; does not touch voodoo! */ +static void ghost_explode(GdCave *cave, const int x, const int y) +{ + gd_sound_play(cave, GD_S_GHOST_EXPLOSION, get(cave, x, y), x, y); + + /* the processing of an explosion took pretty much time: processing 5 elements */ + cave->ckdelay += 650; + + explode_try_skip_voodoo(cave, x, y, O_GHOST_EXPL_1); + explode_try_skip_voodoo(cave, x - 1, y - 1, O_GHOST_EXPL_1); + explode_try_skip_voodoo(cave, x + 1, y + 1, O_GHOST_EXPL_1); + explode_try_skip_voodoo(cave, x - 1, y + 1, O_GHOST_EXPL_1); + explode_try_skip_voodoo(cave, x + 1, y - 1, O_GHOST_EXPL_1); +} + +/* +shaped bomb explosion; does not touch voodoo! */ +static void bomb_explode(GdCave *cave, const int x, const int y) +{ + gd_sound_play(cave, GD_S_BOMB_EXPLOSION, get(cave, x, y), x, y); + + /* the processing of an explosion took pretty much time: processing 5 elements */ + cave->ckdelay += 650; + + explode_try_skip_voodoo(cave, x, y, O_BOMB_EXPL_1); + explode_try_skip_voodoo(cave, x - 1, y, O_BOMB_EXPL_1); + explode_try_skip_voodoo(cave, x + 1, y, O_BOMB_EXPL_1); + explode_try_skip_voodoo(cave, x, y + 1, O_BOMB_EXPL_1); + explode_try_skip_voodoo(cave, x, y - 1, O_BOMB_EXPL_1); +} + +/* + explode an element with the appropriate type of exlposion. + */ +static void explode(GdCave *cave, int x, int y) +{ + GdElement e = get(cave, x, y) & O_MASK; + + switch (e) + { + case O_GHOST: + ghost_explode(cave, x, y); + break; + + case O_BOMB_TICK_7: + bomb_explode(cave, x, y); + break; + + case O_VOODOO: + voodoo_explode(cave, x, y); + break; + + case O_NITRO_PACK: + case O_NITRO_PACK_F: + case O_NITRO_PACK_EXPLODE: + nitro_explode(cave, x, y); + break; + + case O_AMOEBA_2: + creature_explode(cave, x, y, O_AMOEBA_2_EXPL_1); + break; + + case O_FALLING_WALL_F: + creature_explode(cave, x, y, O_EXPLODE_1); + break; + + case O_BUTTER_1: + case O_BUTTER_2: + case O_BUTTER_3: + case O_BUTTER_4: + creature_explode(cave, x, y, cave->butterfly_explode_to); + break; + + case O_ALT_BUTTER_1: + case O_ALT_BUTTER_2: + case O_ALT_BUTTER_3: + case O_ALT_BUTTER_4: + creature_explode(cave, x, y, cave->alt_butterfly_explode_to); + break; + + case O_FIREFLY_1: + case O_FIREFLY_2: + case O_FIREFLY_3: + case O_FIREFLY_4: + creature_explode(cave, x, y, cave->firefly_explode_to); + break; + + case O_ALT_FIREFLY_1: + case O_ALT_FIREFLY_2: + case O_ALT_FIREFLY_3: + case O_ALT_FIREFLY_4: + creature_explode(cave, x, y, cave->alt_firefly_explode_to); + break; + + case O_PLAYER: + case O_PLAYER_BOMB: + case O_PLAYER_GLUED: + case O_PLAYER_STIRRING: + case O_PLAYER_PNEUMATIC_LEFT: + case O_PLAYER_PNEUMATIC_RIGHT: + creature_explode(cave, x, y, O_EXPLODE_1); + break; + + case O_STONEFLY_1: + case O_STONEFLY_2: + case O_STONEFLY_3: + case O_STONEFLY_4: + creature_explode(cave, x, y, cave->stonefly_explode_to); + break; + + case O_DRAGONFLY_1: + case O_DRAGONFLY_2: + case O_DRAGONFLY_3: + case O_DRAGONFLY_4: + creature_explode(cave, x, y, cave->dragonfly_explode_to); + break; + + default: + break; + } +} + +static void inline +explode_dir(GdCave *cave, const int x, const int y, GdDirection dir) +{ + explode(cave, x + gd_dx[dir], y + gd_dy[dir]); +} + +/* + player eats specified object. + returns O_SPACE if he eats it (diamond, dirt, space, outbox) + returns other element if something other appears there and he can't move. + cave pointer is needed to know the diamond values. +*/ +static GdElement player_get_element (GdCave* cave, const GdElement object, int x, int y) +{ + int i; + + switch (object) + { + case O_DIAMOND_KEY: + cave->diamond_key_collected = TRUE; + gd_sound_play(cave, GD_S_DIAMOND_KEY_COLLECT, object, x, y); + return O_SPACE; + + /* KEYS AND DOORS */ + case O_KEY_1: + gd_sound_play(cave, GD_S_KEY_COLLECT, object, x, y); + cave->key1++; + return O_SPACE; + + case O_KEY_2: + gd_sound_play(cave, GD_S_KEY_COLLECT, object, x, y); + cave->key2++; + return O_SPACE; + + case O_KEY_3: + gd_sound_play(cave, GD_S_KEY_COLLECT, object, x, y); + cave->key3++; + return O_SPACE; + + case O_DOOR_1: + if (cave->key1 == 0) + return object; + gd_sound_play(cave, GD_S_DOOR_OPEN, object, x, y); + cave->key1--; + return O_SPACE; + + case O_DOOR_2: + if (cave->key2 == 0) + return object; + gd_sound_play(cave, GD_S_DOOR_OPEN, object, x, y); + cave->key2--; + return O_SPACE; + + case O_DOOR_3: + if (cave->key3 == 0) + return object; + gd_sound_play(cave, GD_S_DOOR_OPEN, object, x, y); + cave->key3--; + return O_SPACE; + + /* SWITCHES */ + case O_CREATURE_SWITCH: /* creatures change direction. */ + gd_sound_play(cave, GD_S_SWITCH_CREATURES, object, x, y); + cave->creatures_backwards = !cave->creatures_backwards; + return object; + + case O_EXPANDING_WALL_SWITCH: /* expanding wall change direction. */ + gd_sound_play(cave, GD_S_SWITCH_EXPANDING, object, x, y); + cave->expanding_wall_changed = !cave->expanding_wall_changed; + return object; + + case O_BITER_SWITCH: /* biter change delay */ + gd_sound_play(cave, GD_S_SWITCH_BITER, object, x, y); + cave->biter_delay_frame++; + if (cave->biter_delay_frame == 4) + cave->biter_delay_frame = 0; + return object; + + case O_REPLICATOR_SWITCH: /* replicator on/off switch */ + gd_sound_play(cave, GD_S_SWITCH_REPLICATOR, object, x, y); + cave->replicators_active = !cave->replicators_active; + return object; + + case O_CONVEYOR_SWITCH: /* conveyor belts on/off */ + gd_sound_play(cave, GD_S_SWITCH_CONVEYOR, object, x, y); + cave->conveyor_belts_active = !cave->conveyor_belts_active; + return object; + + case O_CONVEYOR_DIR_SWITCH: /* conveyor belts switch direction */ + gd_sound_play(cave, GD_S_SWITCH_CONVEYOR, object, x, y); + cave->conveyor_belts_direction_changed = !cave->conveyor_belts_direction_changed; + return object; + + /* USUAL STUFF */ + case O_DIRT: + case O_DIRT2: + case O_STEEL_EATABLE: + case O_BRICK_EATABLE: + case O_DIRT_SLOPED_UP_RIGHT: + case O_DIRT_SLOPED_UP_LEFT: + case O_DIRT_SLOPED_DOWN_LEFT: + case O_DIRT_SLOPED_DOWN_RIGHT: + case O_DIRT_BALL: + case O_DIRT_LOOSE: + gd_sound_play(cave, GD_S_WALK_EARTH, object, x, y); + return O_SPACE; + + case O_SWEET: + gd_sound_play(cave, GD_S_SWEET_COLLECT, object, x, y); + cave->sweet_eaten = TRUE; + return O_SPACE; + + case O_PNEUMATIC_HAMMER: + gd_sound_play(cave, GD_S_PNEUMATIC_COLLECT, object, x, y); + cave->got_pneumatic_hammer = TRUE; + return O_SPACE; + + case O_CLOCK: + /* bonus time */ + gd_sound_play(cave, GD_S_CLOCK_COLLECT, object, x, y); + cave->time += cave->time_bonus * cave->timing_factor; + if (cave->time > cave->max_time * cave->timing_factor) + cave->time -= cave->max_time * cave->timing_factor; + /* no space, rather a dirt remains there... */ + return O_DIRT; + + case O_DIAMOND: + case O_FLYING_DIAMOND: + // prevent diamond sounds for O_SKELETON (see below) + if (x != -1 && y != -1) + gd_sound_play(cave, GD_S_DIAMOND_COLLECT, object, x, y); + + cave->score += cave->diamond_value; + cave->diamonds_collected++; + + if (cave->diamonds_needed == cave->diamonds_collected) + { + cave->gate_open = TRUE; + + /* extra is worth more points. */ + cave->diamond_value = cave->extra_diamond_value; + + cave->gate_open_flash = 1; + cave->sound3 = GD_S_CRACK; + gd_sound_play(cave, GD_S_CRACK, O_OUTBOX, x, y); + } + return O_SPACE; + + case O_SKELETON: + cave->skeletons_collected++; + + /* as if player got a diamond */ + for (i = 0; i < cave->skeletons_worth_diamonds; i++) + player_get_element(cave, O_DIAMOND, -1, -1); + + /* _after_ calling get_element for the fake diamonds, so we overwrite its sounds */ + gd_sound_play(cave, GD_S_SKELETON_COLLECT, object, x, y); + return O_SPACE; + + case O_OUTBOX: + case O_INVIS_OUTBOX: + cave->player_state = GD_PL_EXITED; /* player now exits the cave! */ + return O_SPACE; + + case O_SPACE: + case O_LAVA: /* player goes into lava, as if it was space */ + gd_sound_play(cave, GD_S_WALK_EMPTY, object, x, y); + return O_SPACE; + + default: + /* the object will remain there. */ + return object; + } +} + +/* + process a crazy dream-style teleporter. + called from gd_cave_iterate, for a player or a player_bomb. + player is standing at px, py, and trying to move in the direction player_move, + where there is a teleporter. + we check the whole cave, from px+1,py, till we get back to px,py (by wrapping + around). the first teleporter we find, and which is suitable, will be the destination. + return TRUE if teleporter worked, FALSE if cound not find any suitable teleporter. + */ +static boolean do_teleporter(GdCave *cave, int px, int py, GdDirection player_move) +{ + int tx, ty; + + tx = px; + ty = py; + + do + { + /* jump to next element; wrap around columns and rows. */ + tx++; + + if (tx >= cave->w) + { + tx = 0; + ty++; + + if (ty >= cave->h) + ty = 0; + } + + /* if we found a teleporter... */ + if (get(cave, tx, ty) == O_TELEPORTER && + is_space_dir(cave, tx, ty, player_move)) + { + /* new player appears near teleporter found */ + store_dir(cave, tx, ty, player_move, get(cave, px, py)); + + /* current player disappears */ + store(cave, px, py, O_SPACE); + + gd_sound_play(cave, GD_S_TELEPORTER, O_TELEPORTER, tx, ty); + + return TRUE; /* return true as teleporter worked */ + } + } + /* loop until we get back to original coordinates */ + while (tx != px || ty != py); + + /* return false as we did not find any usable teleporter */ + return FALSE; +} + +/* + try to push an element. + returns true if the push is possible; also does move the specified _element_. + up to the caller to move the _player_itself_. +*/ +static boolean do_push(GdCave *cave, int x, int y, GdDirection player_move, boolean player_fire) +{ + boolean result; + GdElement what = get_dir(cave, x, y, player_move); + + /* gravity for falling wall, bladder, ... */ + GdDirection grav_compat = cave->gravity_affects_all ? cave->gravity : GD_MV_DOWN; + + result = FALSE; + + switch (what) + { + case O_WAITING_STONE: + case O_STONE: + case O_NITRO_PACK: + case O_CHASING_STONE: + case O_MEGA_STONE: + case O_FLYING_STONE: + case O_NUT: + /* pushing some kind of stone or nut */ + /* directions possible: 90degrees cw or ccw to current gravity. */ + /* only push if player dir is orthogonal to gravity, + ie. gravity down, pushing left & right possible */ + if (player_move == ccw_fourth[cave->gravity] || + player_move == cw_fourth[cave->gravity]) + { + int prob; + + prob = 0; + + /* different probabilities for different elements. */ + switch (what) + { + case O_WAITING_STONE: + /* waiting stones are light, can always push */ + prob = 1000000; + break; + + case O_CHASING_STONE: + /* chasing can be pushed if player is turbo */ + if (cave->sweet_eaten) + prob = 1000000; + break; + + case O_MEGA_STONE: + /* mega may(!) be pushed if player is turbo */ + if (cave->mega_stones_pushable_with_sweet && cave->sweet_eaten) + prob = 1000000; + break; + + case O_STONE: + case O_NUT: + case O_FLYING_STONE: + case O_NITRO_PACK: + if (cave->sweet_eaten) + prob = cave->pushing_stone_prob_sweet; /* probability with sweet */ + else + prob = cave->pushing_stone_prob; /* probability without sweet. */ + break; + + default: + break; + } + + if (is_space_dir(cave, x, y, GD_MV_TWICE + player_move) && + g_rand_int_range(cave->random, 0, 1000000) < prob) + { + /* if decided that he will be able to push, */ + store_dir(cave, x, y, GD_MV_TWICE + player_move, what); + play_sound_of_element(cave, what, x, y); + result = TRUE; + } + } + break; + + case O_BLADDER: + case O_BLADDER_1: + case O_BLADDER_2: + case O_BLADDER_3: + case O_BLADDER_4: + case O_BLADDER_5: + case O_BLADDER_6: + case O_BLADDER_7: + case O_BLADDER_8: + /* pushing a bladder. keep in mind that after pushing, we always get an O_BLADDER, + * not an O_BLADDER_x. */ + /* there is no "delayed" state of a bladder, so we use store_dir_no_scanned! */ + + /* first check: we cannot push a bladder "up" */ + if (player_move != opposite[grav_compat]) + { + /* pushing a bladder "down". p = player, o = bladder, 1, 2, 3 = directions to check. */ + /* player moving in the direction of gravity. */ + /* p p g */ + /* 2o3 | | */ + /* 1 v v */ + if (player_move == grav_compat) + { + /* pushing bladder down */ + if (is_space_dir(cave, x, y, GD_MV_TWICE + player_move)) + store_dir_no_scanned(cave, x, y, GD_MV_TWICE + player_move, O_BLADDER), result = TRUE; + /* if no space to push down, maybe left (down-left to player) */ + else if (is_space_dir(cave, x, y, cw_eighth[grav_compat])) + + /* left is "down, turned right (cw)" */ + store_dir_no_scanned(cave, x, y, cw_eighth[grav_compat], O_BLADDER), result = TRUE; + /* if not, maybe right (down-right to player) */ + else if (is_space_dir(cave, x, y, ccw_eighth[grav_compat])) + store_dir_no_scanned(cave, x, y, ccw_eighth[grav_compat], O_BLADDER), result = TRUE; + } + + /* pushing a bladder "left". p = player, o = bladder, 1, 2, 3 = directions to check. */ + /* 3 g */ + /* 1op <-p | */ + /* 2 v */ + else if (player_move == cw_fourth[grav_compat]) + { + if (is_space_dir(cave, x, y, GD_MV_TWICE + cw_fourth[grav_compat])) /* pushing it left */ + store_dir_no_scanned(cave, x, y, GD_MV_TWICE + cw_fourth[grav_compat], O_BLADDER), result = TRUE; + else if (is_space_dir(cave, x, y, cw_eighth[grav_compat])) /* maybe down, and player will move left */ + store_dir_no_scanned(cave, x, y, cw_eighth[grav_compat], O_BLADDER), result = TRUE; + else if (is_space_dir(cave, x, y, cw_eighth[player_move])) /* maybe up, and player will move left */ + store_dir_no_scanned(cave, x, y, cw_eighth[player_move], O_BLADDER), result = TRUE; + } + + /* pushing a bladder "right". p = player, o = bladder, 1, 2, 3 = directions to check. */ + /* 3 g */ + /* po1 p-< | */ + /* 2 v */ + else if (player_move == ccw_fourth[grav_compat]) + { + if (is_space_dir(cave, x, y, GD_MV_TWICE + player_move)) /* pushing it right */ + store_dir_no_scanned(cave, x, y, GD_MV_TWICE + player_move, O_BLADDER), result = TRUE; + else if (is_space_dir(cave, x, y, ccw_eighth[grav_compat])) /* maybe down, and player will move right */ + store_dir_no_scanned(cave, x, y, ccw_eighth[grav_compat], O_BLADDER), result = TRUE; + else if (is_space_dir(cave, x, y, ccw_eighth[player_move])) /* maybe up, and player will move right */ + store_dir_no_scanned(cave, x, y, ccw_eighth[player_move], O_BLADDER), result = TRUE; + } + + if (result) + play_sound_of_element(cave, O_BLADDER, x, y); + } + break; + + case O_BOX: + /* a box is only pushed with the fire pressed */ + if (player_fire) + { + /* but always with 100% probability */ + switch (player_move) + { + case GD_MV_LEFT: + case GD_MV_RIGHT: + case GD_MV_UP: + case GD_MV_DOWN: + /* pushing in some dir, two steps in that dir - is there space? */ + if (is_space_dir(cave, x, y, player_move + GD_MV_TWICE)) + { + /* yes, so push. */ + store_dir(cave, x, y, player_move + GD_MV_TWICE, O_BOX); + result = TRUE; + gd_sound_play(cave, GD_S_BOX_PUSH, what, x, y); + } + break; + + default: + /* push in no other directions possible */ + break; + } + } + break; + + /* pushing of other elements not possible */ + default: + break; + } + + return result; +} + +/* from the key press booleans, create a direction */ +GdDirection gd_direction_from_keypress(boolean up, boolean down, boolean left, boolean right) +{ + GdDirection player_move; + + /* from the key press booleans, create a direction */ + if (up && right) + player_move = GD_MV_UP_RIGHT; + else if (down && right) + player_move = GD_MV_DOWN_RIGHT; + else if (down && left) + player_move = GD_MV_DOWN_LEFT; + else if (up && left) + player_move = GD_MV_UP_LEFT; + else if (up) + player_move = GD_MV_UP; + else if (down) + player_move = GD_MV_DOWN; + else if (left) + player_move = GD_MV_LEFT; + else if (right) + player_move = GD_MV_RIGHT; + else + player_move = GD_MV_STILL; + + return player_move; +} + +/* clear these to no sound; and they will be set during iteration. */ +void gd_cave_clear_sounds(GdCave *cave) +{ + cave->sound1 = GD_S_NONE; + cave->sound2 = GD_S_NONE; + cave->sound3 = GD_S_NONE; +} + +static void do_start_fall(GdCave *cave, int x, int y, GdDirection falling_direction, + GdElement falling_element) +{ + if (cave->gravity_disabled) + return; + + if (is_space_dir(cave, x, y, falling_direction)) + { + /* beginning to fall */ + play_sound_of_element(cave, get(cave, x, y), x, y); + move(cave, x, y, falling_direction, falling_element); + } + + /* check if it is on a sloped element, and it can roll. */ + /* for example, sloped wall looks like: */ + /* /| */ + /* /_| */ + /* this is tagged as sloped up&left. */ + /* first check if the stone or diamond is coming from "up" (ie. opposite of gravity) */ + /* then check the direction to roll (left or right) */ + /* this way, gravity can also be pointing right, and the above slope will work as one would expect */ + else if (sloped_dir(cave, x, y, falling_direction, opposite[falling_direction])) + { + /* rolling down, if sitting on a sloped object */ + if (sloped_dir(cave, x, y, falling_direction, cw_fourth[falling_direction]) && + is_space_dir(cave, x, y, cw_fourth[falling_direction]) && + is_space_dir(cave, x, y, cw_eighth[falling_direction])) + { + /* rolling left? - keep in mind that ccw_fourth rotates gravity ccw, + so here we use cw_fourth */ + play_sound_of_element(cave, get(cave, x, y), x, y); + move(cave, x, y, cw_fourth[falling_direction], falling_element); + } + else if (sloped_dir(cave, x, y, falling_direction, ccw_fourth[falling_direction]) && + is_space_dir(cave, x, y, ccw_fourth[falling_direction]) && + is_space_dir(cave, x, y, ccw_eighth[falling_direction])) + { + /* rolling right? */ + play_sound_of_element(cave, get(cave, x, y), x, y); + move(cave, x, y, ccw_fourth[falling_direction], falling_element); + } + } +} + +static boolean do_fall_try_crush_voodoo(GdCave *cave, int x, int y, GdDirection fall_dir) +{ + if (get_dir(cave, x, y, fall_dir) == O_VOODOO && + cave->voodoo_dies_by_stone) + { + /* this is a 1stB-style vodo. explodes by stone, collects diamonds */ + explode_dir(cave, x, y, fall_dir); + return TRUE; + } + else + return FALSE; +} + +static boolean do_fall_try_eat_voodoo(GdCave *cave, int x, int y, GdDirection fall_dir) +{ + if (get_dir(cave, x, y, fall_dir) == O_VOODOO && + cave->voodoo_collects_diamonds) + { + /* this is a 1stB-style voodoo. explodes by stone, collects diamonds */ + player_get_element(cave, O_DIAMOND, x, y); /* as if player got diamond */ + store(cave, x, y, O_SPACE); /* diamond disappears */ + return TRUE; + } + else + return FALSE; +} + +static boolean do_fall_try_crack_nut(GdCave *cave, int x, int y, + GdDirection fall_dir, GdElement bouncing) +{ + if (get_dir(cave, x, y, fall_dir) == O_NUT || + get_dir(cave, x, y, fall_dir) == O_NUT_F) + { + /* stones */ + store(cave, x, y, bouncing); + store_dir(cave, x, y, fall_dir, cave->nut_turns_to_when_crushed); + + if (cave->nut_sound) + gd_sound_play(cave, GD_S_NUT_CRACK, O_NUT, x, y); + + return TRUE; + } + else + return FALSE; +} + +static boolean do_fall_try_magic(GdCave *cave, int x, int y, + GdDirection fall_dir, GdElement magic) +{ + if (get_dir(cave, x, y, fall_dir) == O_MAGIC_WALL) + { + play_sound_of_element(cave, O_DIAMOND, x, y); /* always play diamond sound */ + + if (cave->magic_wall_state==GD_MW_DORMANT) + cave->magic_wall_state = GD_MW_ACTIVE; + + if (cave->magic_wall_state==GD_MW_ACTIVE && + is_space_dir(cave, x, y, GD_MV_TWICE+fall_dir)) + { + /* if magic wall active and place underneath, it turns element + into anything the effect says to do. */ + store_dir(cave, x, y, GD_MV_TWICE+fall_dir, magic); + } + + /* active or non-active or anything, element falling in will always disappear */ + store(cave, x, y, O_SPACE); + + return TRUE; + } + else + return FALSE; +} + +static boolean do_fall_try_crush(GdCave *cave, int x, int y, GdDirection fall_dir) +{ + if (explodes_by_hit_dir(cave, x, y, fall_dir)) + { + explode_dir(cave, x, y, fall_dir); + return TRUE; + } + else + return FALSE; +} + +static boolean do_fall_roll_or_stop(GdCave *cave, int x, int y, + GdDirection fall_dir, GdElement bouncing) +{ + if (is_space_dir(cave, x, y, fall_dir)) + { + /* falling further */ + move(cave, x, y, fall_dir, get(cave, x, y)); + + return TRUE; + } + + /* check if it is on a sloped element, and it can roll. */ + /* for example, sloped wall looks like: */ + /* /| */ + /* /_| */ + /* this is tagged as sloped up&left. */ + /* first check if the stone or diamond is coming from "up" (ie. opposite of gravity) */ + /* then check the direction to roll (left or right) */ + /* this way, gravity can also be pointing right, and the above slope will work as one would expect */ + + if (sloped_dir(cave, x, y, fall_dir, opposite[fall_dir])) + { + /* sloped element, falling to left or right */ + if (sloped_dir(cave, x, y, fall_dir, cw_fourth[fall_dir]) && + is_space_dir(cave, x, y, cw_eighth[fall_dir]) && + is_space_dir(cave, x, y, cw_fourth[fall_dir])) + { + play_sound_of_element(cave, get(cave, x, y), x, y); + + /* try to roll left first - see O_STONE to understand why cw_fourth */ + move(cave, x, y, cw_fourth[fall_dir], get(cave, x, y)); + } + else if (sloped_dir(cave, x, y, fall_dir, ccw_fourth[fall_dir]) && + is_space_dir(cave, x, y, ccw_eighth[fall_dir]) && + is_space_dir(cave, x, y, ccw_fourth[fall_dir])) + { + play_sound_of_element(cave, get(cave, x, y), x, y); + + /* if not, try to roll right */ + move(cave, x, y, ccw_fourth[fall_dir], get(cave, x, y)); + } + else + { + /* cannot roll in any direction, so it stops */ + play_sound_of_element(cave, get(cave, x, y), x, y); + store(cave, x, y, bouncing); + } + + return TRUE; + } + + /* any other element, stops */ + play_sound_of_element(cave, get(cave, x, y), x, y); + store(cave, x, y, bouncing); + return TRUE; +} + +static void update_cave_speed(GdCave *cave) +{ + /* update timing calculated by iterating and counting elements which were slow to process on c64 */ + switch (cave->scheduling) + { + case GD_SCHEDULING_MILLISECONDS: + /* cave->speed already contains the milliseconds value, do not touch it */ + break; + + case GD_SCHEDULING_BD1: + if (!cave->intermission) + /* non-intermissions */ + cave->speed = (88 + 3.66 * cave->c64_timing + (cave->ckdelay + cave->ckdelay_extra_for_animation) / 1000); + else + /* intermissions were quicker, as only lines 1-12 were processed by the engine. */ + cave->speed = (60 + 3.66 * cave->c64_timing + (cave->ckdelay + cave->ckdelay_extra_for_animation) / 1000); + break; + + case GD_SCHEDULING_BD1_ATARI: + /* about 20ms/frame faster than c64 version */ + if (!cave->intermission) + cave->speed = (74 + 3.2 * cave->c64_timing + (cave->ckdelay) / 1000); /* non-intermissions */ + else + cave->speed = (65 + 2.88 * cave->c64_timing + (cave->ckdelay) / 1000); /* for intermissions */ + break; + + case GD_SCHEDULING_BD2: + /* 60 is a guess. */ + cave->speed = MAX(60 + (cave->ckdelay + cave->ckdelay_extra_for_animation)/1000, cave->c64_timing * 20); + break; + + case GD_SCHEDULING_PLCK: + /* 65 is totally empty cave in construction kit, with delay = 0) */ + cave->speed = MAX(65 + cave->ckdelay / 1000, cave->c64_timing * 20); + break; + + case GD_SCHEDULING_BD2_PLCK_ATARI: + /* a really fast engine; timing works like c64 plck. */ + /* 40 ms was measured in the construction kit, with delay = 0 */ + cave->speed = MAX(40 + cave->ckdelay / 1000, cave->c64_timing * 20); + break; + + case GD_SCHEDULING_CRDR: + if (cave->hammered_walls_reappear) /* this made the engine very slow. */ + cave->ckdelay += 60000; + cave->speed = MAX(130 + cave->ckdelay / 1000, cave->c64_timing * 20); + break; + + case GD_SCHEDULING_MAX: + break; + } +} + +/* process a cave. */ +void gd_cave_iterate(GdCave *cave, GdDirection player_move, boolean player_fire, boolean suicide) +{ + int x, y, i; + + /* for border scan */ + int ymin, ymax; + + /* amoeba found to be enclosed. if not, this is cleared */ + boolean amoeba_found_enclosed, amoeba_2_found_enclosed; + + /* counting the number of amoebas. after scan, check if too much */ + int amoeba_count, amoeba_2_count; + + /* cave scan found water - for sound */ + boolean found_water; + + boolean inbox_toggle; + boolean start_signal; + + /* gravity for falling wall, bladder, ... */ + GdDirection grav_compat = cave->gravity_affects_all ? cave->gravity : GD_MV_DOWN; + + /* directions for o_something_1, 2, 3 and 4 (creatures) */ + static const GdDirection creature_dir[] = + { + GD_MV_LEFT, + GD_MV_UP, + GD_MV_RIGHT, + GD_MV_DOWN + }; + static const GdDirection creature_chdir[] = + { + GD_MV_RIGHT, + GD_MV_DOWN, + GD_MV_LEFT, + GD_MV_UP + }; + int time_decrement_sec; + + /* biters eating elements preference, they try to go in this order */ + GdElement biter_try[] = + { + O_DIRT, + cave->biter_eat, + O_SPACE, O_STONE + }; + + boolean amoeba_sound, magic_sound; + + gd_cave_clear_sounds(cave); + + /* if diagonal movements not allowed, */ + /* horizontal movements have precedence. [BROADRIBB] */ + if (!cave->diagonal_movements) + { + switch (player_move) + { + case GD_MV_UP_RIGHT: + case GD_MV_DOWN_RIGHT: + player_move = GD_MV_RIGHT; + break; + + case GD_MV_UP_LEFT: + case GD_MV_DOWN_LEFT: + player_move = GD_MV_LEFT; + break; + + default: + /* no correction needed */ + break; + } + } + + /* set cave get function; to implement perfect or lineshifting borders */ + if (cave->lineshift) + cave->getp = getp_shift; + else + cave->getp = getp_perfect; + + /* increment this. if the scan routine comes across player, clears it (sets to zero). */ + if (cave->player_seen_ago < 100) + cave->player_seen_ago++; + + if (cave->pneumatic_hammer_active_delay > 0) + cave->pneumatic_hammer_active_delay--; + + /* inboxes and outboxes flash with the rhythm of the game, not the display. + * also, a player can be born only from an open, not from a steel-wall-like inbox. */ + cave->inbox_flash_toggle = !cave->inbox_flash_toggle; + inbox_toggle = cave->inbox_flash_toggle; + + if (cave->gate_open_flash > 0) + cave->gate_open_flash--; + + /* score collected this frame */ + cave->score = 0; + + /* suicide only kills the active player */ + /* player_x, player_y was set by the previous iterate routine, or the cave setup. */ + /* we must check if there is a player or not - he may have exploded or something like that */ + if (suicide && cave->player_state == GD_PL_LIVING && + is_player(cave, cave->player_x, cave->player_y)) + store(cave, cave->player_x, cave->player_y, O_EXPLODE_1); + + /* check for walls reappearing */ + if (cave->hammered_reappear) + { + for (y = 0; y < cave->h; y++) + { + for (x = 0; x < cave->w; x++) + { + /* timer for the cell > 0? */ + if (cave->hammered_reappear[y][x]>0) + { + /* decrease timer */ + cave->hammered_reappear[y][x]--; + + /* check if it became zero */ + if (cave->hammered_reappear[y][x] == 0) + { + store(cave, x, y, O_BRICK); + gd_sound_play(cave, GD_S_WALL_REAPPEAR, O_BRICK, x, y); + } + } + } + } + } + + /* variables to check during the scan */ + + /* will be set to false if any of the amoeba is found free. */ + amoeba_found_enclosed = TRUE; + amoeba_2_found_enclosed = TRUE; + amoeba_count = 0; + amoeba_2_count = 0; + found_water = FALSE; + cave->ckdelay = 0; + time_decrement_sec = 0; + + /* check whether to scan the first and last line */ + if (cave->border_scan_first_and_last) + { + ymin = 0; + ymax = cave->h - 1; + } + else + { + ymin = 1; + ymax = cave->h - 2; + } + + /* the cave scan routine */ + for (y = ymin; y <= ymax; y++) + { + for (x = 0; x < cave->w; x++) + { + /* if we find a scanned element, change it to the normal one, and that's all. */ + /* this is required, for example for chasing stones, which have moved, always passing slime! */ + if (get(cave, x, y)&SCANNED) + { + store(cave, x, y, get(cave, x, y) & ~SCANNED); + + continue; + } + + /* add the ckdelay correction value for every element seen. */ + cave->ckdelay += gd_elements[get(cave, x, y)].ckdelay; + + switch (get(cave, x, y)) + { + /* + * P L A Y E R S + */ + case O_PLAYER: + if (cave->kill_player) + { + explode (cave, x, y); + break; + } + + cave->player_seen_ago = 0; + /* bd4 intermission caves have many players. so if one of them has exited, + * do not change the flag anymore. so this if () is needed */ + if (cave->player_state!=GD_PL_EXITED) + cave->player_state = GD_PL_LIVING; + + /* check for pneumatic hammer things */ + /* 1) press fire, 2) have pneumatic hammer 4) space on left or right + for hammer 5) stand on something */ + if (player_fire && cave->got_pneumatic_hammer && + is_space_dir(cave, x, y, player_move) && + !is_space_dir(cave, x, y, GD_MV_DOWN)) + { + if (player_move == GD_MV_LEFT && + can_be_hammered_dir(cave, x, y, GD_MV_DOWN_LEFT)) + { + cave->pneumatic_hammer_active_delay = cave->pneumatic_hammer_frame; + store_dir(cave, x, y, GD_MV_LEFT, O_PNEUMATIC_ACTIVE_LEFT); + store(cave, x, y, O_PLAYER_PNEUMATIC_LEFT); + break; /* finished. */ + } + + if (player_move == GD_MV_RIGHT && + can_be_hammered_dir(cave, x, y, GD_MV_DOWN_RIGHT)) + { + cave->pneumatic_hammer_active_delay = cave->pneumatic_hammer_frame; + store_dir(cave, x, y, GD_MV_RIGHT, O_PNEUMATIC_ACTIVE_RIGHT); + store(cave, x, y, O_PLAYER_PNEUMATIC_RIGHT); + break; /* finished. */ + } + } + + if (player_move != GD_MV_STILL) + { + /* only do every check if he is not moving */ + GdElement what = get_dir(cave, x, y, player_move); + GdElement remains = what; + boolean push; + + /* if we are 'eating' a teleporter, and the function returns true + (teleporting worked), break here */ + if (what == O_TELEPORTER && do_teleporter(cave, x, y, player_move)) + break; + + /* try to push element; if successful, break */ + push = do_push(cave, x, y, player_move, player_fire); + if (push) + remains = O_SPACE; + else + switch (what) + { + case O_BOMB: + /* if its a bomb, remember he now has one. */ + /* we do not change the "remains" and "what" variables, + so that part of the code will be ineffective */ + gd_sound_play(cave, GD_S_BOMB_COLLECT, what, x, y); + store_dir(cave, x, y, player_move, O_SPACE); + + if (player_fire) + store(cave, x, y, O_PLAYER_BOMB); + else + move(cave, x, y, player_move, O_PLAYER_BOMB); + break; + + case O_POT: + /* we do not change the "remains" and "what" variables, + so that part of the code will be ineffective */ + if (!player_fire && !cave->gravity_switch_active && + cave->skeletons_collected >= cave->skeletons_needed_for_pot) + { + cave->skeletons_collected -= cave->skeletons_needed_for_pot; + move(cave, x, y, player_move, O_PLAYER_STIRRING); + cave->gravity_disabled = TRUE; + } + break; + + case O_GRAVITY_SWITCH: + /* (we cannot use player_get for this as it does not have player_move parameter) */ + /* only allow changing direction if the new dir is not diagonal */ + if (cave->gravity_switch_active && + (player_move == GD_MV_LEFT || + player_move == GD_MV_RIGHT || + player_move == GD_MV_UP || + player_move == GD_MV_DOWN)) + { + gd_sound_play(cave, GD_S_SWITCH_GRAVITY, what, x, y); + cave->gravity_will_change = + cave->gravity_change_time * cave->timing_factor; + cave->gravity_next_direction = player_move; + cave->gravity_switch_active = FALSE; + } + break; + + default: + /* get element - process others. + if cannot get, player_get_element will return the same */ + remains = player_get_element (cave, what, x, y); + break; + } + + if (remains != what || remains == O_SPACE) + { + /* if anything changed, apply the change. */ + + /* if snapping anything and we have snapping explosions set. + but these is not true for pushing. */ + if (remains == O_SPACE && player_fire && !push) + remains = cave->snap_element; + + if (remains != O_SPACE || player_fire) + /* if any other element than space, player cannot move. + also if pressing fire, will not move. */ + store_dir(cave, x, y, player_move, remains); + else + /* if space remains there, the player moves. */ + move(cave, x, y, player_move, O_PLAYER); + } + } + break; + + case O_PLAYER_BOMB: + /* much simpler; cannot steal stones */ + if (cave->kill_player) + { + explode(cave, x, y); + break; + } + + cave->player_seen_ago = 0; + /* bd4 intermission caves have many players. so if one of them has exited, + * do not change the flag anymore. so this if () is needed */ + if (cave->player_state != GD_PL_EXITED) + cave->player_state = GD_PL_LIVING; + + if (player_move != GD_MV_STILL) + { + /* if the player does not move, nothing to do */ + GdElement what = get_dir(cave, x, y, player_move); + GdElement remains = what; + + if (player_fire) + { + /* placing a bomb into empty space or dirt */ + if (is_space_dir(cave, x, y, player_move) || + is_element_dir(cave, x, y, player_move, O_DIRT)) + { + store_dir(cave, x, y, player_move, O_BOMB_TICK_1); + + /* placed bomb, he is normal player again */ + store(cave, x, y, O_PLAYER); + gd_sound_play(cave, GD_S_BOMB_PLACE, O_BOMB, x, y); + } + break; + } + + /* pushing and collecting */ + /* if we are 'eating' a teleporter, and the function returns true + (teleporting worked), break here */ + if (what == O_TELEPORTER && do_teleporter(cave, x, y, player_move)) + break; + + /* player fire is false... */ + if (do_push(cave, x, y, player_move, FALSE)) + remains = O_SPACE; + else + { + switch (what) + { + case O_GRAVITY_SWITCH: + /* (we cannot use player_get for this as it does not have + player_move parameter) */ + /* only allow changing direction if the new dir is not diagonal */ + if (cave->gravity_switch_active && + (player_move==GD_MV_LEFT || + player_move==GD_MV_RIGHT || + player_move==GD_MV_UP || + player_move==GD_MV_DOWN)) + { + gd_sound_play(cave, GD_S_SWITCH_GRAVITY, what, x, y); + cave->gravity_will_change = + cave->gravity_change_time * cave->timing_factor; + cave->gravity_next_direction = player_move; + cave->gravity_switch_active = FALSE; + } + break; + + default: + /* get element. if cannot get, player_get_element will return the same */ + remains = player_get_element (cave, what, x, y); + break; + } + } + + /* if element changed, OR there is space, move. */ + if (remains != what || remains == O_SPACE) + { + /* if anything changed, apply the change. */ + move(cave, x, y, player_move, O_PLAYER_BOMB); + } + } + break; + + case O_PLAYER_STIRRING: + if (cave->kill_player) + { + explode(cave, x, y); + break; + } + + /* stirring sound, if no other walking sound or explosion */ + gd_sound_play(cave, GD_S_STIRRING, O_PLAYER_STIRRING, x, y); + + cave->player_seen_ago = 0; + /* bd4 intermission caves have many players. so if one of them has exited, + * do not change the flag anymore. so this if () is needed */ + if (cave->player_state!=GD_PL_EXITED) + cave->player_state = GD_PL_LIVING; + + if (player_fire) + { + /* player "exits" stirring the pot by pressing fire */ + cave->gravity_disabled = FALSE; + store(cave, x, y, O_PLAYER); + cave->gravity_switch_active = TRUE; + } + break; + + /* player holding pneumatic hammer */ + case O_PLAYER_PNEUMATIC_LEFT: + case O_PLAYER_PNEUMATIC_RIGHT: + /* usual player stuff */ + if (cave->kill_player) + { + explode(cave, x, y); + break; + } + + cave->player_seen_ago = 0; + if (cave->player_state!=GD_PL_EXITED) + cave->player_state = GD_PL_LIVING; + + /* if hammering time is up, becomes a normal player again. */ + if (cave->pneumatic_hammer_active_delay == 0) + store(cave, x, y, O_PLAYER); + break; + + /* the active pneumatic hammer itself */ + case O_PNEUMATIC_ACTIVE_RIGHT: + case O_PNEUMATIC_ACTIVE_LEFT: + if (cave->pneumatic_hammer_active_delay == 0) + { + GdElement new_elem; + + /* pneumatic hammer element disappears */ + store(cave, x, y, O_SPACE); + + /* which is the new element which appears after that one is hammered? */ + new_elem = gd_element_get_hammered(get_dir(cave, x, y, GD_MV_DOWN)); + + /* if there is a new element, display it */ + /* O_NONE might be returned, for example if the element being + hammered explodes during hammering (by a nearby explosion) */ + if (new_elem != O_NONE) + { + store_dir(cave, x, y, GD_MV_DOWN, new_elem); + + /* and if walls reappear, remember it in array */ + if (cave->hammered_walls_reappear) + { + int wall_y; + + wall_y = (y + 1) % cave->h; + cave->hammered_reappear[wall_y][x] = cave->hammered_wall_reappear_frame; + } + } + } + break; + + /* + * S T O N E S, D I A M O N D S + */ + case O_STONE: /* standing stone */ + do_start_fall(cave, x, y, cave->gravity, cave->stone_falling_effect); + break; + + case O_MEGA_STONE: /* standing mega_stone */ + do_start_fall(cave, x, y, cave->gravity, O_MEGA_STONE_F); + break; + + case O_DIAMOND: /* standing diamond */ + do_start_fall(cave, x, y, cave->gravity, cave->diamond_falling_effect); + break; + + case O_NUT: /* standing nut */ + do_start_fall(cave, x, y, cave->gravity, O_NUT_F); + break; + + case O_DIRT_BALL: /* standing dirt ball */ + do_start_fall(cave, x, y, cave->gravity, O_DIRT_BALL_F); + break; + + case O_DIRT_LOOSE: /* standing loose dirt */ + do_start_fall(cave, x, y, cave->gravity, O_DIRT_LOOSE_F); + break; + + case O_FLYING_STONE: /* standing stone */ + do_start_fall(cave, x, y, opposite[cave->gravity], O_FLYING_STONE_F); + break; + + case O_FLYING_DIAMOND: /* standing diamond */ + do_start_fall(cave, x, y, opposite[cave->gravity], O_FLYING_DIAMOND_F); + break; + + /* + * F A L L I N G E L E M E N T S, F L Y I N G S T O N E S, D I A M O N D S + */ + case O_DIRT_BALL_F: /* falling dirt ball */ + if (!cave->gravity_disabled) + do_fall_roll_or_stop(cave, x, y, cave->gravity, O_DIRT_BALL); + break; + + case O_DIRT_LOOSE_F: /* falling loose dirt */ + if (!cave->gravity_disabled) + do_fall_roll_or_stop(cave, x, y, cave->gravity, O_DIRT_LOOSE); + break; + + case O_STONE_F: /* falling stone */ + if (!cave->gravity_disabled) + { + if (do_fall_try_crush_voodoo(cave, x, y, cave->gravity)) + break; + + if (do_fall_try_crack_nut(cave, x, y, cave->gravity, cave->stone_bouncing_effect)) + break; + + if (do_fall_try_magic(cave, x, y, cave->gravity, cave->magic_stone_to)) + break; + + if (do_fall_try_crush(cave, x, y, cave->gravity)) + break; + + do_fall_roll_or_stop(cave, x, y, cave->gravity, cave->stone_bouncing_effect); + } + break; + + case O_MEGA_STONE_F: /* falling mega */ + if (!cave->gravity_disabled) + { + if (do_fall_try_crush_voodoo(cave, x, y, cave->gravity)) + break; + + if (do_fall_try_crack_nut(cave, x, y, cave->gravity, O_MEGA_STONE)) + break; + + if (do_fall_try_magic(cave, x, y, cave->gravity, cave->magic_mega_stone_to)) + break; + + if (do_fall_try_crush(cave, x, y, cave->gravity)) + break; + + do_fall_roll_or_stop(cave, x, y, cave->gravity, O_MEGA_STONE); + } + break; + + case O_DIAMOND_F: /* falling diamond */ + if (!cave->gravity_disabled) + { + if (do_fall_try_eat_voodoo(cave, x, y, cave->gravity)) + break; + + if (do_fall_try_magic(cave, x, y, cave->gravity, cave->magic_diamond_to)) + break; + + if (do_fall_try_crush(cave, x, y, cave->gravity)) + break; + + do_fall_roll_or_stop(cave, x, y, cave->gravity, cave->diamond_bouncing_effect); + } + break; + + case O_NUT_F: /* falling nut */ + if (!cave->gravity_disabled) + { + if (do_fall_try_magic(cave, x, y, cave->gravity, cave->magic_nut_to)) + break; + + if (do_fall_try_crush(cave, x, y, cave->gravity)) + break; + + do_fall_roll_or_stop(cave, x, y, cave->gravity, O_NUT); + } + break; + + case O_FLYING_STONE_F: /* falling stone */ + if (!cave->gravity_disabled) + { + GdDirection fall_dir = opposite[cave->gravity]; + + if (do_fall_try_crush_voodoo(cave, x, y, fall_dir)) + break; + + if (do_fall_try_crack_nut(cave, x, y, fall_dir, O_FLYING_STONE)) + break; + + if (do_fall_try_magic(cave, x, y, fall_dir, cave->magic_flying_stone_to)) + break; + + if (do_fall_try_crush(cave, x, y, fall_dir)) + break; + + do_fall_roll_or_stop(cave, x, y, fall_dir, O_FLYING_STONE); + } + break; + + case O_FLYING_DIAMOND_F: /* falling diamond */ + if (!cave->gravity_disabled) + { + GdDirection fall_dir = opposite[cave->gravity]; + + if (do_fall_try_eat_voodoo(cave, x, y, fall_dir)) + break; + + if (do_fall_try_magic(cave, x, y, fall_dir, cave->magic_flying_diamond_to)) + break; + + if (do_fall_try_crush(cave, x, y, fall_dir)) + break; + + do_fall_roll_or_stop(cave, x, y, fall_dir, O_FLYING_DIAMOND); + } + break; + + /* + * N I T R O P A C K + */ + case O_NITRO_PACK: /* standing nitro pack */ + do_start_fall(cave, x, y, cave->gravity, O_NITRO_PACK_F); + break; + + case O_NITRO_PACK_F: /* falling nitro pack */ + if (!cave->gravity_disabled) + { + if (is_space_dir(cave, x, y, cave->gravity)) /* if space, falling further */ + move(cave, x, y, cave->gravity, get(cave, x, y)); + else if (do_fall_try_magic(cave, x, y, cave->gravity, cave->magic_nitro_pack_to)) + { + /* try magic wall; if true, function did the work */ + } + else if (is_element_dir(cave, x, y, cave->gravity, O_DIRT)) + { + /* falling on a dirt, it does NOT explode - just stops at its place. */ + play_sound_of_element(cave, O_NITRO_PACK, x, y); + store(cave, x, y, O_NITRO_PACK); + } + else + /* falling on any other element it explodes */ + explode(cave, x, y); + } + break; + + case O_NITRO_PACK_EXPLODE: /* a triggered nitro pack */ + explode(cave, x, y); + break; + + /* + * C R E A T U R E S + */ + + case O_COW_1: + case O_COW_2: + case O_COW_3: + case O_COW_4: + /* if cannot move in any direction, becomes an enclosed cow */ + if (!is_space_dir(cave, x, y, GD_MV_UP) && !is_space_dir(cave, x, y, GD_MV_DOWN) && + !is_space_dir(cave, x, y, GD_MV_LEFT) && !is_space_dir(cave, x, y, GD_MV_RIGHT)) + store(cave, x, y, O_COW_ENCLOSED_1); + else + { + /* THIS IS THE CREATURE MOVE thing copied. */ + const GdDirection *creature_move; + boolean ccw = rotates_ccw(cave, x, y); /* check if default is counterclockwise */ + GdElement base; /* base element number (which is like O_***_1) */ + int dir, dirn, dirp; /* direction */ + + base = O_COW_1; + + dir = get(cave, x, y)-base; /* facing where */ + creature_move = cave->creatures_backwards ? creature_chdir : creature_dir; + + /* now change direction if backwards */ + if (cave->creatures_backwards) + ccw = !ccw; + + if (ccw) + { + dirn = (dir + 3) & 3; /* fast turn */ + dirp = (dir + 1) & 3; /* slow turn */ + } + else + { + dirn = (dir + 1) & 3; /* fast turn */ + dirp = (dir + 3) & 3; /* slow turn */ + } + + if (is_space_dir(cave, x, y, creature_move[dirn])) + move(cave, x, y, creature_move[dirn], base + dirn); /* turn and move to preferred dir */ + else if (is_space_dir(cave, x, y, creature_move[dir])) + move(cave, x, y, creature_move[dir], base + dir); /* go on */ + else + store(cave, x, y, base + dirp); /* turn in place if nothing else possible */ + } + break; + + /* enclosed cows wait some time before turning to a skeleton */ + case O_COW_ENCLOSED_1: + case O_COW_ENCLOSED_2: + case O_COW_ENCLOSED_3: + case O_COW_ENCLOSED_4: + case O_COW_ENCLOSED_5: + case O_COW_ENCLOSED_6: + if (is_space_dir(cave, x, y, GD_MV_UP) || + is_space_dir(cave, x, y, GD_MV_LEFT) || + is_space_dir(cave, x, y, GD_MV_RIGHT) || + is_space_dir(cave, x, y, GD_MV_DOWN)) + store(cave, x, y, O_COW_1); + else + next(cave, x, y); + break; + + case O_COW_ENCLOSED_7: + if (is_space_dir(cave, x, y, GD_MV_UP) || + is_space_dir(cave, x, y, GD_MV_LEFT) || + is_space_dir(cave, x, y, GD_MV_RIGHT) || + is_space_dir(cave, x, y, GD_MV_DOWN)) + store(cave, x, y, O_COW_1); + else + store(cave, x, y, O_SKELETON); + break; + + case O_FIREFLY_1: + case O_FIREFLY_2: + case O_FIREFLY_3: + case O_FIREFLY_4: + case O_ALT_FIREFLY_1: + case O_ALT_FIREFLY_2: + case O_ALT_FIREFLY_3: + case O_ALT_FIREFLY_4: + case O_BUTTER_1: + case O_BUTTER_2: + case O_BUTTER_3: + case O_BUTTER_4: + case O_ALT_BUTTER_1: + case O_ALT_BUTTER_2: + case O_ALT_BUTTER_3: + case O_ALT_BUTTER_4: + case O_STONEFLY_1: + case O_STONEFLY_2: + case O_STONEFLY_3: + case O_STONEFLY_4: + /* check if touches a voodoo */ + if (get_dir(cave, x, y, GD_MV_LEFT) == O_VOODOO || + get_dir(cave, x, y, GD_MV_RIGHT) == O_VOODOO || + get_dir(cave, x, y, GD_MV_UP) == O_VOODOO || + get_dir(cave, x, y, GD_MV_DOWN) == O_VOODOO) + cave->voodoo_touched = TRUE; + + /* check if touches something bad and should explode (includes voodoo by the flags) */ + if (blows_up_flies_dir(cave, x, y, GD_MV_DOWN) || + blows_up_flies_dir(cave, x, y, GD_MV_UP) || + blows_up_flies_dir(cave, x, y, GD_MV_LEFT) || + blows_up_flies_dir(cave, x, y, GD_MV_RIGHT)) + explode (cave, x, y); + /* otherwise move */ + else + { + const GdDirection *creature_move; + boolean ccw = rotates_ccw(cave, x, y); /* check if default is counterclockwise */ + GdElement base; /* base element number (which is like O_***_1) */ + int dir, dirn, dirp; /* direction */ + + if (get(cave, x, y) >= O_FIREFLY_1 && + get(cave, x, y) <= O_FIREFLY_4) + base = O_FIREFLY_1; + else if (get(cave, x, y) >= O_BUTTER_1 && + get(cave, x, y) <= O_BUTTER_4) + base = O_BUTTER_1; + else if (get(cave, x, y) >= O_STONEFLY_1 && + get(cave, x, y) <= O_STONEFLY_4) + base = O_STONEFLY_1; + else if (get(cave, x, y) >= O_ALT_FIREFLY_1 && + get(cave, x, y) <= O_ALT_FIREFLY_4) + base = O_ALT_FIREFLY_1; + else if (get(cave, x, y) >= O_ALT_BUTTER_1 && + get(cave, x, y) <= O_ALT_BUTTER_4) + base = O_ALT_BUTTER_1; + + dir = get(cave, x, y)-base; /* facing where */ + creature_move = cave->creatures_backwards ? creature_chdir : creature_dir; + + /* now change direction if backwards */ + if (cave->creatures_backwards) + ccw = !ccw; + + if (ccw) + { + dirn = (dir + 3) & 3; /* fast turn */ + dirp = (dir + 1) & 3; /* slow turn */ + } + else + { + dirn = (dir + 1) & 3; /* fast turn */ + dirp = (dir + 3) & 3; /* slow turn */ + } + + if (is_space_dir(cave, x, y, creature_move[dirn])) + move(cave, x, y, creature_move[dirn], base + dirn); /* turn and move to preferred dir */ + else if (is_space_dir(cave, x, y, creature_move[dir])) + move(cave, x, y, creature_move[dir], base + dir); /* go on */ + else + store(cave, x, y, base + dirp); /* turn in place if nothing else possible */ + } + break; + + case O_WAITING_STONE: + if (is_space_dir(cave, x, y, grav_compat)) + { + /* beginning to fall */ + /* it wakes up. */ + move(cave, x, y, grav_compat, O_CHASING_STONE); + } + else if (sloped_dir(cave, x, y, grav_compat, opposite[grav_compat])) + { + /* rolling down a brick wall or a stone */ + if (sloped_dir(cave, x, y, grav_compat, cw_fourth[grav_compat]) && + is_space_dir(cave, x, y, cw_fourth[grav_compat]) && + is_space_dir(cave, x, y, cw_eighth[grav_compat])) + { + /* maybe rolling left - see case O_STONE to understand why we use cw_fourth here */ + move(cave, x, y, cw_fourth[grav_compat], O_WAITING_STONE); + } + else if (sloped_dir(cave, x, y, grav_compat, ccw_fourth[grav_compat]) && + is_space_dir(cave, x, y, ccw_fourth[grav_compat]) && + is_space_dir(cave, x, y, ccw_eighth[grav_compat])) + { + /* or maybe right */ + move(cave, x, y, ccw_fourth[grav_compat], O_WAITING_STONE); + } + } + break; + + case O_CHASING_STONE: + { + int px = cave->px[0]; + int py = cave->py[0]; + boolean horizontal = g_rand_boolean(cave->random); + boolean dont_move = FALSE; + int i = 3; + + /* try to move... */ + while (1) + { + if (horizontal) + { + /*********************************/ + /* check for a horizontal movement */ + if (px == x) + { + /* if coordinates are the same */ + i -= 1; + horizontal = !horizontal; + + if (i == 2) + continue; + } + else + { + if (px > x && is_space_dir(cave, x, y, GD_MV_RIGHT)) + { + move(cave, x, y, GD_MV_RIGHT, O_CHASING_STONE); + dont_move = TRUE; + break; + } + else if (px < x && is_space_dir(cave, x, y, GD_MV_LEFT)) + { + move(cave, x, y, GD_MV_LEFT, O_CHASING_STONE); + dont_move = TRUE; + break; + } + else + { + i -= 2; + if (i == 1) + { + horizontal = !horizontal; + continue; + } + } + } + } + else + { + /********************************/ + /* check for a vertical movement */ + if (py == y) + { + /* if coordinates are the same */ + i -= 1; + horizontal = !horizontal; + if (i == 2) + continue; + } + else + { + if (py > y && is_space_dir(cave, x, y, GD_MV_DOWN)) + { + move(cave, x, y, GD_MV_DOWN, O_CHASING_STONE); + dont_move = TRUE; + break; + } + else if (py < y && is_space_dir(cave, x, y, GD_MV_UP)) + { + move(cave, x, y, GD_MV_UP, O_CHASING_STONE); + dont_move = TRUE; + break; + } + else + { + i -= 2; + if (i == 1) + { + horizontal = !horizontal; + continue; + } + } + } + } + + if (i != 0) + dont_move = TRUE; + + break; + } + + /* if we should move in both directions, but can not move in any, stop. */ + if (!dont_move) + { + if (horizontal) + { + /* check for horizontal */ + if (x >= px) + { + if (is_space_dir(cave, x, y, GD_MV_UP) && + is_space_dir(cave, x, y, GD_MV_UP_LEFT)) + move(cave, x, y, GD_MV_UP, O_CHASING_STONE); + else if (is_space_dir(cave, x, y, GD_MV_DOWN) && + is_space_dir(cave, x, y, GD_MV_DOWN_LEFT)) + move(cave, x, y, GD_MV_DOWN, O_CHASING_STONE); + } + else + { + if (is_space_dir(cave, x, y, GD_MV_UP) && + is_space_dir(cave, x, y, GD_MV_UP_RIGHT)) + move(cave, x, y, GD_MV_UP, O_CHASING_STONE); + else if (is_space_dir(cave, x, y, GD_MV_DOWN) && + is_space_dir(cave, x, y, GD_MV_DOWN_RIGHT)) + move(cave, x, y, GD_MV_DOWN, O_CHASING_STONE); + } + } + else + { + /* check for vertical */ + if (y >= py) + { + if (is_space_dir(cave, x, y, GD_MV_LEFT) && + is_space_dir(cave, x, y, GD_MV_UP_LEFT)) + move(cave, x, y, GD_MV_LEFT, O_CHASING_STONE); + else if (is_space_dir(cave, x, y, GD_MV_RIGHT) && + is_space_dir(cave, x, y, GD_MV_UP_RIGHT)) + move(cave, x, y, GD_MV_RIGHT, O_CHASING_STONE); + } + else + { + if (is_space_dir(cave, x, y, GD_MV_LEFT) && + is_space_dir(cave, x, y, GD_MV_DOWN_LEFT)) + move(cave, x, y, GD_MV_LEFT, O_CHASING_STONE); + else if (is_space_dir(cave, x, y, GD_MV_RIGHT) && + is_space_dir(cave, x, y, GD_MV_DOWN_RIGHT)) + move(cave, x, y, GD_MV_RIGHT, O_CHASING_STONE); + } + } + } + } + break; + + case O_REPLICATOR: + if (cave->replicators_wait_frame == 0 && + cave->replicators_active && + !cave->gravity_disabled) + { + /* only replicate, if space is under it. */ + /* do not replicate players! */ + /* also obeys gravity settings. */ + /* only replicate element if it is not a scanned one */ + /* do not replicate space... that condition looks like it + makes no sense, but otherwise it generates SCANNED spaces, + which cannot be "collected" by the player, so he cannot run + under a replicator */ + if (is_space_dir(cave, x, y, cave->gravity) && + !is_player_dir(cave, x, y, opposite[cave->gravity]) && + !is_space_dir(cave, x, y, opposite[cave->gravity])) + { + store_dir(cave, x, y, cave->gravity, get_dir(cave, x, y, opposite[cave->gravity])); + gd_sound_play(cave, GD_S_REPLICATOR, O_REPLICATOR, x, y); + } + } + break; + + case O_BITER_1: + case O_BITER_2: + case O_BITER_3: + case O_BITER_4: + if (cave->biters_wait_frame == 0) + { + static GdDirection biter_move[] = + { + GD_MV_UP, + GD_MV_RIGHT, + GD_MV_DOWN, + GD_MV_LEFT + }; + + /* direction, last two bits 0..3 */ + int dir = get(cave, x, y) - O_BITER_1; + int dirn = (dir + 3) & 3; + int dirp = (dir + 1) & 3; + int i; + GdElement made_sound_of = O_NONE; + + for (i = 0; i < G_N_ELEMENTS (biter_try); i++) + { + if (is_element_dir(cave, x, y, biter_move[dir], biter_try[i])) + { + move(cave, x, y, biter_move[dir], O_BITER_1 + dir); + if (biter_try[i] != O_SPACE) + made_sound_of = O_BITER_1; /* sound of a biter eating */ + break; + } + else if (is_element_dir(cave, x, y, biter_move[dirn], biter_try[i])) + { + move(cave, x, y, biter_move[dirn], O_BITER_1 + dirn); + if (biter_try[i] != O_SPACE) + made_sound_of = O_BITER_1; /* sound of a biter eating */ + break; + } + else if (is_element_dir(cave, x, y, biter_move[dirp], biter_try[i])) + { + move(cave, x, y, biter_move[dirp], O_BITER_1 + dirp); + if (biter_try[i] != O_SPACE) + made_sound_of = O_BITER_1; /* sound of a biter eating */ + break; + } + } + + if (i == G_N_ELEMENTS(biter_try)) + /* i = number of elements in array: could not move, so just turn */ + store(cave, x, y, O_BITER_1 + dirp); + else if (biter_try[i] == O_STONE) + { + /* if there was a stone there, where we moved... + do not eat stones, just throw them back */ + store(cave, x, y, O_STONE); + made_sound_of = O_STONE; + } + + /* if biter did move, we had sound. play it. */ + if (made_sound_of!=O_NONE) + play_sound_of_element(cave, made_sound_of, x, y); + } + break; + + case O_DRAGONFLY_1: + case O_DRAGONFLY_2: + case O_DRAGONFLY_3: + case O_DRAGONFLY_4: + /* check if touches a voodoo */ + if (get_dir(cave, x, y, GD_MV_LEFT) == O_VOODOO || + get_dir(cave, x, y, GD_MV_RIGHT) == O_VOODOO || + get_dir(cave, x, y, GD_MV_UP) == O_VOODOO || + get_dir(cave, x, y, GD_MV_DOWN) == O_VOODOO) + cave->voodoo_touched = TRUE; + + /* check if touches something bad and should explode (includes voodoo by the flags) */ + if (blows_up_flies_dir(cave, x, y, GD_MV_DOWN) || + blows_up_flies_dir(cave, x, y, GD_MV_UP) || + blows_up_flies_dir(cave, x, y, GD_MV_LEFT) || + blows_up_flies_dir(cave, x, y, GD_MV_RIGHT)) + explode (cave, x, y); + /* otherwise move */ + else + { + const GdDirection *creature_move; + boolean ccw = rotates_ccw(cave, x, y); /* check if default is counterclockwise */ + GdElement base = O_DRAGONFLY_1; /* base element number (which is like O_***_1) */ + int dir, dirn; /* direction */ + + dir = get(cave, x, y)-base; /* facing where */ + creature_move = cave->creatures_backwards ? creature_chdir : creature_dir; + + /* now change direction if backwards */ + if (cave->creatures_backwards) + ccw = !ccw; + + if (ccw) + dirn = (dir + 3) & 3; /* fast turn */ + else + dirn = (dir + 1) & 3; /* fast turn */ + + /* if can move forward, does so. */ + if (is_space_dir(cave, x, y, creature_move[dir])) + move(cave, x, y, creature_move[dir], base + dir); + else + /* otherwise turns 90 degrees in place. */ + store(cave, x, y, base + dirn); + } + break; + + case O_BLADDER: + store(cave, x, y, O_BLADDER_1); + break; + + case O_BLADDER_1: + case O_BLADDER_2: + case O_BLADDER_3: + case O_BLADDER_4: + case O_BLADDER_5: + case O_BLADDER_6: + case O_BLADDER_7: + case O_BLADDER_8: + /* bladder with any delay state: try to convert to clock. */ + if (is_element_dir(cave, x, y, opposite[grav_compat], cave->bladder_converts_by) || + is_element_dir(cave, x, y, cw_fourth[grav_compat], cave->bladder_converts_by) || is_element_dir(cave, x, y, ccw_fourth[grav_compat], cave->bladder_converts_by)) + { + /* if touches the specified element, let it be a clock */ + store(cave, x, y, O_PRE_CLOCK_1); + + /* plays the bladder convert sound */ + play_sound_of_element(cave, O_PRE_CLOCK_1, x, y); + } + else + { + /* is space over the bladder? */ + if (is_space_dir(cave, x, y, opposite[grav_compat])) + { + if (get(cave, x, y)==O_BLADDER_8) + { + /* if it is a bladder 8, really move up */ + move(cave, x, y, opposite[grav_compat], O_BLADDER_1); + play_sound_of_element(cave, O_BLADDER, x, y); + } + else + /* if smaller delay, just increase delay. */ + next(cave, x, y); + } + else + /* if not space, is something sloped over the bladder? */ + if (sloped_for_bladder_dir(cave, x, y, opposite[grav_compat]) && + sloped_dir(cave, x, y, opposite[grav_compat], opposite[grav_compat])) + { + if (sloped_dir(cave, x, y, opposite[grav_compat], ccw_fourth[opposite[grav_compat]]) && + is_space_dir(cave, x, y, ccw_fourth[opposite[grav_compat]]) && + is_space_dir(cave, x, y, ccw_eighth[opposite[grav_compat]])) + { + /* rolling up, to left */ + if (get(cave, x, y) == O_BLADDER_8) + { + /* if it is a bladder 8, really roll */ + move(cave, x, y, ccw_fourth[opposite[grav_compat]], O_BLADDER_8); + play_sound_of_element(cave, O_BLADDER, x, y); + } + else + /* if smaller delay, just increase delay. */ + next(cave, x, y); + } + else if (sloped_dir(cave, x, y, opposite[grav_compat], cw_fourth[opposite[grav_compat]]) && + is_space_dir(cave, x, y, cw_fourth[opposite[grav_compat]]) && + is_space_dir(cave, x, y, cw_eighth[opposite[grav_compat]])) + { + /* rolling up, to left */ + if (get(cave, x, y) == O_BLADDER_8) + { + /* if it is a bladder 8, really roll */ + move(cave, x, y, cw_fourth[opposite[grav_compat]], O_BLADDER_8); + play_sound_of_element(cave, O_BLADDER, x, y); + } + else + /* if smaller delay, just increase delay. */ + next(cave, x, y); + } + } + + /* no space, no sloped thing over it - store bladder 1 and that is for now. */ + else + store(cave, x, y, O_BLADDER_1); + } + break; + + case O_GHOST: + if (blows_up_flies_dir(cave, x, y, GD_MV_DOWN) || + blows_up_flies_dir(cave, x, y, GD_MV_UP) || + blows_up_flies_dir(cave, x, y, GD_MV_LEFT) || + blows_up_flies_dir(cave, x, y, GD_MV_RIGHT)) + explode (cave, x, y); + else + { + int i; + + /* the ghost is given four possibilities to move. */ + for (i = 0; i < 4; i++) + { + static GdDirection dirs[] = + { + GD_MV_UP, + GD_MV_DOWN, + GD_MV_LEFT, + GD_MV_RIGHT + }; + GdDirection random_dir; + + random_dir = dirs[g_rand_int_range(cave->random, 0, G_N_ELEMENTS(dirs))]; + if (is_space_dir(cave, x, y, random_dir)) + { + move(cave, x, y, random_dir, O_GHOST); + break; /* ghost did move -> exit loop */ + } + } + } + break; + + /* + * A C T I V E E L E M E N T S + */ + + case O_AMOEBA: + amoeba_count++; + switch (cave->amoeba_state) + { + case GD_AM_TOO_BIG: + store(cave, x, y, cave->amoeba_too_big_effect); + break; + + case GD_AM_ENCLOSED: + store(cave, x, y, cave->amoeba_enclosed_effect); + break; + + case GD_AM_SLEEPING: + case GD_AM_AWAKE: + /* if no amoeba found during THIS SCAN yet, which was able to grow, check this one. */ + if (amoeba_found_enclosed) + /* if still found enclosed, check all four directions, + if this one is able to grow. */ + if (amoeba_eats_dir(cave, x, y, GD_MV_UP) || + amoeba_eats_dir(cave, x, y, GD_MV_DOWN) || + amoeba_eats_dir(cave, x, y, GD_MV_LEFT) || + amoeba_eats_dir(cave, x, y, GD_MV_RIGHT)) + { + /* not enclosed. this is a local (per scan) flag! */ + amoeba_found_enclosed = FALSE; + cave->amoeba_state = GD_AM_AWAKE; + } + + /* if alive, check in which dir to grow (or not) */ + if (cave->amoeba_state==GD_AM_AWAKE) + { + if (g_rand_int_range(cave->random, 0, 1000000) < cave->amoeba_growth_prob) + { + switch (g_rand_int_range(cave->random, 0, 4)) + { + /* decided to grow, choose a random direction. */ + case 0: /* let this be up. numbers indifferent. */ + if (amoeba_eats_dir(cave, x, y, GD_MV_UP)) + store_dir(cave, x, y, GD_MV_UP, O_AMOEBA); + break; + + case 1: /* down */ + if (amoeba_eats_dir(cave, x, y, GD_MV_DOWN)) + store_dir(cave, x, y, GD_MV_DOWN, O_AMOEBA); + break; + + case 2: /* left */ + if (amoeba_eats_dir(cave, x, y, GD_MV_LEFT)) + store_dir(cave, x, y, GD_MV_LEFT, O_AMOEBA); + break; + + case 3: /* right */ + if (amoeba_eats_dir(cave, x, y, GD_MV_RIGHT)) + store_dir(cave, x, y, GD_MV_RIGHT, O_AMOEBA); + break; + } + } + } + break; + + } + break; + + case O_AMOEBA_2: + amoeba_2_count++; + /* check if it is touching an amoeba, and explosion is enabled */ + if (cave->amoeba_2_explodes_by_amoeba && + (is_element_dir(cave, x, y, GD_MV_DOWN, O_AMOEBA) || + is_element_dir(cave, x, y, GD_MV_UP, O_AMOEBA) || + is_element_dir(cave, x, y, GD_MV_LEFT, O_AMOEBA) || + is_element_dir(cave, x, y, GD_MV_RIGHT, O_AMOEBA))) + explode (cave, x, y); + else + switch (cave->amoeba_2_state) + { + case GD_AM_TOO_BIG: + store(cave, x, y, cave->amoeba_2_too_big_effect); + break; + + case GD_AM_ENCLOSED: + store(cave, x, y, cave->amoeba_2_enclosed_effect); + break; + + case GD_AM_SLEEPING: + case GD_AM_AWAKE: + /* if no amoeba found during THIS SCAN yet, which was able to grow, check this one. */ + if (amoeba_2_found_enclosed) + if (amoeba_eats_dir(cave, x, y, GD_MV_UP) || + amoeba_eats_dir(cave, x, y, GD_MV_DOWN) || + amoeba_eats_dir(cave, x, y, GD_MV_LEFT) || + amoeba_eats_dir(cave, x, y, GD_MV_RIGHT)) + { + /* not enclosed. this is a local (per scan) flag! */ + amoeba_2_found_enclosed = FALSE; + cave->amoeba_2_state = GD_AM_AWAKE; + } + + /* if it is alive, decide if it attempts to grow */ + if (cave->amoeba_2_state == GD_AM_AWAKE) + if (g_rand_int_range(cave->random, 0, 1000000) < cave->amoeba_2_growth_prob) + { + switch (g_rand_int_range(cave->random, 0, 4)) + { + /* decided to grow, choose a random direction. */ + case 0: /* let this be up. numbers indifferent. */ + if (amoeba_eats_dir(cave, x, y, GD_MV_UP)) + store_dir(cave, x, y, GD_MV_UP, O_AMOEBA_2); + break; + + case 1: /* down */ + if (amoeba_eats_dir(cave, x, y, GD_MV_DOWN)) + store_dir(cave, x, y, GD_MV_DOWN, O_AMOEBA_2); + break; + + case 2: /* left */ + if (amoeba_eats_dir(cave, x, y, GD_MV_LEFT)) + store_dir(cave, x, y, GD_MV_LEFT, O_AMOEBA_2); + break; + + case 3: /* right */ + if (amoeba_eats_dir(cave, x, y, GD_MV_RIGHT)) + store_dir(cave, x, y, GD_MV_RIGHT, O_AMOEBA_2); + break; + } + } + break; + + } + break; + + case O_ACID: + /* choose randomly, if it spreads */ + if (g_rand_int_range(cave->random, 0, 1000000) <= cave->acid_spread_ratio) + { + /* the current one explodes */ + store(cave, x, y, cave->acid_turns_to); + + /* and if neighbours are eaten, put acid there. */ + if (is_element_dir(cave, x, y, GD_MV_UP, cave->acid_eats_this)) + { + play_sound_of_element(cave, O_ACID, x, y); + store_dir(cave, x, y, GD_MV_UP, O_ACID); + } + + if (is_element_dir(cave, x, y, GD_MV_DOWN, cave->acid_eats_this)) + { + play_sound_of_element(cave, O_ACID, x, y); + store_dir(cave, x, y, GD_MV_DOWN, O_ACID); + } + + if (is_element_dir(cave, x, y, GD_MV_LEFT, cave->acid_eats_this)) + { + play_sound_of_element(cave, O_ACID, x, y); + store_dir(cave, x, y, GD_MV_LEFT, O_ACID); + } + + if (is_element_dir(cave, x, y, GD_MV_RIGHT, cave->acid_eats_this)) + { + play_sound_of_element(cave, O_ACID, x, y); + store_dir(cave, x, y, GD_MV_RIGHT, O_ACID); + } + } + break; + + case O_WATER: + found_water = TRUE; + if (!cave->water_does_not_flow_down && + is_space_dir(cave, x, y, GD_MV_DOWN)) + /* emulating the odd behaviour in crdr */ + store_dir(cave, x, y, GD_MV_DOWN, O_WATER_1); + + if (is_space_dir(cave, x, y, GD_MV_UP)) + store_dir(cave, x, y, GD_MV_UP, O_WATER_1); + + if (is_space_dir(cave, x, y, GD_MV_LEFT)) + store_dir(cave, x, y, GD_MV_LEFT, O_WATER_1); + + if (is_space_dir(cave, x, y, GD_MV_RIGHT)) + store_dir(cave, x, y, GD_MV_RIGHT, O_WATER_1); + break; + + case O_WATER_16: + store(cave, x, y, O_WATER); + break; + + case O_H_EXPANDING_WALL: + case O_V_EXPANDING_WALL: + case O_H_EXPANDING_STEEL_WALL: + case O_V_EXPANDING_STEEL_WALL: + /* checks first if direction is changed. */ + if (((get(cave, x, y) == O_H_EXPANDING_WALL || + get(cave, x, y) == O_H_EXPANDING_STEEL_WALL) && + !cave->expanding_wall_changed) || + ((get(cave, x, y)==O_V_EXPANDING_WALL || + get(cave, x, y)==O_V_EXPANDING_STEEL_WALL) && + cave->expanding_wall_changed)) + { + if (is_space_dir(cave, x, y, GD_MV_LEFT)) + { + store_dir(cave, x, y, GD_MV_LEFT, get(cave, x, y)); + play_sound_of_element(cave, get(cave, x, y), x, y); + } + + if (is_space_dir(cave, x, y, GD_MV_RIGHT)) { + store_dir(cave, x, y, GD_MV_RIGHT, get(cave, x, y)); + play_sound_of_element(cave, get(cave, x, y), x, y); + } + } + else + { + if (is_space_dir(cave, x, y, GD_MV_UP)) { + store_dir(cave, x, y, GD_MV_UP, get(cave, x, y)); + play_sound_of_element(cave, get(cave, x, y), x, y); + } + + if (is_space_dir(cave, x, y, GD_MV_DOWN)) { + store_dir(cave, x, y, GD_MV_DOWN, get(cave, x, y)); + play_sound_of_element(cave, get(cave, x, y), x, y); + } + } + break; + + case O_EXPANDING_WALL: + case O_EXPANDING_STEEL_WALL: + /* the wall which grows in all four directions. */ + if (is_space_dir(cave, x, y, GD_MV_LEFT)) + { + store_dir(cave, x, y, GD_MV_LEFT, get(cave, x, y)); + play_sound_of_element(cave, get(cave, x, y), x, y); + } + + if (is_space_dir(cave, x, y, GD_MV_RIGHT)) { + store_dir(cave, x, y, GD_MV_RIGHT, get(cave, x, y)); + play_sound_of_element(cave, get(cave, x, y), x, y); + } + + if (is_space_dir(cave, x, y, GD_MV_UP)) { + store_dir(cave, x, y, GD_MV_UP, get(cave, x, y)); + play_sound_of_element(cave, get(cave, x, y), x, y); + } + + if (is_space_dir(cave, x, y, GD_MV_DOWN)) { + store_dir(cave, x, y, GD_MV_DOWN, get(cave, x, y)); + play_sound_of_element(cave, get(cave, x, y), x, y); + } + break; + + case O_SLIME: +#if 1 + ; // to make compilers happy ... +#else + g_print("Step[%03d]", cave->frame); /* XXX */ +#endif + int rrr = gd_cave_c64_random(cave); +#if 1 +#else + g_print(".Rand[%03d].Perm[%03d].Result[%d]\n", rrr, cave->slime_permeability_c64, + (rrr & cave->slime_permeability_c64) == 0); +#endif + /* + * unpredictable: g_rand_int + * predictable: c64 predictable random generator. + * for predictable, a random number is generated, + * whether or not it is even possible that the stone will be able to pass. + */ + if (cave->slime_predictable ? ((rrr /* XXX */ & cave->slime_permeability_c64) == 0) : g_rand_int_range(cave->random, 0, 1000000) < cave->slime_permeability) + { + GdDirection grav = cave->gravity; + GdDirection oppos = opposite[cave->gravity]; + + /* space under the slime? elements may pass from top to bottom then. */ + if (is_space_dir(cave, x, y, grav)) + { + if (get_dir(cave, x, y, oppos) == cave->slime_eats_1) + { + /* output a falling xy under */ + store_dir(cave, x, y, grav, cave->slime_converts_1); + + store_dir(cave, x, y, oppos, O_SPACE); + play_sound_of_element(cave, O_SLIME, x, y); + } + else if (get_dir(cave, x, y, oppos) == cave->slime_eats_2) + { + store_dir(cave, x, y, grav, cave->slime_converts_2); + store_dir(cave, x, y, oppos, O_SPACE); + play_sound_of_element(cave, O_SLIME, x, y); + } + else if (get_dir(cave, x, y, oppos) == cave->slime_eats_3) + { + store_dir(cave, x, y, grav, cave->slime_converts_3); + store_dir(cave, x, y, oppos, O_SPACE); + play_sound_of_element(cave, O_SLIME, x, y); + } + else if (get_dir(cave, x, y, oppos) == O_WAITING_STONE) + { + /* waiting stones pass without awakening */ + store_dir(cave, x, y, grav, O_WAITING_STONE); + store_dir(cave, x, y, oppos, O_SPACE); + play_sound_of_element(cave, O_SLIME, x, y); + } + else if (get_dir(cave, x, y, oppos) == O_CHASING_STONE) + { + /* chasing stones pass */ + store_dir(cave, x, y, grav, O_CHASING_STONE); + store_dir(cave, x, y, oppos, O_SPACE); + play_sound_of_element(cave, O_SLIME, x, y); + } + } + else + /* or space over the slime? elements may pass from bottom to up then. */ + if (is_space_dir(cave, x, y, oppos)) + { + if (get_dir(cave, x, y, grav) == O_BLADDER) + { + /* bladders move UP the slime */ + store_dir(cave, x, y, grav, O_SPACE); + store_dir(cave, x, y, oppos, O_BLADDER_1); + play_sound_of_element(cave, O_SLIME, x, y); + } + else if (get_dir(cave, x, y, grav)==O_FLYING_STONE) + { + store_dir(cave, x, y, grav, O_SPACE); + store_dir(cave, x, y, oppos, O_FLYING_STONE_F); + play_sound_of_element(cave, O_SLIME, x, y); + } + else if (get_dir(cave, x, y, grav)==O_FLYING_DIAMOND) + { + store_dir(cave, x, y, grav, O_SPACE); + store_dir(cave, x, y, oppos, O_FLYING_DIAMOND_F); + play_sound_of_element(cave, O_SLIME, x, y); + } + } + } + break; + + case O_FALLING_WALL: + if (is_space_dir(cave, x, y, grav_compat)) + { + /* try falling if space under. */ + int yy; + + for (yy = y + 1; yy < y + cave->h; yy++) + /* yy < y + cave->h is to check everything OVER the wall - since caves wrap around !! */ + if (get(cave, x, yy) != O_SPACE) + /* stop cycle when other than space */ + break; + + /* if scanning stopped by a player... start falling! */ + if (get(cave, x, yy) == O_PLAYER || + get(cave, x, yy) == O_PLAYER_GLUED || + get(cave, x, yy) == O_PLAYER_BOMB) + { + move(cave, x, y, grav_compat, O_FALLING_WALL_F); + /* no sound when the falling wall starts falling! */ + } + } + break; + + case O_FALLING_WALL_F: + switch (get_dir(cave, x, y, grav_compat)) + { + case O_PLAYER: + case O_PLAYER_GLUED: + case O_PLAYER_BOMB: + /* if player under, it explodes - the falling wall, not the player! */ + explode(cave, x, y); + break; + + case O_SPACE: + /* continue falling */ + move(cave, x, y, grav_compat, O_FALLING_WALL_F); + break; + + default: + /* stop */ + play_sound_of_element(cave, get(cave, x, y), x, y); + store(cave, x, y, O_FALLING_WALL); + break; + } + break; + + /* + * C O N V E Y O R B E L T S + */ + + case O_CONVEYOR_RIGHT: + case O_CONVEYOR_LEFT: + /* only works if gravity is up or down!!! */ + /* first, check for gravity and running belts. */ + if (!cave->gravity_disabled && cave->conveyor_belts_active) + { + const GdDirection *dir; + boolean left; + + /* decide direction */ + left = get(cave, x, y) != O_CONVEYOR_RIGHT; + if (cave->conveyor_belts_direction_changed) + left = !left; + dir = left ? ccw_eighth : cw_eighth; + + /* CHECK IF IT CONVEYS THE ELEMENT ABOVE IT */ + /* if gravity is normal, and the conveyor belt has something + ABOVE which can be moved + OR + the gravity is up, so anything that should float now goes + DOWN and touches the conveyor */ + if ((cave->gravity == GD_MV_DOWN && + moved_by_conveyor_top_dir(cave, x, y, GD_MV_UP)) || + (cave->gravity == GD_MV_UP && + moved_by_conveyor_bottom_dir(cave, x, y, GD_MV_UP))) + { + if (!is_scanned_dir(cave, x, y, GD_MV_UP) && + is_space_dir(cave, x, y, dir[GD_MV_UP])) + { + store_dir(cave, x, y, dir[GD_MV_UP], get_dir(cave, x, y, GD_MV_UP)); /* move */ + store_dir(cave, x, y, GD_MV_UP, O_SPACE); /* and place a space. */ + } + } + + /* CHECK IF IT CONVEYS THE ELEMENT BELOW IT */ + if ((cave->gravity == GD_MV_UP && + moved_by_conveyor_top_dir(cave, x, y, GD_MV_DOWN)) || + (cave->gravity == GD_MV_DOWN && + moved_by_conveyor_bottom_dir(cave, x, y, GD_MV_DOWN))) + { + if (!is_scanned_dir(cave, x, y, GD_MV_DOWN) && + is_space_dir(cave, x, y, dir[GD_MV_DOWN])) + { + store_dir(cave, x, y, dir[GD_MV_DOWN], get_dir(cave, x, y, GD_MV_DOWN)); /* move */ + store_dir(cave, x, y, GD_MV_DOWN, O_SPACE); /* and clear. */ + } + } + } + break; + + /* + * S I M P L E C H A N G I N G; E X P L O S I O N S + */ + + case O_EXPLODE_5: + store(cave, x, y, cave->explosion_effect); + break; + + case O_NUT_EXPL_4: + store(cave, x, y, O_DIAMOND); + break; + + case O_PRE_DIA_5: + store(cave, x, y, cave->diamond_birth_effect); + break; + + case O_PRE_STONE_4: + store(cave, x, y, O_STONE); + break; + + case O_NITRO_EXPL_4: + store(cave, x, y, cave->nitro_explosion_effect); + break; + + case O_BOMB_EXPL_4: + store(cave, x, y, cave->bomb_explosion_effect); + break; + + case O_AMOEBA_2_EXPL_4: + store(cave, x, y, cave->amoeba_2_explosion_effect); + break; + + case O_GHOST_EXPL_4: + { + static GdElement ghost_explode[] = + { + O_SPACE, O_SPACE, O_DIRT, O_DIRT, O_CLOCK, O_CLOCK, O_PRE_OUTBOX, + O_BOMB, O_BOMB, O_PLAYER, O_GHOST, O_BLADDER, O_DIAMOND, O_SWEET, + O_WAITING_STONE, O_BITER_1 + }; + + store(cave, x, y, ghost_explode[g_rand_int_range(cave->random, 0, G_N_ELEMENTS(ghost_explode))]); + } + break; + + case O_PRE_STEEL_4: + store(cave, x, y, O_STEEL); + break; + + case O_PRE_CLOCK_4: + store(cave, x, y, O_CLOCK); + break; + + case O_BOMB_TICK_7: + explode(cave, x, y); + break; + + case O_TRAPPED_DIAMOND: + if (cave->diamond_key_collected) + store(cave, x, y, O_DIAMOND); + break; + + case O_PRE_OUTBOX: + if (cave->gate_open) /* if no more diamonds needed */ + store(cave, x, y, O_OUTBOX); /* open outbox */ + break; + + case O_PRE_INVIS_OUTBOX: + if (cave->gate_open) /* if no more diamonds needed */ + store(cave, x, y, O_INVIS_OUTBOX); /* open outbox. invisible one :P */ + break; + + case O_INBOX: + if (cave->hatched && !inbox_toggle) /* if it is time of birth */ + store(cave, x, y, O_PRE_PL_1); + inbox_toggle = !inbox_toggle; + break; + + case O_PRE_PL_3: + store(cave, x, y, O_PLAYER); + break; + + case O_PRE_DIA_1: + case O_PRE_DIA_2: + case O_PRE_DIA_3: + case O_PRE_DIA_4: + case O_PRE_STONE_1: + case O_PRE_STONE_2: + case O_PRE_STONE_3: + case O_BOMB_TICK_1: + case O_BOMB_TICK_2: + case O_BOMB_TICK_3: + case O_BOMB_TICK_4: + case O_BOMB_TICK_5: + case O_BOMB_TICK_6: + case O_PRE_STEEL_1: + case O_PRE_STEEL_2: + case O_PRE_STEEL_3: + case O_BOMB_EXPL_1: + case O_BOMB_EXPL_2: + case O_BOMB_EXPL_3: + case O_NUT_EXPL_1: + case O_NUT_EXPL_2: + case O_NUT_EXPL_3: + case O_GHOST_EXPL_1: + case O_GHOST_EXPL_2: + case O_GHOST_EXPL_3: + case O_EXPLODE_1: + case O_EXPLODE_2: + case O_EXPLODE_3: + case O_EXPLODE_4: + case O_PRE_PL_1: + case O_PRE_PL_2: + case O_PRE_CLOCK_1: + case O_PRE_CLOCK_2: + case O_PRE_CLOCK_3: + case O_NITRO_EXPL_1: + case O_NITRO_EXPL_2: + case O_NITRO_EXPL_3: + case O_AMOEBA_2_EXPL_1: + case O_AMOEBA_2_EXPL_2: + case O_AMOEBA_2_EXPL_3: + /* simply the next identifier */ + next(cave, x, y); + break; + + case O_WATER_1: + case O_WATER_2: + case O_WATER_3: + case O_WATER_4: + case O_WATER_5: + case O_WATER_6: + case O_WATER_7: + case O_WATER_8: + case O_WATER_9: + case O_WATER_10: + case O_WATER_11: + case O_WATER_12: + case O_WATER_13: + case O_WATER_14: + case O_WATER_15: + found_water = TRUE; /* for sound */ + /* simply the next identifier */ + next(cave, x, y); + break; + + case O_BLADDER_SPENDER: + if (is_space_dir(cave, x, y, opposite[grav_compat])) + { + store_dir(cave, x, y, opposite[grav_compat], O_BLADDER); + store(cave, x, y, O_PRE_STEEL_1); + play_sound_of_element(cave, O_BLADDER_SPENDER, x, y); + } + break; + + default: + /* other inanimate elements that do nothing */ + break; + } + } + } + + /* POSTPROCESSING */ + + /* another scan-like routine: */ + /* short explosions (for example, in bd1) started with explode_2. */ + /* internally we use explode_1; and change it to explode_2 if needed. */ + if (cave->short_explosions) + { + for (y = 0; y < cave->h; y++) + { + for (x = 0; x < cave->w; x++) + { + if (is_first_stage_of_explosion(cave, x, y)) + { + next(cave, x, y); /* select next frame of explosion */ + + /* forget scanned flag immediately */ + store(cave, x, y, get(cave, x, y) & ~SCANNED); + } + } + } + } + + /* finally: forget "scanned" flags for objects. */ + /* also, check for time penalties. */ + /* these is something like an effect table, but we do not really use one. */ + for (y = 0; y < cave->h; y++) + { + for (x = 0; x < cave->w; x++) + { + if (get(cave, x, y) & SCANNED) + store(cave, x, y, get(cave, x, y) & ~SCANNED); + + if (get(cave, x, y) == O_TIME_PENALTY) + { + store(cave, x, y, O_GRAVESTONE); + + /* there is time penalty for destroying the voodoo */ + time_decrement_sec += cave->time_penalty; + } + } + } + + /* this loop finds the coordinates of the player. needed for scrolling and chasing stone.*/ + /* but we only do this, if a living player was found. if not yet, the setup + routine coordinates are used */ + if (cave->player_state==GD_PL_LIVING) + { + if (cave->active_is_first_found) + { + /* to be 1stb compatible, we do everything backwards. */ + for (y = cave->h - 1; y >= 0; y--) + { + for (x = cave->w - 1; x >= 0; x--) + { + if (is_player(cave, x, y)) + { + /* here we remember the coordinates. */ + cave->player_x = x; + cave->player_y = y; + } + } + } + } + else + { + /* as in the original: look for the last one */ + for (y = 0; y < cave->h; y++) + { + for (x = 0; x < cave->w; x++) + { + if (is_player(cave, x, y)) + { + /* here we remember the coordinates. */ + cave->player_x = x; + cave->player_y = y; + } + } + } + } + } + + /* record coordinates of player for chasing stone */ + for (i = 0; i < G_N_ELEMENTS(cave->px) - 1; i++) + { + cave->px[i] = cave->px[i + 1]; + cave->py[i] = cave->py[i + 1]; + } + + cave->px[G_N_ELEMENTS(cave->px) - 1] = cave->player_x; + cave->py[G_N_ELEMENTS(cave->py) - 1] = cave->player_y; + + /* SCHEDULING */ + + /* update timing calculated by iterating and counting elements */ + update_cave_speed(cave); + + /* cave 3 sounds. precedence is controlled by the sound_play function. */ + /* but we have to check amoeba&magic together as they had a different gritty sound when mixed */ + if (found_water && cave->water_sound) + gd_sound_play(cave, GD_S_WATER, O_WATER, -1, -1); + + magic_sound = (cave->magic_wall_state == GD_MW_ACTIVE && + cave->magic_wall_sound); + + amoeba_sound = (cave->hatched && cave->amoeba_sound && + ((amoeba_count > 0 && cave->amoeba_state == GD_AM_AWAKE) || + (amoeba_2_count > 0 && cave->amoeba_2_state == GD_AM_AWAKE))); + + if (amoeba_sound && magic_sound) + { + gd_sound_play(cave, GD_S_AMOEBA_MAGIC, O_AMOEBA, -1, -1); + } + else + { + if (amoeba_sound) + gd_sound_play(cave, GD_S_AMOEBA, O_AMOEBA, -1, -1); + else if (magic_sound) + gd_sound_play(cave, GD_S_MAGIC_WALL, O_MAGIC_WALL, -1, -1); + } + + if (cave->hatched) + { + if ((amoeba_count > 0 && cave->amoeba_state == GD_AM_AWAKE) || + (amoeba_2_count > 0 && cave->amoeba_2_state == GD_AM_AWAKE)) + play_sound_of_element(cave, O_AMOEBA, x, y); + } + + /* pneumatic hammer sound - overrides everything. */ + if (cave->pneumatic_hammer_active_delay > 0 && cave->pneumatic_hammer_sound) + gd_sound_play(cave, GD_S_PNEUMATIC_HAMMER, O_PNEUMATIC_HAMMER, -1, -1); + + /* CAVE VARIABLES */ + + /* PLAYER */ + + /* check if player is alive. */ + if ((cave->player_state == GD_PL_LIVING && cave->player_seen_ago > 15) || cave->kill_player) + cave->player_state = GD_PL_DIED; + + /* check if any voodoo exploded, and kill players the next scan if that happended. */ + if (cave->voodoo_touched) + cave->kill_player = TRUE; + + /* AMOEBA */ + + /* check flags after evaluating. */ + if (cave->amoeba_state == GD_AM_AWAKE) + { + if (amoeba_count >= cave->amoeba_max_count) + cave->amoeba_state = GD_AM_TOO_BIG; + if (amoeba_found_enclosed) + cave->amoeba_state = GD_AM_ENCLOSED; + } + + /* amoeba can also be turned into diamond by magic wall */ + if (cave->magic_wall_stops_amoeba && cave->magic_wall_state == GD_MW_ACTIVE) + cave->amoeba_state = GD_AM_ENCLOSED; + + /* AMOEBA 2 */ + if (cave->amoeba_2_state == GD_AM_AWAKE) + { + /* check flags after evaluating. */ + if (amoeba_2_count >= cave->amoeba_2_max_count) + cave->amoeba_2_state = GD_AM_TOO_BIG; + + if (amoeba_2_found_enclosed) + cave->amoeba_2_state = GD_AM_ENCLOSED; + } + + /* amoeba 2 can also be turned into diamond by magic wall */ + if (cave->magic_wall_stops_amoeba && cave->magic_wall_state == GD_MW_ACTIVE) + cave->amoeba_2_state = GD_AM_ENCLOSED; + + /* now check times. --------------------------- */ + /* decrement time if a voodoo was killed. */ + cave->time -= time_decrement_sec * cave->timing_factor; + if (cave->time < 0) + cave->time = 0; + + /* only decrement time when player is already born. */ + if (cave->hatched) + { + int secondsbefore, secondsafter; + + secondsbefore = cave->time / cave->timing_factor; + cave->time -= cave->speed; + if (cave->time <= 0) + cave->time = 0; + + secondsafter = cave->time / cave->timing_factor; + if (cave->time / cave->timing_factor < 10) + /* if less than 10 seconds, no walking sound, but play explosion sound */ + gd_sound_play(cave, GD_S_NONE, O_NONE, -1, -1); + + if (secondsbefore != secondsafter) + gd_cave_set_seconds_sound(cave); + } + + /* a gravity switch was activated; seconds counting down */ + if (cave->gravity_will_change > 0) + { + cave->gravity_will_change -= cave->speed; + if (cave->gravity_will_change < 0) + cave->gravity_will_change = 0; + + if (cave->gravity_will_change == 0) + { + cave->gravity = cave->gravity_next_direction; + if (cave->gravity_change_sound) + gd_sound_play(cave, GD_S_GRAVITY_CHANGE, O_GRAVITY_SWITCH, -1, -1); /* takes precedence over amoeba and magic wall sound */ + } + } + + /* creatures direction automatically change */ + if (cave->creatures_direction_will_change > 0) + { + cave->creatures_direction_will_change -= cave->speed; + if (cave->creatures_direction_will_change < 0) + cave->creatures_direction_will_change = 0; + + if (cave->creatures_direction_will_change == 0) + { + if (cave->creature_direction_auto_change_sound) + gd_sound_play(cave, GD_S_SWITCH_CREATURES, O_CREATURE_SWITCH, -1, -1); + + cave->creatures_backwards = !cave->creatures_backwards; + cave->creatures_direction_will_change = + cave->creatures_direction_auto_change_time * cave->timing_factor; + } + } + + /* magic wall; if active&wait or not wait for hatching */ + if (cave->magic_wall_state == GD_MW_ACTIVE && + (cave->hatched || !cave->magic_timer_wait_for_hatching)) + { + cave->magic_wall_time -= cave->speed; + if (cave->magic_wall_time < 0) + cave->magic_wall_time = 0; + if (cave->magic_wall_time == 0) + cave->magic_wall_state = GD_MW_EXPIRED; + } + + /* we may wait for hatching, when starting amoeba */ + if (cave->amoeba_timer_started_immediately || + (cave->amoeba_state == GD_AM_AWAKE && + (cave->hatched || !cave->amoeba_timer_wait_for_hatching))) + { + cave->amoeba_time -= cave->speed; + if (cave->amoeba_time < 0) + cave->amoeba_time = 0; + if (cave->amoeba_time == 0) + cave->amoeba_growth_prob = cave->amoeba_fast_growth_prob; + } + + /* we may wait for hatching, when starting amoeba */ + if (cave->amoeba_timer_started_immediately || + (cave->amoeba_2_state == GD_AM_AWAKE && + (cave->hatched || !cave->amoeba_timer_wait_for_hatching))) + { + cave->amoeba_2_time -= cave->speed; + if (cave->amoeba_2_time < 0) + cave->amoeba_2_time = 0; + if (cave->amoeba_2_time == 0) + cave->amoeba_2_growth_prob = cave->amoeba_2_fast_growth_prob; + } + + /* check for player hatching. */ + start_signal = FALSE; + + /* if not the c64 scheduling, but the correct frametime is used, + hatching delay should always be decremented. */ + /* otherwise, the if (millisecs...) condition below will set this. */ + if (cave->scheduling == GD_SCHEDULING_MILLISECONDS) + { + /* NON-C64 scheduling */ + if (cave->hatching_delay_frame > 0) + { + /* for milliseconds-based, non-c64 schedulings, hatching delay means frames. */ + cave->hatching_delay_frame--; + if (cave->hatching_delay_frame == 0) + start_signal = TRUE; + } + } + else + { + /* C64 scheduling */ + if (cave->hatching_delay_time > 0) + { + /* for c64 schedulings, hatching delay means milliseconds. */ + cave->hatching_delay_time -= cave->speed; + if (cave->hatching_delay_time <= 0) + { + cave->hatching_delay_time = 0; + start_signal = TRUE; + } + } + } + + /* if decremented hatching, and it became zero: */ + if (start_signal) + { + /* THIS IS THE CAVE START SIGNAL */ + + /* record that now the cave is in its normal state */ + cave->hatched = TRUE; + + /* if diamonds needed is below zero, we count the available diamonds now. */ + gd_cave_count_diamonds(cave); + + /* setup direction auto change */ + if (cave->creatures_direction_auto_change_time) + { + cave->creatures_direction_will_change = + cave->creatures_direction_auto_change_time * cave->timing_factor; + + if (cave->creatures_direction_auto_change_on_start) + cave->creatures_backwards = !cave->creatures_backwards; + } + + gd_sound_play(cave, GD_S_CRACK, O_INBOX, -1, -1); + } + + /* for biters */ + if (cave->biters_wait_frame == 0) + cave->biters_wait_frame = cave->biter_delay_frame; + else + cave->biters_wait_frame--; + + /* replicators delay */ + if (cave->replicators_wait_frame == 0) + cave->replicators_wait_frame = cave->replicator_delay_frame; + else + cave->replicators_wait_frame--; + + /* LAST THOUGTS */ + +#if 1 + /* check if cave failed by timeout is done in main game engine */ +#else + /* check if cave failed by timeout */ + if (cave->player_state == GD_PL_LIVING && cave->time == 0) + { + gd_cave_clear_sounds(cave); + cave->player_state = GD_PL_TIMEOUT; + gd_sound_play(cave, GD_S_TIMEOUT_0, O_NONE, -1, -1); + } +#endif + + /* set these for drawing. */ + cave->last_direction = player_move; + /* here we remember last movements for animation. this is needed here, + as animation is in sync with the game, not the keyboard directly. + (for example, after exiting the cave, the player was "running" in the + original, till bonus points were counted for remaining time and so on. */ + if (player_move == GD_MV_LEFT || + player_move == GD_MV_UP_LEFT || + player_move == GD_MV_DOWN_LEFT) + cave->last_horizontal_direction = GD_MV_LEFT; + + if (player_move == GD_MV_RIGHT || + player_move == GD_MV_UP_RIGHT || + player_move == GD_MV_DOWN_RIGHT) + cave->last_horizontal_direction = GD_MV_RIGHT; + + cave->frame++; /* XXX */ +} + +void set_initial_cave_speed(GdCave *cave) +{ + int ymin, ymax; + int x, y; + + /* set cave get function; to implement perfect or lineshifting borders */ + if (cave->lineshift) + cave->getp = getp_shift; + else + cave->getp = getp_perfect; + + /* check whether to scan the first and last line */ + if (cave->border_scan_first_and_last) + { + ymin = 0; + ymax = cave->h - 1; + } + else + { + ymin = 1; + ymax = cave->h - 2; + } + + for (y = ymin; y <= ymax; y++) + { + for (x = 0; x < cave->w; x++) + { + /* add the ckdelay correction value for every element seen. */ + cave->ckdelay += gd_elements[get(cave, x, y)].ckdelay; + } + } + + /* update timing calculated by iterating and counting elements */ + update_cave_speed(cave); +} diff --git a/src/game_bd/bd_caveengine.h b/src/game_bd/bd_caveengine.h new file mode 100644 index 00000000..12c456de --- /dev/null +++ b/src/game_bd/bd_caveengine.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef BD_CAVEENGINE_H +#define BD_CAVEENGINE_H + +#include + +#include "bd_cave.h" + + +/* the game itself */ +GdDirection gd_direction_from_keypress(boolean up, boolean down, boolean left, boolean right); +void gd_cave_iterate(GdCave *cave, GdDirection player_move, boolean player_fire, boolean suicide); +void set_initial_cave_speed(GdCave *cave); +void gd_cave_set_seconds_sound(GdCave *cave); +void gd_cave_clear_sounds(GdCave *cave); + +#endif // BD_CAVEENGINE_H diff --git a/src/game_bd/bd_caveobject.c b/src/game_bd/bd_caveobject.c new file mode 100644 index 00000000..254f373d --- /dev/null +++ b/src/game_bd/bd_caveobject.c @@ -0,0 +1,1608 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include "main_bd.h" + + +GdObjectLevels gd_levels_mask[] = +{ + GD_OBJECT_LEVEL1, + GD_OBJECT_LEVEL2, + GD_OBJECT_LEVEL3, + GD_OBJECT_LEVEL4, + GD_OBJECT_LEVEL5 +}; + +/* bdcff text description of object. caller should free string. */ +char *gd_object_get_bdcff(const GdObject *object) +{ + GString *str; + int j; + const char *type; + + switch (object->type) + { + case GD_POINT: + return g_strdup_printf("Point=%d %d %s", + object->x1, object->y1, + gd_elements[object->element].filename); + + case GD_LINE: + return g_strdup_printf("Line=%d %d %d %d %s", + object->x1, object->y1, object->x2, object->y2, + gd_elements[object->element].filename); + + case GD_RECTANGLE: + return g_strdup_printf("Rectangle=%d %d %d %d %s", + object->x1, object->y1, object->x2, object->y2, + gd_elements[object->element].filename); + + case GD_FILLED_RECTANGLE: + /* if elements are not the same */ + if (object->fill_element != object->element) + return g_strdup_printf("FillRect=%d %d %d %d %s %s", + object->x1, object->y1, object->x2, object->y2, + gd_elements[object->element].filename, + gd_elements[object->fill_element].filename); + + /* they are the same */ + return g_strdup_printf("FillRect=%d %d %d %d %s", + object->x1, object->y1, object->x2, object->y2, + gd_elements[object->element].filename); + + case GD_RASTER: + return g_strdup_printf("Raster=%d %d %d %d %d %d %s", + object->x1, object->y1, + (object->x2 - object->x1) / object->dx + 1, + (object->y2 - object->y1) / object->dy + 1, + object->dx, object->dy, + gd_elements[object->element].filename); + + case GD_JOIN: + return g_strdup_printf("Add=%d %d %s %s", + object->dx, object->dy, + gd_elements[object->element].filename, + gd_elements[object->fill_element].filename); + + case GD_FLOODFILL_BORDER: + return g_strdup_printf("BoundaryFill=%d %d %s %s", + object->x1, object->y1, + gd_elements[object->fill_element].filename, + gd_elements[object->element].filename); + + case GD_FLOODFILL_REPLACE: + return g_strdup_printf("FloodFill=%d %d %s %s", + object->x1, object->y1, + gd_elements[object->fill_element].filename, + gd_elements[object->element].filename); + + case GD_MAZE: + case GD_MAZE_UNICURSAL: + case GD_MAZE_BRAID: + switch (object->type) + { + case GD_MAZE: type = "perfect"; break; + case GD_MAZE_UNICURSAL: type = "unicursal"; break; + case GD_MAZE_BRAID: type = "braid"; break; + default: break; + } + + return g_strdup_printf("Maze=%d %d %d %d %d %d %d %d %d %d %d %d %s %s %s", + object->x1, object->y1, object->x2, object->y2, + object->dx, object->dy, + object->horiz, + object->seed[0], + object->seed[1], + object->seed[2], + object->seed[3], + object->seed[4], + gd_elements[object->element].filename, + gd_elements[object->fill_element].filename, type); + + case GD_RANDOM_FILL: + str = g_string_new(NULL); + /* seed and initial fill */ + g_string_append_printf(str, "%s=%d %d %d %d %d %d %d %d %d %s", + object->c64_random ? "RandomFillC64" : "RandomFill", + object->x1, object->y1, object->x2, object->y2, + object->seed[0], + object->seed[1], + object->seed[2], + object->seed[3], + object->seed[4], + gd_elements[object->fill_element].filename); + + for (j = 0; j < 4; j++) + { + if (object->random_fill_probability[j] != 0) + g_string_append_printf(str, " %s %d", + gd_elements[object->random_fill[j]].filename, + object->random_fill_probability[j]); + } + + if (object->element!=O_NONE) + g_string_append_printf(str, " %s", + gd_elements[object->element].filename); + + /* free string but do not free char *; return char *. */ + return g_string_free(str, FALSE); + + case GD_COPY_PASTE: + return g_strdup_printf("CopyPaste=%d %d %d %d %d %d %s %s", + object->x1, object->y1, object->x2, object->y2, + object->dx, object->dy, + object->mirror ? "mirror" : "nomirror", + object->flip?"flip":"noflip"); + + case NONE: + break; + } + + return NULL; +} + +/* create an INDIVIDUAL POINT CAVE OBJECT */ +GdObject *gd_object_new_point(GdObjectLevels levels, int x, int y, GdElement elem) +{ + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_POINT; + newobj->x1 = x; + newobj->y1 = y; + newobj->element = elem; + + return newobj; +} + +/* create a LINE OBJECT */ +GdObject *gd_object_new_line(GdObjectLevels levels, int x1, int y1, int x2, int y2, + GdElement elem) +{ + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_LINE; + newobj->x1 = x1; + newobj->y1 = y1; + newobj->x2 = x2; + newobj->y2 = y2; + newobj->element = elem; + + return newobj; +} + +/* create a RECTANGLE OBJECT */ +GdObject *gd_object_new_rectangle(GdObjectLevels levels, int x1, int y1, int x2, int y2, + GdElement elem) +{ + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_RECTANGLE; + newobj->x1 = x1; + newobj->y1 = y1; + newobj->x2 = x2; + newobj->y2 = y2; + newobj->element = elem; + + return newobj; +} + +/* create a RECTANGLE OBJECT */ +GdObject *gd_object_new_filled_rectangle(GdObjectLevels levels, int x1, int y1, int x2, int y2, + GdElement elem, GdElement fill_elem) +{ + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_FILLED_RECTANGLE; + newobj->x1 = x1; + newobj->y1 = y1; + newobj->x2 = x2; + newobj->y2 = y2; + newobj->element = elem; + newobj->fill_element = fill_elem; + + return newobj; +} + +/* create a raster object */ +GdObject *gd_object_new_raster(GdObjectLevels levels, int x1, int y1, int x2, int y2, + int dx, int dy, GdElement elem) +{ + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_RASTER; + newobj->x1 = x1; + newobj->y1 = y1; + newobj->x2 = x2; + newobj->y2 = y2; + newobj->dx = dx; + newobj->dy = dy; + newobj->element = elem; + + return newobj; +} + +/* create a raster object */ +GdObject *gd_object_new_join(GdObjectLevels levels, int dx, int dy, + GdElement search, GdElement replace) +{ + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_JOIN; + newobj->dx = dx; + newobj->dy = dy; + newobj->element = search; + newobj->fill_element = replace; + + return newobj; +} + +/* create a new boundary fill object */ +GdObject *gd_object_new_floodfill_border(GdObjectLevels levels, int x1, int y1, + GdElement fill, GdElement border) +{ + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_FLOODFILL_BORDER; + newobj->x1 = x1; + newobj->y1 = y1; + newobj->element = border; + newobj->fill_element = fill; + + return newobj; +} + +GdObject *gd_object_new_floodfill_replace(GdObjectLevels levels, int x1, int y1, + GdElement fill, GdElement to_replace) +{ + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_FLOODFILL_REPLACE; + newobj->x1 = x1; + newobj->y1 = y1; + newobj->element = to_replace; + newobj->fill_element = fill; + + return newobj; +} + +GdObject *gd_object_new_maze(GdObjectLevels levels, int x1, int y1, int x2, int y2, + int wall_w, int path_w, GdElement wall_e, GdElement path_e, + int horiz_percent, const gint32 seed[5]) +{ + int i; + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_MAZE; + newobj->x1 = x1; + newobj->y1 = y1; + newobj->x2 = x2; + newobj->y2 = y2; + newobj->dx = wall_w; + newobj->dy = path_w; + newobj->element = wall_e; + newobj->fill_element = path_e; + newobj->horiz = horiz_percent; + + for (i = 0; i < 5; ++i) + newobj->seed[i] = seed[i]; + + return newobj; +} + +GdObject *gd_object_new_maze_unicursal(GdObjectLevels levels, int x1, int y1, int x2, int y2, + int wall_w, int path_w, GdElement wall_e, GdElement path_e, + int horiz_percent, const gint32 seed[5]) +{ + int i; + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_MAZE_UNICURSAL; + newobj->x1 = x1; + newobj->y1 = y1; + newobj->x2 = x2; + newobj->y2 = y2; + newobj->dx = wall_w; + newobj->dy = path_w; + newobj->element = wall_e; + newobj->fill_element = path_e; + newobj->horiz = horiz_percent; + + for (i = 0; i < 5; ++i) + newobj->seed[i] = seed[i]; + + return newobj; +} + +GdObject *gd_object_new_maze_braid(GdObjectLevels levels, int x1, int y1, int x2, int y2, + int wall_w, int path_w, GdElement wall_e, GdElement path_e, + int horiz_percent, const gint32 seed[5]) +{ + int i; + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_MAZE_BRAID; + newobj->x1 = x1; + newobj->y1 = y1; + newobj->x2 = x2; + newobj->y2 = y2; + newobj->dx = wall_w; + newobj->dy = path_w; + newobj->element = wall_e; + newobj->fill_element = path_e; + newobj->horiz = horiz_percent; + + for (i = 0; i < 5; ++i) + newobj->seed[i] = seed[i]; + + return newobj; +} + +GdObject *gd_object_new_random_fill(GdObjectLevels levels, int x1, int y1, int x2, int y2, + const gint32 seed[5], GdElement initial, + const GdElement random[4], const gint32 prob[4], + GdElement replace_only, boolean c64) +{ + int i; + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_RANDOM_FILL; + newobj->x1 = x1; + newobj->y1 = y1; + newobj->x2 = x2; + newobj->y2 = y2; + newobj->fill_element = initial; + + for (i = 0; i < 5; ++i) + newobj->seed[i] = seed[i]; + + for (i = 0; i < 4; ++i) + { + newobj->random_fill[i] = random[i]; + newobj->random_fill_probability[i] = prob[i]; + } + + newobj->element = replace_only; + newobj->c64_random = c64; + + return newobj; +} + +GdObject *gd_object_new_copy_paste(GdObjectLevels levels, int x1, int y1, int x2, int y2, + int dx, int dy, boolean mirror, boolean flip) +{ + GdObject *newobj = checked_calloc(sizeof(GdObject)); + + newobj->levels = levels; + newobj->type = GD_COPY_PASTE; + newobj->x1 = x1; + newobj->y1 = y1; + newobj->x2 = x2; + newobj->y2 = y2; + newobj->dx = dx; + newobj->dy = dy; + newobj->mirror = mirror; + newobj->flip = flip; + + return newobj; +} + +/* create new object from bdcff description. + return new object if ok; return null if failed. + */ +GdObject *gd_object_new_from_string(char *str) +{ + char *equalsign; + char *name, *param; + GdObject object; + char elem0[100], elem1[100]; + + equalsign = strchr(str, '='); + if (!equalsign) + return NULL; + + /* split string by replacing the equal sign with zero */ + *equalsign = '\0'; + name = str; + param = equalsign + 1; + + /* INDIVIDUAL POINT CAVE OBJECT */ + if (strcasecmp(name, "Point") == 0) + { + object.type = GD_POINT; + if (sscanf(param, "%d %d %s", &object.x1, &object.y1, elem0) == 3) + { + object.element = gd_get_element_from_string(elem0); + + return g_memdup(&object, sizeof (GdObject)); + } + + return NULL; + } + + /* LINE OBJECT */ + if (strcasecmp(name, "Line") == 0) + { + object.type = GD_LINE; + if (sscanf(param, "%d %d %d %d %s", &object.x1, &object.y1, &object.x2, &object.y2, elem0) == 5) + { + object.element = gd_get_element_from_string(elem0); + + return g_memdup(&object, sizeof (GdObject)); + } + + return NULL; + } + + /* RECTANGLE OBJECT */ + if (strcasecmp(name, "Rectangle") == 0) + { + if (sscanf(param, "%d %d %d %d %s", &object.x1, &object.y1, &object.x2, &object.y2, elem0) == 5) + { + object.type = GD_RECTANGLE; + object.element = gd_get_element_from_string (elem0); + + return g_memdup(&object, sizeof (GdObject)); + } + + return NULL; + } + + /* FILLED RECTANGLE OBJECT */ + if (strcasecmp(name, "FillRect") == 0) + { + int paramcount; + + paramcount = sscanf(param, "%d %d %d %d %s %s", &object.x1, &object.y1, &object.x2, &object.y2, elem0, elem1); + object.type = GD_FILLED_RECTANGLE; + + if (paramcount == 6) + { + object.element = gd_get_element_from_string (elem0); + object.fill_element = gd_get_element_from_string (elem1); + + return g_memdup(&object, sizeof (GdObject)); + } + + if (paramcount == 5) + { + object.element = object.fill_element = gd_get_element_from_string (elem0); + + return g_memdup(&object, sizeof (GdObject)); + } + + return NULL; + } + + /* RASTER */ + if (strcasecmp(name, "Raster") == 0) + { + int nx, ny; + + if (sscanf(param, "%d %d %d %d %d %d %s", &object.x1, &object.y1, &nx, &ny, &object.dx, &object.dy, elem0) == 7) + { + nx--; + ny--; + object.x2 = object.x1 + nx * object.dx; + object.y2 = object.y1 + ny * object.dy; + object.type = GD_RASTER; + object.element = gd_get_element_from_string (elem0); + + return g_memdup(&object, sizeof (GdObject)); + } + + return NULL; + } + + /* JOIN */ + if (strcasecmp(name, "Join") == 0 || + strcasecmp(name, "Add") == 0) + { + if (sscanf(param, "%d %d %s %s", &object.dx, &object.dy, elem0, elem1) == 4) + { + object.type = GD_JOIN; + object.element = gd_get_element_from_string (elem0); + object.fill_element = gd_get_element_from_string (elem1); + + return g_memdup(&object, sizeof (GdObject)); + } + + return NULL; + } + + /* FILL TO BORDER OBJECT */ + if (strcasecmp(name, "BoundaryFill") == 0) + { + if (sscanf(param, "%d %d %s %s", &object.x1, &object.y1, elem0, elem1) == 4) + { + object.type = GD_FLOODFILL_BORDER; + object.fill_element = gd_get_element_from_string (elem0); + object.element = gd_get_element_from_string (elem1); + + return g_memdup(&object, sizeof (GdObject)); + } + + return NULL; + } + + /* REPLACE FILL OBJECT */ + if (strcasecmp(name, "FloodFill") == 0) + { + if (sscanf(param, "%d %d %s %s", &object.x1, &object.y1, elem0, elem1) == 4) + { + object.type = GD_FLOODFILL_REPLACE; + object.fill_element = gd_get_element_from_string (elem0); + object.element = gd_get_element_from_string (elem1); + + return g_memdup(&object, sizeof (GdObject)); + } + + return NULL; + } + + /* MAZE OBJECT */ + /* MAZE UNICURSAL OBJECT */ + /* BRAID MAZE OBJECT */ + if (strcasecmp(name, "Maze") == 0) + { + char type[100] = "perfect"; + + if (sscanf(param, "%d %d %d %d %d %d %d %d %d %d %d %d %s %s %s", &object.x1, &object.y1, &object.x2, &object.y2, &object.dx, &object.dy, &object.horiz, &object.seed[0], &object.seed[1], &object.seed[2], &object.seed[3], &object.seed[4], elem0, elem1, type) >= 14) + { + if (strcasecmp(type, "unicursal") == 0) + object.type = GD_MAZE_UNICURSAL; + else if (strcasecmp(type, "perfect") == 0) + object.type = GD_MAZE; + else if (strcasecmp(type, "braid") == 0) + object.type = GD_MAZE_BRAID; + else + { + Warn("unknown maze type: %s, defaulting to perfect", type); + object.type = GD_MAZE; + } + + object.element = gd_get_element_from_string (elem0); + object.fill_element = gd_get_element_from_string (elem1); + + return g_memdup(&object, sizeof (GdObject)); + } + + return NULL; + } + + /* RANDOM FILL OBJECT */ + if (strcasecmp(name, "RandomFill") == 0 || + strcasecmp(name, "RandomFillC64") == 0) + { + static char **words = NULL; + int l, i; + + object.type = GD_RANDOM_FILL; + if (strcasecmp(name, "RandomFillC64") == 0) + /* totally the same, but uses c64 random generator */ + object.c64_random = TRUE; + else + object.c64_random = FALSE; + + if (sscanf(param, "%d %d %d %d", &object.x1, &object.y1, &object.x2, &object.y2) != 4) + return NULL; + + if (words) + g_strfreev(words); + + words = g_strsplit_set(param, " ", -1); + l = g_strv_length(words); + + if (l < 10 || l > 19) + return NULL; + + for (i = 0; i < 5; i++) + if (sscanf(words[4 + i], "%d", &object.seed[i]) != 1) + return NULL; + + object.fill_element = gd_get_element_from_string(words[9]); + + for (i = 0; i < 4; i++) + { + object.random_fill[i] = O_DIRT; + object.random_fill_probability[i] = 0; + } + + for (i = 10; i < l - 1; i += 2) + { + object.random_fill[(i - 10) / 2] = gd_get_element_from_string(words[i]); + if (sscanf(words[i + 1], "%d", &object.random_fill_probability[(i - 10) / 2]) == 0) + return NULL; + } + + object.element = O_NONE; + + if (l > 10 && l % 2 == 1) + object.element = gd_get_element_from_string(words[l - 1]); + + return g_memdup(&object, sizeof (GdObject)); + } + + /* COPY PASTE OBJECT */ + if (strcasecmp(name, "CopyPaste") == 0) + { + char mirror[100] = "nomirror"; + char flip[100] = "noflip"; + object.type = GD_COPY_PASTE; + + object.flip = object.mirror = FALSE; + + if (sscanf(param, "%d %d %d %d %d %d %s %s", &object.x1, &object.y1, &object.x2, &object.y2, &object.dx, &object.dy, mirror, flip) < 6) + return NULL; + + /* MIRROR PROPERTY */ + if (strcasecmp(mirror, "mirror") == 0) + object.mirror = TRUE; + else if (strcasecmp(mirror, "nomirror") == 0) + object.mirror = FALSE; + else + Warn("invalid setting for copypaste mirror property: %s", mirror); + + /* FLIP PROPERTY */ + if (strcasecmp(flip, "flip") == 0) + object.flip = TRUE; + else if (strcasecmp(flip, "noflip") == 0) + object.flip = FALSE; + else + Warn("invalid setting for copypaste flip property: %s", flip); + + return g_memdup(&object, sizeof(GdObject)); + } + + return NULL; +} + +/** drawing a line, using bresenham's */ +static void draw_line (GdCave *cave, const GdObject *object) +{ + int x, y, x1, y1, x2, y2; + boolean steep; + int error, dx, dy, ystep; + + x1 = object->x1; + y1 = object->y1, x2 = object->x2; + y2 = object->y2; + steep = ABS (y2 - y1) > ABS (x2 - x1); + + if (steep) + { + x = x1; + x1 = y1; + y1 = x; + x = x2; + x2 = y2; + y2 = x; + } + + if (x1 > x2) + { + x = x1; + x1 = x2; + x2 = x; + x = y1; + y1 = y2; + y2 = x; + } + + dx = x2 - x1; + dy = ABS (y2 - y1); + y = y1; + error = 0; + ystep = (y1 < y2) ? 1 : -1; + + for (x = x1; x <= x2; x++) + { + if (steep) + gd_cave_store_rc (cave, y, x, object->element, object); + else + gd_cave_store_rc (cave, x, y, object->element, object); + + error += dy; + + if (error * 2 >= dx) + { + y += ystep; + error -= dx; + } + } +} + + + +static void draw_fill_replace_proc(GdCave *cave, int x, int y, const GdObject *object) +{ + /* fill with border so we do not come back */ + gd_cave_store_rc(cave, x, y, object->fill_element, object); + + if (x > 0 && gd_cave_get_rc(cave, x - 1, y) == object->element) + draw_fill_replace_proc(cave, x - 1, y, object); + + if (y > 0 && gd_cave_get_rc(cave, x, y - 1) == object->element) + draw_fill_replace_proc(cave, x, y - 1, object); + + if (x < cave->w - 1 && gd_cave_get_rc(cave, x + 1, y) == object->element) + draw_fill_replace_proc(cave, x + 1, y, object); + + if (y < cave->h - 1 && gd_cave_get_rc(cave, x, y + 1) == object->element) + draw_fill_replace_proc(cave, x, y + 1, object); +} + +static void draw_fill_replace (GdCave *cave, const GdObject *object) +{ + /* check bounds */ + if (object->x1 < 0 || + object->y1 < 0 || + object->x1 >= cave->w || + object->y1 >= cave->h) + return; + + if (object->element == object->fill_element) + return; + + /* this procedure fills the area with the object->element. */ + draw_fill_replace_proc(cave, object->x1, object->y1, object); +} + +static void draw_fill_border_proc (GdCave *cave, int x, int y, const GdObject *object) +{ + /* fill with border so we do not come back */ + gd_cave_store_rc(cave, x, y, object->element, object); + + if (x > 0 && gd_cave_get_rc(cave, x - 1, y) != object->element) + draw_fill_border_proc(cave, x - 1, y, object); + + if (y > 0 && gd_cave_get_rc(cave, x, y - 1) != object->element) + draw_fill_border_proc(cave, x, y - 1, object); + + if (x < cave->w - 1 && gd_cave_get_rc(cave, x + 1, y) != object->element) + draw_fill_border_proc(cave, x + 1, y, object); + + if (y < cave->h-1 && gd_cave_get_rc(cave, x, y + 1) != object->element) + draw_fill_border_proc(cave, x, y + 1, object); +} + +static void draw_fill_border (GdCave *cave, const GdObject *object) +{ + int x, y; + + /* check bounds */ + if (object->x1 < 0 || + object->y1 < 0 || + object->x1 >= cave->w || + object->y1 >= cave->h) + return; + + /* this procedure fills the area with the object->element. */ + draw_fill_border_proc(cave, object->x1, object->y1, object); + + /* after the fill, we change all filled cells to the fill_element. */ + /* we find those by looking at the object_order[][] */ + for (y = 0; y < cave->h; y++) + for (x = 0; x < cave->w; x++) + if (cave->objects_order[y][x] == object) + cave->map[y][x] = object->fill_element; +} + +/* rectangle, frame only */ +static void draw_rectangle(GdCave *cave, const GdObject *object) +{ + int x1, y1, x2, y2, x, y; + + /* reorder coordinates if not drawing from northwest to southeast */ + x1 = object->x1; + y1 = object->y1, x2 = object->x2; + y2 = object->y2; + + if (y1 > y2) + { + y = y1; + y1 = y2; + y2 = y; + } + + if (x1 > x2) + { + x = x1; + x1 = x2; + x2 = x; + } + + for (x = x1; x <= x2; x++) + { + gd_cave_store_rc(cave, x, object->y1, object->element, object); + gd_cave_store_rc(cave, x, object->y2, object->element, object); + } + + for (y = y1; y <= y2; y++) + { + gd_cave_store_rc(cave, object->x1, y, object->element, object); + gd_cave_store_rc(cave, object->x2, y, object->element, object); + } +} + +/* rectangle, filled one */ +static void draw_filled_rectangle(GdCave *cave, const GdObject *object) +{ + int x1, y1, x2, y2, x, y; + + /* reorder coordinates if not drawing from northwest to southeast */ + x1 = object->x1; + y1 = object->y1, x2 = object->x2; + y2 = object->y2; + + if (y1 > y2) + { + y = y1; + y1 = y2; + y2 = y; + } + + if (x1 > x2) + { + x = x1; + x1 = x2; + x2 = x; + } + + for (y = y1; y <= y2; y++) + for (x = x1; x <= x2; x++) + gd_cave_store_rc(cave, x, y, (y == object->y1 || + y == object->y2 || + x == object->x1 || + x == object->x2) ? object->element : object->fill_element, object); +} + +/* something like ordered fill, increment is dx and dy. */ +static void draw_raster(GdCave *cave, const GdObject *object) +{ + int x, y, x1, y1, x2, y2; + int dx, dy; + + /* reorder coordinates if not drawing from northwest to southeast */ + x1 = object->x1; + y1 = object->y1; + x2 = object->x2; + y2 = object->y2; + + if (y1 > y2) + { + y = y1; + y1 = y2; + y2 = y; + } + + if (x1 > x2) + { + x = x1; + x1 = x2; + x2 = x; + } + + dx = object->dx; + + if (dx < 1) + dx = 1; + + dy = object->dy; + + if (dy < 1) + dy = 1; + + for (y = y1; y <= y2; y += dy) + for (x = x1; x <= x2; x += dx) + gd_cave_store_rc(cave, x, y, object->element, object); +} + +/* find every object, and put fill_element next to it. relative coordinates dx,dy */ +static void draw_join(GdCave *cave, const GdObject *object) +{ + int x, y; + + for (y = 0; y < cave->h; y++) + { + for (x = 0; x < cave->w; x++) + { + if (cave->map[y][x] == object->element) + { + int nx = x + object->dx; + int ny = y + object->dy; + /* this one implements wraparound for joins. + it is needed by many caves in profi boulder series */ + while (nx >= cave->w) + nx -= cave->w, ny++; + + gd_cave_store_rc(cave, nx, ny, object->fill_element, object); + } + } + } +} + +/* create a maze in a boolean **maze. */ +/* recursive algorithm. */ +static void mazegen(GRand *rand, boolean **maze, int width, int height, int x, int y, int horiz) +{ + int dirmask = 15; + + maze[y][x] = TRUE; + while (dirmask != 0) + { + int dir; + + /* horiz or vert */ + dir = g_rand_int_range(rand, 0, 100) = 2 && !maze[y - 2][x]) + { + maze[y - 1][x] = TRUE; + mazegen(rand, maze, width, height, x, y - 2, horiz); + } + break; + + case 1: /* down */ + if (y < height-2 && !maze[y + 2][x]) { + maze[y + 1][x] = TRUE; + mazegen(rand, maze, width, height, x, y + 2, horiz); + } + break; + + case 2: /* left */ + if (x >= 2 && !maze[y][x - 2]) { + maze[y][x - 1] = TRUE; + mazegen(rand, maze, width, height, x - 2, y, horiz); + } + break; + + case 3: /* right */ + if (x < width - 2 && !maze[y][x + 2]) { + maze[y][x + 1] = TRUE; + mazegen(rand, maze, width, height, x + 2, y, horiz); + } + break; + + default: + break; + } + } + } +} + +static void braidmaze(GRand *rand, boolean **maze, int w, int h) +{ + int x, y; + + for (y = 0; y < h; y += 2) + { + for (x = 0; x < w; x += 2) + { + int closed = 0, dirs = 0; + int closed_dirs[4]; + + /* if it is the edge of the map, OR no path carved, then we can't go in that direction. */ + if (x < 1 || !maze[y][x - 1]) + { + /* closed from this side. */ + closed++; + + /* if not the edge, we might open this wall (carve a path) to remove a dead end */ + if (x > 0) + closed_dirs[dirs++] = GD_MV_LEFT; + } + + /* other 3 directions similar */ + if (y < 1 || !maze[y - 1][x]) + { + closed++; + if (y > 0) + closed_dirs[dirs++] = GD_MV_UP; + } + + if (x >= w - 1 || !maze[y][x + 1]) + { + closed++; + if (x < w - 1) + closed_dirs[dirs++] = GD_MV_RIGHT; + } + + if (y >= h - 1 || !maze[y + 1][x]) { + closed++; + if (y < h - 1) + closed_dirs[dirs++] = GD_MV_DOWN; + } + + /* if closed from 3 sides, then it is a dead end. also check dirs != 0, + that might fail for a 1x1 maze :) */ + if (closed == 3 && dirs != 0) + { + /* make up a random direction, and open in that direction, so dead end is removed */ + int dir = closed_dirs[g_rand_int_range(rand, 0, dirs)]; + + switch (dir) + { + case GD_MV_LEFT: + maze[y][x - 1] = TRUE; break; + case GD_MV_UP: + maze[y - 1][x] = TRUE; break; + case GD_MV_RIGHT: + maze[y][x + 1] = TRUE; break; + case GD_MV_DOWN: + maze[y + 1][x] = TRUE; break; + } + } + } + } +} + +static void draw_maze(GdCave *cave, const GdObject *object, int level) +{ + int x, y; + boolean **map; + int x1 = object->x1; + int y1 = object->y1; + int x2 = object->x2; + int y2 = object->y2; + int w, h, path, wall; + int xk, yk; + GRand *rand; + int i,j; + + /* change coordinates if not in correct order */ + if (y1 > y2) + { + y = y1; + y1 = y2; + y2 = y; + } + + if (x1 > x2) + { + x = x1; + x1 = x2; + x2 = x; + } + + wall = object->dx; + if (wall < 1) + wall = 1; + + path = object->dy; + if (path < 1) + path = 1; + + /* calculate the width and height of the maze. + n = number of passages, path = path width, wall = wall width, maze = maze width. + if given the number of passages, the width of the maze is: + + n * path + (n - 1) * wall = maze + n * path + n * wall - wall = maze + n * (path + wall) = maze + wall + n = (maze + wall) / (path + wall) + */ + + /* number of passages for each side */ + w = (x2 - x1 + 1 + wall) / (path + wall); + h = (y2 - y1 + 1 + wall) / (path + wall); + + /* and we calculate the size of the internal map */ + if (object->type == GD_MAZE_UNICURSAL) + { + /* for unicursal maze, width and height must be mod2 = 0, + and we will convert to paths & walls later */ + w = w / 2 * 2; + h = h / 2 * 2; + } + else + { + /* for normal maze */ + w = 2 * (w - 1) + 1; + h = 2 * (h - 1) + 1; + } + + /* twodimensional boolean array to generate map in */ + map = checked_malloc((h) * sizeof(boolean *)); + for (y = 0; y < h; y++) + map[y] = checked_calloc(w * sizeof(boolean)); + + /* start generation, if map is big enough. + otherwise the application would crash, as the editor places maze objects + during mouse click & drag that have no sense */ + rand = g_rand_new_with_seed(object->seed[level] == -1 ? + g_rand_int(cave->random) : object->seed[level]); + + if (w >= 1 && h >= 1) + mazegen(rand, map, w, h, 0, 0, object->horiz); + + if (object->type == GD_MAZE_BRAID) + braidmaze(rand, map, w, h); + + g_rand_free(rand); + + if (w >= 1 && h >= 1 && object->type == GD_MAZE_UNICURSAL) + { + boolean **unicursal; + + /* convert to unicursal maze */ + /* original: + xxx x + x x + xxxxx + + unicursal: + xxxxxxx xxx + x x x x + xxxxx x x x + x x x x + xxxxx xxx x + x x + xxxxxxxxxxx + */ + + unicursal = checked_malloc((h * 2 - 1) * sizeof(boolean *)); + + for (y = 0; y < h * 2 - 1; y++) + unicursal[y] = checked_calloc((w * 2 - 1) * sizeof(boolean)); + + for (y = 0; y < h; y++) + { + for(x = 0; x < w; x++) + { + if (map[y][x]) { + unicursal[y * 2][x * 2] = TRUE; + unicursal[y * 2][x * 2 + 2] = TRUE; + unicursal[y * 2 + 2][x * 2] = TRUE; + unicursal[y * 2 + 2][x * 2 + 2] = TRUE; + + if (x < 1 || !map[y][x - 1]) unicursal[y * 2 + 1][x * 2] = TRUE; + if (y < 1 || !map[y - 1][x]) unicursal[y * 2][x * 2 + 1] = TRUE; + if (x >= w - 1 || !map[y][x + 1]) unicursal[y * 2 + 1][x * 2 + 2] = TRUE; + if (y >= h - 1 || !map[y + 1][x]) unicursal[y * 2 + 2][x * 2 + 1] = TRUE; + } + } + } + + /* free original map */ + for (y = 0; y < h; y++) + free(map[y]); + free(map); + + /* change to new map - the unicursal maze */ + map = unicursal; + h = h * 2 - 1; + w = w * 2 - 1; + } + + /* copy map to cave with correct elements and size */ + /* now copy the map into the cave. the copying works like this... + pwpwp + xxxxx p + x x w + x xxx p + x w + xxxxx p + columns and rows denoted with "p" are to be drawn with path width, + the others with wall width. */ + + yk = y1; + + for (y = 0; y < h; y++) + { + for (i = 0; i < (y % 2 == 0 ? path : wall); i++) + { + xk = x1; + + for (x = 0; x < w; x++) + for (j = 0; j < (x % 2 == 0 ? path : wall); j++) + gd_cave_store_rc(cave, xk++, yk, map[y][x] ? object->fill_element : object->element, object); + + /* if width is smaller than requested, fill with wall */ + for(x = xk; x <= x2; x++) + gd_cave_store_rc(cave, x, yk, object->element, object); + + yk++; + } + } + + /* if height is smaller than requested, fill with wall */ + for (y = yk; y <= y2; y++) + for (x = x1; x <= x2; x++) + gd_cave_store_rc(cave, x, y, object->element, object); + + /* free map */ + for (y = 0; y < h; y++) + free(map[y]); + free(map); +} + +static void draw_random_fill(GdCave *cave, const GdObject *object, int level) +{ + int x, y; + int x1 = object->x1; + int y1 = object->y1; + int x2 = object->x2; + int y2 = object->y2; + GRand *rand; + GdC64RandomGenerator c64_rand; + guint32 seed; + + /* -1 means that it should be different every time played. */ + if (object->seed[level] == -1) + seed = g_rand_int(cave->random); + else + seed = object->seed[level]; + + rand = g_rand_new_with_seed(seed); + /* for c64 random, use the 2*8 lsb. */ + gd_c64_random_set_seed(&c64_rand, seed/256%256, seed%256); + + /* change coordinates if not in correct order */ + if (y1 > y2) + { + y = y1; + y1 = y2; + y2 = y; + } + + if (x1 > x2) + { + x = x1; + x1 = x2; + x2 = x; + } + + for (y = y1; y <= y2; y++) + { + for (x = x1; x <= x2; x++) + { + unsigned int randm; + GdElement element; + + if (object->c64_random) + /* use c64 random generator */ + randm = gd_c64_random(&c64_rand); + else + /* use the much better glib random generator */ + randm = g_rand_int_range(rand, 0, 256); + + element = object->fill_element; + if (randm < object->random_fill_probability[0]) + element = object->random_fill[0]; + if (randm < object->random_fill_probability[1]) + element = object->random_fill[1]; + if (randm < object->random_fill_probability[2]) + element = object->random_fill[2]; + if (randm < object->random_fill_probability[3]) + element = object->random_fill[3]; + + if (object->element==O_NONE || + gd_cave_get_rc(cave, x, y) == object->element) + gd_cave_store_rc(cave, x, y, element, object); + } + } + + g_rand_free(rand); +} + + +static void draw_copy_paste(GdCave *cave, const GdObject *object) +{ + int x1 = object->x1, y1 = object->y1, x2 = object->x2, y2 = object->y2; + int x, y; /* iterators */ + int w, h; + GdElement *clipboard; + + /* reorder coordinates if not drawing from northwest to southeast */ + if (x2 < x1) + { + x = x2; + x2 = x1; + x1 = x; + } + + if (y2 < y1) + { + y = y2; + y2 = y1; + y1 = y; + } + + w = x2 - x1 + 1; + h = y2 - y1 + 1; + + clipboard = checked_malloc((w * h) * sizeof(GdElement)); + + /* copy to "clipboard" */ + for (y = 0; y < h; y++) + for (x = 0; x < w; x++) + clipboard[y * w + x] = gd_cave_get_rc(cave, x + x1, y + y1); + + for (y = 0; y < h; y++) + { + int ydest; + + ydest = object->flip ? h - 1 - y : y; + + for (x = 0; x < w; x++) + { + int xdest; + + xdest = object->mirror ? w - 1 - x : x; + + /* dx and dy are used here are "paste to" coordinates */ + gd_cave_store_rc(cave, object->dx + xdest, object->dy + ydest, + clipboard[y * w + x], object); + } + } + + free(clipboard); +} + +/* draw the specified game object into cave's data. + also remember, which cell was set by which cave object. */ +void gd_cave_draw_object(GdCave *cave, const GdObject *object, int level) +{ + switch (object->type) + { + case GD_POINT: + /* single point */ + gd_cave_store_rc(cave, object->x1, object->y1, object->element, object); + break; + + case GD_LINE: + draw_line(cave, object); + break; + + case GD_RECTANGLE: + draw_rectangle(cave, object); + break; + + case GD_FILLED_RECTANGLE: + draw_filled_rectangle(cave, object); + break; + + case GD_RASTER: + draw_raster(cave, object); + break; + + case GD_JOIN: + draw_join(cave, object); + break; + + case GD_FLOODFILL_BORDER: + draw_fill_border(cave, object); + break; + + case GD_FLOODFILL_REPLACE: + draw_fill_replace(cave, object); + break; + + case GD_MAZE: + case GD_MAZE_UNICURSAL: + case GD_MAZE_BRAID: + draw_maze(cave, object, level); + break; + + case GD_RANDOM_FILL: + draw_random_fill(cave, object, level); + break; + + case GD_COPY_PASTE: + draw_copy_paste(cave, object); + break; + + case NONE: + break; + + default: + Error("Unknown object %d", object->type); + break; + } +} + +/* load cave to play... also can be called rendering the cave elements */ +GdCave *gd_cave_new_rendered(const GdCave *data, const int level, const guint32 seed) +{ + GdCave *cave; + GdElement element; + int x, y; + GList *iter; + + /* make a copy */ + cave = gd_cave_new_from_cave(data); + cave->rendered = level + 1; + + cave->render_seed = seed; + cave->random = g_rand_new_with_seed(cave->render_seed); + + /* maps needed during drawing and gameplay */ + cave->objects_order = gd_cave_map_new(cave, gpointer); + + cave->time = data->level_time[level]; + cave->timevalue = data->level_timevalue[level]; + cave->diamonds_needed = data->level_diamonds[level]; + cave->magic_wall_time = data->level_magic_wall_time[level]; + cave->slime_permeability = data->level_slime_permeability[level]; + cave->slime_permeability_c64 = data->level_slime_permeability_c64[level]; + cave->time_bonus = data->level_bonus_time[level]; + cave->time_penalty = data->level_penalty_time[level]; + cave->amoeba_time = data->level_amoeba_time[level]; + cave->amoeba_max_count = data->level_amoeba_threshold[level]; + cave->amoeba_2_time = data->level_amoeba_2_time[level]; + cave->amoeba_2_max_count = data->level_amoeba_2_threshold[level]; + cave->hatching_delay_time = data->level_hatching_delay_time[level]; + cave->hatching_delay_frame = data->level_hatching_delay_frame[level]; + + if (!cave->map) + { + /* if we have no map, fill with predictable random generator. */ + cave->map = gd_cave_map_new(cave, GdElement); + + /* IF CAVE HAS NO MAP, USE THE RANDOM NUMBER GENERATOR */ + /* init c64 randomgenerator */ + if (data->level_rand[level] < 0) + gd_cave_c64_random_set_seed(cave, g_rand_int_range(cave->random, 0, 256), + g_rand_int_range(cave->random, 0, 256)); + else + gd_cave_c64_random_set_seed(cave, 0, data->level_rand[level]); + + /* generate random fill + * start from row 1 (0 skipped), and fill also the borders on left and right hand side, + * as c64 did. this way works the original random generator the right way. + * also, do not fill last row, that is needed for the random seeds to be correct + * after filling! predictable slime will use it. */ + for (y = 1; y < cave->h - 1; y++) + { + for (x = 0; x < cave->w; x++) + { + unsigned int randm; + + if (data->level_rand[level] < 0) + /* use the much better glib random generator */ + randm = g_rand_int_range(cave->random, 0, 256); + else + /* use c64 */ + randm = gd_cave_c64_random(cave); + + element = data->initial_fill; + if (randm < data->random_fill_probability[0]) + element = data->random_fill[0]; + if (randm < data->random_fill_probability[1]) + element = data->random_fill[1]; + if (randm < data->random_fill_probability[2]) + element = data->random_fill[2]; + if (randm < data->random_fill_probability[3]) + element = data->random_fill[3]; + + gd_cave_store_rc(cave, x, y, element, NULL); + } + } + + /* draw initial border */ + for (y = 0; y < cave->h; y++) + { + gd_cave_store_rc(cave, 0, y, cave->initial_border, NULL); + gd_cave_store_rc(cave, cave->w - 1, y, cave->initial_border, NULL); + } + + for (x = 0; x < cave->w; x++) + { + gd_cave_store_rc(cave, x, 0, cave->initial_border, NULL); + gd_cave_store_rc(cave, x, cave->h - 1, cave->initial_border, NULL); + } + } + else + { + /* IF CAVE HAS A MAP, SIMPLY USE IT... no need to fill with random elements */ + + /* initialize c64 predictable random for slime. + the values were taken from afl bd, see docs/internals.txt */ + gd_cave_c64_random_set_seed(cave, 0, 0x1e); + } + + if (data->level_slime_seed_c64[level] != -1) + { + /* if a specific slime seed is requested, change it now. */ + + gd_cave_c64_random_set_seed(cave, + data->level_slime_seed_c64[level] / 256, + data->level_slime_seed_c64[level] % 256); + } + + /* render cave objects above random data or map */ + for (iter = data->objects; iter; iter = g_list_next(iter)) + { + GdObject *object = (GdObject *)iter->data; + + if (object->levels & gd_levels_mask[level]) + gd_cave_draw_object(cave, iter->data, level); + } + + /* check if we use c64 ckdelay or milliseconds for timing */ + if (cave->scheduling == GD_SCHEDULING_MILLISECONDS) + cave->speed = data->level_speed[level]; /* exact timing */ + else + { + /* delay loop based timing... set something for first iteration, + then later it will be calculated */ + cave->speed = 120; + + /* this one may be used by iterate routine to calculate actual delay + if c64scheduling is selected */ + cave->c64_timing = data->level_ckdelay[level]; + } + + gd_cave_correct_visible_size(cave); + + return cave; +} + +/* + render cave at specified level. + copy result to the map; remove objects. + the cave will be map-based. + */ +void gd_flatten_cave(GdCave *cave, const int level) +{ + GdCave *rendered; + + if (cave == NULL) + return; + + /* render cave at specified level to obtain map. seed = 0 */ + rendered = gd_cave_new_rendered(cave, level, 0); + + /* forget old map without objects */ + gd_cave_map_free(cave->map); + + /* copy new map to cave */ + cave->map = gd_cave_map_dup(rendered, map); + gd_cave_free(rendered); + + /* forget objects */ + g_list_foreach(cave->objects, (GFunc) free, NULL); + cave->objects = NULL; +} diff --git a/src/game_bd/bd_caveobject.h b/src/game_bd/bd_caveobject.h new file mode 100644 index 00000000..475255ea --- /dev/null +++ b/src/game_bd/bd_caveobject.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef BD_CAVEOBJECT_H +#define BD_CAVEOBJECT_H + +#include + +#include "bd_cave.h" + + +typedef enum _gd_object_type +{ + NONE, /* this one to be zero. */ + GD_POINT, /* single point of object1 */ + GD_LINE, /* line from (1) to (2) of object1 */ + GD_RECTANGLE, /* rectangle with corners (1) and (2) of object1 */ + GD_FILLED_RECTANGLE, /* rectangle with corners (1) and (2) of object1, filled with object2 */ + GD_RASTER, /* aligned plots */ + GD_JOIN, /* every object1 has an object2 next to it, relative (dx,dy) */ + GD_FLOODFILL_REPLACE, /* fill by replacing */ + GD_FLOODFILL_BORDER, /* fill to another element, a border */ + GD_MAZE, /* maze */ + GD_MAZE_UNICURSAL, /* unicursal maze */ + GD_MAZE_BRAID, /* braid maze */ + GD_RANDOM_FILL, /* random fill */ + GD_COPY_PASTE, /* copy & paste with optional mirror and flip */ +} GdObjectType; + +typedef enum _gd_object_levels +{ + GD_OBJECT_LEVEL1 = 1<<0, + GD_OBJECT_LEVEL2 = 1<<1, + GD_OBJECT_LEVEL3 = 1<<2, + GD_OBJECT_LEVEL4 = 1<<3, + GD_OBJECT_LEVEL5 = 1<<4, + + GD_OBJECT_LEVEL_ALL = (GD_OBJECT_LEVEL1 | + GD_OBJECT_LEVEL2 | + GD_OBJECT_LEVEL3 | + GD_OBJECT_LEVEL4 | + GD_OBJECT_LEVEL5), +} GdObjectLevels; + +extern GdObjectLevels gd_levels_mask[]; + +typedef struct _gd_object +{ + GdObjectType type; /* type */ + GdObjectLevels levels; /* levels to show this object on */ + + int x1, y1; /* (first) coordinate */ + int x2, y2; /* second coordinate */ + int dx, dy; /* distance of elements for raster or join */ + GdElement element, fill_element; /* element type */ + + gint32 seed[5]; /* for maze and random fill */ + int horiz; /* for maze */ + + boolean mirror, flip; /* for copy */ + + boolean c64_random; /* random fill objects: use c64 random generator */ + + GdElement random_fill[4]; + int random_fill_probability[4]; +} GdObject; + +GdObject *gd_object_new_point(GdObjectLevels levels, int x, int y, GdElement elem); +GdObject *gd_object_new_line(GdObjectLevels levels, int x1, int y1, int x2, int y2, GdElement elem); +GdObject *gd_object_new_rectangle(GdObjectLevels levels, int x1, int y1, int x2, int y2, GdElement elem); +GdObject *gd_object_new_filled_rectangle(GdObjectLevels levels, int x1, int y1, int x2, int y2, GdElement elem, GdElement fill_elem); +GdObject *gd_object_new_raster(GdObjectLevels levels, int x1, int y1, int x2, int y2, int dx, int dy, GdElement elem); +GdObject *gd_object_new_join(GdObjectLevels levels, int dx, int dy, GdElement search, GdElement replace); +GdObject *gd_object_new_floodfill_border(GdObjectLevels levels, int x1, int y1, GdElement fill, GdElement border); +GdObject *gd_object_new_floodfill_replace(GdObjectLevels levels, int x1, int y1, GdElement fill, GdElement to_replace); +GdObject *gd_object_new_maze(GdObjectLevels levels, int x1, int y1, int x2, int y2, int wall_w, int path_w, GdElement wall_e, GdElement path_e, int horiz_percent, const gint32 seed[5]); +GdObject *gd_object_new_maze_unicursal(GdObjectLevels levels, int x1, int y1, int x2, int y2, int wall_w, int path_w, GdElement wall_e, GdElement path_e, int horiz_percent, const gint32 seed[5]); +GdObject *gd_object_new_maze_braid(GdObjectLevels levels, int x1, int y1, int x2, int y2, int wall_w, int path_w, GdElement wall_e, GdElement path_e, int horiz_percent, const gint32 seed[5]); +GdObject *gd_object_new_random_fill(GdObjectLevels levels, int x1, int y1, int x2, int y2, const gint32 seed[5], GdElement initial, const GdElement random[4], const gint32 prob[4], GdElement replace_only, boolean c64); +GdObject *gd_object_new_copy_paste(GdObjectLevels levels, int x1, int y1, int x2, int y2, int dx, int dy, boolean mirror, boolean flip); + +void gd_cave_draw_object(GdCave *cave, const GdObject *object, int level); +char *gd_object_get_bdcff(const GdObject *object); +GdObject *gd_object_new_from_string(char *str); + +GdCave *gd_cave_new_rendered(const GdCave *data, const int level, guint32 seed); +void gd_flatten_cave(GdCave *cave, const int level); + +#endif // BD_CAVEOBJECT_H diff --git a/src/game_bd/bd_caveset.c b/src/game_bd/bd_caveset.c new file mode 100644 index 00000000..26fe734b --- /dev/null +++ b/src/game_bd/bd_caveset.c @@ -0,0 +1,684 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + +#include "main_bd.h" + + +/* this stores the caves. */ +GList *gd_caveset; + +/* the data of the caveset: name, highscore, max number of lives, etc. */ +GdCavesetData *gd_caveset_data; + +/* is set to true, when the caveset was edited since the last save. */ +boolean gd_caveset_edited; + +/* last selected-to-play cave */ +int gd_caveset_last_selected; +int gd_caveset_last_selected_level; + +/* list of possible extensions which can be opened */ +char *gd_caveset_extensions[] = +{ + "*.gds", + "*.bd", + "*.bdr", + "*.brc", + + NULL +}; + +#define CAVESET_OFFSET(property) (G_STRUCT_OFFSET(GdCavesetData, property)) + +const GdStructDescriptor gd_caveset_properties[] = +{ + /* default data */ + {"", GD_TAB, 0, N_("Caveset data")}, + {"Name", GD_TYPE_STRING, 0, N_("Name"), CAVESET_OFFSET(name), 1, N_("Name of the game")}, + {"Description", GD_TYPE_STRING, 0, N_("Description"), CAVESET_OFFSET(description), 1, N_("Some words about the game")}, + {"Author", GD_TYPE_STRING, 0, N_("Author"), CAVESET_OFFSET(author), 1, N_("Name of author")}, + {"Date", GD_TYPE_STRING, 0, N_("Date"), CAVESET_OFFSET(date), 1, N_("Date of creation")}, + {"WWW", GD_TYPE_STRING, 0, N_("WWW"), CAVESET_OFFSET(www), 1, N_("Web page or e-mail address")}, + {"Difficulty", GD_TYPE_STRING, 0, N_("Difficulty"), CAVESET_OFFSET(difficulty), 1, N_("Difficulty (informative)")}, + + {"Lives", GD_TYPE_INT, 0, N_("Initial lives"), CAVESET_OFFSET(initial_lives), 1, N_("Number of lives you get at game start."), 3, 9}, + {"Lives", GD_TYPE_INT, 0, N_("Maximum lives"), CAVESET_OFFSET(maximum_lives), 1, N_("Maximum number of lives you can have by collecting bonus points."), 3, 99}, + {"BonusLife", GD_TYPE_INT, 0, N_("Bonus life score"), CAVESET_OFFSET(bonus_life_score), 1, N_("Number of points to collect for a bonus life."), 100, 5000}, + + {"Story", GD_TYPE_LONGSTRING, 0, N_("Story"), CAVESET_OFFSET(story), 1, N_("Long description of the game.")}, + {"Remark", GD_TYPE_LONGSTRING, 0, N_("Remark"), CAVESET_OFFSET(remark), 1, N_("Remark (informative).")}, + + {"TitleScreen", GD_TYPE_LONGSTRING, GD_DONT_SHOW_IN_EDITOR, N_("Title screen"), CAVESET_OFFSET(title_screen), 1, N_("Title screen image")}, + {"TitleScreenScroll", GD_TYPE_LONGSTRING, GD_DONT_SHOW_IN_EDITOR, N_("Title screen, scrolling"), CAVESET_OFFSET(title_screen_scroll), 1, N_("Scrolling background for title screen image")}, + + {NULL}, +}; + +static GdPropertyDefault caveset_defaults[] = +{ + /* default data */ + {CAVESET_OFFSET(initial_lives), 3}, + {CAVESET_OFFSET(maximum_lives), 9}, + {CAVESET_OFFSET(bonus_life_score), 500}, + + {-1}, +}; + +GdCavesetData *gd_caveset_data_new(void) +{ + GdCavesetData *data; + int i; + + data = checked_calloc(sizeof(GdCavesetData)); + + /* create strings */ + for (i = 0; gd_caveset_properties[i].identifier != NULL; i++) + if (gd_caveset_properties[i].type == GD_TYPE_LONGSTRING) + G_STRUCT_MEMBER(GString *, data, gd_caveset_properties[i].offset) = + g_string_new(NULL); + + gd_struct_set_defaults_from_array(data, gd_caveset_properties, caveset_defaults); + + return data; +} + +void gd_caveset_data_free(GdCavesetData *data) +{ + int i; + + /* free strings */ + for (i = 0; gd_caveset_properties[i].identifier != NULL; i++) + if (gd_caveset_properties[i].type == GD_TYPE_LONGSTRING) + g_string_free(G_STRUCT_MEMBER(GString *, data, gd_caveset_properties[i].offset), TRUE); + + free(data); +} + +/****************************************************************************** + * + * Misc caveset functions + * + */ + +/** Clears all caves in the caveset. also to be called at application start */ +void gd_caveset_clear(void) +{ + if (gd_caveset) + { + g_list_foreach(gd_caveset, (GFunc) gd_cave_free, NULL); + g_list_free(gd_caveset); + gd_caveset = NULL; + } + + if (gd_caveset_data) + { + free(gd_caveset_data); + gd_caveset_data = NULL; + } + + /* always newly create this */ + /* create pseudo cave containing default values */ + gd_caveset_data = gd_caveset_data_new(); + gd_strcpy(gd_caveset_data->name, _("New caveset")); +} + +/* return number of caves currently in memory. */ +int gd_caveset_count(void) +{ + return g_list_length(gd_caveset); +} + +/* return index of first selectable cave */ +static int caveset_first_selectable_cave_index(void) +{ + GList *iter; + int i; + + for (i = 0, iter = gd_caveset; iter != NULL; i++, iter = iter->next) + { + GdCave *cave = (GdCave *)iter->data; + + if (cave->selectable) + return i; + } + + Warn("no selectable cave in caveset!"); + + /* and return the first one. */ + return 0; +} + +/* return a cave identified by its index */ +GdCave *gd_return_nth_cave(const int cave) +{ + return g_list_nth_data(gd_caveset, cave); +} + +/* get a selected cave from the loaded caveset (original, unmodified cave) */ +GdCave *gd_get_original_cave_from_caveset(const int cave) +{ + /* get specified cave from caveset already stored in memory */ + GdCave *original_cave = gd_return_nth_cave(cave); + + return original_cave; +} + +/* get a selected cave from the loaded caveset (cave prepared for playing) */ +GdCave *gd_get_prepared_cave_from_caveset(const int cave, const int level) +{ + /* get specified cave from caveset already stored in memory */ + GdCave *original_cave = gd_return_nth_cave(cave); + + /* get prepared cave from original cave */ + GdCave *prepared_cave = gd_get_prepared_cave(original_cave, level); + + return prepared_cave; +} + +/* get a cave prepared for playing from a given original, unmodified cave (with seed) */ +GdCave *gd_get_prepared_cave(const GdCave *original_cave, const int level) +{ + /* get rendered cave using the selected seed for playing */ + GdCave *prepared_cave = gd_cave_new_rendered(original_cave, level, game_bd.random_seed); + + /* initialize some cave variables (like player position) */ + gd_cave_setup_for_game(prepared_cave); + + return prepared_cave; +} + +/* colors: 4: purple 3: ciklamen 2: orange 1: blue 0: green */ +static GdElement brc_import_table[] = +{ + /* 0 */ + O_SPACE, O_DIRT, O_BRICK, O_MAGIC_WALL, O_PRE_OUTBOX, O_OUTBOX, O_UNKNOWN, O_STEEL, + O_H_EXPANDING_WALL, O_H_EXPANDING_WALL /* scanned */, O_FIREFLY_1 /* scanned */, O_FIREFLY_1 /* scanned */, O_FIREFLY_1, O_FIREFLY_2, O_FIREFLY_3, O_FIREFLY_4, + + /* 1 */ + O_BUTTER_1 /* scanned */, O_BUTTER_1 /* scanned */, O_BUTTER_1, O_BUTTER_2, O_BUTTER_3, O_BUTTER_4, O_PLAYER, O_PLAYER /* scanned */, + O_STONE, O_STONE /* scanned */, O_STONE_F, O_STONE_F /* scanned */, O_DIAMOND, O_DIAMOND /* scanned */, O_DIAMOND_F, O_DIAMOND_F /* scanned */, + + /* 2 */ + O_NONE /* WILL_EXPLODE_THING */, O_EXPLODE_1, O_EXPLODE_2, O_EXPLODE_3, O_EXPLODE_4, O_EXPLODE_5, O_NONE /* WILL EXPLODE TO DIAMOND_THING */, O_PRE_DIA_1, + O_PRE_DIA_2, O_PRE_DIA_3, O_PRE_DIA_4, O_PRE_DIA_5, O_AMOEBA, O_AMOEBA /* scanned */, O_SLIME, O_NONE, + + /* 3 */ + O_CLOCK, O_NONE /* clock eaten */, O_INBOX, O_PRE_PL_1, O_PRE_PL_2, O_PRE_PL_3, O_NONE, O_NONE, + O_NONE, O_NONE, O_V_EXPANDING_WALL, O_NONE, O_VOODOO, O_UNKNOWN, O_EXPANDING_WALL, O_EXPANDING_WALL /* sc */, + + /* 4 */ + O_FALLING_WALL, O_FALLING_WALL_F, O_FALLING_WALL_F /* scanned */, O_UNKNOWN, O_ACID, O_ACID /* scanned */, O_NITRO_PACK, O_NITRO_PACK /* scanned */, + O_NITRO_PACK_F, O_NITRO_PACK_F /* scanned */, O_NONE, O_NONE, O_NONE, O_NONE, O_NONE, O_NONE, + + /* 5 */ + O_NONE /* bomb explosion utolso */, O_UNKNOWN, O_NONE /* solid bomb glued */, O_UNKNOWN, O_STONE_GLUED, O_UNKNOWN, O_DIAMOND_GLUED, O_UNKNOWN, + O_UNKNOWN, O_UNKNOWN, O_NONE, O_NONE, O_NONE, O_NONE, O_NONE, O_NONE, + + /* 6 */ + O_ALT_FIREFLY_1 /* scanned */, O_ALT_FIREFLY_1 /* scanned */, O_ALT_FIREFLY_1, O_ALT_FIREFLY_2, O_ALT_FIREFLY_3, O_ALT_FIREFLY_4, O_PLAYER_BOMB, O_PLAYER_BOMB /* scanned */, + O_BOMB, O_BOMB_TICK_1, O_BOMB_TICK_2, O_BOMB_TICK_3, O_BOMB_TICK_4, O_BOMB_TICK_5, O_BOMB_TICK_6, O_BOMB_TICK_7, + + /* 7 */ + O_BOMB_TICK_7, O_BOMB_EXPL_1, O_BOMB_EXPL_2, O_BOMB_EXPL_3, O_BOMB_EXPL_4, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, + O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, +}; + +static GdElement brc_effect_table[] = +{ + O_STEEL, O_DIRT, O_SPACE, O_STONE, O_STONE_F, O_STONE_GLUED, O_DIAMOND, O_DIAMOND_F, O_DIAMOND_GLUED, O_PRE_DIA_1, + O_PLAYER, O_PRE_PL_1, O_PLAYER_BOMB, O_PRE_OUTBOX, O_OUTBOX, O_FIREFLY_1, O_FIREFLY_2, O_FIREFLY_3, O_FIREFLY_4, + O_BUTTER_1, O_BUTTER_2, O_BUTTER_3, O_BUTTER_4, O_BRICK, O_MAGIC_WALL, O_H_EXPANDING_WALL, O_V_EXPANDING_WALL, O_EXPANDING_WALL, + O_FALLING_WALL, O_FALLING_WALL_F, O_AMOEBA, O_SLIME, O_ACID, O_VOODOO, O_CLOCK, O_BOMB, O_UNKNOWN, O_UNKNOWN, O_UNKNOWN, + O_ALT_FIREFLY_1, O_ALT_FIREFLY_2, O_ALT_FIREFLY_3, O_ALT_FIREFLY_4, O_ALT_BUTTER_1, O_ALT_BUTTER_2, O_ALT_BUTTER_3, O_ALT_BUTTER_4, + O_EXPLODE_1, O_BOMB_EXPL_1, O_UNKNOWN, +}; + +static GdColor brc_color_table[] = +{ + 0x518722, 0x3a96fa, 0xdb7618, 0xff3968, + 0x9b5fff, 0x0ee06c, 0xc25ea6, 0xf54826, + 0xf1ff26, +}; + +static GdColor brc_color_table_comp[] = +{ + 0x582287, 0xfa9d39, 0x187ddb, 0x38ffd1, + 0xc1ff5e, 0xe00d81, 0x5dc27a, 0x27d3f5, + 0x3526ff, +}; + +static GdElement brc_effect(guint8 byt) +{ + if (byt >= G_N_ELEMENTS(brc_effect_table)) + { + Warn("invalid element identifier for brc effect: %02x", byt); + + return O_UNKNOWN; + } + + return brc_effect_table[byt]; +} + +static void brc_import(guint8 *data) +{ + int x, y; + int level; + + /* we import 100 caves, and the put them in the correct order. */ + GdCave *imported[100]; + boolean import_effect; + + gd_caveset_clear(); + + /* this is some kind of a version number */ + import_effect = FALSE; + + switch (data[23]) + { + case 0x0: + /* nothing to do */ + break; + + case 0xde: + /* import effects */ + import_effect = TRUE; + break; + + default: + Warn("unknown brc version %02x", data[23]); + break; + } + + for (level = 0; level < 5; level++) + { + int cavenum; + int i; + + for (cavenum = 0; cavenum < 20; cavenum++) + { + GdCave *cave; + + /* 5 levels, 20 caves, 24 bytes - max 40*2 properties for each cave */ + int c = 5 * 20 * 24; + + int datapos = (cavenum * 5 +level) * 24 + 22; + int colind; + + cave = gd_cave_new(); + imported[level * 20 + cavenum] = cave; + + if (cavenum < 16) + g_snprintf(cave->name, sizeof(GdString), "Cave %c/%d", 'A' + cavenum, + level + 1); + else + g_snprintf(cave->name, sizeof(GdString), "Intermission %d/%d", + cavenum - 15, level + 1); + + /* fixed intermission caves; are smaller. */ + if (cavenum >= 16) + { + cave->w = 20; + cave->h = 12; + } + + cave->map = gd_cave_map_new(cave, GdElement); + + for (y = 0; y < cave->h; y++) + { + for (x = 0; x < cave->w; x++) + { + guint8 import; + + import = data[y + level * 24 + cavenum * 24 * 5 + x * 24 * 5 * 20]; + + // if (i == printcave) g_print("%2x", import); + if (import < G_N_ELEMENTS(brc_import_table)) + cave->map[y][x] = brc_import_table[import]; + else + cave->map[y][x] = O_UNKNOWN; + } + } + + for (i = 0; i < 5; i++) + { + cave->level_time[i] = data[0 * c + datapos]; + cave->level_diamonds[i] = data[1 * c + datapos]; + cave->level_magic_wall_time[i] = data[4 * c + datapos]; + cave->level_amoeba_time[i] = data[5 * c + datapos]; + cave->level_amoeba_threshold[i] = data[6 * c + datapos]; + + /* bonus time: 100 was added, so it could also be negative */ + cave->level_bonus_time[i] = (int)data[11 * c + datapos + 1] - 100; + cave->level_hatching_delay_frame[i] = data[10 * c + datapos]; + + /* this was not set in boulder remake. */ + cave->level_speed[i] = 150; + } + + cave->diamond_value = data[2 * c + datapos]; + cave->extra_diamond_value = data[3 * c +datapos]; + + /* BRC PROBABILITIES */ + /* a typical code example: + 46:if (random(slime*4)<4) and (tab[x,y+2] = 0) then + Begin tab[x,y]:=0;col[x,y+2]:=col[x,y];tab[x,y+2]:=27;mat[x,y+2]:=9;Voice4:=2;end; + where slime is the byte loaded from the file as it is. + pascal random function generates a random number between 0..limit-1, inclusive, for random(limit). + + so a random number between 0..limit*4-1 is generated. + for limit=1, 0..3, which is always < 4, so P=1. + for limit=2, 0..7, 0..7 is < 4 in P=50%. + for limit=3, 0..11, is < 4 in P=33%. + So the probability is exactly 100%/limit. + just make sure we do not divide by zero for some broken input. + */ + + if (data[7 * c + datapos] == 0) + Warn("amoeba growth cannot be zero, error at byte %d", + data[7 * c + datapos]); + else + cave->amoeba_growth_prob = 1E6 / data[7 * c + datapos] + 0.5; /* 0.5 for rounding */ + + if (data[8 * c + datapos] == 0) + Warn("amoeba growth cannot be zero, error at byte %d", + data[8 * c + datapos]); + else + cave->amoeba_fast_growth_prob = 1E6 / data[8 * c + datapos] + 0.5; /* 0.5 for rounding */ + + cave->slime_predictable = FALSE; + + for (i = 0; i < 5; i++) + cave->level_slime_permeability[i] = 1E6 / data[9 * c + datapos] + 0.5; /* 0.5 for rounding */ + + /* probability -> *1E6 */ + cave->acid_spread_ratio = 1E6 / data[10 * c + datapos] + 0.5; + + /* br only allowed values 1..8 in here, but works the same way. prob -> *1E6 */ + cave->pushing_stone_prob = 1E6 / data[11 * c + datapos] + 0.5; + + cave->magic_wall_stops_amoeba = (data[12 * c + datapos + 1] != 0); + cave->intermission = (cavenum >= 16 || data[14 * c + datapos + 1] != 0); + + /* colors */ + colind = data[31 * c + datapos] % G_N_ELEMENTS(brc_color_table); + cave->colorb = 0x000000; /* fixed rgb black */ + cave->color0 = 0x000000; /* fixed rgb black */ + cave->color1 = brc_color_table[colind]; + cave->color2 = brc_color_table_comp[colind]; /* complement */ + cave->color3 = 0xffffff; /* white for brick */ + cave->color4 = 0xe5ad23; /* fixed for amoeba */ + cave->color5 = 0x8af713; /* fixed for slime */ + + if (import_effect) + { + cave->amoeba_enclosed_effect = brc_effect(data[14 * c + datapos + 1]); + cave->amoeba_too_big_effect = brc_effect(data[15 * c + datapos + 1]); + cave->explosion_effect = brc_effect(data[16 * c + datapos + 1]); + cave->bomb_explosion_effect = brc_effect(data[17 * c + datapos + 1]); + + /* 18 solid bomb explode to */ + cave->diamond_birth_effect = brc_effect(data[19 * c + datapos + 1]); + cave->stone_bouncing_effect = brc_effect(data[20 * c + datapos + 1]); + cave->diamond_bouncing_effect = brc_effect(data[21 * c + datapos + 1]); + cave->magic_diamond_to = brc_effect(data[22 * c + datapos + 1]); + cave->acid_eats_this = brc_effect(data[23 * c + datapos + 1]); + + /* slime eats: + (diamond,boulder,bomb), + (diamond,boulder), + (diamond,bomb), + (boulder,bomb) */ + cave->amoeba_enclosed_effect = brc_effect(data[14 * c + datapos + 1]); + } + } + } + + /* put them in the caveset - take correct order into consideration. */ + for (level = 0; level < 5; level++) + { + int cavenum; + + for (cavenum = 0; cavenum < 20; cavenum++) + { + static const int reorder[] = + { + 0, 1, 2, 3, 16, 4, 5, 6, 7, 17, 8, 9, 10, 11, 18, 12, 13, 14, 15, 19 + }; + GdCave *cave = imported[level * 20 + reorder[cavenum]]; + boolean only_dirt; + int x, y; + + /* check if cave contains only dirt. + that is an empty cave, and do not import. */ + only_dirt = TRUE; + + for (y = 1; y < cave->h - 1 && only_dirt; y++) + for (x = 1; x < cave->w - 1 && only_dirt; x++) + if (cave->map[y][x] != O_DIRT) + only_dirt = FALSE; + + /* append to caveset or forget it. */ + if (!only_dirt) + gd_caveset = g_list_append(gd_caveset, cave); + else + gd_cave_free(cave); + } + } +} + +static void caveset_name_set_from_filename(const char *filename) +{ + char *name; + char *c; + + /* make up a caveset name from the filename. */ + name = g_path_get_basename(filename); + gd_strcpy(gd_caveset_data->name, name); + free(name); + + /* convert underscores to spaces */ + while ((c = strchr (gd_caveset_data->name, '_')) != NULL) + *c = ' '; + + /* remove extension */ + if ((c = strrchr (gd_caveset_data->name, '.')) != NULL) + *c = 0; +} + +/* Load caveset from file. + Loads the caveset from a file. + + File type is autodetected by extension. + param filename: Name of file. + result: FALSE if failed +*/ +boolean gd_caveset_load_from_file(char *filename) +{ + GError *error = NULL; + gsize length; + char *buf; + boolean read; + GList *new_caveset; + struct stat st; + + if (g_stat(filename, &st) != 0) + { + Warn("cannot stat() file"); + + return FALSE; + } + + if (st.st_size > 1048576) + { + Warn("file bigger than 1MiB, refusing to load"); + + return FALSE; + } + + read = g_file_get_contents (filename, &buf, &length, &error); + if (!read) + { + Warn("%s", error->message); + + g_error_free(error); + + return FALSE; + } + + if (strSuffix(filename, ".brc") || + strSuffix(filename, ".BRC")) + { + /* loading a boulder remake file */ + if (length != 96000) + { + Warn("BRC files must be 96000 bytes long"); + + return FALSE; + } + } + + if (strSuffix(filename, ".brc") || + strSuffix(filename, "*.BRC")) + { + brc_import((guint8 *) buf); + gd_caveset_edited = FALSE; /* newly loaded cave is not edited */ + gd_caveset_last_selected = caveset_first_selectable_cave_index(); + gd_caveset_last_selected_level = 0; + free(buf); + caveset_name_set_from_filename(filename); + + return TRUE; + } + + /* BDCFF */ + if (gd_caveset_imported_get_format((guint8 *) buf) == GD_FORMAT_UNKNOWN) + { + /* try to load as bdcff */ + boolean result; + + /* bdcff: start another function */ + result = gd_caveset_load_from_bdcff(buf); + + /* newly loaded file is not edited. */ + gd_caveset_edited = FALSE; + + gd_caveset_last_selected = caveset_first_selectable_cave_index(); + gd_caveset_last_selected_level = 0; + free(buf); + + return result; + } + + /* try to load as a binary file, as we know the format */ + new_caveset = gd_caveset_import_from_buffer ((guint8 *) buf, length); + free(buf); + + /* if unable to load, exit here. error was reported by import_from_buffer() */ + if (!new_caveset) + return FALSE; + + /* no serious error :) */ + + /* only clear caveset here. if file read was unsuccessful, caveset remains in memory. */ + gd_caveset_clear(); + + gd_caveset = new_caveset; + + /* newly loaded cave is not edited */ + gd_caveset_edited = FALSE; + + gd_caveset_last_selected = caveset_first_selectable_cave_index(); + gd_caveset_last_selected_level = 0; + caveset_name_set_from_filename(filename); + + return TRUE; +} + +int gd_cave_check_replays(GdCave *cave, boolean report, boolean remove, boolean repair) +{ + GList *riter; + int wrong = 0; + + riter = cave->replays; + while (riter != NULL) + { + GdReplay *replay = (GdReplay *)riter->data; + guint32 checksum; + GdCave *rendered; + GList *next = riter->next; + + rendered = gd_cave_new_rendered(cave, replay->level, replay->seed); + checksum = gd_cave_adler_checksum(rendered); + gd_cave_free(rendered); + + replay->wrong_checksum = FALSE; + + /* count wrong ones... the checksum might be changed later to "repair" */ + if (replay->checksum != 0 && checksum != replay->checksum) + wrong++; + + if (replay->checksum == 0 || repair) + { + /* if no checksum found, add one. or if repair requested, overwrite old one. */ + replay->checksum = checksum; + } + else + { + /* if has a checksum, compare with this one. */ + if (replay->checksum != checksum) + { + replay->wrong_checksum = TRUE; + + if (report) + Warn("%s: replay played by %s at %s has wrong checksum", + cave->name, replay->player_name, replay->date); + + if (remove) + { + /* may remove */ + cave->replays = g_list_remove_link(cave->replays, riter); + gd_replay_free(replay); + } + } + } + + /* advance to next list item which we remembered. the current one might have been deleted */ + riter = next; + } + + return wrong; +} + +boolean gd_caveset_has_replays(void) +{ + GList *citer; + + /* for all caves */ + for (citer = gd_caveset; citer != NULL; citer = citer->next) + { + GdCave *cave = (GdCave *)citer->data; + + if (cave->replays) + return TRUE; + } + + /* if neither of the caves had a replay, */ + return FALSE; +} diff --git a/src/game_bd/bd_caveset.h b/src/game_bd/bd_caveset.h new file mode 100644 index 00000000..94ec4378 --- /dev/null +++ b/src/game_bd/bd_caveset.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef BD_CAVESET_H +#define BD_CAVESET_H + +#include + +#include "main_bd.h" + + +typedef struct _gd_caveset_data +{ + GdString name; /* Name of caveset */ + GdString description; /* Some words about the caveset */ + GdString author; /* Author */ + GdString difficulty; /* difficulty of the caveset, for info purposes */ + GdString www; /* link to author's webpage */ + GdString date; /* date of creation */ + + GString *story; /* story for the caves */ + GString *remark; /* notes about the game */ + + GString *title_screen; /* base64-encoded title screen image */ + GString *title_screen_scroll; /* scrolling background for title screen image */ + + GdString charset; /* these are not used by gdash */ + GdString fontset; + + /* these are only for a game. */ + int initial_lives; /* initial lives at game start */ + int maximum_lives; /* maximum lives */ + int bonus_life_score; /* bonus life / number of points */ + + /* and this one the highscores */ + GdHighScore highscore[GD_HIGHSCORE_NUM]; +} GdCavesetData; + +extern const GdStructDescriptor gd_caveset_properties[]; + +extern GdCavesetData *gd_caveset_data; +extern GList *gd_caveset; +extern boolean gd_caveset_edited; +extern int gd_caveset_last_selected; +extern int gd_caveset_last_selected_level; + +extern char *gd_caveset_extensions[]; + +/* #included cavesets; configdir passed to look for .hsc file */ +boolean gd_caveset_load_from_internal(int caveset, const char *configdir); +const gchar **gd_caveset_get_internal_game_names(void); + +/* caveset load from file */ +boolean gd_caveset_load_from_file(char *filename); +/* caveset save to bdcff file */ +boolean gd_caveset_save(const char *filename); + +/* misc caveset functions */ +int gd_caveset_count(void); +void gd_caveset_clear(void); +GdCave *gd_return_nth_cave(const int cave); + +GdCave *gd_get_original_cave_from_caveset(const int cave); +GdCave *gd_get_prepared_cave_from_caveset(const int cave, const int level); +GdCave *gd_get_prepared_cave(const GdCave *cave, const int level); + +/* highscore in config directory */ +void gd_save_highscore(const char* directory); +boolean gd_load_highscore(const char *directory); + +GdCavesetData *gd_caveset_data_new(void); +void gd_caveset_data_free(GdCavesetData *data); + +/* check replays and optionally remove */ +int gd_cave_check_replays(GdCave *cave, boolean report, boolean remove, boolean repair); + +boolean gd_caveset_has_replays(void); + +#endif // BD_CAVESET_H diff --git a/src/game_bd/bd_gameplay.c b/src/game_bd/bd_gameplay.c new file mode 100644 index 00000000..8145cce6 --- /dev/null +++ b/src/game_bd/bd_gameplay.c @@ -0,0 +1,704 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include "main_bd.h" + + +/* universal settings */ +static boolean gd_no_invisible_outbox = FALSE; + + +void gd_game_free(GdGame *game) +{ + /* stop sounds */ + gd_sound_off(); + + if (game->element_buffer) + gd_cave_map_free(game->element_buffer); + if (game->gfx_buffer) + gd_cave_map_free(game->gfx_buffer); + + game->player_lives = 0; + + if (game->cave) + gd_cave_free(game->cave); + + /* if we recorded some replays during this run, we check them. + we remove those which are too short */ + if (game->replays_recorded) + { + GList *citer; + + /* check all caves */ + for (citer = gd_caveset; citer != NULL; citer = citer->next) + { + GdCave *cave = (GdCave *)citer->data; + GList *riter; + + /* check replays of all caves */ + for (riter = cave->replays; riter != NULL; ) + { + GdReplay *replay = (GdReplay *)riter->data; + + /* remember next iter, as we may delete the current */ + GList *nextrep = riter->next; + + /* if we recorded this replay now, and it is too short, we delete it */ + /* but do not delete successful ones! */ + if (g_list_find(game->replays_recorded, replay) && + (replay->movements->len < 16) && + (!replay->success)) + { + /* delete from list */ + cave->replays = g_list_delete_link(cave->replays, riter); + + /* also free replay */ + gd_replay_free(replay); + } + + riter = nextrep; + } + } + + /* free the list of newly recorded replays, as we checked them */ + g_list_free(game->replays_recorded); + game->replays_recorded = NULL; + } + + free(game); +} + +/* add bonus life. if sound enabled, play sound, too. */ +static void add_bonus_life(GdGame *game, boolean inform_user) +{ + /* only inform about bonus life when playing a game */ + /* or when testing the cave (so the user can see that a bonus life can be earned in that cave */ + if (game->type == GD_GAMETYPE_NORMAL || + game->type == GD_GAMETYPE_TEST) + { + if (inform_user) + { + gd_sound_play_bonus_life(); + game->bonus_life_flash = 100; + } + } + + /* really increment number of lifes? only in a real game, nowhere else. */ + if (game->player_lives && + game->player_lives < gd_caveset_data->maximum_lives) + { + /* only add a life, if lives is > 0. + lives == 0 is a test run or a snapshot, no bonus life then. */ + /* also, obey max number of bonus lives. */ + game->player_lives++; + } +} + +/* increment score of player. + flash screen if bonus life +*/ +static void increment_score(GdGame *game, int increment) +{ + int i; + + i = game->player_score / gd_caveset_data->bonus_life_score; + game->player_score += increment; + game->cave_score += increment; + + /* also record to replay */ + if (game->replay_record) + game->replay_record->score += increment; + + /* if score crossed bonus_life_score point boundary, player won a bonus life */ + if (game->player_score / gd_caveset_data->bonus_life_score > i) + add_bonus_life(game, TRUE); +} + +/* do the things associated with loading a new cave. function creates gfx buffer and the like. */ +static void load_cave(GdGame *game) +{ + int x, y; + + /* delete element buffer */ + if (game->element_buffer) + gd_cave_map_free(game->element_buffer); + game->element_buffer = NULL; + + /* delete gfx buffer */ + if (game->gfx_buffer) + gd_cave_map_free(game->gfx_buffer); + game->gfx_buffer = NULL; + + /* load the cave */ + game->cave_score = 0; + + /* delete previous cave */ + gd_cave_free(game->cave); + + if (native_bd_level.loaded_from_caveset) + game->original_cave = gd_get_original_cave_from_caveset(game->cave_num); + else + game->original_cave = native_bd_level.cave; + + game->cave = gd_get_prepared_cave(game->original_cave, game->level_num); + + if (game->cave->intermission && game->cave->intermission_instantlife) + add_bonus_life(game, FALSE); + + game->milliseconds_anim = 0; + game->milliseconds_game = 0; /* set game timer to zero, too */ + + /* create new element buffer */ + game->element_buffer = gd_cave_map_new(game->cave, int); + + for (y = 0; y < game->cave->h; y++) + for (x = 0; x < game->cave->w; x++) + game->element_buffer[y][x] = O_NONE; + + /* create new gfx buffer */ + game->gfx_buffer = gd_cave_map_new(game->cave, int); + + for (y = 0; y < game->cave->h; y++) + for (x = 0; x < game->cave->w; x++) + game->gfx_buffer[y][x] = -1; /* fill with "invalid" */ +} + +GdCave *gd_create_snapshot(GdGame *game) +{ + GdCave *snapshot; + g_return_val_if_fail (game->cave != NULL, NULL); + + /* make an exact copy */ + snapshot = gd_cave_new_from_cave(game->cave); + + return snapshot; +} + +/* this starts a new game */ +GdGame *gd_game_new(const int cave, const int level) +{ + GdGame *game; + + game = checked_calloc(sizeof(GdGame)); + + game->cave_num = cave; + game->level_num = level; + + game->player_lives = gd_caveset_data->initial_lives; + game->player_score = 0; + + game->player_move = GD_MV_STILL; + game->player_move_stick = FALSE; + game->player_fire = FALSE; + + game->type = GD_GAMETYPE_NORMAL; + game->state_counter = GAME_INT_LOAD_CAVE; + + game->show_story = TRUE; + + return game; +} + +/* starts a new snapshot playing */ +GdGame *gd_game_new_replay(GdCave *cave, GdReplay *replay) +{ + GdGame *game; + + game = checked_calloc(sizeof(GdGame)); + + gd_strcpy(game->player_name, ""); + + game->player_lives = 0; + game->player_score = 0; + + game->player_move = GD_MV_STILL; + game->player_move_stick = FALSE; + game->player_fire = FALSE; + + game->original_cave = cave; + game->replay_from = replay; + + game->type = GD_GAMETYPE_REPLAY; + game->state_counter = GAME_INT_LOAD_CAVE; + + return game; +} + +static void iterate_cave(GdGame *game, GdDirection player_move, boolean fire) +{ + boolean suicide = FALSE; + + /* if we are playing a replay, but the user intervents, continue as a snapshot. */ + /* do not trigger this for fire, as it would not be too intuitive. */ + if (game->type == GD_GAMETYPE_REPLAY) + { + if (player_move != GD_MV_STILL) + { + game->type = GD_GAMETYPE_CONTINUE_REPLAY; + game->replay_from = NULL; + } + } + + /* ANYTHING EXCEPT A TIMEOUT, WE ITERATE THE CAVE */ + if (game->cave->player_state != GD_PL_TIMEOUT) + { + /* IF PLAYING FROM REPLAY, OVERWRITE KEYPRESS VARIABLES FROM REPLAY */ + if (game->type == GD_GAMETYPE_REPLAY) + { + boolean result; + + /* if the user does touch the keyboard, we immediately exit replay, + and he can continue playing */ + result = gd_replay_get_next_movement(game->replay_from, &player_move, &fire, &suicide); + /* if could not get move from snapshot, continue from keyboard input. */ + if (!result) + game->replay_no_more_movements++; + + /* if no more available movements, and the user does not do anything, + we cover cave and stop game. */ + if (game->replay_no_more_movements > 15) + game->state_counter = GAME_INT_COVER_START; + } + + if (TapeIsPlaying_ReplayBD()) + { + byte *action_rnd = TapePlayAction_BD(); + + if (action_rnd != NULL) + { + int action_bd = map_action_RND_to_BD(action_rnd[0]); + + player_move = (action_bd & GD_REPLAY_MOVE_MASK); + fire = (action_bd & GD_REPLAY_FIRE_MASK) != 0; + } + } + + /* iterate cave */ + gd_cave_iterate(game->cave, player_move, fire, suicide); + + if (game->replay_record) + gd_replay_store_movement(game->replay_record, player_move, fire, suicide); + + if (game->cave->score) + increment_score(game, game->cave->score); + + gd_sound_play_cave(game->cave); + } + + if (game->cave->player_state == GD_PL_EXITED) + { + if (game->cave->intermission && + game->cave->intermission_rewardlife && + game->player_lives != 0) + { + /* one life extra for completing intermission */ + add_bonus_life(game, FALSE); + } + + if (game->replay_record) + game->replay_record->success = TRUE; + + /* start adding points for remaining time */ + game->state_counter = GAME_INT_CHECK_BONUS_TIME; + gd_cave_clear_sounds(game->cave); + + /* play cave finished sound */ + gd_sound_play(game->cave, GD_S_FINISHED, O_NONE, -1, -1); + gd_sound_play_cave(game->cave); + } + + if (game->cave->player_state == GD_PL_DIED || + game->cave->player_state == GD_PL_TIMEOUT) + { + game_bd.game_over = TRUE; + game_bd.cover_screen = TRUE; + } +} + +static GdGameState gd_game_main_int(GdGame *game, boolean allow_iterate, boolean fast_forward) +{ + int millisecs_elapsed = 20; + boolean frame; /* set to true, if this will be an animation frame */ + GdGameState return_state; + int counter_next; + int x, y; + + counter_next = GAME_INT_INVALID; + return_state = GD_GAME_INVALID_STATE; + game->milliseconds_anim += millisecs_elapsed; /* keep track of time */ + frame = FALSE; /* set to true, if this will be an animation frame */ + + if (game->milliseconds_anim >= 40) + { + frame = TRUE; + game->milliseconds_anim -= 40; + } + + /* cannot be less than uncover start. */ + if (game->state_counter < GAME_INT_LOAD_CAVE) + { + ; + } + else if (game->state_counter == GAME_INT_LOAD_CAVE) + { + /* do the things associated with loading a new cave. function creates gfx buffer and the like. */ + load_cave(game); + + return_state = GD_GAME_NOTHING; + counter_next = GAME_INT_SHOW_STORY; + } + else if (game->state_counter == GAME_INT_SHOW_STORY) + { + /* for normal game, every cave can have a long string of description/story. show that. */ + + /* if we have a story... */ +#if 0 + if (game->show_story && game->original_cave && game->original_cave->story->len != 0) + Info("Cave Story: %s", game->original_cave->story->str); +#endif + + counter_next = GAME_INT_START_UNCOVER; + return_state = GD_GAME_NOTHING; + } + else if (game->state_counter == GAME_INT_START_UNCOVER) + { + /* the very beginning. */ + + /* cover all cells of cave */ + for (y = 0; y < game->cave->h; y++) + for (x = 0; x < game->cave->w; x++) + game->cave->map[y][x] |= COVERED; + + counter_next = game->state_counter + 1; + + /* very important: tell the caller that we loaded a new cave. */ + /* size of the cave might be new, colors might be new, and so on. */ + return_state = GD_GAME_CAVE_LOADED; + } + else if (game->state_counter < GAME_INT_UNCOVER_ALL) + { + /* uncover animation */ + + /* to play cover sound */ + gd_sound_play(game->cave, GD_S_COVER, O_COVERED, -1, -1); + gd_sound_play_cave(game->cave); + + counter_next = game->state_counter; + + if (frame) + { + int j; + + /* original game uncovered one cell per line each frame. + * we have different cave sizes, so uncover width * height / 40 random + * cells each frame. (original was width = 40). + * this way the uncovering is the same speed also for intermissions. */ + for (j = 0; j < game->cave->w * game->cave->h / 40; j++) + { + y = g_random_int_range(0, game->cave->h); + x = g_random_int_range(0, game->cave->w); + + game->cave->map[y][x] &= ~COVERED; + } + + counter_next++; /* as we did something, advance the counter. */ + } + + return_state = GD_GAME_NOTHING; + } + else if (game->state_counter == GAME_INT_UNCOVER_ALL) + { + /* time to uncover the whole cave. */ + for (y = 0; y < game->cave->h; y++) + for (x = 0; x < game->cave->w; x++) + game->cave->map[y][x] &= ~COVERED; + + /* to stop uncover sound. */ + gd_cave_clear_sounds(game->cave); + gd_sound_play_cave(game->cave); + + counter_next = GAME_INT_CAVE_RUNNING; + return_state = GD_GAME_NOTHING; + } + else if (game->state_counter == GAME_INT_CAVE_RUNNING) + { + /* normal. */ + int cavespeed; + + if (!fast_forward) + cavespeed = game->cave->speed; /* cave speed in ms, like 175ms/frame */ + else + cavespeed = 40; /* if fast forward, ignore cave speed, and go as 25 iterations/sec */ + + /* ITERATION - cave is running. */ + + /* normally nothing happes. but if we iterate, this might change. */ + return_state = GD_GAME_NOTHING; + + /* if allowing cave movements, add elapsed time to timer. and then we can check what to do. */ + if (allow_iterate) + game->milliseconds_game += millisecs_elapsed; + + if (game->milliseconds_game >= cavespeed) + { + GdPlayerState pl; + + game->milliseconds_game -= cavespeed; + pl = game->cave->player_state; + + iterate_cave(game, game->player_move, game->player_fire); + + if (game->player_move == GD_MV_STILL) + { + game->player_move_stick = FALSE; + } + else + { + game->player_move_stick = TRUE; + game->player_move = GD_MV_STILL; + } + + /* as we iterated, the score and the like could have been changed. */ + return_state = GD_GAME_LABELS_CHANGED; + + /* and if the cave timeouted at this moment, that is a special case. */ + if (pl != GD_PL_TIMEOUT && game->cave->player_state == GD_PL_TIMEOUT) + return_state = GD_GAME_TIMEOUT_NOW; + } + + /* do not change counter state, as it is set by iterate_cave() */ + counter_next = game->state_counter; + } + else if (game->state_counter == GAME_INT_CHECK_BONUS_TIME) + { + /* before covering, we check for time bonus score */ + if (frame) + { + /* if time remaining, bonus points are added. do not start animation yet. */ + if (game->cave->time > 0) + { + /* subtract number of "milliseconds" - nothing to do with gameplay->millisecs! */ + game->cave->time -= game->cave->timing_factor; + + /* higher levels get more bonus points per second remained */ + increment_score(game, game->cave->timevalue); + + /* if much time (> 60s) remained, fast counter :) */ + if (game->cave->time > 60 * game->cave->timing_factor) + { + /* decrement by nine each frame, so it also looks like a fast counter. 9 is 8 + 1! */ + game->cave->time -= 8 * game->cave->timing_factor; + increment_score(game, game->cave->timevalue * 8); + } + + /* just to be neat */ + if (game->cave->time < 0) + game->cave->time = 0; + + counter_next = game->state_counter; /* do not change yet */ + } + else + { + game_bd.level_solved = TRUE; + game_bd.cover_screen = TRUE; + + /* if no more points, start waiting a bit, and later start covering. */ + counter_next = GAME_INT_WAIT_BEFORE_COVER; + } + + if (game->cave->time / game->cave->timing_factor > 8) + gd_sound_play(game->cave, GD_S_FINISHED, O_NONE, -1, -1); /* play cave finished sound */ + + /* play bonus sound */ + gd_cave_set_seconds_sound(game->cave); + gd_sound_play_cave(game->cave); + + return_state = GD_GAME_LABELS_CHANGED; + } + else + { + return_state = GD_GAME_NOTHING; + + /* do not change counter state, as it is set by iterate_cave() */ + counter_next = game->state_counter; + } + } + else if (game->state_counter == GAME_INT_WAIT_BEFORE_COVER) + { + /* after adding bonus points, we wait some time before starting to cover. + this is the FIRST frame... so we check for game over and maybe jump there */ + /* if no more lives, game is over. */ + counter_next = game->state_counter; + + if (game->type == GD_GAMETYPE_NORMAL && game->player_lives == 0) + return_state = GD_GAME_NO_MORE_LIVES; + else + return_state = GD_GAME_NOTHING; + } + else if (game->state_counter > GAME_INT_WAIT_BEFORE_COVER && + game->state_counter < GAME_INT_COVER_START) + { + /* after adding bonus points, we wait some time before starting to cover. + ... and the other frames. */ + counter_next = game->state_counter; + if (frame) + counter_next++; /* 40 ms elapsed, advance counter */ + + return_state = GD_GAME_NOTHING; + } + else if (game->state_counter == GAME_INT_COVER_START) + { + /* starting to cover. start cover sound. */ + + gd_cave_clear_sounds(game->cave); + gd_sound_play(game->cave, GD_S_COVER, O_COVERED, -1, -1); + + /* to play cover sound */ + gd_sound_play_cave(game->cave); + + counter_next = game->state_counter + 1; + return_state = GD_GAME_NOTHING; + } + else if (game->state_counter > GAME_INT_COVER_START && + game->state_counter < GAME_INT_COVER_ALL) + { + /* covering. */ + gd_sound_play(game->cave, GD_S_COVER, O_COVERED, -1, -1); + + counter_next = game->state_counter; + + if (frame) + { + int j; + + counter_next++; /* 40 ms elapsed, doing cover: advance counter */ + + /* covering eight times faster than uncovering. */ + for (j = 0; j < game->cave->w * game->cave->h * 8 / 40; j++) + game->cave->map[g_random_int_range(0, game->cave->h)][g_random_int_range (0, game->cave->w)] |= COVERED; + } + + return_state = GD_GAME_NOTHING; + } + else if (game->state_counter == GAME_INT_COVER_ALL) + { + /* cover all */ + for (y = 0; y < game->cave->h; y++) + for (x = 0; x < game->cave->w; x++) + game->cave->map[y][x] |= COVERED; + + counter_next = game->state_counter + 1; + return_state = GD_GAME_NOTHING; + + /* to stop uncover sound. */ + gd_cave_clear_sounds(game->cave); + gd_sound_play_cave(game->cave); + } + else + { + /* cover all + 1 */ + + /* if this is a normal game: */ + if (game->type == GD_GAMETYPE_NORMAL) + { + if (game->player_lives != 0) + return_state = GD_GAME_NOTHING; /* and go to next level */ + else + return_state = GD_GAME_GAME_OVER; + } + else + { + /* for snapshots and replays and the like, this is the end. */ + return_state = GD_GAME_STOP; + } + } + + /* draw the cave */ + if (frame) + { + if (game->bonus_life_flash) /* bonus life - frames */ + game->bonus_life_flash--; + + game->animcycle = (game->animcycle + 1) % 8; + } + + /* always render the cave to the gfx buffer; + however it may do nothing if animcycle was not changed. */ + if (game->element_buffer && game->gfx_buffer) + gd_drawcave_game(game->cave, game->element_buffer, game->gfx_buffer, + game->bonus_life_flash != 0, game->animcycle, gd_no_invisible_outbox); + + game->state_counter = counter_next; + + return return_state; +} + +void play_game_func(GdGame *game, int action) +{ + GdGameState state; + boolean move_up = ((action & JOY_UP) != 0); + boolean move_down = ((action & JOY_DOWN) != 0); + boolean move_left = ((action & JOY_LEFT) != 0); + boolean move_right = ((action & JOY_RIGHT) != 0); + boolean fire = ((action & (JOY_BUTTON_1 | JOY_BUTTON_2)) != 0); + + if (game->player_move_stick || move_up || move_down || move_left || move_right) // no "fire"! + { + /* get movement */ + game->player_move = gd_direction_from_keypress(move_up, move_down, move_left, move_right); + + /* when storing last action, only update fire action with direction */ + /* (prevents clearing direction if snapping stopped before action is performed) */ + game->player_fire = fire; + } + + /* if no direction was stored before, allow setting fire to current state */ + if (game->player_move == GD_MV_STILL) + game->player_fire = fire; + + /* tell the interrupt "20ms has passed" */ + state = gd_game_main_int(game, !game->out_of_window, gd_keystate[SDL_SCANCODE_F]); + + /* state of game, returned by gd_game_main_int */ + switch (state) + { + case GD_GAME_CAVE_LOADED: + /* select colors, prepare drawing etc. */ + gd_scroll_to_origin(); + + /* fill whole screen with black - cave might be smaller than previous! */ + FillRectangle(gd_screen_bitmap, 0, 0, SXSIZE, SYSIZE, BLACK_PIXEL); + break; + + default: + break; + } + + /* for the sdl version, it seems nicer if we first scroll, and then draw. */ + /* scrolling for the sdl version will merely invalidate the whole gfx buffer. */ + /* if drawcave was before scrolling, it would draw, scroll would invalidate, + and then it should be drawn again */ + /* only do the drawing if the cave already exists. */ + if (game->cave && game->element_buffer && game->gfx_buffer) + { + /* if fine scrolling, scroll at 50hz. if not, only scroll at every second call, so 25hz. */ + /* do the scrolling. scroll exactly, if player is not yet alive */ + game->out_of_window = gd_scroll(game, game->cave->player_state == GD_PL_NOT_YET, FALSE); + } +} diff --git a/src/game_bd/bd_graphics.c b/src/game_bd/bd_graphics.c new file mode 100644 index 00000000..b68459bf --- /dev/null +++ b/src/game_bd/bd_graphics.c @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include "main_bd.h" + + +// !!! (can be removed later) !!! +#define DO_GFX_SANITY_CHECK TRUE + +/* distance to screen edge in cells when scrolling the screen */ +#define SCROLL_EDGE_DISTANCE 4 + +/* these can't be larger than 31, or they mess up utf8 coding or are the same + as some ascii letter */ +#define GD_DOWN_CHAR 1 +#define GD_LEFT_CHAR 2 +#define GD_UP_CHAR 3 +#define GD_RIGHT_CHAR 4 + +#define GD_BALL_CHAR 5 +#define GD_UNCHECKED_BOX_CHAR 6 +#define GD_CHECKED_BOX_CHAR 7 + +#define GD_PLAYER_CHAR 8 +#define GD_DIAMOND_CHAR 9 +#define GD_SKELETON_CHAR 11 +#define GD_KEY_CHAR 12 +#define GD_COMMENT_CHAR 13 + +/* screen area */ +Bitmap *gd_screen_bitmap = NULL; + +static int play_area_w = 0; +static int play_area_h = 0; + +static int scroll_x, scroll_y; + +/* quit, global variable which is set to true if the application should quit */ +boolean gd_quit = FALSE; + +const guint8 *gd_keystate; + +static int cell_size = 0; + +/* graphic info for game objects/frames and players/actions/frames */ +struct GraphicInfo_BD graphic_info_bd_object[O_MAX_ALL][8]; + +void set_cell_size(int s) +{ + cell_size = s; +} + +void set_play_area(int w, int h) +{ + play_area_w = w; + play_area_h = h; +} + +void gd_init_keystate(void) +{ + set_play_area(SXSIZE, SYSIZE); + + gd_keystate = SDL_GetKeyboardState(NULL); +} + +/* + logical_size: logical pixel size of playfield, usually larger than the screen. + physical_size: visible part. (remember: player_x-x1!) + + center: the coordinates to scroll to. + exact: scroll exactly + start: start scrolling if difference is larger than + to: scroll to, if started, until difference is smaller than + current + + desired: the function stores its data here + speed: the function stores its data here + + cell_size: size of one cell. used to determine if the play field is only a + slightly larger than the screen, in that case no scrolling is desirable +*/ +static boolean cave_scroll(int logical_size, int physical_size, int center, boolean exact, + int *current, int *desired, int speed) +{ + int max = MAX(0, logical_size - physical_size); + int edge_distance = SCROLL_EDGE_DISTANCE; + int cell_size = TILESIZE_VAR; + boolean changed = FALSE; + + /* start scrolling when player reaches certain distance to screen edge */ + int start = physical_size / 2 - cell_size * edge_distance; + + /* scroll so that the player is at the center; the allowed difference is this */ + int to = cell_size; + + /* if cave size smaller than the screen, no scrolling req'd */ + if (logical_size < physical_size) + { + *desired = 0; + + if (*current != 0) + { + *current = 0; + changed = TRUE; + } + + return changed; + } + + if (logical_size <= physical_size + cell_size) + { + /* if cave size is only a slightly larger than the screen, also no scrolling */ + /* scroll to the middle of the cell */ + *desired = max / 2; + } + else + { + if (exact) + { + /* if exact scrolling, just go exactly to the center. */ + *desired = center; + } + else + { + /* hystheresis function. + * when scrolling left, always go a bit less left than player being at the middle. + * when scrolling right, always go a bit less to the right. */ + if (*current < center - start) + *desired = center - to; + if (*current > center + start) + *desired = center + to; + } + } + + *desired = MIN(MAX(0, *desired), max); + + if (*current < *desired) + { + *current = MIN(*current + speed, *desired); + + changed = TRUE; + } + + if (*current > *desired) + { + *current = MAX(*current - speed, *desired); + + changed = TRUE; + } + + return changed; +} + +/* just set current viewport to upper left. */ +void gd_scroll_to_origin(void) +{ + scroll_x = 0; + scroll_y = 0; +} + +int get_scroll_x(void) +{ + return scroll_x / cell_size; +} + +int get_scroll_y(void) +{ + return scroll_y / cell_size; +} + +int get_play_area_w(void) +{ + return play_area_w / cell_size; +} + +int get_play_area_h(void) +{ + return play_area_h / cell_size; +} + +/* SCROLLING + * + * scrolls to the player during game play. + * called by drawcave + * returns true, if player is not visible-ie it is out of the visible size in the drawing area. + */ +boolean gd_scroll(GdGame *game, boolean exact_scroll, boolean immediate) +{ + static int scroll_desired_x = 0, scroll_desired_y = 0; + boolean out_of_window; + int player_x, player_y, visible_x, visible_y; + boolean changed; + int scroll_divisor; + + /* max scrolling speed depends on the speed of the cave. */ + /* game moves cell_size_game * 1s / cave time pixels in a second. */ + /* scrolling moves scroll speed * 1s / scroll_time in a second. */ + /* these should be almost equal; scrolling speed a little slower. */ + /* that way, the player might reach the border with a small probability, */ + /* but the scrolling will not "oscillate", ie. turn on for little intervals as it has */ + /* caught up with the desired position. smaller is better. */ + int scroll_speed = cell_size * 20 / game->cave->speed; + + if (immediate) + scroll_speed = cell_size * MAX(game->cave->w, game->cave->h); + + player_x = game->cave->player_x - game->cave->x1; /* cell coordinates of player */ + player_y = game->cave->player_y - game->cave->y1; + + /* pixel size of visible part of the cave (may be smaller in intermissions) */ + visible_x = (game->cave->x2 - game->cave->x1 + 1) * cell_size; + visible_y = (game->cave->y2 - game->cave->y1 + 1) * cell_size; + + /* cell_size contains the scaled size, but we need the original. */ + changed = FALSE; + + /* some sort of scrolling speed. + with larger cells, the divisor must be smaller, so the scrolling faster. */ + scroll_divisor = 256 / cell_size; + + /* fine scrolling is 50hz (normal would be 25hz only) */ + scroll_divisor *= 2; + + if (cave_scroll(visible_x, play_area_w, player_x * cell_size + cell_size / 2 - play_area_w / 2, + exact_scroll, &scroll_x, &scroll_desired_x, scroll_speed)) + changed = TRUE; + + if (cave_scroll(visible_y, play_area_h, player_y * cell_size + cell_size / 2 - play_area_h / 2, + exact_scroll, &scroll_y, &scroll_desired_y, scroll_speed)) + changed = TRUE; + + /* if scrolling, we should update entire screen. */ + if (changed) + { + int x, y; + + for (y = 0; y < game->cave->h; y++) + for (x = 0; x < game->cave->w; x++) + game->gfx_buffer[y][x] |= GD_REDRAW; + } + + /* check if active player is visible at the moment. */ + out_of_window = FALSE; + + /* check if active player is outside drawing area. if yes, we should wait for scrolling */ + if ((player_x * cell_size) < scroll_x || + (player_x * cell_size + cell_size - 1) > scroll_x + play_area_w) + { + /* but only do the wait, if the player SHOULD BE visible, ie. he is inside + the defined visible area of the cave */ + if (game->cave->player_x >= game->cave->x1 && + game->cave->player_x <= game->cave->x2) + out_of_window = TRUE; + } + + if ((player_y * cell_size) < scroll_y || + (player_y * cell_size + cell_size - 1) > scroll_y + play_area_h) + /* but only do the wait, if the player SHOULD BE visible, ie. he is inside + the defined visible area of the cave */ + if (game->cave->player_y >= game->cave->y1 && + game->cave->player_y <= game->cave->y2) + out_of_window = TRUE; + + /* if not yet born, we treat as visible. so cave will run. + the user is unable to control an unborn player, so this is the right behaviour. */ + if (game->cave->player_state == GD_PL_NOT_YET) + return FALSE; + + return out_of_window; +} + +#if DO_GFX_SANITY_CHECK +/* workaround to prevent variable name scope problem */ +static boolean use_native_bd_graphics_engine(void) +{ + return game.use_native_bd_graphics_engine; +} +#endif + +int gd_drawcave(Bitmap *dest, GdGame *game, boolean force_redraw) +{ + void (*blit_bitmap)(Bitmap *, Bitmap *, int, int, int, int, int, int) = BlitBitmap; + static int show_flash_count = 0; + boolean show_flash = FALSE; + boolean redraw_all = force_redraw; + int scroll_y_aligned = scroll_y; + int x, y, xd, yd; + + if (!game->cave->gate_open_flash) + { + show_flash_count = 0; + } + else + { + if (show_flash_count++ < 4) + show_flash = TRUE; + + redraw_all = TRUE; + } + + if (show_flash) + { + FillRectangle(dest, 0, 0, SXSIZE, SYSIZE, WHITE_PIXEL); + + blit_bitmap = BlitBitmapMasked; + redraw_all = TRUE; + } + + /* here we draw all cells to be redrawn. we do not take scrolling area into + consideration - sdl will do the clipping. */ + for (y = game->cave->y1, yd = 0; y <= game->cave->y2; y++, yd++) + { + for (x = game->cave->x1, xd = 0; x <= game->cave->x2; x++, xd++) + { + if (redraw_all || game->gfx_buffer[y][x] & GD_REDRAW) + { + /* if it needs to be redrawn */ + SDL_Rect offset; + + /* sdl_blitsurface destroys offset, so we have to set y here, too. + (ie. in every iteration) */ + offset.y = y * cell_size - scroll_y_aligned; + offset.x = x * cell_size - scroll_x; + + /* now we have drawn it */ + game->gfx_buffer[y][x] = game->gfx_buffer[y][x] & ~GD_REDRAW; + + int tile = game->element_buffer[y][x]; + int frame = game->animcycle; + struct GraphicInfo_BD *g = &graphic_info_bd_object[tile][frame]; + + blit_bitmap(g->bitmap, dest, g->src_x, g->src_y, g->width, g->height, + offset.x, offset.y); + +#if DO_GFX_SANITY_CHECK + if (use_native_bd_graphics_engine() && !program.headless) + { + int old_x = (game->gfx_buffer[y][x] % GD_NUM_OF_CELLS) % GD_NUM_OF_CELLS_X; + int old_y = (game->gfx_buffer[y][x] % GD_NUM_OF_CELLS) / GD_NUM_OF_CELLS_X; + int new_x = g->src_x / g->width; + int new_y = g->src_y / g->height; + + if (new_x != old_x || new_y != old_y) + { + printf("::: BAD ANIMATION FOR TILE %d, FRAME %d [NEW(%d, %d) != OLD(%d, %d)] ['%s']\n", + tile, frame, + new_x, new_y, + old_x, old_y, + gd_elements[tile].name); + } + } +#endif + } + } + } + + return 0; +} diff --git a/src/game_bd/bd_graphics.h b/src/game_bd/bd_graphics.h new file mode 100644 index 00000000..15a24b99 --- /dev/null +++ b/src/game_bd/bd_graphics.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef BD_GRAPHICS_H +#define BD_GRAPHICS_H + +#include + +#include "bd_cave.h" +#include "bd_gameplay.h" + + +extern Bitmap *gd_screen_bitmap; + +extern const Uint8 *gd_keystate; + +typedef guint32 GdColor; + +void set_cell_size(int s); +void set_play_area(int w, int h); + +int get_play_area_w(void); +int get_play_area_h(void); + +void gd_init_keystate(void); + +int gd_drawcave(Bitmap *dest, GdGame *gameplay, boolean); +boolean gd_scroll(GdGame *gameplay, boolean exact_scroll, boolean immediate); +void gd_scroll_to_origin(void); +int get_scroll_x(void); +int get_scroll_y(void); + +#endif // BD_GRAPHICS_H diff --git a/src/game_bd/bd_sound.c b/src/game_bd/bd_sound.c new file mode 100644 index 00000000..799ff44f --- /dev/null +++ b/src/game_bd/bd_sound.c @@ -0,0 +1,418 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include "main_bd.h" + + +/* + The C64 sound chip (the SID) had 3 channels. Boulder Dash used all 3 of them. + + Different channels were used for different sounds. + + Channel 1: other small sounds, ie. diamonds falling, boulders rolling. + + Channel 2: Walking, diamond collecting and explosion; also time running out + sound. + + Channel 3: amoeba sound, magic wall sound, cave cover & uncover sound, and + the crack sound (gate open) + + Sounds have precedence over each other. Ie. the crack sound is given + precedence over other sounds (amoeba, for example.) + Different channels also behave differently. Channel 2 sounds are not stopped, + ie. walking can not be heard, until the explosion sound is finished playing + completely. + + Explosions are always restarted, though. This is controlled by the array + defined in cave.c. + + Channel 1 sounds are always stopped, if a new sound is requested. + + Here we implement this a bit differently. We use samples, instead of + synthesizing the sounds. By stopping samples, sometimes small clicks + generate. Therefore we do not stop them, rather fade them out quickly. + (The SID had filters, which stopped these small clicks.) + + Also, channel 1 and 2 should be stopped very often. So I decided to use two + SDL_Mixer channels to emulate one C64 channel; and they are used alternating. + SDL channel 4 is the "backup" channel 1, channel 5 is the backup channel 2. + Other channels have the same indexes. +*/ + + +#define MAX_CHANNELS 5 + +typedef enum _sound_flag +{ + GD_SP_LOOPED = 1 << 0, + GD_SP_FORCE = 1 << 1, /* force restart, regardless of precedence level */ +} GdSoundFlag; + +typedef struct _sound_property +{ + GdSound sound; + int channel; /* channel this sound is played on. */ + int precedence; /* greater numbers will have precedence. */ + int flags; +} SoundProperty; + +static SoundProperty sound_flags[] = +{ + { 0, GD_S_NONE, 0, 0 }, + + /* channel 1 sounds. */ + /* CHANNEL 1 SOUNDS ARE ALWAYS RESTARTED, so no need for GD_SP_FORCE flag. */ + { GD_S_STONE, 1, 10 }, + { GD_S_NUT, 1, 8 }, /* nut falling is relatively silent, so low precedence. */ + { GD_S_NUT_CRACK, 1, 12 }, /* higher precedence than a stone bouncing. */ + { GD_S_DIRT_BALL, 1, 8 }, /* sligthly lower precedence, as stones and diamonds should be "louder" */ + { GD_S_NITRO, 1, 10 }, + { GD_S_FALLING_WALL, 1, 10 }, + { GD_S_EXPANDING_WALL, 1, 10 }, + { GD_S_WALL_REAPPEAR, 1, 9 }, + { GD_S_DIAMOND_RANDOM, 1, 10 }, + { GD_S_DIAMOND_1, 1, 10 }, + { GD_S_DIAMOND_2, 1, 10 }, + { GD_S_DIAMOND_3, 1, 10 }, + { GD_S_DIAMOND_4, 1, 10 }, + { GD_S_DIAMOND_5, 1, 10 }, + { GD_S_DIAMOND_6, 1, 10 }, + { GD_S_DIAMOND_7, 1, 10 }, + { GD_S_DIAMOND_8, 1, 10 }, + { GD_S_DIAMOND_COLLECT, 1, 100 }, /* diamond collect sound has precedence over everything. */ + + /* collect sounds have higher precedence than falling sounds and the like. */ + { GD_S_SKELETON_COLLECT, 1, 100 }, + { GD_S_PNEUMATIC_COLLECT, 1, 50 }, + { GD_S_BOMB_COLLECT, 1, 50 }, + { GD_S_CLOCK_COLLECT, 1, 50 }, + { GD_S_SWEET_COLLECT, 1, 50 }, + { GD_S_KEY_COLLECT, 1, 50 }, + { GD_S_DIAMOND_KEY_COLLECT, 1, 50 }, + { GD_S_SLIME, 1, 5 }, /* slime has lower precedence than diamond and stone falling sounds. */ + { GD_S_LAVA, 1, 5 }, /* lava has low precedence, too. */ + { GD_S_REPLICATOR, 1, 5 }, + { GD_S_ACID_SPREAD, 1, 3 }, /* same for acid, even lower. */ + { GD_S_BLADDER_MOVE, 1, 5 }, /* same for bladder. */ + { GD_S_BLADDER_CONVERT, 1, 8 }, + { GD_S_BLADDER_SPENDER, 1, 8 }, + { GD_S_BITER_EAT, 1, 3 }, /* very low precedence. biters tend to produce too much sound. */ + + /* channel2 sounds. */ + { GD_S_DOOR_OPEN, 2, 10 }, + { GD_S_WALK_EARTH, 2, 10 }, + { GD_S_WALK_EMPTY, 2, 10 }, + { GD_S_STIRRING, 2, 10 }, + { GD_S_BOX_PUSH, 2, 10 }, + { GD_S_TELEPORTER, 2, 10 }, + { GD_S_TIMEOUT_10, 2, 20 }, /* timeout sounds have increasing precedence so they are always started */ + { GD_S_TIMEOUT_9, 2, 21 }, /* timeout sounds are examples which do not need "force restart" flag. */ + { GD_S_TIMEOUT_8, 2, 22 }, + { GD_S_TIMEOUT_7, 2, 23 }, + { GD_S_TIMEOUT_6, 2, 24 }, + { GD_S_TIMEOUT_5, 2, 25 }, + { GD_S_TIMEOUT_4, 2, 26 }, + { GD_S_TIMEOUT_3, 2, 27 }, + { GD_S_TIMEOUT_2, 2, 28 }, + { GD_S_TIMEOUT_1, 2, 29 }, + { GD_S_TIMEOUT_0, 2, 150, GD_SP_FORCE }, + { GD_S_EXPLOSION, 2, 100, GD_SP_FORCE }, + { GD_S_BOMB_EXPLOSION, 2, 100, GD_SP_FORCE }, + { GD_S_GHOST_EXPLOSION, 2, 100, GD_SP_FORCE }, + { GD_S_VOODOO_EXPLOSION, 2, 100, GD_SP_FORCE }, + { GD_S_NITRO_EXPLOSION, 2, 100, GD_SP_FORCE }, + { GD_S_BOMB_PLACE, 2, 10 }, + { GD_S_FINISHED, 2, 15, GD_SP_FORCE | GD_SP_LOOPED }, /* precedence larger than normal, but smaller than timeout sounds */ + { GD_S_SWITCH_BITER, 2, 10 }, + { GD_S_SWITCH_CREATURES, 2, 10 }, + { GD_S_SWITCH_GRAVITY, 2, 10 }, + { GD_S_SWITCH_EXPANDING, 2, 10 }, + { GD_S_SWITCH_CONVEYOR, 2, 10 }, + { GD_S_SWITCH_REPLICATOR, 2, 10 }, + + /* channel 3 sounds. */ + { GD_S_AMOEBA, 3, 30, GD_SP_LOOPED }, + { GD_S_AMOEBA_MAGIC, 3, 40, GD_SP_LOOPED }, + { GD_S_MAGIC_WALL, 3, 35, GD_SP_LOOPED }, + { GD_S_COVER, 3, 100, GD_SP_LOOPED }, + { GD_S_PNEUMATIC_HAMMER, 3, 50, GD_SP_LOOPED }, + { GD_S_WATER, 3, 20, GD_SP_LOOPED }, + { GD_S_CRACK, 3, 150 }, + { GD_S_GRAVITY_CHANGE, 3, 60 }, + + /* other sounds */ + /* the bonus life sound has nothing to do with the cave. */ + /* playing on channel 4. */ + { GD_S_BONUS_LIFE, 4, 0 }, +}; + +struct GdSoundInfo +{ + int x, y; + int element; + int sound; +}; + +static GdSound snd_playing[MAX_CHANNELS]; +struct GdSoundInfo sound_info_to_play[MAX_CHANNELS]; +struct GdSoundInfo sound_info_playing[MAX_CHANNELS]; + +static boolean gd_sound_is_looped(GdSound sound) +{ + return (sound_flags[sound].flags & GD_SP_LOOPED) != 0; +} + +static boolean gd_sound_force_start(GdSound sound) +{ + return (sound_flags[sound].flags & GD_SP_FORCE) != 0; +} + +static int gd_sound_get_channel(GdSound sound) +{ + return sound_flags[sound].channel; +} + +static int gd_sound_get_precedence(GdSound sound) +{ + return sound_flags[sound].precedence; +} + +static GdSound sound_playing(int channel) +{ + struct GdSoundInfo *si = &sound_info_playing[channel]; + + // check if sound has finished playing + if (snd_playing[channel] != GD_S_NONE) + if (!isSoundPlaying_BD(si->element, si->sound)) + snd_playing[channel] = GD_S_NONE; + + return snd_playing[channel]; +} + +static void halt_channel(int channel) +{ + struct GdSoundInfo *si = &sound_info_playing[channel]; + +#if 0 + if (isSoundPlaying_BD(si->element, si->sound)) + printf("::: stop sound %d\n", si->sound); +#endif + + if (isSoundPlaying_BD(si->element, si->sound)) + StopSound_BD(si->element, si->sound); + + snd_playing[channel] = GD_S_NONE; +} + +static void play_channel_at_position(int channel) +{ + struct GdSoundInfo *si = &sound_info_to_play[channel]; + + PlayLevelSound_BD(si->x, si->y, si->element, si->sound); + + sound_info_playing[channel] = *si; +} + +static void play_sound(int channel, GdSound sound) +{ + /* channel 1 and channel 4 are used alternating */ + /* channel 2 and channel 5 are used alternating */ + static const GdSound diamond_sounds[] = + { + GD_S_DIAMOND_1, + GD_S_DIAMOND_2, + GD_S_DIAMOND_3, + GD_S_DIAMOND_4, + GD_S_DIAMOND_5, + GD_S_DIAMOND_6, + GD_S_DIAMOND_7, + GD_S_DIAMOND_8, + }; + + if (sound == GD_S_NONE) + return; + + /* change diamond falling random to a selected diamond falling sound. */ + if (sound == GD_S_DIAMOND_RANDOM) + sound = diamond_sounds[g_random_int_range(0, G_N_ELEMENTS(diamond_sounds))]; + + /* channel 1 may have been changed to channel 4 above. */ + + if (!gd_sound_is_looped(sound)) + halt_channel(channel); + + play_channel_at_position(channel); + + snd_playing[channel] = sound; +} + +void gd_sound_init(void) +{ + int i; + + for (i = 0; i < MAX_CHANNELS; i++) + snd_playing[i] = GD_S_NONE; +} + +void gd_sound_off(void) +{ + int i; + + /* stop all sounds. */ + for (i = 0; i < G_N_ELEMENTS(snd_playing); i++) + halt_channel(i); +} + +void gd_sound_play_bonus_life(void) +{ + // required to set extended sound information for native sound engine + gd_sound_play(NULL, GD_S_BONUS_LIFE, O_NONE, -1, -1); + + // now play the sound directly (on non-standard sound channel) + play_sound(gd_sound_get_channel(GD_S_BONUS_LIFE), GD_S_BONUS_LIFE); +} + +static void play_sounds(GdSound sound1, GdSound sound2, GdSound sound3) +{ + /* CHANNEL 1 is for small sounds */ + if (sound1 != GD_S_NONE) + { + /* start new sound if higher or same precedence than the one currently playing */ + if (gd_sound_get_precedence(sound1) >= gd_sound_get_precedence(sound_playing(1))) + play_sound(1, sound1); + } + else + { + /* only interrupt looped sounds. non-looped sounds will go away automatically. */ + if (gd_sound_is_looped(sound_playing(1))) + halt_channel(1); + } + + /* CHANNEL 2 is for walking, explosions */ + /* if no sound requested, do nothing. */ + if (sound2 != GD_S_NONE) + { + boolean play = FALSE; + + /* always start if not currently playing a sound. */ + if (sound_playing(2) == GD_S_NONE || + gd_sound_force_start(sound2) || + gd_sound_get_precedence(sound2) > gd_sound_get_precedence(sound_playing(2))) + play = TRUE; + + /* if figured out to play: do it. */ + /* (if the requested sound is looped, this is required to continue playing it) */ + if (play) + play_sound(2, sound2); + } + else + { + /* only interrupt looped sounds. non-looped sounds will go away automatically. */ + if (gd_sound_is_looped(sound_playing(2))) + halt_channel(2); + } + + /* CHANNEL 3 is for crack sound, amoeba and magic wall. */ + if (sound3 != GD_S_NONE) + { + boolean play = TRUE; + + /* if requests a non-looped sound, play that immediately. + that can be a crack sound, gravity change, new life, ... */ + /* do not interrupt the previous sound, if it is non-looped. + later calls of this function will probably contain the same sound3, + and then it will be set. */ + if (!gd_sound_is_looped(sound_playing(3)) && + gd_sound_is_looped(sound3) && + sound_playing(3) != GD_S_NONE) + play = FALSE; + + /* if figured out to play: do it. */ + if (play) + play_sound(3, sound3); + } + else + { + /* sound3 = none, so interrupt sound requested. */ + /* only interrupt looped sounds. non-looped sounds will go away automatically. */ + if (gd_sound_is_looped(sound_playing(3))) + halt_channel(3); + } +} + +void gd_sound_play_cave(GdCave *cave) +{ + play_sounds(cave->sound1, cave->sound2, cave->sound3); +} + +static void gd_sound_info_to_play(int channel, int x, int y, int element, int sound) +{ + struct GdSoundInfo *si = &sound_info_to_play[channel]; + + si->x = x; + si->y = y; + si->element = element; + si->sound = sound; +} + +/* plays sound in a cave */ +void gd_sound_play(GdCave *cave, GdSound sound, GdElement element, int x, int y) +{ + if (sound == GD_S_NONE) + return; + + // do not play sounds when fast-forwarding until player hatched + if (setup.bd_skip_hatching && !game_bd.game->cave->hatched && + game_bd.game->state_counter == GAME_INT_CAVE_RUNNING) + return; + + // if no player position specified, use middle of the screen position + if (x == -1 && y == -1) + { + x = get_play_area_w() / 2 + get_scroll_x(); + y = get_play_area_h() / 2 + get_scroll_y(); + } + + if (!game.use_native_bd_sound_engine) + { + // when not using native sound engine, just play the sound + PlayLevelSound_BD(x, y, element, sound); + + return; + } + + GdSound *s = &sound; // use reliable default value + int channel = gd_sound_get_channel(sound); + + switch (channel) + { + case 1: s = &cave->sound1; break; + case 2: s = &cave->sound2; break; + case 3: s = &cave->sound3; break; + default: break; + } + + if (gd_sound_get_precedence(sound) >= gd_sound_get_precedence(*s)) + { + // set sound + *s = sound; + + // set extended sound information for native sound engine + gd_sound_info_to_play(channel, x, y, element, sound); + } +} diff --git a/src/game_bd/bd_sound.h b/src/game_bd/bd_sound.h new file mode 100644 index 00000000..ccce604b --- /dev/null +++ b/src/game_bd/bd_sound.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2007, 2008, 2009, Czirkos Zoltan + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef BD_SOUND_H +#define BD_SOUND_H + +#include "bd_cave.h" + + +// init sound. allows setting buffer size (for replays saving), 0 for default +void gd_sound_init(void); +void gd_sound_off(void); +void gd_sound_play_cave(GdCave *cave); +void gd_sound_play_bonus_life(void); +void gd_sound_play(GdCave *cave, GdSound sound, GdElement element, int x, int y); + +#endif // BD_SOUND_H diff --git a/src/game_bd/export_bd.h b/src/game_bd/export_bd.h index 12a6c9aa..5545490b 100644 --- a/src/game_bd/export_bd.h +++ b/src/game_bd/export_bd.h @@ -60,6 +60,13 @@ struct LevelInfo_BD boolean loaded_from_caveset; }; +struct GraphicInfo_BD +{ + Bitmap *bitmap; + int src_x, src_y; + int width, height; +}; + struct EngineSnapshotInfo_BD { }; @@ -71,10 +78,14 @@ struct EngineSnapshotInfo_BD extern struct GameInfo_BD game_bd; extern struct LevelInfo_BD native_bd_level; +extern struct GraphicInfo_BD graphic_info_bd_object[O_MAX_ALL][8]; extern struct EngineSnapshotInfo_BD engine_snapshot_bd; +int map_action_RND_to_BD(int); int map_action_BD_to_RND(int); +boolean checkGameRunning_BD(void); + void setLevelInfoToDefaults_BD_Ext(int, int); void setLevelInfoToDefaults_BD(void); diff --git a/src/game_bd/import_bd.h b/src/game_bd/import_bd.h index cccee3a4..ef2fbd84 100644 --- a/src/game_bd/import_bd.h +++ b/src/game_bd/import_bd.h @@ -26,4 +26,7 @@ void PlayLevelSound_BD(int, int, int, int); void StopSound_BD(int, int); boolean isSoundPlaying_BD(int, int); +byte *TapePlayAction_BD(void); +boolean TapeIsPlaying_ReplayBD(void); + #endif // IMPORT_BD_H diff --git a/src/game_bd/main_bd.c b/src/game_bd/main_bd.c index 1bf30fca..646927fa 100644 --- a/src/game_bd/main_bd.c +++ b/src/game_bd/main_bd.c @@ -21,6 +21,17 @@ struct EngineSnapshotInfo_BD engine_snapshot_bd; // level file functions // ============================================================================ +int map_action_RND_to_BD(int action) +{ + GdDirection player_move = gd_direction_from_keypress(action & JOY_UP, + action & JOY_DOWN, + action & JOY_LEFT, + action & JOY_RIGHT); + boolean player_fire = (action & (JOY_BUTTON_1 | JOY_BUTTON_2)); + + return (player_move | (player_fire ? GD_REPLAY_FIRE_MASK : 0)); +} + int map_action_BD_to_RND(int action) { GdDirection player_move = action & GD_REPLAY_MOVE_MASK; @@ -39,6 +50,11 @@ int map_action_BD_to_RND(int action) return (action_move | action_fire); } +boolean checkGameRunning_BD(void) +{ + return (game_bd.game != NULL && game_bd.game->state_counter == GAME_INT_CAVE_RUNNING); +} + void setLevelInfoToDefaults_BD_Ext(int width, int height) { // ... diff --git a/src/game_bd/main_bd.h b/src/game_bd/main_bd.h index bf4be3a8..1018cb23 100644 --- a/src/game_bd/main_bd.h +++ b/src/game_bd/main_bd.h @@ -30,10 +30,39 @@ // internal functions and definitions that are not exported to main program // ============================================================================ +#include "bd_bdcff.h" +#include "bd_cave.h" +#include "bd_cavedb.h" +#include "bd_caveset.h" +#include "bd_caveobject.h" +#include "bd_caveengine.h" +#include "bd_gameplay.h" +#include "bd_c64import.h" +#include "bd_graphics.h" +#include "bd_sound.h" + + // ---------------------------------------------------------------------------- // constant definitions // ---------------------------------------------------------------------------- +/* screen sizes and positions for BD engine */ + +extern int TILESIZE_VAR; + +#define TILEX TILESIZE_VAR +#define TILEY TILESIZE_VAR + +extern int SCR_FIELDX, SCR_FIELDY; + +/* often used screen positions */ + +extern int SX, SY; + +#define SXSIZE (SCR_FIELDX * TILEX) +#define SYSIZE (SCR_FIELDY * TILEY) + + // ---------------------------------------------------------------------------- // data structure definitions // ---------------------------------------------------------------------------- diff --git a/src/libgame/system.h b/src/libgame/system.h index 3cee3696..4bc436eb 100644 --- a/src/libgame/system.h +++ b/src/libgame/system.h @@ -1557,6 +1557,8 @@ struct SetupInfo boolean prefer_extra_panel_items; boolean game_speed_extended; int game_frame_delay; + boolean bd_skip_uncovering; + boolean bd_skip_hatching; boolean sp_show_border_elements; boolean small_game_graphics; boolean show_load_save_buttons; diff --git a/src/tape.c b/src/tape.c index f9c56d1a..a07ebbde 100644 --- a/src/tape.c +++ b/src/tape.c @@ -1008,7 +1008,7 @@ void TapeStopPlaying(void) MapTapeEjectButton(); } -byte *TapePlayAction(void) +byte *TapePlayActionExt(boolean bd_replay) { int update_delay = FRAMES_PER_SECOND / 2; boolean update_video_display = (FrameCounter % update_delay == 0); @@ -1019,6 +1019,12 @@ byte *TapePlayAction(void) if (!tape.playing || tape.pausing) return NULL; + if (!checkGameRunning()) + return NULL; + + if (tape.bd_replay && !bd_replay) + return NULL; + if (tape.pause_before_end) // stop some seconds before end of tape { if (TapeTime > (int)tape.length_seconds - TAPE_PAUSE_SECONDS_BEFORE_DEATH) @@ -1097,7 +1103,7 @@ byte *TapePlayAction(void) } tape.delay_played++; - if (tape.delay_played >= tape.pos[tape.counter].delay) + if (tape.delay_played >= tape.pos[tape.counter].delay || tape.bd_replay) { tape.counter++; tape.delay_played = 0; @@ -1112,6 +1118,16 @@ byte *TapePlayAction(void) return action; } +byte *TapePlayAction_BD(void) +{ + return TapePlayActionExt(TRUE); +} + +byte *TapePlayAction(void) +{ + return TapePlayActionExt(FALSE); +} + void TapeStop(void) { if (tape.pausing) @@ -1373,6 +1389,11 @@ void TapeReplayAndPauseBeforeEnd(void) tape.quick_resume = TRUE; } +boolean TapeIsPlaying_ReplayBD(void) +{ + return (tape.playing && tape.bd_replay); +} + boolean hasSolutionTape(void) { boolean tape_file_exists = fileExists(getSolutionTapeFilename(level_nr)); diff --git a/src/tape.h b/src/tape.h index cfd67be2..30637d42 100644 --- a/src/tape.h +++ b/src/tape.h @@ -266,6 +266,8 @@ void TapeRecordAction(byte[MAX_TAPE_ACTIONS]); void TapeTogglePause(boolean); void TapeStartPlaying(void); void TapeStopPlaying(void); +byte *TapePlayActionExt(boolean); +byte *TapePlayAction_BD(void); byte *TapePlayAction(void); void TapeStop(void); void TapeStopGame(void); @@ -277,6 +279,7 @@ void TapeQuickSave(void); void TapeQuickLoad(void); void TapeRestartGame(void); void TapeReplayAndPauseBeforeEnd(void); +boolean TapeIsPlaying_ReplayBD(void); boolean hasSolutionTape(void); boolean InsertSolutionTape(void); -- 2.34.1