├── LICENSE ├── Makefile ├── README.md ├── assetflags.mk └── src ├── audioutils.c ├── audioutils.h ├── effects.c ├── effects.h ├── engine_eeprom.c ├── engine_eeprom.h ├── engine_gamestatus.c ├── engine_gamestatus.h ├── engine_gfx.c ├── engine_gfx.h ├── engine_locale.c ├── engine_locale.h ├── entity_logic.c ├── entity_logic.h ├── inih ├── ini.c └── ini.h ├── intro.c ├── intro.h ├── main.c ├── maps.c ├── maps.h ├── menu.c ├── menu.h ├── particles_sim.c ├── particles_sim.h ├── playtime_logic.c ├── playtime_logic.h ├── teams.c ├── teams.h ├── tinyphysicsengine.c └── tinyphysicsengine.h /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME = drivingstrikers64 2 | # Internal ROM name 3 | ROM_NAME = "Driving Strikers 64" 4 | BUILD_DIR = build 5 | ASSETS_DIR = assets 6 | FILESYSTEM_DIR = filesystem 7 | T3D_INST=$(shell realpath ../..) 8 | 9 | include $(N64_INST)/include/n64.mk 10 | include $(N64_INST)/include/t3d.mk 11 | 12 | include assetflags.mk 13 | 14 | N64_CFLAGS += -std=gnu2x 15 | N64_C_AND_CXX_FLAGS += -ftrivial-auto-var-init=zero 16 | 17 | src = $(shell find ./src/ -type f -name '*.c') 18 | 19 | IMAGE_LIST = $(shell find $(ASSETS_DIR)/models/ -type f -name '*.png') \ 20 | $(shell find $(ASSETS_DIR)/textures/ -type f -name '*.png') 21 | FONT_LIST = $(shell find $(ASSETS_DIR)/fonts/ -type f -name '*.ttf') 22 | SOUND_LIST = $(shell find $(ASSETS_DIR)/sfx/ -type f -name '*.wav') 23 | MUSIC_LIST = $(shell find $(ASSETS_DIR)/music/ -type f -name '*.wav') 24 | MODELS_LIST = $(shell find $(ASSETS_DIR)/models/ -type f -name '*.glb') 25 | TEXT_LIST = $(shell find $(ASSETS_DIR)/locale/ -type f -name '*.*') 26 | ASSETS_LIST += $(subst $(ASSETS_DIR),$(FILESYSTEM_DIR),$(IMAGE_LIST:%.png=%.sprite)) 27 | ASSETS_LIST += $(subst $(ASSETS_DIR),$(FILESYSTEM_DIR),$(FONT_LIST:%.ttf=%.font64)) 28 | ASSETS_LIST += $(subst $(ASSETS_DIR),$(FILESYSTEM_DIR),$(SOUND_LIST:%.wav=%.wav64)) 29 | ASSETS_LIST += $(subst $(ASSETS_DIR),$(FILESYSTEM_DIR),$(MUSIC_LIST:%.wav=%.wav64)) 30 | ASSETS_LIST += $(subst $(ASSETS_DIR),$(FILESYSTEM_DIR),$(MODELS_LIST:%.glb=%.t3dm)) 31 | ASSETS_LIST += $(subst $(ASSETS_DIR),$(FILESYSTEM_DIR),$(TEXT_LIST)) 32 | 33 | all: $(NAME).z64 34 | 35 | $(FILESYSTEM_DIR)/textures/%.sprite: $(ASSETS_DIR)/textures/%.png 36 | @mkdir -p $(dir $@) 37 | @echo " [SPRITE] $@" 38 | $(N64_MKSPRITE) $(MKSPRITE_FLAGS) --compress 1 -o $(dir $@) "$<" 39 | 40 | $(FILESYSTEM_DIR)/models/%.sprite: $(ASSETS_DIR)/models/%.png 41 | @mkdir -p $(dir $@) 42 | @echo " [SPRITE] $@" 43 | $(N64_MKSPRITE) $(MKSPRITE_FLAGS) --compress 1 -o $(dir $@) "$<" 44 | 45 | $(FILESYSTEM_DIR)/fonts/%.font64: $(ASSETS_DIR)/fonts/%.ttf 46 | @mkdir -p $(dir $@) 47 | @echo " [FONT] $@" 48 | $(N64_MKFONT) $(MKFONT_FLAGS) -o $(dir $@) "$<" 49 | 50 | $(FILESYSTEM_DIR)/sfx/%.wav64: $(ASSETS_DIR)/sfx/%.wav 51 | @mkdir -p $(dir $@) 52 | @echo " [SFX] $@" 53 | $(N64_AUDIOCONV) $(AUDIOCONV_FLAGS) --wav-compress 1 -o $(dir $@) "$<" 54 | 55 | $(FILESYSTEM_DIR)/music/%.wav64: $(ASSETS_DIR)/music/%.wav 56 | @mkdir -p $(dir $@) 57 | @echo " [MUSIC] $@" 58 | $(N64_AUDIOCONV) --wav-compress 1,bits=3 --wav-resample 28000 $(AUDIOCONV_FLAGS) -o $(dir $@) "$<" 59 | 60 | $(FILESYSTEM_DIR)/%.t3dm: assets/%.glb 61 | @mkdir -p $(dir $@) 62 | @echo " [T3D-MODEL] $@" 63 | $(T3D_GLTF_TO_3D) "$<" $@ 64 | $(N64_BINDIR)/mkasset -c 2 -o $(FILESYSTEM_DIR) $@ 65 | 66 | $(FILESYSTEM_DIR)/locale/%: $(ASSETS_DIR)/locale/% 67 | @mkdir -p $(dir $@) 68 | @echo " [TEXT] $@" 69 | cp "$<" $@ 70 | 71 | $(BUILD_DIR)/$(NAME).dfs: $(ASSETS_LIST) 72 | $(BUILD_DIR)/$(NAME).elf: $(src:%.c=$(BUILD_DIR)/%.o) 73 | 74 | $(NAME).z64: N64_ROM_TITLE=$(ROM_NAME) 75 | $(NAME).z64: $(BUILD_DIR)/$(NAME).dfs 76 | $(NAME).z64: N64_ROM_SAVETYPE = eeprom4k 77 | 78 | clean: 79 | rm -rf $(BUILD_DIR) *.z64 80 | rm -rf $(FILESYSTEM_DIR) 81 | 82 | build_lib: 83 | rm -rf $(BUILD_DIR) *.z64 84 | make -C $(T3D_INST) 85 | make all 86 | 87 | -include $(wildcard $(BUILD_DIR)/*.d) 88 | 89 | .PHONY: all clean 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Driving Strikers 64 2 | A port of the Driving Strikers indie game to the Nintendo 64 using Libdragon SDK, Tiny3D and a custom-made codebase in C. 3 | 4 | ![boxart](https://github.com/user-attachments/assets/a9567dbd-5887-4077-9705-ca01d82db943) 5 | 6 | This is an indie game that was originally released on Dreamcast and was ported across various platforms. With permission from the author I've ported it onto the Nintendo 64 platform as well. 7 | This is a 6-th gen game, that runs well on a 5-th gen system with minimal compromises and some improvements from the Dreamcast version. 8 | 9 | This game uses advanced features, not previously seen in any N64 official title, that is a result of extensive research and optimization. 10 | 11 | Expansion Pak is ___NOT___ needed, but recommended to play this game. 12 | 13 | This release of the N64 port of Driving Strikers has been approved for distribution by the original developers of the game; Luke Benstead and David Reichelt (LD2K). The gameplay and style may vary from the original release. If you enjoy this port please consider supporting the developers by purchasing the original game on [Steam](https://store.steampowered.com/app/2384430/Driving_Strikers/) (PC/Linux) or [Itch.io](https://reality-jump.itch.io/driving-strikers). 14 | 15 | # Known issues: 16 | - EEPROM may not work on older Everdrive flashcart models, or with outdated software (libdragon incompatibility) 17 | 18 | # Controls: 19 | - Stick: move around 20 | - Z trigger: nitro 21 | - L/R triggers: jump 22 | 23 | # Features: 24 | - Physics-driven gameplay using the [Tiny Physics Engine](https://github.com/ESPboy-edu/ESPboy_tinyphysicsengine) open-source library 25 | - Multilanguage support with 8 different languages using .ini configs and [inih](https://github.com/benhoyt/inih) open-source library 26 | - Stereo CD quality music with voiced audio on a 16MB cartridge 27 | - Full 480i TrueColor graphics at high framerates of up to 60 FPS ___without the expansion pak___, extremely rare in N64 titles 28 | - Quality artwork and levels with antialiasing and high-res fullcolor mipmapped, bumpmapped textures 29 | - HDR rendering and bright lighting with autoexposure, never before seen on any N64 title 30 | - Rumble Pak and EEPROM save support 31 | 32 | # Build the game 33 | - Install [Libdragon SDK](https://github.com/DragonMinded/libdragon/tree/unstable) (unstable) 34 | - Use the [texparms PR](https://github.com/DragonMinded/libdragon/pull/667) from the libdragon unstable PR's 35 | - Install [Tiny3D](https://github.com/HailToDodongo/tiny3d/tree/no_light) library (no_light branch or later) 36 | - Clone this repository 37 | - Add your Driving Strikers assets from the Release to the "filesystem" folder 38 | - Build it with make command 39 | 40 | # Screenshots 41 | 42 | ![drivingstrikers64 2025-05-12 01-32-02](https://github.com/user-attachments/assets/ecf9522f-bc18-4e72-a50b-384b89b180b2) 43 | ![drivingstrikers64 2025-05-10 16-11-51](https://github.com/user-attachments/assets/29db1ff2-2a90-40e1-8186-97666a1df0ab) 44 | ![drivingstrikers64 2025-05-10 00-40-28](https://github.com/user-attachments/assets/77c22f7c-128f-4236-bc38-674b3cc71fe5) 45 | ![drivingstrikers64 2025-05-10 00-40-13](https://github.com/user-attachments/assets/17ef8e04-420f-444e-971c-691ea16f2e78) 46 | ![drivingstrikers64 2025-05-09 16-35-00](https://github.com/user-attachments/assets/29ac863e-2b88-450e-bd9b-33cc7be8c240) 47 | ![drivingstrikers64 2025-05-06 11-48-03](https://github.com/user-attachments/assets/22263558-a7ee-4a37-a985-d111f4401768) 48 | ![drivingstrikers64 2025-05-06 11-47-37](https://github.com/user-attachments/assets/769c4fdb-0a27-4bb6-b2ab-78419cca0e37) 49 | ![drivingstrikers64 2025-04-30 01-07-46](https://github.com/user-attachments/assets/d8161f4f-937b-401c-ad47-a2efbe1cbe2e) 50 | 51 | # Visual comparison between N64 (left) with DC (right) version 52 | 53 | ![image](https://github.com/user-attachments/assets/6e5c92e9-08d2-48fa-8a41-99df41b86029) 54 | ![image](https://github.com/user-attachments/assets/3c6f979f-eb54-44b4-bff4-16a6dc151897) 55 | ![image](https://github.com/user-attachments/assets/892c1f4b-818f-4f1d-9d1b-a147288e3bad) 56 | ![image](https://github.com/user-attachments/assets/53b9b09f-0a60-4b50-aaa3-3732de8748ca) 57 | ![image](https://github.com/user-attachments/assets/ea007ff9-b710-4e11-967a-1c55f5f47e84) 58 | 59 | -------------------------------------------------------------------------------- /assetflags.mk: -------------------------------------------------------------------------------- 1 | #a list of custom asset flags to use when converting to Libradgon formats 2 | 3 | filesystem/fonts/Kanit-Black.font64: MKFONT_FLAGS+= --size 18 --range 20-7F --range 80-FF --range 100-17F --range 180-24F 4 | filesystem/fonts/Kanit-BlackItalic.font64: MKFONT_FLAGS+= --size 42 --range 20-7F --range 80-FF --range 100-17F --range 180-24F 5 | filesystem/fonts/Kanit-BlackItalic_small.font64: MKFONT_FLAGS+= --size 18 --range 20-7F --range 80-FF --range 100-17F --range 180-24F 6 | filesystem/fonts/LuckiestGuy-Regular.font64: MKFONT_FLAGS+= --size 26 7 | filesystem/fonts/RobotoCondensed-Bold.font64: MKFONT_FLAGS+= --size 24 8 | filesystem/fonts/UbuntuMono-Bold.font64: MKFONT_FLAGS+= --size 12 9 | 10 | 11 | filesystem/fonts/ru/ru_Montserrat_black.font64: MKFONT_FLAGS+= --size 16 --range 20-7F --range 0400-052F 12 | filesystem/fonts/ru/ru_Montserrat_italic.font64: MKFONT_FLAGS+= --size 38 --range 20-7F --range 0400-052F 13 | filesystem/fonts/ru/ru_Montserrat_italic_small.font64: MKFONT_FLAGS+= --size 16 --range 20-7F --range 0400-052F 14 | 15 | filesystem/fonts/jp/NikkyouSans-mLKax_small.font64: MKFONT_FLAGS+= --size 18 --range 20-7F --range 30A0-30FF 16 | filesystem/fonts/jp/NikkyouSans-mLKax.font64: MKFONT_FLAGS+= --size 38 --range 20-7F --range 30A0-30FF 17 | 18 | filesystem/textures/%.sprite: MKSPRITE_FLAGS += --dither ORDERED 19 | filesystem/models/%.sprite: MKSPRITE_FLAGS += --mipmap BOX 20 | 21 | filesystem/music/1_select.wav64: AUDIOCONV_FLAGS += --wav-compress 3 22 | filesystem/music/7_theme.wav64: AUDIOCONV_FLAGS += --wav-compress 3 23 | filesystem/music/9_congrats.wav64: AUDIOCONV_FLAGS += --wav-compress 3 -------------------------------------------------------------------------------- /src/audioutils.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "audioutils.h" 3 | #include "engine_gamestatus.h" 4 | #include "ctype.h" 5 | 6 | wav64_t bgmusic[64]; 7 | wav64_t sounds[64]; 8 | char bgmusicnames[64][64]; 9 | char soundsnames[64][64]; 10 | 11 | int musiccount = 0; 12 | int soundscount = 0; 13 | 14 | int sound_channel = 0; 15 | 16 | bool bgmusic_playing; 17 | bool sound_playing; 18 | char bgmusic_name[SHORTSTR_LENGTH]; 19 | char sound_name[SHORTSTR_LENGTH]; 20 | 21 | int transitionstate; 22 | float transitiontime; 23 | float transitiontimemax; 24 | bool loopingmusic; 25 | 26 | int audio_prewarm_all_sounds_callback(const char *fn, dir_t *dir, void *data){ 27 | char nameonly[128] = {0}; 28 | strcpy(nameonly, strrchr(fn, '/') + 1); 29 | *strrchr(nameonly, '.') = '\0'; 30 | strcpy(soundsnames[soundscount], nameonly); 31 | 32 | debugf("Found sound %i: %s | filename %s\n", soundscount, nameonly, fn); 33 | wav64_open(&sounds[soundscount], fn); 34 | 35 | soundscount++; 36 | return DIR_WALK_CONTINUE; 37 | } 38 | 39 | int audio_prewarm_all_music_callback(const char *fn, dir_t *dir, void *data){ 40 | 41 | char nameonly[128] = {0}; 42 | strcpy(nameonly, strrchr(fn, '/') + 1); 43 | *strrchr(nameonly, '.') = '\0'; 44 | strcpy(bgmusicnames[musiccount], nameonly); 45 | debugf("Found music %i: %s | filename %s\n", musiccount, nameonly, fn); 46 | wav64_open(&bgmusic[musiccount], fn); 47 | 48 | musiccount++; 49 | return DIR_WALK_CONTINUE; 50 | } 51 | 52 | 53 | void audio_prewarm_all(){ 54 | dir_glob("**/*.wav64", "rom:/music/", audio_prewarm_all_music_callback, NULL); 55 | dir_glob("**/*.wav64", "rom:/sfx/", audio_prewarm_all_sounds_callback, NULL); 56 | } 57 | 58 | int audio_find_sound(const char* name){ 59 | int index = 0; 60 | while(strcmp(soundsnames[index], name) && index < 64) index++; 61 | if(index >= 63) assertf(0, "Sound not found %s", name); 62 | return index; 63 | } 64 | 65 | int audio_find_music(const char* name){ 66 | int index = 0; 67 | while(strcmp(bgmusicnames[index], name) && index < 64) index++; 68 | if(index >= 63) assertf(0, "Music not found %s", name); 69 | return index; 70 | } 71 | 72 | void audioutils_mixer_update(){ 73 | float volume = gamestatus.state.audio.bgmusic_vol; 74 | mixer_try_play(); 75 | if( transitionstate == 1 && transitiontime > 0){ 76 | transitiontime -= display_get_delta_time(); 77 | 78 | if( transitiontime <= 0) { 79 | transitionstate = 2; 80 | if( bgmusic_playing){ 81 | bgmusic_playing = false; 82 | mixer_ch_stop(AUDIO_CHANNEL_MUSIC); 83 | rspq_wait(); 84 | } 85 | if( bgmusic_name[0]){ 86 | //char fn[512]; sprintf(fn, "rom:/music/%s.wav64", bgmusic_name); 87 | //wav64_open(&bgmusic, fn); 88 | wav64_t* mus = &bgmusic[audio_find_music(bgmusic_name)]; 89 | wav64_set_loop(mus, loopingmusic); 90 | wav64_play(mus, AUDIO_CHANNEL_MUSIC); 91 | bgmusic_playing = true; 92 | } 93 | } 94 | volume *= ( transitiontime / transitiontimemax); 95 | } 96 | if( transitionstate == 2 && transitiontime < transitiontimemax){ 97 | transitiontime += display_get_delta_time(); 98 | volume *= ( transitiontime / transitiontimemax); 99 | } 100 | mixer_ch_set_vol(AUDIO_CHANNEL_MUSIC, volume, volume); 101 | } 102 | 103 | void bgm_hardplay(const char* name, bool loop, float transition){ 104 | loopingmusic = loop; 105 | transitionstate = 0; 106 | transitiontime = 0; 107 | transitiontimemax = 1; 108 | wav64_t* mus = &bgmusic[audio_find_music(name)]; 109 | wav64_set_loop(mus, loop); 110 | wav64_play(mus, AUDIO_CHANNEL_MUSIC); 111 | bgmusic_playing = true; 112 | strcpy( bgmusic_name, name); 113 | } 114 | 115 | void bgm_play(const char* name, bool loop, float transition){ 116 | if(transition == 0) { bgm_hardplay(name, loop, transition); return;} 117 | bgm_stop(1); 118 | loopingmusic = loop; 119 | transitionstate = 1; 120 | transitiontime = transition; 121 | transitiontimemax = transition; 122 | strcpy( bgmusic_name, name); 123 | } 124 | 125 | 126 | void bgm_hardstop(){ 127 | bgmusic_playing = false; 128 | transitionstate = 0; 129 | transitiontime = 0.1; 130 | transitiontimemax = 0.1; 131 | bgmusic_name[0] = 0; 132 | mixer_ch_stop(AUDIO_CHANNEL_MUSIC); 133 | rspq_wait(); 134 | } 135 | 136 | void bgm_stop(float transition){ 137 | if(transition == 0) { bgm_hardstop(); return;} 138 | transitionstate = 1; 139 | transitiontime = 1; 140 | transitiontimemax = 1; 141 | bgmusic_name[0] = 0; 142 | } 143 | 144 | void sound_play(const char* name, bool loop){ 145 | sound_stop(); 146 | wav64_t* snd = &sounds[audio_find_sound(name)]; 147 | wav64_set_loop(snd, loop); 148 | wav64_play(snd, AUDIO_CHANNEL_SOUND + sound_channel*2); 149 | mixer_ch_set_vol(AUDIO_CHANNEL_SOUND + sound_channel*2, gamestatus.state.audio.sound_vol, gamestatus.state.audio.sound_vol); 150 | sound_channel++; 151 | if(sound_channel >= AUDIO_SOUND_MAXSOUNDS) sound_channel = 0; 152 | sound_playing = true; 153 | strcpy( sound_name, name); 154 | } 155 | 156 | void sound_stop(){ 157 | if( sound_playing){ 158 | mixer_ch_stop(AUDIO_CHANNEL_SOUND + sound_channel*2); 159 | } 160 | sound_playing = false; 161 | sound_name[0] = 0; 162 | } 163 | 164 | void music_volume(float vol){ 165 | gamestatus.state.audio.bgmusic_vol = vol; 166 | } 167 | 168 | void sound_volume(float vol){ 169 | mixer_ch_set_vol(AUDIO_CHANNEL_SOUND, vol, vol); 170 | gamestatus.state.audio.sound_vol = vol; 171 | } 172 | 173 | float music_volume_get() {return gamestatus.state.audio.bgmusic_vol;}; 174 | 175 | float sound_volume_get() {return gamestatus.state.audio.sound_vol;}; -------------------------------------------------------------------------------- /src/audioutils.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include "engine_gamestatus.h" 3 | #ifndef AUDIOUTILS_H 4 | #define AUDIOUTILS_H 5 | 6 | /// made by SpookyIluha 7 | /// Audio util functions 8 | 9 | #ifdef __cplusplus 10 | extern "C"{ 11 | #endif 12 | 13 | extern bool bgmusic_playing; 14 | extern bool sound_playing; 15 | extern char bgmusic_name[SHORTSTR_LENGTH]; 16 | extern char sound_name[SHORTSTR_LENGTH]; 17 | 18 | #define AUDIO_CHANNEL_MUSIC 0 19 | #define AUDIO_CHANNEL_SOUND 2 20 | #define AUDIO_SOUND_MAXSOUNDS 4 21 | 22 | /// @brief Should be called on each game tick for audio to be played (between rdpq_attach and rdpq_detach) 23 | void audioutils_mixer_update(); 24 | 25 | void audio_prewarm_all(); 26 | 27 | /// @brief Play music in the background 28 | /// @param name fn of the music in the bgm folder 29 | /// @param loop is the music looped 30 | /// @param transition transition in seconds 31 | void bgm_play(const char* name, bool loop, float transition); 32 | 33 | /// @brief Stop the currently playing music 34 | /// @param transition transition in seconds 35 | void bgm_stop(float transition); 36 | 37 | /// @brief Play sound effect 38 | /// @param name fn of the sound in the sfx folder 39 | /// @param loop is the music looped 40 | void sound_play(const char* name, bool loop); 41 | 42 | /// @brief Stop the currently playing music 43 | void sound_stop(); 44 | 45 | /// @brief Set the music volume 46 | /// @param vol 0-1 range 47 | void music_volume(float vol); 48 | 49 | /// @brief Set the sounds volume 50 | /// @param vol 0-1 range 51 | void sound_volume(float vol); 52 | 53 | /// @brief Get the music current volume 54 | /// @return 0-1 float 55 | float music_volume_get(); 56 | 57 | /// @brief Get the sounds current volume 58 | /// @return 0-1 float 59 | float sound_volume_get(); 60 | 61 | #ifdef __cplusplus 62 | } 63 | #endif 64 | 65 | #endif -------------------------------------------------------------------------------- /src/effects.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "engine_gamestatus.h" 9 | #include "engine_gfx.h" 10 | #include "audioutils.h" 11 | #include "effects.h" 12 | 13 | effectdata_t effects; 14 | 15 | T3DModel* expmodel = NULL; 16 | 17 | void effects_init(){ 18 | if(!expmodel) expmodel = t3d_model_load("rom:/models/exp/exp3d.t3dm"); 19 | for(int i = 0; i < MAX_EFFECTS; i++){ 20 | effects.exp3d[i].enabled = false; 21 | if(!effects.exp3d[i].matx) effects.exp3d[i].matx = malloc_uncached(sizeof(T3DMat4FP)); 22 | 23 | effects.exp2d[i].enabled = false; 24 | 25 | } 26 | effects.screenshaketime = 0.0f; 27 | for(int i = 0; i < MAXPLAYERS; i++){ 28 | effects.rumbletime[i] = 0; 29 | effects.rumblestate[i] = false; 30 | } 31 | } 32 | void effects_update(){ 33 | for(int i = 0; i < MAX_EFFECTS; i++){ 34 | if(effects.exp3d[i].enabled){ 35 | effects.exp3d[i].time -= display_get_delta_time(); 36 | effects.exp3d[i].color = get_rainbow_color(effects.exp3d[i].time * 2); 37 | effects.exp3d[i].color.a = effects.exp3d[i].time * 127; 38 | if(effects.exp3d[i].time <= 0) effects.exp3d[i].enabled = false; 39 | } 40 | if(effects.exp2d[i].enabled){ 41 | effects.exp2d[i].time -= display_get_delta_time(); 42 | if(effects.exp2d[i].time <= 0) effects.exp2d[i].enabled = false; 43 | } 44 | } 45 | for(int i = 0; i < MAXPLAYERS; i++){ 46 | if(effects.rumbletime[i] > 0){ 47 | effects.rumbletime[i] -= display_get_delta_time(); 48 | if(!effects.rumblestate[i]) {joypad_set_rumble_active(i, true); effects.rumblestate[i] = true;} 49 | } else if(effects.rumblestate[i]) {joypad_set_rumble_active(i, false); effects.rumblestate[i] = false;} 50 | } 51 | if(effects.screenshaketime > 0){ 52 | effects.screenshaketime -= display_get_delta_time(); 53 | } else effects.screenshaketime = 0.0f; 54 | for(int i = 0; i < MAXPLAYERS; i++){ 55 | if(effects.ambientlight.r > 3) effects.ambientlight.r -=3; 56 | if(effects.ambientlight.g > 3) effects.ambientlight.g -=3; 57 | if(effects.ambientlight.b > 3) effects.ambientlight.b -=3; 58 | if(effects.ambientlight.a > 3) effects.ambientlight.a -=3; 59 | } 60 | } 61 | 62 | void effects_add_exp3d(T3DVec3 pos, color_t color){ 63 | int i = 0; while(effects.exp3d[i].enabled && i < MAX_EFFECTS - 1) i++; 64 | 65 | effects.exp3d[i].enabled = true; 66 | effects.exp3d[i].position = pos; 67 | effects.exp3d[i].color = color; 68 | effects.exp3d[i].time = 2.0f; 69 | sound_play("explode", false); 70 | } 71 | 72 | void effects_rumble_stop(){ 73 | for(int i = 0; i < MAXPLAYERS; i++){ 74 | joypad_set_rumble_active(i, false); 75 | effects.rumblestate[i] = false; 76 | } 77 | } 78 | 79 | void effects_add_exp2d(T3DVec3 pos, color_t color){ 80 | int i = 0; while(effects.exp2d[i].enabled && i < MAX_EFFECTS - 1) i++; 81 | 82 | effects.exp2d[i].enabled = true; 83 | effects.exp2d[i].position = pos; 84 | effects.exp2d[i].color = color; 85 | effects.exp2d[i].color.a = 255; 86 | effects.exp2d[i].time = 1.6f; 87 | } 88 | 89 | void effects_add_rumble(joypad_port_t port, float time){ 90 | if(port < 0) return; 91 | if(!gamestatus.state.game.settings.vibration) return; 92 | effects.rumbletime[port] += time; 93 | } 94 | 95 | void effects_add_shake(float time){ 96 | effects.screenshaketime += time; 97 | } 98 | 99 | void effects_add_ambientlight(color_t light){ 100 | effects.ambientlight.r += light.r; 101 | effects.ambientlight.g += light.g; 102 | effects.ambientlight.b += light.b; 103 | effects.ambientlight.a += light.a; 104 | } 105 | 106 | void effects_draw(){ 107 | for(int i = 0; i < MAX_EFFECTS; i++) 108 | if(effects.exp3d[i].enabled){ 109 | float scale = 2.3f - effects.exp3d[i].time; 110 | effects.exp3d[i].color.a = effects.exp3d[i].time * (255 / 2); 111 | rdpq_set_prim_color(effects.exp3d[i].color); 112 | t3d_mat4fp_from_srt_euler(effects.exp3d[i].matx, 113 | (float[3]){scale,scale,scale}, 114 | (float[3]){0,0,0}, 115 | (float[3]){effects.exp3d[i].position.x * 64, effects.exp3d[i].position.y * 64, effects.exp3d[i].position.z * 64}); 116 | t3d_matrix_push(effects.exp3d[i].matx); 117 | rdpq_sync_pipe(); // Hardware crashes otherwise 118 | rdpq_sync_tile(); // Hardware crashes otherwise 119 | t3d_model_draw(expmodel); 120 | rdpq_sync_pipe(); // Hardware crashes otherwise 121 | rdpq_sync_tile(); // Hardware crashes otherwise 122 | t3d_matrix_pop(1); 123 | } 124 | /* 125 | rdpq_set_mode_standard(); 126 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 127 | rdpq_mode_combiner(RDPQ_COMBINER_TEX_FLAT); 128 | rdpq_mode_dithering(dither); 129 | rdpq_mode_filter(FILTER_BILINEAR); 130 | rdpq_mode_zbuf(false, false); 131 | for(int i = 0; i < MAX_EFFECTS; i++) 132 | if(effects.exp2d[i].enabled){ 133 | T3DVec3 viewpos; 134 | t3d_viewport_calc_viewspace_pos(&viewport, &viewpos, &(effects.exp2d[i].position)); 135 | rdpq_set_prim_color(effects.exp2d[i].color); 136 | float xpos = viewpos.v[0]; 137 | float ypos = viewpos.v[1]; 138 | int index = 16 - (effects.exp2d[i].time * 10); 139 | if(index >= 15) index = 15; 140 | if(index <= 0) index = 0; 141 | 142 | if(gfx_pos_within_viewport(xpos, ypos)) 143 | rdpq_sprite_blit(sprites[spr_exp], xpos, ypos, &(rdpq_blitparms_t){.cx = 16, .cy = 16, .s0 = (index % 4)*32, (index / 4)*32, .width = 32, .height = 32}); 144 | }*/ 145 | } 146 | 147 | void effects_close(){ 148 | for(int i = 0; i < MAX_EFFECTS; i++){ 149 | effects.exp3d[i].enabled = false; 150 | if(effects.exp3d[i].matx) free_uncached(effects.exp3d[i].matx); 151 | effects.exp3d[i].matx = NULL; 152 | effects.exp2d[i].enabled = false; 153 | } 154 | for(int i = 0; i < MAXPLAYERS; i++) 155 | joypad_set_rumble_active(i, false); 156 | if(expmodel) t3d_model_free(expmodel); 157 | expmodel = NULL; 158 | } -------------------------------------------------------------------------------- /src/effects.h: -------------------------------------------------------------------------------- 1 | #ifndef EFFECTS_H 2 | #define EFFECTS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define MAX_EFFECTS 4 12 | #define MAXPLAYERS 4 13 | 14 | typedef struct effectdata_s{ 15 | struct{ 16 | bool enabled; 17 | T3DVec3 position; 18 | float time; 19 | color_t color; 20 | T3DMat4FP* matx; 21 | } exp3d[MAX_EFFECTS]; 22 | struct{ 23 | bool enabled; 24 | T3DVec3 position; 25 | float time; 26 | color_t color; 27 | } exp2d[MAX_EFFECTS]; 28 | 29 | float rumbletime[MAXPLAYERS]; 30 | bool rumblestate[MAXPLAYERS]; 31 | float screenshaketime; 32 | color_t ambientlight; 33 | } effectdata_t; 34 | 35 | extern effectdata_t effects; 36 | 37 | void effects_init(); 38 | void effects_update(); 39 | void effects_draw(); 40 | void effects_close(); 41 | 42 | void effects_add_exp3d(T3DVec3 pos, color_t color); 43 | void effects_add_exp2d(T3DVec3 pos, color_t color); 44 | 45 | void effects_add_rumble(joypad_port_t port, float time); 46 | void effects_add_shake(float time); 47 | void effects_add_ambientlight(color_t light); 48 | 49 | void effects_rumble_stop(); 50 | 51 | 52 | #endif -------------------------------------------------------------------------------- /src/engine_eeprom.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "intro.h" 3 | #include "engine_gamestatus.h" 4 | #include "audioutils.h" 5 | #include "engine_eeprom.h" 6 | 7 | bool eeprom_disabled = false; 8 | 9 | void print_eeprom_error(int result){ 10 | switch ( result ) 11 | { 12 | case EEPFS_ESUCCESS: 13 | debugf( "Success!\n" ); 14 | break; 15 | case EEPFS_EBADFS: 16 | debugf( "Failed with error: bad filesystem\n" ); 17 | break; 18 | case EEPFS_ENOMEM: 19 | debugf( "Failed with error: not enough memory\n" ); 20 | break; 21 | case EEPFS_EBADINPUT: 22 | debugf( "Failed with error: Input parameters invalid\n" ); 23 | break; 24 | case EEPFS_ENOFILE: 25 | debugf( "Failed with error: File does not exist\n" ); 26 | break; 27 | case EEPFS_EBADHANDLE: 28 | debugf( "Failed with error: Invalid file handle\n" ); 29 | break; 30 | case EEPFS_ECONFLICT: 31 | debugf( "Failed with error: Filesystem already initialized\n" ); 32 | break; 33 | default: 34 | debugf( "Failed with error: Failed in an unexpected manner\n" ); 35 | break; 36 | } 37 | } 38 | 39 | void engine_eeprom_init(){ 40 | int result = 0; 41 | const eeprom_type_t eeprom_type = eeprom_present(); 42 | if(eeprom_type == EEPROM_4K){ 43 | const eepfs_entry_t eeprom_16k_files[] = { 44 | { "/persistent.sv", (size_t)(sizeof(gamestatus.state_persistent)) }, 45 | { "/manualsave.sv", (size_t)(sizeof(gamestatus.state)) }, 46 | }; 47 | 48 | debugf( "EEPROM Detected: 4 Kibit (16 blocks)\n" ); 49 | debugf( "Initializing EEPROM Filesystem...\n" ); 50 | result = eepfs_init(eeprom_16k_files, 2); 51 | } else {debugf("EEPROM wrong format, expected 4KB. Please check your flashcart/emulator settings. You won't be able to save your game!"); eeprom_disabled = true;} 52 | if(result != EEPFS_ESUCCESS) {debugf("EEPROM not found: 4 Kibit (16 blocks)\nPlease check your flashcart/emulator settings. You won't be able to save your game!"); eeprom_disabled = true;} 53 | 54 | if(eeprom_disabled){ 55 | console_init(); 56 | console_set_render_mode(RENDER_MANUAL); 57 | bool pressed_a = false; 58 | float time = 0; 59 | while(!pressed_a && time < 8){ 60 | joypad_poll(); 61 | joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 62 | if(pressed.a) pressed_a = true; 63 | 64 | console_clear(); 65 | 66 | if(result != EEPFS_ESUCCESS) {printf("\n\n\nEEPROM not found: 4 Kibit (16 blocks)\nPlease check your flashcart/emulator settings. You won't be able to save your game!\n\nPress [A] to continue.");} 67 | else {printf("\n\n\nEEPROM wrong format, expected 4KB. Please check your flashcart/emulator settings. You won't be able to save your game! \n\nPress [A] to continue.");} 68 | console_render(); 69 | time += 0.02f; 70 | } 71 | rspq_wait(); 72 | console_close(); 73 | } 74 | 75 | if(eeprom_type == EEPROM_4K && result == EEPFS_ESUCCESS){ 76 | print_eeprom_error(result); 77 | joypad_poll(); auto held = joypad_get_buttons_held(JOYPAD_PORT_1); 78 | if ( !eepfs_verify_signature() || (held.l && held.r) ) 79 | { 80 | /* If not, erase it and start from scratch */ 81 | debugf( "Filesystem signature is invalid!\n" ); 82 | debugf( "Wiping EEPROM...\n" ); 83 | eepfs_wipe(); 84 | } 85 | } 86 | } 87 | 88 | void engine_eeprom_checksaves(){ 89 | 90 | } 91 | 92 | void engine_eeprom_delete_saves(){ 93 | gamestatus.state_persistent.manualsaved = false; 94 | gamestatus.state_persistent.autosaved = false; 95 | gamestatus.state_persistent.lastsavetype = SAVE_NONE; 96 | engine_eeprom_save_persistent(); 97 | } 98 | 99 | void engine_eeprom_delete_persistent(){ 100 | if(eeprom_disabled) return; 101 | 102 | eepfs_wipe(); 103 | } 104 | 105 | bool engine_eeprom_save_manual(){ 106 | if(eeprom_disabled) return false; 107 | gamestatus.state_persistent.manualsaved = true; 108 | gamestatus.state_persistent.lastsavetype = SAVE_MANUALSAVE; 109 | engine_eeprom_save_persistent(); 110 | debugf( "Writing '%s'\n", "/manualsave.sv" ); 111 | const int result = eepfs_write("/manualsave.sv", &gamestatus.state, (size_t)(sizeof(gamestatus.state))); 112 | if(result != EEPFS_ESUCCESS || gamestatus.state.magicnumber != STATE_MAGIC_NUMBER) { 113 | print_eeprom_error(result); 114 | assertf(0, "eeprom file write unsuccessful: %s\n", gamestatus.state.magicnumber != STATE_MAGIC_NUMBER? "MAGIC number mismatch" : "eeprom error"); 115 | } 116 | return result == EEPFS_ESUCCESS; 117 | } 118 | 119 | bool engine_eeprom_load_manual(){ 120 | if(eeprom_disabled) return false; 121 | debugf( "Reading '%s'\n", "/manualsave.sv" ); 122 | 123 | const int result = eepfs_read("/manualsave.sv", &gamestatus.state, (size_t)(sizeof(gamestatus.state))); 124 | if(result != EEPFS_ESUCCESS || gamestatus.state.magicnumber != STATE_MAGIC_NUMBER) 125 | { 126 | print_eeprom_error(result); 127 | debugf("eeprom file read unsuccessful: %s\n", gamestatus.state.magicnumber != STATE_MAGIC_NUMBER? "MAGIC number mismatch" : "eeprom error"); 128 | state_init(); 129 | } 130 | return result == EEPFS_ESUCCESS; 131 | } 132 | 133 | bool engine_eeprom_save_persistent(){ 134 | if(eeprom_disabled) return false; 135 | debugf( "Writing '%s'\n", "/persistent.sv" ); 136 | 137 | const int result = eepfs_write("/persistent.sv", &gamestatus.state_persistent, (size_t)(sizeof(gamestatus.state_persistent))); 138 | if(result != EEPFS_ESUCCESS || gamestatus.state_persistent.magicnumber != STATE_PERSISTENT_MAGIC_NUMBER) 139 | { 140 | print_eeprom_error(result); 141 | debugf("eeprom file write unsuccessful: %s\n", gamestatus.state_persistent.magicnumber != STATE_MAGIC_NUMBER? "MAGIC number mismatch" : "eeprom error"); 142 | } 143 | return result == EEPFS_ESUCCESS; 144 | } 145 | 146 | bool engine_eeprom_load_persistent(){ 147 | if(eeprom_disabled) return false; 148 | debugf( "Reading '%s'\n", "/persistent.sv" ); 149 | 150 | const int result = eepfs_read("/persistent.sv", &gamestatus.state_persistent, (size_t)(sizeof(gamestatus.state_persistent))); 151 | if(result != EEPFS_ESUCCESS || gamestatus.state_persistent.magicnumber != STATE_PERSISTENT_MAGIC_NUMBER) 152 | { 153 | print_eeprom_error(result); 154 | debugf("eeprom file read unsuccessful: %s\n", gamestatus.state_persistent.magicnumber != STATE_MAGIC_NUMBER? "MAGIC number mismatch" : "eeprom error"); 155 | state_init(); 156 | } 157 | return result == EEPFS_ESUCCESS; 158 | } 159 | -------------------------------------------------------------------------------- /src/engine_eeprom.h: -------------------------------------------------------------------------------- 1 | #ifndef ENGINE_EEPROM 2 | #define ENGINE_EEPROM 3 | /// made by SpookyIluha 4 | /// EEPROM save functions 5 | 6 | #include 7 | #include "engine_gamestatus.h" 8 | #include "audioutils.h" 9 | 10 | /// @brief Init the eeprom system and saves 11 | void engine_eeprom_init(); 12 | 13 | /// @brief Unused 14 | void engine_eeprom_checksaves(); 15 | 16 | /// @brief Save a manual file, should only be used by the game, or inside the game loop (i.e after calling newgame) 17 | bool engine_eeprom_save_manual(); 18 | 19 | /// @brief Load a manual file replacing the gamestatus 20 | bool engine_eeprom_load_manual(); 21 | 22 | /// Save any persistent data, such as language, global game state etc. 23 | bool engine_eeprom_save_persistent(); 24 | 25 | /// Load any persistent data into gamestatus, such as language, global game state etc. 26 | bool engine_eeprom_load_persistent(); 27 | 28 | /// Delete all saves except persistent 29 | void engine_eeprom_delete_saves(); 30 | 31 | /// Delete persistent save (and all saves as well) 32 | void engine_eeprom_delete_persistent(); 33 | 34 | #endif -------------------------------------------------------------------------------- /src/engine_gamestatus.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "engine_gamestatus.h" 5 | 6 | gamestatus_t gamestatus; 7 | 8 | float fclampr(float x, float min, float max){ 9 | if (x > max) return max; 10 | if (x < min) return min; 11 | return x; 12 | } 13 | 14 | void state_init(){ 15 | gamestatus.currenttime = 0.0f; 16 | gamestatus.realtime = TICKS_TO_MS(timer_ticks()) / 1000.0f; 17 | 18 | gamestatus.deltatime = 0.0f; 19 | gamestatus.deltarealtime = 0.0f; 20 | 21 | gamestatus.gamespeed = 1.0f; 22 | gamestatus.paused = false; 23 | 24 | memset(&gamestatus.state, 0, sizeof(gamestatus.state)); 25 | memset(&gamestatus.state_persistent, 0, sizeof(gamestatus.state_persistent)); 26 | gamestatus.state.magicnumber = STATE_MAGIC_NUMBER; 27 | gamestatus.state_persistent.magicnumber = STATE_PERSISTENT_MAGIC_NUMBER; 28 | gamestatus.statetime = 0; 29 | 30 | gamestatus.fixedframerate = 30; 31 | gamestatus.fixedtime = 0.0f; 32 | gamestatus.fixeddeltatime = 0.0f; 33 | 34 | gamestatus.state.game.settings.deadzone = 0.1f; 35 | gamestatus.state.game.settings.duration = TWO_MINUTES; 36 | gamestatus.state.game.settings.graphics = DEFAULT; 37 | gamestatus.state.game.settings.vibration = true; 38 | 39 | gamestatus.state.audio.bgmusic_vol = 0.5f; 40 | gamestatus.state.audio.sound_vol = 0.33f; 41 | } 42 | 43 | void timesys_update(){ 44 | double last = gamestatus.realtime; 45 | double current = TICKS_TO_MS(timer_ticks()) / 1000.0f; 46 | double deltareal = current - last; 47 | double delta = deltareal * gamestatus.gamespeed; 48 | gamestatus.statetime = fclampr(gamestatus.statetime, 0.0f, INFINITY); 49 | 50 | if(!gamestatus.paused){ 51 | double lastgametime = gamestatus.currenttime; 52 | gamestatus.currenttime += delta; 53 | gamestatus.deltatime = gamestatus.currenttime - lastgametime; 54 | } else 55 | gamestatus.deltatime = 0.0f; 56 | 57 | gamestatus.realtime = current; 58 | gamestatus.deltarealtime = deltareal; 59 | } 60 | 61 | bool timesys_update_fixed(){ 62 | gamestatus.fixeddeltatime = 0.0f; 63 | 64 | if(gamestatus.paused) return false; 65 | 66 | double fixed = (1.0f / gamestatus.fixedframerate); 67 | double nexttime = gamestatus.fixedtime + fixed; 68 | 69 | if(nexttime > CURRENT_TIME) return false; 70 | 71 | gamestatus.fixedtime = nexttime; 72 | gamestatus.fixeddeltatime = fixed; 73 | return true; 74 | } 75 | 76 | void timesys_close(){ 77 | 78 | } -------------------------------------------------------------------------------- /src/engine_gamestatus.h: -------------------------------------------------------------------------------- 1 | #ifndef ENGINE_GAMESTATUS_H 2 | #define ENGINE_GAMESTATUS_H 3 | /// made by SpookyIluha 4 | /// The global gamestatus variables that can be used at runtime and EEPROM saving 5 | 6 | #ifdef __cplusplus 7 | extern "C"{ 8 | #endif 9 | 10 | #define MAX_FONTS 32 11 | #define MAX_CHARACTERS_LIMIT 8 12 | #define SHORTSTR_LENGTH 32 13 | #define LONGSTR_LENGTH 64 14 | #define CHARACTER_MAX_VARS 8 15 | 16 | #define STATE_MAGIC_NUMBER ((uint16_t)sizeof(gamestate_t)) 17 | #define STATE_PERSISTENT_MAGIC_NUMBER ((uint16_t)sizeof(gamestate_persistent_t)) 18 | 19 | typedef enum { 20 | ONE_MINUTE = 1, 21 | TWO_MINUTES = 2, 22 | THREE_MINUTES = 3, 23 | FOUR_MINUTES = 4, 24 | FIVE_MINUTES = 5 25 | } matchduration_t; 26 | 27 | typedef enum { 28 | FASTEST = 0, 29 | DEFAULT = 1, 30 | NICEST = 2, 31 | } graphicssetting_t; 32 | 33 | typedef struct gamestate_s{ 34 | unsigned short magicnumber; 35 | struct{ 36 | float bgmusic_vol; 37 | float sound_vol; 38 | } audio; 39 | 40 | struct{ 41 | struct{ 42 | matchduration_t duration; 43 | graphicssetting_t graphics; 44 | int vibration; 45 | float deadzone; 46 | } settings; 47 | } game; 48 | 49 | bool stadium_unlocked; 50 | 51 | } gamestate_t; 52 | 53 | typedef enum{ 54 | SAVE_NONE = 0, 55 | SAVE_AUTOSAVE, 56 | SAVE_MANUALSAVE 57 | } saveenum_t; 58 | 59 | typedef struct{ 60 | unsigned short magicnumber; 61 | uint64_t global_game_state; // game incomplete, complete, broken etc. 62 | saveenum_t lastsavetype; // true - manual, false - autosave 63 | bool autosaved; 64 | bool manualsaved; 65 | int current_language; // "en" - english, etc. 66 | bool modded; 67 | } gamestate_persistent_t; 68 | 69 | typedef struct gamestatus_s{ 70 | double currenttime; 71 | double realtime; 72 | double deltatime; 73 | double deltarealtime; 74 | double fixeddeltatime; 75 | double fixedtime; 76 | double fixedframerate; 77 | 78 | double gamespeed; 79 | bool paused; 80 | 81 | gamestate_t state; 82 | gamestate_persistent_t state_persistent; 83 | 84 | float statetime; 85 | } gamestatus_t; 86 | 87 | /// @brief Global game state, includes a persistent state, a state for the game, state of the engine's datapoints 88 | extern gamestatus_t gamestatus; 89 | 90 | #define CURRENT_TIME gamestatus.currenttime 91 | #define CURRENT_TIME_REAL gamestatus.realtime 92 | 93 | #define GAMESPEED gamestatus.gamespeed 94 | #define GAME_PAUSED gamestatus.paused 95 | 96 | #define DELTA_TIME gamestatus.deltatime 97 | #define DELTA_TIME_REAL gamestatus.deltarealtime 98 | 99 | #define DELTA_TIME_FIXED gamestatus.fixeddeltatime 100 | #define CURRENT_TIME_FIXED gamestatus.fixedtime 101 | 102 | /// Init the global game state to 0 103 | void state_init(); 104 | 105 | #ifdef __cplusplus 106 | } 107 | #endif 108 | 109 | #endif -------------------------------------------------------------------------------- /src/engine_gfx.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "engine_gfx.h" 6 | #include 7 | 8 | int frame = 0; 9 | float globaltime = 0; 10 | 11 | float frandr( float min, float max ) 12 | { 13 | float scale = rand() / (float) RAND_MAX; /* [0, 1.0] */ 14 | return min + scale * ( max - min ); /* [min, max] */ 15 | } 16 | 17 | color_t get_rainbow_color(float s) { 18 | float r = fm_sinf(s + 0.0f) * 127.0f + 128.0f; 19 | float g = fm_sinf(s + 2.0f) * 127.0f + 128.0f; 20 | float b = fm_sinf(s + 4.0f) * 127.0f + 128.0f; 21 | return RGBA32(r, g, b, 255); 22 | } 23 | 24 | int randm(int max){ 25 | return (rand() % max); 26 | } 27 | 28 | int randr(int min, int max){ 29 | int range = min - max; 30 | return (rand() % range) + min; 31 | } 32 | 33 | void iswap(int* a, int* b){ 34 | int t = *a; 35 | *a = *b; 36 | *b = t; 37 | } 38 | 39 | void rdpq_sprite_blit_anchor(sprite_t* sprite, rdpq_align_t horizontal, rdpq_valign_t vertical, float x, float y, rdpq_blitparms_t* parms){ 40 | assert(sprite); 41 | int width = sprite->width; 42 | int height = sprite->height; 43 | switch(horizontal){ 44 | case ALIGN_RIGHT: 45 | x -= width; break; 46 | case ALIGN_CENTER: 47 | x -= width / 2; break; 48 | default: break; 49 | } 50 | switch(vertical){ 51 | case VALIGN_BOTTOM: 52 | y -= height; break; 53 | case VALIGN_CENTER: 54 | y -= height / 2; break; 55 | default: break; 56 | } 57 | rdpq_sprite_blit(sprite, x,y,parms); 58 | } 59 | 60 | void rdpq_tex_blit_anchor(const surface_t* surface, rdpq_align_t horizontal, rdpq_valign_t vertical, float x, float y, rdpq_blitparms_t* parms){ 61 | assert(surface); 62 | int width = surface->width; 63 | int height = surface->height; 64 | switch(horizontal){ 65 | case ALIGN_RIGHT: 66 | x -= width; break; 67 | case ALIGN_CENTER: 68 | x -= width / 2; break; 69 | default: break; 70 | } 71 | switch(vertical){ 72 | case VALIGN_BOTTOM: 73 | y -= height; break; 74 | case VALIGN_CENTER: 75 | y -= height / 2; break; 76 | default: break; 77 | } 78 | rdpq_tex_blit(surface, x,y,parms); 79 | } 80 | -------------------------------------------------------------------------------- /src/engine_gfx.h: -------------------------------------------------------------------------------- 1 | #ifndef ENGINE_GFX_H 2 | #define ENGINE_GFX_H 3 | /// made by SpookyIluha 4 | /// Various small GFX, util and string functions 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #define SFX_CHANNEL_MUSIC 0 11 | #define SFX_CHANNEL_SOUND 2 12 | #define SFX_MAX_CHANNELS 8 13 | 14 | extern int frame; 15 | extern float globaltime; 16 | 17 | #define T3D_TOUNITS(x) (64.0f*x) 18 | #define T3D_FROMUNITS(x) (x*(1.0/64.0f)) 19 | 20 | #define TPE_TOUNITS(x) (512.0f*x) 21 | #define TPE_FROMUNITS(x) (x*(1.0/512.0f)) 22 | 23 | float frandr( float min, float max ); 24 | 25 | /// @brief rdpq_sprite_blit but with anchor support 26 | /// @param sprite 27 | /// @param horizontal horizontal anchor 28 | /// @param vertical vertical anchor 29 | /// @param x 30 | /// @param y 31 | /// @param parms 32 | void rdpq_sprite_blit_anchor(sprite_t* sprite, rdpq_align_t horizontal, rdpq_valign_t vertical, float x, float y, rdpq_blitparms_t* parms); 33 | 34 | /// @brief rdpq_tex_blit but with anchor support 35 | /// @param surface 36 | /// @param horizontal horizontal anchor 37 | /// @param vertical vertical anchor 38 | /// @param x 39 | /// @param y 40 | /// @param parms 41 | void rdpq_tex_blit_anchor(const surface_t* surface, rdpq_align_t horizontal, rdpq_valign_t vertical, float x, float y, rdpq_blitparms_t* parms); 42 | 43 | /// @brief Returns whether a point is within the screen 44 | /// @param x 45 | /// @param y 46 | /// @return true if it is 47 | inline bool gfx_pos_within_viewport(float x, float y){ 48 | return x > 0 && x < display_get_width() && y > 0 && y < display_get_height(); 49 | } 50 | 51 | /// @brief Returns whether a point is within the rectangle 52 | /// @param x 53 | /// @param y 54 | /// @return true if it is 55 | inline bool gfx_pos_within_rect(float x, float y, float xa, float ya, float xb, float yb){ 56 | return x > xa && x < xb && y > ya && y < yb; 57 | } 58 | 59 | /// @brief Generic linear interp function 60 | /// @param a 61 | /// @param b 62 | /// @param t 63 | /// @return 64 | inline float gfx_lerp(float a, float b, float t) 65 | { 66 | if(fabs(a - b) < FM_EPSILON) return b; 67 | return a + (b - a) * t; 68 | } 69 | 70 | 71 | inline float fclampr(float x, float min, float max){ 72 | if (x > max) return max; 73 | if (x < min) return min; 74 | return x; 75 | } 76 | 77 | inline float fwrap(float x, float min, float max) { 78 | if (min > max) { 79 | return fwrap(x, max, min); 80 | } 81 | return (x >= 0 ? min : max) + fmod(x, max - min); 82 | } 83 | 84 | inline int iwrap(int x, float min, float max) { 85 | if(x > max) return min; 86 | if(x < min) return max; 87 | return x; 88 | } 89 | 90 | void iswap(int* a, int* b); 91 | 92 | color_t get_rainbow_color(float s); 93 | 94 | /// @brief Random int [0-max) 95 | /// @param max 96 | /// @return 97 | int randm(int max); 98 | 99 | /// @brief Random int [min-max) 100 | /// @param max 101 | /// @return 102 | int randr(int min, int max); 103 | 104 | #endif -------------------------------------------------------------------------------- /src/engine_locale.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "ctype.h" 3 | #include "inih/ini.h" 4 | #include "audioutils.h" 5 | #include "engine_gamestatus.h" 6 | #include "engine_eeprom.h" 7 | #include "engine_gfx.h" 8 | #include "engine_locale.h" 9 | 10 | ini_t* inifile = NULL; 11 | char languages[64][64]; 12 | char languages_names[64][64]; 13 | int language_count = 0; 14 | 15 | rdpq_font_t* fonts[16]; 16 | 17 | void font_clear(){ 18 | for(int i = 0; i < 16; i++) if(fonts[i]) {rdpq_text_unregister_font(i+1); rdpq_font_free(fonts[i]); fonts[i] = NULL;} 19 | } 20 | 21 | void font_setup(){ 22 | for(int i = 0; i < 16; i++) if(fonts[i]) {rdpq_text_unregister_font(i+1); rdpq_font_free(fonts[i]); fonts[i] = NULL;} 23 | const char* fontname = NULL; 24 | char fontkey[64]; 25 | int i = 0; 26 | do{ 27 | sprintf(fontkey, "font%i", i); 28 | fontname = inistr("Fonts", fontkey); 29 | if(fontname){ 30 | fonts[i] = rdpq_font_load(fontname); 31 | rdpq_text_register_font(i+1, fonts[i]); 32 | rdpq_fontstyle_t style; style.color = RGBA32(0,0,0,255); 33 | rdpq_font_style(fonts[i], 0, &style); 34 | style.color = RGBA32(255,255,255,255); 35 | rdpq_font_style(fonts[i], 1, &style); 36 | style.color = RGBA32(200,200,200,255); 37 | rdpq_font_style(fonts[i], 2, &style); 38 | } 39 | i++; 40 | } while(fontname); 41 | } 42 | 43 | void engine_load_dictionary(){ 44 | if(inifile) {ini_free(inifile); inifile = NULL;} 45 | inifile = ini_load(languages[gamestatus.state_persistent.current_language]); 46 | } 47 | 48 | const char* dictstr(const char* name){ 49 | const char* str = ini_get(inifile, "Dictionary", name); 50 | if(!str) return "null"; 51 | else return str; 52 | } 53 | 54 | const char* inistr(const char* section, const char* name){ 55 | const char* str = ini_get(inifile, section, name); 56 | if(!str) return NULL; 57 | else return str; 58 | } 59 | 60 | int engine_load_languages_callback(const char *fn, dir_t *dir, void *data){ 61 | debugf("Found language %s\n", fn); 62 | strcpy(&(languages[language_count][0]), fn); 63 | 64 | char nameonly[128] = {0}; 65 | strcpy(nameonly, strrchr(fn, '/') + 1); 66 | *strrchr(nameonly, '.') = '\0'; 67 | char *s = nameonly; while (*s) { *s = toupper((unsigned char) *s);s++;} 68 | strcpy(languages_names[language_count], nameonly); 69 | 70 | language_count++; 71 | return DIR_WALK_CONTINUE; 72 | } 73 | 74 | void engine_load_languages(){ 75 | language_count = 0; 76 | gamestatus.state_persistent.current_language = 0; 77 | dir_glob("*.ini", "rom:/locale/", engine_load_languages_callback, NULL); 78 | } 79 | 80 | void engine_set_language(int index){ 81 | gamestatus.state_persistent.current_language = index; 82 | engine_load_dictionary(); 83 | } 84 | 85 | char* engine_get_language(){ 86 | return &(languages_names[gamestatus.state_persistent.current_language][0]); 87 | } -------------------------------------------------------------------------------- /src/engine_locale.h: -------------------------------------------------------------------------------- 1 | #ifndef ENGINE_LOCALE_H 2 | #define ENGINE_LOCALE_H 3 | /// made by SpookyIluha 4 | /// language code 5 | 6 | #include 7 | #include "audioutils.h" 8 | #include "engine_gamestatus.h" 9 | #include "inih/ini.h" 10 | #include "engine_eeprom.h" 11 | #include "engine_gfx.h" 12 | 13 | extern ini_t* inifile; 14 | extern char languages[64][64]; 15 | extern char languages_names[64][64]; 16 | extern int language_count; 17 | 18 | extern rdpq_font_t* fonts[16]; 19 | 20 | void font_clear(); 21 | void font_setup(); 22 | 23 | /// @brief Load a dictionary with selected language 24 | void engine_load_dictionary(); 25 | 26 | /// @brief Get a translated string with the current language by a key 27 | /// @param name key of the translated string as found in dictionary.ini 28 | /// @return translated string 29 | const char* dictstr(const char* name); 30 | 31 | const char* inistr(const char* section, const char* name); 32 | 33 | /// @brief Load languages list into the engine 34 | void engine_load_languages(); 35 | 36 | /// @brief Set the desired language of the game 37 | /// @param lang shortname of the language as found in languages.ini (i.e en/ru/de/fr etc) 38 | void engine_set_language(int index); 39 | 40 | /// @brief Get the current language code 41 | /// @return shortname of the language as found in languages.ini (i.e en/ru/de/fr etc) 42 | char* engine_get_language(); 43 | 44 | #endif -------------------------------------------------------------------------------- /src/entity_logic.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "engine_gfx.h" 7 | #include "entity_logic.h" 8 | #include "teams.h" 9 | #include "playtime_logic.h" 10 | #include "particles_sim.h" 11 | 12 | TPE_Body bodies[64] = {0}; 13 | int bodiescount = 0; 14 | 15 | playball_t playball; 16 | carplayer_t carplayer[4]; 17 | 18 | uint32_t playercolors[4] = { 19 | 0xff1e1eFF, 20 | 0x1eff1eFF, 21 | 0x1e1effFF, 22 | 0xffff1eFF 23 | }; 24 | 25 | void playball_init(){ 26 | if(!playball.modelball) playball.modelball = t3d_model_load("rom:/models/ball.t3dm"); 27 | if(!playball.modelshadow) playball.modelshadow = t3d_model_load("rom:/models/ball_shadow.t3dm"); 28 | t3d_mat4_identity(&playball.modelMatball); 29 | t3d_mat4_identity(&playball.modelshadowMat); 30 | if(!playball.modelMatFPball) playball.modelMatFPball = malloc_uncached(sizeof(T3DMat4FP)*5); 31 | if(!playball.modelMatFPshadow) playball.modelMatFPshadow = malloc_uncached(sizeof(T3DMat4FP)*5); 32 | playball.ballPoslast = (T3DVec3){.x = 0,.y = TPE_F * 8,.z = 0}; 33 | fm_quat_identity(&playball.ballquat); 34 | playball.joint = TPE_joint((TPE_Vec3){0,TPE_F * 8,0},TPE_F / 1.3); 35 | playball.bodyidx = bodiescount; bodiescount++; 36 | TPE_bodyInit(&bodies[playball.bodyidx],&playball.joint,1,0,0, TPE_F / 2); 37 | bodies[playball.bodyidx].friction = TPE_F / 25; // decrease friction for fun 38 | bodies[playball.bodyidx].elasticity = TPE_F / 3; 39 | } 40 | 41 | void playball_free(){ 42 | if(playball.modelball) t3d_model_free(playball.modelball); 43 | if(playball.modelshadow) t3d_model_free(playball.modelshadow); 44 | if(playball.modelMatFPball) free_uncached(playball.modelMatFPball); 45 | if(playball.modelMatFPshadow) free_uncached(playball.modelMatFPshadow); 46 | if(playball.blockmodel) rspq_block_free(playball.blockmodel); 47 | if(playball.blockshadow) rspq_block_free(playball.blockshadow); 48 | memset(&playball, 0, sizeof(playball)); 49 | } 50 | 51 | void playball_reset(){ 52 | playball.init = true; 53 | t3d_mat4_identity(&playball.modelMatball); 54 | t3d_mat4_identity(&playball.modelshadowMat); 55 | playball.ballPoslast = (T3DVec3){.x = 0,.y = TPE_F * 8,.z = 0}; 56 | fm_quat_identity(&playball.ballquat); 57 | playball.joint = TPE_joint((TPE_Vec3){0,TPE_F * 8,0},TPE_F / 1.5); 58 | TPE_bodyInit(&bodies[playball.bodyidx],&playball.joint,1,0,0, TPE_F / 2); 59 | bodies[playball.bodyidx].friction = TPE_F / 25; // decrease friction for fun 60 | bodies[playball.bodyidx].elasticity = TPE_F / 3; 61 | } 62 | 63 | void playball_update(){ 64 | if(!playball.init) return; 65 | 66 | if(TPE_bodyIsActive(&bodies[playball.bodyidx])) 67 | TPE_bodyApplyGravity(&bodies[playball.bodyidx],TPE_F / 100); 68 | 69 | TPE_Vec3 ballPos = bodies[playball.bodyidx].joints[0].position; 70 | playball.ballPos_t3d = (T3DVec3){.x = ballPos.x * (1.0/TPE_FRACTIONS_PER_UNIT), .y = ballPos.y * (1.0/TPE_FRACTIONS_PER_UNIT), .z = ballPos.z * (1.0/TPE_FRACTIONS_PER_UNIT)}; 71 | 72 | // ======== Update ======== // 73 | { 74 | T3DVec3 diff; t3d_vec3_diff(&diff, &playball.ballPoslast, &playball.ballPos_t3d); 75 | diff.y = 0; 76 | float distance = t3d_vec3_len(&diff); 77 | if(distance > 0){ 78 | t3d_vec3_norm(&diff); 79 | T3DVec3 diff_cross; t3d_vec3_cross(&diff_cross, &diff, &(T3DVec3){.x = 0,.y = 1,.z = 0}); 80 | diff_cross.y = 0; 81 | t3d_vec3_norm(&diff_cross); 82 | t3d_quat_rotate_euler(&playball.ballquat, (float*)&diff_cross, distance); 83 | t3d_quat_normalize(&playball.ballquat); 84 | } 85 | } playball.ballPoslast = playball.ballPos_t3d; 86 | //t3d_quat_rotate_euler(&quat, (float[3]){0,0,1}, -ballPost3d.x); 87 | //t3d_quat_rotate_euler(&quat, (float[3]){1,0,0}, ballPost3d.z); 88 | //t3d_quat_rotate_euler(&quat, (float[3]){0,1,0}, 0); 89 | 90 | t3d_mat4_from_srt(&playball.modelMatball, 91 | (float[3]){1, 1, 1}, 92 | (float*)&playball.ballquat, 93 | (float[3]){playball.ballPos_t3d.x* 64, (playball.ballPos_t3d.y + 0.3)* 64, playball.ballPos_t3d.z* 64} 94 | ); 95 | t3d_mat4_from_srt_euler(&playball.modelshadowMat, 96 | (float[3]){1, 1, 1}, 97 | (float[3]){0, 0, 0}, 98 | (float[3]){playball.ballPos_t3d.x* 64, 0, playball.ballPos_t3d.z* 64} 99 | ); 100 | t3d_mat4_to_fixed(&playball.modelMatFPball[(frame) % 5], &playball.modelMatball); 101 | t3d_mat4_to_fixed(&playball.modelMatFPshadow[(frame) % 5], &playball.modelshadowMat); 102 | } 103 | 104 | void playball_draw(){ 105 | if(!playball.init) return; 106 | t3d_matrix_set(&playball.modelMatFPshadow[(frame) % 5], true); 107 | if(!playball.blockshadow){ 108 | rspq_block_begin(); 109 | t3d_model_draw(playball.modelshadow); 110 | playball.blockshadow = rspq_block_end(); 111 | } rspq_block_run(playball.blockshadow); 112 | t3d_matrix_set(&playball.modelMatFPball[(frame) % 5], true); 113 | if(!playball.blockmodel){ 114 | rspq_block_begin(); 115 | t3d_model_draw(playball.modelball); 116 | rdpq_sync_pipe(); 117 | playball.blockmodel = rspq_block_end(); 118 | } rspq_block_run(playball.blockmodel); 119 | //t3d_matrix_pop(1); 120 | } 121 | 122 | void playball_draw_shadow(){ 123 | if(!playball.init) return; 124 | t3d_matrix_set(&playball.modelMatFPshadow[(frame) % 5], true); 125 | if(!playball.blockshadow){ 126 | rspq_block_begin(); 127 | t3d_model_draw(playball.modelshadow); 128 | playball.blockshadow = rspq_block_end(); 129 | } rspq_block_run(playball.blockshadow); 130 | //t3d_matrix_pop(1); 131 | } 132 | 133 | /*void carplayer_particles(carplayer_t* car){ 134 | switch(car->particle_type) 135 | { 136 | case 2: // Flame 137 | time += deltaTime * 1.0f; 138 | float posX = car->Pos_t3d.x; 139 | float posZ = car->Pos_t3d.z; 140 | 141 | simulate_particles_fire(car->particles, PARTICLES_MAX, posX, posZ); 142 | particleMatScale = (T3DVec3){{0.9f, partMatScaleVal, 0.9f}}; 143 | particlePos.y = partMatScaleVal * 130.0f; 144 | isSpriteRot = true; 145 | break; 146 | case 1: // Random 147 | generate_particles_random(car->particles, PARTICLES_MAX); 148 | break; 149 | default: break; 150 | } 151 | }*/ 152 | 153 | void carplayer_init(carplayer_t* car, int index){ 154 | car->team = index < 2? matchinfo.tleft.team : matchinfo.tright.team; 155 | car->is_team_left = index < 2; 156 | car->playercontroller = matchinfo.playercontrollers[index]; 157 | car->ai_maxturnspeed = car->team->maxturnspeed + frandr(-0.25, 0.1); 158 | car->ai_difficulty = car->team->difficulty + frandr(-0.025, 0.03); 159 | if(!car->model) car->model = t3d_model_load(car->team->modelfilename); 160 | if(!car->debugjointmodel) car->debugjointmodel = t3d_model_load("rom:/models/celica/debugball.t3dm"); 161 | if(!car->shadowmodel) car->shadowmodel = t3d_model_load("rom:/models/celica/celica_shadow.t3dm"); 162 | if(!car->nitromodel) car->nitromodel = t3d_model_load("rom:/models/celica/celica_nitro.t3dm"); 163 | t3d_mat4_identity(&car->modelMat); 164 | t3d_mat4_identity(&car->shadowmodelMat); 165 | if(!car->modelMatFP) car->modelMatFP = malloc_uncached(sizeof(T3DMat4FP)*5); 166 | if(!car->shadowmodelMatFP) car->shadowmodelMatFP = malloc_uncached(sizeof(T3DMat4FP)*5); 167 | if(!car->particles) { 168 | uint32_t allocSize = sizeof(TPXParticle) * PARTICLES_MAX / 2; 169 | car->particles = malloc_uncached(allocSize); 170 | } 171 | if(!car->part_firespr) car->part_firespr = sprite_load("rom:/textures/parts/fire.i8.sprite"); 172 | if(!car->part_glowspr) car->part_glowspr = sprite_load("rom:/textures/parts/flare.i8.sprite"); 173 | //carplayer.debugjointmodelMatFP = malloc_uncached(sizeof(T3DMat4FP)*4); 174 | T3DVec3 startpos; 175 | startpos.x = index < 2? -12 : 12; 176 | startpos.z = index % 2? 7 : -7; 177 | 178 | car->Poslast = (T3DVec3){.x = TPE_F * (startpos.x),.y = TPE_F * 0.5,.z = TPE_F * (startpos.z)}; 179 | fm_quat_identity(&car->quat); 180 | car->joints[0] = TPE_joint((TPE_Vec3){TPE_F * (startpos.x),TPE_F * 0.5, TPE_F * (startpos.z)}, TPE_F / 1.5); 181 | car->joints[1] = TPE_joint((TPE_Vec3){TPE_F * (index < 2? startpos.x + 2 : startpos.x - 2),TPE_F * 0.5, TPE_F * (startpos.z) * 0.85f}, TPE_F / 1.5); 182 | car->connections[0].joint1 = 0; 183 | car->connections[0].joint2 = 1; 184 | car->connections[0].length = TPE_F * 1; 185 | car->bodyidx = bodiescount; bodiescount++; 186 | TPE_bodyInit(&bodies[car->bodyidx], car->joints,2,car->connections,1,1 * TPE_F); 187 | bodies[car->bodyidx].friction = TPE_F / 10; 188 | bodies[car->bodyidx].elasticity = TPE_F / 10; 189 | } 190 | 191 | void carplayer_free(carplayer_t* car){ 192 | if(car->model) t3d_model_free(car->model); 193 | if(car->debugjointmodel) t3d_model_free(car->debugjointmodel); 194 | if(car->shadowmodel) t3d_model_free(car->shadowmodel); 195 | if(car->nitromodel) t3d_model_free(car->nitromodel); 196 | if(car->modelMatFP) free_uncached(car->modelMatFP); 197 | if(car->shadowmodelMatFP) free_uncached(car->shadowmodelMatFP); 198 | if(car->debugjointmodelMatFP) free_uncached(car->debugjointmodelMatFP); 199 | if(car->particles) free_uncached(car->particles); 200 | if(car->part_firespr) sprite_free(car->part_firespr); 201 | if(car->part_glowspr) sprite_free(car->part_glowspr); 202 | if(car->blockmodel) rspq_block_free(car->blockmodel); 203 | if(car->blockshadow) rspq_block_free(car->blockshadow); 204 | memset(car, 0, sizeof(carplayer_t)); 205 | } 206 | 207 | void carplayer_reset(carplayer_t* car, int index){ 208 | t3d_mat4_identity(&car->modelMat); 209 | t3d_mat4_identity(&car->shadowmodelMat); 210 | T3DVec3 startpos; 211 | startpos.x = index < 2? -12 : 12; 212 | startpos.z = index % 2? 7 : -7; 213 | 214 | car->ballcollided = false; 215 | car->carcollided = -1; 216 | 217 | car->ai_maxturnspeed = car->team->maxturnspeed + frandr(-0.25, 0.1); 218 | car->ai_difficulty = car->team->difficulty + frandr(-0.02, 0.03); 219 | 220 | car->Poslast = (T3DVec3){.x = TPE_F * (startpos.x),.y = TPE_F * 0.5,.z = TPE_F * (startpos.z)}; 221 | fm_quat_identity(&car->quat); 222 | car->joints[0] = TPE_joint((TPE_Vec3){TPE_F * (startpos.x),TPE_F * 0.5, TPE_F * (startpos.z)}, TPE_F / 1.5); 223 | car->joints[1] = TPE_joint((TPE_Vec3){TPE_F * (index < 2? startpos.x + 2 : startpos.x - 2),TPE_F * 0.5, TPE_F * (startpos.z) * 0.85f}, TPE_F / 1.5); 224 | TPE_bodyInit(&bodies[car->bodyidx], car->joints,2,car->connections,1,1 * TPE_F); 225 | bodies[car->bodyidx].friction = TPE_F / 10; 226 | bodies[car->bodyidx].elasticity = TPE_F / 10; 227 | generate_particles_random(car->particles, PARTICLES_MAX); 228 | } 229 | 230 | void carplayer_ai(int index, joypad_inputs_t* outinput, joypad_buttons_t* outpressed){ 231 | T3DVec3 carpos = carplayer[index].Pos_t3d; 232 | t3d_vec3_lerp(&carplayer[index].ai_targetpos, &carplayer[index].ai_targetpos, &playball.ballPos_t3d, carplayer[index].ai_difficulty); 233 | T3DVec3 target = carplayer[index].ai_targetpos; 234 | T3DVec3 goal = (T3DVec3){.x = -19, .y = 0, .z = 0}; 235 | float dist = t3d_vec3_distance(&carpos, &target); 236 | bool can_strike = false; 237 | // set target vectors 238 | if(carplayer[index].is_team_left){ 239 | goal.x = -goal.x; 240 | can_strike = carpos.x < target.x; 241 | } else can_strike = carpos.x >= target.x; 242 | if(can_strike){ // target the ball 243 | T3DVec3 balloffset; t3d_vec3_diff(&balloffset, &target, &goal); t3d_vec3_norm(&balloffset); 244 | t3d_vec3_add(&target, &target, &balloffset); 245 | } else { // drive around the ball 246 | T3DVec3 balloffset = (T3DVec3){.x = 0, .y = 0, .z = 4}; 247 | if(carpos.z < target.z) balloffset.z = -balloffset.z; 248 | t3d_vec3_add(&target, &target, &balloffset); 249 | } // target to inputs 250 | if(dist > 2) { 251 | if(target.y - 1 > carpos.y) {outpressed->l = 1;} // jump if the target is high 252 | outinput->btn.z = 1; // nitro if the target is far enough 253 | } 254 | T3DVec3 forw; t3d_vec3_diff(&forw, &target, &carpos); t3d_vec3_norm(&forw); 255 | outinput->stick_x = forw.x * 68; 256 | outinput->stick_y = -forw.z * 68; 257 | } 258 | 259 | void carplayer_update(){ 260 | for(int i = 0; i < 4; i++){ 261 | if(TPE_bodyIsActive(&bodies[carplayer[i].bodyidx])) 262 | TPE_bodyApplyGravity(&bodies[carplayer[i].bodyidx],TPE_F / 100); 263 | } 264 | 265 | for(int i = 0; i < 4; i++){ 266 | TPE_Vec3 Pos = TPE_bodyGetCenterOfMass(&bodies[carplayer[i].bodyidx]); 267 | carplayer[i].Pos_t3d = (T3DVec3){.x = Pos.x * (1.0/TPE_F), .y = Pos.y * (1.0/TPE_F), .z = Pos.z * (1.0/TPE_F)}; 268 | 269 | carplayer[i].Rot = TPE_bodyGetRotation(&bodies[carplayer[i].bodyidx], 0, 1, 3); 270 | TPE_Vec3 diff = TPE_vec3Subtract(carplayer[i].joints[1].position, carplayer[i].joints[0].position); 271 | TPE_vec3Normalize(&diff); 272 | carplayer[i].forward = (T3DVec3){.x = diff.x, .y = diff.y, .z = diff.z}; 273 | t3d_vec3_norm(&carplayer[i].forward); 274 | } 275 | 276 | for(int i = 0; i < 4; i++){ 277 | carplayer[i].yaw = atan2f(carplayer[i].forward.z, carplayer[i].forward.x); 278 | TPE_Vec3 diff = TPE_vec3Subtract(carplayer[i].joints[1].position, carplayer[i].joints[0].position); 279 | // handle if there was a collision 280 | if(carplayer[i].carcollided >= 0) 281 | if(t3d_vec3_distance2(&carplayer[i].Pos_t3d, &carplayer[carplayer[i].carcollided].Pos_t3d) > 16) carplayer[i].carcollided = -1; 282 | 283 | if(carplayer[i].ballcollided) 284 | if(t3d_vec3_distance2(&carplayer[i].Pos_t3d, &playball.ballPos_t3d) > 8) carplayer[i].ballcollided = false; 285 | 286 | // apply back axel acceleration 287 | joypad_inputs_t input; 288 | joypad_buttons_t pressed; 289 | if(carplayer[i].playercontroller < 0) {carplayer_ai(i, &input, &pressed);} 290 | else{ 291 | input = joypad_get_inputs(carplayer[i].playercontroller); 292 | pressed = joypad_get_buttons_pressed(carplayer[i].playercontroller); } 293 | if(matchinfo.state != MATCH_PLAY) { 294 | memset(&input, 0, sizeof(joypad_inputs_t)); 295 | memset(&pressed, 0, sizeof(joypad_buttons_t));} 296 | 297 | carplayer[i].isnitro = false; 298 | if(input.btn.z) carplayer[i].nitro -= display_get_delta_time(); 299 | else carplayer[i].nitro += display_get_delta_time() / 2; 300 | if(pressed.z && carplayer[i].nitro > 0) sound_play("boost", false); 301 | if(carplayer[i].nitro < 0) carplayer[i].nitro = 0; 302 | if(carplayer[i].nitro > 1) carplayer[i].nitro = 1; 303 | if(input.btn.z && carplayer[i].nitro > 0) carplayer[i].isnitro = true; 304 | 305 | float inputyaw = 0; 306 | float inputvel = 0; 307 | fm_vec3_t vec; vec.x = input.stick_x; vec.y = input.stick_y; 308 | if(!gfx_pos_within_rect(vec.x, vec.y, -10, -10, 10, 10)){ 309 | inputyaw = fm_atan2f(vec.y, -vec.x); 310 | inputvel = fm_vec3_len(&vec); 311 | float a = carplayer[i].yaw - inputyaw; 312 | if (a > FM_PI) a -= FM_PI*2; 313 | if (a < -FM_PI) a += FM_PI*2; 314 | inputyaw = a * 180 / FM_PI; 315 | if(abs(inputyaw) < 5.0f) inputyaw = 0; 316 | } 317 | { 318 | TPE_Vec3 vel = TPE_bodyGetLinearVelocity(&bodies[carplayer[i].bodyidx]); 319 | int vellen = TPE_vec3Len(vel); 320 | int forwardaccel = inputvel; 321 | if(forwardaccel < gamestatus.state.game.settings.deadzone * 65) forwardaccel = 0; 322 | int siderotation = inputyaw; 323 | if(carplayer[i].isnitro) forwardaccel = 65; 324 | if(carplayer[i].joints[0].position.y < (TPE_F) + 20){ 325 | if(pressed.l || pressed.r){ 326 | TPE_Vec3 ax = {.x = 0, .y = TPE_F / 4, .z = 0}; 327 | TPE_bodyAccelerate(&bodies[carplayer[i].bodyidx], ax); 328 | } 329 | if(vellen > 0){ 330 | if(siderotation > 65) siderotation = 65; 331 | if(siderotation < -65) siderotation = -65; 332 | int vellen_siderot = vellen; if(vellen_siderot > (TPE_F / 5)) vellen_siderot = (TPE_F / 5); 333 | siderotation = siderotation * vellen_siderot / TPE_F; 334 | if(carplayer[i].playercontroller < 0) siderotation *= carplayer[i].ai_maxturnspeed; 335 | TPE_bodyRotateByAxis(&bodies[carplayer[i].bodyidx], (TPE_Vec3){.y = siderotation}); 336 | } 337 | } 338 | if((carplayer[i].isnitro && vellen < (TPE_F / 4)) || (forwardaccel > 0 && vellen < (TPE_F / 5)) || (forwardaccel < 0 && vellen < (TPE_F / 10))){ 339 | if(forwardaccel > 65) forwardaccel = 65; 340 | if(forwardaccel < -65) forwardaccel = -65; 341 | if(forwardaccel < 0) forwardaccel >>= 1; 342 | TPE_Vec3 ax = TPE_vec3Times(diff, carplayer[i].isnitro? forwardaccel : forwardaccel >> 2); 343 | TPE_bodyAccelerate(&bodies[carplayer[i].bodyidx], ax); 344 | } 345 | } 346 | } 347 | 348 | for(int i = 0; i < 4; i++){ 349 | t3d_mat4_from_srt_euler(&carplayer[i].modelMat, 350 | (float[3]){1, 1, 1}, 351 | (float[3]){0, carplayer[i].yaw, 0}, 352 | (float[3]){carplayer[i].Pos_t3d.x * 64, carplayer[i].Pos_t3d.y * 64, carplayer[i].Pos_t3d.z * 64} 353 | ); 354 | t3d_mat4_from_srt_euler(&carplayer[i].shadowmodelMat, 355 | (float[3]){1, 1, 1}, 356 | (float[3]){0, carplayer[i].yaw, 0}, 357 | (float[3]){carplayer[i].Pos_t3d.x * 64, 0, carplayer[i].Pos_t3d.z * 64} 358 | ); 359 | // ======== Update ======== // 360 | t3d_mat4_to_fixed(&carplayer[i].modelMatFP[(frame) % 5], &carplayer[i].modelMat); 361 | t3d_mat4_to_fixed(&carplayer[i].shadowmodelMatFP[(frame) % 5], &carplayer[i].shadowmodelMat); 362 | } 363 | } 364 | 365 | void carplayer_draw(carplayer_t* car){ 366 | t3d_matrix_set(&car->modelMatFP[(frame) % 5], true); 367 | if(car->isnitro){ 368 | rdpq_set_prim_color(car->playercontroller >= 0? color_from_packed32(playercolors[car->playercontroller]) : RGBA32(255,255,255,255)); 369 | t3d_model_draw(car->nitromodel); 370 | } 371 | if(!car->blockmodel){ 372 | rspq_block_begin(); 373 | rdpq_set_prim_color(car->playercontroller >= 0? color_from_packed32(playercolors[car->playercontroller]) : RGBA32(255,255,255,255)); 374 | t3d_model_draw(car->model); 375 | rdpq_sync_pipe(); 376 | car->blockmodel = rspq_block_end(); 377 | } rspq_block_run(car->blockmodel); 378 | //t3d_matrix_pop(1); 379 | 380 | } 381 | 382 | void carplayer_draw_teleport_particles(carplayer_t* car){ 383 | t3d_matrix_set(&car->modelMatFP[(frame) % 5], true); 384 | // Upload texture for the following particles. 385 | // The ucode itself never loads or switches any textures, 386 | // so you can only use what you have uploaded before in a single draw call. 387 | rdpq_texparms_t p = {0}; 388 | p.s.repeats = REPEAT_INFINITE; 389 | p.t.repeats = REPEAT_INFINITE; 390 | p.s.scale_log = -2; 391 | p.t.scale_log = -2; 392 | rdpq_sprite_upload(TILE0, car->part_glowspr, &p); 393 | 394 | tpx_state_from_t3d(); 395 | tpx_state_set_scale(0.45f, 0.45f); 396 | 397 | tpx_state_set_tex_params(0, 0); 398 | tpx_particle_draw_tex(car->particles, PARTICLES_MAX); 399 | } 400 | 401 | void carplayer_draw_shadow(carplayer_t* car){ 402 | t3d_matrix_set(&car->shadowmodelMatFP[(frame) % 5], true); 403 | if(!car->blockshadow){ 404 | rspq_block_begin(); 405 | t3d_model_draw(car->shadowmodel); 406 | car->blockshadow = rspq_block_end(); 407 | } rspq_block_run(car->blockshadow); 408 | //t3d_matrix_pop(1); 409 | } 410 | -------------------------------------------------------------------------------- /src/entity_logic.h: -------------------------------------------------------------------------------- 1 | #ifndef ENTITY_LOGIC_H 2 | #define ENTITY_LOGIC_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "engine_gfx.h" 10 | #include "teams.h" 11 | #include "tinyphysicsengine.h" 12 | 13 | typedef struct{ 14 | //TPE_Body body; 15 | int bodyidx; 16 | TPE_Joint joint; 17 | 18 | T3DVec3 ballPos_t3d; 19 | T3DVec3 ballPoslast; 20 | T3DQuat ballquat; 21 | TPE_Vec3 ballRot, ballPreviousPos; 22 | 23 | T3DModel *modelball; 24 | T3DMat4 modelMatball; // matrix for our model, this is a "normal" float matrix 25 | T3DMat4 modelshadowMat; // matrix for our model, this is a "normal" float matrix 26 | T3DMat4FP* modelMatFPball; 27 | 28 | T3DModel *modelshadow; 29 | T3DMat4FP* modelMatFPshadow; 30 | 31 | rspq_block_t* blockmodel; 32 | rspq_block_t* blockshadow; 33 | bool init; 34 | } playball_t; 35 | extern playball_t playball; 36 | 37 | void playball_init(); 38 | 39 | void playball_reset(); 40 | 41 | void playball_update(); 42 | 43 | void playball_draw(); 44 | 45 | void playball_draw_shadow(); 46 | 47 | void playball_free(); 48 | 49 | extern TPE_Body bodies[64]; 50 | extern int bodiescount; 51 | #define PARTICLES_MAX 16 52 | 53 | typedef struct{ 54 | teamdef_t* team; 55 | bool is_team_left; 56 | int playercontroller; 57 | //TPE_Body body; 58 | int bodyidx; 59 | TPE_Joint joints[2]; 60 | TPE_Connection connections[1]; 61 | 62 | T3DVec3 Pos_t3d; 63 | T3DVec3 Poslast; 64 | T3DVec3 forward; 65 | T3DVec3 ai_targetpos; 66 | T3DQuat quat; 67 | TPE_Vec3 Rot, PreviousPos, playerDirectionVec; 68 | 69 | TPXParticle *particles; 70 | sprite_t *part_firespr; 71 | sprite_t *part_glowspr; 72 | int particle_type; 73 | 74 | float yaw; 75 | bool ballcollided; 76 | int carcollided; 77 | float nitro; 78 | bool isnitro; 79 | float ai_difficulty; 80 | float ai_maxturnspeed; 81 | 82 | T3DModel *model; 83 | T3DModel *debugjointmodel; 84 | T3DModel *shadowmodel; 85 | T3DModel *nitromodel; 86 | T3DMat4 modelMat; // matrix for our model, this is a "normal" float matrix 87 | T3DMat4 shadowmodelMat; // matrix for our model, this is a "normal" float matrix 88 | T3DMat4FP* modelMatFP; 89 | T3DMat4FP* debugjointmodelMatFP; 90 | T3DMat4FP* shadowmodelMatFP; 91 | rspq_block_t* blockmodel; 92 | rspq_block_t* blockshadow; 93 | } carplayer_t; 94 | extern carplayer_t carplayer[4]; 95 | 96 | void carplayer_init(carplayer_t* car, int index); 97 | 98 | void carplayer_reset(carplayer_t* car, int index); 99 | 100 | void carplayer_update(); 101 | 102 | void carplayer_free(carplayer_t* car); 103 | 104 | void carplayer_draw(carplayer_t* car); 105 | 106 | void carplayer_draw_shadow(carplayer_t* car); 107 | 108 | void carplayer_draw_teleport_particles(carplayer_t* car); 109 | 110 | #endif -------------------------------------------------------------------------------- /src/inih/ini.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 rxi 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include "ini.h" 29 | 30 | struct ini_t { 31 | char *data; 32 | char *end; 33 | }; 34 | 35 | 36 | /* Case insensitive string compare */ 37 | static int strcmpci(const char *a, const char *b) { 38 | for (;;) { 39 | int d = tolower(*a) - tolower(*b); 40 | if (d != 0 || !*a) { 41 | return d; 42 | } 43 | a++, b++; 44 | } 45 | } 46 | 47 | /* Returns the next string in the split data */ 48 | static char* next(ini_t *ini, char *p) { 49 | p += strlen(p); 50 | while (p < ini->end && *p == '\0') { 51 | p++; 52 | } 53 | return p; 54 | } 55 | 56 | static void trim_back(ini_t *ini, char *p) { 57 | while (p >= ini->data && (*p == ' ' || *p == '\t' || *p == '\r')) { 58 | *p-- = '\0'; 59 | } 60 | } 61 | 62 | static char* discard_line(ini_t *ini, char *p) { 63 | while (p < ini->end && *p != '\n') { 64 | *p++ = '\0'; 65 | } 66 | return p; 67 | } 68 | 69 | 70 | static char *unescape_quoted_value(ini_t *ini, char *p) { 71 | /* Use `q` as write-head and `p` as read-head, `p` is always ahead of `q` 72 | * as escape sequences are always larger than their resultant data */ 73 | char *q = p; 74 | p++; 75 | while (p < ini->end && *p != '"' && *p != '\r' && *p != '\n') { 76 | if (*p == '\\') { 77 | /* Handle escaped char */ 78 | p++; 79 | switch (*p) { 80 | default : *q = *p; break; 81 | case 'r' : *q = '\r'; break; 82 | case 'n' : *q = '\n'; break; 83 | case 't' : *q = '\t'; break; 84 | case '\r' : 85 | case '\n' : 86 | case '\0' : goto end; 87 | } 88 | 89 | } else { 90 | /* Handle normal char */ 91 | *q = *p; 92 | } 93 | q++, p++; 94 | } 95 | end: 96 | return q; 97 | } 98 | 99 | 100 | /* Splits data in place into strings containing section-headers, keys and 101 | * values using one or more '\0' as a delimiter. Unescapes quoted values */ 102 | static void split_data(ini_t *ini) { 103 | char *value_start, *line_start; 104 | char *p = ini->data; 105 | 106 | while (p < ini->end) { 107 | switch (*p) { 108 | case '\r': 109 | case '\n': 110 | case '\t': 111 | case ' ': 112 | *p = '\0'; 113 | /* Fall through */ 114 | 115 | case '\0': 116 | p++; 117 | break; 118 | 119 | case '[': 120 | p += strcspn(p, "]\n"); 121 | *p = '\0'; 122 | break; 123 | 124 | case ';': 125 | p = discard_line(ini, p); 126 | break; 127 | 128 | default: 129 | line_start = p; 130 | p += strcspn(p, "=\n"); 131 | 132 | /* Is line missing a '='? */ 133 | if (*p != '=') { 134 | p = discard_line(ini, line_start); 135 | break; 136 | } 137 | trim_back(ini, p - 1); 138 | 139 | /* Replace '=' and whitespace after it with '\0' */ 140 | do { 141 | *p++ = '\0'; 142 | } while (*p == ' ' || *p == '\r' || *p == '\t'); 143 | 144 | /* Is a value after '=' missing? */ 145 | if (*p == '\n' || *p == '\0') { 146 | p = discard_line(ini, line_start); 147 | break; 148 | } 149 | 150 | if (*p == '"') { 151 | /* Handle quoted string value */ 152 | value_start = p; 153 | p = unescape_quoted_value(ini, p); 154 | 155 | /* Was the string empty? */ 156 | if (p == value_start) { 157 | p = discard_line(ini, line_start); 158 | break; 159 | } 160 | 161 | /* Discard the rest of the line after the string value */ 162 | p = discard_line(ini, p); 163 | 164 | } else { 165 | /* Handle normal value */ 166 | p += strcspn(p, "\n"); 167 | trim_back(ini, p - 1); 168 | } 169 | break; 170 | } 171 | } 172 | } 173 | 174 | 175 | 176 | ini_t* ini_load(const char *filename) { 177 | ini_t *ini = NULL; 178 | FILE *fp = NULL; 179 | int n, sz; 180 | 181 | /* Init ini struct */ 182 | ini = malloc(sizeof(*ini)); 183 | if (!ini) { 184 | goto fail; 185 | } 186 | memset(ini, 0, sizeof(*ini)); 187 | 188 | /* Open file */ 189 | fp = fopen(filename, "rb"); 190 | if (!fp) { 191 | goto fail; 192 | } 193 | 194 | /* Get file size */ 195 | fseek(fp, 0, SEEK_END); 196 | sz = ftell(fp); 197 | rewind(fp); 198 | 199 | /* Load file content into memory, null terminate, init end var */ 200 | ini->data = malloc(sz + 1); 201 | ini->data[sz] = '\0'; 202 | ini->end = ini->data + sz; 203 | n = fread(ini->data, 1, sz, fp); 204 | if (n != sz) { 205 | goto fail; 206 | } 207 | 208 | /* Prepare data */ 209 | split_data(ini); 210 | 211 | /* Clean up and return */ 212 | fclose(fp); 213 | return ini; 214 | 215 | fail: 216 | if (fp) fclose(fp); 217 | if (ini) ini_free(ini); 218 | return NULL; 219 | } 220 | 221 | 222 | void ini_free(ini_t *ini) { 223 | free(ini->data); 224 | free(ini); 225 | } 226 | 227 | 228 | const char* ini_get(ini_t *ini, const char *section, const char *key) { 229 | char *current_section = ""; 230 | char *val; 231 | char *p = ini->data; 232 | 233 | if (*p == '\0') { 234 | p = next(ini, p); 235 | } 236 | 237 | while (p < ini->end) { 238 | if (*p == '[') { 239 | /* Handle section */ 240 | current_section = p + 1; 241 | 242 | } else { 243 | /* Handle key */ 244 | val = next(ini, p); 245 | if (!section || !strcmpci(section, current_section)) { 246 | if (!strcmpci(p, key)) { 247 | return val; 248 | } 249 | } 250 | p = val; 251 | } 252 | 253 | p = next(ini, p); 254 | } 255 | 256 | return NULL; 257 | } 258 | 259 | 260 | int ini_sget( 261 | ini_t *ini, const char *section, const char *key, 262 | const char *scanfmt, void *dst 263 | ) { 264 | const char *val = ini_get(ini, section, key); 265 | if (!val) { 266 | return 0; 267 | } 268 | if (scanfmt) { 269 | sscanf(val, scanfmt, dst); 270 | } else { 271 | *((const char**) dst) = val; 272 | } 273 | return 1; 274 | } 275 | -------------------------------------------------------------------------------- /src/inih/ini.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 rxi 3 | * 4 | * This library is free software; you can redistribute it and/or modify it 5 | * under the terms of the MIT license. See `ini.c` for details. 6 | */ 7 | 8 | #ifndef INI_H 9 | #define INI_H 10 | 11 | #define INI_VERSION "0.1.1" 12 | 13 | typedef struct ini_t ini_t; 14 | 15 | ini_t* ini_load(const char *filename); 16 | void ini_free(ini_t *ini); 17 | const char* ini_get(ini_t *ini, const char *section, const char *key); 18 | int ini_sget(ini_t *ini, const char *section, const char *key, const char *scanfmt, void *dst); 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /src/intro.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "intro.h" 3 | 4 | 5 | void libdragon_logo() 6 | { 7 | const color_t RED = RGBA32(221, 46, 26, 255); 8 | const color_t WHITE = RGBA32(255, 255, 255, 255); 9 | 10 | sprite_t *d1 = sprite_load("rom:/textures/intro/dragon1.i8.sprite"); 11 | sprite_t *d2 = sprite_load("rom:/textures/intro/dragon2.i8.sprite"); 12 | sprite_t *d3 = sprite_load("rom:/textures/intro/dragon3.i8.sprite"); 13 | sprite_t *d4 = sprite_load("rom:/textures/intro/dragon4.i8.sprite"); 14 | wav64_t music; 15 | wav64_open(&music, "rom:/sfx/dragon.wav64"); 16 | mixer_ch_set_limits(0, 0, 48000, 0); 17 | 18 | display_init(RESOLUTION_640x480, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_RESAMPLE); 19 | 20 | float angle1 = 0, angle2 = 0, angle3 = 0; 21 | float scale1 = 0, scale2 = 0, scale3 = 0, scroll4 = 0; 22 | uint32_t ms0 = 0; 23 | int anim_part = 0; 24 | const int X0 = 10, Y0 = 30; // translation offset of the animation (simplify centering) 25 | 26 | void reset() { 27 | ms0 = get_ticks_ms(); 28 | anim_part = 0; 29 | 30 | angle1 = 3.2f; 31 | angle2 = 1.9f; 32 | angle3 = 0.9f; 33 | scale1 = 0.0f; 34 | scale2 = 0.4f; 35 | scale3 = 0.8f; 36 | scroll4 = 400; 37 | wav64_play(&music, 0); 38 | } 39 | 40 | reset(); 41 | while (1) { 42 | mixer_try_play(); 43 | 44 | // Calculate animation part: 45 | // 0: rotate dragon head 46 | // 1: rotate dragon body and tail, scale up 47 | // 2: scroll dragon logo 48 | // 3: fade out 49 | uint32_t tt = get_ticks_ms() - ms0; 50 | if (tt < 1000) anim_part = 0; 51 | else if (tt < 1500) anim_part = 1; 52 | else if (tt < 4000) anim_part = 2; 53 | else if (tt < 5000) anim_part = 3; 54 | else break; 55 | 56 | // Update animation parameters using quadratic ease-out 57 | angle1 -= angle1 * 0.04f; if (angle1 < 0.010f) angle1 = 0; 58 | if (anim_part >= 1) { 59 | angle2 -= angle2 * 0.06f; if (angle2 < 0.01f) angle2 = 0; 60 | angle3 -= angle3 * 0.06f; if (angle3 < 0.01f) angle3 = 0; 61 | scale2 -= scale2 * 0.06f; if (scale2 < 0.01f) scale2 = 0; 62 | scale3 -= scale3 * 0.06f; if (scale3 < 0.01f) scale3 = 0; 63 | } 64 | if (anim_part >= 2) { 65 | scroll4 -= scroll4 * 0.08f; 66 | } 67 | 68 | // Update colors for fade out effect 69 | color_t red = RED; 70 | color_t white = WHITE; 71 | if (anim_part >= 3) { 72 | red.a = 255 - (tt-4000) * 255 / 1000; 73 | white.a = 255 - (tt-4000) * 255 / 1000; 74 | } 75 | 76 | #if 0 77 | // Debug: re-run logo animation on button press 78 | joypad_poll(); 79 | joypad_buttons_t btn = joypad_get_buttons_pressed(JOYPAD_PORT_1); 80 | if (btn.a) reset(); 81 | #endif 82 | 83 | surface_t *fb = display_get(); 84 | rdpq_attach_clear(fb, NULL); 85 | 86 | // To simulate the dragon jumping out, we scissor the head so that 87 | // it appears as it moves. 88 | if (angle1 > 1.0f) { 89 | // Initially, also scissor horizontally, 90 | // so that the head tail is not visible on the right. 91 | rdpq_set_scissor(0, 0, X0+300, Y0+240); 92 | } else { 93 | rdpq_set_scissor(0, 0, 640, Y0+240); 94 | } 95 | 96 | // Draw dragon head 97 | rdpq_set_mode_standard(); 98 | rdpq_mode_alphacompare(1); 99 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 100 | rdpq_mode_combiner(RDPQ_COMBINER1((0,0,0,PRIM),(TEX0,0,PRIM,0))); 101 | rdpq_set_prim_color(red); 102 | rdpq_sprite_blit(d1, X0+216, Y0+205, &(rdpq_blitparms_t){ 103 | .theta = angle1, .scale_x = scale1+1, .scale_y = scale1+1, 104 | .cx = 176, .cy = 171, 105 | }); 106 | 107 | // Restore scissor to standard 108 | rdpq_set_scissor(0, 0, 640, 480); 109 | 110 | // Draw a black rectangle with alpha gradient, to cover the head tail 111 | rdpq_mode_combiner(RDPQ_COMBINER_SHADE); 112 | rdpq_mode_dithering(DITHER_NOISE_NOISE); 113 | float vtx[4][6] = { 114 | // x, y, r,g,b,a 115 | { X0+0, Y0+180, 0,0,0,0 }, 116 | { X0+200, Y0+180, 0,0,0,0 }, 117 | { X0+200, Y0+240, 0,0,0,1 }, 118 | { X0+0, Y0+240, 0,0,0,1 }, 119 | }; 120 | rdpq_triangle(&TRIFMT_SHADE, vtx[0], vtx[1], vtx[2]); 121 | rdpq_triangle(&TRIFMT_SHADE, vtx[0], vtx[2], vtx[3]); 122 | 123 | if (anim_part >= 1) { 124 | // Draw dragon body and tail 125 | rdpq_set_mode_standard(); 126 | rdpq_mode_alphacompare(1); 127 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 128 | rdpq_mode_combiner(RDPQ_COMBINER1((0,0,0,PRIM),(TEX0,0,PRIM,0))); 129 | 130 | // Fade them in 131 | color_t color = red; 132 | color.r *= 1-scale3; color.g *= 1-scale3; color.b *= 1-scale3; 133 | rdpq_set_prim_color(color); 134 | 135 | rdpq_sprite_blit(d2, X0+246, Y0+230, &(rdpq_blitparms_t){ 136 | .theta = angle2, .scale_x = 1-scale2, .scale_y = 1-scale2, 137 | .cx = 145, .cy = 113, 138 | }); 139 | 140 | rdpq_sprite_blit(d3, X0+266, Y0+256, &(rdpq_blitparms_t){ 141 | .theta = -angle3, .scale_x = 1-scale3, .scale_y = 1-scale3, 142 | .cx = 91, .cy = 24, 143 | }); 144 | } 145 | 146 | // Draw scrolling logo 147 | if (anim_part >= 2) { 148 | rdpq_set_prim_color(white); 149 | rdpq_sprite_blit(d4, X0 + 161 + (int)scroll4, Y0 + 182, NULL); 150 | } 151 | 152 | rdpq_detach_show(); 153 | } 154 | 155 | //wait_ms(500); // avoid immediate switch to next screen 156 | rspq_wait(); 157 | sprite_free(d1); 158 | sprite_free(d2); 159 | sprite_free(d3); 160 | sprite_free(d4); 161 | wav64_close(&music); 162 | display_close(); 163 | } -------------------------------------------------------------------------------- /src/intro.h: -------------------------------------------------------------------------------- 1 | #ifndef INTRO_H 2 | #define INTRO_H 3 | 4 | #ifdef __cplusplus 5 | extern "C"{ 6 | #endif 7 | /// made by SpookyIluha 8 | /// Libragon intro entrypoint 9 | extern void libdragon_logo(); 10 | 11 | #ifdef __cplusplus 12 | } 13 | #endif 14 | 15 | #endif -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "engine_gfx.h" 6 | #include "teams.h" 7 | #include "entity_logic.h" 8 | #include "audioutils.h" 9 | #include "effects.h" 10 | #include "playtime_logic.h" 11 | #include "maps.h" 12 | #include "menu.h" 13 | #include "intro.h" 14 | #include "engine_eeprom.h" 15 | #include "engine_locale.h" 16 | 17 | void setup(){ 18 | //debug_init_isviewer(); 19 | //debug_init_usblog(); 20 | wav64_init_compression(3); 21 | 22 | dfs_init(DFS_DEFAULT_LOCATION); 23 | joypad_init(); 24 | audio_init(48000, 4); 25 | mixer_init(10); 26 | vi_init(); 27 | 28 | rdpq_init(); 29 | state_init(); 30 | effects_init(); 31 | engine_eeprom_init(); 32 | engine_load_languages(); 33 | 34 | t3d_init((T3DInitParams){}); 35 | tpx_init((TPXInitParams){}); 36 | 37 | srand(getentropy32()); 38 | register_VI_handler((void(*)())rand); 39 | 40 | libdragon_logo(); 41 | display_init(RESOLUTION_640x480, DEPTH_16_BPP, is_memory_expanded()? 3 : 2, GAMMA_NONE, FILTERS_DEDITHER); 42 | audio_prewarm_all(); 43 | 44 | #if DEBUG_RDP 45 | rdpq_debug_start(); 46 | rdpq_debug_log(true); 47 | #endif 48 | } 49 | 50 | int main() 51 | { 52 | setup(); 53 | menu_start(); 54 | return 0; 55 | } 56 | -------------------------------------------------------------------------------- /src/maps.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "particles_sim.h" 8 | #include "maps.h" 9 | 10 | 11 | const char* beach_matqueue[] = { 12 | "sky01", 13 | "sky01b", 14 | "sky01c", 15 | "sky01d", 16 | "sky01e", 17 | "A_A_water01", 18 | "A_B_sand01", 19 | "A_B_sand01b", 20 | "A_C_rock01", 21 | "A_C_rock01b", 22 | "A_D_bark01", 23 | "A_E_leaves01", 24 | "B_A_border01", 25 | "B_A_border01b", 26 | }; 27 | 28 | const char* beach_matqueue_b[] = { 29 | "B_A_border01c", 30 | "B_B_rainbow01", 31 | "B_C_blue01", 32 | "B_D_red01", 33 | "D_A_bark01", 34 | "D_B_leaves01" 35 | }; 36 | 37 | const char* lava_matqueue[] = { 38 | "mat_lava", 39 | "mat_rocks_00", 40 | "mat_rocks", 41 | "mat_rocks_2", 42 | "mat_rocks_3", 43 | "mat_rocks_4", 44 | }; 45 | 46 | const char* lava_matqueue_b[] = { 47 | "mat_rocks_5", 48 | }; 49 | 50 | 51 | const char* castle_matqueue[] = { 52 | "f3d_trees", 53 | "f3d_stars", 54 | "f3d_moon", 55 | "f3d_dirt", 56 | "f3d_floor", 57 | "f3d_wall", 58 | "f3d_vtx", 59 | "f3d_metal", 60 | "f3d_wall_2", 61 | "f3d_metal_2", 62 | }; 63 | 64 | const char* castle_matqueue_b[] = { 65 | "f3d_wall_3", 66 | "f3d_wall_4", 67 | }; 68 | 69 | 70 | const char* city_matqueue[] = { 71 | "f3d_asphalt", 72 | "f3d_stars", 73 | "f3d_building4", 74 | "f3d_building3", 75 | "f3d_building2", 76 | "f3d_building5", 77 | "f3d_building1", 78 | "f3d_building1b", 79 | "f3d_light1", 80 | "f3d_advert7", 81 | "f3d_advert2", 82 | "f3d_advert6", 83 | "f3d_advert5", 84 | "f3d_advert4", 85 | "f3d_advert2b", 86 | "f3d_advert3", 87 | "f3d_tree", 88 | "f3d_streetlamps", 89 | "f3d_signs", 90 | "f3d_line2", 91 | "f3d_line1", 92 | "f3d_fence1", 93 | }; 94 | 95 | const char* city_matqueue_b[] = { 96 | "f3d_fence_b", 97 | }; 98 | 99 | 100 | const char* snow_matqueue[] = { 101 | "f3d_stars", 102 | "f3d_advert2b", 103 | "f3d_snow1", 104 | "f3d_snow2", 105 | "f3d_house", 106 | "f3d_trees", 107 | "f3d_advert1", 108 | "f3d_red1", 109 | "f3d_red2", 110 | "f3d_blue1", 111 | "f3d_blue2", 112 | }; 113 | 114 | const char* snow_matqueue_b[] = { 115 | "f3d_advert3", 116 | "f3d_advert4", 117 | "f3d_lines", 118 | }; 119 | 120 | /* 121 | typedef struct{ 122 | const char* name; 123 | const char* previewimgfn; 124 | bool unlocked; 125 | 126 | const char* modelfn; 127 | 128 | const char* model_matnames; 129 | int model_matnames_count; 130 | const char* model_matnames_b; 131 | int model_matnames_b_count; 132 | 133 | const char* musicfn; 134 | } mapinfo_t; 135 | */ 136 | 137 | mapinfo_t maps[6] = { 138 | { .name = "BLUE SKY BEACH", 139 | .previewimgfn = "rom:/textures/ui/stadiums/BSB.rgba32.sprite", 140 | .unlocked = true, 141 | .modelfn = "rom:/models/track01/track01_282.t3dm", 142 | .model_matnames = beach_matqueue, .model_matnames_count = 14, 143 | .model_matnames_b = beach_matqueue_b, .model_matnames_b_count = 6, 144 | .musicfn = "3_beach", 145 | .voicenamefn = "Stadium_BlueSkyBeach" 146 | }, 147 | { .name = "COBBLESTONE CASTLE", 148 | .previewimgfn = "rom:/textures/ui/stadiums/CSC.rgba32.sprite", 149 | .unlocked = true, 150 | .modelfn = "rom:/models/track03/track03_282.t3dm", 151 | .model_matnames = castle_matqueue, .model_matnames_count = 10, 152 | .model_matnames_b = castle_matqueue_b, .model_matnames_b_count = 2, 153 | .musicfn = "2_castle", 154 | .hdr.enabled = true, .hdr.tonemappingaverage = 0.33, 155 | .voicenamefn = "Stadium_CobblestoneCastle", 156 | }, 157 | { .name = "MOON LIGHT CITY", 158 | .previewimgfn = "rom:/textures/ui/stadiums/MLC.rgba32.sprite", 159 | .unlocked = true, 160 | .modelfn = "rom:/models/track04/track04_282.t3dm", 161 | .model_matnames = city_matqueue, .model_matnames_count = 22, 162 | .model_matnames_b = city_matqueue_b, .model_matnames_b_count = 1, 163 | .musicfn = "5_urban", 164 | .particles.count = 32, .particles.color = 0x2d343dff, .particles.scale = (T3DVec3){.x = 10, .y = 3, .z = 10}, .particles.size = 0.2f, 165 | .particles.texturefn = "rom:/textures/parts/rain.i8.sprite", 166 | .particles.initfunc = (void (*)(void *, int))generate_particles_random, 167 | .particles.updatefunc = (void (*)(void *, int))simulate_particles_rain, 168 | .hdr.enabled = true, .hdr.tonemappingaverage = 0.5, 169 | .voicenamefn = "Stadium_MoonLightCity", 170 | }, 171 | { .name = "RED HOT ROCKS", 172 | .previewimgfn = "rom:/textures/ui/stadiums/RHR.rgba32.sprite", 173 | .unlocked = true, 174 | .modelfn = "rom:/models/track02/track02_282.t3dm", 175 | .model_matnames = lava_matqueue, .model_matnames_count = 6, 176 | .model_matnames_b = lava_matqueue_b, .model_matnames_b_count = 1, 177 | .musicfn = "4_lava", 178 | .particles.count = 64, .particles.color = 0xffbd66ff, .particles.scale = (T3DVec3){.x = 10, .y = 3, .z = 10}, .particles.size = 0.2f, 179 | .particles.texturefn = "rom:/textures/parts/fire.i8.sprite", 180 | .particles.initfunc = (void (*)(void *, int))generate_particles_random, 181 | .particles.updatefunc = (void (*)(void *, int))simulate_particles_embers, 182 | .hdr.enabled = true, .hdr.tonemappingaverage = 0.2, 183 | .voicenamefn = "Stadium_RedHotRocks", 184 | }, 185 | { .name = "LAPLAND VILLAGE", 186 | .previewimgfn = "rom:/textures/ui/stadiums/LLV.rgba32.sprite", 187 | .unlocked = false, 188 | .modelfn = "rom:/models/track05/track05_282.t3dm", 189 | .model_matnames = snow_matqueue, .model_matnames_count = 11, 190 | .model_matnames_b = snow_matqueue_b, .model_matnames_b_count = 3, 191 | .musicfn = "8_holiday", 192 | .particles.count = 48, .particles.color = 0xb0edf7ff, .particles.scale = (T3DVec3){.x = 15, .y = 6, .z = 15}, .particles.size = 0.35f, 193 | .particles.texturefn = "rom:/textures/parts/fire.i8.sprite", 194 | .particles.initfunc = (void (*)(void *, int))generate_particles_random, 195 | .particles.updatefunc = (void (*)(void *, int))simulate_particles_snow, 196 | .hdr.enabled = true, .hdr.tonemappingaverage = 0.5, 197 | .voicenamefn = "Stadium_LaplandVillage", 198 | }, 199 | { .name = "RANDOM", 200 | .previewimgfn = "rom:/textures/ui/stadiums/RND.rgba32.sprite", 201 | .unlocked = true, 202 | .modelfn = NULL}, 203 | }; 204 | 205 | -------------------------------------------------------------------------------- /src/maps.h: -------------------------------------------------------------------------------- 1 | #ifndef MAPS_H 2 | #define MAPS_H 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | typedef struct{ 9 | const char* name; 10 | const char* previewimgfn; 11 | bool unlocked; 12 | 13 | const char* modelfn; 14 | 15 | const char** model_matnames; 16 | int model_matnames_count; 17 | const char** model_matnames_b; 18 | int model_matnames_b_count; 19 | 20 | const char* musicfn; 21 | 22 | struct{ 23 | int count; 24 | uint32_t color; 25 | const char* texturefn; 26 | void (*initfunc)(void*,int); 27 | void (*updatefunc)(void*,int); 28 | T3DVec3 scale; 29 | float size; 30 | } particles; 31 | struct{ 32 | bool enabled; 33 | float tonemappingaverage; 34 | } hdr; 35 | const char* voicenamefn; 36 | } mapinfo_t; 37 | 38 | extern mapinfo_t maps[6]; 39 | 40 | #endif -------------------------------------------------------------------------------- /src/menu.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "audioutils.h" 3 | #include "intro.h" 4 | #include "playtime_logic.h" 5 | #include "maps.h" 6 | #include "effects.h" 7 | #include "engine_eeprom.h" 8 | #include "engine_gamestatus.h" 9 | #include "engine_locale.h" 10 | 11 | bool cont = false; 12 | sprite_t* background; 13 | rspq_block_t* background_block; 14 | sprite_t* selector; 15 | sprite_t* button_a; 16 | sprite_t* button_b; 17 | 18 | float bg_time = 0; 19 | void render_background(){ 20 | bg_time += display_get_delta_time(); 21 | int alpha = 255 * (0.25f * sinf(bg_time) + 0.25); 22 | rdpq_set_prim_color(RGBA32(alpha,alpha,alpha,255)); 23 | rdpq_set_env_color(RGBA32(127,127,127,255)); 24 | if(!background_block){ 25 | rspq_block_begin(); 26 | rdpq_set_mode_standard(); 27 | rdpq_mode_combiner(RDPQ_COMBINER1((ENV,PRIM,TEX0,TEX0), (0,0,0,TEX0))); 28 | rdpq_mode_dithering(DITHER_BAYER_INVBAYER); 29 | rdpq_sprite_blit(background,0,0,NULL); 30 | background_block = rspq_block_end(); 31 | } rspq_block_run(background_block); 32 | 33 | // rdpq_text_printf(NULL, 1, 30, 30, "^02FPS: %.2f", display_get_fps()); 34 | //heap_stats_t stats; sys_get_heap_stats(&stats); 35 | //rdpq_text_printf(NULL, 1, 30, 50, "^02MEM: %i total, %i used", stats.total, stats.used); 36 | rdpq_set_mode_standard(); 37 | rdpq_mode_combiner(RDPQ_COMBINER1((ENV,PRIM,TEX0,TEX0), (0,0,0,TEX0))); 38 | rdpq_mode_dithering(DITHER_BAYER_INVBAYER); 39 | } 40 | 41 | float spr_time = 0; 42 | void render_sprite(sprite_t* sprite, rspq_block_t** block, float x, float y){ 43 | spr_time += display_get_delta_time(); 44 | int alpha = 255 * (0.5f * sinf(spr_time * 4) + 0.5f); 45 | rdpq_set_env_color(RGBA32(alpha,alpha,alpha,255)); 46 | if(!*block){ 47 | rspq_block_begin(); 48 | rdpq_set_mode_standard(); 49 | rdpq_mode_combiner(RDPQ_COMBINER1((PRIM,ENV,TEX0,TEX0), (TEX0,0,PRIM,0))); 50 | rdpq_mode_dithering(DITHER_BAYER_INVBAYER); 51 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 52 | rdpq_sprite_blit_anchor(sprite,ALIGN_CENTER,VALIGN_CENTER,x,y,NULL); 53 | *block = rspq_block_end(); 54 | } rspq_block_run(*block); 55 | } 56 | 57 | void menu_credits(){ 58 | rspq_wait(); 59 | float time = 0; 60 | time = 0; 61 | bgm_play("7_theme", true, 1); 62 | while(time < 2){ 63 | audioutils_mixer_update(); 64 | joypad_poll(); 65 | joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 66 | if(pressed.b){ 67 | bgm_play("1_select", true, 1); 68 | return; 69 | } 70 | rdpq_attach(display_get(), NULL); 71 | render_background(); 72 | rdpq_set_mode_standard(); 73 | rdpq_mode_combiner(RDPQ_COMBINER_TEX); 74 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 75 | rdpq_mode_dithering(DITHER_BAYER_INVBAYER); 76 | rdpq_sprite_blit(button_b, 500, 420, NULL); 77 | rdpq_textparms_t parms2; parms2.style_id = 2; 78 | rdpq_text_printf(&parms2, 3, 540, 445, dictstr("mm_back")); 79 | rdpq_detach_show(); 80 | time += display_get_delta_time(); 81 | } 82 | char keyt[32]; char keyd[32]; size_t index = 0; 83 | const char* title = NULL; 84 | do { 85 | time = 0; 86 | sprintf(keyt, "mm_c_t%i", index + 1); sprintf(keyd, "mm_c_d%i", index + 1); 87 | title = inistr("Credits", keyt); 88 | const char* description = inistr("Credits", keyd); 89 | char description_arr[512]; if(description) strcpy(description_arr, description); 90 | int lines = 1; 91 | for(int c = 0; description_arr[c] != '\0'; c++){if(description_arr[c] == '\n') { lines++;}} 92 | while(time < 4.5f){ 93 | audioutils_mixer_update(); 94 | joypad_poll(); 95 | joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 96 | if(pressed.b){ 97 | bgm_play("1_select", true, 1); 98 | return; 99 | } 100 | float alpha = 255; 101 | if(time < 1) alpha = time * 255; 102 | if(time > 2.5f) alpha = (4.5f - time) * 127; 103 | rdpq_attach(display_get(), NULL); 104 | render_background(); 105 | rdpq_set_mode_standard(); 106 | rdpq_mode_combiner(RDPQ_COMBINER_TEX); 107 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 108 | rdpq_mode_dithering(DITHER_BAYER_INVBAYER); 109 | rdpq_sprite_blit(button_b, 500, 420, NULL); 110 | rdpq_fontstyle_t style; style.color = RGBA32(255,255,255, alpha); 111 | rdpq_fontstyle_t style2; style2.color = RGBA32(180,180,180, alpha); 112 | for(int i = 0; i < 4; i++) {rdpq_font_style(fonts[i], 3, &style); rdpq_font_style(fonts[i], 4, &style2);} 113 | rdpq_textparms_t textparms; textparms.align = ALIGN_CENTER; textparms.style_id = 3; 114 | textparms.width = display_get_width(); 115 | if(title) rdpq_text_printf(&textparms, 3, 0, display_get_height() / 2 - 12*lines , title); 116 | textparms.style_id = 4; 117 | if(description) rdpq_text_printf(&textparms, 4, 0, display_get_height() / 2 - 12*lines + 24 , description_arr); 118 | 119 | rdpq_textparms_t parms2; parms2.style_id = 2; 120 | rdpq_text_printf(&parms2, 3, 540, 445, dictstr("mm_back")); 121 | rdpq_detach_show(); 122 | time += display_get_delta_time(); 123 | } 124 | rspq_wait(); 125 | index++; 126 | } while(title); 127 | bool pressed_b = false; 128 | while(!pressed_b){ 129 | audioutils_mixer_update(); 130 | joypad_poll(); 131 | joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 132 | if(pressed.b){ 133 | pressed_b = true; 134 | } 135 | rdpq_attach(display_get(), NULL); 136 | render_background(); 137 | rdpq_set_mode_standard(); 138 | rdpq_mode_combiner(RDPQ_COMBINER_TEX); 139 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 140 | rdpq_mode_dithering(DITHER_BAYER_INVBAYER); 141 | rdpq_sprite_blit(button_b, 500, 420, NULL); 142 | rdpq_textparms_t parms2; parms2.style_id = 2; 143 | rdpq_text_printf(&parms2, 3, 540, 445, dictstr("mm_back")); 144 | rdpq_detach_show(); 145 | time += display_get_delta_time(); 146 | } 147 | bgm_play("1_select", true, 1); 148 | } 149 | 150 | void menu_logos(){ 151 | const char* logos_fn[] = { 152 | "rom:/textures/logos/logo.sprite", 153 | "rom:/textures/logos/realityjump.sprite" 154 | }; 155 | for(int i = 0; i < 2; i++){ 156 | float logotime = 0; 157 | if(background_block) {rspq_block_free(background_block); background_block = NULL;} 158 | if(background) {sprite_free(background); background = NULL;} 159 | background = sprite_load(logos_fn[i]); 160 | while(logotime < 4){ 161 | audioutils_mixer_update(); 162 | rdpq_attach(display_get(), NULL); 163 | float modulate = logotime < 1? logotime * 250 : 250; 164 | if(logotime > 3.1f) modulate = (4.1f - logotime) * 250; 165 | rdpq_set_prim_color(RGBA32(modulate,modulate,modulate,255)); 166 | if(!background_block){ 167 | rspq_block_begin(); 168 | rdpq_set_mode_standard(); 169 | rdpq_mode_combiner(RDPQ_COMBINER_TEX_FLAT); 170 | rdpq_mode_filter(FILTER_BILINEAR); 171 | rdpq_mode_dithering(DITHER_BAYER_INVBAYER); 172 | rdpq_sprite_blit(background,0,0,NULL); 173 | background_block = rspq_block_end(); 174 | } rspq_block_run(background_block); 175 | rdpq_detach_show(); 176 | logotime += display_get_delta_time(); 177 | } 178 | rdpq_attach(display_get(), NULL); 179 | rdpq_clear(RGBA32(0,0,0,0)); 180 | rdpq_detach_show(); 181 | rspq_wait(); 182 | if(background_block) {rspq_block_free(background_block); background_block = NULL;} 183 | if(background) {sprite_free(background); background = NULL;} 184 | } 185 | } 186 | 187 | void menu_cover(){ 188 | if(background_block) {rspq_block_free(background_block); background_block = NULL;} 189 | if(background) {sprite_free(background); background = NULL;} 190 | background = sprite_load("rom:/textures/logos/cover.sprite"); 191 | if(bgmusic_name[0] == 0) bgm_play("1_select", true, 0); 192 | bool pressed_start = false; 193 | float logotime = 1; 194 | float gtime = 0; 195 | while(logotime > 0){ 196 | joypad_poll(); 197 | joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 198 | if(pressed.start && !pressed_start) { 199 | pressed_start = true; 200 | sound_play("menu_confirm", false); 201 | effects_add_rumble(JOYPAD_PORT_1, 0.1f); 202 | sound_play("DrivingStrikers", false); 203 | } 204 | if(pressed_start) logotime -= display_get_delta_time(); 205 | gtime += display_get_delta_time(); 206 | 207 | audioutils_mixer_update(); 208 | effects_update(); 209 | 210 | rdpq_attach(display_get(), NULL); 211 | render_background(); 212 | 213 | rdpq_set_mode_standard(); 214 | rdpq_mode_combiner(RDPQ_COMBINER_FLAT); 215 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 216 | rdpq_mode_dithering(DITHER_BAYER_INVBAYER); 217 | rdpq_set_prim_color(RGBA32(0,0,0, logotime * 120 + 10)); 218 | rdpq_fill_rectangle(0, 350, display_get_width(), 430); 219 | 220 | rdpq_textparms_t parmstext = {0}; parmstext.valign = VALIGN_CENTER; parmstext.align = ALIGN_CENTER; parmstext.width = display_get_width(); parmstext.height = 80; parmstext.style_id = 1; 221 | if(((int)gtime % 2) == 0 && !pressed_start) rdpq_text_printf(&parmstext, 2, 0,350, dictstr("press_start")); 222 | rdpq_detach_show(); 223 | } 224 | rspq_wait(); 225 | if(background_block) {rspq_block_free(background_block); background_block = NULL;} 226 | if(background) {sprite_free(background); background = NULL;} 227 | } 228 | 229 | void wait_sec_audio(float s){ 230 | while(s > 0){ 231 | s -= display_get_delta_time(); 232 | audioutils_mixer_update(); 233 | rdpq_attach(display_get(), NULL); 234 | rdpq_detach_show(); 235 | } 236 | } 237 | 238 | // cursed thing 239 | void menu_quick_game(){ 240 | int selection = 0; 241 | float offset = 400; 242 | sprite_t* selected_spr = sprite_load(teams[selection].logofilename); 243 | sprite_t* arrow_spr = sprite_load("rom:/textures/ui/chevron_right.rgba32.sprite"); 244 | sprite_t* controller_spr[4]; for(int i = 0; i < MAXPLAYERS; i++) {char csprfn[256]; sprintf(csprfn, "rom:/textures/ui/controller%i.rgba32.sprite", i+1); controller_spr[i] = sprite_load(csprfn);} 245 | rspq_block_t* spr_block = NULL; 246 | int selection_stage = 0; 247 | float stage_time = -1; 248 | bool map_start = false; 249 | teamdef_t* teams_selected[2]= {NULL}; int teams_selected_index[2] = {0}; 250 | mapinfo_t* map_selected = NULL; 251 | struct{ 252 | int c_team[MAXPLAYERS]; 253 | bool c_pressedstart[MAXPLAYERS]; 254 | float c_position[MAXPLAYERS]; 255 | int controllers[MAXPLAYERS]; 256 | bool can_start; 257 | } team_assignment = {0}; 258 | if(joypad_is_connected(0)) {team_assignment.c_pressedstart[0] = true; team_assignment.c_team[0] = -1;} // first controller player is joined by default 259 | 260 | effects_rumble_stop(); 261 | while(true){ 262 | offset = fm_lerp(offset, 0, 0.25f); 263 | joypad_poll(); 264 | // team selection 265 | int axis_stick_x = joypad_get_axis_pressed(JOYPAD_PORT_1, JOYPAD_AXIS_STICK_X); 266 | bool changed_selection = false; 267 | if(stage_time > 0) { 268 | stage_time -= display_get_delta_time(); 269 | if(stage_time <= 0 && selection_stage == 0) {selection_stage++; selection = 5; changed_selection = true;} 270 | else if(stage_time <= 0 && selection_stage == 1) {selection_stage++; if(selection == 5) selection = randr(0,4); map_selected = &maps[selection];} 271 | else if(stage_time <= 0 && selection_stage == 2) {selection_stage++; map_start = true; break;} 272 | } 273 | 274 | if(selection_stage == 1 || selection_stage == 0) 275 | if(axis_stick_x != 0 && stage_time <= 0){ 276 | effects_add_rumble(JOYPAD_PORT_1, 0.1f); 277 | sound_play("menu_navigate", false); 278 | selection += axis_stick_x; 279 | if(&teams[selection] == teams_selected[0] && selection_stage == 0) selection += axis_stick_x; 280 | if(selection_stage == 1) selection = iwrap(selection, 0, 5); 281 | else selection = iwrap(selection, 0, 7); 282 | changed_selection = true; 283 | } 284 | if(selection_stage == 2 && stage_time <= 0){ 285 | for(int i = 0; i < MAXPLAYERS; i++){ 286 | if(joypad_is_connected(i)){ 287 | joypad_buttons_t pressed = joypad_get_buttons_pressed(i); 288 | int axis_stick_x = joypad_get_axis_pressed(i, JOYPAD_AXIS_STICK_X); 289 | if(pressed.start) { 290 | effects_add_rumble(i, 0.1f); 291 | sound_play("menu_navigate", false); 292 | team_assignment.c_pressedstart[i] = !team_assignment.c_pressedstart[i];} 293 | if(axis_stick_x != 0 && team_assignment.c_pressedstart[i]){ 294 | effects_add_rumble(i, 0.1f); 295 | sound_play("menu_navigate", false); 296 | team_assignment.c_team[i] += axis_stick_x; team_assignment.c_team[i] = iwrap(team_assignment.c_team[i], -1, 1); 297 | } 298 | } else {team_assignment.c_pressedstart[i] = false; team_assignment.c_team[i] = 0;} 299 | team_assignment.c_position[i] = fm_lerp(team_assignment.c_position[i], 100 * team_assignment.c_team[i], 0.15f); 300 | } 301 | int left_amount = 0, right_amount = 0; 302 | team_assignment.can_start = false; 303 | for(int i = 0; i < MAXPLAYERS; i++){ 304 | team_assignment.controllers[i] = -1; 305 | if(team_assignment.c_pressedstart[i]) team_assignment.can_start = true; 306 | } 307 | if(team_assignment.can_start) 308 | for(int i = 0; i < MAXPLAYERS; i++){ 309 | if(team_assignment.c_pressedstart[i] && !team_assignment.c_team[i]) team_assignment.can_start = false; 310 | } if(team_assignment.can_start){ 311 | for(int i = 0; i < MAXPLAYERS; i++){ 312 | if(team_assignment.c_pressedstart[i]){ 313 | if(team_assignment.c_team[i] == 1) {if(right_amount < 2) team_assignment.controllers[2+right_amount] = i; right_amount++;} 314 | else if(team_assignment.c_team[i] == -1) {if(left_amount < 2) team_assignment.controllers[left_amount] = i; left_amount++;} 315 | } 316 | } 317 | if(left_amount > 2 || right_amount > 2) team_assignment.can_start = false; 318 | } 319 | } 320 | // select the team from the menu if A pressed 321 | joypad_buttons_t pressed = {0}; 322 | if(stage_time <= 0) pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 323 | 324 | if(pressed.a) { 325 | effects_add_rumble(JOYPAD_PORT_1, 0.1f); 326 | sound_play("menu_confirm", false); 327 | if(selection_stage == 0){ 328 | if(!teams_selected[0]) {teams_selected[0] = &teams[selection]; sound_play(teams_selected[0]->voicenamefn, false); teams_selected_index[0] = selection; selection++; selection = iwrap(selection, 0, 7); changed_selection = true;} 329 | else {teams_selected[1] = &teams[selection]; sound_play(teams_selected[1]->voicenamefn, false); teams_selected_index[1] = selection; stage_time = 2.0f;} 330 | } else if(selection_stage == 1){ 331 | if(maps[selection].unlocked || gamestatus.state.stadium_unlocked){ 332 | stage_time = 2.0f; if(maps[selection].voicenamefn) sound_play(maps[selection].voicenamefn, false); 333 | } 334 | } else if(selection_stage == 2 && team_assignment.can_start) {stage_time = 2.0f; sound_play("StartYourEngines", false);} 335 | } // back from the menu if B pressed 336 | if(pressed.b) { 337 | effects_add_rumble(JOYPAD_PORT_1, 0.1f); 338 | sound_play("menu_confirm", false); 339 | break; 340 | } 341 | 342 | rdpq_attach(display_get(), NULL); 343 | // background drawing 344 | render_background(); 345 | 346 | // team sprite drawing 347 | if(selection_stage == 1 || selection_stage == 0){ 348 | if(stage_time <= 0 || stage_time > 1.5f) rdpq_set_prim_color(RGBA32(127,127,127,255)); 349 | else rdpq_set_prim_color(RGBA32(127,127,127,(int)(stage_time*30)%3 == 0? 255 : 50)); 350 | render_sprite(selected_spr, &spr_block, display_get_width() / 2, display_get_height() / 2); 351 | } 352 | if(selection_stage == 2){ 353 | if(stage_time <= 0 || stage_time > 1.5f) rdpq_set_env_color(RGBA32(127,127,127,255)); 354 | else rdpq_set_env_color(RGBA32(127,127,127,(int)(stage_time*30)%3 == 0? 255 : 50)); 355 | rdpq_mode_combiner(RDPQ_COMBINER1((ENV,PRIM,TEX0,TEX0), (TEX0,0,ENV,0))); 356 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 357 | for(int i = 0; i < MAXPLAYERS; i++){ 358 | if(joypad_is_connected(i)){ 359 | if(stage_time <= 0 || stage_time > 1.5f) rdpq_set_env_color(RGBA32(127,127,127,team_assignment.c_pressedstart[i]? 255 : 50)); 360 | rdpq_sprite_blit_anchor(controller_spr[i], ALIGN_CENTER, VALIGN_CENTER, team_assignment.c_position[i] + (display_get_width() / 2), 150 + 64*i, NULL); 361 | } 362 | } 363 | //rdpq_textparms_t parmstext = {0}; parmstext.style_id = 1; 364 | //rdpq_text_printf(&parmstext, 3, 40, 40, "%i %i %i %i", team_assignment.controllers[0], team_assignment.controllers[1],team_assignment.controllers[2],team_assignment.controllers[3]); 365 | } 366 | 367 | // various sprites 368 | rdpq_blitparms_t bparms; 369 | rdpq_sprite_blit(arrow_spr, 440, 200, &bparms); bparms.flip_x = true; 370 | rdpq_sprite_blit(arrow_spr, 135, 200, &bparms); 371 | rdpq_mode_combiner(RDPQ_COMBINER_TEX); 372 | if(selection_stage != 2 || team_assignment.can_start) rdpq_sprite_blit(button_a, 360 - offset, 420, NULL); 373 | rdpq_sprite_blit(button_b, 500 - offset, 420, NULL); 374 | 375 | rdpq_textparms_t parmstext = {0}; parmstext.valign = VALIGN_CENTER; parmstext.align = ALIGN_CENTER; parmstext.width = display_get_width(); parmstext.height = 80; parmstext.style_id = 1; 376 | if(selection_stage == 0){ 377 | rdpq_text_printf(&parmstext, 2, 0, 40, dictstr("mm_qm_choose_teams")); 378 | rdpq_text_printf(&parmstext, 3, 0,360, "%s VS %s", teams_selected[0]? teams_selected[0]->teamname : "?", teams_selected[1]? teams_selected[1]->teamname : "?"); 379 | } else if (selection_stage == 1){ 380 | rdpq_text_printf(&parmstext, 2, 0, 40, dictstr("mm_qm_choose_map")); 381 | 382 | rdpq_text_printf(&parmstext, 3, 0,330, maps[selection].unlocked || gamestatus.state.stadium_unlocked? maps[selection].name : dictstr("mm_qm_map_unknown")); 383 | } else{ 384 | rdpq_text_printf(&parmstext, 2, 0, 40, dictstr("mm_qm_choose_assign")); 385 | rdpq_text_printf(&parmstext, 3, 0,360, "%s %s", teams_selected[0]? teams_selected[0]->teamname : "?", teams_selected[1]? teams_selected[1]->teamname : "?"); 386 | } 387 | // team selection description 388 | 389 | rdpq_textparms_t parms2; parms2.style_id = 2; 390 | rdpq_text_printf(&parms2, 3, 540 - offset, 445, dictstr("mm_back")); 391 | if(selection_stage != 2) rdpq_text_printf(&parms2, 3, 400 - offset, 445, dictstr("mm_select")); 392 | else if(team_assignment.can_start) rdpq_text_printf(&parms2, 3, 400 - offset, 445, dictstr("mm_play")); 393 | 394 | rdpq_detach_show(); 395 | 396 | if(changed_selection){ // load the appropriate team image if selected 397 | rspq_wait(); 398 | sprite_free(selected_spr); rspq_block_free(spr_block); spr_block = NULL; 399 | if(selection_stage == 0) 400 | selected_spr = sprite_load(teams[selection].logofilename); 401 | else { 402 | if(maps[selection].unlocked || gamestatus.state.stadium_unlocked) 403 | selected_spr = sprite_load(maps[selection].previewimgfn); 404 | else selected_spr = sprite_load("rom:/textures/ui/stadiums/UNL.rgba32.sprite"); 405 | } 406 | } 407 | 408 | audioutils_mixer_update(); 409 | effects_update(); 410 | } 411 | rspq_wait(); 412 | sprite_free(arrow_spr); 413 | sprite_free(selected_spr); rspq_block_free(spr_block); spr_block = NULL; 414 | for(int i = 0; i < MAXPLAYERS; i++) sprite_free(controller_spr[i]); 415 | if(map_start){ 416 | if(background_block) {rspq_block_free(background_block); background_block = NULL;} 417 | if(background) {sprite_free(background); background = NULL;} 418 | matchinfo_init(); 419 | teaminfo_init_controllers(team_assignment.controllers); 420 | teaminfo_init(teams_selected_index[0], TEAM_LEFT); 421 | teaminfo_init(teams_selected_index[1], TEAM_RIGHT); 422 | rspq_wait(); 423 | display_close(); 424 | display_init(RESOLUTION_640x480, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_DEDITHER); 425 | game_start(map_selected); 426 | rspq_wait(); 427 | display_close(); 428 | display_init(RESOLUTION_640x480, DEPTH_16_BPP, is_memory_expanded()? 3 : 2, GAMMA_NONE, FILTERS_DEDITHER); 429 | background = sprite_load("rom:/textures/ui/menu_bg.sprite"); 430 | bgm_play("1_select", true, 1); 431 | } 432 | } 433 | 434 | typedef struct{ 435 | teamdef_t* team; 436 | int teamindex; 437 | int wins, draws, losts; 438 | int goals_for, goals_against; 439 | int points; 440 | int plays; 441 | } teams_league_t; 442 | 443 | int teams_league_compare( const void* a, const void* b){ 444 | teams_league_t* A = ( (teams_league_t*) a ); 445 | teams_league_t* B = ( (teams_league_t*) b ); 446 | 447 | if ( A->points == B->points ) return 0; 448 | else if ( A->points < B->points ) return 1; 449 | else return -1; 450 | } 451 | 452 | // cursed thing number 2 453 | void menu_league(){ 454 | int selection = 0; 455 | sprite_t* selected_spr = sprite_load(teams[selection].logofilename); 456 | sprite_t* arrow_spr = sprite_load("rom:/textures/ui/chevron_right.rgba32.sprite"); 457 | rspq_block_t* spr_block = NULL; 458 | int selection_stage = 0; 459 | float stage_time = -1; 460 | bool map_start = false; 461 | teamdef_t* team_selected = NULL; int teams_selected_index = -1; 462 | mapinfo_t* map_selected = NULL; 463 | 464 | effects_rumble_stop(); 465 | float offset = 400; 466 | while(selection_stage == 0){ 467 | offset = fm_lerp(offset,0,0.25f); 468 | joypad_poll(); 469 | // team selection 470 | int axis_stick_x = joypad_get_axis_pressed(JOYPAD_PORT_1, JOYPAD_AXIS_STICK_X); 471 | bool changed_selection = false; 472 | if(stage_time > 0) { 473 | stage_time -= display_get_delta_time(); 474 | if(stage_time <= 0 && selection_stage == 0) {selection_stage++; break;} 475 | } 476 | 477 | if(selection_stage == 1 || selection_stage == 0) 478 | if(axis_stick_x != 0 && stage_time <= 0){ 479 | effects_add_rumble(JOYPAD_PORT_1, 0.1f); 480 | sound_play("menu_navigate", false); 481 | selection += axis_stick_x; 482 | if(selection_stage == 1) selection = iwrap(selection, 0, 5); 483 | else selection = iwrap(selection, 0, 7); 484 | changed_selection = true; 485 | } 486 | 487 | // select the team from the menu if A pressed 488 | joypad_buttons_t pressed = {0}; 489 | if(stage_time <= 0) pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 490 | 491 | if(pressed.a && stage_time <= 0) { 492 | effects_add_rumble(JOYPAD_PORT_1, 0.1f); 493 | sound_play("menu_confirm", false); 494 | if(selection_stage == 0){ 495 | {team_selected = &teams[selection]; sound_play(team_selected->voicenamefn, false); teams_selected_index = selection; stage_time = 2.0f;} 496 | } 497 | } // back from the menu if B pressed 498 | if(pressed.b) { 499 | effects_add_rumble(JOYPAD_PORT_1, 0.1f); 500 | sound_play("menu_confirm", false); 501 | break; 502 | } 503 | 504 | rdpq_attach(display_get(), NULL); 505 | // background drawing 506 | render_background(); 507 | 508 | // team sprite drawing 509 | if(selection_stage == 1 || selection_stage == 0){ 510 | if(stage_time <= 0 || stage_time > 1.5f) rdpq_set_prim_color(RGBA32(127,127,127,255)); 511 | else rdpq_set_prim_color(RGBA32(127,127,127,(int)(stage_time*30)%3 == 0? 255 : 50)); 512 | render_sprite(selected_spr, &spr_block, display_get_width() / 2, display_get_height() / 2); 513 | } 514 | 515 | // various sprites 516 | rdpq_blitparms_t bparms; 517 | rdpq_sprite_blit(arrow_spr, 440, 200, &bparms); bparms.flip_x = true; 518 | rdpq_sprite_blit(arrow_spr, 135, 200, &bparms); 519 | rdpq_mode_combiner(RDPQ_COMBINER_TEX); 520 | rdpq_sprite_blit(button_a, 360 - offset, 420, NULL); 521 | rdpq_sprite_blit(button_b, 500 - offset, 420, NULL); 522 | 523 | rdpq_textparms_t parmstext = {0}; parmstext.valign = VALIGN_CENTER; parmstext.align = ALIGN_CENTER; parmstext.width = display_get_width(); parmstext.height = 80; parmstext.style_id = 1; 524 | if(selection_stage == 0){ 525 | rdpq_text_printf(&parmstext, 2, 0, 40, dictstr("mm_qm_choose_teams")); 526 | rdpq_text_printf(&parmstext, 3, 0,360, "%s", team_selected? team_selected->teamname : "?"); 527 | } 528 | // team selection description 529 | 530 | rdpq_textparms_t parms2; parms2.style_id = 2; 531 | rdpq_text_printf(&parms2, 3, 540 - offset, 445, dictstr("mm_back")); 532 | rdpq_text_printf(&parms2, 3, 400 - offset, 445, dictstr("mm_select")); 533 | 534 | rdpq_detach_show(); 535 | 536 | if(changed_selection){ // load the appropriate team image if selected 537 | rspq_wait(); 538 | sprite_free(selected_spr); rspq_block_free(spr_block); spr_block = NULL; 539 | if(selection_stage == 0) 540 | selected_spr = sprite_load(teams[selection].logofilename); 541 | } 542 | 543 | audioutils_mixer_update(); 544 | effects_update(); 545 | } 546 | rspq_wait(); 547 | sprite_free(arrow_spr); 548 | sprite_free(selected_spr); rspq_block_free(spr_block); spr_block = NULL; 549 | if(teams_selected_index >= 0){ 550 | teams_league_t teams_league[8] = {0};{ 551 | int curteam = teams_selected_index; 552 | for(int i = 0; i < 8; i++){ 553 | teams_league[i].team = &teams[curteam]; 554 | teams_league[i].teamindex = curteam; 555 | curteam += 1; curteam = iwrap(curteam, 0, 7); 556 | } 557 | } 558 | teams_league_t teams_league_sorted[8]; 559 | 560 | int lastmapplayed = -1; 561 | int curteamagainst = teams_selected_index; 562 | int curteamagainst_lli = 0; 563 | for(int i = 0; i < 8; i++){ 564 | float offset = 400; 565 | memcpy(teams_league_sorted, teams_league, sizeof(teams_league)); 566 | qsort(teams_league_sorted, 8, sizeof(teams_league_t), teams_league_compare); 567 | 568 | curteamagainst = iwrap(curteamagainst + 1, 0, 7); 569 | curteamagainst_lli++; 570 | rspq_block_t* tableblock = NULL; 571 | while(true){ 572 | offset = fm_lerp(offset, 0, 0.25f); 573 | joypad_poll(); 574 | audioutils_mixer_update(); 575 | 576 | joypad_buttons_t pressed = {0}; 577 | pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 578 | if(pressed.b && i < 7) {i = 8; teams_league[0].points = -1; break;} 579 | if(pressed.a && i < 7) {sound_play(teams[curteamagainst].voicenamefn, false); break;} 580 | if(pressed.b && i >= 7) break; 581 | 582 | rdpq_attach(display_get(), NULL); 583 | // background drawing 584 | render_background(); 585 | 586 | rdpq_textparms_t parmstext = {0}; parmstext.valign = VALIGN_CENTER; parmstext.align = ALIGN_CENTER; parmstext.width = display_get_width(); parmstext.height = 80; parmstext.style_id = 1; 587 | rdpq_text_printf(&parmstext, 2, 0, 40, dictstr("mm_league")); 588 | // team selection description 589 | rdpq_fontstyle_t style; style.color = RGBA32(255,0,0,255); 590 | rdpq_font_style(fonts[2], 5, &style); 591 | 592 | rdpq_textparms_t parms2; parms2.style_id = 2; 593 | rdpq_text_printf(&parms2, 3, (i < 7? 540 : 500) - offset, 445, i < 7? dictstr("mm_back") : dictstr("match_s_continue")); 594 | if(i < 7) rdpq_text_printf(&parms2, 3, 400 - offset, 445, dictstr("mm_play")); 595 | 596 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 597 | rdpq_mode_combiner(RDPQ_COMBINER_TEX); 598 | if(i < 7) rdpq_sprite_blit(button_a, 360 - offset, 420, NULL); 599 | rdpq_sprite_blit(button_b, (i < 7? 500 : 460) - offset, 420, NULL); 600 | 601 | if(!tableblock){ 602 | rspq_block_begin(); 603 | rdpq_mode_combiner(RDPQ_COMBINER_FLAT); 604 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 605 | rdpq_set_prim_color(RGBA32(0,0,0,128)); 606 | rdpq_fill_rectangle(60 , 140, 600 , 400); 607 | rdpq_set_prim_color(RGBA32(255,255,0,128)); 608 | for(int i = 0; i < 8; i++) 609 | if(teams_league_sorted[i].teamindex == teams_selected_index) 610 | rdpq_fill_rectangle(60 , 200 + 25*i, 600 , 225 + 25*i); 611 | 612 | parmstext.valign = VALIGN_CENTER; parmstext.align = ALIGN_LEFT; parmstext.width = display_get_width(); parmstext.height = 40; parmstext.style_id = 1; 613 | const char* tablenames[7] = {"P", "W", "D", "L", "GF", "GA", "PTS"}; 614 | for(int i = 0; i < 7; i++){ 615 | parmstext.style_id = 5; 616 | rdpq_text_printf(&parmstext, 3, 330 + i*35, 150, tablenames[i]); 617 | } 618 | for(int i = 0; i < 8; i++){ 619 | parmstext.style_id = 2; 620 | rdpq_text_printf(&parmstext, 3, 80 , 195 + 25*i, "%s", teams_league_sorted[i].team->teamname); 621 | rdpq_text_printf(&parmstext, 3, 300 + 30 , 195 + 25*i, "%i", teams_league_sorted[i].plays); 622 | rdpq_text_printf(&parmstext, 3, 335 + 30 , 195 + 25*i, "%i", teams_league_sorted[i].wins); 623 | rdpq_text_printf(&parmstext, 3, 370 + 30 , 195 + 25*i, "%i", teams_league_sorted[i].draws); 624 | rdpq_text_printf(&parmstext, 3, 405 + 30 , 195 + 25*i, "%i", teams_league_sorted[i].losts); 625 | rdpq_text_printf(&parmstext, 3, 440 + 30 , 195 + 25*i, "%i", teams_league_sorted[i].goals_for); 626 | rdpq_text_printf(&parmstext, 3, 475 + 30 , 195 + 25*i, "%i", teams_league_sorted[i].goals_against); 627 | parmstext.style_id = 5; 628 | rdpq_text_printf(&parmstext, 3, 510 + 30 , 195 + 25*i, "%i", teams_league_sorted[i].points); 629 | } 630 | tableblock = rspq_block_end(); 631 | } rspq_block_run(tableblock); 632 | 633 | rdpq_detach_show(); 634 | } 635 | rspq_wait(); 636 | rspq_block_free(tableblock); 637 | if(i < 7){ 638 | if(background_block) {rspq_block_free(background_block); background_block = NULL;} 639 | if(background) {sprite_free(background); background = NULL;} 640 | matchinfo_init(); 641 | int controllers[4] = {0,-1,-1,-1}; 642 | teaminfo_init_controllers(controllers); 643 | teaminfo_init(teams_selected_index, TEAM_LEFT); 644 | teaminfo_init(curteamagainst, TEAM_RIGHT); 645 | rspq_wait(); 646 | display_close(); 647 | display_init(RESOLUTION_640x480, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_DEDITHER); 648 | int curmap = randm(4); while(curmap == lastmapplayed) curmap = randm(4); // random map, don't repeat consecutevly 649 | game_start(&maps[curmap]); 650 | rspq_wait(); 651 | display_close(); 652 | display_init(RESOLUTION_640x480, DEPTH_16_BPP,is_memory_expanded()? 3 : 2, GAMMA_NONE, FILTERS_DEDITHER); 653 | background = sprite_load("rom:/textures/ui/menu_bg.sprite"); 654 | bgm_play("1_select", true, 1); 655 | 656 | teams_league[0].wins += matchinfo.tleft.score > matchinfo.tright.score? 1 : 0; 657 | teams_league[0].draws += matchinfo.tleft.score == matchinfo.tright.score? 1 : 0; 658 | teams_league[0].losts += matchinfo.tleft.score < matchinfo.tright.score? 1 : 0; 659 | teams_league[0].goals_for += matchinfo.tleft.score; 660 | teams_league[0].goals_against += matchinfo.tright.score; 661 | teams_league[0].points += matchinfo.tleft.score > matchinfo.tright.score? 2 : (matchinfo.tleft.score == matchinfo.tright.score? 1 : 0); 662 | teams_league[0].plays += 1; 663 | 664 | teams_league[i + 1].wins += matchinfo.tleft.score < matchinfo.tright.score? 1 : 0; 665 | teams_league[i + 1].draws += matchinfo.tleft.score == matchinfo.tright.score? 1 : 0; 666 | teams_league[i + 1].losts += matchinfo.tleft.score > matchinfo.tright.score? 1 : 0; 667 | teams_league[i + 1].goals_for += matchinfo.tright.score; 668 | teams_league[i + 1].goals_against += matchinfo.tleft.score; 669 | teams_league[i + 1].points += matchinfo.tleft.score < matchinfo.tright.score? 2 : (matchinfo.tleft.score == matchinfo.tright.score? 1 : 0); 670 | teams_league[i + 1].plays += 1; 671 | 672 | if(matchinfo.exited) {teams_league[0].points = 0; i = 6;} 673 | }if(i < 7){ 674 | int compteam = 1; 675 | for(int i = 0; i < 3; i++){ // add random scores to the rest of the teams 676 | match_t info = {0}; 677 | info.tleft.score = randm(6); 678 | info.tright.score = randm(6); 679 | 680 | if(compteam == curteamagainst_lli) compteam++; 681 | 682 | if(teams_league[compteam].points >= 5 && info.tleft.score > info.tright.score) 683 | iswap(&info.tleft.score, &info.tright.score); // classic trollface moment 684 | 685 | teams_league[compteam].wins += info.tleft.score > info.tright.score? 1 : 0; 686 | teams_league[compteam].draws += info.tleft.score == info.tright.score? 1 : 0; 687 | teams_league[compteam].losts += info.tleft.score < info.tright.score? 1 : 0; 688 | teams_league[compteam].goals_for += info.tleft.score; 689 | teams_league[compteam].goals_against += info.tright.score; 690 | teams_league[compteam].points += info.tleft.score > info.tright.score? 2 : (info.tleft.score == info.tright.score? 1 : 0); 691 | teams_league[compteam].plays += 1; 692 | 693 | compteam++; if(compteam == curteamagainst_lli) compteam++; 694 | 695 | if(teams_league[compteam].points > 6) info.tleft.score = info.tright.score; // trollface moment no 2 696 | 697 | teams_league[compteam].wins += info.tleft.score < info.tright.score? 1 : 0; 698 | teams_league[compteam].draws += info.tleft.score == info.tright.score? 1 : 0; 699 | teams_league[compteam].losts += info.tleft.score > info.tright.score? 1 : 0; 700 | teams_league[compteam].goals_for += info.tright.score; 701 | teams_league[compteam].goals_against += info.tleft.score; 702 | teams_league[compteam].points += info.tleft.score < info.tright.score? 2 : (info.tleft.score == info.tright.score? 1 : 0); 703 | teams_league[compteam].plays += 1; 704 | 705 | compteam++; if(compteam == curteamagainst_lli) compteam++; 706 | } 707 | } else{ 708 | int maxpoints = 0; bool player_winner = false; 709 | for(int i = 0; i < 8; i++){ 710 | if(teams_league[i].points > maxpoints) maxpoints = teams_league[i].points; 711 | } if(maxpoints == teams_league[0].points) player_winner = true; 712 | 713 | if(player_winner){ 714 | gamestatus.state.stadium_unlocked = true; 715 | engine_eeprom_save_manual(); 716 | engine_eeprom_save_persistent(); 717 | float offset = 400; 718 | sound_play("Congratulations", false); 719 | bgm_play("9_congrats", true, 1); 720 | bool pressed_start = false; 721 | float logotime = 1; 722 | float gtime = 0; 723 | while(logotime > 0){ 724 | offset = fm_lerp(offset,0,0.25f); 725 | joypad_poll(); 726 | joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 727 | if(pressed.a && !pressed_start) { 728 | pressed_start = true; 729 | sound_play("menu_confirm", false); 730 | effects_add_rumble(JOYPAD_PORT_1, 0.1f); 731 | } 732 | if(pressed_start) logotime -= display_get_delta_time(); 733 | gtime += display_get_delta_time(); 734 | 735 | audioutils_mixer_update(); 736 | effects_update(); 737 | 738 | rdpq_attach(display_get(), NULL); 739 | render_background(); 740 | 741 | rdpq_set_mode_standard(); 742 | rdpq_mode_combiner(RDPQ_COMBINER_FLAT); 743 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 744 | rdpq_mode_dithering(DITHER_BAYER_INVBAYER); 745 | rdpq_set_prim_color(RGBA32(0,0,0, logotime * 128)); 746 | rdpq_fill_rectangle(0, 250, display_get_width(), 330); 747 | 748 | rdpq_textparms_t parmstext = {0}; parmstext.valign = VALIGN_CENTER; parmstext.align = ALIGN_CENTER; parmstext.width = display_get_width(); parmstext.height = 80; parmstext.style_id = 1; 749 | if(((int)gtime % 2) == 0 && !pressed_start) rdpq_text_printf(&parmstext, 2, 0,250, dictstr("lg_congratulations")); 750 | 751 | rdpq_mode_combiner(RDPQ_COMBINER_TEX); 752 | rdpq_sprite_blit(button_a, 460 - offset, 420, NULL); 753 | 754 | parmstext.valign = VALIGN_CENTER; parmstext.align = ALIGN_CENTER; parmstext.width = display_get_width(); parmstext.height = 80; parmstext.style_id = 1; 755 | // team selection description 756 | rdpq_fontstyle_t style; style.color = RGBA32(255,0,0,255); 757 | rdpq_font_style(fonts[2], 5, &style); 758 | 759 | rdpq_textparms_t parms2; parms2.style_id = 2; 760 | rdpq_text_printf(&parms2, 3, 500 - offset, 445, dictstr("match_s_continue")); 761 | 762 | parmstext.valign = VALIGN_CENTER; parmstext.align = ALIGN_CENTER; parmstext.width = display_get_width(); parmstext.height = 80; parmstext.style_id = 1; 763 | if(((int)gtime % 2) == 0 && !pressed_start) rdpq_text_printf(&parmstext, 3, 0 + offset,130, "%s", dictstr("lg_youwon")); 764 | if(((int)gtime % 2) == 0 && !pressed_start) rdpq_text_printf(&parmstext, 3, 0 + offset,160, "%s", dictstr("lg_stadiumunlocked")); 765 | 766 | rdpq_detach_show(); 767 | } 768 | menu_credits(); 769 | } 770 | } 771 | } 772 | 773 | } 774 | } 775 | 776 | 777 | void menu_settings(){ 778 | int selection = 0; 779 | float offset = 400; 780 | while(true){ 781 | offset = fm_lerp(offset, 0, 0.25f); 782 | joypad_poll(); 783 | joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 784 | if(joypad_get_axis_pressed(JOYPAD_PORT_1, JOYPAD_AXIS_STICK_Y)){ 785 | sound_play("menu_navigate", false); 786 | selection -= joypad_get_axis_pressed(JOYPAD_PORT_1, JOYPAD_AXIS_STICK_Y); 787 | } 788 | selection = iwrap(selection, 0, 4); 789 | 790 | if(joypad_get_axis_pressed(JOYPAD_PORT_1, JOYPAD_AXIS_STICK_X)){ 791 | sound_play("menu_navigate", false); 792 | effects_add_rumble(JOYPAD_PORT_1, 0.1f); 793 | int incr = joypad_get_axis_pressed(JOYPAD_PORT_1, JOYPAD_AXIS_STICK_X); 794 | switch(selection){ 795 | case 0:{ gamestatus.state.game.settings.duration = iwrap(gamestatus.state.game.settings.duration + incr, ONE_MINUTE, FIVE_MINUTES); break; } 796 | case 1:{ gamestatus.state.game.settings.graphics = iwrap(gamestatus.state.game.settings.graphics + incr, FASTEST, NICEST); break; } 797 | case 2:{ gamestatus.state.game.settings.vibration = !gamestatus.state.game.settings.vibration; break;} 798 | case 3:{ gamestatus.state.game.settings.deadzone = fwrap(gamestatus.state.game.settings.deadzone + ((float)incr * 0.05f), 0.0f, 0.9f); break; } 799 | case 4:{ 800 | gamestatus.state_persistent.current_language = iwrap(gamestatus.state_persistent.current_language + incr, 0, language_count - 1); 801 | rspq_wait(); 802 | font_clear(); 803 | engine_set_language(gamestatus.state_persistent.current_language); 804 | engine_load_dictionary(); 805 | font_setup(); 806 | break; } 807 | } 808 | } 809 | 810 | if(pressed.b) { 811 | sound_play("menu_confirm", false); 812 | effects_add_rumble(JOYPAD_PORT_1, 0.1f); 813 | engine_eeprom_save_manual(); 814 | engine_eeprom_save_persistent(); 815 | return; 816 | } 817 | 818 | audioutils_mixer_update(); 819 | effects_update(); 820 | 821 | rdpq_attach(display_get(), NULL); 822 | render_background(); 823 | 824 | rdpq_set_mode_standard(); 825 | rdpq_mode_combiner(RDPQ_COMBINER_TEX); 826 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 827 | rdpq_mode_dithering(DITHER_BAYER_INVBAYER); 828 | rdpq_blitparms_t parms; parms.cx = selector->width / 2; parms.scale_x = 2; 829 | rdpq_sprite_blit(selector, display_get_width() / 2 + offset, 160 + selection*40, &parms); 830 | rdpq_sprite_blit(button_b, 500 - offset, 420, NULL); 831 | 832 | rdpq_textparms_t parmstext = {0}; parmstext.valign = VALIGN_CENTER; parmstext.align = ALIGN_CENTER; parmstext.width = display_get_width(); parmstext.height = 80; parmstext.style_id = 1; 833 | rdpq_text_printf(&parmstext, 2, 0,40, dictstr("mm_o_options")); 834 | parmstext.height = 40; 835 | parmstext.align = ALIGN_RIGHT; parmstext.width = 200; 836 | rdpq_text_printf(&parmstext, 3, 80 + offset,160, dictstr("mm_o_duration")); 837 | rdpq_text_printf(&parmstext, 3, 80 + offset,200, dictstr("mm_o_graphics")); 838 | rdpq_text_printf(&parmstext, 3, 80 + offset,240, dictstr("mm_o_vibration")); 839 | rdpq_text_printf(&parmstext, 3, 80 + offset,280, dictstr("mm_o_deadzone")); 840 | rdpq_text_printf(&parmstext, 3, 80 + offset,320, dictstr("mm_o_language")); 841 | 842 | parmstext.align = ALIGN_CENTER; parmstext.width = 200; parmstext.style_id = 2; 843 | const char* text = "null"; 844 | {switch(gamestatus.state.game.settings.duration){case ONE_MINUTE: text = dictstr("mm_o_duration_1"); break; case TWO_MINUTES: text = dictstr("mm_o_duration_2"); break; case THREE_MINUTES: text = dictstr("mm_o_duration_3"); break; case FOUR_MINUTES: text = dictstr("mm_o_duration_4"); break; case FIVE_MINUTES: text = dictstr("mm_o_duration_5"); break;} 845 | rdpq_text_printf(&parmstext, 4, 320 + offset,160, text);} 846 | {switch(gamestatus.state.game.settings.graphics){case FASTEST: text = dictstr("mm_o_graphics_0"); break; case DEFAULT: text = dictstr("mm_o_graphics_1"); break; case NICEST: text = dictstr("mm_o_graphics_2"); break;} 847 | rdpq_text_printf(&parmstext, 4, 320 + offset,200, text);} 848 | rdpq_text_printf(&parmstext, 4, 320 + offset,240, gamestatus.state.game.settings.vibration? dictstr("mm_o_vibration_1") : dictstr("mm_o_vibration_0")); 849 | rdpq_text_printf(&parmstext, 4, 320 + offset,280, "%i%%", (int)(gamestatus.state.game.settings.deadzone * 100)); 850 | rdpq_text_printf(&parmstext, 4, 320 + offset,320, engine_get_language()); 851 | 852 | rdpq_textparms_t parms2; parms2.style_id = 2; 853 | rdpq_text_printf(&parms2, 3, 540 - offset, 445, dictstr("mm_save")); 854 | rdpq_detach_show(); 855 | } 856 | } 857 | 858 | void menu_main(){ 859 | if(background_block) {rspq_block_free(background_block); background_block = NULL;} 860 | if(background) {sprite_free(background); background = NULL;} 861 | background = sprite_load("rom:/textures/ui/menu_bg.sprite"); 862 | selector = sprite_load("rom:/textures/ui/selector.rgba32.sprite"); 863 | button_a = sprite_load("rom:/textures/ui/button_a.rgba32.sprite"); 864 | button_b = sprite_load("rom:/textures/ui/button_b.rgba32.sprite"); 865 | 866 | if(bgmusic_name[0] == 0) bgm_play("1_select", true, 0); 867 | int selection = 0; 868 | float offset = 400; 869 | 870 | while(true){ 871 | offset = fm_lerp(offset,0, 0.25f); 872 | joypad_poll(); 873 | joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 874 | if(joypad_get_axis_pressed(JOYPAD_PORT_1, JOYPAD_AXIS_STICK_Y)){ 875 | sound_play("menu_navigate", false); 876 | selection -= joypad_get_axis_pressed(JOYPAD_PORT_1, JOYPAD_AXIS_STICK_Y); 877 | } 878 | selection = iwrap(selection, 0, 3); 879 | 880 | if(pressed.a){ 881 | sound_play("menu_confirm", false); 882 | effects_add_rumble(JOYPAD_PORT_1, 0.1f); 883 | switch(selection){ 884 | case 0: 885 | menu_quick_game(); 886 | break; 887 | case 1: 888 | menu_league(); 889 | break; 890 | case 2: 891 | menu_settings(); 892 | break; 893 | case 3: 894 | menu_credits(); 895 | break; 896 | } 897 | offset = 400; 898 | } 899 | 900 | if(pressed.b) { 901 | sound_play("menu_confirm", false); 902 | effects_add_rumble(JOYPAD_PORT_1, 0.1f); 903 | menu_cover(); 904 | if(!background) background = sprite_load("rom:/textures/ui/menu_bg.sprite"); 905 | } 906 | 907 | audioutils_mixer_update(); 908 | effects_update(); 909 | 910 | rdpq_attach(display_get(), NULL); 911 | render_background(); 912 | 913 | rdpq_set_mode_standard(); 914 | rdpq_mode_combiner(RDPQ_COMBINER_TEX); 915 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 916 | rdpq_mode_dithering(DITHER_BAYER_INVBAYER); 917 | rdpq_sprite_blit_anchor(selector, ALIGN_CENTER, VALIGN_TOP, display_get_width() / 2 + offset, 160 + selection*40, NULL); 918 | rdpq_sprite_blit(button_a, 360 - offset, 420, NULL); 919 | rdpq_sprite_blit(button_b, 500 - offset, 420, NULL); 920 | 921 | rdpq_textparms_t parmstext = {0}; parmstext.valign = VALIGN_CENTER; parmstext.align = ALIGN_CENTER; parmstext.width = display_get_width(); parmstext.height = 80; parmstext.style_id = 1; 922 | rdpq_text_printf(&parmstext, 2, 0,40, dictstr("mm_title")); 923 | parmstext.height = 40; 924 | rdpq_text_printf(&parmstext, 3, 0 + offset,160, dictstr("mm_quick")); 925 | rdpq_text_printf(&parmstext, 3, 0 + offset,200, dictstr("mm_league")); 926 | rdpq_text_printf(&parmstext, 3, 0 + offset,240, dictstr("mm_options")); 927 | rdpq_text_printf(&parmstext, 3, 0 + offset,280, dictstr("mm_credits")); 928 | 929 | rdpq_textparms_t parms2; parms2.style_id = 2; 930 | rdpq_text_printf(&parms2, 3, 540- offset, 445, dictstr("mm_back")); 931 | rdpq_text_printf(&parms2, 3, 400- offset, 445, dictstr("mm_select")); 932 | rdpq_detach_show(); 933 | } 934 | rspq_wait(); 935 | if(background_block) {rspq_block_free(background_block); background_block = NULL;} 936 | if(background) {sprite_free(background); background = NULL;} 937 | 938 | } 939 | 940 | void menu_start(){ 941 | if(!engine_eeprom_load_persistent() || !engine_eeprom_load_manual()) 942 | state_init(); 943 | engine_load_dictionary(); 944 | font_setup(); 945 | menu_logos(); 946 | menu_cover(); 947 | menu_main(); 948 | rspq_wait(); 949 | display_close(); 950 | game_start(&maps[0]); 951 | } 952 | -------------------------------------------------------------------------------- /src/menu.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include "audioutils.h" 3 | 4 | void menu_logos(); 5 | 6 | void menu_cover(); 7 | 8 | void menu_start(); -------------------------------------------------------------------------------- /src/particles_sim.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // from example 19_particles_tex in Tiny3D 8 | 9 | int currentPart = 0; 10 | 11 | color_t blend_colors(color_t colorA, color_t colorB, float t) { 12 | color_t color; 13 | color.r = (uint8_t)(colorA.r * (1.0f - t) + colorB.r * t); 14 | color.g = (uint8_t)(colorA.g * (1.0f - t) + colorB.g * t); 15 | color.b = (uint8_t)(colorA.b * (1.0f - t) + colorB.b * t); 16 | color.a = (uint8_t)(colorA.a * (1.0f - t) + colorB.a * t); 17 | return color; 18 | } 19 | 20 | 21 | 22 | color_t get_rand_color(int bright) { 23 | return (color_t){ 24 | (uint8_t)(bright + (rand() % (256 - bright))), 25 | (uint8_t)(bright + (rand() % (256 - bright))), 26 | (uint8_t)(bright + (rand() % (256 - bright))), 27 | 0xFF 28 | }; 29 | } 30 | 31 | // Fire color: white -> yellow/orange -> red -> black 32 | void gradient_fire(uint8_t *color, float t) { 33 | t = fminf(1.0f, fmaxf(0.0f, t)); 34 | t = 0.8f - t; 35 | t *= t; 36 | 37 | if (t < 0.25f) { // Dark red to bright red 38 | color[0] = (uint8_t)(200 * (t / 0.25f)) + 55; 39 | color[1] = 0; 40 | color[2] = 0; 41 | } else if (t < 0.5f) { // Bright red to yellow 42 | color[0] = 255; 43 | color[1] = (uint8_t)(255 * ((t - 0.25f) / 0.25f)); 44 | color[2] = 0; 45 | } else if (t < 0.75f) { // Yellow to white (optional, if you want a bright white center) 46 | color[0] = 255; 47 | color[1] = 255; 48 | color[2] = (uint8_t)(255 * ((t - 0.5f) / 0.25f)); 49 | } else { // White to black 50 | color[0] = (uint8_t)(255 * (1.0f - (t - 0.75f) / 0.25f)); 51 | color[1] = (uint8_t)(255 * (1.0f - (t - 0.75f) / 0.25f)); 52 | color[2] = (uint8_t)(255 * (1.0f - (t - 0.75f) / 0.25f)); 53 | } 54 | } 55 | 56 | float fract(float a) { 57 | return a - floorf(a); 58 | } 59 | 60 | float randNoise3d_rand(float coX, float coY){ 61 | coX = fabsf(coX) * 15.1335f; 62 | coY = fabsf(coY) * 61.15654f; 63 | coX += coY; 64 | return fract(fm_sinf(coX) * 65979.1347f); 65 | } 66 | 67 | T3DVec3 randNoise3d(float uvX, float uvY) { 68 | return (T3DVec3){{ 69 | randNoise3d_rand(uvX + 0.23f * 382.567f, uvX + 0.23f * 382.567f), 70 | randNoise3d_rand(uvY + 0.65f * 330.356f, uvX + 0.65f * 330.356f), 71 | randNoise3d_rand(uvX + 0.33f * 356.346f, uvY + 0.33f * 356.346f) 72 | }}; 73 | } 74 | 75 | int noise_2d(int x, int y) { 76 | int n = x + y * 57; 77 | n = (n << 13) ^ n; 78 | return (n * (n * n * 60493 + 19990303) + 89); 79 | } 80 | 81 | void generate_particles_random(TPXParticle *particles, uint32_t count) { 82 | for (int i = 0; i < count; i++) { 83 | int p = i / 2; 84 | int8_t *ptPos = i % 2 == 0 ? particles[p].posA : particles[p].posB; 85 | uint8_t *ptColor = i % 2 == 0 ? particles[p].colorA : particles[p].colorB; 86 | 87 | particles[p].sizeA = 20 + (rand() % 10); 88 | particles[p].sizeB = 20 + (rand() % 10); 89 | 90 | T3DVec3 pos = {{ 91 | (i * 1 + rand()) % 128 - 64, 92 | (i * 3 + rand()) % 128 - 64, 93 | (i * 4 + rand()) % 128 - 64 94 | }}; 95 | 96 | t3d_vec3_norm(&pos); 97 | float len = rand() % 40; 98 | pos.v[0] *= len; 99 | pos.v[1] *= len; 100 | pos.v[2] *= len; 101 | 102 | ptPos[0] = (rand() % 256) - 128; 103 | ptPos[1] = (rand() % 256) - 128; 104 | ptPos[2] = (rand() % 256) - 128; 105 | 106 | ptColor[0] = 25 + (rand() % 230); 107 | ptColor[1] = 25 + (rand() % 230); 108 | ptColor[2] = 25 + (rand() % 230); 109 | ptColor[3] = 0; // alpha is the texture offset, as with the global one in 1/4h of a pixel steps 110 | } 111 | } 112 | 113 | void simulate_particles_rain(TPXParticle *particles, uint32_t partCount) { 114 | // move all up by one unit 115 | for (int i = 0; i < partCount/2; i++) { 116 | particles[i].posA[1] -= 14; 117 | particles[i].posB[1] -= 14; 118 | } 119 | } 120 | 121 | void simulate_particles_embers(TPXParticle *particles, uint32_t partCount) { 122 | // move all up by one unit 123 | for (int i = 0; i < partCount/2; i++) { 124 | int rnd = rand() % 7; particles[i].posA[0] += (rnd - 3) / 3; 125 | rnd = rand() % 7; particles[i].posA[2] += (rnd - 3) / 3; 126 | particles[i].posA[1] += 1; 127 | particles[i].posB[1] += 1; 128 | } 129 | } 130 | 131 | void simulate_particles_snow(TPXParticle *particles, uint32_t partCount) { 132 | // move all up by one unit 133 | for (int i = 0; i < partCount/2; i++) { 134 | int rnd = rand() % 7; particles[i].posA[0] += (rnd - 3) / 3; 135 | rnd = rand() % 7; particles[i].posA[2] += (rnd - 3) / 3; 136 | particles[i].posA[1] -= 1; 137 | particles[i].posB[1] -= 1; 138 | } 139 | } 140 | 141 | /** 142 | * Particle system for a fire effect. 143 | * This will simulate particles over time by moving them up and changing their color. 144 | * The current position is used to spawn new particles, so it can move over time leaving a trail behind. 145 | */ 146 | void simulate_particles_fire(TPXParticle *particles, uint32_t partCount, float posX, float posZ) { 147 | uint32_t p = currentPart / 2; 148 | if(currentPart % (1+(rand() % 3)) == 0) { 149 | int8_t *ptPos = currentPart % 2 == 0 ? particles[p].posA : particles[p].posB; 150 | int8_t *size = currentPart % 2 == 0 ? &particles[p].sizeA : &particles[p].sizeB; 151 | uint8_t *color = currentPart % 2 == 0 ? particles[p].colorA : particles[p].colorB; 152 | 153 | ptPos[0] = posX + (rand() % 16) - 8; 154 | ptPos[1] = -126; 155 | gradient_fire(color, 0); 156 | color[3] = (PhysicalAddr(ptPos) % 8) * 32; 157 | 158 | ptPos[2] = posZ + (rand() % 16) - 8; 159 | *size = 60 + (rand() % 10); 160 | } 161 | currentPart = (currentPart + 1) % partCount; 162 | 163 | // move all up by one unit 164 | for (int i = 0; i < partCount/2; i++) { 165 | gradient_fire(particles[i].colorA, (particles[i].posA[1] + 127) / 150.0f); 166 | gradient_fire(particles[i].colorB, (particles[i].posB[1] + 127) / 150.0f); 167 | 168 | particles[i].posA[1] += 1; 169 | particles[i].posB[1] += 1; 170 | if(currentPart % 4 == 0) { 171 | particles[i].sizeA -= 2; 172 | particles[i].sizeB -= 2; 173 | if(particles[i].sizeA < 0)particles[i].sizeA = 0; 174 | if(particles[i].sizeB < 0)particles[i].sizeB = 0; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/particles_sim.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef PARTICLES_SIM 3 | #define PARTICLES_SIM 4 | 5 | // from example 19_particles_tex in Tiny3D 6 | 7 | extern int currentPart; 8 | 9 | color_t blend_colors(color_t colorA, color_t colorB, float t); 10 | 11 | 12 | 13 | color_t get_rand_color(int bright); 14 | 15 | // Fire color: white -> yellow/orange -> red -> black 16 | void gradient_fire(uint8_t *color, float t); 17 | 18 | float fract(float a); 19 | 20 | float randNoise3d_rand(float coX, float coY); 21 | 22 | T3DVec3 randNoise3d(float uvX, float uvY); 23 | 24 | int noise_2d(int x, int y); 25 | 26 | void generate_particles_random(TPXParticle *particles, uint32_t count); 27 | 28 | void simulate_particles_embers(TPXParticle *particles, uint32_t partCount); 29 | 30 | void simulate_particles_rain(TPXParticle *particles, uint32_t partCount); 31 | 32 | void simulate_particles_snow(TPXParticle *particles, uint32_t partCount); 33 | 34 | /** 35 | * Particle system for a fire effect. 36 | * This will simulate particles over time by moving them up and changing their color. 37 | * The current position is used to spawn new particles, so it can move over time leaving a trail behind. 38 | */ 39 | void simulate_particles_fire(TPXParticle *particles, uint32_t partCount, float posX, float posZ); 40 | 41 | #endif -------------------------------------------------------------------------------- /src/playtime_logic.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "vi.h" 7 | #include "engine_gfx.h" 8 | #include "teams.h" 9 | #include "entity_logic.h" 10 | #include "audioutils.h" 11 | #include "effects.h" 12 | #include "playtime_logic.h" 13 | #include "maps.h" 14 | #include "engine_locale.h" 15 | 16 | match_t matchinfo; 17 | mapinfo_t* current_map = NULL; 18 | 19 | T3DViewport viewport[5]; 20 | color_t border_time_color; 21 | color_t border_time_score; 22 | uint8_t colorAmbient[4] = {255, 255, 255, 0xFF}; 23 | 24 | 25 | // Now allocate a fixed-point matrix, this is what t3d uses internally. 26 | // Note: this gets DMA'd to the RSP, so it needs to be uncached. 27 | // If you can't allocate uncached memory, remember to flush the cache after writing to it instead. 28 | T3DModel *model; 29 | T3DMat4 modelMat; 30 | T3DMat4FP* modelMatFP = NULL; 31 | 32 | T3DMat4FP* particleMatFP = NULL; 33 | TPXParticle* mapparticles = NULL; 34 | sprite_t* mapparticles_sprite = NULL; 35 | // Load a model-file, this contains the geometry and some metadata 36 | 37 | float offset[2] = {0}; 38 | T3DModelState matstate; 39 | TPE_World world; 40 | 41 | /** 42 | * Simple example with a 3d-model file created in blender. 43 | * This uses the builtin model format for loading and drawing a model. 44 | */ 45 | // The demo will only run for a single frame and stop. 46 | 47 | rspq_block_t *dplDraw = NULL; 48 | rspq_block_t *dplDrawB = NULL; 49 | rspq_block_t *dplDrawHUD = NULL; 50 | 51 | TPE_Unit 52 | ramp1[6] = { TPE_TOUNITS(3),TPE_TOUNITS(-3), TPE_TOUNITS(3),TPE_TOUNITS(3), TPE_TOUNITS(-3),TPE_TOUNITS(3) }, 53 | ramp2[6] = { TPE_TOUNITS(-3),TPE_TOUNITS(-3), TPE_TOUNITS(3),TPE_TOUNITS(-3), TPE_TOUNITS(3),TPE_TOUNITS(3) }, 54 | ramp3[6] = { TPE_TOUNITS(3),TPE_TOUNITS(3), TPE_TOUNITS(-3),TPE_TOUNITS(3), TPE_TOUNITS(-3),TPE_TOUNITS(-3) }, 55 | ramp4[6] = { TPE_TOUNITS(-3),TPE_TOUNITS(3), TPE_TOUNITS(-3),TPE_TOUNITS(-3), TPE_TOUNITS(3),TPE_TOUNITS(-3) }; 56 | 57 | typedef struct{ 58 | T3DVec3 camPos; 59 | T3DVec3 camTarget; 60 | 61 | T3DVec3 camPos_off; 62 | T3DVec3 camTarget_off; 63 | } camera_t; 64 | camera_t maincamera; 65 | 66 | typedef struct{ 67 | int idx; 68 | float dist; 69 | } entitysortgraph_t; 70 | 71 | int entitysortgraph_compare( const void* a, const void* b) 72 | { 73 | entitysortgraph_t* A = ( (entitysortgraph_t*) a ); 74 | entitysortgraph_t* B = ( (entitysortgraph_t*) b ); 75 | 76 | if ( A->dist == B->dist ) return 0; 77 | else if ( A->dist < B->dist ) return -1; 78 | else return 1; 79 | } 80 | 81 | float exposure = 5.0f; 82 | float exposure_bias = 0.55; 83 | float average_brightness = 0; 84 | 85 | #define RAND_SAMPLE_COUNT 16 86 | uint32_t sampleOffsets[RAND_SAMPLE_COUNT] = {0}; 87 | 88 | // autoexposure function for HDR lighting, this will control how bright the vertex colors are in range of 0-1 after T&L for the RDP HDR modulation through color combiner 89 | void exposure_set(void* framebuffer){ 90 | if(!framebuffer) return; 91 | surface_t* frame = (surface_t*) framebuffer; 92 | 93 | // sample points across the screen 94 | uint64_t *pixels = frame->buffer; // get the previous framebuffer (assumed 320x240 RGBA16 format), it shouldn't be cleared for consistency 95 | uint32_t maxIndex = frame->width * frame->height * 2 / 8; 96 | 97 | if(sampleOffsets[0] == 0) { 98 | for(int i = 0; i < RAND_SAMPLE_COUNT; i++){ 99 | sampleOffsets[i] = rand() % maxIndex; 100 | } 101 | } 102 | 103 | uint64_t brightInt = 0; 104 | for (int i = 0; i < RAND_SAMPLE_COUNT; ++i) { 105 | uint64_t pixels4 = pixels[sampleOffsets[i]]; 106 | for(int j = 0; j < 4; j++){ 107 | brightInt += (pixels4 & 0b11111'00000'00000'0) >> 10; 108 | brightInt += (pixels4 & 0b00000'11111'00000'0) >> 5; 109 | brightInt += (pixels4 & 0b00000'00000'11111'0); 110 | pixels4 >>= 16; 111 | } 112 | } 113 | 114 | brightInt /= RAND_SAMPLE_COUNT * 4; 115 | average_brightness = brightInt / (63.0f * 3.0f); 116 | 117 | // exposure bracket uses an overall bias of how the bright the framebuffer is at 0-1 scale 118 | // eg. if the avegare brightness of the framebuffer is > 0.7, then the exposure needs to go down until 119 | // the average brightness is 0.7, and the other way around 120 | if(average_brightness > exposure_bias) { 121 | exposure -= 0.01f; 122 | } 123 | else if(average_brightness < exposure_bias - 0.1f) { 124 | exposure += 0.01f; 125 | } 126 | 127 | // min/max exposure levels 128 | if(exposure > 3) exposure -= 0.05f; 129 | if(exposure < 0) exposure = 0; 130 | } 131 | 132 | 133 | float camlocations[5][4][3] = { 134 | {{7,5,24},{-7,5,24},{7,5,-24},{-7,5,-24}}, 135 | {{16,8,15},{-16,8,15},{20,4,5},{-20,4,5}}, 136 | {{18,12,15},{18,5,15},{6,4,5},{6,4,5}}, 137 | {{0,5,17},{0,5,17},{6,4,5},{-6,4,5}}, 138 | {{0,5,24},{0,5,12},{0,0,0},{0,0,0}} 139 | }; 140 | 141 | /** Function used for defining static environment, working similarly to an SDF 142 | (signed distance function). The parameters are: 3D point P, max distance D. 143 | The function should behave like this: if P is inside the solid environment 144 | volume, P will be returned; otherwise closest point (by Euclidean distance) to 145 | the solid environment volume from P will be returned, except for a case when 146 | this closest point would be further away than D, in which case any arbitrary 147 | point further away than D may be returned (this allows for optimizations). */ 148 | TPE_Vec3 environmentDistance(TPE_Vec3 point, TPE_Unit maxDistance) 149 | { 150 | TPE_ENV_START( TPE_envGround(point,0),point ) 151 | TPE_ENV_NEXT( TPE_envAABoxInside(point, TPE_vec3(0,0,0), TPE_vec3(TPE_TOUNITS(40),TPE_TOUNITS(40),TPE_TOUNITS(26))), point) 152 | if(point.x > 0){ 153 | if(point.z > 0){ 154 | TPE_ENV_NEXT( TPE_envAABox(point, TPE_vec3(TPE_TOUNITS(20),TPE_TOUNITS(0),TPE_TOUNITS(8)), TPE_vec3(TPE_TOUNITS(2),TPE_TOUNITS(40),TPE_TOUNITS(4))), point) 155 | TPE_ENV_NEXT(TPE_envAATriPrism(point, TPE_vec3(TPE_TOUNITS(15),(0),TPE_TOUNITS(10)), ramp1, TPE_TOUNITS(40), 1), point) 156 | } else{ 157 | TPE_ENV_NEXT( TPE_envAABox(point, TPE_vec3(TPE_TOUNITS(20),TPE_TOUNITS(0),TPE_TOUNITS(-8)), TPE_vec3(TPE_TOUNITS(2),TPE_TOUNITS(40),TPE_TOUNITS(4))), point) 158 | TPE_ENV_NEXT(TPE_envAATriPrism(point, TPE_vec3(TPE_TOUNITS(15),(0),TPE_TOUNITS(-10)), ramp2, TPE_TOUNITS(40), 1), point) 159 | } 160 | } else{ 161 | if(point.z > 0){ 162 | TPE_ENV_NEXT( TPE_envAABox(point, TPE_vec3(TPE_TOUNITS(-20),TPE_TOUNITS(0),TPE_TOUNITS(8)), TPE_vec3(TPE_TOUNITS(2),TPE_TOUNITS(40),TPE_TOUNITS(4))), point) 163 | TPE_ENV_NEXT(TPE_envAATriPrism(point, TPE_vec3(TPE_TOUNITS(-15),(0),TPE_TOUNITS(10)), ramp3, TPE_TOUNITS(40), 1), point) 164 | }else { 165 | TPE_ENV_NEXT( TPE_envAABox(point, TPE_vec3(TPE_TOUNITS(-20),TPE_TOUNITS(0),TPE_TOUNITS(-8)), TPE_vec3(TPE_TOUNITS(2),TPE_TOUNITS(40),TPE_TOUNITS(4))), point) 166 | TPE_ENV_NEXT(TPE_envAATriPrism(point, TPE_vec3(TPE_TOUNITS(-15),(0),TPE_TOUNITS(-10)), ramp4, TPE_TOUNITS(40), 1), point) 167 | } 168 | } 169 | 170 | TPE_ENV_END 171 | } 172 | 173 | 174 | /** Function that can be used as a joint-joint or joint-environment collision 175 | callback, parameters are following: body1 index, joint1 index, body2 index, 176 | joint2 index, collision world position. If body1 index is the same as body1 177 | index, then collision type is body-environment, otherwise it is body-body 178 | type. The function has to return either 1 if the collision is to be allowed 179 | or 0 if it is to be discarded. This can besides others be used to disable 180 | collisions between some bodies. */ 181 | 182 | 183 | void entityCollisionCallback_ballcar(uint16_t body1idx, uint16_t body2idx){ 184 | int caridx = playball.bodyidx == body1idx? body2idx : body1idx; 185 | bool shotontarget = playball.ballPos_t3d.x > 10 || playball.ballPos_t3d.x < -10; 186 | 187 | if(carplayer[caridx].team->id == matchinfo.tleft.team->id) {matchinfo.tleft.posession++; } 188 | else {matchinfo.tright.posession++; } 189 | 190 | if(caridx < 4) { 191 | if(!carplayer[caridx].ballcollided) { 192 | if(carplayer[caridx].team->id == matchinfo.tleft.team->id) {matchinfo.tleft.shots++; if (shotontarget) matchinfo.tleft.shotsontarget++; } 193 | else {matchinfo.tright.shots++; if(shotontarget) matchinfo.tright.shotsontarget++;} 194 | sound_play("ball", false); 195 | effects_add_rumble(carplayer[caridx].playercontroller, 0.1f); 196 | effects_add_shake(0.1f); 197 | } 198 | carplayer[caridx].ballcollided = true; 199 | } 200 | } 201 | 202 | void entityCollisionCallback_carcar(uint16_t body1idx, uint16_t body2idx) { 203 | if(carplayer[body1idx].carcollided < 0) { 204 | if(rand() % 4 == 0) sound_play("crash_horn", false); 205 | else sound_play("crash", false); 206 | effects_add_rumble(carplayer[body1idx].playercontroller, 0.2f); 207 | effects_add_shake(0.15f); 208 | } 209 | carplayer[body1idx].carcollided = body2idx; 210 | 211 | if(carplayer[body2idx].carcollided < 0) { 212 | if(rand() % 4 == 0) sound_play("crash_horn", false); 213 | else sound_play("crash", false); 214 | effects_add_rumble(carplayer[body2idx].playercontroller, 0.2f); 215 | effects_add_shake(0.15f); 216 | } 217 | carplayer[body2idx].carcollided = body1idx; 218 | } 219 | 220 | uint8_t entityCollisionCallback(uint16_t body1idx, uint16_t joint1idx, uint16_t body2idx, uint16_t joint2idx, TPE_Vec3 position){ 221 | if(body1idx != body2idx){ 222 | // ball-car collision 223 | if(playball.bodyidx == body1idx || playball.bodyidx == body2idx) 224 | entityCollisionCallback_ballcar(body1idx, body2idx); 225 | 226 | // car-car collision 227 | if(playball.bodyidx != body1idx && playball.bodyidx != body2idx) 228 | entityCollisionCallback_carcar(body1idx, body2idx); 229 | } 230 | return 1; 231 | } 232 | 233 | void matchinfo_init(){ 234 | memset(&matchinfo, 0, sizeof(matchinfo)); 235 | } 236 | 237 | void game_draw_particles(){ 238 | rdpq_texparms_t p = {0}; 239 | p.s.repeats = REPEAT_INFINITE; 240 | p.t.repeats = REPEAT_INFINITE; 241 | p.s.scale_log = -2; 242 | p.t.scale_log = -2; 243 | rdpq_sprite_upload(TILE0, mapparticles_sprite, &p); 244 | rdpq_set_mode_standard(); 245 | rdpq_set_env_color(color_from_packed32(current_map->particles.color)); 246 | rdpq_mode_filter(FILTER_BILINEAR); 247 | rdpq_mode_combiner(RDPQ_COMBINER1((ENV,0,TEX0,0), (ENV,0,TEX0,0))); 248 | rdpq_mode_antialias(AA_REDUCED); 249 | rdpq_mode_alphacompare(10); 250 | 251 | tpx_state_from_t3d(); 252 | tpx_state_set_scale(current_map->particles.size, current_map->particles.size); 253 | 254 | tpx_state_set_tex_params(0, 0); 255 | tpx_particle_draw_tex(mapparticles, current_map->particles.count); 256 | } 257 | 258 | void game_draw(){ 259 | surface_t fb = display_get_current_framebuffer(); 260 | if(current_map->hdr.enabled) exposure_set(&fb); 261 | else exposure = 1; 262 | float modelScale = 1.0f / 8; 263 | 264 | t3d_vec3_lerp(&maincamera.camTarget_off, &maincamera.camTarget_off, &maincamera.camTarget, 0.1f); 265 | t3d_vec3_lerp(&maincamera.camPos_off, &maincamera.camPos_off, &maincamera.camPos, 0.1f); 266 | t3d_viewport_set_projection(&viewport[(frame) % 5], T3D_DEG_TO_RAD(70.0f), 16.0f, 1000.0f); 267 | T3DVec3 camtarg_shake = (T3DVec3){.x = frandr(-1,1) * effects.screenshaketime, .y = frandr(-1,1) * effects.screenshaketime, .z = frandr(-1,1) * effects.screenshaketime}; 268 | t3d_vec3_add(&camtarg_shake, &maincamera.camTarget_off, &camtarg_shake); 269 | t3d_viewport_look_at(&viewport[(frame) % 5], &maincamera.camPos_off, &camtarg_shake, &(T3DVec3){{0,1,0}}); 270 | 271 | t3d_mat4_from_srt_euler(&modelMat, 272 | (float[3]){modelScale, modelScale, modelScale}, 273 | (float[3]){0,0,0}, 274 | (float[3]){0,0,0} 275 | ); 276 | 277 | if(particleMatFP) 278 | t3d_mat4fp_from_srt_euler(particleMatFP, 279 | (float[3]){current_map->particles.scale.x, current_map->particles.scale.y, current_map->particles.scale.z}, 280 | (float[3]){0,0,0}, 281 | (float[3]){0,current_map->particles.scale.y * 64,0} 282 | ); 283 | 284 | t3d_mat4_to_fixed(modelMatFP, &modelMat); 285 | 286 | t3d_frame_start(); 287 | t3d_viewport_attach(&viewport[(frame) % 5]); 288 | 289 | t3d_light_set_ambient(colorAmbient); 290 | t3d_light_set_count(0); 291 | 292 | // you can use the regular rdpq_* functions with t3d. 293 | // In this example, the colored-band in the 3d-model is using the prim-color, 294 | // even though the model is recorded, you change it here dynamically. 295 | rdpq_sync_pipe(); 296 | t3d_matrix_push(modelMatFP); 297 | if(!dplDraw) { 298 | rspq_block_begin(); 299 | rdpq_mode_zbuf(false,false); 300 | rdpq_mode_antialias(AA_REDUCED); 301 | // Draw the model, material settings (e.g. textures, color-combiner) are handled internally 302 | for(int i = 0; i < current_map->model_matnames_count; i++){ 303 | T3DModelIter iter = t3d_model_iter_create(model, T3D_CHUNK_TYPE_OBJECT); 304 | while(t3d_model_iter_next(&iter)){ 305 | if(!strcmp(iter.object->material->name, current_map->model_matnames[i])){ 306 | t3d_model_draw_material(iter.object->material, &matstate); 307 | t3d_model_draw_object(iter.object, NULL); 308 | } 309 | } 310 | } 311 | dplDraw = rspq_block_end(); 312 | } 313 | 314 | 315 | // for the actual draw, you can use the generic rspq-api. 316 | rspq_block_run(dplDraw); 317 | t3d_matrix_push(modelMatFP); 318 | t3d_light_set_exposure(exposure); 319 | 320 | if(gamestatus.state.game.settings.graphics == NICEST) 321 | for(int i = 0; i < 4; i++) carplayer_draw_shadow(&carplayer[i]); 322 | playball_draw_shadow(); 323 | 324 | //rdpq_mode_antialias(AA_STANDARD); 325 | // quick-sort the entities so that they appear correct without a zbuffer 326 | entitysortgraph_t graph[5] = { 327 | {.idx = 0, .dist = t3d_vec3_distance2(&maincamera.camPos_off, &carplayer[0].Pos_t3d)}, 328 | {.idx = 1, .dist = t3d_vec3_distance2(&maincamera.camPos_off, &carplayer[1].Pos_t3d)}, 329 | {.idx = 2, .dist = t3d_vec3_distance2(&maincamera.camPos_off, &carplayer[2].Pos_t3d)}, 330 | {.idx = 3, .dist = t3d_vec3_distance2(&maincamera.camPos_off, &carplayer[3].Pos_t3d)}, 331 | {.idx = 4, .dist = t3d_vec3_distance2(&maincamera.camPos_off, &playball.ballPos_t3d)}, 332 | }; 333 | qsort(graph, 5, sizeof(entitysortgraph_t), entitysortgraph_compare); 334 | for(int i = 4; i >= 0; i--){ 335 | if(graph[i].idx == 4) playball_draw(); 336 | else carplayer_draw(&carplayer[graph[i].idx]); 337 | } 338 | t3d_matrix_pop(1); 339 | matstate = t3d_model_state_create(); 340 | if(!dplDrawB) { 341 | rspq_block_begin(); 342 | rdpq_mode_zbuf(false,false); 343 | rdpq_mode_antialias(gamestatus.state.game.settings.graphics == NICEST? AA_STANDARD : AA_REDUCED); 344 | // Draw the model, material settings (e.g. textures, color-combiner) are handled internally 345 | for(int i = 0; i < current_map->model_matnames_b_count; i++){ 346 | T3DModelIter iter = t3d_model_iter_create(model, T3D_CHUNK_TYPE_OBJECT); 347 | while(t3d_model_iter_next(&iter)){ 348 | if(!strcmp(iter.object->material->name, current_map->model_matnames_b[i])){ 349 | t3d_model_draw_material(iter.object->material, &matstate); 350 | t3d_model_draw_object(iter.object, NULL); 351 | } 352 | } 353 | } 354 | dplDrawB = rspq_block_end(); 355 | } rspq_block_run(dplDrawB); 356 | 357 | effects_draw(); 358 | if(matchinfo.countdown > 0){ 359 | t3d_matrix_push(modelMatFP); 360 | rdpq_sync_pipe(); 361 | rdpq_set_mode_standard(); 362 | rdpq_mode_combiner(RDPQ_COMBINER1((0,0,0,ENV), (TEX0,0,ENV,0))); 363 | rdpq_mode_alphacompare(50); 364 | rdpq_set_env_color(RGBA32(128,255,255,matchinfo.countdown * 96)); 365 | for(int i = 0; i < 4; i++) carplayer_draw_teleport_particles(&carplayer[i]); 366 | t3d_matrix_pop(1); 367 | } 368 | 369 | if(current_map->particles.count) { 370 | t3d_matrix_push(particleMatFP); 371 | game_draw_particles(); 372 | t3d_matrix_pop(1); 373 | } 374 | 375 | if(!dplDrawHUD){ 376 | rspq_block_begin(); 377 | t3d_matrix_pop(1); 378 | rdpq_sync_pipe(); 379 | rdpq_set_mode_fill(border_time_color); 380 | rdpq_fill_rectangle(222,26,260,42); 381 | rdpq_set_mode_fill(color_from_packed32(matchinfo.tleft.team->teamcolor)); 382 | rdpq_fill_rectangle(260,26,293,42); 383 | rdpq_set_mode_fill(border_time_score); 384 | rdpq_fill_rectangle(293,26,346,42); 385 | rdpq_set_mode_fill(color_from_packed32(matchinfo.tright.team->teamcolor)); 386 | rdpq_fill_rectangle(346,26,380,42); 387 | 388 | rdpq_textparms_t textparms = {0}; 389 | textparms.align = ALIGN_CENTER; 390 | textparms.width = 260 - 222; 391 | rdpq_text_printf(&textparms, 1, 222, 38, "%02i:%02i", (int)matchinfo.matchtimeleft / 60, (int)matchinfo.matchtimeleft % 60); 392 | textparms.width = 293 - 260; 393 | textparms.style_id = matchinfo.tleft.team->style; 394 | rdpq_text_printf(&textparms, 1, 262, 38, matchinfo.tleft.team->teamshortname); 395 | textparms.style_id = matchinfo.tright.team->style; 396 | rdpq_text_printf(&textparms, 1, 346, 38, matchinfo.tright.team->teamshortname); 397 | textparms.width = 346 - 293; 398 | textparms.style_id = 1; 399 | rdpq_text_printf(&textparms, 1, 293, 38, "%i - %i", matchinfo.tleft.score, matchinfo.tright.score); 400 | dplDrawHUD = rspq_block_end(); 401 | } rspq_block_run(dplDrawHUD); 402 | 403 | //rdpq_text_printf(NULL, 1, 30, 30, "^01FPS: %.2f", display_get_fps()); 404 | //heap_stats_t stats; sys_get_heap_stats(&stats); 405 | //rdpq_text_printf(NULL, 1, 30, 50, "MEM: %i total, %i used", stats.total, stats.used); 406 | } 407 | 408 | bool game_pause(){ 409 | sprite_t* selector_spr = sprite_load("rom:/textures/ui/selector.rgba32.sprite"); 410 | sprite_t* button_a = sprite_load("rom:/textures/ui/button_a.rgba32.sprite"); 411 | bool quit = false; 412 | int selection = 0; 413 | frame--; // to fix the matrix buffering 414 | while(matchinfo.state == MATCH_PAUSE){ 415 | joypad_poll(); 416 | audioutils_mixer_update(); 417 | effects_update(); 418 | joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 419 | int axis_pressed = joypad_get_axis_pressed(JOYPAD_PORT_1, JOYPAD_AXIS_STICK_Y); 420 | if(pressed.start) { 421 | matchinfo.state = MATCH_PLAY; 422 | break; 423 | } if(pressed.a) { 424 | matchinfo.state = MATCH_PLAY; 425 | if(selection == 1) {quit = true; matchinfo.exited = true;} 426 | break; 427 | } if(axis_pressed){ 428 | sound_play("menu_navigate", false); 429 | selection = 1 - selection; 430 | } 431 | rdpq_attach(display_get(), NULL); 432 | game_draw(); 433 | rdpq_set_mode_standard(); 434 | rdpq_mode_combiner(RDPQ_COMBINER_FLAT); 435 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 436 | rdpq_mode_antialias(AA_REDUCED); 437 | rdpq_set_prim_color(RGBA32(0,0,0,128)); 438 | rdpq_fill_rectangle(190, 200, 450, 280); 439 | rdpq_mode_combiner(RDPQ_COMBINER_TEX); 440 | rdpq_sprite_blit(button_a, 450, 425, NULL); 441 | rdpq_sprite_blit(selector_spr, 190, 205 + 40*selection, NULL); 442 | rdpq_textparms_t parms; parms.align = ALIGN_CENTER; parms.valign = VALIGN_CENTER; parms.width = display_get_width(); parms.height = 40; parms.style_id = 1; 443 | rdpq_text_printf(&parms, 4, 0, 200, dictstr("match_pause_continue")); 444 | rdpq_text_printf(&parms, 4, 0, 240, dictstr("match_pause_exitmatch")); 445 | rdpq_textparms_t parms2; parms2.style_id = 2; 446 | rdpq_text_printf(&parms2, 3, 490, 445, dictstr("match_s_select")); 447 | rdpq_detach_show(); 448 | } 449 | sprite_free(selector_spr); 450 | sprite_free(button_a); 451 | if(quit) rspq_wait(); 452 | return !quit; 453 | } 454 | 455 | bool game_update(){ 456 | frame++; 457 | rdpq_sync_pipe(); 458 | audioutils_mixer_update(); 459 | 460 | if(matchinfo.state == MATCH_PLAY){ 461 | int inttime = matchinfo.matchtimeleft; 462 | globaltime += display_get_delta_time(); 463 | matchinfo.matchtimeleft -= display_get_delta_time(); 464 | if((int) matchinfo.matchtimeleft != inttime) {rspq_wait(); rspq_block_free(dplDrawHUD); dplDrawHUD = NULL;} 465 | } 466 | joypad_poll(); 467 | 468 | joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 469 | if(pressed.start && (matchinfo.state == MATCH_PAUSE || matchinfo.state == MATCH_PLAY)) { 470 | matchinfo.state = MATCH_PAUSE; 471 | bool cont = game_pause(); 472 | if (!cont) return false; 473 | } 474 | 475 | playball_update(); 476 | effects_update(); 477 | carplayer_update(); 478 | if(current_map->particles.updatefunc) current_map->particles.updatefunc(mapparticles, current_map->particles.count); 479 | TPE_worldStep(&world); 480 | // ======== Update ======== // 481 | T3DVec3 camp = {0}; 482 | if(matchinfo.state == MATCH_SCORE){ 483 | for(int i = 0; i < 4; i++){t3d_vec3_add(&camp, &camp, &carplayer[i].Pos_t3d);} t3d_vec3_scale(&camp, &camp, 0.25f); 484 | float disttoball = t3d_vec3_distance(&camp, &playball.ballPos_t3d); 485 | T3DVec3 camp_offset = {.x = 0,.y = 10 + playball.ballPos_t3d.y,.z = 15 + disttoball}; 486 | t3d_vec3_add(&camp, &camp, &camp_offset); 487 | } else{ 488 | float maxdist = 0; 489 | for(int i = 0; i < 4; i++){float d = t3d_vec3_distance2(&playball.ballPos_t3d, &carplayer[i].Pos_t3d); if (d > maxdist) maxdist = d;} 490 | maxdist = sqrt(maxdist); 491 | T3DVec3 camp_offset = {.x = playball.ballPos_t3d.x / 2,.y = 10 + playball.ballPos_t3d.y, .z = 10 + playball.ballPos_t3d.z / 2 + maxdist / 2}; 492 | t3d_vec3_add(&camp, &camp, &camp_offset); 493 | } 494 | maincamera.camPos = camp; 495 | t3d_vec3_scale(&maincamera.camPos, &maincamera.camPos, 8); 496 | maincamera.camTarget = playball.ballPos_t3d; 497 | t3d_vec3_scale(&maincamera.camTarget, &maincamera.camTarget, 8); 498 | return true; 499 | } 500 | 501 | void game_intro(){ 502 | bool pressed_start = false; 503 | float camlocationtimer = 0; 504 | int camidx = 0; 505 | while(!pressed_start){ 506 | camlocationtimer += display_get_delta_time() / 7; 507 | if(camlocationtimer >= 1) {camidx++; camlocationtimer = 0;} 508 | 509 | game_update(); 510 | 511 | joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 512 | if(pressed.start) pressed_start = true; 513 | 514 | camidx = camidx % 5; 515 | T3DVec3 campos; t3d_vec3_lerp(&campos, (T3DVec3*)&camlocations[camidx][0], (T3DVec3*)&camlocations[camidx][1], camlocationtimer); 516 | T3DVec3 camrot; t3d_vec3_lerp(&camrot, (T3DVec3*)&camlocations[camidx][2], (T3DVec3*)&camlocations[camidx][3], camlocationtimer); 517 | 518 | t3d_vec3_scale(&campos, &campos, 8); 519 | t3d_vec3_scale(&camrot, &camrot, 8); 520 | 521 | maincamera.camPos = campos; 522 | maincamera.camTarget = camrot; 523 | 524 | maincamera.camPos_off = campos; 525 | maincamera.camTarget_off = camrot; 526 | 527 | 528 | rdpq_attach(display_get(), NULL); 529 | game_draw(); 530 | rdpq_textparms_t parms; parms.align = ALIGN_RIGHT; parms.width = display_get_width(); parms.style_id = 1; 531 | rdpq_text_printf(&parms, 2, -40, display_get_height() - 45, current_map->name); 532 | rdpq_detach_show(); 533 | 534 | } 535 | sound_play("LetsGo", false); 536 | } 537 | 538 | void game_countdown(){ 539 | playball_reset(); 540 | for(int i = 0; i < 4; i++) carplayer_reset(&carplayer[i], i); 541 | sprite_t* numbers = sprite_load("rom:/textures/ui/numbers.rgba32.sprite"); 542 | int numbersize = numbers->width / 4; 543 | 544 | matchinfo.countdown = 3; 545 | sound_play("countdown", false); 546 | while(matchinfo.countdown > 0){ 547 | matchinfo.countdown -= display_get_delta_time(); 548 | 549 | game_update(); 550 | 551 | rdpq_attach(display_get(), NULL); 552 | game_draw(); 553 | int timerint = matchinfo.countdown; 554 | rdpq_blitparms_t parms; 555 | parms.s0 = timerint*numbersize; 556 | parms.t0 = 0; 557 | parms.width = numbersize; 558 | parms.height = numbersize; 559 | rdpq_set_mode_standard(); 560 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 561 | rdpq_sprite_blit(numbers, (display_get_width() - numbersize) / 2, (display_get_height() - numbersize) / 2, &parms); 562 | rdpq_detach_show(); 563 | } 564 | sprite_free(numbers); 565 | } 566 | 567 | 568 | bool game_play(){ 569 | matchinfo.state = MATCH_PLAY; 570 | bool finished = false; 571 | while(!finished){ 572 | if(!game_update()) return false; 573 | 574 | if(playball.ballPos_t3d.y < 3){ 575 | if(playball.ballPos_t3d.x > 18.5f){ 576 | finished = true; 577 | matchinfo.tleft.score++; 578 | } else if(playball.ballPos_t3d.x < -18.5f){ 579 | finished = true; 580 | matchinfo.tright.score++; 581 | } 582 | } if(matchinfo.matchtimeleft <= 0.5f){ 583 | finished = true; 584 | } 585 | 586 | rdpq_attach(display_get(), NULL); 587 | game_draw(); 588 | rdpq_detach_show(); 589 | 590 | } 591 | {rspq_wait(); rspq_block_free(dplDrawHUD); dplDrawHUD = NULL;} 592 | sprite_t* message = NULL; 593 | if(matchinfo.matchtimeleft <= 0.5f) { 594 | matchinfo.state = MATCH_END; 595 | sound_play("airhorn", false); 596 | } 597 | else { 598 | matchinfo.state = MATCH_SCORE; 599 | effects_add_exp3d(playball.ballPos_t3d, RGBA32(255,255,255,255)); 600 | message = sprite_load("rom:/textures/ui/goal.rgba32.sprite"); 601 | playball.init = false; 602 | effects_add_shake(2.5f); 603 | for(int i = 0; i < 4; i++) effects_add_rumble(i, 1.0f); 604 | TPE_Vec3 force = {.x = playball.ballPos_t3d.x > 0? -0.5 * TPE_F : 0.5 * TPE_F, 0.5 * TPE_F, 0}; 605 | for(int i = 0; i < 4; i++) TPE_bodyAccelerate(&bodies[carplayer[i].bodyidx], force); 606 | } 607 | float countdowntimer = 3; 608 | while(countdowntimer > 0){ 609 | countdowntimer -= display_get_delta_time(); 610 | game_update(); 611 | rdpq_attach(display_get(), NULL); 612 | game_draw(); 613 | if(message) { 614 | float scale = ((4 - countdowntimer) / 2); 615 | rdpq_blitparms_t parms; parms.cx = message->width / 2; parms.cy = message->height / 2; 616 | parms.scale_x = scale; parms.scale_y = scale; parms.filtering = true; 617 | rdpq_set_mode_standard(); 618 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 619 | rdpq_mode_filter(FILTER_BILINEAR); 620 | rdpq_sprite_blit(message, display_get_width() / 2, display_get_height() / 2 , &parms); 621 | } 622 | rdpq_detach_show(); 623 | } 624 | if(message) {sprite_free(message);} message = NULL; 625 | return true; 626 | } 627 | 628 | void game_score(){ 629 | float countdowntimer = 15; 630 | bool pressed_a = false; 631 | sprite_t* button_a = sprite_load("rom:/textures/ui/button_a.rgba32.sprite"); 632 | while(countdowntimer > 0 && !pressed_a){ 633 | countdowntimer -= display_get_delta_time(); 634 | game_update(); 635 | joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1); 636 | if(pressed.a) pressed_a = true; 637 | rdpq_attach(display_get(), NULL); 638 | game_draw(); 639 | rdpq_set_mode_standard(); 640 | rdpq_mode_combiner(RDPQ_COMBINER_FLAT); 641 | rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); 642 | rdpq_mode_antialias(AA_REDUCED); 643 | 644 | for(int i = 0; i < 5; i++){ 645 | int y = (i * 40) + 130; 646 | rdpq_set_prim_color(i % 2? RGBA32(0,20,20,128) : RGBA32(0,0,0,128)); 647 | rdpq_fill_rectangle(45, y, display_get_width() - 45, y + 40); 648 | } 649 | 650 | rdpq_mode_combiner(RDPQ_COMBINER_TEX); 651 | rdpq_sprite_blit(button_a, 450, 425, NULL); 652 | rdpq_textparms_t parms; parms.align = ALIGN_CENTER; parms.valign = VALIGN_CENTER; parms.width = display_get_width(); parms.height = 40; parms.style_id = 1; 653 | float left_possession = (matchinfo.tleft.posession / (matchinfo.tleft.posession + matchinfo.tright.posession)) * 100; 654 | float right_possession = (matchinfo.tright.posession / (matchinfo.tleft.posession + matchinfo.tright.posession)) * 100; 655 | rdpq_text_printf(&parms, 4, 0, 130, dictstr("match_s_stats")); 656 | rdpq_text_printf(&parms, 4, 0, 170, "%i %s %i", matchinfo.tleft.score, dictstr("match_s_goals"), matchinfo.tright.score); 657 | rdpq_text_printf(&parms, 4, 0, 210, "%i %s %i", matchinfo.tleft.shots, dictstr("match_s_shots"), matchinfo.tright.shots); 658 | rdpq_text_printf(&parms, 4, 0, 250, "%i %s %i", matchinfo.tleft.shotsontarget, dictstr("match_s_shotsont"), matchinfo.tright.shotsontarget); 659 | rdpq_text_printf(&parms, 4, 0, 290, "%.0f%% %s %.0f%%", left_possession, dictstr("match_s_posession"), right_possession); 660 | rdpq_textparms_t parms2; parms2.style_id = 2; 661 | rdpq_text_printf(&parms2, 3, 490, 445, dictstr("match_s_continue")); 662 | rdpq_detach_show(); 663 | } 664 | rspq_wait(); 665 | sprite_free(button_a); 666 | } 667 | 668 | void game_free(){ 669 | rspq_wait(); 670 | playball_free(); 671 | for(int i = 0; i < MAXPLAYERS; i++) {carplayer_free(&carplayer[i]);} 672 | 673 | memset(bodies, 0, sizeof(bodies)); 674 | bodiescount = 0; 675 | 676 | current_map = NULL; 677 | memset(viewport, 0, sizeof(viewport)); 678 | 679 | if(model) {t3d_model_free(model); model = NULL;} 680 | memset(viewport, 0, sizeof(modelMat)); 681 | if(modelMatFP) {free_uncached(modelMatFP); modelMatFP = NULL;} 682 | 683 | memset(offset, 0, sizeof(offset)); 684 | memset(&matstate, 0, sizeof(matstate)); 685 | memset(&world, 0, sizeof(TPE_World)); 686 | 687 | if(dplDraw) {rspq_block_free(dplDraw); dplDraw = NULL;} 688 | if(dplDrawB) {rspq_block_free(dplDrawB); dplDrawB = NULL;} 689 | if(dplDrawHUD) {rspq_block_free(dplDrawHUD); dplDrawHUD = NULL;} 690 | 691 | if(mapparticles) {free_uncached(mapparticles); mapparticles = NULL;} 692 | if(mapparticles_sprite) {sprite_free(mapparticles_sprite); mapparticles_sprite = NULL;} 693 | if(particleMatFP) {free_uncached(particleMatFP); particleMatFP = NULL;} 694 | 695 | memset(&maincamera, 0, sizeof(maincamera)); 696 | } 697 | 698 | void game_start(mapinfo_t* map) 699 | { 700 | switch(gamestatus.state.game.settings.graphics){ 701 | case FASTEST: vi_set_aa_mode(VI_AA_MODE_NONE); vi_set_dedither(false); break; 702 | case DEFAULT: vi_set_aa_mode(VI_AA_MODE_RESAMPLE_FETCH_ALWAYS); vi_set_dedither(false); break; 703 | case NICEST: vi_set_aa_mode(VI_AA_MODE_RESAMPLE_FETCH_ALWAYS); vi_set_dedither(true);break; 704 | } 705 | current_map = map; 706 | border_time_color = RGBA32(255,217,0,255); 707 | border_time_score = RGBA32(0,84,255,255); 708 | viewport[0] = t3d_viewport_create(); viewport[1] = t3d_viewport_create(); viewport[2] = t3d_viewport_create(); viewport[3] = t3d_viewport_create(); viewport[4] = t3d_viewport_create(); 709 | exposure = 5; 710 | exposure_bias = current_map->hdr.tonemappingaverage; 711 | 712 | t3d_mat4_identity(&modelMat); 713 | 714 | // Now allocate a fixed-point matrix, this is what t3d uses internally. 715 | // Note: this gets DMA'd to the RSP, so it needs to be uncached. 716 | // If you can't allocate uncached memory, remember to flush the cache after writing to it instead. 717 | modelMatFP = malloc_uncached(sizeof(T3DMat4FP)); 718 | if(current_map->particles.count){ 719 | particleMatFP = malloc_uncached(sizeof(T3DMat4FP)); 720 | uint32_t allocSize = sizeof(TPXParticle) * current_map->particles.count / 2; 721 | mapparticles = malloc_uncached(allocSize); 722 | if(current_map->particles.initfunc) current_map->particles.initfunc(mapparticles, current_map->particles.count); 723 | mapparticles_sprite = sprite_load(current_map->particles.texturefn); 724 | } 725 | 726 | // Load a model-file, this contains the geometry and some metadata 727 | model = t3d_model_load(map->modelfn); 728 | bgm_play(map->musicfn, true, 1); 729 | 730 | float offset[2] = {0}; 731 | matstate = t3d_model_state_create(); 732 | 733 | matchinfo.matchtimeleft = (gamestatus.state.game.settings.duration) * 60; 734 | matchinfo.state = MATCH_INTRO; 735 | for(int i = 0; i < 4; i++) carplayer_init(&carplayer[i], i); 736 | playball_init(); 737 | TPE_worldInit(&world,bodies,bodiescount,environmentDistance); 738 | world.collisionCallback = entityCollisionCallback; 739 | game_intro(); 740 | 741 | #if !DEBUG_RDP 742 | while (matchinfo.matchtimeleft > 0.5f) 743 | #endif 744 | { 745 | game_countdown(); 746 | if(!game_play()) goto match_end; 747 | // ======== Draw ======== // 748 | } 749 | game_score(); 750 | match_end: 751 | vi_set_dedither(true); vi_set_aa_mode(VI_AA_MODE_NONE); 752 | game_free(); 753 | } 754 | -------------------------------------------------------------------------------- /src/playtime_logic.h: -------------------------------------------------------------------------------- 1 | #ifndef PLAYTIME_LOGIC_H 2 | #define PLAYTIME_LOGIC_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "engine_gfx.h" 9 | #include "teams.h" 10 | #include "entity_logic.h" 11 | #include "audioutils.h" 12 | #include "maps.h" 13 | 14 | #define DEBUG_RDP 0 15 | 16 | typedef enum{ 17 | MATCH_INTRO, 18 | MATCH_COUNTDOWN, 19 | MATCH_PLAY, 20 | MATCH_PAUSE, 21 | MATCH_SCORE, 22 | MATCH_END 23 | } matchstate_t; 24 | 25 | typedef struct{ 26 | teamstats_t tleft, tright; 27 | float matchtimeleft; 28 | matchstate_t state; 29 | float countdown; 30 | int playercontrollers[4]; 31 | bool exited; 32 | } match_t; 33 | 34 | extern match_t matchinfo; 35 | 36 | void matchinfo_init(); 37 | 38 | void game_start(mapinfo_t* map); 39 | 40 | #endif -------------------------------------------------------------------------------- /src/teams.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "engine_gfx.h" 6 | #include "entity_logic.h" 7 | #include "teams.h" 8 | #include "playtime_logic.h" 9 | 10 | teamdef_t teams[8] = { 11 | { .id = 0, .style = 0, .teamcolor = (0xFFFFFFFF), .difficulty = 0.13f, .maxturnspeed = 0.8f, .teamname = "FROZEN THUNDER", .teamshortname = "FZT", .logofilename = "rom://textures/ui/teams/fronzen.rgba32.sprite", .modelfilename = "rom://models/celica/celica_white.t3dm", .voicenamefn = "Team_FrozenThunder",}, 12 | { .id = 1, .style = 1, .teamcolor = (0xFF0000FF), .difficulty = 0.07f, .maxturnspeed = 0.55f,.teamname = "FLAMING WHEELS", .teamshortname = "FLW", .logofilename = "rom://textures/ui/teams/flaming.rgba32.sprite", .modelfilename = "rom://models/celica/celica_red.t3dm", .voicenamefn = "Team_FlamingWheels",}, 13 | { .id = 2, .style = 0, .teamcolor = (0x808080FF), .difficulty = 0.09f, .maxturnspeed = 0.6f, .teamname = "STEEL REBELS", .teamshortname = "STR", .logofilename = "rom://textures/ui/teams/steel.rgba32.sprite", .modelfilename = "rom://models/celica/celica_gray.t3dm", .voicenamefn = "Team_SteelRebels",}, 14 | { .id = 3, .style = 0, .teamcolor = (0xFF80FFFF), .difficulty = 0.07f, .maxturnspeed = 1.0f, .teamname = "LUCKY VIXENS", .teamshortname = "LVX", .logofilename = "rom://textures/ui/teams/lucky.rgba32.sprite", .modelfilename = "rom://models/celica/celica_pink.t3dm", .voicenamefn = "Team_LuckyVixens",}, 15 | { .id = 4, .style = 0, .teamcolor = (0x4287f5FF), .difficulty = 0.05f, .maxturnspeed = 0.4f, .teamname = "ELECTRIC SQUAD", .teamshortname = "ESQ", .logofilename = "rom://textures/ui/teams/electric.rgba32.sprite", .modelfilename = "rom://models/celica/celica_yellow.t3dm", .voicenamefn = "Team_ElectricSquad",}, 16 | { .id = 5, .style = 1, .teamcolor = (0x000000FF), .difficulty = 0.1f, .maxturnspeed = 0.6f, .teamname = "CRUEL FUEL", .teamshortname = "CRF", .logofilename = "rom://textures/ui/teams/cruel.rgba32.sprite", .modelfilename = "rom://models/celica/celica_black.t3dm", .voicenamefn = "Team_CruelFuel",}, 17 | { .id = 6, .style = 0, .teamcolor = (0x61ff64FF), .difficulty = 0.2f, .maxturnspeed = 0.5f, .teamname = "VENOMOUS RHINOS", .teamshortname = "VNR", .logofilename = "rom://textures/ui/teams/venomous.rgba32.sprite", .modelfilename = "rom://models/celica/celica_green.t3dm", .voicenamefn = "Team_VenomousRhinos",}, 18 | { .id = 7, .style = 0, .teamcolor = (0xff5820FF), .difficulty = 0.1f, .maxturnspeed = 1.0f, .teamname = "SKILLED ALL-STARS",.teamshortname = "SKA", .logofilename = "rom://textures/ui/teams/allstars.rgba32.sprite", .modelfilename = "rom://models/celica/celica_orange.t3dm", .voicenamefn = "Team_SkilledAllstars",}, 19 | }; 20 | 21 | void teaminfo_init_controllers(int playercontrollers[4]){ 22 | for(int i = 0; i < 4; i++) 23 | matchinfo.playercontrollers[i] = playercontrollers[i]; 24 | } 25 | 26 | void teaminfo_init(int tindex, bool right){ 27 | teamstats_t stats = {0}; 28 | stats.team = &teams[tindex]; 29 | if(right) matchinfo.tright = stats; 30 | else matchinfo.tleft = stats; 31 | } 32 | -------------------------------------------------------------------------------- /src/teams.h: -------------------------------------------------------------------------------- 1 | #ifndef TEAMS_H 2 | #define TEAMS_H 3 | 4 | typedef struct{ 5 | int id; 6 | int style; 7 | uint32_t teamcolor; 8 | const char* teamname; 9 | const char* teamshortname; 10 | const char* logofilename; 11 | const char* modelfilename; 12 | const char* voicenamefn; 13 | float difficulty; 14 | float maxturnspeed; 15 | } teamdef_t; 16 | 17 | extern teamdef_t teams[8]; 18 | 19 | typedef struct{ 20 | int score; 21 | int shots; 22 | int shotsontarget; 23 | float posession; 24 | teamdef_t* team; 25 | } teamstats_t; 26 | 27 | 28 | #define TEAM_RIGHT 1 29 | #define TEAM_LEFT 0 30 | 31 | void teaminfo_init_controllers(int playercontrollers[4]); 32 | 33 | void teaminfo_init(int tindex, bool right); 34 | 35 | #endif -------------------------------------------------------------------------------- /src/tinyphysicsengine.h: -------------------------------------------------------------------------------- 1 | #ifndef _TINYPHYSICSENGINE_H 2 | #define _TINYPHYSICSENGINE_H 3 | 4 | #ifndef TPSH_H 5 | #define TPSH_H 6 | 7 | /** 8 | tinyphysicsengine (TPE) 9 | 10 | Simple/suckless header-only hybrid 3D physics engine with no floating point, 11 | only 32 bit int arithmetic, similar to e.g. small3dlib. 12 | 13 | Conventions and formats are the same or similar to those of small3dlib so as 14 | to make them easily integrate with each other. 15 | 16 | The library works with bodies made of spheres connected by elastic springs, 17 | i.e. soft bodies which however behave as "stiff" bodies by default and can 18 | be used to fake rigid body physics as well. Bodies are placed in environemnts 19 | specified by a distance function that allows to implement any mathematical 20 | shape. 21 | 22 | Orientations/rotations are in extrinsic Euler angles in the ZXY order (by Z, 23 | then by X, then by Y), if not mentioned otherwise. Angles are in TPE_Units, 24 | TPE_FRACTIONS_PER_UNIT is full angle (2 PI). Sometimes rotations can also be 25 | specified in the "about axis" format: here the object is rotated CW by given 26 | axis by an angle that's specified by the magnitude of the vector. 27 | 28 | Where it matters (e.g. rotations about axes) we consider a left-handed coord. 29 | system (x right, y up, z forward). 30 | 31 | ------------------------------------------------------------------------------ 32 | 33 | by drummyfish, 2022 34 | 35 | version 0.8d 36 | 37 | This work's goal is to never be encumbered by any exclusive intellectual 38 | property rights. The work is therefore provided under CC0 1.0 39 | (https://creativecommons.org/publicdomain/zero/1.0/) + additional WAIVER OF 40 | ALL INTELLECTUAL PROPERTY RIGHTS that waives the rest of intellectual property 41 | rights not already waived by CC0 1.0. The WAIVER OF ALL INTELLECTUAL PROPERTY 42 | RGHTS is as follows: 43 | 44 | Each contributor to this work agrees that they waive any exclusive rights, 45 | including but not limited to copyright, patents, trademark, trade dress, 46 | industrial design, plant varieties and trade secrets, to any and all ideas, 47 | concepts, processes, discoveries, improvements and inventions conceived, 48 | discovered, made, designed, researched or developed by the contributor either 49 | solely or jointly with others, which relate to this work or result from this 50 | work. Should any waiver of such right be judged legally invalid or 51 | ineffective under applicable law, the contributor hereby grants to each 52 | affected person a royalty-free, non transferable, non sublicensable, non 53 | exclusive, irrevocable and unconditional license to this right. 54 | */ 55 | 56 | #include 57 | 58 | typedef int32_t TPE_Unit; ///< Basic fixed point unit type. 59 | typedef int16_t TPE_UnitReduced; ///< Like TPE_Unit but saving space 60 | 61 | #define TPE_FRACTIONS_PER_UNIT 512 ///< one fixed point unit, don't change 62 | #define TPE_F TPE_FRACTIONS_PER_UNIT ///< short for TPE_FRACTIONS_PER_UNIT 63 | #define TPE_JOINT_SIZE_MULTIPLIER 32 ///< joint size is scaled (size saving) 64 | 65 | #define TPE_INFINITY 2147483647 66 | 67 | #define TPE_JOINT_SIZE(joint) ((joint).sizeDivided * TPE_JOINT_SIZE_MULTIPLIER) 68 | 69 | #ifndef TPE_APPROXIMATE_LENGTH 70 | #define TPE_APPROXIMATE_LENGTH 0 /**< whether or not use length/distance 71 | approximation rather than exact 72 | calculation (1 is faster but less 73 | accurate), beware of possible lower 74 | stability */ 75 | #endif 76 | 77 | #if !TPE_APPROXIMATE_LENGTH 78 | #define TPE_DISTANCE TPE_dist 79 | #define TPE_LENGTH TPE_vec3Len 80 | #else 81 | #define TPE_DISTANCE TPE_distApprox 82 | #define TPE_LENGTH TPE_vec3LenApprox 83 | #endif 84 | 85 | #ifndef TPE_LOG 86 | #define TPE_LOG(s) ; // redefine to some print function to show debug logs 87 | #endif 88 | 89 | #ifndef TPE_LOW_SPEED 90 | /** Speed, in TPE_Units per ticks, that is considered low (used e.g. for auto 91 | deactivation of bodies). */ 92 | #define TPE_LOW_SPEED 30 93 | #endif 94 | 95 | #ifndef TPE_RESHAPE_TENSION_LIMIT 96 | /** Tension limit, in TPE_Units, after which a non-soft body will be reshaped. 97 | Smaller number will keep more stable shapes but will cost more performance. */ 98 | #define TPE_RESHAPE_TENSION_LIMIT 20 99 | #endif 100 | 101 | #ifndef TPE_RESHAPE_ITERATIONS 102 | /** How many iterations of reshaping will be performed by the step function if 103 | the body's shape needs to be reshaped. Greater number will keep shapes more 104 | stable but will cost some performance. */ 105 | #define TPE_RESHAPE_ITERATIONS 3 106 | #endif 107 | 108 | #ifndef TPE_DEACTIVATE_AFTER 109 | /** After how many ticks of low speed should a body be disabled. This mustn't 110 | be greater than 255. */ 111 | #define TPE_DEACTIVATE_AFTER 128 112 | #endif 113 | 114 | #ifndef TPE_LIGHT_DEACTIVATION 115 | /** When a body is activated by a collision, its deactivation counter will be 116 | set to this value, i.e. after a collision the body will be prone to deactivate 117 | sooner than normally. This is to handle situations with many bodies touching 118 | each other that would normally keep activating each other, never coming to 119 | rest. */ 120 | #define TPE_LIGHT_DEACTIVATION \ 121 | (TPE_DEACTIVATE_AFTER - TPE_DEACTIVATE_AFTER / 10) 122 | #endif 123 | 124 | #ifndef TPE_TENSION_ACCELERATION_DIVIDER 125 | /** Number by which the base acceleration (TPE_FRACTIONS_PER_UNIT per tick 126 | squared) caused by the connection tension will be divided. This should be 127 | power of 2. */ 128 | #define TPE_TENSION_ACCELERATION_DIVIDER 32 129 | #endif 130 | 131 | #ifndef TPE_TENSION_ACCELERATION_THRESHOLD 132 | /** Limit within which acceleration caused by connection tension won't be 133 | applied. */ 134 | #define TPE_TENSION_ACCELERATION_THRESHOLD 5 135 | #endif 136 | 137 | #ifndef TPE_TENSION_GREATER_ACCELERATION_THRESHOLD 138 | /** Connection tension threshold after which twice as much acceleration will 139 | be applied. This helps prevent diverting joints that are "impaled" by 140 | environment.*/ 141 | #define TPE_TENSION_GREATER_ACCELERATION_THRESHOLD \ 142 | (TPE_TENSION_ACCELERATION_THRESHOLD * 3) 143 | #endif 144 | 145 | #ifndef TPE_COLLISION_RESOLUTION_ITERATIONS 146 | /** Maximum number of iterations to try to uncollide two colliding bodies. */ 147 | #define TPE_COLLISION_RESOLUTION_ITERATIONS 16 148 | #endif 149 | 150 | #ifndef TPE_COLLISION_RESOLUTION_MARGIN 151 | /** Margin, in TPE_Units, by which a body will be shifted back to get out of 152 | collision. */ 153 | #define TPE_COLLISION_RESOLUTION_MARGIN (TPE_F / 64) 154 | #endif 155 | 156 | #ifndef TPE_NONROTATING_COLLISION_RESOLVE_ATTEMPTS 157 | /** Number of times a collision of nonrotating bodies with environment will be 158 | attempted to resolve. This probably won't have great performance implications 159 | as complex collisions of this kind should be relatively rare. */ 160 | #define TPE_NONROTATING_COLLISION_RESOLVE_ATTEMPTS 8 161 | #endif 162 | 163 | #ifndef TPE_APPROXIMATE_NET_SPEED 164 | /** Whether to use a fast approximation for calculating net speed of bodies 165 | which increases performance a bit. */ 166 | #define TPE_APPROXIMATE_NET_SPEED 1 167 | #endif 168 | 169 | #define TPE_PRINTF_VEC3(v) printf("[%d %d %d]",(v).x,(v).y,(v).z); 170 | 171 | typedef struct 172 | { 173 | TPE_Unit x; 174 | TPE_Unit y; 175 | TPE_Unit z; 176 | } TPE_Vec3; 177 | 178 | /** Keeps given point within specified axis-aligned box. This can be used e.g. 179 | to smooth rendered movement of jittering physics bodies. */ 180 | TPE_Vec3 TPE_vec3KeepWithinBox(TPE_Vec3 point, TPE_Vec3 boxCenter, 181 | TPE_Vec3 boxMaxVect); 182 | 183 | TPE_Vec3 TPE_vec3KeepWithinDistanceBand(TPE_Vec3 point, TPE_Vec3 center, 184 | TPE_Unit minDistance, TPE_Unit maxDistance); 185 | 186 | TPE_Vec3 TPE_vec3(TPE_Unit x, TPE_Unit y, TPE_Unit z); 187 | TPE_Vec3 TPE_vec3Minus(TPE_Vec3 v1, TPE_Vec3 v2); 188 | TPE_Vec3 TPE_vec3Subtract(TPE_Vec3 v1, TPE_Vec3 v2); 189 | TPE_Vec3 TPE_vec3Plus(TPE_Vec3 v1, TPE_Vec3 v2); 190 | TPE_Vec3 TPE_vec3Cross(TPE_Vec3 v1, TPE_Vec3 v2); 191 | TPE_Vec3 TPE_vec3Project(TPE_Vec3 v, TPE_Vec3 base); 192 | TPE_Vec3 TPE_vec3ProjectNormalized(TPE_Vec3 v, TPE_Vec3 baseNormalized); 193 | TPE_Vec3 TPE_vec3Times(TPE_Vec3 v, TPE_Unit units); 194 | TPE_Vec3 TPE_vec3TimesPlain(TPE_Vec3 v, TPE_Unit q); 195 | TPE_Vec3 TPE_vec3Normalized(TPE_Vec3 v); 196 | 197 | TPE_Unit TPE_vec3Dot(TPE_Vec3 v1, TPE_Vec3 v2); 198 | TPE_Unit TPE_vec3Len(TPE_Vec3 v); 199 | TPE_Unit TPE_vec3LenApprox(TPE_Vec3 v); 200 | 201 | /** Returns an angle in TPE_Units (see angle conventions) of a 2D vector with 202 | the X axis, CCW. */ 203 | TPE_Unit TPE_vec2Angle(TPE_Unit x, TPE_Unit y); 204 | 205 | /** Keeps given value within specified range. This can be used e.g. for movement 206 | smoothing. */ 207 | TPE_Unit TPE_keepInRange(TPE_Unit x, TPE_Unit xMin, TPE_Unit xMax); 208 | 209 | TPE_Unit TPE_abs(TPE_Unit x); 210 | TPE_Unit TPE_max(TPE_Unit a, TPE_Unit b); 211 | TPE_Unit TPE_min(TPE_Unit a, TPE_Unit b); 212 | TPE_Unit TPE_nonZero(TPE_Unit x); 213 | TPE_Unit TPE_dist(TPE_Vec3 p1, TPE_Vec3 p2); 214 | TPE_Unit TPE_distApprox(TPE_Vec3 p1, TPE_Vec3 p2); 215 | TPE_Unit TPE_sqrt(TPE_Unit x); 216 | 217 | /** Compute sine, TPE_FRACTIONS_PER_UNIT as argument corresponds to 2 * PI 218 | radians. Returns a number from -TPE_FRACTIONS_PER_UNIT to 219 | TPE_FRACTIONS_PER_UNIT. */ 220 | TPE_Unit TPE_sin(TPE_Unit x); 221 | TPE_Unit TPE_cos(TPE_Unit x); 222 | TPE_Unit TPE_atan(TPE_Unit x); 223 | 224 | typedef struct 225 | { 226 | TPE_Vec3 position; 227 | TPE_UnitReduced velocity[3]; ///< not TPE_Vec3 to save size 228 | uint8_t sizeDivided; /**< size (radius, ...), for saving space divided by 229 | TPE_JOINT_SIZE_MULTIPLIER */ 230 | } TPE_Joint; 231 | 232 | typedef struct 233 | { 234 | uint8_t joint1; 235 | uint8_t joint2; 236 | uint16_t length; ///< connection's preferred length, uint16_t saves space 237 | } TPE_Connection; 238 | 239 | #define TPE_BODY_FLAG_DEACTIVATED 1 /**< Not being updated due to low energy, 240 | "sleeping", will be woken by 241 | collisions etc. */ 242 | #define TPE_BODY_FLAG_NONROTATING 2 /**< When set, the body won't rotate, 243 | will only move linearly. Here the 244 | velocity of the body's first joint 245 | is the velocity of the whole 246 | body. */ 247 | #define TPE_BODY_FLAG_DISABLED 4 /**< Disabled, not taking part in 248 | simulation. */ 249 | #define TPE_BODY_FLAG_SOFT 8 /**< Soft connections, effort won't be 250 | made to keep the body's shape. */ 251 | #define TPE_BODY_FLAG_SIMPLE_CONN 16 /**< Simple connections, don't zero out 252 | antagonist forces or apply 253 | connection friction, can increase 254 | performance. */ 255 | #define TPE_BODY_FLAG_ALWAYS_ACTIVE 32 /**< Will never deactivate due to low 256 | energy. */ 257 | 258 | /** Function used for defining static environment, working similarly to an SDF 259 | (signed distance function). The parameters are: 3D point P, max distance D. 260 | The function should behave like this: if P is inside the solid environment 261 | volume, P will be returned; otherwise closest point (by Euclidean distance) to 262 | the solid environment volume from P will be returned, except for a case when 263 | this closest point would be further away than D, in which case any arbitrary 264 | point further away than D may be returned (this allows for optimizations). */ 265 | typedef TPE_Vec3 (*TPE_ClosestPointFunction)(TPE_Vec3, TPE_Unit); 266 | 267 | /** Function that can be used as a joint-joint or joint-environment collision 268 | callback, parameters are following: body1 index, joint1 index, body2 index, 269 | joint2 index, collision world position. If body1 index is the same as body1 270 | index, then collision type is body-environment, otherwise it is body-body 271 | type. The function has to return either 1 if the collision is to be allowed 272 | or 0 if it is to be discarded. This can besides others be used to disable 273 | collisions between some bodies. */ 274 | typedef uint8_t (*TPE_CollisionCallback)(uint16_t, uint16_t, uint16_t, uint16_t, 275 | TPE_Vec3); 276 | 277 | /** Function used by the debug drawing functions to draw individual pixels to 278 | the screen. The parameters are following: pixel x, pixel y, pixel color. */ 279 | typedef void (*TPE_DebugDrawFunction)(uint16_t, uint16_t, uint8_t); 280 | 281 | /** Physics body made of spheres (each of same weight but possibly different 282 | radia) connected by elastic springs. */ 283 | typedef struct 284 | { 285 | TPE_Joint *joints; 286 | uint8_t jointCount; 287 | TPE_Connection *connections; 288 | uint8_t connectionCount; 289 | TPE_UnitReduced jointMass; ///< mass of a single joint 290 | TPE_UnitReduced friction; ///< friction of each joint 291 | TPE_UnitReduced elasticity; ///< elasticity of each joint 292 | uint8_t flags; 293 | uint8_t deactivateCount; 294 | } TPE_Body; 295 | 296 | typedef struct 297 | { 298 | TPE_Body *bodies; 299 | uint16_t bodyCount; 300 | TPE_ClosestPointFunction environmentFunction; 301 | TPE_CollisionCallback collisionCallback; 302 | } TPE_World; 303 | 304 | /** Tests the mathematical validity of given closest point function (function 305 | representing the physics environment), i.e. whether for example approaching 306 | some closest point in a straight line keeps approximately the same closest 307 | point. Note that this function may take a long time to complete, especially 308 | with higher gridResolution values and more complex environment functions. You 309 | should use this function to test your environment function, especially if you 310 | create functions for your own shapes etc. The cornerFrom and cornerTo points 311 | are corners of an axis-aligned box within which testing will take place, 312 | gridResolution defines numbers of points (i.e. step length) along each 313 | dimension to test (recommended e.g. 64), allowedError says error within which 314 | points will be considered the same (recommended range approx. 10 to 200). If 315 | testing is successful, 1 is returned, otherwise 0 is returned and the point 316 | around which error was detected is returned in errorPoint (unless the pointer 317 | is 0 in which case it is ignored). */ 318 | uint8_t TPE_testClosestPointFunction(TPE_ClosestPointFunction f, 319 | TPE_Vec3 cornerFrom, TPE_Vec3 cornerTo, uint8_t gridResolution, 320 | TPE_UnitReduced allowedError, TPE_Vec3 *errorPoint); 321 | 322 | void TPE_bodyInit(TPE_Body *body, 323 | TPE_Joint *joints, uint8_t jointCount, 324 | TPE_Connection *connections, uint8_t connectionCount, 325 | TPE_Unit mass); 326 | 327 | void TPE_worldInit(TPE_World *world, 328 | TPE_Body *bodies, uint16_t bodyCount, 329 | TPE_ClosestPointFunction environmentFunction); 330 | 331 | /** Gets orientation (rotation) of a body from a position of three of its 332 | joints. The vector from joint1 to joint2 is considered the body's forward 333 | direction, the vector from joint1 to joint3 its right direction. The returned 334 | rotation is in Euler angles (see rotation conventions). */ 335 | TPE_Vec3 TPE_bodyGetRotation(const TPE_Body *body, uint16_t joint1, 336 | uint16_t joint2, uint16_t joint3); 337 | 338 | void TPE_vec3Normalize(TPE_Vec3 *v); 339 | 340 | /** Rotates a 3D point by given Euler angle rotation (see rotation 341 | conventions). */ 342 | TPE_Vec3 TPE_pointRotate(TPE_Vec3 point, TPE_Vec3 rotation); 343 | 344 | /** Returns an inverse rotation to given rotation, in Euler angles (see rotation 345 | conventions). */ 346 | TPE_Vec3 TPE_rotationInverse(TPE_Vec3 rotation); 347 | 348 | /** Returns a connection tension, i.e. a signed percentage difference against 349 | desired length (TPE_FRACTIONS_PER_UNIT means 100%). */ 350 | static TPE_Unit TPE_connectionTension(TPE_Unit length, 351 | TPE_Unit desiredLength); 352 | 353 | /** Rotates a rotation specified in Euler angles by given axis + angle (see 354 | rotation conventions). Returns a rotation in Eurler angles. */ 355 | TPE_Vec3 TPE_rotationRotateByAxis(TPE_Vec3 rotation, TPE_Vec3 rotationByAxis); 356 | 357 | /** Computes the formula of a 1D collision of rigid bodies. */ 358 | void TPE_getVelocitiesAfterCollision(TPE_Unit *v1, TPE_Unit *v2, TPE_Unit m1, 359 | TPE_Unit m2, TPE_Unit elasticity); 360 | 361 | /** Computes orientation/rotation (see docs for orientation format) from two 362 | vectors (which should be at least close to being perpendicular and do NOT 363 | need to be normalized). This can be used to determine orientation of a body 364 | from a relative position of its joints. */ 365 | TPE_Vec3 TPE_rotationFromVecs(TPE_Vec3 forward, TPE_Vec3 right); 366 | 367 | TPE_Joint TPE_joint(TPE_Vec3 position, TPE_Unit size); 368 | 369 | /** Mostly for internal use, resolves a potential collision of two joints in a 370 | way that keeps the joints outside provided environment (if the function 371 | pointer is not 0). Returns 1 if joints collided or 0 otherwise. */ 372 | uint8_t TPE_jointsResolveCollision(TPE_Joint *j1, TPE_Joint *j2, 373 | TPE_Unit mass1, TPE_Unit mass2, TPE_Unit elasticity, TPE_Unit friction, 374 | TPE_ClosestPointFunction env); 375 | 376 | /** Mostly for internal use, tests and potentially resolves a collision between 377 | a joint and environment, returns 0 if no collision happened, 1 if it happened 378 | and was resolved normally and 2 if it couldn't be resolved normally. */ 379 | uint8_t TPE_jointEnvironmentResolveCollision(TPE_Joint *joint, TPE_Unit 380 | elasticity, TPE_Unit friction, TPE_ClosestPointFunction env); 381 | 382 | /** Tests whether a body is currently colliding with the environment. */ 383 | uint8_t TPE_bodyEnvironmentCollide(const TPE_Body *body, 384 | TPE_ClosestPointFunction env); 385 | 386 | /** Mostly for internal use, tests and potentially resolves a collision of a 387 | body with the environment, returns 1 if collision happened or 0 otherwise. */ 388 | uint8_t TPE_bodyEnvironmentResolveCollision(TPE_Body *body, 389 | TPE_ClosestPointFunction env); 390 | 391 | TPE_Vec3 TPE_bodyGetLinearVelocity(const TPE_Body *body); 392 | 393 | /** Computes the minimum bounding box of given body. */ 394 | void TPE_bodyGetAABB(const TPE_Body *body, TPE_Vec3 *vMin, TPE_Vec3 *vMax); 395 | 396 | /** Computes a bounding sphere of a body which is not minimal but faster to 397 | compute than the minimum bounding sphere. */ 398 | void TPE_bodyGetFastBSphere(const TPE_Body *body, TPE_Vec3 *center, 399 | TPE_Unit *radius); 400 | 401 | /** Computes the minimum bounding sphere of a body (there is another function 402 | for a faster approximate bounding sphere). */ 403 | void TPE_bodyGetBSphere(const TPE_Body *body, TPE_Vec3 *center, 404 | TPE_Unit *radius); 405 | 406 | uint8_t TPE_checkOverlapAABB(TPE_Vec3 v1Min, TPE_Vec3 v1Max, TPE_Vec3 v2Min, 407 | TPE_Vec3 v2Max); 408 | 409 | /** Mostly for internal use, checks and potentiall resolves collision of two 410 | bodies so as to keep them outside given environment. Returns 1 if collision 411 | happened or 0 otherwise. */ 412 | uint8_t TPE_bodiesResolveCollision(TPE_Body *b1, TPE_Body *b2, 413 | TPE_ClosestPointFunction env); 414 | 415 | /** Pins a joint of a body to specified location in space (sets its location 416 | and zeros its velocity). */ 417 | void TPE_jointPin(TPE_Joint *joint, TPE_Vec3 position); 418 | 419 | /** "Fakes" a rotation of a moving sphere by rotating it in the direction of 420 | its movement; this can create the illusion of the sphere actually rotating 421 | due to friction even if the physics sphere object (a body with a single joint) 422 | isn't rotating at all. Returns a rotation in the "about axis" format (see 423 | library conventions). */ 424 | TPE_Vec3 TPE_fakeSphereRotation(TPE_Vec3 position1, TPE_Vec3 position2, 425 | TPE_Unit radius); 426 | 427 | /** Casts a ray against environment and returns the closest hit of a surface. If 428 | no surface was hit, a vector with all elements equal to TPE_INFINITY will be 429 | returned. The function internally works differently for outside rays (rays 430 | cast from the outside of the environment) and inside rays. Outside rays can 431 | be traced with raymarching and will be processed very quickly and precisely; 432 | in this case if any intersection is found, the function will try to return a 433 | point outside (not guaranteed) the environment that's just in front of the hit 434 | surface. Inside rays are difficult and slow to trace because environment 435 | function won't provide distance, so the results aren't guaranteed to be 436 | precise (the ray may miss some intersections); here rays will be traced by 437 | given step (insideStepSize) and eventually iterated a bit towards the 438 | intersection -- if any intersection is found, the function will try to return 439 | a point inside (not guaranteed) the environment just before the hit 440 | surface. */ 441 | TPE_Vec3 TPE_castEnvironmentRay(TPE_Vec3 rayPos, TPE_Vec3 rayDir, 442 | TPE_ClosestPointFunction environment, TPE_Unit insideStepSize, 443 | TPE_Unit rayMarchMaxStep, uint32_t maxSteps); 444 | 445 | /** Casts a ray against bodies in a world (ignoring the environment), returns 446 | the position of the closest hit as well as the hit body's index in bodyIndex 447 | (unless the bodyIndex pointer is 0 in which case it is ignored). Similarly 448 | with jointIndex. If no hit is found a vector with all elements equal to 449 | TPE_INFINITY will be returned and bodyIndex will be -1. A specific body can be 450 | excluded with excludeBody (negative value will just make this parameter 451 | ignored). */ 452 | TPE_Vec3 TPE_castBodyRay(TPE_Vec3 rayPos, TPE_Vec3 rayDir, int16_t excludeBody, 453 | const TPE_World *world, int16_t *bodyIndex, int16_t *jointIndex); 454 | 455 | /** Performs one step (tick, frame, ...) of the physics world simulation 456 | including updating positions and velocities of bodies, collision detection and 457 | resolution, possible reshaping or deactivation of inactive bodies etc. The 458 | time length of the step is relative to all other units but it's ideal if it is 459 | 1/30th of a second. */ 460 | void TPE_worldStep(TPE_World *world); 461 | 462 | void TPE_worldDeactivateAll(TPE_World *world); 463 | void TPE_worldActivateAll(TPE_World *world); 464 | 465 | TPE_Unit TPE_worldGetNetSpeed(const TPE_World *world); 466 | TPE_Unit TPE_bodyGetNetSpeed(const TPE_Body *body); 467 | TPE_Unit TPE_bodyGetAverageSpeed(const TPE_Body *body); 468 | void TPE_bodyMultiplyNetSpeed(TPE_Body *body, TPE_Unit factor); 469 | void TPE_bodyLimitAverageSpeed(TPE_Body *body, TPE_Unit speedMin, 470 | TPE_Unit speedMax); 471 | 472 | /** Deactivates a body (puts it to sleep until another collision or force wake 473 | up). */ 474 | void TPE_bodyDeactivate(TPE_Body *body); 475 | 476 | uint8_t TPE_bodyIsActive(const TPE_Body *body); 477 | 478 | /** Attempts to shift the joints of a soft body so that the tension of all 479 | springs becomes zero while keeping the joints near their current position. 480 | This function performs one iteration of the equalizing algorithm and doesn't 481 | guarantee a perfect solution, it may help to run multiple iterations (call 482 | this function multiple times). */ 483 | void TPE_bodyReshape(TPE_Body *body, TPE_ClosestPointFunction 484 | environmentFunction); 485 | 486 | /** Mostly for internal use, performs some "magic" on body connections, mainly 487 | cancelling out of velocities going against each other and also applying 488 | connection friction in soft bodies. The strong parameter indicates if the 489 | body is soft or not. */ 490 | void TPE_bodyCancelOutVelocities(TPE_Body *body, uint8_t strong); 491 | 492 | /** Moves a body by certain offset. */ 493 | void TPE_bodyMoveBy(TPE_Body *body, TPE_Vec3 offset); 494 | 495 | /** Moves a body (its center of mass) to given position. */ 496 | void TPE_bodyMoveTo(TPE_Body *body, TPE_Vec3 position); 497 | 498 | /** Zeros velocities of all soft body joints. */ 499 | void TPE_bodyStop(TPE_Body *body); 500 | 501 | void TPE_bodyActivate(TPE_Body *body); 502 | 503 | /** Adds velocity to a soft body. */ 504 | void TPE_bodyAccelerate(TPE_Body *body, TPE_Vec3 velocity); 505 | 506 | void TPE_bodyApplyGravity(TPE_Body *body, TPE_Unit downwardsAccel); 507 | 508 | /** Adds angular velocity to a soft body. The rotation vector specifies the axis 509 | of rotation by its direction and angular velocity by its magnitude (magnitude 510 | of TPE_FRACTIONS_PER_UNIT will add linear velocity of TPE_FRACTIONS_PER_UNIT 511 | per tick to a point in the distance of TPE_FRACTIONS_PER_UNIT from the 512 | rotation axis). */ 513 | void TPE_bodySpin(TPE_Body *body, TPE_Vec3 rotation); 514 | 515 | /** Same as TPE_bodySpin but additionally allows to specify the center of 516 | the spin. */ 517 | void TPE_bodySpinWithCenter(TPE_Body *body, TPE_Vec3 rotation, TPE_Vec3 center); 518 | 519 | /** Instantly rotates a body about an axis (see library conventions for 520 | the rotation format). */ 521 | void TPE_bodyRotateByAxis(TPE_Body *body, TPE_Vec3 rotation); 522 | 523 | /** Computes the center of mass of a body. This averages the position of all 524 | joints; note that if you need, you may estimate the center of the body faster, 525 | e.g. by taking a position of a single "center joint", or averaging just 2 526 | extreme points. */ 527 | TPE_Vec3 TPE_bodyGetCenterOfMass(const TPE_Body *body); 528 | 529 | /** Draws a debug view of a 3D physics world using a provided pixel drawing 530 | function. This can be used to overlay a simple visualization of the physics 531 | objects to your main render, to spot exact borders of objects etc. The 532 | function draws simple dotted lines and circles with different "colors" for 533 | different types of objects (joints, connections, environemnt). camPos, camRot 534 | and camView should match the camera settings of your main renderer. CamView.x 535 | is horizontal resolution in pixels, camView.y is the vertical resolution, 536 | CamView.z says the camera focal length (~FOV) in TPE_Units (0 means 537 | orthographic projection). envGridRes is the resolution of an environment probe 538 | grid (the function will probe points in space and draw borders of the physics 539 | environemnt), envGridSize is the size (int TPE_Units) of the grid cell. Note 540 | the function may be slow (reducing envGridRes can help, workable value can be 541 | e.g. 16). */ 542 | void TPE_worldDebugDraw(TPE_World *world, TPE_DebugDrawFunction drawFunc, 543 | TPE_Vec3 camPos, TPE_Vec3 camRot, TPE_Vec3 camView, uint16_t envGridRes, 544 | TPE_Unit envGridSize); 545 | 546 | #define TPE_DEBUG_COLOR_CONNECTION 0 547 | #define TPE_DEBUG_COLOR_JOINT 1 548 | #define TPE_DEBUG_COLOR_ENVIRONMENT 2 549 | #define TPE_DEBUG_COLOR_INACTIVE 3 550 | 551 | uint32_t TPE_jointHash(const TPE_Joint *joint); 552 | uint32_t TPE_connectionHash(const TPE_Connection *connection); 553 | uint32_t TPE_bodyHash(const TPE_Body *body); 554 | 555 | /** Computes 32 bit hash of the world, useful for checking if two states of the 556 | world differ. The function takes into account most of the relevant state but 557 | possibly not all of it, for details check the code. */ 558 | uint32_t TPE_worldHash(const TPE_World *world); 559 | 560 | // FUNCTIONS FOR GENERATING BODIES 561 | 562 | void TPE_makeBox(TPE_Joint joints[8], TPE_Connection connections[16], 563 | TPE_Unit width, TPE_Unit depth, TPE_Unit height, TPE_Unit jointSize); 564 | void TPE_makeCenterBox(TPE_Joint joints[9], TPE_Connection connections[18], 565 | TPE_Unit width, TPE_Unit depth, TPE_Unit height, TPE_Unit jointSize); 566 | void TPE_makeRect(TPE_Joint joints[4], TPE_Connection connections[6], 567 | TPE_Unit width, TPE_Unit depth, TPE_Unit jointSize); 568 | void TPE_makeTriangle(TPE_Joint joints[3], TPE_Connection connections[3], 569 | TPE_Unit sideLength, TPE_Unit jointSize); 570 | void TPE_makeCenterRect(TPE_Joint joints[5], TPE_Connection connections[8], 571 | TPE_Unit width, TPE_Unit depth, TPE_Unit jointSize); 572 | void TPE_makeCenterRectFull(TPE_Joint joints[5], TPE_Connection connections[10], 573 | TPE_Unit width, TPE_Unit depth, TPE_Unit jointSize); 574 | void TPE_make2Line(TPE_Joint joints[2], TPE_Connection connections[1], 575 | TPE_Unit length, TPE_Unit jointSize); 576 | 577 | // FUNCTIONS FOR BUILDING ENVIRONMENT 578 | 579 | TPE_Vec3 TPE_envAABoxInside(TPE_Vec3 point, TPE_Vec3 center, TPE_Vec3 size); 580 | TPE_Vec3 TPE_envAABox(TPE_Vec3 point, TPE_Vec3 center, TPE_Vec3 maxCornerVec); 581 | TPE_Vec3 TPE_envBox(TPE_Vec3 point, TPE_Vec3 center, TPE_Vec3 maxCornerVec, 582 | TPE_Vec3 rotation); 583 | TPE_Vec3 TPE_envSphere(TPE_Vec3 point, TPE_Vec3 center, TPE_Unit radius); 584 | TPE_Vec3 TPE_envSphereInside(TPE_Vec3 point, TPE_Vec3 center, TPE_Unit radius); 585 | TPE_Vec3 TPE_envHalfPlane(TPE_Vec3 point, TPE_Vec3 center, TPE_Vec3 normal); 586 | TPE_Vec3 TPE_envGround(TPE_Vec3 point, TPE_Unit height); 587 | TPE_Vec3 TPE_envInfiniteCylinder(TPE_Vec3 point, TPE_Vec3 center, TPE_Vec3 588 | direction, TPE_Unit radius); 589 | TPE_Vec3 TPE_envCylinder(TPE_Vec3 point, TPE_Vec3 center, TPE_Vec3 direction, 590 | TPE_Unit radius); 591 | TPE_Vec3 TPE_envCone(TPE_Vec3 point, TPE_Vec3 center, TPE_Vec3 direction, 592 | TPE_Unit radius); 593 | TPE_Vec3 TPE_envLineSegment(TPE_Vec3 point, TPE_Vec3 a, TPE_Vec3 b); 594 | TPE_Vec3 TPE_envHeightmap(TPE_Vec3 point, TPE_Vec3 center, TPE_Unit gridSize, 595 | TPE_Unit (*heightFunction)(int32_t x, int32_t y), TPE_Unit maxDist); 596 | 597 | /** Environment function for triagnular prism, e.g. for ramps. The sides array 598 | contains three 2D coordinates of points of the triangle in given plane with 599 | respect to the center. WARNING: the points must be specified in counter 600 | clowckwise direction! The direction var specified axis direction (0, 1 or 601 | 2).*/ 602 | TPE_Vec3 TPE_envAATriPrism(TPE_Vec3 point, TPE_Vec3 center, 603 | const TPE_Unit sides[6], TPE_Unit depth, uint8_t direction); 604 | 605 | /* The following are helper macros for creating a union of shapes inside an 606 | environment function and accelerating them with bounding volumes. */ 607 | 608 | #define TPE_ENV_START(test,point) TPE_Vec3 _pBest = test, _pTest; \ 609 | TPE_Unit _dBest = TPE_DISTANCE(_pBest,point), _dTest; \ 610 | (void)(_pBest); (void)(_dBest); (void)(_dTest); (void)(_pTest); // supress war 611 | 612 | #define TPE_ENV_NEXT(test,point) \ 613 | { if (_pBest.x == point.x && _pBest.y == point.y && _pBest.z == point.z) \ 614 | return _pBest; \ 615 | _pTest = test; _dTest = TPE_DISTANCE(_pTest,point); \ 616 | if (_dTest < _dBest) { _pBest = _pTest; _dBest = _dTest; } } 617 | 618 | #define TPE_ENV_END return _pBest; 619 | 620 | #define TPE_ENV_BCUBE_TEST(bodyBCubeC,bodyBCubeR,envBCubeC,envBCubeR) ( \ 621 | (TPE_abs(envBCubeC.x - bodyBCubeC.x) <= ((bodyBCubeR) + (envBCubeR))) && \ 622 | (TPE_abs(envBCubeC.y - bodyBCubeC.y) <= ((bodyBCubeR) + (envBCubeR))) && \ 623 | (TPE_abs(envBCubeC.z - bodyBCubeC.z) <= ((bodyBCubeR) + (envBCubeR)))) 624 | 625 | #define TPE_ENV_BSPHERE_TEST(bodyBSphereC,bodyBSphereR,envBSphereC,envBSphereR)\ 626 | (TPE_DISTANCE(bodyBSphereC,envBSphereC) <= ((bodyBSphereR) + (envBSphereR))) 627 | 628 | 629 | #endif 630 | 631 | #endif // guard 632 | --------------------------------------------------------------------------------