diff --git a/assets/sb_holes/TheMorningAfter.xm b/assets/sb_holes/TheMorningAfter.xm new file mode 100644 index 00000000..341d8051 Binary files /dev/null and b/assets/sb_holes/TheMorningAfter.xm differ diff --git a/assets/sb_holes/building.glb b/assets/sb_holes/building.glb new file mode 100644 index 00000000..cb62fdc6 Binary files /dev/null and b/assets/sb_holes/building.glb differ diff --git a/assets/sb_holes/car.ci4.png b/assets/sb_holes/car.ci4.png new file mode 100644 index 00000000..af6e3c74 Binary files /dev/null and b/assets/sb_holes/car.ci4.png differ diff --git a/assets/sb_holes/car.glb b/assets/sb_holes/car.glb new file mode 100644 index 00000000..7df7b44e Binary files /dev/null and b/assets/sb_holes/car.glb differ diff --git a/assets/sb_holes/car.i4.png b/assets/sb_holes/car.i4.png new file mode 100644 index 00000000..cf8622e1 Binary files /dev/null and b/assets/sb_holes/car.i4.png differ diff --git a/assets/sb_holes/car.mp3 b/assets/sb_holes/car.mp3 new file mode 100644 index 00000000..5f8d3dde Binary files /dev/null and b/assets/sb_holes/car.mp3 differ diff --git a/assets/sb_holes/car1.i4.png b/assets/sb_holes/car1.i4.png new file mode 100644 index 00000000..0ef4e09d Binary files /dev/null and b/assets/sb_holes/car1.i4.png differ diff --git a/assets/sb_holes/hole.glb b/assets/sb_holes/hole.glb new file mode 100644 index 00000000..ef89e070 Binary files /dev/null and b/assets/sb_holes/hole.glb differ diff --git a/assets/sb_holes/hole.ia8.png b/assets/sb_holes/hole.ia8.png new file mode 100644 index 00000000..5d16be8f Binary files /dev/null and b/assets/sb_holes/hole.ia8.png differ diff --git a/assets/sb_holes/hydrant.ci8.png b/assets/sb_holes/hydrant.ci8.png new file mode 100644 index 00000000..cb5d9830 Binary files /dev/null and b/assets/sb_holes/hydrant.ci8.png differ diff --git a/assets/sb_holes/hydrant.glb b/assets/sb_holes/hydrant.glb new file mode 100644 index 00000000..601ff825 Binary files /dev/null and b/assets/sb_holes/hydrant.glb differ diff --git a/assets/sb_holes/hydrant.mp3 b/assets/sb_holes/hydrant.mp3 new file mode 100644 index 00000000..2be9de64 Binary files /dev/null and b/assets/sb_holes/hydrant.mp3 differ diff --git a/assets/sb_holes/map.glb b/assets/sb_holes/map.glb new file mode 100644 index 00000000..5293574e Binary files /dev/null and b/assets/sb_holes/map.glb differ diff --git a/assets/sb_holes/sound/LICENSE b/assets/sb_holes/sound/LICENSE new file mode 100644 index 00000000..a0307619 --- /dev/null +++ b/assets/sb_holes/sound/LICENSE @@ -0,0 +1 @@ +Car Crash by squareal -- https://freesound.org/s/237375/ -- License: Creative Commons 0 \ No newline at end of file diff --git a/assets/sb_holes/street.ci4.png b/assets/sb_holes/street.ci4.png new file mode 100644 index 00000000..47fe746f Binary files /dev/null and b/assets/sb_holes/street.ci4.png differ diff --git a/assets/sb_holes/window.ci4.png b/assets/sb_holes/window.ci4.png new file mode 100644 index 00000000..13584de9 Binary files /dev/null and b/assets/sb_holes/window.ci4.png differ diff --git a/assets/sb_holes/window1.ci4.png b/assets/sb_holes/window1.ci4.png new file mode 100644 index 00000000..25d5dbcc Binary files /dev/null and b/assets/sb_holes/window1.ci4.png differ diff --git a/assets/strawberry_byte/bricks48.i8.png b/assets/strawberry_byte/bricks48.i8.png new file mode 100644 index 00000000..9e7c4617 Binary files /dev/null and b/assets/strawberry_byte/bricks48.i8.png differ diff --git a/assets/strawberry_byte/cloud_base.glb b/assets/strawberry_byte/cloud_base.glb new file mode 100644 index 00000000..2cb93716 Binary files /dev/null and b/assets/strawberry_byte/cloud_base.glb differ diff --git a/assets/strawberry_byte/dogman.glb b/assets/strawberry_byte/dogman.glb new file mode 100644 index 00000000..981bf362 Binary files /dev/null and b/assets/strawberry_byte/dogman.glb differ diff --git a/assets/strawberry_byte/dogman_eye.png b/assets/strawberry_byte/dogman_eye.png new file mode 100644 index 00000000..5d1429a9 Binary files /dev/null and b/assets/strawberry_byte/dogman_eye.png differ diff --git a/assets/strawberry_byte/dogman_eyebrow.png b/assets/strawberry_byte/dogman_eyebrow.png new file mode 100644 index 00000000..680d4732 Binary files /dev/null and b/assets/strawberry_byte/dogman_eyebrow.png differ diff --git a/assets/strawberry_byte/dogman_mouth.png b/assets/strawberry_byte/dogman_mouth.png new file mode 100644 index 00000000..f63a848d Binary files /dev/null and b/assets/strawberry_byte/dogman_mouth.png differ diff --git a/assets/strawberry_byte/fast64.png b/assets/strawberry_byte/fast64.png new file mode 100644 index 00000000..3762d21c Binary files /dev/null and b/assets/strawberry_byte/fast64.png differ diff --git a/assets/strawberry_byte/frog_eye.png b/assets/strawberry_byte/frog_eye.png new file mode 100644 index 00000000..ac5673eb Binary files /dev/null and b/assets/strawberry_byte/frog_eye.png differ diff --git a/assets/strawberry_byte/jam_logo.png b/assets/strawberry_byte/jam_logo.png new file mode 100644 index 00000000..c54ddeb6 Binary files /dev/null and b/assets/strawberry_byte/jam_logo.png differ diff --git a/assets/strawberry_byte/lava.glb b/assets/strawberry_byte/lava.glb new file mode 100644 index 00000000..cf4b0133 Binary files /dev/null and b/assets/strawberry_byte/lava.glb differ diff --git a/assets/strawberry_byte/lava00.rgba16.png b/assets/strawberry_byte/lava00.rgba16.png new file mode 100644 index 00000000..08fa21cb Binary files /dev/null and b/assets/strawberry_byte/lava00.rgba16.png differ diff --git a/assets/strawberry_byte/lava08.rgba16.png b/assets/strawberry_byte/lava08.rgba16.png new file mode 100644 index 00000000..afb4add6 Binary files /dev/null and b/assets/strawberry_byte/lava08.rgba16.png differ diff --git a/assets/strawberry_byte/libdragon_logo.png b/assets/strawberry_byte/libdragon_logo.png new file mode 100644 index 00000000..bd9266f8 Binary files /dev/null and b/assets/strawberry_byte/libdragon_logo.png differ diff --git a/assets/strawberry_byte/mew.glb b/assets/strawberry_byte/mew.glb new file mode 100644 index 00000000..327fb9ba Binary files /dev/null and b/assets/strawberry_byte/mew.glb differ diff --git a/assets/strawberry_byte/mew_ear.png b/assets/strawberry_byte/mew_ear.png new file mode 100644 index 00000000..8207759f Binary files /dev/null and b/assets/strawberry_byte/mew_ear.png differ diff --git a/assets/strawberry_byte/mew_eye.png b/assets/strawberry_byte/mew_eye.png new file mode 100644 index 00000000..ca28b75b Binary files /dev/null and b/assets/strawberry_byte/mew_eye.png differ diff --git a/assets/strawberry_byte/n64brew.png b/assets/strawberry_byte/n64brew.png new file mode 100644 index 00000000..1a550829 Binary files /dev/null and b/assets/strawberry_byte/n64brew.png differ diff --git a/assets/strawberry_byte/nose.png b/assets/strawberry_byte/nose.png new file mode 100644 index 00000000..ba0f3a16 Binary files /dev/null and b/assets/strawberry_byte/nose.png differ diff --git a/assets/strawberry_byte/platform.glb b/assets/strawberry_byte/platform.glb new file mode 100644 index 00000000..e245d76a Binary files /dev/null and b/assets/strawberry_byte/platform.glb differ diff --git a/assets/strawberry_byte/platform2.glb b/assets/strawberry_byte/platform2.glb new file mode 100644 index 00000000..4b5afb06 Binary files /dev/null and b/assets/strawberry_byte/platform2.glb differ diff --git a/assets/strawberry_byte/s4ys.glb b/assets/strawberry_byte/s4ys.glb new file mode 100644 index 00000000..013a8726 Binary files /dev/null and b/assets/strawberry_byte/s4ys.glb differ diff --git a/assets/strawberry_byte/sound/LICENSE.txt b/assets/strawberry_byte/sound/LICENSE.txt new file mode 100644 index 00000000..28ebeaf6 --- /dev/null +++ b/assets/strawberry_byte/sound/LICENSE.txt @@ -0,0 +1,5 @@ +Stones Falling by iwanPlays -- https://freesound.org/s/567251/ -- License: Creative Commons 0 + +Strong wind blowing in the plain in Anatolia (Turkey) by felix.blume -- https://freesound.org/s/167684/ -- License: Creative Commons 0 + +Grunt 01 - Retro, Lo-Fi.wav by 8bitmyketison -- https://freesound.org/s/699927/ -- License: Creative Commons 0 \ No newline at end of file diff --git a/assets/strawberry_byte/sound/grunt-01.wav b/assets/strawberry_byte/sound/grunt-01.wav new file mode 100644 index 00000000..9a9a0185 Binary files /dev/null and b/assets/strawberry_byte/sound/grunt-01.wav differ diff --git a/assets/strawberry_byte/sound/hexagone.mp3 b/assets/strawberry_byte/sound/hexagone.mp3 new file mode 100644 index 00000000..bfb81c7d Binary files /dev/null and b/assets/strawberry_byte/sound/hexagone.mp3 differ diff --git a/assets/strawberry_byte/sound/sky_high.xm b/assets/strawberry_byte/sound/sky_high.xm new file mode 100644 index 00000000..ec75f7dc Binary files /dev/null and b/assets/strawberry_byte/sound/sky_high.xm differ diff --git a/assets/strawberry_byte/sound/stones-falling.mp3 b/assets/strawberry_byte/sound/stones-falling.mp3 new file mode 100644 index 00000000..f2854c47 Binary files /dev/null and b/assets/strawberry_byte/sound/stones-falling.mp3 differ diff --git a/assets/strawberry_byte/sound/strong_wind_blowing.mp3 b/assets/strawberry_byte/sound/strong_wind_blowing.mp3 new file mode 100644 index 00000000..5769d296 Binary files /dev/null and b/assets/strawberry_byte/sound/strong_wind_blowing.mp3 differ diff --git a/assets/strawberry_byte/ui/buttons/License.txt b/assets/strawberry_byte/ui/buttons/License.txt new file mode 100644 index 00000000..a304655d --- /dev/null +++ b/assets/strawberry_byte/ui/buttons/License.txt @@ -0,0 +1,23 @@ + + + Input Prompts Pixel 16× (1.0) + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 23-09-2021 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + This content is free to use in personal, educational and commercial projects. + + Support us by crediting Kenney or www.kenney.nl (this is not mandatory) + + ------------------------------ + + Donate: http://support.kenney.nl + Patreon: http://patreon.com/kenney/ + + Follow on Twitter for updates: + http://twitter.com/KenneyNL \ No newline at end of file diff --git a/assets/strawberry_byte/ui/buttons/c_buttons0.rgba32.png b/assets/strawberry_byte/ui/buttons/c_buttons0.rgba32.png new file mode 100644 index 00000000..5161f221 Binary files /dev/null and b/assets/strawberry_byte/ui/buttons/c_buttons0.rgba32.png differ diff --git a/assets/strawberry_byte/ui/buttons/c_buttons1.rgba32.png b/assets/strawberry_byte/ui/buttons/c_buttons1.rgba32.png new file mode 100644 index 00000000..e72b9884 Binary files /dev/null and b/assets/strawberry_byte/ui/buttons/c_buttons1.rgba32.png differ diff --git a/assets/strawberry_byte/ui/buttons/control_stick.ia8.png b/assets/strawberry_byte/ui/buttons/control_stick.ia8.png new file mode 100644 index 00000000..46fed939 Binary files /dev/null and b/assets/strawberry_byte/ui/buttons/control_stick.ia8.png differ diff --git a/assets/strawberry_byte/ui/buttons/d_pad_triggers.ia8.png b/assets/strawberry_byte/ui/buttons/d_pad_triggers.ia8.png new file mode 100644 index 00000000..d5f1b807 Binary files /dev/null and b/assets/strawberry_byte/ui/buttons/d_pad_triggers.ia8.png differ diff --git a/assets/strawberry_byte/ui/buttons/face_buttons0.rgba32.png b/assets/strawberry_byte/ui/buttons/face_buttons0.rgba32.png new file mode 100644 index 00000000..4eb5424e Binary files /dev/null and b/assets/strawberry_byte/ui/buttons/face_buttons0.rgba32.png differ diff --git a/assets/strawberry_byte/ui/buttons/face_buttons1.rgba32.png b/assets/strawberry_byte/ui/buttons/face_buttons1.rgba32.png new file mode 100644 index 00000000..4c9d3a66 Binary files /dev/null and b/assets/strawberry_byte/ui/buttons/face_buttons1.rgba32.png differ diff --git a/assets/strawberry_byte/ui/fonts/OFL.txt b/assets/strawberry_byte/ui/fonts/OFL.txt new file mode 100644 index 00000000..6f8d0f4f --- /dev/null +++ b/assets/strawberry_byte/ui/fonts/OFL.txt @@ -0,0 +1,94 @@ +Copyright (c) 2011, Rodrigo Fuenzalida (www.rfuenzalida.com|hello@rfuenzalida.com), +with Reserved Font Name Titan. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/strawberry_byte/ui/fonts/OilOnTheWater-ee5O.ttf b/assets/strawberry_byte/ui/fonts/OilOnTheWater-ee5O.ttf new file mode 100644 index 00000000..08209ef4 Binary files /dev/null and b/assets/strawberry_byte/ui/fonts/OilOnTheWater-ee5O.ttf differ diff --git a/assets/strawberry_byte/ui/fonts/TitanOne-Regular.ttf b/assets/strawberry_byte/ui/fonts/TitanOne-Regular.ttf new file mode 100644 index 00000000..8a9d7230 Binary files /dev/null and b/assets/strawberry_byte/ui/fonts/TitanOne-Regular.ttf differ diff --git a/assets/strawberry_byte/ui/fonts/info.txt b/assets/strawberry_byte/ui/fonts/info.txt new file mode 100644 index 00000000..c5597c4d --- /dev/null +++ b/assets/strawberry_byte/ui/fonts/info.txt @@ -0,0 +1,2 @@ +license: Freeware +link: https://www.fontspace.com/oil-on-the-water-font-f4172 \ No newline at end of file diff --git a/assets/strawberry_byte/ui/logos/LICENSE.txt b/assets/strawberry_byte/ui/logos/LICENSE.txt new file mode 100644 index 00000000..dd1aa6de --- /dev/null +++ b/assets/strawberry_byte/ui/logos/LICENSE.txt @@ -0,0 +1,8 @@ +The following images: + - `sb_b0.rgba32.png` + - `sb_b1.rgba32.png` + - `sb_top.rgba32.png` + +are attributed to this work: + "Juicy Strawberry Cartoon on a Transparent Background Summer Fruit Collection" by Sonika Rud + https://www.vecteezy.com/vector-art/19135160-juicy-strawberry-cartoon-on-a-transparent-background-summer-fruit-collection \ No newline at end of file diff --git a/assets/strawberry_byte/ui/logos/libdragon.ia4.png b/assets/strawberry_byte/ui/logos/libdragon.ia4.png new file mode 100644 index 00000000..607fcf4e Binary files /dev/null and b/assets/strawberry_byte/ui/logos/libdragon.ia4.png differ diff --git a/assets/strawberry_byte/ui/logos/mixamo.ia4.png b/assets/strawberry_byte/ui/logos/mixamo.ia4.png new file mode 100644 index 00000000..d0aebf24 Binary files /dev/null and b/assets/strawberry_byte/ui/logos/mixamo.ia4.png differ diff --git a/assets/strawberry_byte/ui/logos/sb_b0.rgba32.png b/assets/strawberry_byte/ui/logos/sb_b0.rgba32.png new file mode 100644 index 00000000..39e8fbad Binary files /dev/null and b/assets/strawberry_byte/ui/logos/sb_b0.rgba32.png differ diff --git a/assets/strawberry_byte/ui/logos/sb_b1.rgba32.png b/assets/strawberry_byte/ui/logos/sb_b1.rgba32.png new file mode 100644 index 00000000..727f78ed Binary files /dev/null and b/assets/strawberry_byte/ui/logos/sb_b1.rgba32.png differ diff --git a/assets/strawberry_byte/ui/logos/sb_top.rgba32.png b/assets/strawberry_byte/ui/logos/sb_top.rgba32.png new file mode 100644 index 00000000..1e767a08 Binary files /dev/null and b/assets/strawberry_byte/ui/logos/sb_top.rgba32.png differ diff --git a/assets/strawberry_byte/ui/logos/t3d.ia8.png b/assets/strawberry_byte/ui/logos/t3d.ia8.png new file mode 100644 index 00000000..9f380bd1 Binary files /dev/null and b/assets/strawberry_byte/ui/logos/t3d.ia8.png differ diff --git a/assets/strawberry_byte/ui/panels/License.txt b/assets/strawberry_byte/ui/panels/License.txt new file mode 100644 index 00000000..58a7901e --- /dev/null +++ b/assets/strawberry_byte/ui/panels/License.txt @@ -0,0 +1,23 @@ + + + UI Pack (2.0) + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 12-06-2024 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + This content is free to use in personal, educational and commercial projects. + + Support us by crediting Kenney or www.kenney.nl (this is not mandatory) + + ------------------------------ + + Donate: http://support.kenney.nl + Patreon: http://patreon.com/kenney/ + + Follow on Twitter for updates: + http://twitter.com/KenneyNL \ No newline at end of file diff --git a/assets/strawberry_byte/ui/panels/clouds.ia8.png b/assets/strawberry_byte/ui/panels/clouds.ia8.png new file mode 100644 index 00000000..ee8921b9 Binary files /dev/null and b/assets/strawberry_byte/ui/panels/clouds.ia8.png differ diff --git a/assets/strawberry_byte/ui/panels/gloss.ia4.png b/assets/strawberry_byte/ui/panels/gloss.ia4.png new file mode 100644 index 00000000..aad6eb4b Binary files /dev/null and b/assets/strawberry_byte/ui/panels/gloss.ia4.png differ diff --git a/assets/strawberry_byte/ui/panels/pattern_tessalate.ia4.png b/assets/strawberry_byte/ui/panels/pattern_tessalate.ia4.png new file mode 100644 index 00000000..d7e09823 Binary files /dev/null and b/assets/strawberry_byte/ui/panels/pattern_tessalate.ia4.png differ diff --git a/assets/strawberry_byte/wolf_eye.png b/assets/strawberry_byte/wolf_eye.png new file mode 100644 index 00000000..23d3865e Binary files /dev/null and b/assets/strawberry_byte/wolf_eye.png differ diff --git a/assets/strawberry_byte/wolfie.glb b/assets/strawberry_byte/wolfie.glb new file mode 100644 index 00000000..63091c0e Binary files /dev/null and b/assets/strawberry_byte/wolfie.glb differ diff --git a/code/sb_halcyon/README.md b/code/sb_halcyon/README.md new file mode 100644 index 00000000..db407530 --- /dev/null +++ b/code/sb_halcyon/README.md @@ -0,0 +1,21 @@ +# Strawberry Byte +* Strawberry Sprite by Sonika Rud [link](https://www.vecteezy.com/vector-art/19135160-juicy-strawberry-cartoon-on-a-transparent-background-summer-fruit-collection) +* Original 'Olli' Model by FazanaJ [link](https://github.com/FazanaJ/gldemo/blob/bring-back-movement/assets/models/humanoid.glb) +* UI Sprites by Kenney [link](https://kenney.nl/assets) +* SFX obtained from [Freesound](https://freesound.org) + +## Halcyon Hexagons +* Programming: zoncabe, s4ys +* Models: zoncabe, mewde, s4ys +* Original Composition by Kaelin Stemmler + +## holes +* Programming: s4ys +* Models: s4ys +* Music: 'The Morning After" by Soft One [link](https://github.com/DragonMinded/libdragon/blob/trunk/examples/audioplayer/assets/TheMorningAfter.xm) + +## Hot Hot Hexagons +* Programming: zoncabe, s4ys +* Models: zoncabe, mewde, s4ys +* * Lava model by HailToDodongo [link](https://github.com/HailToDodongo/tiny3d/blob/main/examples/04_dynamic/assets/lava.blend) +* Original Composition by Kaelin Stemmler diff --git a/code/sb_halcyon/actor/actor.h b/code/sb_halcyon/actor/actor.h new file mode 100644 index 00000000..d9495d1b --- /dev/null +++ b/code/sb_halcyon/actor/actor.h @@ -0,0 +1,216 @@ +#ifndef ACTOR_H +#define ACTOR_H + +// structures + +typedef struct +{ + + float idle_acceleration_rate; + float walk_acceleration_rate; + float run_acceleration_rate; + float roll_acceleration_rate; + float roll_acceleration_grip_rate; + float jump_acceleration_rate; + float aerial_control_rate; + + float walk_target_speed; + float run_target_speed; + float sprint_target_speed; + float idle_to_roll_target_speed; + float idle_to_roll_grip_target_speed; + float walk_to_roll_target_speed; + float run_to_roll_target_speed; + float sprint_to_roll_target_speed; + float jump_target_speed; + + float jump_timer_max; + + float fall_max_speed; + float jump_max_speed; + float jump_horizontal_boost; + float jump_max_height; + +} ActorSettings; + +typedef struct +{ + + float stick_magnitude; + float stick_x; + float stick_y; + float jump_time_held; + float jump_time_buffer; + bool jump_hold; + bool jump_released; + +} Actorinput; + +typedef struct +{ + + T3DSkeleton main; + T3DSkeleton blend; + +} ActorArmature; + +typedef struct +{ + + T3DAnim breathing_idle; + T3DAnim running_left; + T3DAnim jump_left; + T3DAnim falling_left; + T3DAnim land_left; + +} AnimationSet; + +typedef struct +{ + + uint8_t previous; + uint8_t current; + + AnimationSet main; + AnimationSet blend; + + uint8_t change_delay; + float blending_ratio; + float speed_rate; + bool synced; + +} ActorAnimation; + +typedef struct +{ + + uint32_t id; + rspq_block_t *dl; + T3DMat4FP *modelMat; + T3DModel *model; + Vector3 scale; + + char model_path; + ActorArmature armature; + ActorAnimation animation; + + RigidBody body; + + float target_yaw; + float horizontal_target_speed; + Vector3 target_velocity; + + float horizontal_speed; + bool grounded; + float grounding_height; + + bool hasCollided; // Testing a collision boolean + + uint8_t locomotion_state; + uint8_t previous_state; + uint8_t state; + + Vector3 home; + + ActorSettings settings; + Actorinput input; + + // Not me adding a dumb fields instead of refactoring + int colorID; + +} Actor; + +// function prototypes + +Actor actor_create(uint32_t id, const char *model_path); + +void actor_draw(Actor *actor); +void actor_delete(Actor *actor); + +// function implemenations + +Actor actor_create(uint32_t id, const char *model_path) +{ + Actor actor = { + + .id = id, + .model = t3d_model_load(model_path), + .modelMat = malloc_uncached(sizeof(T3DMat4FP)), + + .scale = {1.0f, 1.0f, 1.0f}, + + .state = 1, + .previous_state = 1, + .locomotion_state = 1, + + .body = { + .position = {0.0f, 0.0f, 0.0f}, + .velocity = {0.0f, 0.0f, 0.0f}, + .rotation = {0.0f, 0.0f, 0.0f}, + }, + + .grounding_height = -2000.0f, // magic number + + .settings = {.idle_acceleration_rate = 9, .walk_acceleration_rate = 4, .run_acceleration_rate = 10, .roll_acceleration_rate = 20, .roll_acceleration_grip_rate = 2, .jump_acceleration_rate = 60, .aerial_control_rate = 6.0, .walk_target_speed = 200, .run_target_speed = 700, .sprint_target_speed = 900, .idle_to_roll_target_speed = 300, .idle_to_roll_grip_target_speed = 50, .walk_to_roll_target_speed = 400, .run_to_roll_target_speed = 780, .sprint_to_roll_target_speed = 980, .jump_target_speed = 800, .jump_timer_max = 0.21, .fall_max_speed = -2650.0f, .jump_max_speed = 1000.0f, .jump_horizontal_boost = 125.0f, .jump_max_height = 1000.0f}, + }; + + actor.armature.main = t3d_skeleton_create(actor.model); + // actor.armature.blend = t3d_skeleton_clone(&actor.armature.main, false); + + rspq_block_begin(); + t3d_matrix_set(actor.modelMat, true); + t3d_model_draw_skinned(actor.model, &actor.armature.main); + actor.dl = rspq_block_end(); + + t3d_mat4fp_identity(actor.modelMat); + + return actor; +} + +void actor_updateMat(Actor *actor) +{ + if (actor->state == 9) + return; // DEATH + + t3d_mat4fp_from_srt_euler(actor->modelMat, + (float[3]){actor->scale.x, actor->scale.y, actor->scale.z}, + (float[3]){rad(actor->body.rotation.x), rad(actor->body.rotation.y), rad(actor->body.rotation.z)}, + (float[3]){actor->body.position.x, actor->body.position.y, actor->body.position.z}); +} + +void actor_draw(Actor *actor) +{ + for (uint8_t i = 0; i < ACTOR_COUNT; i++) + { + + if (actor[i].state == 9) + continue; // DEATH + + rspq_block_run(actor[i].dl); + }; +} + +void actor_delete(Actor *actor) +{ + free_uncached(actor->modelMat); + + t3d_skeleton_destroy(&actor->armature.main); + t3d_anim_destroy(&actor->animation.main.breathing_idle); + t3d_anim_destroy(&actor->animation.main.running_left); + t3d_anim_destroy(&actor->animation.main.falling_left); + // t3d_anim_destroy(&actor->animation.main.jump_left); + // t3d_anim_destroy(&actor->animation.main.land_left); + + // t3d_skeleton_destroy(&actor->armature.blend); + // t3d_anim_destroy(&actor->animation.blend.breathing_idle); + // t3d_anim_destroy(&actor->animation.blend.running_left); + // t3d_anim_destroy(&actor->animation.blend.falling_left); + // t3d_anim_destroy(&actor->animation.blend.jump_left); + // t3d_anim_destroy(&actor->animation.blend.land_left); + + t3d_model_free(actor->model); + if (actor->dl != NULL) + rspq_block_free(actor->dl); +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/actor/actor_animation.h b/code/sb_halcyon/actor/actor_animation.h new file mode 100644 index 00000000..b1e1a008 --- /dev/null +++ b/code/sb_halcyon/actor/actor_animation.h @@ -0,0 +1,137 @@ +#ifndef ACTOR_ANIMATION_H +#define ACTOR_ANIMATION_H + +// function implemenations + +ActorAnimation actorAnimation_create(const Actor *actor) +{ + ActorAnimation animation; + animation.previous = STAND_IDLE; + animation.current = STAND_IDLE; + animation.blending_ratio = 0.0f; + return animation; +} + +void animationSet_init(const Actor *actor, AnimationSet *set) +{ + set->breathing_idle = t3d_anim_create(actor->model, "breathing-idle"); + set->running_left = t3d_anim_create(actor->model, "running-10-left"); + // set->jump_left = t3d_anim_create(actor->model, "jump-left"); + set->falling_left = t3d_anim_create(actor->model, "falling-idle-left"); + // set->land_left = t3d_anim_create(actor->model, "falling-to-landing-left"); +} + +void actorAnimation_init(const Actor *actor, ActorAnimation *animation) +{ + animationSet_init(actor, &animation->main); + // animationSet_init(actor, &animation->blend); + // attach main + t3d_anim_attach(&animation->main.breathing_idle, &actor->armature.main); + t3d_anim_attach(&animation->main.falling_left, &actor->armature.main); + t3d_anim_attach(&animation->main.running_left, &actor->armature.main); + + // attach blend + // t3d_anim_attach(&animation->blend.running_left, &actor->armature.blend); +} + +void actorAnimation_setStandIdle(Actor *actor, ActorAnimation *animation, const float frame_time, rspq_syncpoint_t *syncpoint) +{ + t3d_anim_update(&animation->main.breathing_idle, frame_time); +} + +void actorAnimation_setRunning(Actor *actor, ActorAnimation *animation, const float frame_time, rspq_syncpoint_t *syncpoint) +{ + // if (animation->previous == STAND_IDLE || animation->current == STAND_IDLE) { + // animation->blending_ratio = actor->horizontal_speed / 320; + // if (animation->blending_ratio > 1.0f) animation->blending_ratio = 1.0f; + // if (animation->current == STAND_IDLE) t3d_anim_set_time(&animation->blend.running_left, 0.0f); + // if(animation->blending_ratio > 0.0f && animation->blending_ratio < 1.0f) + // { + // t3d_anim_update(&animation->main.breathing_idle, frame_time); + // t3d_anim_set_speed(&animation->blend.running_left, animation->blending_ratio); + // t3d_anim_update(&animation->blend.running_left, frame_time); + // + // t3d_skeleton_blend(&actor->armature.main, &actor->armature.main, &actor->armature.blend, animation->blending_ratio); + // } + // } + // else + t3d_anim_update(&animation->main.running_left, frame_time); +} + +void actorAnimation_setJump(Actor *actor, ActorAnimation *animation, const float frame_time, rspq_syncpoint_t *syncpoint) +{ + t3d_anim_update(&animation->main.falling_left, frame_time); +} + +void actor_setAnimation(Actor *actor, ActorAnimation *animation, const float frame_time, rspq_syncpoint_t *syncpoint) +{ + switch (actor->state) + { + + case STAND_IDLE: + { + actorAnimation_setStandIdle(actor, animation, frame_time, syncpoint); + if (animation->current != STAND_IDLE) + { + animation->previous = animation->current; + animation->current = STAND_IDLE; + } + break; + } + + case RUNNING: + { + actorAnimation_setRunning(actor, animation, frame_time, syncpoint); + if (animation->current != RUNNING) + { + animation->previous = animation->current; + animation->current = RUNNING; + } + break; + } + + case JUMP: + { + actorAnimation_setJump(actor, animation, frame_time, syncpoint); + break; + } + case FALLING: + { + actorAnimation_setJump(actor, animation, frame_time, syncpoint); + break; + } + case DEATH: + { + actorAnimation_setJump(actor, animation, frame_time, syncpoint); + break; + } + } + + /* + * The original engine only accounts for 1 actor, + * but looping through the actor update for each + * means that the syncpoint would be updated + * 4 times, which is 3 too many. + */ + // if(syncpoint)rspq_syncpoint_wait(*syncpoint); + // t3d_skeleton_update(&actor->armature.main); +} + +// temporary place for this until i solve the circular dependency +void actor_init(Actor *actor) +{ + actor->animation = actorAnimation_create(actor); + actorAnimation_init(actor, &actor->animation); +} + +void actor_update(Actor *actor, ControllerData *control, TimeData *timing, float camera_angle_around, float camera_offset, rspq_syncpoint_t *syncpoint) +{ + if (control != NULL) + actor_setControlData(actor, control, timing->fixed_time_s, camera_angle_around, camera_offset); + if (actor->previous_state != actor->state) + actor_setState(actor, actor->state); // Skip setting state if it hasn't changed + actor_setAnimation(actor, &actor->animation, timing->fixed_time_s, syncpoint); + actor_setMotion(actor, timing->fixed_time_s); +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/actor/actor_control.h b/code/sb_halcyon/actor/actor_control.h new file mode 100644 index 00000000..aa8d314d --- /dev/null +++ b/code/sb_halcyon/actor/actor_control.h @@ -0,0 +1,124 @@ +#ifndef ACTOR_CONTROLS_H +#define ACTOR_CONTROLS_H + +// function prototypes + +void actorControl_setJump(Actor *actor, ControllerData *control, float frame_time); +void actorControl_moveWithStick(Actor *actor, ControllerData *control, float camera_angle_around, float camera_offset); +void actor_setControlData(Actor *actor, ControllerData *control, float frame_time, float camera_angle_around, float camera_offset); + +// function implementations + +void actorControl_setJump(Actor *actor, ControllerData *control, float frame_time) +{ + bool canJump = false; + bool wantJump = false; + + switch (actor->state) + { + case FALLING: + if (control->held.a) + actor->input.jump_time_buffer += frame_time; + actor->input.jump_released = true; + actor->input.jump_hold = false; + canJump = false; + break; + case JUMP: + if (control->held.a) + actor->input.jump_time_held += frame_time; + actor->input.jump_time_buffer = 0; + canJump = false; + wantJump = false; + break; + case STAND_IDLE: + case RUNNING: + actor->input.jump_released = true; + actor->input.jump_hold = false; + canJump = true; + break; + } + + if (control->pressed.a) + wantJump = true; + if (actor->input.jump_time_buffer > 0.0f && actor->input.jump_time_buffer < 0.3f) + wantJump = true; + + if (wantJump && canJump) + { + + actor->body.velocity.z = 30; + actor->input.jump_hold = true; + actor->input.jump_released = false; + sound_wavPlay(SFX_JUMP, false); + actor_setState(actor, JUMP); + } + else + { + actor->input.jump_released = true; + actor->input.jump_hold = false; + } + + if (control->released.a) + { + actor->input.jump_time_buffer = 0; + } +} + +void actorControl_moveWithStick(Actor *actor, ControllerData *control, float camera_angle_around, float camera_offset) +{ + int deadzone = 3; + float stick_magnitude = 0; + + // Store previous camera angle and offset + static float prev_camera_angle = -1.0f; // Initialize with a value that will trigger the first calculation + static float prev_camera_offset = -1.0f; + + static float yaw = 0; + + // Check if the camera angle or offset has changed + bool camera_changed = fabsf(camera_angle_around - prev_camera_angle) > 0.001f || fabsf(camera_offset - prev_camera_offset) > 0.001f; + + do + { + // Update the previous camera angle and offset values + prev_camera_angle = camera_angle_around; + prev_camera_offset = camera_offset; + + // Only change yaw if the camera angle or offset has changed + if (fabsf(control->input.stick_x) >= deadzone || fabsf(control->input.stick_y) >= deadzone) + { + yaw = deg(fm_atan2f(control->input.stick_x, -control->input.stick_y) - rad(camera_angle_around - (0.5 * camera_offset))); + } + break; + + } while (camera_changed); + + actor->target_yaw = yaw; + + if (fabsf(control->input.stick_x) >= deadzone || fabsf(control->input.stick_y) >= deadzone) + { + Vector2 stick = {control->input.stick_x, control->input.stick_y}; + stick_magnitude = vector2_magnitude(&stick); + actor->horizontal_target_speed = stick_magnitude * 7; + } + + if (stick_magnitude == 0 && actor->state != JUMP && actor->state != FALLING) + { + actor->state = STAND_IDLE; + } + + else if (stick_magnitude > 0 && actor->state != JUMP && actor->state != FALLING) + { + actor->state = RUNNING; + } +} + +void actor_setControlData(Actor *actor, ControllerData *control, float frame_time, float camera_angle_around, float camera_offset) +{ + + actorControl_setJump(actor, control, frame_time); + + actorControl_moveWithStick(actor, control, camera_angle_around, camera_offset); +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/actor/actor_motion.h b/code/sb_halcyon/actor/actor_motion.h new file mode 100644 index 00000000..d1a730ee --- /dev/null +++ b/code/sb_halcyon/actor/actor_motion.h @@ -0,0 +1,187 @@ +#ifndef ACTOR_MOVEMENT_H +#define ACTOR_MOVEMENT_H + +#define ACTOR_GRAVITY -2250 + +// function prototypes + +void actorMotion_setHorizontalAcceleration(Actor *actor, float target_speed, float acceleration_rate); + +void actorMotion_setHorizontalInertiaAcceleration(Actor *actor, float target_speed, float acceleration_rate); + +void actorMotion_setStopingAcceleration(Actor *actor); + +void actorMotion_setJumpAcceleration(Actor *actor, float target_speed, float acceleration_rate); + +void actorMotion_integrate(Actor *actor, float frame_time); + +void actorMotion_setHorizontalAcceleration(Actor *actor, float target_speed, float acceleration_rate) +{ + actor->target_velocity.x = target_speed * fm_sinf(rad(actor->target_yaw)); + actor->target_velocity.y = target_speed * -fm_cosf(rad(actor->target_yaw)); + + actor->body.acceleration.x = acceleration_rate * (actor->target_velocity.x - actor->body.velocity.x); + actor->body.acceleration.y = acceleration_rate * (actor->target_velocity.y - actor->body.velocity.y); +} + +void actorMotion_setHorizontalInertiaAcceleration(Actor *actor, float target_speed, float acceleration_rate) +{ + actor->target_velocity.x = target_speed * fm_sinf(rad(actor->body.rotation.z)); + actor->target_velocity.y = target_speed * -fm_cosf(rad(actor->body.rotation.z)); + + actor->body.acceleration.x = acceleration_rate * (actor->target_velocity.x - actor->body.velocity.x); + actor->body.acceleration.y = acceleration_rate * (actor->target_velocity.y - actor->body.velocity.y); +} + +void actorMotion_setStopingAcceleration(Actor *actor) +{ + actor->body.acceleration.x = actor->settings.idle_acceleration_rate * (0 - actor->body.velocity.x); + actor->body.acceleration.y = actor->settings.idle_acceleration_rate * (0 - actor->body.velocity.y); +} + +void actorMotion_setJumpAcceleration(Actor *actor, float target_speed, float acceleration_rate) +{ + actor->body.acceleration.z = acceleration_rate * (target_speed - actor->body.velocity.z); +} + +void actorMotion_integrate(Actor *actor, float frame_time) +{ + + if (actor->body.acceleration.x != 0 || actor->body.acceleration.y != 0 || actor->body.acceleration.z != 0) + { + vector3_addScaledVector(&actor->body.velocity, &actor->body.acceleration, frame_time); + } + + if (fabsf(actor->body.velocity.x) < 10.0f && fabsf(actor->body.velocity.y) < 10.0f) + { + actor->body.velocity.x = 0; + actor->body.velocity.y = 0; + } + + if (actor->body.velocity.x != 0 || actor->body.velocity.y != 0 || actor->body.velocity.z != 0) + { + vector3_addScaledVector(&actor->body.position, &actor->body.velocity, frame_time); + if (actor->body.velocity.z < actor->settings.fall_max_speed) + actor->body.velocity.z = actor->settings.fall_max_speed; + if (actor->body.velocity.z > actor->settings.jump_max_speed) + actor->body.velocity.z = actor->settings.jump_max_speed; + if (actor->body.position.z > actor->settings.jump_max_height) + { + actor->body.velocity.z -= 10.0f; + } + } + + if (actor->body.velocity.x != 0 || actor->body.velocity.y != 0) + { + + actor->body.rotation.z = deg(fm_atan2f(-actor->body.velocity.x, -actor->body.velocity.y)); + + Vector2 horizontal_velocity = {actor->body.velocity.x, actor->body.velocity.y}; + actor->horizontal_speed = vector2_magnitude(&horizontal_velocity); + } +} + +void actorMotion_setIdle(Actor *actor) +{ + actorMotion_setStopingAcceleration(actor); + + if (fabsf(actor->body.velocity.x) < 1.0f && fabsf(actor->body.velocity.y) < 1.0f) + { + + vector3_init(&actor->body.velocity); + actor->horizontal_speed = 0; + actor->target_yaw = actor->body.rotation.z; + } +} + +void actorMotion_setRunning(Actor *actor) +{ + actorMotion_setHorizontalAcceleration(actor, actor->horizontal_target_speed, actor->settings.run_acceleration_rate); + // actorMotion_setHorizontalAcceleration (actor, actor->settings.run_target_speed, actor->settings.run_acceleration_rate); +} + +void actorMotion_setJump(Actor *actor) +{ + if (actor->input.jump_hold && !actor->input.jump_released && actor->input.jump_time_held < actor->settings.jump_timer_max) + { + // Scale horizontal boost based on how long the jump button is held + float hold_factor = (float)actor->input.jump_time_held / actor->settings.jump_timer_max; + float boosted_speed = actor->horizontal_speed + (actor->settings.jump_horizontal_boost * hold_factor); + + // Set horizontal acceleration to the boosted speed + actorMotion_setHorizontalAcceleration(actor, boosted_speed, actor->settings.aerial_control_rate); + + // Maintain vertical acceleration for the jump + actorMotion_setJumpAcceleration(actor, actor->settings.jump_target_speed, actor->settings.jump_acceleration_rate); + } + else if (actor->body.velocity.z > 0) + { + + // Scale horizontal boost based on how long the jump button is held + float hold_factor = (float)actor->input.jump_time_held / actor->settings.jump_timer_max; + float degraded_speed = actor->horizontal_speed - (hold_factor * 0.5f); + + // Maintain aerial control while falling + actorMotion_setHorizontalAcceleration(actor, degraded_speed, actor->settings.aerial_control_rate); + actor->body.acceleration.z = ACTOR_GRAVITY; + } + else + { + // Transition to falling state + actor->state = FALLING; + actor->input.jump_time_held = 0; + return; + } +} + +void actorMotion_setFalling(Actor *actor) +{ + actor->grounded = 0; + actorMotion_setHorizontalAcceleration(actor, actor->horizontal_speed, actor->settings.aerial_control_rate); + actor->body.acceleration.z = ACTOR_GRAVITY; + + if (actor->body.position.z <= actor->grounding_height) + { + + actor->grounded = 1; + actor->body.acceleration.z = 0; + actor->body.velocity.z = 0; + actor->body.position.z = actor->grounding_height; + + actor->state = STAND_IDLE; + + return; + } +} + +void actor_setMotion(Actor *actor, float frame_time) +{ + switch (actor->state) + { + + case STAND_IDLE: + { + actorMotion_setIdle(actor); + break; + } + case RUNNING: + { + actorMotion_setRunning(actor); + break; + } + case JUMP: + { + actorMotion_setJump(actor); + break; + } + case FALLING: + { + actorMotion_setFalling(actor); + break; + } + } + + actorMotion_integrate(actor, frame_time); +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/actor/actor_states.h b/code/sb_halcyon/actor/actor_states.h new file mode 100644 index 00000000..ad855929 --- /dev/null +++ b/code/sb_halcyon/actor/actor_states.h @@ -0,0 +1,111 @@ +#ifndef ACTORSTATES_H +#define ACTORSTATES_H + +#define STAND_IDLE 1 +#define RUNNING 3 +#define JUMP 6 +#define FALLING 7 +#define LANDING 8 +#define DEATH 9 + +// function prototypes + +void actorState_setIdle(Actor *actor); + +void actorState_setRunning(Actor *actor); + +void actorState_setJump(Actor *actor); + +void actorState_setFalling(Actor *actor); + +void actorState_setDeath(Actor *actor); + +void actor_setState(Actor *actor, uint8_t state); + +void actorState_setIdle(Actor *actor) +{ + if (actor->state == STAND_IDLE) + return; + if (actor->previous_state != FALLING && actor->state != JUMP) + actor->previous_state = actor->state; + actor->state = STAND_IDLE; + actor->locomotion_state = STAND_IDLE; +} + +void actorState_setRunning(Actor *actor) +{ + if (actor->state == RUNNING) + return; + if (actor->previous_state != FALLING && actor->state != JUMP) + actor->previous_state = actor->state; + actor->state = RUNNING; + actor->locomotion_state = RUNNING; +} + +void actorState_setJump(Actor *actor) +{ + if (actor->state == JUMP) + return; + + if (actor->previous_state != FALLING && actor->state != JUMP) + actor->previous_state = actor->state; + actor->state = JUMP; + actor->grounded = 0; + actor->grounding_height = 0.0f; +} + +void actorState_setFalling(Actor *actor) +{ + if (actor->state == FALLING) + return; + + if (actor->state != FALLING && actor->state != JUMP) + actor->previous_state = actor->state; + actor->state = FALLING; + actor->grounding_height = -0.0f; +} + +void actorState_setDeath(Actor *actor) +{ + if (actor->state == DEATH) + return; + + actor->previous_state = actor->state; + actor->grounding_height = -50.0f; + actor->state = DEATH; +} + +void actor_setState(Actor *actor, uint8_t state) +{ + switch (state) + { + + case STAND_IDLE: + { + actorState_setIdle(actor); + break; + } + case RUNNING: + { + actorState_setRunning(actor); + break; + } + case JUMP: + { + actorState_setJump(actor); + break; + } + case FALLING: + { + actorState_setFalling(actor); + break; + } + case DEATH: + { + actorState_setDeath(actor); + break; + } + } +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/actor/collision/actor_collision_detection.h b/code/sb_halcyon/actor/collision/actor_collision_detection.h new file mode 100644 index 00000000..23c503db --- /dev/null +++ b/code/sb_halcyon/actor/collision/actor_collision_detection.h @@ -0,0 +1,163 @@ +#ifndef ACTOR_COLLISION_H +#define ACTOR_COLLISION_H + +// collision types + +/* +#define SPHERE 1 +#define AABB 2 +#define BOX 3 +#define PLANE 4 +#define RAY 5 +#define CAPSULE 6 +#define TERRAIN 7 +#define MESH 8 +*/ + +// structures + +typedef struct +{ + float body_radius; + float body_height; + // float sword_radius; + // float sword_lenght; + // float shield_radius; +} ActorColliderSettings; + +typedef struct +{ + Capsule body; + // Capsule sword; + // Sphere shield; + ActorColliderSettings settings; +} ActorCollider; + +typedef struct +{ + Vector3 axis_closest_to_point; // closest point in the capsule axis to the point of contact + Vector3 velocity_penetration; // penetration vector in the direction of the velocity + float slope; // angle of inclination of the the plane of contact + float angle_of_incidence; // angle between the velocity vector and the plane of contact + float displacement; // distance from the origin to the plane of contact + float ground_distance; // vertical distance from the actor's position to the nearest plane of contact + ContactData data; +} ActorContactData; + +void actorCollider_init(ActorCollider *collider) +{ + collider->body.radius = collider->settings.body_radius; + collider->body.length = collider->settings.body_height; +} + +void actorCollider_setVertical(ActorCollider *collider, Vector3 *position) +{ + capsule_setVertical(&collider->body, position); +} + +void actorCollider_set(ActorCollider *collider, Vector3 *position, Vector3 *rotation) +{ +} + +void actorContactData_clear(ActorContactData *contact) +{ + contact->axis_closest_to_point = (Vector3){0.0f, 0.0f, 0.0f}; + contact->velocity_penetration = (Vector3){0.0f, 0.0f, 0.0f}; + contact->slope = 1000.0f; // Set the slope to an out of range value to indicate no contact + contact->displacement = 0.0f; + contact->ground_distance = 1000.0f; + contactData_init(&contact->data); +} + +void actorContactData_setAxisClosestToPoint(ActorContactData *contact, const ActorCollider *collider) +{ + contact->axis_closest_to_point = segment_closestToPoint(&collider->body.start, &collider->body.end, &contact->data.point); +} + +void actorContactData_setSlope(ActorContactData *contact) +{ + float magnitude = vector3_magnitude(&contact->data.normal); + if (magnitude > 0) + { + float cos_slope = contact->data.normal.z / magnitude; // Calculate the cosine of the angle between the normal and the z-axis + float slope = acosf(cos_slope); + contact->slope = deg(slope); + } +} + +void actorContactData_setAngleOfIncidence(ActorContactData *contact, const Vector3 *velocity) +{ + contact->angle_of_incidence = -deg((M_PI * 0.5f) - acosf(vector3_returnDotProduct(velocity, &contact->data.normal) / vector3_magnitude(velocity))); +} + +void actorContactData_setDisplacement(ActorContactData *contact) +{ + contact->displacement = vector3_returnDotProduct(&contact->data.point, &contact->data.normal); +} + +void actorCollision_setGroundDistance(ActorContactData *contact, Vector3 *position) +{ + if (contact->data.normal.z == 0.0f) + contact->ground_distance = 1000.0; // arbitrary large value to indicate no grounding + else + contact->ground_distance = (contact->displacement - vector3_returnDotProduct(position, &contact->data.normal)) / -contact->data.normal.z; +} + +bool actorCollision_contactSphere(const ActorCollider *collider, const Sphere *sphere) +{ + return capsule_contactSphere(&collider->body, sphere); +} + +void actorCollision_contactSphereSetData(ActorContactData *contact, const ActorCollider *collider, const Sphere *sphere) +{ + capsule_contactSphereSetData(&contact->data, &collider->body, sphere); + actorContactData_setSlope(contact); + actorContactData_setDisplacement(contact); + actorContactData_setAxisClosestToPoint(contact, collider); +} + +bool actorCollision_contactAABB(const ActorCollider *collider, const AABB *aabb) +{ + return capsule_contactAABB(&collider->body, aabb); +} + +void actorCollision_contactAABBsetData(ActorContactData *contact, const ActorCollider *collider, const AABB *aabb) +{ + capsule_contactAABBSetData(&contact->data, &collider->body, aabb); + actorContactData_setSlope(contact); + actorContactData_setDisplacement(contact); + actorContactData_setAxisClosestToPoint(contact, collider); +} + +bool actorCollision_contactBox(const ActorCollider *collider, const Box *box) +{ + return capsule_contactBox(&collider->body, box); +} + +void actorCollision_contactBoxSetData(ActorContactData *contact, const ActorCollider *collider, const Box *box) +{ + capsule_contactBoxSetData(&contact->data, &collider->body, box); + actorContactData_setSlope(contact); + actorContactData_setDisplacement(contact); + actorContactData_setAxisClosestToPoint(contact, collider); +} + +bool actorCollision_contactPlane(const ActorCollider *collider, const Plane *plane) +{ + return capsule_contactPlane(&collider->body, plane); +} + +void actorCollision_contactPlaneSetData(ActorContactData *contact, const ActorCollider *collider, const Plane *plane) +{ + capsule_contactPlaneSetData(&contact->data, &collider->body, plane); + actorContactData_setSlope(contact); + contact->displacement = plane->displacement; + actorContactData_setAxisClosestToPoint(contact, collider); +} + +bool actorCollision_intersectionRay(const ActorCollider *collider, const Ray *ray) +{ + return capsule_intersectionRay(&collider->body, ray); +} + +#endif diff --git a/code/sb_halcyon/actor/collision/actor_collision_response.h b/code/sb_halcyon/actor/collision/actor_collision_response.h new file mode 100644 index 00000000..3cae6102 --- /dev/null +++ b/code/sb_halcyon/actor/collision/actor_collision_response.h @@ -0,0 +1,251 @@ +#ifndef ACTOR_COLLISION_RESPONSE_H +#define ACTOR_COLLISION_RESPONSE_H + +void actorCollision_pushTowardsNormal(Actor *actor, ActorContactData *contact) +{ + // Calculate the necessary displacement vector in the direction of the contact normal + Vector3 displacement_vector = vector3_returnScaled(&contact->data.normal, -contact->data.penetration); + + // Apply the displacement to the actor's position + vector3_subtract(&actor->body.position, &displacement_vector); +} + +// lighter solution to use together with the push towards normal function. gives almost same results, for now i will use the correct algorithm +void actorCollision_projectAcceleration(Actor *actor, ActorContactData *contact) +{ + float t = vector3_returnDotProduct(&actor->body.acceleration, &contact->data.normal); + vector3_addScaledVector(&actor->body.acceleration, &contact->data.normal, -t); +} + +void actorCollision_projectVelocity(Actor *actor, ActorContactData *contact) +{ + float t = vector3_returnDotProduct(&actor->body.velocity, &contact->data.normal); + vector3_addScaledVector(&actor->body.velocity, &contact->data.normal, -t); +} + +void actorCollision_solvePenetration(Actor *actor, ActorContactData *contact, ActorCollider *collider) +{ + // Normalize the actor's velocity vector + Vector3 velocity_normal = vector3_returnNormalized(&actor->body.velocity); + + // Calculate the intersection of the ray (contact point + velocity normal) with the plane + float denominator = vector3_returnDotProduct(&velocity_normal, &contact->data.normal); + float numerator = contact->displacement + collider->body.radius - vector3_returnDotProduct(&contact->data.point, &contact->data.normal); + + float t; + if (fabsf(denominator) > 0.0001f) + t = numerator / denominator; + else + return; + + Vector3 axis_closest_at_contact = contact->data.point; + vector3_addScaledVector(&axis_closest_at_contact, &velocity_normal, t); + + Vector3 displacement_vector = axis_closest_at_contact; + vector3_subtract(&displacement_vector, &contact->axis_closest_to_point); + + contact->velocity_penetration = displacement_vector; + vector3_invert(&contact->velocity_penetration); + + vector3_add(&actor->body.position, &displacement_vector); +} + +void actorCollision_collideAndSlide(Actor *actor, ActorContactData *contact) +{ + float t = vector3_returnDotProduct(&contact->velocity_penetration, &contact->data.normal); + Vector3 projection = contact->velocity_penetration; + vector3_addScaledVector(&projection, &contact->data.normal, -t); + + vector3_add(&actor->body.position, &projection); +} + +void actorCollision_setGroundResponse(Actor *actor, ActorContactData *contact, ActorCollider *collider) +{ + actorCollision_pushTowardsNormal(actor, contact); + actor->grounded = true; + actor->body.acceleration.z = 0; + actor->body.velocity.z = 0; + actor->grounding_height = actor->body.position.z + 2.0f; + actor->state = actor->previous_state; + + // Lower the ground height slightly when on a slope + if (contact->slope > 7.0f && contact->slope < 50.0f && contact->ground_distance > 0.1f) + { + float slope_offset = 0.1f * contact->slope; + actor->grounding_height -= slope_offset; + if (actor->body.velocity.x != 0) + actor->body.position.z = actor->grounding_height; + } +} + +void actorCollision_setCeilingResponse(Actor *actor, ActorContactData *contact) +{ + if (actor->body.velocity.z > 0) + { + vector3_scale(&actor->body.velocity, 1 - (contact->angle_of_incidence * 0.01)); // angle of incidence can be up to 90 degrees + actor->body.velocity = vector3_reflect(&actor->body.velocity, &contact->data.normal); + actor->body.velocity.z = 0.0f; + } + else + { + actor->body.velocity.x = 0.0f; + actor->body.velocity.y = 0.0f; + } + + // Possible fix for Sloped Ceilings forcing Player downwards + if (!(actor->grounded)) + { + actor->state = FALLING; + actor->hasCollided = false; + } +} + +void actorCollision_setResponse(Actor *actor, ActorContactData *contact, ActorCollider *collider) +{ + actorContactData_setAngleOfIncidence(contact, &actor->body.velocity); + actorCollision_solvePenetration(actor, contact, collider); + + if (contact->slope > 0 && contact->slope < 50) + { + actorCollision_setGroundResponse(actor, contact, collider); + actorCollision_collideAndSlide(actor, contact); + } + else if (contact->slope > 95 && actor->grounded == false) + { + actorCollision_collideAndSlide(actor, contact); + actorCollision_setCeilingResponse(actor, contact); + } + else + { + actorCollision_setGroundResponse(actor, contact, collider); + actorCollision_collideAndSlide(actor, contact); + } + + actorCollider_setVertical(collider, &actor->body.position); +} + +void actorCollision_updateFalling(Actor *actor, ActorContactData *actor_contact, ActorCollider *actor_collider) +{ + actorContactData_clear(actor_contact); + actorCollider_setVertical(actor_collider, &actor->body.position); + + // Check if the actor is neither jumping nor falling + if (actor->body.position.z != LOWER_LIMIT_HEIGHT && actor->state != JUMP && actor->state != FALLING && actor->hasCollided == false) + { + + actor->state = FALLING; + actor->grounded = false; + actor->grounding_height = LOWER_LIMIT_HEIGHT; + } +} + +void actorCollision_collidePlatforms(Actor *actor, ActorContactData *actor_contact, ActorCollider *actor_collider, Platform *platforms) +{ + + actorCollision_updateFalling(actor, actor_contact, actor_collider); + + // Calculate the grid cell the actor is in + int xCell = (int)fm_floorf((actor->body.position.x + 775) / 350); + int yCell = (int)fm_floorf((actor->body.position.y + 775) / 350); + + if (xCell < 0 || xCell >= 7 || yCell < 0 || yCell >= 7) + { + // Actor is out of bounds; fall and skip collision + actor->state = FALLING; + actor->grounded = false; + actor->grounding_height = LOWER_LIMIT_HEIGHT; + if (actor->body.position.z <= DEATH_PLANE_HEIGHT) + actorState_setDeath(actor); + return; + } + + const float collisionRangeSq = 175.0f * 175.0f; + + // Reset actor's collision state + actor->hasCollided = false; + + // Iterate through platforms in the same and adjacent cells + for (int dx = -1; dx <= 1; dx++) + { + for (int dy = -1; dy <= 1; dy++) + { + int nx = xCell + dx; + int ny = yCell + dy; + + if (nx < 0 || nx >= 7 || ny < 0 || ny >= 7) + { + // Actor is out of bounds; fall and skip collision + actor->state = FALLING; + actor->grounded = false; + actor->grounding_height = LOWER_LIMIT_HEIGHT; + if (actor->body.position.z <= DEATH_PLANE_HEIGHT) + actorState_setDeath(actor); + continue; + } + + PlatformGridCell *cell = &platformGrid[nx][ny]; + for (size_t i = 0; i < cell->count; i++) + { + Platform *platform = &platforms[cell->platformIndices[i]]; + fm_vec3_t actorPos = Vector3_to_fast(actor->body.position); + fm_vec3_t platformPos = Vector3_to_fast(platform->position); + float distanceSq = fm_vec3_distance2(&actorPos, &platformPos); + if (distanceSq <= collisionRangeSq) + { + + // Check collision with each box in the platform's collider + for (int j = 0; j < 3; j++) + { + Box *box = &platform->collider.box[j]; + + // If the actor hits a box + if (actorCollision_contactBox(actor_collider, box)) + { + // Set collision response + actorCollision_contactBoxSetData(actor_contact, actor_collider, box); + actorCollision_collideAndSlide(actor, actor_contact); + actorCollision_setGroundResponse(actor, actor_contact, actor_collider); + + // If the actor is lower the top of the box (center.z+(size.z/2)), move there + if (actor->body.position.z < box->center.z + (box->size.z * 0.5f)) + actor->body.position.z = box->center.z + (box->size.z * 0.5f); + + // Set collided state parameter + actor->hasCollided = true; + + // Handle platform collision here instead again for the platforms + platform->contact = true; + platform->colorID = actor->colorID; + + switch (actor->colorID) + { + case 0: + platform->color = PLAYERCOLOR_1; + break; + case 1: + platform->color = PLAYERCOLOR_2; + break; + case 2: + platform->color = PLAYERCOLOR_3; + break; + case 3: + platform->color = PLAYERCOLOR_4; + break; + } + + return; // Early exit if collision is detected + } + } + } + } + } + } + + // Call setState after processing collision responses + actor_setState(actor, actor->state); + + if (actor->body.position.z <= DEATH_PLANE_HEIGHT) + actorState_setDeath(actor); +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/camera/camera.h b/code/sb_halcyon/camera/camera.h new file mode 100644 index 00000000..0c4e91c0 --- /dev/null +++ b/code/sb_halcyon/camera/camera.h @@ -0,0 +1,194 @@ +#ifndef CAMERA_H +#define CAMERA_H + +#include "light.h" + +// structures + +typedef enum +{ + ORBITAL, + AIMING, + MINIGAME, + +} CameraState; + +typedef struct +{ + + float orbitational_acceleration_rate; + Vector2 orbitational_max_velocity; + + float zoom_acceleration_rate; + float zoom_deceleration_rate; + float zoom_max_speed; + + float distance_from_baricenter; + + float field_of_view; + float field_of_view_aim; + + float offset_acceleration_rate; + float offset_deceleration_rate; + float offset_max_speed; + + float offset_angle; + float offset_angle_aim; + + float max_pitch; + +} CameraSettings; + +typedef struct +{ + + T3DViewport viewport; + + Vector3 position; + float offset_height; + + float distance_from_barycenter; // the barycenter is choosen and it's the center of the orbitational movement + float angle_around_barycenter; + float pitch; + + float horizontal_barycenter_distance; + float vertical_barycenter_distance; + + Vector3 target; // target as in the place at which the camera must aim + float target_distance; + float horizontal_target_distance; + float vertical_target_distance; + + Vector2 orbitational_acceleration; + Vector2 orbitational_velocity; + Vector2 orbitational_target_velocity; // target as in intended velocity + + float offset_angle; + + float offset_acceleration; + float offset_speed; + int offset_direction; + + float field_of_view; + + float zoom_acceleration; + float zoom_speed; + int zoom_direction; + + float near_clipping; + float far_clipping; + + uint8_t cam_mode; + float lerpTime; + float camTime; + + CameraSettings settings; +} Camera; + +// functions prototypes + +Camera camera_create(); +void camera_getOrbitalPosition(Camera *camera, Vector3 barycenter, float frame_time); +void camera_set(Camera *camera, Screen *screen); + +// function implementations + +Camera camera_create() +{ + Camera camera = { + .position = (Vector3){0, -1200, 1200}, + .distance_from_barycenter = 500, + .target_distance = 200, + .angle_around_barycenter = 0, + .pitch = 25, + .offset_angle = 0, + .offset_height = 250, + .field_of_view = 80, + .near_clipping = 100, + .far_clipping = 3000, + .cam_mode = 0, + .lerpTime = 6.1f, + .camTime = 0, + }; + + return camera; +} + +void camera_getOrbitalPosition(Camera *camera, Vector3 barycenter, float frame_time) +{ + camera->orbitational_velocity.x += camera->orbitational_acceleration.x * frame_time; + camera->orbitational_velocity.y += camera->orbitational_acceleration.y * frame_time; + camera->zoom_speed += camera->zoom_acceleration * frame_time; + camera->offset_speed += camera->offset_acceleration * frame_time; + + if (fabsf(camera->orbitational_velocity.x) < 1.0f && fabsf(camera->orbitational_velocity.y) < 1.0f && fabsf(camera->zoom_speed) < 1.0f && fabsf(camera->offset_speed) < 1.0f) + { + camera->orbitational_velocity.x = 0; + camera->orbitational_velocity.y = 0; + camera->zoom_speed = 0; + camera->offset_speed = 0; + } + + camera->pitch += camera->orbitational_velocity.x * frame_time; + camera->angle_around_barycenter += camera->orbitational_velocity.y * frame_time; + + camera->field_of_view += camera->zoom_direction * camera->zoom_speed * frame_time; + camera->offset_angle += camera->offset_direction * camera->offset_speed * frame_time; + + if (camera->angle_around_barycenter > 360) + camera->angle_around_barycenter -= 360; + if (camera->angle_around_barycenter < 0) + camera->angle_around_barycenter += 360; + + if (camera->pitch > camera->settings.max_pitch) + camera->pitch = camera->settings.max_pitch; + if (camera->pitch < -camera->settings.max_pitch + 30) + camera->pitch = -camera->settings.max_pitch + 30; // this hard coded + 20 is for the near plane to not enter the actor geometry during "camera collision" + + camera->horizontal_barycenter_distance = camera->distance_from_barycenter * fm_cosf(rad(camera->pitch)); + camera->vertical_barycenter_distance = camera->distance_from_barycenter * fm_sinf(rad(camera->pitch)); + + camera->horizontal_target_distance = camera->target_distance * fm_cosf(rad(camera->pitch)); + camera->vertical_target_distance = camera->target_distance * fm_sinf(rad(camera->pitch + 180)); + + camera->position.x = barycenter.x - (camera->horizontal_barycenter_distance * fm_sinf(rad(camera->angle_around_barycenter - camera->offset_angle))); + camera->position.y = barycenter.y - (camera->horizontal_barycenter_distance * fm_cosf(rad(camera->angle_around_barycenter - camera->offset_angle))); + camera->position.z = barycenter.z + camera->offset_height + camera->vertical_barycenter_distance; + + /* this is a temporary brute force abomination to "collide" the camera with an horizontal plane at height 20 simulating the floor, + will be modyfied when camera collision happens */ + /* + */ + camera->distance_from_barycenter = camera->settings.distance_from_baricenter; + while (camera->position.z < 30) + { + camera->distance_from_barycenter--; + camera->horizontal_barycenter_distance = camera->distance_from_barycenter * fm_cosf(rad(camera->pitch)); + camera->vertical_barycenter_distance = camera->distance_from_barycenter * fm_sinf(rad(camera->pitch)); + + camera->position.x = barycenter.x - camera->horizontal_barycenter_distance * fm_sinf(rad(camera->angle_around_barycenter - camera->offset_angle)); + camera->position.y = barycenter.y - camera->horizontal_barycenter_distance * fm_cosf(rad(camera->angle_around_barycenter - camera->offset_angle)); + camera->position.z = barycenter.z + camera->offset_height + camera->vertical_barycenter_distance; + } + + camera->target.x = barycenter.x - camera->horizontal_target_distance * fm_sinf(rad(camera->angle_around_barycenter + 180)); + camera->target.y = barycenter.y - camera->horizontal_target_distance * fm_cosf(rad(camera->angle_around_barycenter + 180)); + camera->target.z = barycenter.z + camera->offset_height + camera->vertical_target_distance; +} + +void camera_set(Camera *camera, Screen *screen) +{ + t3d_viewport_set_projection( + &screen->gameplay_viewport, + T3D_DEG_TO_RAD(camera->field_of_view), + camera->near_clipping, + camera->far_clipping); + + t3d_viewport_look_at( + &screen->gameplay_viewport, + &(T3DVec3){{camera->position.x, camera->position.y, camera->position.z}}, + &(T3DVec3){{camera->target.x, camera->target.y, camera->target.z}}, + &(T3DVec3){{0, 0, 1}}); +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/camera/camera_control.h b/code/sb_halcyon/camera/camera_control.h new file mode 100644 index 00000000..6943304d --- /dev/null +++ b/code/sb_halcyon/camera/camera_control.h @@ -0,0 +1,119 @@ +#ifndef CAMERA_CONTROLS_H +#define CAMERA_CONTROLS_H + +/* CAMERA_CONTROLS.H +here are all the camera control related functions */ + +int input(int input); + +void camera_orbit_withStick(Camera *camera, ControllerData *data); +void camera_orbit_withCButtons(Camera *camera, ControllerData *data); +void camera_aim(Camera *camera, ControllerData *data); + +void cameraControl_setOrbitalMovement(Camera *camera, ControllerData *data); + +/* input + auxiliary function for 8 directional movement*/ + +int input(int input) +{ + if (input == 0) + { + return 0; + } + else + { + return 1; + } +} + +/* camera_move_stick +changes the camera variables depending on controller input*/ + +void camera_orbit_withStick(Camera *camera, ControllerData *data) +{ + int deadzone = 8; + float stick_x = 0; + float stick_y = 0; + + if (fabsf(data->input.stick_x) >= deadzone || fabsf(data->input.stick_y) >= deadzone) + { + stick_x = data->input.stick_x; + stick_y = data->input.stick_y; + } + + if (stick_x == 0 && stick_y == 0) + { + camera->orbitational_target_velocity.x = 0; + camera->orbitational_target_velocity.y = 0; + } + + else if (stick_x != 0 || stick_y != 0) + { + camera->orbitational_target_velocity.x = stick_y; + camera->orbitational_target_velocity.y = stick_x; + } +} + +void camera_orbit_withCButtons(Camera *camera, ControllerData *data) +{ + float input_x = 0; + float input_y = 0; + + if ((data->held.c_right) || (data->held.c_left) || (data->held.c_up) || (data->held.c_down)) + { + + input_x = input(data->held.c_right) - input(data->held.c_left); + input_y = input(data->held.c_up) - input(data->held.c_down); + } + + if (input_x == 0) + camera->orbitational_target_velocity.y = 0; + else + camera->orbitational_target_velocity.y = input_x * camera->settings.orbitational_max_velocity.y; + + if (input_y == 0) + camera->orbitational_target_velocity.x = 0; + else + camera->orbitational_target_velocity.x = input_y * camera->settings.orbitational_max_velocity.x; +} + +void cameraControl_freeCam(Camera *camera, ControllerData *data, float time) +{ + + const float speed = 500.0f; + + float input_x = 0; + float input_y = 0; + float input_z = 0; + + if ((data->held.c_right) || (data->held.c_left) || (data->held.c_up) || (data->held.c_down) || (data->held.a) || (data->held.b)) + { + + input_x = input(data->held.c_right) - input(data->held.c_left); + input_y = input(data->held.c_up) - input(data->held.c_down); + input_z = input(data->held.a) - input(data->held.b); + } + + camera->position.x += input_x * time * speed; + camera->position.y += input_y * time * speed; + camera->position.z += input_z * time * speed; +} + +void camera_aim(Camera *camera, ControllerData *data) +{ + if (data->held.z) + camera_setState(camera, AIMING); + else + camera_setState(camera, ORBITAL); +} + +void cameraControl_setOrbitalMovement(Camera *camera, ControllerData *data) +{ + // camera_orbit_withStick(camera, data_1); + camera_orbit_withCButtons(camera, data); + camera_setState(camera, MINIGAME); + // camera_aim(camera, data); +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/camera/camera_states.h b/code/sb_halcyon/camera/camera_states.h new file mode 100644 index 00000000..2687132d --- /dev/null +++ b/code/sb_halcyon/camera/camera_states.h @@ -0,0 +1,86 @@ +#ifndef CAMERA_STATES_H +#define CAMERA_STATES_H + +// function prototypes + +void cameraState_setOrbital(Camera *camera); +void cameraState_setAiming(Camera *camera); +void camera_setState(Camera *camera, CameraState new_state); + +// function implemetations + +void cameraState_setOrbital(Camera *camera) +{ + if (camera->field_of_view < camera->settings.field_of_view) + camera->zoom_acceleration = camera->settings.zoom_acceleration_rate * (camera->settings.zoom_max_speed - camera->zoom_speed); + + else + camera->zoom_acceleration = (camera->settings.zoom_deceleration_rate + 10) * (0 - camera->zoom_speed); + + camera->zoom_direction = 1; + + if (camera->offset_angle > camera->settings.offset_angle) + camera->offset_acceleration = camera->settings.offset_acceleration_rate * (camera->settings.offset_max_speed - camera->offset_speed); + + else + camera->offset_acceleration = camera->settings.offset_deceleration_rate * (0 - camera->offset_speed); + + camera->offset_direction = -1; + + camera->orbitational_acceleration.x = camera->settings.orbitational_acceleration_rate * (camera->orbitational_target_velocity.x - camera->orbitational_velocity.x); + camera->orbitational_acceleration.y = camera->settings.orbitational_acceleration_rate * (camera->orbitational_target_velocity.y - camera->orbitational_velocity.y); +} + +// the set aiming not ready for it's prime yet :( +void cameraState_setAiming(Camera *camera) +{ + if (camera->field_of_view > camera->settings.field_of_view_aim) + camera->zoom_acceleration = (camera->settings.zoom_acceleration_rate + 10) * (camera->settings.zoom_max_speed - camera->zoom_speed); + + else + camera->zoom_acceleration = camera->settings.zoom_deceleration_rate * (0 - camera->zoom_speed); + + camera->zoom_direction = -1; + + if (camera->offset_angle < camera->settings.offset_angle_aim) + camera->offset_acceleration = camera->settings.offset_acceleration_rate * (camera->settings.offset_max_speed - camera->offset_speed); + + else + camera->offset_acceleration = camera->settings.offset_deceleration_rate * (0 - camera->offset_speed); + + camera->offset_direction = 1; + + camera->orbitational_acceleration.x = camera->settings.orbitational_acceleration_rate * ((camera->orbitational_target_velocity.x / 2) - camera->orbitational_velocity.x); + camera->orbitational_acceleration.y = camera->settings.orbitational_acceleration_rate * ((camera->orbitational_target_velocity.y / 2) - camera->orbitational_velocity.y); +} + +void set_minigame(Camera *camera) +{ + camera->orbitational_acceleration.x = camera->settings.orbitational_acceleration_rate * (camera->orbitational_target_velocity.x - camera->orbitational_velocity.x); + camera->orbitational_acceleration.y = camera->settings.orbitational_acceleration_rate * (camera->orbitational_target_velocity.y - camera->orbitational_velocity.y); +} + +void camera_setState(Camera *camera, CameraState new_state) +{ + switch (new_state) + { + + case ORBITAL: + { + cameraState_setOrbital(camera); + break; + } + case AIMING: + { + cameraState_setAiming(camera); + break; + } + case MINIGAME: + { + set_minigame(camera); + break; + } + } +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/camera/light.h b/code/sb_halcyon/camera/light.h new file mode 100644 index 00000000..d75c8e55 --- /dev/null +++ b/code/sb_halcyon/camera/light.h @@ -0,0 +1,62 @@ +#ifndef LIGHT_H +#define LIGHT_H + +typedef struct +{ + + uint8_t ambient_color[4]; + uint8_t directional_color1[4]; + T3DVec3 direction1; + + uint8_t directional_color2[4]; + T3DVec3 direction2; + +} LightData; + +void light_set(LightData *light); +void light_setAmbient(LightData *light, uint8_t value); +void light_resetAmbient(LightData *light); + +LightData light_create() +{ + LightData light = { + .ambient_color = {245, 232, 196, 0xFF}, + .directional_color1 = {245, 232, 196, 0xFF}, + .direction1 = {{0.0f, -1.0f, 0.0f}}, + .directional_color2 = {208, 242, 242, 0xFF}, + .direction2 = {{0.0f, 100.0f, 0.0f}}, + }; + + t3d_vec3_norm(&light.direction1); + + return light; +} + +/* set light +temporary function until i learn how the lights work */ +void light_set(LightData *light) +{ + t3d_light_set_ambient(light->ambient_color); + t3d_light_set_directional(0, light->directional_color1, &light->direction1); + t3d_light_set_directional(1, light->directional_color2, &light->direction2); + t3d_light_set_count(2); +} + +void light_setAmbient(LightData *light, uint8_t value) +{ + for (size_t i = 0; i < 3; i++) + { + light->ambient_color[i] = value; + } + light_set(light); +} + +void light_resetAmbient(LightData *light) +{ + light->ambient_color[0] = 0x32; + light->ambient_color[1] = 0x20; + light->ambient_color[2] = 0x06; + light_set(light); +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/control/controls.h b/code/sb_halcyon/control/controls.h new file mode 100644 index 00000000..3c8a54a3 --- /dev/null +++ b/code/sb_halcyon/control/controls.h @@ -0,0 +1,108 @@ +#ifndef CONTROLS_H +#define CONTROLS_H + +typedef struct +{ + + joypad_buttons_t pressed; + joypad_buttons_t held; + joypad_buttons_t released; + joypad_inputs_t input; + + // RUMBLE + bool rumble_active; + bool has_rumbled; + uint8_t rumble_time; + +} ControllerData; + +void controllerData_getInputs(ControllerData *data, uint8_t port); +void controllerData_rumbleStart(ControllerData *data, uint8_t port); +void controllerData_rumbleStop(ControllerData *data, uint8_t port); +void controllerData_rumbleFrames(ControllerData *data, uint8_t port, uint8_t frames); + +void controllerData_getInputs(ControllerData *data, uint8_t port) +{ + data->pressed = joypad_get_buttons_pressed(port); + data->held = joypad_get_buttons_held(port); + data->released = joypad_get_buttons_released(port); + data->input = joypad_get_inputs(port); + + // Check if the rumble pak has been unplugged + if (!joypad_get_rumble_supported(port)) + controllerData_rumbleStop(data, port); +} + +/* RUMBLE */ + +// Set rumble to active state +void controllerData_rumbleStart(ControllerData *data, uint8_t port) +{ + joypad_set_rumble_active(port, true); + data->rumble_time = 0; + data->rumble_active = true; +} + +// Reset rumble to idle state +void controllerData_rumbleStop(ControllerData *data, uint8_t port) +{ + joypad_set_rumble_active(port, false); + data->rumble_time = 0; + data->rumble_active = false; +} + +// Rumble controller for n number of frames +void controllerData_rumbleFrames(ControllerData *data, uint8_t port, uint8_t frames) +{ + + if (data->rumble_active == false) + { + controllerData_rumbleStart(data, port); + } + + data->rumble_time++; + + if (data->rumble_time >= frames) + controllerData_rumbleStop(data, port); +} + +///// 8 WAY ///// + +#define DEAD_ZONE 50 +#define INPUT_DELAY 0.3f + +// Treats joystick inputs as digital pad buttons, for menu navigation +void controllerData_8way(ControllerData *data) +{ + static float input_time = 0; + const float current_time = display_get_delta_time(); + + input_time -= current_time; + + if (input_time <= 0.0f) + { + if (data->input.stick_y > DEAD_ZONE) + { + data->pressed.d_up = 1; + input_time = INPUT_DELAY; + } + else if (data->input.stick_y < -DEAD_ZONE) + { + data->pressed.d_down = 1; + input_time = INPUT_DELAY; + } + + if (data->input.stick_x > DEAD_ZONE) + { + data->pressed.d_right = 1; + input_time = INPUT_DELAY; + } + else if (data->input.stick_x < -DEAD_ZONE) + { + data->pressed.d_left = 1; + input_time = INPUT_DELAY; + } + } +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/game/game.h b/code/sb_halcyon/game/game.h new file mode 100644 index 00000000..20c8c480 --- /dev/null +++ b/code/sb_halcyon/game/game.h @@ -0,0 +1,62 @@ +#ifndef GAME_H +#define GAME_H + +enum GAME_STATES +{ + INTRO, + MAIN_MENU, + CHARACTER_SELECT, + GAMEPLAY, + PAUSE, + GAME_OVER +}; + +typedef struct +{ + + uint8_t state; + Screen screen; + TimeData timing; + rspq_syncpoint_t syncPoint; + int diff; + int8_t winTimer; + uint8_t winnerID; + uint8_t countdownTimer; + uint8_t humanCount; + uint8_t deadPool; + bool actorSet; + bool winnerSet; + Scene scene; + +} Game; + +void game_init(Game *game); + +void game_init(Game *game) +{ + + screen_initDisplay(&game->screen); + screen_initT3dViewport(&game->screen); + + t3d_init((T3DInitParams){}); + + // TPX + ptx_init(&cloudMist); + + time_init(&game->timing); + + scene_init(&game->scene); + + // + ui_init(); + sound_load(); + // + + game->countdownTimer = 150; // Oops, forget to set this + game->syncPoint = 0; + game->state = INTRO; + game->humanCount = core_get_playercount(); + game->deadPool = 0; +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/game/game_control.h b/code/sb_halcyon/game/game_control.h new file mode 100644 index 00000000..05bd4415 --- /dev/null +++ b/code/sb_halcyon/game/game_control.h @@ -0,0 +1,33 @@ +#ifndef GAME_CONTROLS_H +#define GAME_CONTROLS_H + +void game_setControlData(Game *game, Player *player) +{ + for (uint8_t i = 0; i < PLAYER_COUNT; i++) + { + + if (player[i].control.pressed.start) + { + + switch (game->state) + { + case PAUSE: + game->state = GAMEPLAY; + break; + case GAMEPLAY: + game->state = PAUSE; + break; + case INTRO: + game->state = MAIN_MENU; + break; + case MAIN_MENU: + game->state = CHARACTER_SELECT; + break; + case CHARACTER_SELECT: + break; + } + } + } +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/game/game_states.h b/code/sb_halcyon/game/game_states.h new file mode 100644 index 00000000..51532c67 --- /dev/null +++ b/code/sb_halcyon/game/game_states.h @@ -0,0 +1,944 @@ +#ifndef GAME_STATES_H +#define GAME_STATES_H + +// Comment out to disable RSPQ Profiling +// #define PROFILING + +#ifdef PROFILING +#include "rspq_profile.h" +static rspq_profile_data_t profile_data; +#endif + +// function prototypes + +void gameState_setIntro(Game *game, Player *player, Scenery *scenery); +void gameState_setMainMenu(Game *game, Player *player, Actor *actor, Scenery *scenery); +void gameState_setCS(Game *game, Player *player, Actor *actor, Scenery *scenery); + +void gameState_setGameplay(Game *game, Player *player, AI *ai, Actor *actor, Scenery *scenery, ActorCollider *actor_collider, ActorContactData *actor_contact); +void gameState_setPause(Game *game, Player *player, Actor *actor, Scenery *scenery); + +void gameState_setGameOver(); + +void game_play(Game *game, Player *player, AI *ai, Actor *actor, Scenery *scenery, ActorCollider *actor_collider, ActorContactData *actor_contact); + +void gameState_background(Game *game, int color) +{ + static float s0 = 0.0f, s1 = 16.0f; + float t0 = 0.0f, t1 = 16.0f; + float x0 = 0, y0 = 0; + float x1 = SCREEN_WIDTH, y1 = SCREEN_HEIGHT; + + // Scroll horizontally + float scrollSpeed = display_get_refresh_rate() * 0.1f; + float textureWidth = 32.0f; + + s0 += (game->timing.fixed_time_s * scrollSpeed); + s1 += (game->timing.fixed_time_s * scrollSpeed); + + if (s0 > textureWidth) + s0 = 0; + if (s1 > textureWidth) + s1 = 0; + + // Stretch/Squash vertically + float counter = 0; + counter++; + float stretchFactor = fm_sinf(counter * game->timing.fixed_time_s) * 0.25f + 1.0f; + float originalHeight = SCREEN_HEIGHT - 100; // @TODO: make height a argument + float scaledHeight = originalHeight * stretchFactor; + + y0 = SCREEN_HEIGHT - scaledHeight; + y1 = SCREEN_HEIGHT; + + ui_spriteDrawPanel(TILE2, sprite_clouds, color, x0, y0, x1, y1, s0, t0, s1, t1); // @TODO: make sprite a argument + + if (counter > 255.0f) + counter = 0; +} + +// new camera code //// + +void camera_getMinigamePosition(Camera *camera, Actor *actor, Player *player, Vector3 camera_distance) +{ + Vector3 camera_target; + + uint8_t average_count = 0; + + vector3_init(&camera->position); + vector3_init(&camera_target); + + for (uint8_t i = 0; i < ACTOR_COUNT; i++) + { + if (!player[i].died && player[i].isHuman) + { + + vector3_add(&camera_target, &actor[player[i].actor_id].body.position); + average_count++; + } + } + + if (average_count > 0) + vector3_divideByNumber(&camera_target, average_count); + camera_target.z = 200; + + if (camera_target.x != camera->target.x || camera_target.y != camera->target.y) + camera->target = vector3_lerp(&camera->target, &camera_target, 0.2f); + + camera->position = camera->target; + + vector3_add(&camera->position, &camera_distance); +} + +////////////////////// + +void gameState_setIntro(Game *game, Player *player, Scenery *scenery) +{ + + for (size_t j = 0; j < PLATFORM_COUNT; j++) + { + platform_loop(&hexagons[j], NULL, 0); + } + + move_cloud(scenery); + + // ======== Draw ======== // + + screen_clearDisplay(&game->screen); + screen_clearT3dViewport(&game->screen); + screen_applyColor_Depth(&game->screen, RGBA32(154, 181, 198, 0xFF), false); + + // BACKGROUND + gameState_background(game, WHITE); + + t3d_frame_start(); + + light_set(&game->scene.light); + + t3d_matrix_push_pos(1); + + scenery_draw(scenery); + + // light_setAmbient(&game->scene.light, 0xFF); + platform_drawBatch(); + light_resetAmbient(&game->scene.light); + + t3d_matrix_pop(1); + + game->syncPoint = rspq_syncpoint_new(); + + ui_intro(&player[0].control); + + if (player[0].control.held.r) + { + ui_fps(game->timing.frame_rate, 20.0f, 20.0f); + if (core_get_playercount() == 1) + ui_input_display(&player[0].control); + } + + rdpq_detach_show(); + sound_update(); +} + +void gameState_setMainMenu(Game *game, Player *player, Actor *actor, Scenery *scenery) +{ + move_cloud(scenery); + + // ======== Draw ======== // + + screen_clearDisplay(&game->screen); + screen_clearT3dViewport(&game->screen); + screen_applyColor_Depth(&game->screen, RGBA32(154, 181, 198, 0xFF), false); + + gameState_background(game, WHITE); + + t3d_frame_start(); + + light_set(&game->scene.light); + // Instead drawing a dark transparent texture over the scene, just change the light direction + game->scene.light.direction1 = (T3DVec3){{0, -1, 0}}; + + t3d_matrix_push_pos(1); + + scenery_draw(scenery); + + // light_setAmbient(&game->scene.light, 0xFF); + platform_drawBatch(); + light_resetAmbient(&game->scene.light); + + t3d_matrix_pop(1); + + game->syncPoint = rspq_syncpoint_new(); + + if (core_get_playercount() == 4) + { + if (player[0].control.pressed.b) + { + if (game->diff <= 1) + { + game->diff++; + } + else + { + game->diff = 0; + } + } + } + ui_main_menu(&player[0].control, game->diff); + if (player[0].control.held.r) + { + ui_fps(game->timing.frame_rate, 20.0f, 20.0f); + if (core_get_playercount() == 1) + ui_input_display(&player[0].control); + } + + rdpq_detach_show(); + sound_update(); + +#ifdef PROFILING + rspq_profile_next_frame(); + if (game->timing.frame_counter > 29) + { + rspq_profile_dump(); + rspq_profile_reset(); + game->timing.frame_counter = 0; + } + rspq_profile_get_data(&profile_data); +#endif // PROFILING +} + +void gameState_setCS(Game *game, Player *player, Actor *actor, Scenery *scenery) +{ + + static uint8_t activePlayer = 0; + const uint8_t totalPlayers = core_get_playercount(); + static uint8_t selectedCharacter[ACTOR_COUNT] = {0}; + static bool actorSelected[ACTOR_COUNT] = {false}; + + if (activePlayer >= 4) + { + game->state = GAMEPLAY; + return; + } + + controllerData_8way(&player[activePlayer].control); + + if (player[activePlayer].control.pressed.d_right) + { + uint8_t initialSelection = selectedCharacter[activePlayer]; + do + { + selectedCharacter[activePlayer] = (selectedCharacter[activePlayer] + 1) % ACTOR_COUNT; + } while (actorSelected[selectedCharacter[activePlayer]] && selectedCharacter[activePlayer] != initialSelection); + } + + if (player[activePlayer].control.pressed.d_left) + { + uint8_t initialSelection = selectedCharacter[activePlayer]; + do + { + selectedCharacter[activePlayer] = (selectedCharacter[activePlayer] - 1 + ACTOR_COUNT) % ACTOR_COUNT; + } while (actorSelected[selectedCharacter[activePlayer]] && selectedCharacter[activePlayer] != initialSelection); + } + + if (player[activePlayer].control.pressed.a) + { + // @TODO: More of a bandaid than a fix + // Bugfix: Ensure selected actor is next available one + if (!actorSelected[selectedCharacter[activePlayer]]) + { + uint8_t selectedActorId = selectedCharacter[activePlayer]; + player[activePlayer].actor_id = selectedActorId; + player[activePlayer].isHuman = true; + actorSelected[selectedActorId] = true; + + // Visual feedback for selecting actor + actor[selectedActorId].body.rotation.z = actor[selectedActorId].body.rotation.z + 180.0f; + + activePlayer++; + } + else + { + // Audio feedback for selecting unavailable actor + sound_wavPlay(SFX_JUMP, false); + } + + // Automatically assign actors to AI players + if (activePlayer >= totalPlayers) + { + for (uint8_t i = totalPlayers; i < 4; i++) // AI players start after human players + { + for (uint8_t j = 0; j < ACTOR_COUNT; j++) + { + if (!actorSelected[j]) // Assign the first unselected actor + { + player[i].actor_id = j; + player[i].isHuman = false; + actorSelected[j] = true; + break; + } + } + } + activePlayer = 4; // Lock selection + } + + if (activePlayer >= 4) + { + game->state = GAMEPLAY; + return; + } + } + + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + actor_update(&actor[i], NULL, &game->timing, game->scene.camera.angle_around_barycenter, game->scene.camera.offset_angle, &game->syncPoint); + + // Reset non-selected actors + if (!actorSelected[i]) + { + actor[i].body.position = actor[i].home; + actor[i].body.rotation.z = 0; + } + + actor_updateMat(&actor[i]); + } + + // Sync RSPQ once, and then update each actors' skeleton + if (game->syncPoint) + rspq_syncpoint_wait(game->syncPoint); + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + t3d_skeleton_update(&actor[i].armature.main); + } + + move_cloud(scenery); + + // ======== Draw ======== // + + screen_clearDisplay(&game->screen); + screen_clearT3dViewport(&game->screen); + screen_applyColor_Depth(&game->screen, RGBA32(154, 181, 198, 0xFF), false); + + gameState_background(game, WHITE); + + t3d_frame_start(); + + light_set(&game->scene.light); + + // Change light direction to illuminate players + game->scene.light.direction1 = (T3DVec3){{0, -1, 0}}; + // game->scene.light.directional_color1[0] = 200; + // game->scene.light.directional_color1[1] = 200; + // game->scene.light.directional_color1[2] = 200; + + t3d_matrix_push_pos(1); + + scenery_draw(scenery); + + // light_setAmbient(&game->scene.light, 0xBF); + platform_drawBatch(); + light_resetAmbient(&game->scene.light); + + actor_draw(actor); + + t3d_matrix_pop(1); + + game->syncPoint = rspq_syncpoint_new(); + + if (activePlayer < MAXPLAYERS) + { + player[activePlayer].position.x = (actor[selectedCharacter[activePlayer]].body.position.x * 3.6f) - 30.0f; + player[activePlayer].position.z = 125.0f; + ui_print_playerNum(&player[activePlayer], &game->screen); + } + + ui_character_select(&player[activePlayer].control, selectedCharacter[activePlayer]); + + if (player[0].control.held.r) + { + ui_fps(game->timing.frame_rate, 20.0f, 20.0f); + if (core_get_playercount() == 1) + ui_input_display(&player[0].control); + } + + rdpq_detach_show(); + sound_update(); +} + +void gameState_setGameplay(Game *game, Player *player, AI *ai, Actor *actor, Scenery *scenery, ActorCollider *actor_collider, ActorContactData *actor_contact) +{ + + if (!game->actorSet) + { + // Unique spawn positions + actor[0].body.position = hexagons[3].position; + actor[1].body.position = hexagons[6].position; + actor[2].body.position = hexagons[12].position; + actor[3].body.position = hexagons[15].position; + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + actor[i].body.position.z = actor[i].body.position.z + 150.0f; + actor[i].home = actor[i].body.position; + actor[player[i].actor_id].colorID = i; + } + game->actorSet ^= 1; + } + + // ======== Countdown ======== // + if (game->countdownTimer > 0) + { + if (game->countdownTimer % 44 == 0) + sound_wavPlay(SFX_COUNTDOWN, false); + + if (game->countdownTimer == 3) + sound_wavPlay(SFX_START, false); + + move_cloud(scenery); + + // ======== Draw ======== // + screen_clearDisplay(&game->screen); + screen_clearT3dViewport(&game->screen); + screen_applyColor_Depth(&game->screen, RGBA32(154, 181, 198, 0xFF), false); + + gameState_background(game, WHITE); + + t3d_frame_start(); + + light_set(&game->scene.light); + + game->scene.light.direction1 = (T3DVec3){{0, -1, 0}}; + // game->scene.light.directional_color1[0] = 200; + // game->scene.light.directional_color1[1] = 100; + // game->scene.light.directional_color1[2] = 50; + + t3d_matrix_push_pos(1); + + scenery_draw(scenery); + + light_setAmbient(&game->scene.light, 0xBF); + platform_drawBatch(); + light_resetAmbient(&game->scene.light); + + t3d_matrix_pop(1); + + game->syncPoint = rspq_syncpoint_new(); + + // Convert frames to seconds based on refresh rate + uint8_t secondsLeft = (game->countdownTimer / display_get_refresh_rate()) + 1; + ui_countdown(secondsLeft); + + game->countdownTimer--; + + game->timing.frame_counter = 0; + + rdpq_detach_show(); + sound_update(); + return; // Exit early until countdown finishes + } + + // ======== Gameplay ======== // + + float endTimerFactor = 22.0f; + + move_cloud(scenery); + + // AI +#ifndef AI_BATTLE + for (size_t i = 0; i < AI_COUNT; i++) + { + if (player[i + PLAYER_COUNT].died) + continue; + if (game->winnerSet) + continue; + ai_generateControlData(&ai[i], &player[i + PLAYER_COUNT].control, &actor[player[i + PLAYER_COUNT].actor_id], hexagons, game->scene.camera.offset_angle); + } +#else + for (size_t i = 0; i < AI_COUNT; i++) + { + if (player[i].died) + continue; + if (game->winnerSet) + continue; + ai_generateControlData(&ai[i], &player[i].control, &actor[player[i].actor_id], hexagons, game->scene.camera.offset_angle); + } +#endif + + // Platforms + if (game->timing.frame_counter > display_get_refresh_rate() * 10) + { + platform_dropGroup(hexagons, 2, game->timing.fixed_time_s); + } + if (game->timing.frame_counter > display_get_refresh_rate() * 20) + { + platform_dropGroup(hexagons, 1, game->timing.fixed_time_s); + } + + if (!game->winnerSet) + { + for (size_t j = 0; j < PLATFORM_COUNT; j++) + { + platform_loop(&hexagons[j], actor, game->diff); + } + } + + // Actors + uint8_t loserCount = 0; + uint8_t aliveCount = 0; + uint8_t lastAlivePlayer = 0; + + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + // Use player[i].actor_id to identify the assigned actor + uint8_t actorIndex = player[i].actor_id; + Actor *currentActor = &actor[actorIndex]; + // Sync player's position with the actor + player[i].position = currentActor->body.position; + if (currentActor->state != DEATH) + { + if (!game->winnerSet) + { + aliveCount++; + lastAlivePlayer = i; // Track the last alive player + // Update the assigned actor using its actor ID + actor_update(currentActor, &player[i].control, &game->timing, game->scene.camera.angle_around_barycenter, game->scene.camera.offset_angle, &game->syncPoint); + // Update collision data for the assigned actor + actorCollision_collidePlatforms(currentActor, &actor_contact[actorIndex], &actor_collider[actorIndex], hexagons); + + // Update matrix + actor_updateMat(currentActor); + } + } + else + { + + // Bugfix: Center dead actor's position to not break camera + currentActor->body.position = (Vector3){0, 0, 250}; + static int8_t timer[MAXPLAYERS] = {0}; + if (player[i].isHuman) + { + if (!player[i].control.has_rumbled) + { + controllerData_rumbleFrames(&player[i].control, i, 5); + if (++timer[i] > 25) + player[i].control.has_rumbled = true; + } + else + { + controllerData_rumbleStop(&player[i].control, i); + } + } + player[i].died = true; + loserCount++; + } + } + + for (int i = 0; i < MAXPLAYERS; i++) + { + + // Get the player's actor colorID + int playerColorID = actor[player[i].actor_id].colorID; + + // Count platforms with the same colorID + for (int j = 0; j < PLATFORM_COUNT; j++) + { + if (hexagons[j].colorID == playerColorID) + { + if (!game->winnerSet && hexagons[j].platformTimer == 120 - (core_get_aidifficulty() * 10)) + player[i].score++; + } + } + } + + // Check if we have a winner (only one alive player left) OR if there's only one platform left + if ((aliveCount <= 1 || game->timing.frame_counter > display_get_refresh_rate() * endTimerFactor) && !game->winnerSet) + { + // Give last player alive bonus points + if (aliveCount == 1) + { + player[lastAlivePlayer].score++; + wait_ms(1); // Loop is too fast, this doesn't count for the winner + } + + int scores[4] = { + player[0].score, + player[1].score, + player[2].score, + player[3].score}; + + // Find the highest score + int highestScore = scores[0]; + + for (int i = 1; i < 4; i++) + { + if (scores[i] > highestScore) + highestScore = scores[i]; + } + + // Identify all players with the highest score + bool winners[4] = {false, false, false, false}; + int winnerCount = 0; + + for (int i = 0; i < 4; i++) + { + if (scores[i] == highestScore) + { + winners[i] = true; + winnerCount++; + } + } + + // Set the winners + if (winnerCount > 1) + { + // Handle tie case with multiple winners + for (int i = 0; i < 4; i++) + { + if (winners[i]) + { + core_set_winner(i); // Set each tied player as a winner + } + } + game->winnerID = 5; // Optional: Use -1 to represent a tie scenario + } + else + { + // Handle single winner + for (int i = 0; i < 4; i++) + { + if (winners[i]) + { + core_set_winner(i); + game->winnerID = i; + break; + } + } + } + game->winnerSet = true; + } + + // Sync RSPQ once, and then update each actors' skeleton + if (game->syncPoint) + rspq_syncpoint_wait(game->syncPoint); + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + t3d_skeleton_update(&actor[i].armature.main); + } + + // ======== Draw ======== // + + screen_clearDisplay(&game->screen); + screen_clearT3dViewport(&game->screen); + screen_applyColor_Depth(&game->screen, RGBA32(154, 181, 198, 0xFF), false); + + gameState_background(game, WHITE); + + t3d_frame_start(); + + light_set(&game->scene.light); + + // Reset light direction to default in case players have paused + game->scene.light.direction1 = (T3DVec3){{0, -1, 0}}; + + t3d_matrix_push_pos(1); + + scenery_draw(scenery); + + light_setAmbient(&game->scene.light, 0xBF); + platform_drawBatch(); + light_resetAmbient(&game->scene.light); + + // Don't bother drawing shadows for the AI + for (size_t s = 0; s < PLAYER_COUNT; s++) + { + if (actor[player[s].actor_id].state == FALLING || actor[player[s].actor_id].state == JUMP) + player_drawShadow(actor[player[s].actor_id].body.position, &game->screen.gameplay_viewport); + } + + t3d_frame_start(); // reset after drawing shadows + + actor_draw(actor); + + t3d_matrix_pop(1); + + game->syncPoint = rspq_syncpoint_new(); + + // TPX + ptx_draw(&game->screen.gameplay_viewport, &hexagons[0], &cloudMist); + ptx_draw(&game->screen.gameplay_viewport, &hexagons[4], &cloudMist); + + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + if (!game->winnerSet) + { + ui_print_playerNum(&player[i], &game->screen); + } + + ui_playerScores(&player[i]); + } + + if (loserCount >= 3 || game->timing.frame_counter > display_get_refresh_rate() * endTimerFactor) + { + if (game->winnerSet) + { + game->winTimer++; + sound_xmStop(); + sound_wavClose(SFX_WIND); + sound_wavClose(SFX_JUMP); + sound_wavClose(SFX_STONES); + wait_ticks(4); + if (game->winTimer == 3) + sound_wavPlay(SFX_STOP, false); + if (game->winTimer == 60) + sound_wavPlay(SFX_WINNER, false); + if (game->winTimer < 120) + { + if (game->winnerID != 5) + { + ui_print_winner(game->winnerID + 1); + } + else + { + ui_print_winner(game->winnerID); // TIE condition + } + } + if (game->winTimer >= 118) + game->state = GAME_OVER; + } + } + + if (player[0].control.held.r) + { + ui_fps(game->timing.frame_rate, 20.0f, 20.0f); + if (core_get_playercount() == 1) + ui_input_display(&player[0].control); + } + + rdpq_detach_show(); + sound_update(); + +#ifdef PROFILING + rspq_profile_next_frame(); + if (game->timing.frame_counter > 29) + { + rspq_profile_dump(); + rspq_profile_reset(); + game->timing.frame_counter = 0; + } + rspq_profile_get_data(&profile_data); +#endif // PROFILING +} + +void gameState_setPause(Game *game, Player *player, Actor *actor, Scenery *scenery) +{ + + move_cloud(scenery); + + // ======== Draw ======== // + + screen_clearDisplay(&game->screen); + screen_clearT3dViewport(&game->screen); + screen_applyColor_Depth(&game->screen, ui_color(VIOLET), false); + + gameState_background(game, DARK_GREEN); + + t3d_frame_start(); + + light_set(&game->scene.light); + + // Instead drawing a dark transparent texture over the scene, just change the light direction + game->scene.light.direction1 = (T3DVec3){{-1, -1, -1}}; + + t3d_matrix_push_pos(1); + + scenery_draw(scenery); + + // light_setAmbient(&game->scene.light, 0xFF); + platform_drawBatch(); + light_resetAmbient(&game->scene.light); + + actor_draw(actor); + + t3d_matrix_pop(1); + + game->syncPoint = rspq_syncpoint_new(); + + if (player[0].control.held.r) + { + ui_fps(game->timing.frame_rate, 20.0f, 20.0f); + if (core_get_playercount() == 1) + ui_input_display(&player[0].control); + } + else + { + ui_pause(&player[0].control); + } + + rdpq_detach_show(); + sound_update(); + +#ifdef PROFILING + rspq_profile_next_frame(); + if (game->timing.frame_counter > 29) + { + rspq_profile_dump(); + rspq_profile_reset(); + game->timing.frame_counter = 0; + } + rspq_profile_get_data(&profile_data); +#endif // PROFILING +} + +void gameState_setGameOver() +{ + minigame_end(); +} + +void game_play(Game *game, Player *player, AI *ai, Actor *actor, Scenery *scenery, ActorCollider *actor_collider, ActorContactData *actor_contact) +{ + for (;;) + { + + // Time + if (game->state == PAUSE) + { + time_setData(&game->timing, true); + } + else + { + time_setData(&game->timing, false); + } + + // Controls + game_setControlData(game, player); + player_setControlData(player); + + // Hold Z to quit on the Pause screen + static int resetTimer = 0; + if (game->state == PAUSE && player[0].control.held.z) + { + resetTimer++; + if (resetTimer == 30) + { + sound_wavPlay(SFX_JUMP, false); + } + else if (resetTimer > 40) + { + game->state = GAME_OVER; + } + } + + if (game->state == PAUSE && player[0].control.released.z) + resetTimer = 0; + + //// CAMERA ///// + if (player[0].control.pressed.l) + game->scene.camera.cam_mode ^= 1; + + Vector3 introStartPos = (Vector3){-1000, -3000, -100}; + Vector3 centerHex = hexagons[10].home; + Vector3 csPos = (Vector3){0, -1000, 525}; + Vector3 gamePlayPos = (Vector3){0, -600, 1000}; + + game->scene.camera.camTime += game->timing.fixed_time_s; + + if (game->state == INTRO) + { + float t = game->scene.camera.camTime / game->scene.camera.lerpTime; + t = clamp(t, 0.0f, 1.0f); + Vector3 camPos = vector3_lerp(&introStartPos, ¢erHex, t); + camera_getMinigamePosition(&game->scene.camera, actor, player, camPos); + } + else + { + + if (game->scene.camera.cam_mode == 0) + { + camera_getOrbitalPosition(&game->scene.camera, csPos, game->timing.fixed_time_s); + } + else + { + float t = game->scene.camera.camTime / game->scene.camera.lerpTime; + t = clamp(t, 0.0f, 1.0f); + Vector3 camPos = vector3_lerp(&csPos, &gamePlayPos, t); + + if (game->state == PAUSE) + { + cameraControl_freeCam(&game->scene.camera, &player[0].control, game->timing.fixed_time_s); + } + else + { + camera_getMinigamePosition(&game->scene.camera, actor, player, camPos); + } + } + } + camera_set(&game->scene.camera, &game->screen); + //// CAMERA ///// + + // Sound: reverb + for (int i = 0; i < SFX_WINNER; i++) + { + if (i < SFX_COUNTDOWN) + { + mixer_ch_set_vol_pan(SFX_CHANNEL - i, sound_reverb(0.9f, 0.6f), 0.5f); + } + else + { + mixer_ch_set_vol_pan(SFX_CHANNEL - i, sound_reverb(0.5f, 0.8f), 0.5f); + } + } + + switch (game->state) + { + case INTRO: + { + gameState_setIntro(game, player, scenery); + break; + } + case MAIN_MENU: + { + gameState_setMainMenu(game, player, actor, scenery); + break; + } + case CHARACTER_SELECT: + { + gameState_setCS(game, player, actor, scenery); + break; + } + case GAMEPLAY: + { + game->scene.camera.cam_mode = 1; + sound_xmUpdate(0.4f, true); + for (uint8_t p = 0; p < game->humanCount; p++) + { + if (player[p].isHuman && player[p].died && !player[p].deathCounted) + { + game->deadPool++; + player[p].deathCounted = true; + } + } + if (game->deadPool == game->humanCount) + { + display_set_fps_limit(0); + } + else + { + display_set_fps_limit((display_get_refresh_rate() / 4) * 2); + } + gameState_setGameplay(game, player, ai, actor, scenery, actor_collider, actor_contact); + break; + } + case PAUSE: + { + gameState_setPause(game, player, actor, scenery); + break; + } + case GAME_OVER: + { + gameState_setGameOver(); + return; + } + } + } +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/main.c b/code/sb_halcyon/main.c new file mode 100644 index 00000000..934a4ba2 --- /dev/null +++ b/code/sb_halcyon/main.c @@ -0,0 +1,208 @@ +#include +#include +#include +#include +#include +#include +#include + +// May make this an easter egg +// #define AI_BATTLE + +// This define is to test if running the game loop +// in the fixed or the delta matters +// #define FIXED + +#define ACTOR_COUNT 4 +#define PLAYER_COUNT core_get_playercount() + +#ifndef AI_BATTLE +#define AI_COUNT ACTOR_COUNT - PLAYER_COUNT +#else +#define AI_COUNT ACTOR_COUNT +#endif + +#define SCENERY_COUNT 1 +#define PLATFORM_COUNT 19 + +#define S4YS 0 +#define WOLFIE 1 +#define MEW 2 +#define DOGMAN 3 + +#include "../../core.h" +#include "../../minigame.h" + +#include "screen/screen.h" +#include "control/controls.h" +#include "time/time.h" + +#include "physics/physics.h" + +#include "camera/camera.h" +#include "camera/camera_states.h" +#include "camera/camera_control.h" + +#include "sound/sound.h" + +#include "actor/actor.h" +#include "actor/actor_states.h" +#include "actor/actor_motion.h" +#include "actor/actor_control.h" +#include "actor/actor_animation.h" + +#include "player/player.h" + +#include "scene/scene.h" +#include "scene/scenery.h" +#include "scene/platform.h" +#include "scene/room.h" + +#include "actor/collision/actor_collision_detection.h" +#include "actor/collision/actor_collision_response.h" + +#include "player/ai.h" + +#include "ui/ui.h" + +// TPX +#include +#include "scene/particles.h" + +#include "game/game.h" +#include "game/game_control.h" +#include "game/game_states.h" + +const MinigameDef minigame_def = { + .gamename = "Halcyon Hexagons", + .developername = "Strawberry Byte: .zoncabe, s4ys, mewde", + .description = "Don't look down!", + .instructions = "Turn as many platforms as you can\nto your player color."}; + +Game minigame = { + .state = INTRO}; + +Player player[MAXPLAYERS]; + +AI aiPlayer[MAXPLAYERS]; + +Actor actors[ACTOR_COUNT]; + +ActorCollider actor_collider[ACTOR_COUNT]; + +ActorContactData actor_contact[ACTOR_COUNT]; + +Scenery scenery[SCENERY_COUNT]; + +void minigame_init() +{ + game_init(&minigame); +#ifdef PROFILING + // rdpq_debug_start(); + profile_data.frame_count = 0; + rspq_profile_start(); +#endif + + // As per Rasky's suggestion, limit the frame rate to 2 frames every 3 vblanks + display_set_fps_limit((display_get_refresh_rate() / 3) * 2); + + // actors + actors[S4YS] = actor_create(0, "rom:/strawberry_byte/s4ys.t3dm"); + actors[WOLFIE] = actor_create(1, "rom:/strawberry_byte/wolfie.t3dm"); + actors[MEW] = actor_create(2, "rom:/strawberry_byte/mew.t3dm"); + actors[DOGMAN] = actor_create(3, "rom:/strawberry_byte/dogman.t3dm"); + + for (uint8_t i = 0; i < ACTOR_COUNT; i++) + { + actor_init(&actors[i]); + actorCollider_init(&actor_collider[i]); + actor_collider[i].settings.body_radius = 35.0f; + actor_collider[i].settings.body_height = 190.f; + + actors[i].body.position.y = -800.0f; + actors[i].body.rotation.x = 25.0f; + actors[i].body.position.z = 450.0f; + + // Evenly space characters along x-axis + float spacing = 100.0f; // Distance between characters + actors[i].body.position.x = -((ACTOR_COUNT - 1) * spacing) / 2 + i * spacing; + + actors[i].home = actors[i].body.position; + + player_init(&player[i], i, i); + } + + // AI + for (uint8_t i = 0; i < AI_COUNT; i++) + { + ai_init(&aiPlayer[i], core_get_aidifficulty()); + } + + // scenery + scenery[0] = scenery_create(0, "rom:/strawberry_byte/cloud_base.t3dm"); + + for (uint8_t i = 0; i < SCENERY_COUNT; i++) + { + scenery_set(&scenery[i]); + } + + // platforms + platform_hexagonGrid(hexagons, t3d_model_load("rom:/strawberry_byte/platform2.t3dm"), 250.0f, ui_color(WHITE)); + + // Sound: Play wind SFX + sound_wavPlay(SFX_WIND, true); +} + +#ifdef FIXED +void minigame_fixedloop() +{ + game_play(&minigame, player, aiPlayer, actors, scenery, actor_collider, actor_contact); +} +void minigame_loop() +{ +} +#else +void minigame_fixedloop(float dt) +{ +} +void minigame_loop(float dt) +{ + game_play(&minigame, player, aiPlayer, actors, scenery, actor_collider, actor_contact); +} +#endif + +void minigame_cleanup() +{ + +#ifdef PROFILING + rspq_profile_stop(); +#endif + + // Step 1: Disable Frame Limiter + display_set_fps_limit(0); + + // Step 2: Clean up Subsystems + sound_cleanup(); + ui_cleanup(); + + // TPX + ptx_cleanup(&cloudMist); + + // Step 3: Destroy Tiny3D models, matrices, animations and RSPQ blocks + for (uint8_t i = 0; i < ACTOR_COUNT; i++) + { + + actor_delete(&actors[i]); + }; + + for (uint8_t i = 0; i < SCENERY_COUNT; i++) + { + scenery_delete(&scenery[i]); + } + platform_destroy(hexagons); + t3d_destroy(); // Then destroy library + + // Step 4: Free allocated surface buffers + surface_free(&minigame.screen.depthBuffer); + display_close(); +} diff --git a/code/sb_halcyon/physics/body/rigid_body.h b/code/sb_halcyon/physics/body/rigid_body.h new file mode 100644 index 00000000..17d6853f --- /dev/null +++ b/code/sb_halcyon/physics/body/rigid_body.h @@ -0,0 +1,25 @@ +/** + * @file + * + * This file contains the definitions for the rigid body class, the + * basic building block of all the physics system. + */ + +#ifndef RIGID_BODY_H +#define RIGID_BODY_H + +typedef struct RigidBody +{ + + Vector3 acceleration; + Vector3 velocity; + Vector3 position; + Vector3 rotation; + + Vector3 previous_position; + + // Quaternion orientation; + +} RigidBody; + +#endif diff --git a/code/sb_halcyon/physics/collision/contact_data.h b/code/sb_halcyon/physics/collision/contact_data.h new file mode 100644 index 00000000..61ec9f1f --- /dev/null +++ b/code/sb_halcyon/physics/collision/contact_data.h @@ -0,0 +1,20 @@ +#ifndef CONTACT_DATA +#define CONTACT_DATA + +#define MAX_CONTACTS 3 + +typedef struct +{ + Vector3 point; + Vector3 normal; + float penetration; +} ContactData; + +void contactData_init(ContactData *contact) +{ + contact->point = (Vector3){0, 0, 0}; + contact->normal = (Vector3){0, 0, 0}; + contact->penetration = 0.0f; +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/physics/collision/shapes/AABB.h b/code/sb_halcyon/physics/collision/shapes/AABB.h new file mode 100644 index 00000000..0de6f9c7 --- /dev/null +++ b/code/sb_halcyon/physics/collision/shapes/AABB.h @@ -0,0 +1,322 @@ +#ifndef AABB_H +#define AABB_H + +// structures + +typedef struct AABB +{ + Vector3 minCoordinates; + Vector3 maxCoordinates; +} AABB; + +#define MAGIC_NUM 0 + +// function prototypes + +void aabb_setFromCenterAndSize(AABB *aabb, const Vector3 *center, const Vector3 *size); +void aabb_getCorners(const AABB *aabb, Vector3 corners[8]); + +Vector3 aabb_closestToPoint(const AABB *aabb, const Vector3 *point); +Vector3 aabb_closestToSegment(const AABB *aabb, const Vector3 *a, const Vector3 *b); + +bool aabb_containsPoint(const AABB *aabb, const Vector3 *point); +bool aabb_contactAABB(const AABB *a, const AABB *b); +void aabb_contactAABBsetData(ContactData *contact, const AABB *a, const AABB *b); +bool aabb_contactSphere(const AABB *aabb, const Sphere *sphere); +void aabb_contactSphereSetData(ContactData *contact, const AABB *aabb, const Sphere *sphere); + +// function implementations + +void aabb_setFromCenterAndSize(AABB *aabb, const Vector3 *center, const Vector3 *size) +{ + Vector3 halfSize = vector3_returnScaled(size, 0.5f); + aabb->minCoordinates = vector3_difference(center, &halfSize); + aabb->maxCoordinates = vector3_sum(center, &halfSize); +} + +void aabb_getCorners(const AABB *aabb, Vector3 corners[8]) +{ + corners[0] = (Vector3){aabb->minCoordinates.x, aabb->minCoordinates.y, aabb->minCoordinates.z}; + corners[1] = (Vector3){aabb->maxCoordinates.x, aabb->minCoordinates.y, aabb->minCoordinates.z}; + corners[2] = (Vector3){aabb->minCoordinates.x, aabb->maxCoordinates.y, aabb->minCoordinates.z}; + corners[3] = (Vector3){aabb->maxCoordinates.x, aabb->maxCoordinates.y, aabb->minCoordinates.z}; + corners[4] = (Vector3){aabb->minCoordinates.x, aabb->minCoordinates.y, aabb->maxCoordinates.z}; + corners[5] = (Vector3){aabb->maxCoordinates.x, aabb->minCoordinates.y, aabb->maxCoordinates.z}; + corners[6] = (Vector3){aabb->minCoordinates.x, aabb->maxCoordinates.y, aabb->maxCoordinates.z}; + corners[7] = (Vector3){aabb->maxCoordinates.x, aabb->maxCoordinates.y, aabb->maxCoordinates.z}; +} + +Vector3 aabb_closestToPoint(const AABB *aabb, const Vector3 *point) +{ + Vector3 closest; + closest.x = clamp(point->x, aabb->minCoordinates.x, aabb->maxCoordinates.x); + closest.y = clamp(point->y, aabb->minCoordinates.y, aabb->maxCoordinates.y); + closest.z = clamp(point->z, aabb->minCoordinates.z, aabb->maxCoordinates.z); + return closest; +} + +Vector3 aabb_getCenter(const AABB *aabb) +{ + Vector3 sum = vector3_sum(&aabb->minCoordinates, &aabb->maxCoordinates); + return vector3_returnScaled(&sum, 0.5f); +} + +Vector3 aabb_getHalfSize(const AABB *aabb) +{ + Vector3 difference = vector3_difference(&aabb->maxCoordinates, &aabb->minCoordinates); + return vector3_returnScaled(&difference, 0.5f); +} + +Vector3 aabb_closestToSegment(const AABB *aabb, const Vector3 *a, const Vector3 *b) +{ + Vector3 s, v, sign = {1, 1, 1}; + Vector3 aabb_center = aabb_getCenter(aabb); + Vector3 half_size = aabb_getHalfSize(aabb); + Vector3 tanchor = {2.0f, 2.0f, 2.0f}; // Initial large number for tanchor + Vector3 region = {0, 0, 0}; + Vector3 v2; + float t = 0; + + // Translate the segment's starting point relative to the AABB's center + s = vector3_difference(a, &aabb_center); + + // Calculate the direction vector of the segment + v = vector3_difference(b, a); + + // Mirror the line direction if necessary + if (v.x < 0) + { + s.x = -s.x; + v.x = -v.x; + sign.x = -1; + } + if (v.y < 0) + { + s.y = -s.y; + v.y = -v.y; + sign.y = -1; + } + if (v.z < 0) + { + s.z = -s.z; + v.z = -v.z; + sign.z = -1; + } + + // Calculate v^2 (the square of each component of v) + v2.x = v.x * v.x; + v2.y = v.y * v.y; + v2.z = v.z * v.z; + + // Determine regions and tanchor values for a + if (v.x > MAGIC_NUM) + { + if (s.x < -half_size.x) + { + region.x = -1; + tanchor.x = (-half_size.x - s.x) / v.x; + } + else if (s.x > half_size.x) + { + region.x = 1; + tanchor.x = (half_size.x - s.x) / v.x; + } + } + else + { + region.x = 0; + tanchor.x = 2; // this will never be a valid tanchor + } + + if (v.y > MAGIC_NUM) + { + if (s.y < -half_size.y) + { + region.y = -1; + tanchor.y = (-half_size.y - s.y) / v.y; + } + else if (s.y > half_size.y) + { + region.y = 1; + tanchor.y = (half_size.y - s.y) / v.y; + } + } + else + { + region.y = 0; + tanchor.y = 2; // this will never be a valid tanchor + } + if (v.z > MAGIC_NUM) + { + if (s.z < -half_size.z) + { + region.z = -1; + tanchor.z = (-half_size.z - s.z) / v.z; + } + else if (s.z > half_size.z) + { + region.z = 1; + tanchor.z = (half_size.z - s.z) / v.z; + } + } + else + { + region.z = 0; + tanchor.z = 2; // this will never be a valid tanchor + } + + // Check the initial dd2dt + float dd2dt = 0; + if (region.x != 0) + dd2dt -= v2.x * tanchor.x; + if (region.y != 0) + dd2dt -= v2.y * tanchor.y; + if (region.z != 0) + dd2dt -= v2.z * tanchor.z; + if (dd2dt >= 0) + goto finalize; + + // Iterate to find the smallest t + float next_t, next_dd2dt; + + do + { + + // find the point on the line that is at the next clip plane boundary + next_t = 1.0f; + next_t = (tanchor.x > t && tanchor.x < 1 && tanchor.x < next_t) ? tanchor.x : next_t; + next_t = (tanchor.y > t && tanchor.y < 1 && tanchor.y < next_t) ? tanchor.y : next_t; + next_t = (tanchor.z > t && tanchor.z < 1 && tanchor.z < next_t) ? tanchor.z : next_t; + + // compute d|d|^2/dt for the next t + next_dd2dt = 0; + next_dd2dt += (region.x ? v2.x : 0) * (next_t - tanchor.x); + next_dd2dt += (region.y ? v2.y : 0) * (next_t - tanchor.y); + next_dd2dt += (region.z ? v2.z : 0) * (next_t - tanchor.z); + + // if the sign of d|d|^2/dt has changed, solution = the crossover point + if (next_dd2dt >= 0) + { + float m = (next_dd2dt - dd2dt) / (next_t - t); + t -= dd2dt / m; + goto finalize; + } + + // advance to the next anchor point / region + if (tanchor.x == next_t) + { + tanchor.x = (half_size.x - s.x) / v.x; + region.x++; + } + if (tanchor.y == next_t) + { + tanchor.y = (half_size.y - s.y) / v.y; + region.y++; + } + if (tanchor.z == next_t) + { + tanchor.z = (half_size.z - s.z) / v.z; + region.z++; + } + + t = next_t; + dd2dt = next_dd2dt; + + } while (t < 1.0f); + + t = 1.0f; + +finalize: + + // Compute the closest point on the box + Vector3 tmp = {sign.x * (s.x + t * v.x), sign.y * (s.y + t * v.y), sign.z * (s.z + t * v.z)}; + + // Clamp tmp to the AABB's extents + if (tmp.x < -half_size.x) + tmp.x = -half_size.x; + else if (tmp.x > half_size.x) + tmp.x = half_size.x; + + if (tmp.y < -half_size.y) + tmp.y = -half_size.y; + else if (tmp.y > half_size.y) + tmp.y = half_size.y; + + if (tmp.z < -half_size.z) + tmp.z = -half_size.z; + else if (tmp.z > half_size.z) + tmp.z = half_size.z; + + return (Vector3){tmp.x + aabb_center.x, tmp.y + aabb_center.y, tmp.z + aabb_center.z}; +} + +bool aabb_containsPoint(const AABB *aabb, const Vector3 *point) +{ + return (point->x >= aabb->minCoordinates.x && point->x <= aabb->maxCoordinates.x && + point->y >= aabb->minCoordinates.y && point->y <= aabb->maxCoordinates.y && + point->z >= aabb->minCoordinates.z && point->z <= aabb->maxCoordinates.z); +} + +bool aabb_contactAABB(const AABB *a, const AABB *b) +{ + if (a->maxCoordinates.x < b->minCoordinates.x || b->maxCoordinates.x < a->minCoordinates.x) + return false; + if (a->maxCoordinates.y < b->minCoordinates.y || b->maxCoordinates.y < a->minCoordinates.y) + return false; + if (a->maxCoordinates.z < b->minCoordinates.z || b->maxCoordinates.z < a->minCoordinates.z) + return false; + return true; +} + +void aabb_contactAABBsetData(ContactData *contact, const AABB *a, const AABB *b) +{ + // Calculate overlap on each axis + float overlapX = min2(a->maxCoordinates.x, b->maxCoordinates.x) - max2(a->minCoordinates.x, b->minCoordinates.x); + float overlapY = min2(a->maxCoordinates.y, b->maxCoordinates.y) - max2(a->minCoordinates.y, b->minCoordinates.y); + float overlapZ = min2(a->maxCoordinates.z, b->maxCoordinates.z) - max2(a->minCoordinates.z, b->minCoordinates.z); + + // Find the axis of least penetration + if (overlapX < overlapY && overlapX < overlapZ) + { + contact->normal = (Vector3){a->maxCoordinates.x < b->maxCoordinates.x ? -1 : 1, 0, 0}; + } + else if (overlapY < overlapX && overlapY < overlapZ) + { + contact->normal = (Vector3){0, a->maxCoordinates.y < b->maxCoordinates.y ? -1 : 1, 0}; + } + else + { + contact->normal = (Vector3){0, 0, a->maxCoordinates.z < b->maxCoordinates.z ? -1 : 1}; + } + + // Calculate the contact point + contact->point = (Vector3){ + (max2(a->minCoordinates.x, b->minCoordinates.x) + min2(a->maxCoordinates.x, b->maxCoordinates.x)) / 2.0f, + (max2(a->minCoordinates.y, b->minCoordinates.y) + min2(a->maxCoordinates.y, b->maxCoordinates.y)) / 2.0f, + (max2(a->minCoordinates.z, b->minCoordinates.z) + min2(a->maxCoordinates.z, b->maxCoordinates.z)) / 2.0f}; +} + +bool aabb_contactSphere(const AABB *aabb, const Sphere *sphere) +{ + // Find the point on the AABB closest to the center of the sphere + Vector3 closestPoint = aabb_closestToPoint(aabb, &sphere->center); + + // Calculate the distance from the closest point to the center of the sphere + Vector3 difference = vector3_difference(&closestPoint, &sphere->center); + float distanceSquared = vector3_squaredMagnitude(&difference); + + // Check if the distance is less than or equal to the radius squared + return distanceSquared <= (sphere->radius * sphere->radius); +} + +void aabb_contactSphereSetData(ContactData *contact, const AABB *aabb, const Sphere *sphere) +{ + // the contact point is the point on the AABB closest to the center of the sphere + contact->point = aabb_closestToPoint(aabb, &sphere->center); + + // Calculate the vector from the sphere center to the closest point on the AABB + contact->normal = vector3_difference(&sphere->center, &contact->point); + vector3_normalize(&contact->normal); +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/physics/collision/shapes/box.h b/code/sb_halcyon/physics/collision/shapes/box.h new file mode 100644 index 00000000..aad6e547 --- /dev/null +++ b/code/sb_halcyon/physics/collision/shapes/box.h @@ -0,0 +1,77 @@ +#ifndef BOX_H +#define BOX_H + +// structures + +typedef struct +{ + Vector3 size; + Vector3 center; + Vector3 rotation; +} Box; + +// function prototypes + +AABB box_getLocalAABB(const Box *box); + +bool box_contactSphere(const Box *box, const Sphere *sphere); +void box_contactSphereSetData(ContactData *contact, const Box *box, const Sphere *sphere); + +void box_init(Box *box, Vector3 size, Vector3 center, Vector3 rotation, float scalar); + +// function implementations + +AABB box_getLocalAABB(const Box *box) +{ + AABB aabb; + aabb.minCoordinates = vector3_returnScaled(&box->size, -0.5f); + aabb.maxCoordinates = vector3_returnScaled(&box->size, 0.5f); + return aabb; +} + +bool box_contactSphere(const Box *box, const Sphere *sphere) +{ + // Transform the center of the sphere to the local space of the box + Vector3 local_sphere_center = sphere->center; + point_transformToLocalSpace(&local_sphere_center, &box->center, &box->rotation); + + // Get the local AABB of the box and local sphere + AABB local_aabb = box_getLocalAABB(box); + const Sphere local_sphere = { + center : local_sphere_center, + radius : sphere->radius + }; + + // Check for collision in the local space + return aabb_contactSphere(&local_aabb, &local_sphere); +} + +void box_contactSphereSetData(ContactData *contact, const Box *box, const Sphere *sphere) +{ + // Transform the center of the sphere to the local space of the box + Vector3 local_sphere_center = sphere->center; + point_transformToLocalSpace(&local_sphere_center, &box->center, &box->rotation); + + // Get the local AABB of the box + AABB local_aabb = box_getLocalAABB(box); + + // Find the point on the AABB closest to the center of the sphere + contact->point = aabb_closestToPoint(&local_aabb, &local_sphere_center); + + // Calculate the normal in local space pointing from the AABB towards the sphere + contact->normal = vector3_difference(&local_sphere_center, &contact->point); + vector3_normalize(&contact->normal); + + // Transform the closest point and normal back to global space + point_transformToGlobalSpace(&contact->point, &box->center, &box->rotation); + rotate_normal(&contact->normal, &box->rotation); // Rotate normal to global space +} + +void box_init(Box *box, Vector3 size, Vector3 center, Vector3 rotation, float scalar) +{ + box->size = vector3_returnScaled(&size, scalar); + box->center = vector3_returnScaled(¢er, scalar); + box->rotation = rotation; +} + +#endif diff --git a/code/sb_halcyon/physics/collision/shapes/capsule.h b/code/sb_halcyon/physics/collision/shapes/capsule.h new file mode 100644 index 00000000..b3566831 --- /dev/null +++ b/code/sb_halcyon/physics/collision/shapes/capsule.h @@ -0,0 +1,220 @@ +#ifndef CAPSULE_H +#define CAPSULE_H + +typedef struct +{ + Vector3 start; + Vector3 end; + float radius; + float length; +} Capsule; + +// Function prototypes + +void capsule_setVertical(Capsule *capsule, const Vector3 *position); + +bool capsule_contactSphere(const Capsule *capsule, const Sphere *sphere); +void capsule_contactSphereSetData(ContactData *contact, const Capsule *capsule, const Sphere *sphere); + +bool capsule_contactAABB(const Capsule *capsule, const AABB *aabb); +void capsule_contactAABBSetData(ContactData *contact, const Capsule *capsule, const AABB *aabb); + +bool capsule_contactBox(const Capsule *capsule, const Box *box); +void capsule_contactBoxSetData(ContactData *contact, const Capsule *capsule, const Box *box); + +bool capsule_contactPlane(const Capsule *capsule, const Plane *plane); + +bool capsule_intersectionRay(const Capsule *capsule, const Ray *ray); + +// Function implementations + +void capsule_setVertical(Capsule *capsule, const Vector3 *position) +{ + capsule->start = *position; + capsule->start.z += capsule->radius; + capsule->end = *position; + capsule->end.z = capsule->end.z + capsule->length - capsule->radius; +} + +bool capsule_contactSphere(const Capsule *capsule, const Sphere *sphere) +{ + // Calculate the closest point on the capsule segment to the sphere center + Vector3 closest_on_axis = segment_closestToPoint(&capsule->start, &capsule->end, &sphere->center); + + // Calculate the squared distance from the closest point to the sphere center + Vector3 difference = vector3_difference(&closest_on_axis, &sphere->center); + float distanceSquared = vector3_squaredMagnitude(&difference); + + // Compare the squared distance with the squared combined radius + float combinedRadius = capsule->radius + sphere->radius; + return distanceSquared <= combinedRadius * combinedRadius; +} + +void capsule_contactSphereSetData(ContactData *contact, const Capsule *capsule, const Sphere *sphere) +{ + // Calculate the closest point on the capsule segment to the sphere center + Vector3 closest_on_axis = segment_closestToPoint(&capsule->start, &capsule->end, &sphere->center); + + // Calculate the squared distance from the closest point to the sphere center + contact->normal = vector3_difference(&closest_on_axis, &sphere->center); + contact->penetration = capsule->radius + sphere->radius - fabsf(vector3_magnitude(&contact->normal)); + vector3_normalize(&contact->normal); + + // Calculate the contact point in reference to the sphere + contact->point = sphere->center; + vector3_addScaledVector(&contact->point, &contact->normal, sphere->radius); +} + +bool capsule_contactAABB(const Capsule *capsule, const AABB *aabb) +{ + Vector3 closest_point_on_aabb = aabb_closestToSegment(aabb, &capsule->end, &capsule->start); + Vector3 closest_point_on_axis = segment_closestToPoint(&capsule->end, &capsule->start, &closest_point_on_aabb); + Vector3 distance_vector = vector3_difference(&closest_point_on_axis, &closest_point_on_aabb); + + return vector3_squaredMagnitude(&distance_vector) <= capsule->radius * capsule->radius; +} + +void capsule_contactAABBSetData(ContactData *contact, const Capsule *capsule, const AABB *aabb) +{ + contact->point = aabb_closestToSegment(aabb, &capsule->end, &capsule->start); + + Vector3 closest_point_on_axis = segment_closestToPoint(&capsule->end, &capsule->start, &contact->point); + Vector3 distance_vector = vector3_difference(&closest_point_on_axis, &contact->point); + contact->penetration = capsule->radius - fabsf(vector3_magnitude(&distance_vector)); + contact->normal = distance_vector; + vector3_normalize(&contact->normal); +} + +bool capsule_contactBox(const Capsule *capsule, const Box *box) +{ + Capsule local_capsule = *capsule; + point_transformToLocalSpace(&local_capsule.start, &box->center, &box->rotation); + point_transformToLocalSpace(&local_capsule.end, &box->center, &box->rotation); + AABB local_aabb = box_getLocalAABB(box); + return capsule_contactAABB(&local_capsule, &local_aabb); +} + +void capsule_contactBoxSetData(ContactData *contact, const Capsule *capsule, const Box *box) +{ + Capsule local_capsule = *capsule; + point_transformToLocalSpace(&local_capsule.start, &box->center, &box->rotation); + point_transformToLocalSpace(&local_capsule.end, &box->center, &box->rotation); + AABB local_aabb = box_getLocalAABB(box); + + capsule_contactAABBSetData(contact, &local_capsule, &local_aabb); + point_transformToGlobalSpace(&contact->point, &box->center, &box->rotation); + rotate_normal(&contact->normal, &box->rotation); +} + +bool capsule_contactPlane(const Capsule *capsule, const Plane *plane) +{ + // Calculate the distances of the capsule endpoints from the plane + float distance_to_start = plane_distanceToPoint(plane, &capsule->start); + float distance_to_end = plane_distanceToPoint(plane, &capsule->end); + + // Check if either endpoint of the capsule is within the radius distance from the plane + if (fabsf(distance_to_start) <= capsule->radius || fabsf(distance_to_end) <= capsule->radius) + { + return true; + } + + // Check if the plane intersects the segment of the capsule + if ((distance_to_start > 0 && distance_to_end < 0) || (distance_to_start < 0 && distance_to_end > 0)) + { + return true; + } + + return false; +} + +void capsule_contactPlaneSetData(ContactData *contact, const Capsule *capsule, const Plane *plane) +{ + // Calculate the distances of the capsule endpoints from the plane + float distance_to_start = plane_distanceToPoint(plane, &capsule->start); + float distance_to_end = plane_distanceToPoint(plane, &capsule->end); + + // Set the contact normal as the plane's normal + contact->normal = plane->normal; + + // Determine the point of contact and penetration depth + if (fabsf(distance_to_start) <= capsule->radius) + { + // The start point of the capsule is within the radius distance from the plane + contact->penetration = capsule->radius - fabsf(distance_to_start); + contact->point = capsule->start; + vector3_addScaledVector(&contact->point, &contact->normal, -distance_to_start); + } + else if (fabsf(distance_to_end) <= capsule->radius) + { + // The end point of the capsule is within the radius distance from the plane + contact->penetration = capsule->radius - fabsf(distance_to_end); + contact->point = capsule->end; + vector3_addScaledVector(&contact->point, &contact->normal, -distance_to_end); + } +} + +bool capsule_intersectionRay(const Capsule *capsule, const Ray *ray) +{ + // Calculate the vector from the start to the end of the capsule + Vector3 ba = vector3_difference(&capsule->end, &capsule->start); + // Calculate the vector from the start of the capsule to the origin of the ray + Vector3 oa = vector3_difference(&ray->origin, &capsule->start); + + // Compute dot products used in the quadratic equation + float baba = vector3_returnDotProduct(&ba, &ba); + float bard = vector3_returnDotProduct(&ba, &ray->direction); + float baoa = vector3_returnDotProduct(&ba, &oa); + float rdoa = vector3_returnDotProduct(&ray->direction, &oa); + float oaoa = vector3_returnDotProduct(&oa, &oa); + + // Compute the coefficients of the quadratic equation + float a = baba - bard * bard; + float b = baba * rdoa - baoa * bard; + float c = baba * oaoa - baoa * baoa - capsule->radius * capsule->radius * baba; + float h = b * b - a * c; + + // Check if the discriminant is non-negative (real roots) + if (h >= 0.0f && a != 0.0f) + { + // Compute the smallest root of the quadratic equation + float t = (-b - sqrtf(h)) / a; + float y = baoa + t * bard; + // Check if the intersection is within the cylindrical body of the capsule + if (y > 0.0f && y < baba) + return true; + // Otherwise, check for intersections with the spherical caps + Vector3 oc = (y <= 0.0f) ? oa : vector3_difference(&ray->origin, &capsule->end); + b = vector3_returnDotProduct(&ray->direction, &oc); + c = vector3_returnDotProduct(&oc, &oc) - capsule->radius * capsule->radius; + h = b * b - c; + // Check if the intersection with the cap is valid + if (h > 0.0f) + return true; + } + // Return false if no intersection is found + return false; +} + +bool capsule_intersectsEdge(const Capsule *capsule, const Vector3 *edgeStart, const Vector3 *edgeEnd) +{ + // Step 1: Find the closest point on the capsule's segment to the edge + Vector3 closestPointOnCapsule = segment_closestToPoint(&capsule->start, &capsule->end, edgeStart); + Vector3 closestPointOnEdge = segment_closestToPoint(edgeStart, edgeEnd, &capsule->start); + + // Step 2: Calculate the distance vector between these two closest points + Vector3 distanceVector = vector3_difference(&closestPointOnCapsule, &closestPointOnEdge); + + // Step 3: Calculate the squared distance + float distanceSquared = vector3_squaredMagnitude(&distanceVector); + + // Step 4: Calculate the combined radius + float combinedRadius = capsule->radius; // Adjust if the edge has its own thickness + + // Step 5: Check for intersection + return distanceSquared <= combinedRadius * combinedRadius; +} + +/* + */ + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/physics/collision/shapes/plane.h b/code/sb_halcyon/physics/collision/shapes/plane.h new file mode 100644 index 00000000..2d867f63 --- /dev/null +++ b/code/sb_halcyon/physics/collision/shapes/plane.h @@ -0,0 +1,76 @@ +#ifndef PLANE_H +#define PLANE_H + +// structures + +typedef struct +{ + Vector3 normal; // Normal vector of the plane + float displacement; // Displacement from the origin along the normal +} Plane; + +// function prototypes + +Vector3 plane_getNormalFromRotation(const Vector3 *rotation); +float plane_getDisplacement(const Vector3 *normal, const Vector3 *point); +void plane_setFromRotationAndPoint(Plane *plane, const Vector3 *rotation, const Vector3 *point); +void plane_setFromNormalAndPoint(Plane *plane, const Vector3 *normal, const Vector3 *point); +float plane_distanceToPoint(const Plane *plane, const Vector3 *point); + +// function implementations + +Vector3 plane_getNormalFromRotation(const Vector3 *rotation) +{ + Vector3 normal = {0.0f, 0.0f, 1.0f}; + Vector3 rad_rotation = vector3_degToRad(rotation); + Quaternion q_rotation = quaternion_getFromVector(&rad_rotation); + normal = vector3_rotateByQuaternion(&normal, &q_rotation); + + return normal; +} + +float plane_getDisplacement(const Vector3 *normal, const Vector3 *point) +{ + + return vector3_returnDotProduct(normal, point); +} + +void plane_setFromRotationAndPoint(Plane *plane, const Vector3 *rotation, const Vector3 *point) +{ + plane->normal = plane_getNormalFromRotation(rotation); + plane->displacement = plane_getDisplacement(&plane->normal, point); +} + +void plane_setFromNormalAndPoint(Plane *plane, const Vector3 *normal, const Vector3 *point) +{ + plane->normal = *normal; + plane->displacement = plane_getDisplacement(normal, point); +} + +float plane_distanceToPoint(const Plane *plane, const Vector3 *point) +{ + return vector3_returnDotProduct(&plane->normal, point) - plane->displacement; +} + +bool plane_contactSphere(const Plane *plane, const Sphere *sphere) +{ + // Project the center of the sphere onto the plane's normal vector + float distance = vector3_returnDotProduct(&plane->normal, &sphere->center) - plane->displacement; + + // Check if the distance is less than the radius of the sphere + return fabsf(distance) <= sphere->radius; +} + +void plane_contactSphereGetData(ContactData *contact, const Plane *plane, const Sphere *sphere) +{ + // Project the center of the sphere onto the plane's normal vector + float distance = vector3_returnDotProduct(&plane->normal, &sphere->center) - plane->displacement; + + contact->normal = plane->normal; + + // Calculate the contact point on the plane + Vector3 scaled_normal = vector3_returnScaled(&plane->normal, distance); + contact->point = vector3_difference(&sphere->center, &scaled_normal); +} + +#endif diff --git a/code/sb_halcyon/physics/collision/shapes/ray.h b/code/sb_halcyon/physics/collision/shapes/ray.h new file mode 100644 index 00000000..da6013f0 --- /dev/null +++ b/code/sb_halcyon/physics/collision/shapes/ray.h @@ -0,0 +1,290 @@ +#ifndef RAY_H +#define RAY_H + +// structures + +typedef struct +{ + Vector3 origin; + Vector3 direction; +} Ray; + +// function prototypes + +Vector3 ray_getDirectionFromRotation(const Vector3 *rotation); +void ray_setFromRotationAndPoint(Ray *ray, const Vector3 *origin, const Vector3 *rotation); + +bool ray_intersectionSphere(const Ray *ray, const Sphere *sphere); +void raycast_sphere(ContactData *contact, const Ray *ray, const Sphere *sphere); + +bool ray_intersectionAABB(const Ray *ray, const AABB *aabb); +void raycast_aabb(ContactData *contact, const Ray *ray, const AABB *aabb); + +bool ray_intersectionBox(const Ray *ray, const Box *box); +void raycast_box(ContactData *contact, const Ray *ray, const Box *box); + +// function implementations + +Vector3 ray_getDirectionFromRotation(const Vector3 *rotation) +{ + Vector3 direction = {0.0f, 1.0f, 0.0f}; + Vector3 rad_rotation = vector3_degToRad(rotation); + Quaternion q_rotation = quaternion_getFromVector(&rad_rotation); + direction = vector3_rotateByQuaternion(&direction, &q_rotation); + + return direction; +} + +void ray_setFromRotationAndPoint(Ray *ray, const Vector3 *origin, const Vector3 *rotation) +{ + ray->origin = *origin; + ray->direction = ray_getDirectionFromRotation(rotation); +} + +bool ray_intersectionSphere(const Ray *ray, const Sphere *sphere) +{ + // Vector from ray origin to sphere center + Vector3 oc = vector3_difference(&ray->origin, &sphere->center); + + // Compute dot products used in the quadratic equation + float a = vector3_returnDotProduct(&ray->direction, &ray->direction); + float b = 2.0f * vector3_returnDotProduct(&oc, &ray->direction); + float c = vector3_returnDotProduct(&oc, &oc) - sphere->radius * sphere->radius; + + // Compute the discriminant + float discriminant = b * b - 4 * a * c; + + // Check if the discriminant is non-negative (real roots) + return (discriminant >= 0); +} + +void raycast_sphere(ContactData *contact, const Ray *ray, const Sphere *sphere) +{ + // Vector from ray origin to sphere center + Vector3 oc = vector3_difference(&ray->origin, &sphere->center); + + // Compute dot products used in the quadratic equation + float a = vector3_returnDotProduct(&ray->direction, &ray->direction); + float b = 2.0f * vector3_returnDotProduct(&oc, &ray->direction); + float c = vector3_returnDotProduct(&oc, &oc) - sphere->radius * sphere->radius; + + // Compute the discriminant + float discriminant = b * b - 4 * a * c; + + // Calculate the nearest intersection point + float t = (-b - 1.0f / qi_sqrt(discriminant)) / (2.0f * a); + + // Calculate the contact point + contact->point = (Vector3){ + ray->origin.x + t * ray->direction.x, + ray->origin.y + t * ray->direction.y, + ray->origin.z + t * ray->direction.z}; + + // Calculate the normal at the contact point + contact->normal = vector3_difference(&contact->point, &sphere->center); + vector3_normalize(&contact->normal); +} + +bool ray_intersectionAABB(const Ray *ray, const AABB *aabb) +{ + Vector3 rayDirectionInverse = { + 1.0f / ray->direction.x, + 1.0f / ray->direction.y, + 1.0f / ray->direction.z}; + + // Calculate intersection times for the x-axis + float t1 = (aabb->minCoordinates.x - ray->origin.x) * rayDirectionInverse.x; + float t2 = (aabb->maxCoordinates.x - ray->origin.x) * rayDirectionInverse.x; + float tMin = min2(t1, t2); + float tMax = max2(t1, t2); + + // Calculate intersection times for the y-axis + float t1_y = (aabb->minCoordinates.y - ray->origin.y) * rayDirectionInverse.y; + float t2_y = (aabb->maxCoordinates.y - ray->origin.y) * rayDirectionInverse.y; + tMin = max2(tMin, min2(t1_y, t2_y)); + tMax = min2(tMax, max2(t1_y, t2_y)); + + // Calculate intersection times for the z-axis + float t1_z = (aabb->minCoordinates.z - ray->origin.z) * rayDirectionInverse.z; + float t2_z = (aabb->maxCoordinates.z - ray->origin.z) * rayDirectionInverse.z; + tMin = max2(tMin, min2(t1_z, t2_z)); + tMax = min2(tMax, max2(t1_z, t2_z)); + + // Check if there is a valid intersection + return tMax >= max2(tMin, 0.0f); +} + +void raycast_aabb(ContactData *contact, const Ray *ray, const AABB *aabb) +{ + float tMin = 0.0f; + float tMax = FLT_MAX; + const float epsilon = 0.00001f; + + float t1 = 0; + float t2 = 0; + float rayDirectionInverse = 0; + + // For x-axis + if (fabsf(ray->direction.x) < epsilon) + { + if (ray->origin.x < aabb->minCoordinates.x || ray->origin.x > aabb->maxCoordinates.x) + return; + } + else + { + rayDirectionInverse = 1.0f / ray->direction.x; + t1 = (aabb->minCoordinates.x - ray->origin.x) * rayDirectionInverse; + t2 = (aabb->maxCoordinates.x - ray->origin.x) * rayDirectionInverse; + if (t1 > t2) + { + float temp = t2; + t2 = t1; + t1 = temp; + } + tMin = max2(tMin, t1); + tMax = min2(tMax, t2); + if (tMin > tMax) + return; + } + + // For y-axis + if (fabsf(ray->direction.y) < epsilon) + { + if (ray->origin.y < aabb->minCoordinates.y || ray->origin.y > aabb->maxCoordinates.y) + return; + } + else + { + rayDirectionInverse = 1.0f / ray->direction.y; + t1 = (aabb->minCoordinates.y - ray->origin.y) * rayDirectionInverse; + t2 = (aabb->maxCoordinates.y - ray->origin.y) * rayDirectionInverse; + if (t1 > t2) + { + float temp = t2; + t2 = t1; + t1 = temp; + } + tMin = max2(tMin, t1); + tMax = min2(tMax, t2); + if (tMin > tMax) + return; + } + + // For z-axis + if (fabsf(ray->direction.z) < epsilon) + { + if (ray->origin.z < aabb->minCoordinates.z || ray->origin.z > aabb->maxCoordinates.z) + return; + } + else + { + rayDirectionInverse = 1.0f / ray->direction.z; + t1 = (aabb->minCoordinates.z - ray->origin.z) * rayDirectionInverse; + t2 = (aabb->maxCoordinates.z - ray->origin.z) * rayDirectionInverse; + if (t1 > t2) + { + float temp = t2; + t2 = t1; + t1 = temp; + } + tMin = max2(tMin, t1); + tMax = min2(tMax, t2); + if (tMin > tMax) + return; + } + + Vector3 temp = ray->direction; + vector3_scale(&temp, tMin); + vector3_add(&temp, &ray->origin); + contact->point = temp; + + // Assuming the normal should be calculated based on which side the intersection occurred + if (tMin == t1) + { + contact->normal = (Vector3){1, 0, 0}; + } + else if (tMin == t2) + { + contact->normal = (Vector3){-1, 0, 0}; + } + else if (tMin == t1) + { + contact->normal = (Vector3){0, 1, 0}; + } + else if (tMin == t2) + { + contact->normal = (Vector3){0, -1, 0}; + } + else if (tMin == t1) + { + contact->normal = (Vector3){0, 0, 1}; + } + else if (tMin == t2) + { + contact->normal = (Vector3){0, 0, -1}; + } +} + +bool ray_intersectionBox(const Ray *ray, const Box *box) +{ + // Transform the ray to the local space of the box + Ray local_ray = *ray; + point_transformToLocalSpace(&local_ray.origin, &box->center, &box->rotation); + rotate_normal(&local_ray.direction, &box->rotation); + + // Invert the direction of the local ray for the intersection test + vector3_invert(&local_ray.direction); + + // Get the local AABB of the box + AABB local_AABB = box_getLocalAABB(box); + + // Use the existing AABB intersection function + return ray_intersectionAABB(&local_ray, &local_AABB); +} + +void raycast_box(ContactData *contact, const Ray *ray, const Box *box) +{ + // Transform the ray to the local space of the box + Ray local_ray = *ray; + point_transformToLocalSpace(&local_ray.origin, &box->center, &box->rotation); + rotate_normal(&local_ray.direction, &box->rotation); + + // Invert the direction of the local ray for the intersection test + vector3_invert(&local_ray.direction); + + // Get the local AABB of the box + AABB local_aabb = box_getLocalAABB(box); + + // Perform the intersection using raycast_aabb + raycast_aabb(contact, &local_ray, &local_aabb); + + // Transform the hit point and normal back to global space + point_transformToGlobalSpace(&contact->point, &box->center, &box->rotation); + rotate_normal(&contact->normal, &box->rotation); +} + +bool ray_intersectionPlane(const Ray *ray, const Plane *plane) +{ + float denominator = vector3_returnDotProduct(&ray->direction, &plane->normal); + + // If the denominator is zero, the ray is parallel to the plane + if (fabsf(denominator) < 1e-6) + return false; + + // If t is negative, the ray intersects the plane in the opposite direction + float t = (plane->displacement - vector3_returnDotProduct(&ray->origin, &plane->normal)) / denominator; + if (t < 0) + return false; + + return true; +} + +void raycast_plane(ContactData *contact, const Ray *ray, const Plane *plane) +{ + contact->point = ray->origin; + vector3_addScaledVector(&contact->point, &ray->direction, (plane->displacement - vector3_returnDotProduct(&ray->origin, &plane->normal)) / vector3_returnDotProduct(&ray->direction, &plane->normal)); + + contact->normal = plane->normal; +} + +#endif diff --git a/code/sb_halcyon/physics/collision/shapes/sphere.h b/code/sb_halcyon/physics/collision/shapes/sphere.h new file mode 100644 index 00000000..56533770 --- /dev/null +++ b/code/sb_halcyon/physics/collision/shapes/sphere.h @@ -0,0 +1,38 @@ +#ifndef SPHERE_H +#define SPHERE_H + +// structures + +typedef struct +{ + Vector3 center; + float radius; +} Sphere; + +bool sphere_contactSphere(const Sphere *s, const Sphere *t); +void sphere_collisionTestSphere(ContactData *contact, const Sphere *s, const Sphere *t); + +bool sphere_contactSphere(const Sphere *s, const Sphere *t) +{ + float radiusSum = s->radius + t->radius; + + Vector3 diff = s->center; + vector3_subtract(&diff, &t->center); + // Check if the squared distance is less than or equal to the square of the sum of the radii + return vector3_squaredMagnitude(&diff) <= radiusSum * radiusSum; +} + +void sphere_collisionTestSphere(ContactData *contact, const Sphere *s, const Sphere *t) +{ + // Calculate the normal pointing towards the first sphere + contact->normal = vector3_difference(&t->center, &s->center); + vector3_normalize(&contact->normal); + + // Calculate the contact point in reference to the second sphere + contact->point = (Vector3){ + t->center.x - contact->normal.x * t->radius, + t->center.y - contact->normal.y * t->radius, + t->center.z - contact->normal.z * t->radius}; +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/physics/collision/shapes/triangle.h b/code/sb_halcyon/physics/collision/shapes/triangle.h new file mode 100644 index 00000000..c37583cd --- /dev/null +++ b/code/sb_halcyon/physics/collision/shapes/triangle.h @@ -0,0 +1,74 @@ +#ifndef TRIANGLE_H +#define TRIANGLE_H + +typedef struct +{ + Vector3 start; + Vector3 end; +} Edge; + +// Structure for a triangular plane +typedef struct Triangle +{ + Vector3 vertA; + Vector3 vertB; + Vector3 vertC; + Vector3 normal; + Edge edge; +} Triangle; + +void triangle_setVertices(Triangle *triangle, const Vector3 *vertexA, const Vector3 *vertexB, const Vector3 *vertexC); +bool triangle_containsPoint(const Triangle *triangle, const Vector3 *point); + +// Sets up the triangle plane and computes its normal +void triangle_setVertices(Triangle *triangle, const Vector3 *a, const Vector3 *b, const Vector3 *c) +{ + triangle->vertA = *a; + triangle->vertB = *b; + triangle->vertC = *c; + + // Calculate the normal by taking the cross product of two edges + Vector3 edge1 = vector3_difference(b, a); + Vector3 edge2 = vector3_difference(c, a); + Vector3 normal = vector3_returnCrossProduct(&edge1, &edge2); + vector3_normalize(&normal); + triangle->normal = normal; +} + +// Check if a point is on or near the plane of the triangle +bool triangle_containsPoint(const Triangle *triangle, const Vector3 *point) +{ + // Compute vector from point to one vertex + Vector3 point_to_a = vector3_difference(point, &triangle->vertA); + + // Compute dot product to find the distance to the plane along the normal + float distance = vector3_returnDotProduct(&point_to_a, &triangle->normal); + + // Consider the point on the plane if the distance is near zero (tolerance can be adjusted) + return fabsf(distance) < TOLERANCE; +} + +// Initialize edges of the hexagon from vertices +void hex_initEdges(Edge *edges, Vector3 *vertices) +{ + for (int i = 0; i < 6; ++i) + { + edges[i].start = vertices[i]; + edges[i].end = vertices[(i + 1) % 6]; // Loop back to the first vertex + } +} + +// Initialize each triangle in the hexagon +void hex_init(Triangle *hexagon, Vector3 *center, Vector3 *vertices) +{ + triangle_setVertices(&hexagon[0], center, &vertices[0], &vertices[1]); + triangle_setVertices(&hexagon[1], center, &vertices[1], &vertices[2]); + triangle_setVertices(&hexagon[2], center, &vertices[2], &vertices[3]); + triangle_setVertices(&hexagon[3], center, &vertices[3], &vertices[4]); + triangle_setVertices(&hexagon[4], center, &vertices[4], &vertices[5]); + triangle_setVertices(&hexagon[5], center, &vertices[5], &vertices[0]); + + hex_initEdges(&hexagon->edge, vertices); +} + +#endif // TRIANGLE_H diff --git a/code/sb_halcyon/physics/math/math_common.h b/code/sb_halcyon/physics/math/math_common.h new file mode 100644 index 00000000..e76bc4f1 --- /dev/null +++ b/code/sb_halcyon/physics/math/math_common.h @@ -0,0 +1,125 @@ +#ifndef PHYSICS_MATH_COMMON_H +#define PHYSICS_MATH_COMMON_H + +#define PI 3.141592653589f +#define PI_TIMES_2 6.28318530f + +#define TOLERANCE 1e-6f + +// ---------- Mathematics functions ---------- // + +float qi_sqrt(float number); + +float rad(float angle); +float deg(float rad); + +int clamp_int(int value, int lowerLimit, int upperLimit); +float clamp(float value, float lowerLimit, float upperLimit); + +float max2(float a, float b); +float min2(float a, float b); + +float min3(float a, float b, float c); +float max3(float a, float b, float c); + +bool sameSign(float a, float b); +bool approxEqual(float a, float b); + +bool isfinite(float x); + +/* quick inverse square root */ +inline float qi_sqrt(float number) +{ + float x2, y; + const float threehalfs = 1.5F; + + x2 = number * 0.5F; + y = number; + + union + { + float f; + long l; + } converter; + + converter.f = y; // Almacena el float en la unión + converter.l = 0x5f3759df - (converter.l >> 1); // Manipula los bits como long + y = converter.f; // Recupera el resultado como float + + y = y * (threehalfs - (x2 * y * y)); // 1st iteration + y = y * (threehalfs - (x2 * y * y)); // 2nd iteration (puede eliminarse) + + return y; +} + +/* degrees to radians */ +inline float rad(float angle) +{ + return PI / 180 * angle; +} + +/* radians to degrees */ +inline float deg(float rad) +{ + return 180 / PI * rad; +} + +/* return the result of the "value" clamped by "lowerLimit" and "upperLimit" */ +inline int clamp_int(int value, int lowerLimit, int upperLimit) +{ + assert(lowerLimit <= upperLimit); + return (value < lowerLimit) ? lowerLimit : (value > upperLimit) ? upperLimit + : value; +} + +/* return the result of the "value" clamped by "lowerLimit" and "upperLimit" */ +inline float clamp(float value, float lowerLimit, float upperLimit) +{ + assert(lowerLimit <= upperLimit); + return (value < lowerLimit) ? lowerLimit : (value > upperLimit) ? upperLimit + : value; +} + +/* return higher value*/ +inline float max2(float a, float b) +{ + return (a > b) ? a : b; +} + +/* return lower value*/ +inline float min2(float a, float b) +{ + return (a < b) ? a : b; +} + +/* return the minimum value among three values */ +inline float min3(float a, float b, float c) +{ + return (a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c); +} + +/* return the maximum value among three values */ +inline float max3(float a, float b, float c) +{ + return (a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c); +} + +/* return true if two values have the same sign */ +inline bool sameSign(float a, float b) +{ + return a * b >= 0.0f; +} + +/* Function to test if two real numbers are (almost) equal +We test if two numbers a and b are such that (a-b) are in [-EPSILON; EPSILON] */ +inline bool approxEqual(float a, float b) +{ + return (fabsf(a - b) < FLT_EPSILON); +} + +inline bool isfinite(float x) +{ + return (x == x) && (x != INFINITY) && (x != -INFINITY); +} + +#endif diff --git a/code/sb_halcyon/physics/math/math_functions.h b/code/sb_halcyon/physics/math/math_functions.h new file mode 100644 index 00000000..b22e1bf1 --- /dev/null +++ b/code/sb_halcyon/physics/math/math_functions.h @@ -0,0 +1,483 @@ +#ifndef MATH_FUNCTIONS_H +#define MATH_FUNCTIONS_H + +// function prototypes + +Vector3 vector3_multiplyByMatrix3x3(const Matrix3x3 *matrix, const Vector3 *vector); +Vector3 vector3_rotateByQuaternion(const Vector3 *v, const Quaternion *q); + +Vector3 vector3_transformToLocalSpace(const Vector3 *global_point, Vector3 local_center, Vector3 rotation); +Vector3 vector3_transformToGlobalSpace(const Vector3 *local_point, Vector3 local_center, Vector3 rotation); + +Vector3 vector3_reflect(const Vector3 *vector, const Vector3 *normal); + +Vector3 vector3_degToRad(const Vector3 *rotation); +Vector3 vector3_clamp(const Vector3 *vector, float maxLength); + +bool vector3_areParallel(const Vector3 *vector1, const Vector3 *vector2); +bool vector3_areOrthogonal(const Vector3 *vector1, const Vector3 *vector2); + +void point_rotateZYX(Vector3 *point, const Vector3 *rotation); +void point_rotateXYZ(Vector3 *point, const Vector3 *rotation); +void point_transformToLocalSpace(Vector3 *global_point, const Vector3 *local_center, const Vector3 *local_rotation); +void point_transformToGlobalSpace(Vector3 *local_point, const Vector3 *local_center, const Vector3 *local_rotation); + +Vector3 segment_closestToPoint(const Vector3 *seg_a, const Vector3 *seg_b, const Vector3 *point_c); +void segment_closestPointsWithSegment(const Vector3 *seg1_a, const Vector3 *seg1_b, const Vector3 *seg2_a, const Vector3 *seg2_b, Vector3 *closest_seg1, Vector3 *closest_seg2); +float segment_distanceToPoint(const Vector3 *a, const Vector3 *b, const Vector3 *p); + +float line_distanceToPoint(const Vector3 *linePointA, const Vector3 *linePointB, const Vector3 *point); + +float plane_intersectionWithSegment(const Vector3 *a, const Vector3 *b, float plane_displacement, const Vector3 *planeormal); + +void triangle_getBarycentricCoordinates(const Vector3 *a, const Vector3 *b, const Vector3 *c, const Vector3 *p, float *u, float *v, float *w); + +Matrix3x3 rotationMatrix_getFromEuler(const Vector3 *rotation); + +void rotate_normal(Vector3 *vector, const Vector3 *rotation); + +Vector3 vector3_fromQuaternion(Quaternion q); + +// function implementations + +inline Vector3 vector3_multiplyByMatrix3x3(const Matrix3x3 *matrix, const Vector3 *vector) +{ + return (Vector3){ + .x = matrix->row[0].x * vector->x + matrix->row[0].y * vector->y + matrix->row[0].z * vector->z, + .y = matrix->row[1].x * vector->x + matrix->row[1].y * vector->y + matrix->row[1].z * vector->z, + .z = matrix->row[2].x * vector->x + matrix->row[2].y * vector->y + matrix->row[2].z * vector->z}; +} + +inline Vector3 vector3_rotateByQuaternion(const Vector3 *v, const Quaternion *q) +{ + Vector3 u = {q->x, q->y, q->z}; + float s = q->w; + + Vector3 rv1 = vector3_returnScaled(&u, 2.0f * vector3_returnDotProduct(&u, v)); + Vector3 rv2 = vector3_returnScaled(v, (s * s - vector3_returnDotProduct(&u, &u))); + Vector3 crossProduct = vector3_returnCrossProduct(&u, v); + Vector3 rv3 = vector3_returnScaled(&crossProduct, 2.0f * s); + + Vector3 result = vector3_sum(&rv1, &rv2); + + return vector3_sum(&result, &rv3); +} + +inline Vector3 vector3_transformToLocalSpace(const Vector3 *global_point, Vector3 local_center, Vector3 rotation) +{ + // Translate point by the inverse of Box's center + Vector3 local_point = vector3_difference(global_point, &local_center); + + // Rotate the translated point by the inverse of Box's rotation + Vector3 inverse_rotation = vector3_getInverse(&rotation); + point_rotateXYZ(&local_point, &inverse_rotation); + + return local_point; +} + +inline Vector3 vector3_transformToGlobalSpace(const Vector3 *local_point, Vector3 local_center, Vector3 rotation) +{ + Vector3 global_point = *local_point; + // Apply rotation to the local point to get it in the global space orientation + point_rotateZYX(&global_point, &rotation); + + // Translate the rotated point by adding the Box's center + global_point = vector3_sum(&global_point, &local_center); + + return global_point; +} + +inline Vector3 vector3_degToRad(const Vector3 *rotation) +{ + Vector3 result; + result.x = rad(rotation->x); + result.y = rad(rotation->y); + result.z = rad(rotation->z); + return result; +} + +inline bool vector3_areParallel(const Vector3 *vector1, const Vector3 *vector2) +{ + Vector3 crossProduct = { + vector1->y * vector2->z - vector1->z * vector2->y, + vector1->z * vector2->x - vector1->x * vector2->z, + vector1->x * vector2->y - vector1->y * vector2->x}; + return (crossProduct.x * crossProduct.x + crossProduct.y * crossProduct.y + crossProduct.z * crossProduct.z) < 0.00001f; +} + +inline bool vector3_areOrthogonal(const Vector3 *vector1, const Vector3 *vector2) +{ + float dotProduct = vector1->x * vector2->x + vector1->y * vector2->y + vector1->z * vector2->z; + return fabsf(dotProduct) < 0.001f; +} + +/* clamp a vector to a maximum length */ +inline Vector3 vector3_clamp(const Vector3 *vector, float maxLength) +{ + float lengthSquare = vector->x * vector->x + vector->y * vector->y + vector->z * vector->z; + if (lengthSquare > maxLength * maxLength) + { + float length = sqrt(lengthSquare); + return (Vector3){vector->x * maxLength / length, vector->y * maxLength / length, vector->z * maxLength / length}; + } + return *vector; +} + +inline Vector3 vector3_reflect(const Vector3 *vector, const Vector3 *normal) +{ + Vector3 scaled_normal = vector3_returnScaled(normal, vector3_returnDotProduct(vector, normal) * 2.0f); + return vector3_difference(vector, &scaled_normal); +} + +/* compute and return a point on segment from "seg_a" and "seg_b" that is closest to point "point_c" */ +inline Vector3 segment_closestToPoint(const Vector3 *seg_a, const Vector3 *seg_b, const Vector3 *point_c) +{ + Vector3 ab = {seg_b->x - seg_a->x, seg_b->y - seg_a->y, seg_b->z - seg_a->z}; + Vector3 ac = {point_c->x - seg_a->x, point_c->y - seg_a->y, point_c->z - seg_a->z}; + float abLengthSquare = ab.x * ab.x + ab.y * ab.y + ab.z * ab.z; + + // If the segment has almost zero length + if (abLengthSquare < 1e-6f) + { + // Return one end-point of the segment as the closest point + return *seg_a; + } + + // Project point C onto "AB" line + float t = (ac.x * ab.x + ac.y * ab.y + ac.z * ab.z) / abLengthSquare; + + // If projected point onto the line is outside the segment, clamp it to the segment + if (t < 0.0f) + t = 0.0f; + if (t > 1.0f) + t = 1.0f; + + // Return the closest point on the segment + return (Vector3){seg_a->x + t * ab.x, seg_a->y + t * ab.y, seg_a->z + t * ab.z}; +} + +/* segment_closestPointInBetween +compute the closest points between two segments */ +inline void segment_closestPointsWithSegment(const Vector3 *seg1_a, const Vector3 *seg1_b, + const Vector3 *seg2_a, const Vector3 *seg2_b, + Vector3 *closest_seg1, Vector3 *closest_seg2) +{ + + Vector3 d1 = {seg1_b->x - seg1_a->x, seg1_b->y - seg1_a->y, seg1_b->z - seg1_a->z}; + Vector3 d2 = {seg2_b->x - seg2_a->x, seg2_b->y - seg2_a->y, seg2_b->z - seg2_a->z}; + Vector3 r = {seg1_a->x - seg2_a->x, seg1_a->y - seg2_a->y, seg1_a->z - seg2_a->z}; + float a = d1.x * d1.x + d1.y * d1.y + d1.z * d1.z; + float e = d2.x * d2.x + d2.y * d2.y + d2.z * d2.z; + float f = d2.x * r.x + d2.y * r.y + d2.z * r.z; + float s, t; + + // If both segments degenerate into points + if (a <= 1e-6f && e <= 1e-6f) + { + *closest_seg1 = *seg1_a; + *closest_seg2 = *seg2_a; + return; + } + if (a <= 1e-6f) + { // If first segment degenerates into a point + s = 0.0f; + // Compute the closest point on second segment + t = clamp(f / e, 0.0f, 1.0f); + } + else + { + float c = d1.x * r.x + d1.y * r.y + d1.z * r.z; + // If the second segment degenerates into a point + if (e <= 1e-6f) + { + t = 0.0f; + s = clamp(-c / a, 0.0f, 1.0f); + } + else + { + float b = d1.x * d2.x + d1.y * d2.y + d1.z * d2.z; + float denom = a * e - b * b; + // If the segments are not parallel + if (denom != 0.0f) + { + // Compute the closest point on line 1 to line 2 and + // clamp to first segment. + s = clamp((b * f - c * e) / denom, 0.0f, 1.0f); + } + else + { + // Pick an arbitrary point on first segment + s = 0.0f; + } + // Compute the point on line 2 closest to the closest point + // we have just found + t = (b * s + f) / e; + // If this closest point is inside second segment (t in [0, 1]), we are done. + // Otherwise, we clamp the point to the second segment and compute again the + // closest point on segment 1 + if (t < 0.0f) + { + t = 0.0f; + s = clamp(-c / a, 0.0f, 1.0f); + } + else if (t > 1.0f) + { + t = 1.0f; + s = clamp((b - c) / a, 0.0f, 1.0f); + } + } + } + + // Compute the closest points on both segments + *closest_seg1 = (Vector3){seg1_a->x + d1.x * s, seg1_a->y + d1.y * s, seg1_a->z + d1.z * s}; + *closest_seg2 = (Vector3){seg2_a->x + d2.x * t, seg2_a->y + d2.y * t, seg2_a->z + d2.z * t}; +} + +/* triangle_getBarycentricCoordinates +compute the barycentric coordinates u, v, w of a point p inside the triangle (a, b, c) */ +inline void triangle_getBarycentricCoordinates(const Vector3 *a, const Vector3 *b, const Vector3 *c, + const Vector3 *p, float *u, float *v, float *w) +{ + Vector3 v0 = {b->x - a->x, b->y - a->y, b->z - a->z}; + Vector3 v1 = {c->x - a->x, c->y - a->y, c->z - a->z}; + Vector3 v2 = {p->x - a->x, p->y - a->y, p->z - a->z}; + + float d00 = v0.x * v0.x + v0.y * v0.y + v0.z * v0.z; + float d01 = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z; + float d11 = v1.x * v1.x + v1.y * v1.y + v1.z * v1.z; + float d20 = v2.x * v0.x + v2.y * v0.y + v2.z * v0.z; + float d21 = v2.x * v1.x + v2.y * v1.y + v2.z * v1.z; + + float denom = d00 * d11 - d01 * d01; + *v = (d11 * d20 - d01 * d21) / denom; + *w = (d00 * d21 - d01 * d20) / denom; + *u = 1.0f - *v - *w; +} + +/* plane_intersectionWithSegment +compute the intersection between a plane and a segment */ +inline float plane_intersectionWithSegment(const Vector3 *a, const Vector3 *b, float plane_displacement, const Vector3 *planeormal) +{ + const float parallelEpsilon = 0.0001f; + float t = -1.0f; + + Vector3 ab = {b->x - a->x, b->y - a->y, b->z - a->z}; + float nDotAB = planeormal->x * ab.x + planeormal->y * ab.y + planeormal->z * ab.z; + + // If the segment is not parallel to the plane + if (fabsf(nDotAB) > parallelEpsilon) + { + t = (plane_displacement - (planeormal->x * a->x + planeormal->y * a->y + planeormal->z * a->z)) / nDotAB; + } + + return t; +} + +/* line_distanceToPoint +compute the distance between a point "point" and a line given by the points "linePointA" and "linePointB" */ +inline float line_distanceToPoint(const Vector3 *linePointA, const Vector3 *linePointB, const Vector3 *point) +{ + float distAB = sqrt((linePointB->x - linePointA->x) * (linePointB->x - linePointA->x) + + (linePointB->y - linePointA->y) * (linePointB->y - linePointA->y) + + (linePointB->z - linePointA->z) * (linePointB->z - linePointA->z)); + + if (distAB < 1e-6f) + { + return sqrt((point->x - linePointA->x) * (point->x - linePointA->x) + + (point->y - linePointA->y) * (point->y - linePointA->y) + + (point->z - linePointA->z) * (point->z - linePointA->z)); + } + + Vector3 crossProduct = {(point->y - linePointA->y) * (point->z - linePointB->z) - (point->z - linePointA->z) * (point->y - linePointB->y), + (point->z - linePointA->z) * (point->x - linePointB->x) - (point->x - linePointA->x) * (point->z - linePointB->z), + (point->x - linePointA->x) * (point->y - linePointB->y) - (point->y - linePointA->y) * (point->x - linePointB->x)}; + + float crossLength = sqrt(crossProduct.x * crossProduct.x + crossProduct.y * crossProduct.y + crossProduct.z * crossProduct.z); + + return crossLength / distAB; +} + +inline float segment_distanceToPoint(const Vector3 *a, const Vector3 *b, const Vector3 *p) +{ + // Calculate the vector from a to b and from a to p + Vector3 ab = vector3_difference(b, a); + Vector3 ap = vector3_difference(p, a); + + // Project the vector ap onto ab and clamp the value of t + float t = vector3_returnDotProduct(&ap, &ab) / vector3_returnDotProduct(&ab, &ab); + t = max2(0, min2(1, t)); // Clamp t to the range [0, 1] + + // Calculate the closest point on the segment + Vector3 scaled_ab = vector3_returnScaled(&ab, t); + Vector3 closestPoint = vector3_sum(a, &scaled_ab); + + // Calculate the distance from p to the closest point + Vector3 diff = vector3_difference(p, &closestPoint); + return vector3_magnitude(&diff); +} + +Matrix3x3 rotationMatrix_getFromEuler(const Vector3 *rotation) +{ + float rad_x = rad(rotation->x); + float cos_rad_x = fm_cosf(rad_x); + float sin_rad_x = fm_sinf(rad_x); + + float rad_y = rad(rotation->y); + float cos_rad_y = fm_cosf(rad_y); + float sin_rad_y = fm_sinf(rad_y); + + float rad_z = rad(rotation->z); + float cos_rad_z = fm_cosf(rad_z); + float sin_rad_z = fm_sinf(rad_z); + + Matrix3x3 R_x = { + .row = { + {1, 0, 0}, + {0, cos_rad_x, -sin_rad_x}, + {0, sin_rad_x, cos_rad_x}}}; + + Matrix3x3 R_y = { + .row = { + {cos_rad_y, 0, sin_rad_y}, + {0, 1, 0}, + {-sin_rad_y, 0, cos_rad_y}}}; + + Matrix3x3 R_z = { + .row = { + {cos_rad_z, -sin_rad_z, 0}, + {sin_rad_z, cos_rad_z, 0}, + {0, 0, 1}}}; + + Matrix3x3 R_temp = matrix3x3_multiply(&R_x, &R_y); + Matrix3x3 R = matrix3x3_multiply(&R_temp, &R_z); + + return R; +} + +// this function delivers an elegant simplification saving significant performance +// in comparison with the standard way to do it using matrix and vector operations; +// +// Matrix3x3 matrix = rotationMatrix_getFromEuler(rotation); +// point = matrix3x3_multiplyByVector(matrix, point); +// +// as with the rotation matrix algorithm, this does not work when the point lays on any axis +// for that you must use a quaternion rotation +void point_rotateZYX(Vector3 *point, const Vector3 *rotation) +{ + float rad_x = rad(rotation->x); + float cos_rad_x = fm_cosf(rad_x); + float sin_rad_x = fm_sinf(rad_x); + + float rad_y = rad(rotation->y); + float cos_rad_y = fm_cosf(rad_y); + float sin_rad_y = fm_sinf(rad_y); + + float rad_z = rad(rotation->z); + float cos_rad_z = fm_cosf(rad_z); + float sin_rad_z = fm_sinf(rad_z); + + // Rotate around Z axis + float xZ = point->x * cos_rad_z - point->y * sin_rad_z; + float yZ = point->x * sin_rad_z + point->y * cos_rad_z; + + // Rotate around Y axis + float xY = xZ * cos_rad_y + point->z * sin_rad_y; + float zY = -xZ * sin_rad_y + point->z * cos_rad_y; + + // Rotate around X axis + point->y = yZ * cos_rad_x - zY * sin_rad_x; + point->z = yZ * sin_rad_x + zY * cos_rad_x; + point->x = xY; +} + +void point_rotateXYZ(Vector3 *point, const Vector3 *rotation) +{ + float rad_x = rad(rotation->x); + float cos_rad_x = fm_cosf(rad_x); + float sin_rad_x = fm_sinf(rad_x); + + float rad_y = rad(rotation->y); + float cos_rad_y = fm_cosf(rad_y); + float sin_rad_y = fm_sinf(rad_y); + + float rad_z = rad(rotation->z); + float cos_rad_z = fm_cosf(rad_z); + float sin_rad_z = fm_sinf(rad_z); + + // Rotate around X axis (inverse order) + float yX = point->y * cos_rad_x + point->z * sin_rad_x; + float zX = -point->y * sin_rad_x + point->z * cos_rad_x; + + // Rotate around Y axis (inverse order) + float xY = point->x * cos_rad_y - zX * sin_rad_y; + float zY = point->x * sin_rad_y + zX * cos_rad_y; + + // Rotate around Z axis (inverse order) + float xZ = xY * cos_rad_z + yX * sin_rad_z; + float yZ = -xY * sin_rad_z + yX * cos_rad_z; + + point->x = xZ; + point->y = yZ; + point->z = zY; +} + +void point_transformToLocalSpace(Vector3 *global_point, const Vector3 *local_center, const Vector3 *local_rotation) +{ + // Translate point by the inverse of Box's center + vector3_subtract(global_point, local_center); + + // Rotate the translated point by the negative of Box's rotation + Vector3 inverse_rotation = vector3_getInverse(local_rotation); + point_rotateZYX(global_point, &inverse_rotation); +} + +void point_transformToGlobalSpace(Vector3 *local_point, const Vector3 *local_center, const Vector3 *local_rotation) +{ + // Apply rotation to the local point to get it in the global space orientation + Vector3 inverse_rotation = vector3_getInverse(local_rotation); + point_rotateXYZ(local_point, &inverse_rotation); + + // Translate the rotated point by adding the Box's center + vector3_add(local_point, local_center); +} + +// another very convenient and very difficult to figure out algorithm +void rotate_normal(Vector3 *vector, const Vector3 *rotation) +{ + Vector3 rad_rotation = vector3_degToRad(rotation); + Quaternion q_rotation = quaternion_getFromVector(&rad_rotation); + *vector = vector3_rotateByQuaternion(vector, &q_rotation); +} + +void rotate_vector(Vector3 *vector, const Vector3 *rotation) +{ + Vector3 rad_rotation = vector3_degToRad(rotation); + Quaternion q_rotation = quaternion_getFromVector(&rad_rotation); + *vector = vector3_rotateByQuaternion(vector, &q_rotation); +} + +inline Vector3 vector3_fromQuaternion(Quaternion q) +{ + Vector3 euler; + + // Roll (X-axis rotation) + float sinr_cosp = 2 * (q.w * q.x + q.y * q.z); + float cosr_cosp = 1 - 2 * (q.x * q.x + q.y * q.y); + euler.x = atan2f(sinr_cosp, cosr_cosp); + + // Pitch (Y-axis rotation) + float sinp = 2 * (q.w * q.y - q.z * q.x); + if (fabs(sinp) >= 1) + euler.y = copysignf(M_PI / 2, sinp); // Use 90 degrees if out of range + else + euler.y = asinf(sinp); + + // Yaw (Z-axis rotation) + float siny_cosp = 2 * (q.w * q.z + q.x * q.y); + float cosy_cosp = 1 - 2 * (q.y * q.y + q.z * q.z); + euler.z = atan2f(siny_cosp, cosy_cosp); + + return euler; +} + +#endif diff --git a/code/sb_halcyon/physics/math/matrix2x2.h b/code/sb_halcyon/physics/math/matrix2x2.h new file mode 100644 index 00000000..133b24a3 --- /dev/null +++ b/code/sb_halcyon/physics/math/matrix2x2.h @@ -0,0 +1,254 @@ +#ifndef MATRIX2X2_H +#define MATRIX2X2_H + +// Libraries +#include +#include +#include +#include "vector2.h" // Asegúrate de que esto incluya la definición de Vector2 + +typedef struct +{ + Vector2 row[2]; +} Matrix2x2; + +// Function prototypes +void matrix2x2_init(Matrix2x2 *matrix); +void matrix2x2_clear(Matrix2x2 *matrix); + +void matrix2x2_set(Matrix2x2 *matrix, float a1, float a2, float b1, float b2); +void matrix2x2_setWithValue(Matrix2x2 *matrix, float value); + +Vector2 matrix2x2_returnColumn(const Matrix2x2 *matrix, int i); +Vector2 matrix2x2_returnRow(const Matrix2x2 *matrix, int i); + +Matrix2x2 matrix2x2_sum(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2); +void matrix2x2_add(Matrix2x2 *matrix1, const Matrix2x2 *matrix2); + +Matrix2x2 matrix2x2_difference(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2); +void matrix2x2_subtract(Matrix2x2 *matrix1, const Matrix2x2 *matrix2); + +Matrix2x2 matrix2x2_returnScaled(const Matrix2x2 *matrix, float scalar); +void matrix2x2_scale(Matrix2x2 *matrix, float scalar); + +Matrix2x2 matrix2x2_returnProduct(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2); +Vector2 matrix2x2_returnProductByVector(const Matrix2x2 *matrix, const Vector2 *vector); + +Matrix2x2 matrix2x2_returnNegative(const Matrix2x2 *matrix); +Matrix2x2 matrix2x2_returnTranspose(const Matrix2x2 *matrix); +float matrix2x2_returnDeterminant(const Matrix2x2 *matrix); +float matrix2x2_returnTrace(const Matrix2x2 *matrix); +Matrix2x2 matrix2x2_returnInverse(const Matrix2x2 *matrix); +Matrix2x2 matrix2x2_returnAbsoluteMatrix(const Matrix2x2 *matrix); + +void matrix2x2_setIdentity(Matrix2x2 *matrix); +Matrix2x2 matrix2x2_returnIdentity(); + +int matrix2x2_equals(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2); +int matrix2x2_notEquals(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2); + +// Implementations + +/* Initializes all values in the matrix to zero. */ +void matrix2x2_init(Matrix2x2 *matrix) +{ + matrix2x2_set(matrix, 0.0f, 0.0f, 0.0f, 0.0f); +} + +/* Initializes the matrix with a given value. */ +void matrix2x2_setWithValue(Matrix2x2 *matrix, float value) +{ + matrix2x2_set(matrix, value, value, value, value); +} + +/* Sets all values in the matrix. */ +void matrix2x2_set(Matrix2x2 *matrix, float a1, float a2, float b1, float b2) +{ + matrix->row[0].x = a1; + matrix->row[0].y = a2; + matrix->row[1].x = b1; + matrix->row[1].y = b2; +} + +/* Sets the matrix to zero. */ +void matrix2x2_clear(Matrix2x2 *matrix) +{ + matrix2x2_set(matrix, 0.0f, 0.0f, 0.0f, 0.0f); +} + +/* Returns a column of the matrix. */ +Vector2 matrix2x2_returnColumn(const Matrix2x2 *matrix, int i) +{ + assert(i >= 0 && i < 2); + return (Vector2){matrix->row[0].x, matrix->row[1].x}; +} + +/* Returns a row of the matrix. */ +Vector2 matrix2x2_returnRow(const Matrix2x2 *matrix, int i) +{ + assert(i >= 0 && i < 2); + return matrix->row[i]; +} + +/* Returns the transpose of the matrix. */ +Matrix2x2 matrix2x2_returnTranspose(const Matrix2x2 *matrix) +{ + return (Matrix2x2){ + .row = { + {matrix->row[0].x, matrix->row[1].x}, + {matrix->row[0].y, matrix->row[1].y}}}; +} + +/* Returns the determinant of the matrix. */ +float matrix2x2_returnDeterminant(const Matrix2x2 *matrix) +{ + return matrix->row[0].x * matrix->row[1].y - matrix->row[1].x * matrix->row[0].y; +} + +/* Returns the trace of the matrix. */ +float matrix2x2_returnTrace(const Matrix2x2 *matrix) +{ + return matrix->row[0].x + matrix->row[1].y; +} + +/* Sets the matrix to the identity matrix. */ +void matrix2x2_setIdentity(Matrix2x2 *matrix) +{ + matrix2x2_set(matrix, 1.0f, 0.0f, 0.0f, 1.0f); +} + +/* Returns the 2x2 identity matrix. */ +Matrix2x2 matrix2x2_returnIdentity() +{ + Matrix2x2 identityMatrix; + matrix2x2_setIdentity(&identityMatrix); + return identityMatrix; +} + +/* Returns the 2x2 zero matrix. */ +Matrix2x2 matrix2x2_zero() +{ + Matrix2x2 zeroMatrix; + matrix2x2_clear(&zeroMatrix); + return zeroMatrix; +} + +/* Returns the inverse of the matrix. */ +Matrix2x2 matrix2x2_returnInverse(const Matrix2x2 *matrix) +{ + float determinant = matrix2x2_returnDeterminant(matrix); + assert(determinant > FLT_EPSILON); + + float invDeterminant = 1.0f / determinant; + + return (Matrix2x2){ + .row = { + {matrix->row[1].y * invDeterminant, -matrix->row[0].y * invDeterminant}, + {-matrix->row[1].x * invDeterminant, matrix->row[0].x * invDeterminant}}}; +} + +/* Returns the matrix with absolute values. */ +Matrix2x2 matrix2x2_returnAbsoluteMatrix(const Matrix2x2 *matrix) +{ + return (Matrix2x2){ + .row = { + {fabsf(matrix->row[0].x), fabsf(matrix->row[0].y)}, + {fabsf(matrix->row[1].x), fabsf(matrix->row[1].y)}}}; +} + +/* Adds two matrices. */ +Matrix2x2 matrix2x2_sum(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + return (Matrix2x2){ + .row = { + {matrix1->row[0].x + matrix2->row[0].x, matrix1->row[0].y + matrix2->row[0].y}, + {matrix1->row[1].x + matrix2->row[1].x, matrix1->row[1].y + matrix2->row[1].y}}}; +} + +/* Subtracts matrix2 from matrix1. */ +Matrix2x2 matrix2x2_difference(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + return (Matrix2x2){ + .row = { + {matrix1->row[0].x - matrix2->row[0].x, matrix1->row[0].y - matrix2->row[0].y}, + {matrix1->row[1].x - matrix2->row[1].x, matrix1->row[1].y - matrix2->row[1].y}}}; +} + +/* Returns the negative of the matrix. */ +Matrix2x2 matrix2x2_returnNegative(const Matrix2x2 *matrix) +{ + return (Matrix2x2){ + .row = { + {-matrix->row[0].x, -matrix->row[0].y}, + {-matrix->row[1].x, -matrix->row[1].y}}}; +} + +/* Multiplies the matrix by a scalar. */ +Matrix2x2 matrix2x2_returnScaled(const Matrix2x2 *matrix, float scalar) +{ + return (Matrix2x2){ + .row = { + {matrix->row[0].x * scalar, matrix->row[0].y * scalar}, + {matrix->row[1].x * scalar, matrix->row[1].y * scalar}}}; +} + +/* Multiplies two matrices. */ +Matrix2x2 matrix2x2_returnProduct(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + return (Matrix2x2){ + .row = { + {matrix1->row[0].x * matrix2->row[0].x + matrix1->row[0].y * matrix2->row[1].x, + matrix1->row[0].x * matrix2->row[0].y + matrix1->row[0].y * matrix2->row[1].y}, + {matrix1->row[1].x * matrix2->row[0].x + matrix1->row[1].y * matrix2->row[1].x, + matrix1->row[1].x * matrix2->row[0].y + matrix1->row[1].y * matrix2->row[1].y}}}; +} + +/* Multiplies the matrix by a vector. */ +Vector2 matrix2x2_returnProductByVector(const Matrix2x2 *matrix, const Vector2 *vector) +{ + return (Vector2){ + .x = matrix->row[0].x * vector->x + matrix->row[0].y * vector->y, + .y = matrix->row[1].x * vector->x + matrix->row[1].y * vector->y}; +} + +/* Checks if two matrices are equal. */ +int matrix2x2_equals(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + return (matrix1->row[0].x == matrix2->row[0].x && matrix1->row[0].y == matrix2->row[0].y && + matrix1->row[1].x == matrix2->row[1].x && matrix1->row[1].y == matrix2->row[1].y); +} + +/* Checks if two matrices are not equal. */ +int matrix2x2_notEquals(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + return !matrix2x2_equals(matrix1, matrix2); +} + +/* Adds matrix2 to matrix1 and assigns the result to matrix1. */ +void matrix2x2_add(Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + matrix1->row[0].x += matrix2->row[0].x; + matrix1->row[0].y += matrix2->row[0].y; + matrix1->row[1].x += matrix2->row[1].x; + matrix1->row[1].y += matrix2->row[1].y; +} + +/* Subtracts matrix2 from matrix1 and assigns the result to matrix1. */ +void matrix2x2_subtract(Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + matrix1->row[0].x -= matrix2->row[0].x; + matrix1->row[0].y -= matrix2->row[0].y; + matrix1->row[1].x -= matrix2->row[1].x; + matrix1->row[1].y -= matrix2->row[1].y; +} + +/* Multiplies matrix1 by a scalar and assigns the result to matrix1. */ +void matrix2x2_scale(Matrix2x2 *matrix, float scalar) +{ + matrix->row[0].x *= scalar; + matrix->row[0].y *= scalar; + matrix->row[1].x *= scalar; + matrix->row[1].y *= scalar; +} + +#endif // MATRIX2X2_H diff --git a/code/sb_halcyon/physics/math/matrix3x3.h b/code/sb_halcyon/physics/math/matrix3x3.h new file mode 100644 index 00000000..c8a09f7b --- /dev/null +++ b/code/sb_halcyon/physics/math/matrix3x3.h @@ -0,0 +1,312 @@ +/** + * @file + * + * holds a 3x3 matrix. + */ + +#ifndef MATRIX3X3_H +#define MATRIX3X3_H + +#include "vector3.h" + +typedef struct +{ + Vector3 row[3]; +} Matrix3x3; + +// Function prototypes +void matrix3x3_init(Matrix3x3 *matrix); +void matrix3x3_clear(Matrix3x3 *matrix); + +void matrix3x3_set(Matrix3x3 *matrix, float a1, float a2, float a3, float b1, float b2, float b3, float c1, float c2, float c3); +void matrix3x3_setWithValue(Matrix3x3 *matrix, float value); + +Vector3 matrix3x3_returnColumn(const Matrix3x3 *matrix, int i); +Vector3 matrix3x3_returnRow(const Matrix3x3 *matrix, int i); + +void matrix3x3_add(Matrix3x3 *matrix1, const Matrix3x3 *matrix2); +Matrix3x3 matrix3x3_sum(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2); + +void matrix3x3_subtract(Matrix3x3 *matrix1, const Matrix3x3 *matrix2); +Matrix3x3 matrix3x3_difference(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2); + +Matrix3x3 matrix3x3_returnScaled(const Matrix3x3 *matrix, float scalar); +void matrix3x3_scale(Matrix3x3 *matrix, float scalar); + +Matrix3x3 matrix3x3_multiply(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2); +Vector3 matrix3x3_multiplyByVector(const Matrix3x3 *matrix, const Vector3 *vector); + +Matrix3x3 matrix3x3_returnNegative(const Matrix3x3 *matrix); +Matrix3x3 matrix3x3_returnTranspose(const Matrix3x3 *matrix); +float matrix3x3_returnDeterminant(const Matrix3x3 *matrix); +float matrix3x3_returnTrace(const Matrix3x3 *matrix); +Matrix3x3 matrix3x3_returnInverse(const Matrix3x3 *matrix); +Matrix3x3 matrix3x3_returnAbsoluteMatrix(const Matrix3x3 *matrix); + +void matrix3x3_setIdentity(Matrix3x3 *matrix); +Matrix3x3 matrix3x3_returnIdentity(); + +Matrix3x3 matrix3x3_computeSkewSymmetricMatrixForCrossProduct(const Vector3 *vector); + +int matrix3x3_equals(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2); +int matrix3x3_notEquals(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2); + +// Implementations + +/* Initializes the matrix to zero. */ +void matrix3x3_init(Matrix3x3 *matrix) +{ + matrix3x3_set(matrix, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); +} + +/* Initializes the matrix with a given value. */ +void matrix3x3_setWithValue(Matrix3x3 *matrix, float value) +{ + matrix3x3_set(matrix, value, value, value, value, value, value, value, value, value); +} + +/* Sets all values in the matrix. */ +void matrix3x3_set(Matrix3x3 *matrix, float a1, float a2, float a3, + float b1, float b2, float b3, float c1, float c2, float c3) +{ + matrix->row[0].x = a1; + matrix->row[0].y = a2; + matrix->row[0].z = a3; + matrix->row[1].x = b1; + matrix->row[1].y = b2; + matrix->row[1].z = b3; + matrix->row[2].x = c1; + matrix->row[2].y = c2; + matrix->row[2].z = c3; +} + +/* Sets the matrix to zero. */ +void matrix3x3_clear(Matrix3x3 *matrix) +{ + vector3_set(&matrix->row[0], 0.0f, 0.0f, 0.0f); + vector3_set(&matrix->row[1], 0.0f, 0.0f, 0.0f); + vector3_set(&matrix->row[2], 0.0f, 0.0f, 0.0f); +} + +/* Returns a column of the matrix. */ +Vector3 matrix3x3_returnColumn(const Matrix3x3 *matrix, int i) +{ + assert(i >= 0 && i < 3); + return (Vector3){matrix->row[0].x, matrix->row[1].x, matrix->row[2].x}; +} + +/* Returns a row of the matrix. */ +Vector3 matrix3x3_returnRow(const Matrix3x3 *matrix, int i) +{ + assert(i >= 0 && i < 3); + return matrix->row[i]; +} + +/* Returns the transpose of the matrix. */ +Matrix3x3 matrix3x3_returnTranspose(const Matrix3x3 *matrix) +{ + return (Matrix3x3){ + .row = { + {matrix->row[0].x, matrix->row[1].x, matrix->row[2].x}, + {matrix->row[0].y, matrix->row[1].y, matrix->row[2].y}, + {matrix->row[0].z, matrix->row[1].z, matrix->row[2].z}}}; +} + +/* Returns the determinant of the matrix. */ +float matrix3x3_returnDeterminant(const Matrix3x3 *matrix) +{ + return (matrix->row[0].x * (matrix->row[1].y * matrix->row[2].z - matrix->row[2].y * matrix->row[1].z) - + matrix->row[0].y * (matrix->row[1].x * matrix->row[2].z - matrix->row[2].x * matrix->row[1].z) + + matrix->row[0].z * (matrix->row[1].x * matrix->row[2].y - matrix->row[2].x * matrix->row[1].y)); +} + +/* Returns the trace of the matrix. */ +float matrix3x3_returnTrace(const Matrix3x3 *matrix) +{ + return (matrix->row[0].x + matrix->row[1].y + matrix->row[2].z); +} + +/* Returns the inverse of the matrix. */ +Matrix3x3 matrix3x3_returnInverse(const Matrix3x3 *matrix) +{ + float determinant = matrix3x3_returnDeterminant(matrix); + // Check if the determinant is equal to zero + assert(determinant != 0.0f); + + float invDeterminant = 1.0f / determinant; + + Matrix3x3 tempMatrix; + matrix3x3_set(&tempMatrix, + (matrix->row[1].y * matrix->row[2].z - matrix->row[2].y * matrix->row[1].z), + -(matrix->row[0].y * matrix->row[2].z - matrix->row[2].y * matrix->row[0].z), + (matrix->row[0].y * matrix->row[1].z - matrix->row[0].z * matrix->row[1].y), + -(matrix->row[1].x * matrix->row[2].z - matrix->row[2].x * matrix->row[1].z), + (matrix->row[0].x * matrix->row[2].z - matrix->row[2].x * matrix->row[0].z), + -(matrix->row[0].x * matrix->row[1].z - matrix->row[1].x * matrix->row[0].z), + (matrix->row[1].x * matrix->row[2].y - matrix->row[2].x * matrix->row[1].y), + -(matrix->row[0].x * matrix->row[2].y - matrix->row[2].x * matrix->row[0].y), + (matrix->row[0].x * matrix->row[1].y - matrix->row[0].y * matrix->row[1].x)); + + // Return the inverse matrix + return matrix3x3_returnScaled(&tempMatrix, invDeterminant); +} + +/* Returns the matrix with absolute values. */ +Matrix3x3 matrix3x3_returnAbsoluteMatrix(const Matrix3x3 *matrix) +{ + return (Matrix3x3){ + .row = { + {fabsf(matrix->row[0].x), fabsf(matrix->row[0].y), fabsf(matrix->row[0].z)}, + {fabsf(matrix->row[1].x), fabsf(matrix->row[1].y), fabsf(matrix->row[1].z)}, + {fabsf(matrix->row[2].x), fabsf(matrix->row[2].y), fabsf(matrix->row[2].z)}}}; +} + +/* Sets the matrix to the identity matrix. */ +void matrix3x3_setIdentity(Matrix3x3 *matrix) +{ + matrix3x3_set(matrix, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f); +} + +/* Returns the 3x3 identity matrix. */ +Matrix3x3 matrix3x3_returnIdentity() +{ + Matrix3x3 identityMatrix; + matrix3x3_setIdentity(&identityMatrix); + return identityMatrix; +} + +/* Returns a skew-symmetric matrix for cross product. */ +Matrix3x3 matrix3x3_computeSkewSymmetricMatrixForCrossProduct(const Vector3 *vector) +{ + return (Matrix3x3){ + .row = { + {0.0f, -vector->z, vector->y}, + {vector->z, 0.0f, -vector->x}, + {-vector->y, vector->x, 0.0f}}}; +} + +/* Adds two matrices. */ +Matrix3x3 matrix3x3_sum(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + return (Matrix3x3){ + .row = { + {matrix1->row[0].x + matrix2->row[0].x, matrix1->row[0].y + matrix2->row[0].y, matrix1->row[0].z + matrix2->row[0].z}, + {matrix1->row[1].x + matrix2->row[1].x, matrix1->row[1].y + matrix2->row[1].y, matrix1->row[1].z + matrix2->row[1].z}, + {matrix1->row[2].x + matrix2->row[2].x, matrix1->row[2].y + matrix2->row[2].y, matrix1->row[2].z + matrix2->row[2].z}}}; +} + +/* Subtracts matrix2 from matrix1. */ +Matrix3x3 matrix3x3_difference(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + return (Matrix3x3){ + .row = { + {matrix1->row[0].x - matrix2->row[0].x, matrix1->row[0].y - matrix2->row[0].y, matrix1->row[0].z - matrix2->row[0].z}, + {matrix1->row[1].x - matrix2->row[1].x, matrix1->row[1].y - matrix2->row[1].y, matrix1->row[1].z - matrix2->row[1].z}, + {matrix1->row[2].x - matrix2->row[2].x, matrix1->row[2].y - matrix2->row[2].y, matrix1->row[2].z - matrix2->row[2].z}}}; +} + +/* Returns the negative of the matrix. */ +Matrix3x3 matrix3x3_returnNegative(const Matrix3x3 *matrix) +{ + return (Matrix3x3){ + .row = { + {-matrix->row[0].x, -matrix->row[0].y, -matrix->row[0].z}, + {-matrix->row[1].x, -matrix->row[1].y, -matrix->row[1].z}, + {-matrix->row[2].x, -matrix->row[2].y, -matrix->row[2].z}}}; +} + +/* Multiplies the matrix by a scalar. */ +Matrix3x3 matrix3x3_returnScaled(const Matrix3x3 *matrix, float scalar) +{ + return (Matrix3x3){ + .row = { + {matrix->row[0].x * scalar, matrix->row[0].y * scalar, matrix->row[0].z * scalar}, + {matrix->row[1].x * scalar, matrix->row[1].y * scalar, matrix->row[1].z * scalar}, + {matrix->row[2].x * scalar, matrix->row[2].y * scalar, matrix->row[2].z * scalar}}}; +} + +/* Multiplies two matrices. */ +Matrix3x3 matrix3x3_multiply(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + Matrix3x3 result; + result.row[0].x = matrix1->row[0].x * matrix2->row[0].x + matrix1->row[0].y * matrix2->row[1].x + matrix1->row[0].z * matrix2->row[2].x; + result.row[0].y = matrix1->row[0].x * matrix2->row[0].y + matrix1->row[0].y * matrix2->row[1].y + matrix1->row[0].z * matrix2->row[2].y; + result.row[0].z = matrix1->row[0].x * matrix2->row[0].z + matrix1->row[0].y * matrix2->row[1].z + matrix1->row[0].z * matrix2->row[2].z; + + result.row[1].x = matrix1->row[1].x * matrix2->row[0].x + matrix1->row[1].y * matrix2->row[1].x + matrix1->row[1].z * matrix2->row[2].x; + result.row[1].y = matrix1->row[1].x * matrix2->row[0].y + matrix1->row[1].y * matrix2->row[1].y + matrix1->row[1].z * matrix2->row[2].y; + result.row[1].z = matrix1->row[1].x * matrix2->row[0].z + matrix1->row[1].y * matrix2->row[1].z + matrix1->row[1].z * matrix2->row[2].z; + + result.row[2].x = matrix1->row[2].x * matrix2->row[0].x + matrix1->row[2].y * matrix2->row[1].x + matrix1->row[2].z * matrix2->row[2].x; + result.row[2].y = matrix1->row[2].x * matrix2->row[0].y + matrix1->row[2].y * matrix2->row[1].y + matrix1->row[2].z * matrix2->row[2].y; + result.row[2].z = matrix1->row[2].x * matrix2->row[0].z + matrix1->row[2].y * matrix2->row[1].z + matrix1->row[2].z * matrix2->row[2].z; + + return result; +} + +/* Multiplies the matrix by a vector. */ +Vector3 matrix3x3_multiplyByVector(const Matrix3x3 *matrix, const Vector3 *vector) +{ + return (Vector3){ + .x = matrix->row[0].x * vector->x + matrix->row[0].y * vector->y + matrix->row[0].z * vector->z, + .y = matrix->row[1].x * vector->x + matrix->row[1].y * vector->y + matrix->row[1].z * vector->z, + .z = matrix->row[2].x * vector->x + matrix->row[2].y * vector->y + matrix->row[2].z * vector->z}; +} + +/* Checks if two matrices are equal. */ +int matrix3x3_equals(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + return (matrix1->row[0].x == matrix2->row[0].x && matrix1->row[0].y == matrix2->row[0].y && matrix1->row[0].z == matrix2->row[0].z && + matrix1->row[1].x == matrix2->row[1].x && matrix1->row[1].y == matrix2->row[1].y && matrix1->row[1].z == matrix2->row[1].z && + matrix1->row[2].x == matrix2->row[2].x && matrix1->row[2].y == matrix2->row[2].y && matrix1->row[2].z == matrix2->row[2].z); +} + +/* Checks if two matrices are not equal. */ +int matrix3x3_notEquals(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + return !matrix3x3_equals(matrix1, matrix2); +} + +/* Adds matrix2 to matrix1 and assigns the result to matrix1. */ +void matrix3x3_add(Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + matrix1->row[0].x += matrix2->row[0].x; + matrix1->row[0].y += matrix2->row[0].y; + matrix1->row[0].z += matrix2->row[0].z; + matrix1->row[1].x += matrix2->row[1].x; + matrix1->row[1].y += matrix2->row[1].y; + matrix1->row[1].z += matrix2->row[1].z; + matrix1->row[2].x += matrix2->row[2].x; + matrix1->row[2].y += matrix2->row[2].y; + matrix1->row[2].z += matrix2->row[2].z; +} + +/* Subtracts matrix2 from matrix1 and assigns the result to matrix1. */ +void matrix3x3_subtract(Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + matrix1->row[0].x -= matrix2->row[0].x; + matrix1->row[0].y -= matrix2->row[0].y; + matrix1->row[0].z -= matrix2->row[0].z; + matrix1->row[1].x -= matrix2->row[1].x; + matrix1->row[1].y -= matrix2->row[1].y; + matrix1->row[1].z -= matrix2->row[1].z; + matrix1->row[2].x -= matrix2->row[2].x; + matrix1->row[2].y -= matrix2->row[2].y; + matrix1->row[2].z -= matrix2->row[2].z; +} + +/* Multiplies matrix1 by a scalar and assigns the result to matrix1. */ +void matrix3x3_scale(Matrix3x3 *matrix, float scalar) +{ + matrix->row[0].x *= scalar; + matrix->row[0].y *= scalar; + matrix->row[0].z *= scalar; + matrix->row[1].x *= scalar; + matrix->row[1].y *= scalar; + matrix->row[1].z *= scalar; + matrix->row[2].x *= scalar; + matrix->row[2].y *= scalar; + matrix->row[2].z *= scalar; +} + +#endif // MATRIX3X3_H diff --git a/code/sb_halcyon/physics/math/physics_math.h b/code/sb_halcyon/physics/math/physics_math.h new file mode 100644 index 00000000..299f295e --- /dev/null +++ b/code/sb_halcyon/physics/math/physics_math.h @@ -0,0 +1,23 @@ +#ifndef PHYSICS_MATH_H +#define PHYSICS_MATH_H + +// Libraries + +#include +#include + +#include "math_common.h" + +#include "vector2.h" +#include "vector3.h" + +#include "matrix3x3.h" +#include "matrix2x2.h" + +#include "quaternion.h" + +#include "transform.h" + +#include "math_functions.h" + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/physics/math/quaternion.h b/code/sb_halcyon/physics/math/quaternion.h new file mode 100644 index 00000000..ffd13789 --- /dev/null +++ b/code/sb_halcyon/physics/math/quaternion.h @@ -0,0 +1,473 @@ +/** + * @file + * + * Holds a quaternion. + */ + +#ifndef QUATERNION_H +#define QUATERNION_H + +typedef struct +{ + float x; + float y; + float z; + float w; +} Quaternion; + +// Function prototypes +void quaternion_set(Quaternion *q, float x, float y, float z, float w); +void quaternion_init(Quaternion *q); +void quaternion_clear(Quaternion *q); + +void quaternion_setWithVector(Quaternion *q, float w, const Vector3 *v); + +Quaternion quaternion_sum(const Quaternion *q, const Quaternion *r); +Quaternion quaternion_difference(const Quaternion *q, const Quaternion *r); +Quaternion quaternion_returnScaled(const Quaternion *q, float scalar); +Quaternion quaternion_returnProduct(const Quaternion *q, const Quaternion *r); +Vector3 quaternion_getVectorProduct(const Quaternion *q, const Vector3 *vector); + +void quaternion_setIdentity(Quaternion *q); +Vector3 quaternion_returnVectorV(const Quaternion *q); +float quaternion_magnitude(const Quaternion *q); +float quaternion_squaredMagnitude(const Quaternion *q); +void quaternion_normalize(Quaternion *q); +void quaternion_invert(Quaternion *q); + +Quaternion quaternion_returnUnit(const Quaternion *q); + +Quaternion quaternion_getConjugate(const Quaternion *q); +Quaternion quaternion_getInverse(const Quaternion *q); +float quaternion_dotProduct(const Quaternion *q, const Quaternion *r); + +bool quaternion_isFinite(const Quaternion *q); +bool quaternion_isUnit(const Quaternion *q); +bool quaternion_isValid(const Quaternion *q); +bool quaternion_equals(const Quaternion *q, const Quaternion *r); + +void quaternion_setFromEulerAngles(Quaternion *quaternion, float angleX, float angleY, float angleZ); +Quaternion quaternion_getFromEulerAngles(float angleX, float angleY, float angleZ); +Quaternion quaternion_getFromVector(const Vector3 *rotation); +Quaternion quaternion_getFromMatrix(const Matrix3x3 *matrix); + +void quaternion_setRotationAngleAxis(Quaternion *quaternion, float *angle, Vector3 *axis); +Matrix3x3 quaternion_getMatrix(const Quaternion *quaternion); + +Quaternion quaternion_slerp(const Quaternion *q, const Quaternion *r, float t); + +Quaternion quat_from_array(float arr[4]); + +// Implementations + +/* Initializes the quaternion to zero. */ +void quaternion_init(Quaternion *q) +{ + quaternion_set(q, 0.0f, 0.0f, 0.0f, 0.0f); +} + +/* Initializes the quaternion with the component w and the vector v=(x y z). */ +void quaternion_setWithVector(Quaternion *q, float w, const Vector3 *v) +{ + quaternion_set(q, v->x, v->y, v->z, w); +} + +/* Sets all values in the quaternion. */ +void quaternion_set(Quaternion *q, float x, float y, float z, float w) +{ + q->x = x; + q->y = y; + q->z = z; + q->w = w; +} + +/* Sets the quaternion to zero. */ +void quaternion_clear(Quaternion *q) +{ + quaternion_set(q, 0.0f, 0.0f, 0.0f, 0.0f); +} + +/* Sets the quaternion to the identity quaternion. */ +void quaternion_setIdentity(Quaternion *q) +{ + quaternion_set(q, 0.0f, 0.0f, 0.0f, 1.0f); +} + +/* Returns the vector v=(x y z) of the quaternion. */ +Vector3 quaternion_returnVectorV(const Quaternion *q) +{ + return (Vector3){q->x, q->y, q->z}; +} + +/* Returns the length of the quaternion. */ +float quaternion_magnitude(const Quaternion *q) +{ + return sqrtf(q->x * q->x + q->y * q->y + q->z * q->z + q->w * q->w); +} + +/* Returns the square of the length of the quaternion. */ +float quaternion_squaredMagnitude(const Quaternion *q) +{ + return q->x * q->x + q->y * q->y + q->z * q->z + q->w * q->w; +} + +/* Normalizes the quaternion. */ +void quaternion_normalize(Quaternion *q) +{ + float l = quaternion_magnitude(q); + assert(l > TOLERANCE); + q->x /= l; + q->y /= l; + q->z /= l; + q->w /= l; +} + +/* Inverses the quaternion. */ +void quaternion_invert(Quaternion *q) +{ + q->x = -q->x; + q->y = -q->y; + q->z = -q->z; +} + +/* Returns the unit quaternion. */ +Quaternion quaternion_returnUnit(const Quaternion *q) +{ + float ql = quaternion_magnitude(q); + assert(ql > TOLERANCE); + return (Quaternion){q->x / ql, q->y / ql, q->z / ql, q->w / ql}; +} + +/* Returns the identity quaternion. */ +Quaternion quaternion_identity() +{ + return (Quaternion){0.0f, 0.0f, 0.0f, 1.0f}; +} + +/* Returns the conjugate of the quaternion. */ +Quaternion quaternion_getConjugate(const Quaternion *q) +{ + return (Quaternion){-q->x, -q->y, -q->z, q->w}; +} + +/* Returns the inverse of the quaternion. */ +Quaternion quaternion_getInverse(const Quaternion *q) +{ + return quaternion_getConjugate(q); +} + +/* Computes the dot product between two quaternions. */ +float quaternion_dotProduct(const Quaternion *q, const Quaternion *r) +{ + return q->x * r->x + q->y * r->y + q->z * r->z + q->w * r->w; +} + +/* Checks if the quaternion's values are finite. */ +bool quaternion_isFinite(const Quaternion *q) +{ + return isfinite(q->x) && isfinite(q->y) && isfinite(q->z) && isfinite(q->w); +} + +/* Checks if the quaternion is a unit quaternion. */ +bool quaternion_isUnit(const Quaternion *q) +{ + float length = quaternion_magnitude(q); + return fabsf(length - 1.0f) < 1e-5f; +} + +/* Checks if the quaternion is valid. */ +bool quaternion_isValid(const Quaternion *q) +{ + return quaternion_isFinite(q) && quaternion_isUnit(q); +} + +/* Adds two quaternions. */ +Quaternion quaternion_sum(const Quaternion *q, const Quaternion *r) +{ + return (Quaternion){q->x + r->x, q->y + r->y, q->z + r->z, q->w + r->w}; +} + +/* Subtracts r from q. */ +Quaternion quaternion_difference(const Quaternion *q, const Quaternion *r) +{ + return (Quaternion){q->x - r->x, q->y - r->y, q->z - r->z, q->w - r->w}; +} + +/* Multiplies the quaternion by a scalar. */ +Quaternion quaternion_returnScaled(const Quaternion *q, float scalar) +{ + return (Quaternion){q->x * scalar, q->y * scalar, q->z * scalar, q->w * scalar}; +} + +/* Multiplies two quaternions. */ +Quaternion quaternion_returnProduct(const Quaternion *q, const Quaternion *r) +{ + return (Quaternion){ + q->w * r->x + r->w * q->x + q->y * r->z - q->z * r->y, + q->w * r->y + r->w * q->y + q->z * r->x - q->x * r->z, + q->w * r->z + r->w * q->z + q->x * r->y - q->y * r->x, + q->w * r->w - q->x * r->x - q->y * r->y - q->z * r->z}; +} + +/* Multiplies the quaternion by a vector. */ +Vector3 quaternion_getVectorProduct(const Quaternion *q, const Vector3 *vector) +{ + float prodX = q->w * vector->x + q->y * vector->z - q->z * vector->y; + float prodY = q->w * vector->y + q->z * vector->x - q->x * vector->z; + float prodZ = q->w * vector->z + q->x * vector->y - q->y * vector->x; + float prodW = -q->x * vector->x - q->y * vector->y - q->z * vector->z; + + return (Vector3){ + q->w * prodX - prodY * q->z + prodZ * q->y - prodW * q->x, + q->w * prodY - prodZ * q->x + prodX * q->z - prodW * q->y, + q->w * prodZ - prodX * q->y + prodY * q->x - prodW * q->z}; +} + +/* Checks if two quaternions are equal. */ +bool quaternion_equals(const Quaternion *q, const Quaternion *r) +{ + return (q->x == r->x && q->y == r->y && q->z == r->z && q->w == r->w); +} + +/* Initializes the quaternion using Euler angles. */ +void quaternion_setFromEulerAngles(Quaternion *quaternion, float angleX, float angleY, float angleZ) +{ + float angle = angleX * 0.5f; + float sinX = fm_sinf(angle); + float cosX = fm_cosf(angle); + + angle = angleY * 0.5f; + float sinY = fm_sinf(angle); + float cosY = fm_cosf(angle); + + angle = angleZ * 0.5f; + float sinZ = fm_sinf(angle); + float cosZ = fm_cosf(angle); + + float cosYcosZ = cosY * cosZ; + float sinYcosZ = sinY * cosZ; + float cosYsinZ = cosY * sinZ; + float sinYsinZ = sinY * sinZ; + + quaternion->x = sinX * cosYcosZ - cosX * sinYsinZ; + quaternion->y = cosX * sinYcosZ + sinX * cosYsinZ; + quaternion->z = cosX * cosYsinZ - sinX * sinYcosZ; + quaternion->w = cosX * cosYcosZ + sinX * sinYsinZ; + + /* Normalize the quaternion */ + // quaternion_normalize(quaternion); +} + +/* Returns a quaternion constructed from Euler angles (in radians). */ +Quaternion quaternion_getFromEulerAngles(float angleX, float angleY, float angleZ) +{ + Quaternion quaternion; + quaternion_setFromEulerAngles(&quaternion, angleX, angleY, angleZ); + return quaternion; +} + +/* Returns a quaternion constructed from Euler angles (in radians). */ +Quaternion quaternion_getFromVector(const Vector3 *rotation) +{ + Quaternion quaternion; + quaternion_setFromEulerAngles(&quaternion, rotation->x, rotation->y, rotation->z); + return quaternion; +} + +/* Creates a unit quaternion from a rotation matrix. */ +Quaternion quaternion_getFromMatrix(const Matrix3x3 *matrix) +{ + Quaternion quaternion; + + /* Get the trace of the matrix */ + float trace = matrix->row[0].x + matrix->row[1].y + matrix->row[2].z; + + float r; + float s; + + if (trace < 0.0f) + { + if (matrix->row[1].y > matrix->row[0].x) + { + if (matrix->row[2].z > matrix->row[1].y) + { + r = sqrtf(matrix->row[2].z - matrix->row[0].x - matrix->row[1].y + 1.0f); + s = 0.5f / r; + + /* Compute the quaternion */ + quaternion.x = (matrix->row[2].x + matrix->row[0].z) * s; + quaternion.y = (matrix->row[1].z + matrix->row[2].y) * s; + quaternion.z = 0.5f * r; + quaternion.w = (matrix->row[1].x - matrix->row[0].y) * s; + } + else + { + r = sqrtf(matrix->row[1].y - matrix->row[2].z - matrix->row[0].x + 1.0f); + s = 0.5f / r; + + /* Compute the quaternion */ + quaternion.x = (matrix->row[0].y + matrix->row[1].x) * s; + quaternion.y = 0.5f * r; + quaternion.z = (matrix->row[1].z + matrix->row[2].y) * s; + quaternion.w = (matrix->row[0].z - matrix->row[2].x) * s; + } + } + else if (matrix->row[2].z > matrix->row[0].x) + { + r = sqrtf(matrix->row[2].z - matrix->row[0].x - matrix->row[1].y + 1.0f); + s = 0.5f / r; + + /* Compute the quaternion */ + quaternion.x = (matrix->row[2].x + matrix->row[0].z) * s; + quaternion.y = (matrix->row[1].z + matrix->row[2].y) * s; + quaternion.z = 0.5f * r; + quaternion.w = (matrix->row[1].x - matrix->row[0].y) * s; + } + else + { + r = sqrtf(matrix->row[0].x - matrix->row[1].y - matrix->row[2].z + 1.0f); + s = 0.5f / r; + + /* Compute the quaternion */ + quaternion.x = 0.5f * r; + quaternion.y = (matrix->row[0].y + matrix->row[1].x) * s; + quaternion.z = (matrix->row[2].x + matrix->row[0].z) * s; + quaternion.w = (matrix->row[2].y - matrix->row[1].z) * s; + } + } + else + { + r = sqrtf(trace + 1.0f); + s = 0.5f / r; + + /* Compute the quaternion */ + quaternion.x = (matrix->row[2].y - matrix->row[1].z) * s; + quaternion.y = (matrix->row[0].z - matrix->row[2].x) * s; + quaternion.z = (matrix->row[1].x - matrix->row[0].y) * s; + quaternion.w = 0.5f * r; + } + + /* Normalize the quaternion to ensure it represents a valid rotation */ + quaternion_normalize(&quaternion); + return quaternion; +} + +/* Computes the rotation angle (in radians) and the rotation axis. */ +void quaternion_setRotationAngleAxis(Quaternion *quaternion, float *angle, Vector3 *axis) +{ + /* Compute the rotation angle */ + *angle = acosf(quaternion->w) * 2.0f; + + /* Compute the 3D rotation axis */ + Vector3 rotationAxis = {quaternion->x, quaternion->y, quaternion->z}; + + /* Normalize the rotation axis */ + vector3_normalize(&rotationAxis); + + /* Set the rotation axis values */ + vector3_set(axis, rotationAxis.x, rotationAxis.y, rotationAxis.z); +} + +/* Returns the orientation matrix corresponding to this quaternion. */ +Matrix3x3 quaternion_getMatrix(const Quaternion *quaternion) +{ + float nQ = quaternion->x * quaternion->x + quaternion->y * quaternion->y + quaternion->z * quaternion->z + quaternion->w * quaternion->w; + float s = 0.0f; + + if (nQ > 0.0f) + { + s = 2.0f / nQ; + } + + /* Computations used for optimization (less multiplications) */ + float xs = quaternion->x * s; + float ys = quaternion->y * s; + float zs = quaternion->z * s; + float wxs = quaternion->w * xs; + float wys = quaternion->w * ys; + float wzs = quaternion->w * zs; + float xxs = quaternion->x * xs; + float xys = quaternion->x * ys; + float xzs = quaternion->x * zs; + float yys = quaternion->y * ys; + float yzs = quaternion->y * zs; + float zzs = quaternion->z * zs; + + /* Create the matrix corresponding to the quaternion */ + return (Matrix3x3){ + .row = { + {1.0f - yys - zzs, xys - wzs, xzs + wys}, + {xys + wzs, 1.0f - xxs - zzs, yzs - wxs}, + {xzs - wys, yzs + wxs, 1.0f - xxs - yys}}}; +} + +Quaternion quaternion_slerp(const Quaternion *q, const Quaternion *r, float t) +{ + assert(t >= 0.0f && t <= 1.0f); + + float invert = 1.0f; + + /* Compute cos(theta) using the quaternion scalar product */ + float cosineTheta = quaternion_dotProduct(q, r); + + /* Take care of the sign of cosineTheta */ + if (cosineTheta < 0.0f) + { + cosineTheta = -cosineTheta; + invert = -1.0f; + } + + /* Because of precision, if cos(theta) is nearly 1, + therefore theta is nearly 0 and we can write + sin((1-t)*theta) as (1-t) and sin(t*theta) as t */ + float epsilon = 0.00001f; + if (1.0f - cosineTheta < epsilon) + { + Quaternion q1 = quaternion_returnScaled(q, 1.0f - t); + Quaternion q2 = quaternion_returnScaled(r, t * invert); + return quaternion_sum(&q1, &q2); + } + + /* Compute the theta angle */ + float theta = acosf(cosineTheta); + + /* Compute sin(theta) */ + float sineTheta = fm_sinf(theta); + + /* Compute the two coefficients that are in the spherical linear interpolation formula */ + float coeff1 = fm_sinf((1.0f - t) * theta) / sineTheta; + float coeff2 = fm_sinf(t * theta) / sineTheta * invert; + + /* Compute and return the interpolated quaternion */ + Quaternion q1 = quaternion_returnScaled(q, coeff1); + Quaternion q2 = quaternion_returnScaled(r, coeff2); + return quaternion_sum(&q1, &q2); +} + +Vector3 quaternion_rotateVector(Vector3 v, Quaternion q) +{ + Vector3 u = {q.x, q.y, q.z}; + float s = q.w; + + Vector3 rv1 = vector3_returnScaled(&u, 2.0f * vector3_returnDotProduct(&u, &v)); + Vector3 rv2 = vector3_returnScaled(&v, (s * s - vector3_returnDotProduct(&u, &u))); + Vector3 crossProduct = vector3_returnCrossProduct(&u, &v); + Vector3 rv3 = vector3_returnScaled(&crossProduct, 2.0f * s); + + Vector3 result = vector3_sum(&rv1, &rv2); + result = vector3_sum(&result, &rv3); + + return result; +} + +inline Quaternion quat_from_array(float arr[4]) +{ + Quaternion result; + result.x = arr[0]; + result.y = arr[1]; + result.z = arr[2]; + result.w = arr[3]; + return result; +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/physics/math/transform.h b/code/sb_halcyon/physics/math/transform.h new file mode 100644 index 00000000..1f1fb1ad --- /dev/null +++ b/code/sb_halcyon/physics/math/transform.h @@ -0,0 +1,155 @@ +/** + * @file + * + * Holds the transform matrix. + */ + +#ifndef TRANSFORM_H +#define TRANSFORM_H + +typedef struct +{ + Vector3 position; + Quaternion orientation; +} Transform; + +void transform_init(Transform *transform); +void transform_initWithMatrix(Transform *transform, const Vector3 *position, const Matrix3x3 *orientation); +void transform_initWithQuaternion(Transform *transform, const Vector3 *position, const Quaternion *orientation); + +Vector3 transform_getPosition(const Transform *transform); +void transform_setPosition(Transform *transform, const Vector3 *position); +Quaternion transform_getOrientation(const Transform *transform); +void transform_setOrientation(Transform *transform, const Quaternion *orientation); + +void transform_setIdentity(Transform *transform); +Transform transform_returnIdentity(); + +Transform transform_getInverse(const Transform *transform); +Transform transform_getInterpolated(const Transform *oldTransform, const Transform *newTransform, float factor); + +Vector3 transform_getProductVector(const Transform *transform, const Vector3 *vector); +Transform transform_product(const Transform *t1, const Transform *t2); + +bool transform_isValid(const Transform *transform); +bool transform_equals(const Transform *t1, const Transform *t2); +bool transform_notEquals(const Transform *t1, const Transform *t2); +void transform_toString(const Transform *transform, char *buffer, size_t bufferSize); + +/* Initializes the transform to the identity transform. */ +void transform_init(Transform *transform) +{ + vector3_clear(&transform->position); + quaternion_setIdentity(&transform->orientation); +} + +/* Initializes the transform with a given position and orientation matrix. */ +void transform_initWithMatrix(Transform *transform, const Vector3 *position, const Matrix3x3 *orientation) +{ + transform->position = *position; + transform->orientation = quaternion_getFromMatrix(orientation); +} + +/* Initializes the transform with a given position and orientation quaternion. */ +void transform_initWithQuaternion(Transform *transform, const Vector3 *position, const Quaternion *orientation) +{ + transform->position = *position; + transform->orientation = *orientation; +} + +/* Returns the position of the transform. */ +Vector3 transform_getPosition(const Transform *transform) +{ + return transform->position; +} + +/* Sets the position of the transform. */ +void transform_setPosition(Transform *transform, const Vector3 *position) +{ + transform->position = *position; +} + +/* Returns the orientation quaternion of the transform. */ +Quaternion transform_getOrientation(const Transform *transform) +{ + return transform->orientation; +} + +/* Sets the orientation quaternion of the transform. */ +void transform_setOrientation(Transform *transform, const Quaternion *orientation) +{ + transform->orientation = *orientation; +} + +/* Sets the transform to the identity transform. */ +void transform_setIdentity(Transform *transform) +{ + vector3_set(&transform->position, 0.0f, 0.0f, 0.0f); + quaternion_setIdentity(&transform->orientation); +} + +/* Returns the inverse of the transform. */ +Transform transform_getInverse(const Transform *transform) +{ + Quaternion invQuaternion = quaternion_getInverse(&transform->orientation); + Vector3 invertedPosition = vector3_getInverse(&transform->position); + Vector3 invPosition = quaternion_getVectorProduct(&invQuaternion, &invertedPosition); + Transform inverseTransform = {invPosition, invQuaternion}; + return inverseTransform; +} + +/* Returns an interpolated transform between two transforms. */ +Transform transform_getInterpolated(const Transform *oldTransform, const Transform *newTransform, float factor) +{ + Vector3 scaledOldPosition = vector3_returnScaled(&oldTransform->position, 1.0f - factor); + Vector3 scaledNewPosition = vector3_returnScaled(&newTransform->position, factor); + Vector3 interPosition = vector3_sum(&scaledOldPosition, &scaledNewPosition); + Quaternion interOrientation = quaternion_slerp(&oldTransform->orientation, &newTransform->orientation, factor); + Transform interpolatedTransform = {interPosition, interOrientation}; + return interpolatedTransform; +} + +/* Returns the identity transform. */ +Transform transform_returnIdentity() +{ + Transform identityTransform; + transform_setIdentity(&identityTransform); + return identityTransform; +} + +/* Returns true if the transform is valid. */ +bool transform_isValid(const Transform *transform) +{ + return vector3_isFinite(&transform->position) && quaternion_isValid(&transform->orientation); +} + +/* Returns the transformed vector. */ +Vector3 transform_getProductVector(const Transform *transform, const Vector3 *vector) +{ + Vector3 rotatedVector = quaternion_getVectorProduct(&transform->orientation, vector); + return vector3_sum(&rotatedVector, &transform->position); +} + +/* Multiplies two transforms. */ +Transform transform_product(const Transform *t1, const Transform *t2) +{ + Vector3 rotatedPosition = quaternion_getVectorProduct(&t1->orientation, &t2->position); + Vector3 newPosition = vector3_sum(&t1->position, &rotatedPosition); + Quaternion newOrientation = quaternion_returnProduct(&t1->orientation, &t2->orientation); + Transform result = {newPosition, newOrientation}; + return result; +} + +/* Returns true if the two transforms are equal. */ +bool transform_equals(const Transform *t1, const Transform *t2) +{ + return vector3_equals(&t1->position, &t2->position) && quaternion_equals(&t1->orientation, &t2->orientation); +} + +/* Returns true if the two transforms are different. */ +bool transform_notEquals(const Transform *t1, const Transform *t2) +{ + return !transform_equals(t1, t2); +} + +#endif diff --git a/code/sb_halcyon/physics/math/vector2.h b/code/sb_halcyon/physics/math/vector2.h new file mode 100644 index 00000000..5fc7528f --- /dev/null +++ b/code/sb_halcyon/physics/math/vector2.h @@ -0,0 +1,234 @@ +/** + * @file + * + * holds a vector in 2 dimensions. + */ + +#ifndef VECTOR2_H +#define VECTOR2_H + +// Vector2 structure +typedef struct +{ + float x; + float y; +} Vector2; + +// Function prototypes + +void vector2_init(Vector2 *v); +void vector2_clear(Vector2 *v); +void vector2_set(Vector2 *v, float x, float y); + +void vector2_setValue(Vector2 *vector, int index, float value); +float vector2_returnValue(const Vector2 *vector, int index); + +void vector2_add(Vector2 *v, const Vector2 *vector2); +void vector2_subtract(Vector2 *v, const Vector2 *vector2); +void vector2_scale(Vector2 *vector, float number); +void vector2_divideByNumber(Vector2 *vector, float number); + +float vector2_magnitude(const Vector2 *vector); +float vector2_squaredMagnitude(const Vector2 *vector); + +Vector2 vector2_returnUnit(const Vector2 *vector); +Vector2 vector2_returnUnitOrthogonalVector(const Vector2 *vector); + +float vector2_dotProduct(const Vector2 *v, const Vector2 *vector2); +void vector2_normalize(Vector2 *vector); +Vector2 vector2_returnAbsoluteVector(const Vector2 *vector); + +Vector2 vector2_min(const Vector2 *v, const Vector2 *vector2); +Vector2 vector2_max(const Vector2 *v, const Vector2 *vector2); + +int vector2_returnMinAxis(const Vector2 *vector); +int vector2_returnMaxAxis(const Vector2 *vector); + +bool vector2_isUnit(const Vector2 *vector); +bool vector2_isFinite(const Vector2 *vector); +bool vector2_isZero(const Vector2 *vector); + +bool vector2_equals(const Vector2 *v, const Vector2 *vector2); +bool vector2_notEquals(const Vector2 *v, const Vector2 *vector2); +bool vector2_lessThan(const Vector2 *v, const Vector2 *vector2); +bool vector2_approxEqual(const Vector2 *v, const Vector2 *w, float epsilon); + +// Function implementations + +/* Initializes all components of the vector to zero. */ +void vector2_init(Vector2 *v) +{ + v->x = 0.0f; + v->y = 0.0f; +} + +/* Clears all components of the vector to zero. */ +void vector2_clear(Vector2 *v) +{ + vector2_init(v); +} + +/* Sets the components of the vector to the specified values. */ +void vector2_set(Vector2 *v, float x, float y) +{ + v->x = x; + v->y = y; +} + +float vector2_magnitude(const Vector2 *v) +{ + return sqrtf(v->x * v->x + v->y * v->y); +} + +float vector2_squaredMagnitude(const Vector2 *v) +{ + return v->x * v->x + v->y * v->y; +} + +Vector2 vector2_returnUnit(const Vector2 *v) +{ + Vector2 vector = {0.0f, 0.0f}; + float l = vector2_magnitude(v); + + if (l < FLT_EPSILON) + return vector; + + vector2_set(&vector, v->x / l, v->y / l); + + return vector; +} + +Vector2 vector2_returnUnitOrthogonalVector(const Vector2 *v) +{ + float l = vector2_magnitude(v); + assert(l > FLT_EPSILON); + // Assuming clockwise rotation for orthogonal vector + Vector2 vector = {-v->y / l, v->x / l}; + return vector; +} + +bool vector2_isUnit(const Vector2 *vector) +{ + return fabs(vector2_squaredMagnitude(vector) - 1.0) < FLT_EPSILON; +} + +bool vector2_isFinite(const Vector2 *vector) +{ + return isfinite(vector->x) && isfinite(vector->y); +} + +bool vector2_isZero(const Vector2 *vector) +{ + return vector2_squaredMagnitude(vector) < FLT_EPSILON; +} + +float vector2_dotProduct(const Vector2 *v, const Vector2 *w) +{ + return v->x * w->x + v->y * w->y; +} + +void vector2_normalize(Vector2 *v) +{ + float l = vector2_magnitude(v); + if (l < FLT_EPSILON) + return; + v->x /= l; + v->y /= l; +} + +Vector2 vector2_returnAbsoluteVector(const Vector2 *v) +{ + Vector2 result; + result.x = fabsf(v->x); + result.y = fabsf(v->y); + return result; +} + +int vector2_returnMinAxis(const Vector2 *v) +{ + return (v->x < v->y ? 0 : 1); +} + +int vector2_returnMaxAxis(const Vector2 *v) +{ + return (v->x < v->y ? 1 : 0); +} + +bool vector2_equals(const Vector2 *v, const Vector2 *w) +{ + return (v->x == w->x && v->y == w->y); +} + +bool vector2_notEquals(const Vector2 *v, const Vector2 *w) +{ + return !vector2_equals(v, w); +} + +bool vector2_lessThan(const Vector2 *v, const Vector2 *w) +{ + return (v->x == w->x ? v->y < w->y : v->x < w->x); +} + +bool vector2_approxEqual(const Vector2 *v, const Vector2 *w, float epsilon) +{ + return approxEqual(v->x, w->x) && approxEqual(v->y, w->y); +} + +void vector2_add(Vector2 *v, const Vector2 *w) +{ + v->x += w->x; + v->y += w->y; +} + +void vector2_subtract(Vector2 *v, const Vector2 *w) +{ + v->x -= w->x; + v->y -= w->y; +} + +void vector2_scale(Vector2 *v, float scalar) +{ + v->x *= scalar; + v->y *= scalar; +} + +void vector2_divideByNumber(Vector2 *v, float number) +{ + assert(number > FLT_EPSILON); + v->x /= number; + v->y /= number; +} + +float vector2_returnValue(const Vector2 *v, int index) +{ + if (index == 0) + return v->x; + else + return v->y; +} + +void vector2_setValue(Vector2 *v, int index, float value) +{ + if (index == 0) + v->x = value; + else + v->y = value; +} + +Vector2 vector2_min(const Vector2 *v, const Vector2 *w) +{ + Vector2 result; + result.x = min2(v->x, w->x); + result.y = min2(v->y, w->y); + return result; +} + +Vector2 vector2_max(const Vector2 *v, const Vector2 *w) +{ + Vector2 result; + result.x = max2(v->x, w->x); + result.y = max2(v->y, w->y); + return result; +} + +#endif // VECTOR2_H diff --git a/code/sb_halcyon/physics/math/vector3.h b/code/sb_halcyon/physics/math/vector3.h new file mode 100644 index 00000000..42b26b02 --- /dev/null +++ b/code/sb_halcyon/physics/math/vector3.h @@ -0,0 +1,385 @@ +#ifndef VECTOR_3_H +#define VECTOR_3_H + +// structures + +typedef struct Vector3 +{ + float x; + float y; + float z; +} Vector3; + +// Macros to use t3dmath if necessary +#define T3DVec3_to_Vector3(t3dVec) ((Vector3){(t3dVec).v[0], (t3dVec).v[1], (t3dVec).v[2]}) +#define Vector3_to_T3DVec3(vec) ((T3DVec3){{(vec).x, (vec).y, (vec).z}}) +#define Vector3_to_fast(vec) ((fm_vec3_t){{(vec).x, (vec).y, (vec).z}}) +#define fast_to_Vector3(fast) ((Vector3){{(fast).x, (fast).y, (fast).z}}) + +// function prototypes + +void vector3_init(Vector3 *v); +void vector3_clear(Vector3 *v); +void vector3_set(Vector3 *v, float x, float y, float z); +void vector3_setElement(Vector3 *v, int index, float value); +float vector3_returnElement(const Vector3 *v, int index); + +void vector3_invert(Vector3 *v); +Vector3 vector3_getInverse(const Vector3 *v); + +void vector3_add(Vector3 *v, const Vector3 *w); +Vector3 vector3_sum(const Vector3 *v, const Vector3 *w); +void vector3_subtract(Vector3 *v, const Vector3 *w); +Vector3 vector3_difference(const Vector3 *v, const Vector3 *w); + +void vector3_scale(Vector3 *v, float scalar); +Vector3 vector3_returnScaled(const Vector3 *v, float scalar); +void vector3_divideByNumber(Vector3 *v, float number); +Vector3 vector3_returnQuotientByNumber(const Vector3 *v, float number); +Vector3 vector3_returnQuotientByVector(const Vector3 *v, const Vector3 *w); + +void vector3_componentProduct(Vector3 *v, const Vector3 *w); +Vector3 vector3_returnComponentProduct(const Vector3 *v, const Vector3 *w); +void vector3_crossProduct(Vector3 *v, const Vector3 *w); +Vector3 vector3_returnCrossProduct(const Vector3 *v, const Vector3 *w); +float vector3_returnDotProduct(const Vector3 *v, const Vector3 *w); +void vector3_addScaledVector(Vector3 *v, const Vector3 *w, float scalar); + +float vector3_magnitude(const Vector3 *v); +float vector3_squaredMagnitude(const Vector3 *v); +void vector3_normalize(Vector3 *v); +Vector3 vector3_returnNormalized(const Vector3 *v); +Vector3 vector3_returnAbsoluteVector(const Vector3 *v); + +Vector3 vector3_min(const Vector3 *v, const Vector3 *w); +Vector3 vector3_max(const Vector3 *v, const Vector3 *w); +float vector3_returnMinValue(const Vector3 *v); +float vector3_returnMaxValue(const Vector3 *v); +int vector3_returnMinAxis(const Vector3 *v); +int vector3_returnMaxAxis(const Vector3 *v); + +bool vector3_isUnit(const Vector3 *v); +bool vector3_isFinite(const Vector3 *v); +bool vector3_isZero(const Vector3 *v); +bool vector3_equals(const Vector3 *v, const Vector3 *w); +bool vector3_notEquals(const Vector3 *v, const Vector3 *w); +bool vector3_lessThan(const Vector3 *v, const Vector3 *w); +bool vector3_approxEquals(const Vector3 *v, const Vector3 *w); +Vector3 vector3_lerp(const Vector3 *v1, const Vector3 *v2, float t); + +Vector3 vector3_from_array(float arr[3]); +Vector3 vector3_flip_coords(Vector3 vec); +Vector3 vector3_flip_up(Vector3 vec); +float vector3_squaredDistance(const Vector3 *v, const Vector3 *w); +float vector3_distance(const Vector3 *v, const Vector3 *w); +Vector3 vector3_average4(const Vector3 *v1, const Vector3 *v2, const Vector3 *v3, const Vector3 *v4); + +inline void vector3_init(Vector3 *v) +{ + v->x = 0.0f; + v->y = 0.0f; + v->z = 0.0f; +} + +inline void vector3_clear(Vector3 *v) +{ + vector3_init(v); +} + +inline void vector3_set(Vector3 *v, float x, float y, float z) +{ + v->x = x; + v->y = y; + v->z = z; +} + +inline void vector3_invert(Vector3 *v) +{ + v->x = -v->x; + v->y = -v->y; + v->z = -v->z; +} + +inline Vector3 vector3_getInverse(const Vector3 *v) +{ + + return (Vector3){-v->x, -v->y, -v->z}; +} + +inline void vector3_copy(Vector3 *v, const Vector3 *w) +{ + v->x = w->x; + v->y = w->y; + v->z = w->z; +} + +inline void vector3_add(Vector3 *v, const Vector3 *w) +{ + v->x += w->x; + v->y += w->y; + v->z += w->z; +} + +inline Vector3 vector3_sum(const Vector3 *v, const Vector3 *w) +{ + return (Vector3){v->x + w->x, v->y + w->y, v->z + w->z}; +} + +inline void vector3_subtract(Vector3 *v, const Vector3 *w) +{ + v->x -= w->x; + v->y -= w->y; + v->z -= w->z; +} + +inline Vector3 vector3_difference(const Vector3 *v, const Vector3 *w) +{ + return (Vector3){v->x - w->x, v->y - w->y, v->z - w->z}; +} + +inline void vector3_scale(Vector3 *v, float scalar) +{ + v->x *= scalar; + v->y *= scalar; + v->z *= scalar; +} + +inline Vector3 vector3_returnScaled(const Vector3 *v, float scalar) +{ + return (Vector3){v->x * scalar, v->y * scalar, v->z * scalar}; +} + +void vector3_divideByNumber(Vector3 *v, float number) +{ + assert(number > FLT_EPSILON); + v->x /= number; + v->y /= number; + v->z /= number; +} + +Vector3 vector3_returnQuotientByNumber(const Vector3 *v, float number) +{ + assert(number > FLT_EPSILON); + return (Vector3){v->x / number, v->y / number, v->z / number}; +} + +Vector3 vector3_returnQuotientByVector(const Vector3 *v, const Vector3 *w) +{ + assert(w->x > FLT_EPSILON); + assert(w->y > FLT_EPSILON); + assert(w->z > FLT_EPSILON); + return (Vector3){v->x / w->x, v->y / w->y, v->z / w->z}; +} + +inline void vector3_componentProduct(Vector3 *v, const Vector3 *w) +{ + v->x *= w->x; + v->y *= w->y; + v->z *= w->z; +} + +inline Vector3 vector3_returnComponentProduct(const Vector3 *v, const Vector3 *w) +{ + return (Vector3){v->x * w->x, v->y * w->y, v->z * w->z}; +} + +inline void vector3_crossProduct(Vector3 *v, const Vector3 *w) +{ + v->x = v->y * w->z - v->z * w->y; + v->y = v->z * w->x - v->x * w->z; + v->z = v->x * w->y - v->y * w->x; +} + +Vector3 vector3_returnCrossProduct(const Vector3 *v, const Vector3 *w) +{ + return (Vector3){v->y * w->z - v->z * w->y, v->z * w->x - v->x * w->z, v->x * w->y - v->y * w->x}; +} + +inline float vector3_returnDotProduct(const Vector3 *v, const Vector3 *w) +{ + return v->x * w->x + v->y * w->y + v->z * w->z; +} + +inline void vector3_addScaledVector(Vector3 *v, const Vector3 *w, float scalar) +{ + v->x += w->x * scalar; + v->y += w->y * scalar; + v->z += w->z * scalar; +} + +inline float vector3_magnitude(const Vector3 *v) +{ + return sqrtf(v->x * v->x + v->y * v->y + v->z * v->z); +} + +inline float vector3_squaredMagnitude(const Vector3 *v) +{ + return v->x * v->x + v->y * v->y + v->z * v->z; +} + +void vector3_normalize(Vector3 *v) +{ + float m = vector3_magnitude(v); + if (m > 0) + { + m = 1.0f / m; + vector3_scale(v, m); + } +} + +Vector3 vector3_returnNormalized(const Vector3 *v) +{ + Vector3 result = *v; + vector3_normalize(&result); + return result; +} + +Vector3 vector3_returnAbsoluteVector(const Vector3 *v) +{ + return (Vector3){fabsf(v->x), fabsf(v->y), fabsf(v->z)}; +} + +inline int vector3_returnMinAxis(const Vector3 *v) +{ + return (v->x < v->y ? (v->x < v->z ? 0 : 2) : (v->y < v->z ? 1 : 2)); +} + +inline int vector3_returnMaxAxis(const Vector3 *v) +{ + return (v->x < v->y ? (v->y < v->z ? 2 : 1) : (v->x < v->z ? 2 : 0)); +} + +bool vector3_isUnit(const Vector3 *v) +{ + return approxEqual(vector3_squaredMagnitude(v), 1.0f); +} + +bool vector3_isFinite(const Vector3 *v) +{ + return isfinite(v->x) && isfinite(v->y) && isfinite(v->z); +} + +bool vector3_isZero(const Vector3 *v) +{ + return approxEqual(vector3_squaredMagnitude(v), 0.0f); +} + +inline bool vector3_equals(const Vector3 *v, const Vector3 *w) +{ + return (v->x == w->x && v->y == w->y && v->z == w->z); +} + +inline bool vector3_notEquals(const Vector3 *v, const Vector3 *w) +{ + return !vector3_equals(v, w); +} + +inline bool vector3_lessThan(const Vector3 *v, const Vector3 *w) +{ + return (v->x == w->x ? (v->y == w->y ? v->z < w->z : v->y < w->y) : v->x < w->x); +} + +bool vector3_approxEquals(const Vector3 *v, const Vector3 *w) +{ + return approxEqual(v->x, w->x) && approxEqual(v->y, w->y) && approxEqual(v->z, w->z); +} + +inline float vector3_returnElement(const Vector3 *v, int index) +{ + return ((const float *)v)[index]; +} + +inline void vector3_setElement(Vector3 *v, int index, float value) +{ + ((float *)v)[index] = value; +} + +inline Vector3 vector3_min(const Vector3 *v, const Vector3 *w) +{ + return (Vector3){min2(v->x, w->x), min2(v->y, w->y), min2(v->z, w->z)}; +} + +inline Vector3 vector3_max(const Vector3 *v, const Vector3 *w) +{ + return (Vector3){max2(v->x, w->x), max2(v->y, w->y), max2(v->z, w->z)}; +} + +inline float vector3_returnMinValue(const Vector3 *v) +{ + return min2(min2(v->x, v->y), v->z); +} + +inline float vector3_returnMaxValue(const Vector3 *v) +{ + return max3(v->x, v->y, v->z); +} + +Vector3 vector3_lerp(const Vector3 *v1, const Vector3 *v2, float t) +{ + Vector3 result; + result.x = v1->x + (v2->x - v1->x) * t; + result.y = v1->y + (v2->y - v1->y) * t; + result.z = v1->z + (v2->z - v1->z) * t; + return result; +} + +inline Vector3 vector3_from_array(float arr[3]) +{ + Vector3 result; + result.x = arr[0]; + result.y = arr[1]; + result.z = arr[2]; + return result; +} + +// Function to convert a vector from a right-handed Y-up coordinate system to a left-handed Z-up coordinate system +inline Vector3 vector3_flip_coords(Vector3 vec) +{ + Vector3 result; + result.x = vec.x; + result.y = vec.z; + result.z = -vec.y; + return result; +} + +// Function to convert a vector from a right-handed Y-up coordinate system to a right-handed Z-up coordinate system +inline Vector3 vector3_flip_up(Vector3 vec) +{ + Vector3 result; + result.x = vec.x; + result.y = -vec.z; + result.z = vec.y; + return result; +} + +// Function to convert T3D AABB coordinates to engine's format +inline Vector3 vector3_from_int16(const int16_t int_arr[3]) +{ + Vector3 vec; + vec.x = (float)int_arr[0]; + vec.y = -(float)int_arr[2]; + vec.z = (float)int_arr[1]; + return vec; +} + +float vector3_squaredDistance(const Vector3 *v, const Vector3 *w) +{ + Vector3 diff = vector3_difference(v, w); + return vector3_returnDotProduct(&diff, &diff); +} + +float vector3_distance(const Vector3 *v, const Vector3 *w) +{ + return sqrtf(vector3_squaredDistance(v, w)); +} + +Vector3 vector3_average4(const Vector3 *v1, const Vector3 *v2, const Vector3 *v3, const Vector3 *v4) +{ + Vector3 avg; + avg.x = (v1->x + v2->x + v3->x + v4->x) / 4.0f; + avg.y = (v1->y + v2->y + v3->y + v4->y) / 4.0f; + avg.z = (v1->z + v2->z + v3->z + v4->z) / 4.0f; + return avg; +} + +#endif diff --git a/code/sb_halcyon/physics/physics.h b/code/sb_halcyon/physics/physics.h new file mode 100644 index 00000000..8b597fbf --- /dev/null +++ b/code/sb_halcyon/physics/physics.h @@ -0,0 +1,17 @@ +#ifndef PHYSICS_H +#define PHYSICS_H + +#include "math/physics_math.h" + +#include "body/rigid_body.h" + +#include "collision/contact_data.h" +#include "collision/shapes/sphere.h" +#include "collision/shapes/AABB.h" +#include "collision/shapes/box.h" +#include "collision/shapes/plane.h" +#include "collision/shapes/ray.h" +#include "collision/shapes/capsule.h" +#include "collision/shapes/triangle.h" + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/physics/physics_config.h b/code/sb_halcyon/physics/physics_config.h new file mode 100644 index 00000000..203c1323 --- /dev/null +++ b/code/sb_halcyon/physics/physics_config.h @@ -0,0 +1,15 @@ +#ifndef PHYSICS_CONFIG_H +#define PHYSICS_CONFIG_H + +// ---------- Constants ---------- // + +/* Pi constant */ +#define PI 3.141592653589f + +/* 2*Pi constant */ +#define PI_TIMES_2 6.28318530f + +/* A numeric tolerance value used to handle floating-point precision issues. */ +#define TOLERANCE 1e-3f + +#endif diff --git a/code/sb_halcyon/player/ai.h b/code/sb_halcyon/player/ai.h new file mode 100644 index 00000000..6820466e --- /dev/null +++ b/code/sb_halcyon/player/ai.h @@ -0,0 +1,178 @@ +#ifndef AI_H +#define AI_H + +typedef struct +{ + float jump_threshold; + float safe_height; + uint8_t difficulty; + uint8_t error_margin; + uint8_t reaction_delay; + uint8_t max_reaction_delay; +} AI; + +void ai_init(AI *ai, uint8_t difficulty); +void ai_generateControlData(AI *ai, ControllerData *control, Actor *actor, Platform *platforms, float camera_angle); + +void ai_init(AI *ai, uint8_t difficulty) +{ + + // Reset reaction delay for fresh initialization + ai->reaction_delay = 0; + + // Set other fields based on difficulty + switch (difficulty) + { + case DIFF_EASY: + ai->jump_threshold = 600.0f; + ai->safe_height = 240.0f; + ai->difficulty = DIFF_EASY; + ai->error_margin = 4; + ai->max_reaction_delay = 6; + break; + case DIFF_MEDIUM: + ai->jump_threshold = 550.0f; + ai->safe_height = 240.0f; + ai->difficulty = DIFF_MEDIUM; + ai->error_margin = 4; + ai->max_reaction_delay = 4; + break; + case DIFF_HARD: + ai->jump_threshold = 500.0f; + ai->safe_height = 240.0f; + ai->difficulty = DIFF_HARD; + ai->error_margin = 4; + ai->max_reaction_delay = 2; + break; + } +} + +// Helper function to rotate input by camera angle +void ai_updateCam(ControllerData *control, float camera_angle) +{ + // Convert angle to radians + float angle_rad = camera_angle * (T3D_PI / 180.0f); + int8_t original_x = control->input.stick_x; + int8_t original_y = control->input.stick_y; + + // Rotate stick_x and stick_y based on camera angle + control->input.stick_x = (int8_t)(original_x * fm_cosf(angle_rad) - original_y * fm_sinf(angle_rad)); + control->input.stick_y = (int8_t)(original_x * fm_sinf(angle_rad) + original_y * fm_cosf(angle_rad)); +} + +// Function to find the nearest platform at a safe height +Platform *find_nearest_safe_platform(AI *ai, Actor *actor, Platform *platforms) +{ + Platform *nearest_platform = NULL; + float min_distance_sq = FLT_MAX; // Store squared distance to avoid square root computation + const float current_platform_threshold_sq = 0.011f * 0.011f; // Squared threshold to ignore the current platform + + // Calculate grid cell for the actor's current position + int xCell = (int)fm_floorf((actor->body.position.x + 700) / GRID_SIZE); + int yCell = (int)fm_floorf((actor->body.position.y + 700) / GRID_SIZE); + + // Iterate through platforms in the same and adjacent grid cells + for (int dx = -1; dx <= 1; dx++) + { + for (int dy = -1; dy <= 1; dy++) + { + int nx = xCell + dx; + int ny = yCell + dy; + + // Check if the cell is within bounds + if (nx >= 0 && nx < MAX_GRID_CELLS && ny >= 0 && ny < MAX_GRID_CELLS) + { + PlatformGridCell *cell = &platformGrid[nx][ny]; + for (size_t i = 0; i < cell->count; i++) + { + size_t platformIndex = cell->platformIndices[i]; + Platform *platform = &platforms[platformIndex]; + + // Skip platforms not at a safe height + if (platform->position.z < ai->safe_height) + continue; + + // Calculate squared distance using fast math vector3 + fm_vec3_t actorPos = Vector3_to_fast(actor->body.position); + fm_vec3_t platformPos = Vector3_to_fast(platform->position); + float distance_sq = fm_vec3_distance2(&platformPos, &actorPos); + + if (platform->contact) + { + if (platform->colorID == actor->colorID) + { + continue; + } + else + { + distance_sq *= 2.5f; + } + } + + // Ignore the current platform the AI is standing on + if (distance_sq < current_platform_threshold_sq) + continue; + + // Check if this platform is the nearest valid one + if (distance_sq < min_distance_sq) + { + min_distance_sq = distance_sq; + nearest_platform = platform; + } + } + } + } + } + + return nearest_platform; +} + +// Generate control data for the AI +void ai_generateControlData(AI *ai, ControllerData *control, Actor *actor, Platform *platforms, float camera_angle) +{ + // Initialize control data to zero for each frame + memset(control, 0, sizeof(ControllerData)); + + // Set difficulty-based reaction delay + if (ai->reaction_delay < ai->max_reaction_delay) + { + ai->reaction_delay++; + return; // Skip processing this frame to simulate slower reaction + } + + // Find the nearest safe platform + Platform *target_platform = find_nearest_safe_platform(ai, actor, platforms); + if (target_platform == NULL) + return; // No valid platform found, do nothing + + // Calculate direction towards the target platform + fm_vec3_t direction_to_target = {{ + target_platform->position.x - actor->body.position.x, + target_platform->position.y - actor->body.position.y, + 0 // Only move in x-y plane, no need for z movement + }}; + fm_vec3_norm(&direction_to_target, &direction_to_target); + + // Set movement based on target distance + control->input.stick_x = (int8_t)(direction_to_target.x * 127); // Max joystick range + control->input.stick_y = (int8_t)(direction_to_target.y * 127); + + // Add slight randomness to mimic human error, increasing with easier difficulties + control->input.stick_x += (rand() % ai->error_margin) - (ai->error_margin / 2); + control->input.stick_y += (rand() % ai->error_margin) - (ai->error_margin / 2); + + // Check if the actor should jump when horizontal speed is greater than 10 + if (actor->horizontal_speed + (rand() % ai->error_margin) > ai->jump_threshold && actor->state != FALLING) + { + control->pressed.a = 1; // Press jump button + control->held.a = 1; // Hold jump button + } + + if (actor->state == JUMP || actor->state == FALLING) + control->held.a = 1; + + // Adjust for camera angle if needed + ai_updateCam(control, camera_angle); +} + +#endif // AI_H \ No newline at end of file diff --git a/code/sb_halcyon/player/player.h b/code/sb_halcyon/player/player.h new file mode 100644 index 00000000..c78aee0d --- /dev/null +++ b/code/sb_halcyon/player/player.h @@ -0,0 +1,100 @@ +#ifndef PLAYER_H +#define PLAYER_H + +typedef struct +{ + + Vector3 position; + uint8_t id; + uint8_t actor_id; + uint8_t score; + bool died; + bool isHuman; + bool deathCounted; + ControllerData control; + +} Player; + +void player_init(Player *player, uint8_t id, uint8_t actor_id); + +void player_init(Player *player, uint8_t id, uint8_t actor_id) +{ + player->position = (Vector3){0, 0, 0}; + player->id = id; + player->actor_id = actor_id; + player->died = false; + player->isHuman = true; + player->deathCounted = false; +} + +void player_setControlData(Player *player) +{ + joypad_poll(); + + for (int i = 0; i < PLAYER_COUNT; i++) + { + if (i != 0 && player[i].died) + continue; // Allow player one to still pause when eliminated + controllerData_getInputs(&player[i].control, i); + } +} + +// Modified from `player_draw_billboard` in Snake3D example +Vector3 player_getBillboard(Player *player, T3DViewport *viewport) +{ + Vector3 result = (Vector3){0, 0, 0}; + if (player->died) + return result; + + Vector3 billboardPos = (Vector3){ + player->position.x, + player->position.y, + player->position.z + 200.0f}; + + T3DVec3 billboardPosConvert = Vector3_to_T3DVec3(billboardPos); + + T3DVec3 billboardScreenPos; + t3d_viewport_calc_viewspace_pos(viewport, &billboardScreenPos, &billboardPosConvert); + + int x = fm_floorf(billboardScreenPos.v[0]); + int y = fm_floorf(billboardScreenPos.v[2]); + int z = fm_floorf(billboardScreenPos.v[1]); + + result = (Vector3){x, y, z}; + return result; +} + +void player_drawShadow(Vector3 position, T3DViewport *viewport) +{ + + Vector3 shadowPos = (Vector3){ + position.x, + position.y, + 325 // @TODO: in a bigger game, I'd probably just raycasst for the floor Z + }; + + T3DVec3 playerPosConvert = Vector3_to_T3DVec3(position); + T3DVec3 shadowPosConvert = Vector3_to_T3DVec3(shadowPos); + + T3DVec3 playerScreenPos; + T3DVec3 shadowScreenPos; + t3d_viewport_calc_viewspace_pos(viewport, &playerScreenPos, &playerPosConvert); + t3d_viewport_calc_viewspace_pos(viewport, &shadowScreenPos, &shadowPosConvert); + + int offset = 3; + float v1[] = {playerScreenPos.x, playerScreenPos.y - offset}; + float v2[] = {shadowScreenPos.x - offset, shadowScreenPos.y}; + float v3[] = {shadowScreenPos.x + offset, shadowScreenPos.y}; + float v4[] = {shadowScreenPos.x, shadowScreenPos.y + offset}; + + rdpq_sync_pipe(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_set_prim_color(RGBA32(0, 0, 0, 127)); + + // Draw rhombus shape with 2 tris + rdpq_triangle(&TRIFMT_FILL, v1, v2, v3); + rdpq_triangle(&TRIFMT_FILL, v3, v4, v2); +} +#endif \ No newline at end of file diff --git a/code/sb_halcyon/sb_halcyon.mk b/code/sb_halcyon/sb_halcyon.mk new file mode 100644 index 00000000..77f19c85 --- /dev/null +++ b/code/sb_halcyon/sb_halcyon.mk @@ -0,0 +1,79 @@ +# Project-specific assets +ASSETS = filesystem/strawberry_byte +SOUND_DIR = $(ASSETS)/sound +UI_DIR = $(ASSETS)/ui + +T3DM_FILES := $(ASSETS)/wolfie.t3dm \ + $(ASSETS)/s4ys.t3dm \ + $(ASSETS)/dogman.t3dm \ + $(ASSETS)/mew.t3dm \ + $(ASSETS)/platform.t3dm \ + $(ASSETS)/lava.t3dm \ + $(ASSETS)/platform2.t3dm \ + $(ASSETS)/cloud_base.t3dm + +SPRITE_FILES := $(ASSETS)/wolf_eye.sprite \ + $(ASSETS)/frog_eye.sprite \ + $(ASSETS)/libdragon_logo.sprite \ + $(ASSETS)/nose.sprite \ + $(ASSETS)/n64brew.sprite \ + $(ASSETS)/mew_eye.sprite \ + $(ASSETS)/mew_ear.sprite \ + $(ASSETS)/jam_logo.sprite \ + $(ASSETS)/dogman_eye.sprite \ + $(ASSETS)/dogman_eyebrow.sprite \ + $(ASSETS)/dogman_mouth.sprite \ + $(ASSETS)/fast64.sprite \ + $(ASSETS)/bricks48.i8.sprite \ + $(ASSETS)/lava00.rgba16.sprite \ + $(ASSETS)/lava08.rgba16.sprite + +SOUND_FILES := $(SOUND_DIR)/hexagone.wav64 \ + $(SOUND_DIR)/sky_high.xm64 \ + $(SOUND_DIR)/grunt-01.wav64 \ + $(SOUND_DIR)/stones-falling.wav64 \ + $(SOUND_DIR)/strong_wind_blowing.wav64 + +UI_SPRITE_FILES := $(UI_DIR)/buttons/control_stick.ia8.sprite \ + $(UI_DIR)/buttons/d_pad_triggers.ia8.sprite \ + $(UI_DIR)/buttons/c_buttons0.rgba32.sprite \ + $(UI_DIR)/buttons/c_buttons1.rgba32.sprite \ + $(UI_DIR)/buttons/face_buttons0.rgba32.sprite \ + $(UI_DIR)/buttons/face_buttons1.rgba32.sprite \ + $(UI_DIR)/logos/libdragon.ia4.sprite \ + $(UI_DIR)/logos/mixamo.ia4.sprite \ + $(UI_DIR)/logos/t3d.ia8.sprite \ + $(UI_DIR)/logos/sb_b0.rgba32.sprite \ + $(UI_DIR)/logos/sb_b1.rgba32.sprite \ + $(UI_DIR)/logos/sb_top.rgba32.sprite \ + $(UI_DIR)/panels/gloss.ia4.sprite \ + $(UI_DIR)/panels/clouds.ia8.sprite \ + $(UI_DIR)/panels/pattern_tessalate.ia4.sprite + +FONT_FILES := $(UI_DIR)/fonts/TitanOne-Regular.font64 \ + $(UI_DIR)/fonts/OilOnTheWater-ee5O.font64 + +# Final assets list +ASSETS_LIST += $(T3DM_FILES) $(SPRITE_FILES) $(SOUND_FILES) $(UI_SPRITE_FILES) $(FONT_FILES) + +# t3d flags +$(ASSETS)/s4ys.t3dm: T3DM_FLAGS = --base-scale=1 +$(ASSETS)/wolfie.t3dm: T3DM_FLAGS = --base-scale=1 +$(ASSETS)/dogman.t3dm: T3DM_FLAGS = --base-scale=1 +$(ASSETS)/mew.t3dm: T3DM_FLAGS = --base-scale=1 +$(ASSETS)/platform.t3dm: T3DM_FLAGS = --base-scale=1 +$(ASSETS)/lava.t3dm: T3DM_FLAGS = --base-scale=1 +$(ASSETS)/platform2.t3dm: T3DM_FLAGS = --base-scale=1 +$(ASSETS)/cloud_base.t3dm: T3DM_FLAGS = --base-scale=1 + +# audioconv flags +$(ASSETS)/hexagone.wav64: AUDIOCONV_FLAGS = --wav-compress=3 +$(ASSETS)/sky_high.xm64: AUDIOCONV_FLAGS = '' +$(ASSETS)/stones-falling.wav64: AUDIOCONV_FLAGS = --wav-compress=3 --wav-mono +$(ASSETS)/strong_wind_blowing.wav64: AUDIOCONV_FLAGS = --wav-compress=3 --wav-mono +$(ASSETS)/grunt-01.wav64: AUDIOCONV_FLAGS = --wav-compress=3 --wav-mono + + +# font64 flags +$(UI_DIR)/fonts/OilOnTheWater-ee5O.font64: MKFONT_FLAGS += --outline 2 --size 18 +$(UI_DIR)/fonts/TitanOne-Regular.font64: MKFONT_FLAGS += --outline 1 --size 12 \ No newline at end of file diff --git a/code/sb_halcyon/scene/particles.h b/code/sb_halcyon/scene/particles.h new file mode 100644 index 00000000..1915edec --- /dev/null +++ b/code/sb_halcyon/scene/particles.h @@ -0,0 +1,134 @@ +/* + * This file includes code from Tiny3D. + * Tiny3D is licensed under the MIT License. + * + * Original code by Max Bebök + * Adapted by s4ys + * November 2024 + * + * Description of changes or adaptations made: + * - Generate positions randomly within an AABB + * - Utilize `gradient_fire` as `gradient_alpha` to add random alpha values to white + * + * + * Original source: https://github.com/HailToDodongo/tiny3d/tree/main/examples/18_particles + */ + +#ifndef PARTICLES_H +#define PARTICLES_H + +typedef struct +{ + uint32_t count; + uint32_t bufSize; + TPXParticle *buf; + T3DMat4FP *mat; + +} Particles; + +Particles cloudMist; + +void ptx_init(Particles *ptx) +{ + tpx_init((TPXInitParams){}); + ptx->count = 100; + ptx->bufSize = sizeof(TPXParticle) * (ptx->count + 2); + ptx->buf = malloc_uncached(ptx->bufSize); + ptx->mat = malloc_uncached(sizeof(T3DMat4FP)); +} + +// Alpha gradient +void gradient_alpha(uint8_t *color, float t) +{ + t = fminf(1.0f, fmaxf(0.0f, t)); + t = 0.1f - t; + t *= t; + + color[0] = (uint8_t)(240); + color[1] = (uint8_t)(240); + color[2] = (uint8_t)(240); + color[3] = (uint8_t)(200 * (1.0f - (t - 0.5f) / 0.5f)); +} + +void ptx_randomPos(T3DViewport *vp, Particles *ptx) +{ + for (int i = 0; i < ptx->count; i++) + { + int p = i / 2; + int8_t *ptxPos = (i % 2 == 0) ? ptx->buf[p].posA : ptx->buf[p].posB; + + // Assign random sizes + ptx->buf[p].sizeA = 20 + (rand() % 10); + ptx->buf[p].sizeB = 20 + (rand() % 10); + + // Random positions within the bounding box + float min = -127.9f; + float max = 126.9f; + T3DVec3 randomPos; + randomPos.v[0] = min + ((float)rand() / max) * (max - min); + randomPos.v[1] = min; + randomPos.v[2] = min + ((float)rand() / max) * (max - min); + + // Calculate from view space + T3DVec3 screenPos; + t3d_viewport_calc_viewspace_pos(vp, &screenPos, &randomPos); + + float t = (float)i / ptx->count; // Vary by particle index + screenPos.v[1] += t * (max - min); // Move upward + + // Clamp final values to fit within int8_t range + ptxPos[0] = (int8_t)screenPos.v[0]; + ptxPos[1] = (int8_t)screenPos.v[1]; + ptxPos[2] = (int8_t)screenPos.v[2]; + + gradient_alpha(ptx->buf[p].colorA, (ptxPos[0] + 127) * 0.0012f); + gradient_alpha(ptx->buf[p].colorB, (ptxPos[0] + 127) * 0.0012f); + } +} + +void ptx_draw(T3DViewport *vp, Platform *platform, Particles *ptx) +{ + + static int frameCounter = 0; + const int updateInterval = 3; + + // Prepare the RDPQ + rdpq_sync_pipe(); + rdpq_sync_tile(); + rdpq_set_mode_standard(); + rdpq_mode_zbuf(true, true); + rdpq_mode_zoverride(true, 0, 0); + rdpq_mode_combiner(RDPQ_COMBINER_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + + if (platform->position.z <= 50.0f && platform->position.z >= -50.0f) + { + if (frameCounter % updateInterval == 0) + { + ptx_randomPos(vp, ptx); + } + frameCounter++; + + tpx_state_from_t3d(); + + tpx_matrix_push_pos(1); + tpx_matrix_set(ptx->mat, true); + tpx_state_set_scale(1, 1); + t3d_mat4fp_from_srt_euler( + ptx->mat, + (float[3]){7, 4, 7}, + (float[3]){0, 0, 0}, + (float[3]){0, 250, 0}); + tpx_particle_draw(ptx->buf, ptx->count); + tpx_matrix_pop(1); + } +} + +void ptx_cleanup(Particles *ptx) +{ + free_uncached(ptx->mat); + free_uncached(ptx->buf); + tpx_destroy(); +} + +#endif // PARTICLES_H \ No newline at end of file diff --git a/code/sb_halcyon/scene/platform.h b/code/sb_halcyon/scene/platform.h new file mode 100644 index 00000000..fcec3d6d --- /dev/null +++ b/code/sb_halcyon/scene/platform.h @@ -0,0 +1,406 @@ +#ifndef PLATFORM_H +#define PLATFORM_H + +typedef struct +{ + + Box box[3]; + +} PlatformCollider; + +typedef struct +{ + + uint32_t id; + T3DMat4FP *mat; + T3DObject *obj; + Vector3 position; + Vector3 home; + PlatformCollider collider; + color_t color; + int colorID; + uint32_t platformTimer; + bool contact; + +} Platform; + +#define OFFSET 350 +#define GRID_SIZE OFFSET // Size of each grid cell +#define MAX_GRID_CELLS 7 // Adjust based on level size + +typedef struct +{ + size_t platformIndices[PLATFORM_COUNT]; // Indices of platforms in this cell + size_t count; // Number of platforms in this cell +} PlatformGridCell; + +PlatformGridCell platformGrid[MAX_GRID_CELLS][MAX_GRID_CELLS]; + +Platform hexagons[PLATFORM_COUNT]; + +Platform innerRing[6]; +Platform outerRing[12]; + +int innerRingID[] = {4, 5, 8, 10, 13, 14}; +int outerRingID[] = {0, 1, 2, 3, 6, 7, 11, 12, 15, 16, 17, 18}; + +// Forward Declarations + +void platform_init(Platform *platform, T3DModel *model, Vector3 position, color_t color); +void platform_loop(Platform *platform, Actor *actor, int diff); +void platform_drawBatch(void); +void platform_hexagonGrid(Platform *platform, T3DModel *model, float z, color_t color); +void platform_destroy(Platform *platform); + +// Definitions + +void platform_init(Platform *platform, T3DModel *model, Vector3 position, color_t color) +{ + + static uint32_t platformIdx = 0; + + platform->id = platformIdx; + platform->mat = malloc_uncached(sizeof(T3DMat4FP)); // needed for t3d + platform->position = position; + platform->home = position; + + // Initialize the three boxes for collision within each hexagon + for (int j = 0; j < 3; j++) + { + platform->collider.box[j] = (Box){ + .size = {200.0f, 300.0f, 75.0f}, + .center = platform->position, + .rotation = { + // Set only Z rotation explicitly + 0.0f, + 0.0f, + (j == 0) ? 0.0f : (j == 1) ? 30.0f + : -30.0f // Z rotation for boxes[0], boxes[1], and boxes[2] + }}; + } + + platform->color = color; // Set color + + platform->colorID = -1; + + platform->platformTimer = 0; + + platform->contact = false; + + platformIdx++; +} + +void platform_assignGrid(Platform *platforms) +{ + memset(platformGrid, 0, sizeof(platformGrid)); // Clear the grid + + for (size_t i = 0; i < PLATFORM_COUNT; i++) + { + int xCell = (int)fm_floorf((platforms[i].position.x + (OFFSET * 2)) / GRID_SIZE); + int yCell = (int)fm_floorf((platforms[i].position.y + (OFFSET * 2)) / GRID_SIZE); + + if (xCell >= 0 && xCell < MAX_GRID_CELLS && yCell >= 0 && yCell < MAX_GRID_CELLS) + { + PlatformGridCell *cell = &platformGrid[xCell][yCell]; + if (cell->count < PLATFORM_COUNT) // Ensure we don't exceed bounds + cell->platformIndices[cell->count++] = i; + } + } +} + +//// BEHAVIORS ~ Start //// + +// Example behavior: Oscillate platform x position to simulate shake +void platform_shake(Platform *platform, float time) +{ + float amplitude = 10.0f; // Maximum oscillation distance from home + float baseX = platform->home.x; // Use a stored "home" position for the base + + // Oscillate `platform->position.x` around `baseX` + platform->position.x = baseX + amplitude * fm_sinf(time); +} + +// Example behavior: Lower platform over time +void platform_updateHeight(Platform *platform, float time) +{ + if (platform->position.z > -150.0f) + platform->position.z = platform->position.z - time; +} + +void platform_dropGroup(Platform *platform, int groupID, float time) +{ + int ringSize; // Variable to hold the size of the current ring group + + if (groupID == 1) + { + ringSize = sizeof(innerRing) / sizeof(innerRing[0]); + for (int i = 0; i < ringSize; i++) + { + platform_shake(&platform[innerRingID[i]], (time * 20) + rand() % 5); + platform_updateHeight(&platform[innerRingID[i]], (time * 20) + rand() % 5); + } + } + else + { + ringSize = sizeof(outerRing) / sizeof(outerRing[0]); + for (int i = 0; i < ringSize; i++) + { + platform_shake(&platform[outerRingID[i]], (time * 20) + rand() % 5); + platform_updateHeight(&platform[outerRingID[i]], (time * 20) + rand() % 5); + } + } +} + +void platform_collideCheck(Platform *platform, Actor *actor) +{ + if (platform->contact) + return; // If already in collided state, do nothing + + const float squaredDistanceThreshold = 150.0f * 150.0f; + + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + // Skip actors that are not grounded (not relevant for collision) + if (!actor[i].grounded) + continue; + + // Calculate the difference vector + Vector3 diff = vector3_difference(&platform->position, &actor[i].body.position); + + // Calculate squared distance + float distanceSq = vector3_squaredMagnitude(&diff); + + if (distanceSq <= squaredDistanceThreshold) + { + platform->contact = true; + return; // Exit early on first collision + } + } +} + +void platform_collideCheckOptimized(Platform *platforms, Actor *actor) +{ + int xCell = (int)floorf((actor->body.position.x + (OFFSET * 2)) / GRID_SIZE); + int yCell = (int)floorf((actor->body.position.y + (OFFSET * 2)) / GRID_SIZE); + + if (xCell < 0 || xCell >= MAX_GRID_CELLS || yCell < 0 || yCell >= MAX_GRID_CELLS) + { + return; // Actor is out of bounds + } + + const float collisionRangeSq = 150.0f * 150.0f; + + // Check platforms in the same and adjacent cells + for (int dx = -1; dx <= 1; dx++) + { + for (int dy = -1; dy <= 1; dy++) + { + int nx = xCell + dx; + int ny = yCell + dy; + + if (nx >= 0 && nx < MAX_GRID_CELLS && ny >= 0 && ny < MAX_GRID_CELLS) + { + PlatformGridCell *cell = &platformGrid[nx][ny]; + for (size_t i = 0; i < cell->count; i++) + { + size_t platformIndex = cell->platformIndices[i]; + float distanceSq = vector3_squaredDistance(&actor->body.position, &platforms[platformIndex].position); + if (distanceSq <= collisionRangeSq) + { + platforms[platformIndex].contact = true; + return; // Early exit on collision + } + } + } + } + } +} + +void platform_loop(Platform *platform, Actor *actor, int diff) +{ + int difficulty = (core_get_playercount() == 4) ? diff : core_get_aidifficulty(); + + // Translate collision + for (int j = 0; j < 3; j++) + platform->collider.box[j].center = platform->position; + + // Run behaviors + // if(actor != NULL) platform_collideCheck(platform, actor); + if (platform->contact) + { + platform->platformTimer++; + + // Action 1: Play sound + if (platform->platformTimer > 0 && platform->platformTimer < 2) + { + sound_wavPlay(SFX_STONES, false); + + // Action 2: Shake platform + } + else if (platform->platformTimer < 120 - (difficulty * 10)) + { + platform_shake(platform, platform->platformTimer); + + // Action 3: Reset to idle + } + else + { + // change the platform color to white + platform->color = RGBA32(255, 255, 255, 255); + platform->contact = false; + } + } + else + { + + // Reset to initial position + platform->platformTimer = 0; + // if(platform->position.z < platform->home.z) platform->position.z = platform->position.z + 1.0f + difficulty; + } + + // Update matrix + t3d_mat4fp_from_srt_euler( + platform->mat, + (float[3]){1.0f, 1.0f, 1.0f}, + (float[3]){0.0f, 0.0f, 0.0f}, + (float[3]){platform->position.x, platform->position.y, platform->position.z}); +} + +//// BEHAVIORS ~ End //// + +//// RENDERING ~ Start //// + +// T3D MODEL DRAW BATCHING +#define BATCH_LIMIT 1 // Number of objects per rspq block +#define BLOCK_COUNT 19 // Pre-calculated block count + +T3DModel *batchModel = NULL; +rspq_block_t *rspqBlocks[BLOCK_COUNT] = {NULL}; // Static array of rspq block pointers +rspq_block_t *materialBlock = NULL; // Block for single material load and draw + +void platform_createBatch(Platform *platform, T3DModel *model) +{ + // Load model once for the entire batch if not already loaded + if (batchModel == NULL) + { + batchModel = model; + } + + // Create T3DObjects from batch for each platform + for (size_t i = 0; i < PLATFORM_COUNT; i++) + { + platform[i].obj = t3d_model_get_object_by_index(batchModel, 0); + } + + // Create material RSPQ block to run before drawing objects + rspq_block_begin(); + t3d_model_draw_material(platform[0].obj->material, NULL); + materialBlock = rspq_block_end(); + + // Initialize the rspq block index and start a new rspq block + size_t blockIndex = 0; + rspq_block_begin(); + + for (size_t i = 0; i < PLATFORM_COUNT; i++) + { + + // Set the model matrix and draw + t3d_matrix_set(platform[i].mat, true); + t3d_model_draw_object(platform[i].obj, NULL); + + // End the current rspq block and start a new one every n objects + if ((i + 1) % BATCH_LIMIT == 0 || i == PLATFORM_COUNT - 1) + { + rspqBlocks[blockIndex] = rspq_block_end(); // Store the completed rspq block + blockIndex++; + if (i < PLATFORM_COUNT - 1) + rspq_block_begin(); // Start a new rspq block if more objects remain + } + } +} + +// Iterate through and run RSPQ blocks +void platform_drawBatch(void) +{ + + // Draw material once per batch + rspq_block_run(materialBlock); + + for (size_t i = 0; i < BLOCK_COUNT; i++) + { + if (rspqBlocks[i] != NULL) // Check for NULL before running + { + rdpq_set_prim_color(hexagons[i].color); + rspq_block_run(rspqBlocks[i]); + } + } +} + +// Generate a hexagonal grid of 19 platforms at desired height, with desired model and color +void platform_hexagonGrid(Platform *platform, T3DModel *model, float z, color_t color) +{ + float x_offset = OFFSET; // Horizontal distance between centers of adjacent columns + float y_offset = OFFSET; // Vertical distance between centers of adjacent rows + float start_x = 0.0f; // Starting X coordinate for the first row + float start_y = -500.0f; // Starting Y coordinate for the first row + + int rows[] = {3, 4, 5, 4, 3}; // Number of hexagons per row + int hexagon_index = 0; + + for (int row_index = 0; row_index < 5; row_index++) + { + int hex_count = rows[row_index]; + float row_start_x = start_x - (hex_count - 1) * x_offset / 2.0f; + + for (int i = 0; i < hex_count; i++) + { + platform[hexagon_index].position.x = row_start_x + i * x_offset; + platform[hexagon_index].position.y = start_y + row_index * y_offset; + platform[hexagon_index].position.z = z; + hexagon_index++; + } + } + + // Initialize the platforms + for (size_t p = 0; p < PLATFORM_COUNT; p++) + { + platform_init(&platform[p], model, platform[p].position, color); + } + + platform_assignGrid(platform); + + // Assign platforms to ring by predefined IDs + for (int g = 0; g < sizeof(innerRingID) / sizeof(innerRingID[0]); g++) + { + innerRing[g] = platform[innerRingID[g]]; + } + for (int h = 0; h < sizeof(outerRingID) / sizeof(outerRingID[0]); h++) + { + outerRing[h] = platform[outerRingID[h]]; + } + + platform_createBatch(platform, model); +} + +// Frees T3D model, matrices and RSPQ Blocks used for rendering +void platform_destroy(Platform *platform) +{ + rspq_block_free(materialBlock); + + for (size_t b = 0; b < BLOCK_COUNT; b++) + { + if (rspqBlocks[b] != NULL) + rspq_block_free(rspqBlocks[b]); + } + + for (size_t p = 0; p < PLATFORM_COUNT; p++) + { + if (platform[p].mat != NULL) + free_uncached(platform[p].mat); + } + + if (batchModel != NULL) + t3d_model_free(batchModel); +} + +#endif // PLATFORM_H \ No newline at end of file diff --git a/code/sb_halcyon/scene/room.h b/code/sb_halcyon/scene/room.h new file mode 100644 index 00000000..fc7fbf67 --- /dev/null +++ b/code/sb_halcyon/scene/room.h @@ -0,0 +1,57 @@ +/* + * This file includes code from Tiny3D. + * Tiny3D is licensed under the MIT License. + * + * Original code by Max Bebök + * Adapted by s4ys + * November 2024 + * + * Description of changes or adaptations made: + * - water like wobble + * + * + * Original source: https://github.com/HailToDodongo/tiny3d/tree/main/examples/04_dynamic + */ + +#ifndef ROOM_H +#define ROOM_H + +#define DEATH_PLANE_HEIGHT -50.0f +#define LOWER_LIMIT_HEIGHT -200.0f + +void move_cloud(Scenery *scenery) +{ + + scenery[0].transform_offset += 0.008f; + + // returns the global vertex buffer for a model. + // If you have multiple models and want to only update one, you have to manually iterate over the objects. + // see the implementation of t3d_model_draw_custom in that case. + T3DVertPacked *verts = t3d_model_get_vertices(scenery[0].model); + float globalHeight = fm_sinf(scenery[0].transform_offset * 2.5f); + + for (uint16_t i = 0; i < scenery[0].model->totalVertCount; ++i) + { + // To better handle the interleaved vertex format, + // t3d provides a few helper functions to access attributes + int16_t *pos = t3d_vertbuffer_get_pos(verts, i); + + // water-like wobble effect + float height = fm_sinf( + scenery[0].transform_offset * 4.5f + pos[0] * 30.1f + pos[1] * 20.1f); + pos[2] = 4.0f * height + globalHeight; + } + + // Don't forget to flush the cache again! (or use an uncached buffer in the first place) + data_cache_hit_writeback(verts, sizeof(T3DVertPacked) * scenery[0].model->totalVertCount / 2); +} + +void room_draw(Scenery *scenery) +{ + + rdpq_mode_zbuf(false, true); + t3d_model_draw(scenery[0].model); + rdpq_mode_zbuf(true, true); +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/scene/scene.h b/code/sb_halcyon/scene/scene.h new file mode 100644 index 00000000..b43b0ae0 --- /dev/null +++ b/code/sb_halcyon/scene/scene.h @@ -0,0 +1,17 @@ +#ifndef SCENE_H +#define SCENE_H + +typedef struct +{ + Camera camera; + LightData light; + +} Scene; + +void scene_init(Scene *scene) +{ + scene->camera = camera_create(); + scene->light = light_create(); +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/scene/scenery.h b/code/sb_halcyon/scene/scenery.h new file mode 100644 index 00000000..8abce3ac --- /dev/null +++ b/code/sb_halcyon/scene/scenery.h @@ -0,0 +1,80 @@ +#ifndef SCENERY_H +#define SCENERY_H + +// structures + +typedef struct +{ + + uint32_t id; + rspq_block_t *dl; + T3DMat4FP *modelMat; + T3DModel *model; + + float tile_offset; + float transform_offset; + + Vector3 scale; + Vector3 position; + Vector3 rotation; + +} Scenery; + +// function prototypes + +Scenery scenery_create(uint32_t id, const char *model_path); +void scenery_set(Scenery *scenery); +void scenery_draw(Scenery *scenery); +void scenery_delete(Scenery *scenery); + +// function implementations + +Scenery scenery_create(uint32_t id, const char *model_path) +{ + Scenery scenery = { + .id = id, + .model = t3d_model_load(model_path), + .modelMat = malloc_uncached(sizeof(T3DMat4FP)), // needed for t3d + + .scale = {12.0f, 11.0f, 15.0f}, + .position = {0.0f, 150.0f, -50.0f}, + .rotation = {0.0f, 0.0f, 0.0f}, + }; + + rspq_block_begin(); + t3d_matrix_set(scenery.modelMat, true); + t3d_model_draw(scenery.model); + scenery.dl = rspq_block_end(); + + t3d_mat4fp_identity(scenery.modelMat); + + return scenery; +} + +void scenery_set(Scenery *scenery) +{ + + t3d_mat4fp_from_srt_euler(scenery->modelMat, + (float[3]){scenery->scale.x, scenery->scale.y, scenery->scale.z}, + (float[3]){rad(scenery->rotation.x), rad(scenery->rotation.y), rad(scenery->rotation.z)}, + (float[3]){scenery->position.x, scenery->position.y, scenery->position.z}); +} + +void scenery_draw(Scenery *scenery) +{ + rdpq_mode_zbuf(false, true); + for (int i = 0; i < SCENERY_COUNT; i++) + { + rspq_block_run(scenery[i].dl); + }; + rdpq_mode_zbuf(true, true); +} + +void scenery_delete(Scenery *scenery) +{ + free_uncached(scenery->modelMat); + t3d_model_free(scenery->model); + rspq_block_free(scenery->dl); +} + +#endif diff --git a/code/sb_halcyon/screen/screen.h b/code/sb_halcyon/screen/screen.h new file mode 100644 index 00000000..2f0c1a93 --- /dev/null +++ b/code/sb_halcyon/screen/screen.h @@ -0,0 +1,68 @@ +#ifndef SCREEN_H +#define SCREEN_H + +#define SCREEN_WIDTH display_get_width() +#define SCREEN_HEIGHT display_get_height() + +typedef struct +{ + + surface_t depthBuffer; + T3DViewport gameplay_viewport; + +} Screen; + +void screen_initDisplay(Screen *screen); +void screen_clearDisplay(Screen *screen); + +void screen_initDisplay(Screen *screen) +{ + display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE); + screen->depthBuffer = surface_alloc(FMT_RGBA16, SCREEN_WIDTH, SCREEN_HEIGHT); +} + +void screen_clearDisplay(Screen *screen) +{ + rdpq_attach(display_get(), &screen->depthBuffer); + t3d_frame_start(); +} + +void screen_initT3dViewport(Screen *screen) +{ + screen->gameplay_viewport = t3d_viewport_create(); +} + +void screen_clearT3dViewport(Screen *screen) +{ + t3d_viewport_attach(&screen->gameplay_viewport); +} + +void screen_applyColor_Depth(Screen *screen, color_t color, bool fog) +{ + if (fog) + { + rdpq_mode_fog(RDPQ_FOG_STANDARD); + rdpq_set_fog_color(color); + } + + t3d_screen_clear_color(color); + t3d_screen_clear_depth(); + + if (fog) + t3d_fog_set_range(750.0f, 900.0f); +} + +void screen_applyColor(Screen *screen, color_t color, bool fog) +{ + if (fog) + { + rdpq_mode_fog(RDPQ_FOG_STANDARD); + rdpq_set_fog_color(color); + } + t3d_screen_clear_color(color); + + if (fog) + t3d_fog_set_range(750.0f, 900.0f); +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/sound/sound.h b/code/sb_halcyon/sound/sound.h new file mode 100644 index 00000000..c1bac020 --- /dev/null +++ b/code/sb_halcyon/sound/sound.h @@ -0,0 +1,291 @@ +#ifndef SOUND_H +#define SOUND_H + +#include + +// Core Definitions +#define MUSIC_CHANNEL 0 +#define SFX_CHANNEL 31 + +// XM sequences +enum BG_XM +{ + XM_SH, + NUM_XM +}; + +xm64player_t xmPlayer; + +const char *xmFileNames[NUM_XM] = { + "rom:/strawberry_byte/sound/sky_high.xm64", +}; + +// WAV files +enum SFX_WAV +{ + SFX_JUMP, + SFX_STONES, + SFX_WIND, + SFX_START, + SFX_COUNTDOWN, + SFX_STOP, + SFX_WINNER, + NUM_WAV +}; + +// Each WAV must have its own structure +wav64_t soundEffects[NUM_WAV]; + +const char *wavFileNames[NUM_WAV] = { + "rom:/strawberry_byte/sound/grunt-01.wav64", + "rom:/strawberry_byte/sound/stones-falling.wav64", + "rom:/strawberry_byte/sound/strong_wind_blowing.wav64", + "rom:/core/Start.wav64", + "rom:/core/Countdown.wav64", + "rom:/core/Stop.wav64", + "rom:/core/Winner.wav64", +}; + +/* Function Declarations */ +void sound_load(void); +void sound_xmSwitch(int songID, float volume, bool loop); +void sound_xmStop(void); +void sound_xmUpdate(float volume, bool loop); +void sound_wavPlay(int sfxID, bool loop); +void sound_wavClose(int sfxID); +void sound_wavCleanup(void); +void sound_cleanup(void); +void sound_update(void); + +/* Function Definitions */ + +void sound_load(void) +{ + // Open all WAVs at boot + for (int w = 0; w < NUM_WAV; ++w) + wav64_open(&soundEffects[w], wavFileNames[w]); + + // Open and play first XM in the list + xm64player_open(&xmPlayer, xmFileNames[0]); + xm64player_set_vol(&xmPlayer, 0.5f); + xm64player_play(&xmPlayer, MUSIC_CHANNEL); +} + +// Stops current XM, opens and plays requested module with set volume and whether to loop +void sound_xmSwitch(int songID, float volume, bool loop) +{ + xm64player_stop(&xmPlayer); + xm64player_close(&xmPlayer); + xm64player_open(&xmPlayer, xmFileNames[songID]); + xm64player_set_loop(&xmPlayer, loop); + xm64player_set_vol(&xmPlayer, volume); + xm64player_play(&xmPlayer, MUSIC_CHANNEL); +} + +// Stops and closes XM player +void sound_xmStop(void) +{ + xm64player_stop(&xmPlayer); +} + +// Adjusts volume and looping of current XM module +void sound_xmUpdate(float volume, bool loop) +{ + xm64player_set_loop(&xmPlayer, loop); + xm64player_set_vol(&xmPlayer, volume); +} + +// Plays requested WAV and whether to loop +void sound_wavPlay(int sfxID, bool loop) +{ + wav64_set_loop(&soundEffects[sfxID], loop); + wav64_play(&soundEffects[sfxID], SFX_CHANNEL - sfxID); +} + +void sound_wavClose(int sfxID) +{ + wav64_close(&soundEffects[sfxID]); +} + +void sound_wavCleanup(void) +{ + for (int w = 0; w < NUM_WAV; ++w) + wav64_close(&soundEffects[w]); +} + +void sound_cleanup(void) +{ + xm64player_close(&xmPlayer); + sound_wavCleanup(); +} + +void sound_update(void) +{ + mixer_try_play(); +} + +// Function to calculate forward facing direction of the camera based on its target +Vector3 calculate_camera_forward(const Camera *camera) +{ + Vector3 direction = { + camera->target.x - camera->position.x, + camera->target.y - camera->position.y, + camera->target.z - camera->position.z}; + vector3_normalize(&direction); + return direction; +} + +////// Audio filters + +#define REVERB_BUFFER_SIZE 32000 // Size for the delay buffer +#define MAX_COMB_FILTERS 3 +#define MAX_ALLPASS_FILTERS 2 + +typedef struct +{ + float comb_delays[MAX_COMB_FILTERS]; + float comb_feedback; + float allpass_delays[MAX_ALLPASS_FILTERS]; + float allpass_feedback; + float sample_rate; +} ReverbParams; + +// Schroeder Reverberator Parameters +ReverbParams paramsSchroeder = { + {2.0f, 3.0f, 4.0f}, // Comb filter delays in frames + 0.5f, + {1.0f, 2.0f}, // All-pass filter delays in frames + 0.4f, + 16000.0f, +}; + +// Circular buffers for the comb and all-pass filters +float comb_delay_buffer[REVERB_BUFFER_SIZE]; +int comb_buffer_index = 0; + +float allpass_delay_buffer[REVERB_BUFFER_SIZE]; +int allpass_buffer_index = 0; + +// Comb Filter implementation +float comb_filter(float input, float delay_seconds, float feedback, float sample_rate) +{ + int delay_samples = (int)(delay_seconds * sample_rate); + int buffer_index = (comb_buffer_index + REVERB_BUFFER_SIZE - delay_samples) % REVERB_BUFFER_SIZE; + + float delayed_sample = comb_delay_buffer[buffer_index]; + float output = delayed_sample + input; + + // Store the output with feedback in the buffer + comb_delay_buffer[comb_buffer_index] = input + delayed_sample * feedback; + comb_buffer_index = (comb_buffer_index + 1) % REVERB_BUFFER_SIZE; + + return output; +} + +// All-Pass Filter implementation +float allpass_filter(float input, float delay_seconds, float feedback, float sample_rate) +{ + int delay_samples = (int)(delay_seconds * sample_rate); + int buffer_index = (allpass_buffer_index + REVERB_BUFFER_SIZE - delay_samples) % REVERB_BUFFER_SIZE; + + float delayed_sample = allpass_delay_buffer[buffer_index]; + float output = delayed_sample - (input * feedback) + input; + + // Store the new input into the delay buffer + allpass_delay_buffer[allpass_buffer_index] = input + delayed_sample * feedback; + allpass_buffer_index = (allpass_buffer_index + 1) % REVERB_BUFFER_SIZE; + + return output; +} + +// Applies reverb based on current volume and set mix +float sound_reverb(float volume, float mix) +{ + + if (volume < 0.2f) + volume = 0.2f; // Clamp volume to minimum + + // Apply comb filters + float reverb_volume = 0.0f; + for (int i = 0; i < MAX_COMB_FILTERS; i++) + reverb_volume += comb_filter(volume, paramsSchroeder.comb_delays[i], paramsSchroeder.comb_feedback, paramsSchroeder.sample_rate); + + // Apply all-pass filters + for (int i = 0; i < MAX_ALLPASS_FILTERS; i++) + reverb_volume = allpass_filter(reverb_volume, paramsSchroeder.allpass_delays[i], paramsSchroeder.allpass_feedback, paramsSchroeder.sample_rate); + + // Mix original sound with reverb + return volume * (1.0f - mix) + reverb_volume * mix; +} + +////// Spatial 3D sound experiment +void sound_spatial(const Vector3 *spawner, + const Vector3 *player, + const Camera *camera) +{ + + float pan = 0.5f; + float volume = 1.0f; + + // Calculate distance vector for volume attenuation + Vector3 diff = vector3_difference(spawner, player); + float distance = vector3_magnitude(&diff); + + // If close enough to the sound spawner, skip unnecessary spatial calculations + if (distance > 250.0f) + { + // Volume attenuation (inverse-square or linear falloff) + float max_distance = 8000.0f; // Maximum distance for audible sound + volume = 1.0f - (distance / max_distance); + volume = fmaxf(0.1f, volume); // Clamp volume to minimum + + // Calculate the horizontal angle (azimuth) for panning + Vector3 horizontal_diff = {diff.x, diff.y, 0.0f}; + float horizontal_distance = vector3_magnitude(&horizontal_diff); + + // Avoid division by zero + if (horizontal_distance > 0.0f) + { + Vector3 player_forward = calculate_camera_forward(camera); + float cos_angle = vector3_returnDotProduct(&horizontal_diff, &player_forward) / horizontal_distance; + float angle = acosf(cos_angle); // Angle in radians + + // Determine if sound is to the left or right + pan = 0.5f * (1.0f - angle / M_PI); // Centered at 0.5, where 0 is left and 1 is right + + // Flip pan if the sound source is on the right side + if (diff.x * player_forward.y - diff.y * player_forward.x > 0) + pan = 1.0f - pan; + + // Cap the pan + pan = fminf(0.8f, fmaxf(0.2f, pan)); + } + else + { + // If horizontal_distance is zero, keep the pan centered + pan = 0.5f; + } + + // Apply a vertical attenuation if sound changes by height + float vertical_attenuation = fminf(1.0f, fmaxf(0.1f, 1.0f - fabsf(diff.z) / max_distance)); + + // Volume adjustment with vertical attenuation + volume *= vertical_attenuation; + + // Calculate reverb mix based on distance (0.3 at 250, 1.0 at max_distance) + float min_reverb_distance = 250.0f; + float mix = (distance - min_reverb_distance) / (max_distance - min_reverb_distance); + mix = fminf(1.0f, fmaxf(0.3f, mix)); // Clamp mix to [0.0, 1.0] + + // Apply reverb + volume = sound_reverb(volume, mix); + } + + // Set the volume and panning for each SFX channel + for (int i = 0; i < NUM_WAV; i++) + { + mixer_ch_set_vol_pan(SFX_CHANNEL - i, volume, pan); + } +} + +#endif // SOUND_H \ No newline at end of file diff --git a/code/sb_halcyon/time/time.h b/code/sb_halcyon/time/time.h new file mode 100644 index 00000000..cf20cdf4 --- /dev/null +++ b/code/sb_halcyon/time/time.h @@ -0,0 +1,48 @@ +#ifndef TIME_H +#define TIME_H + +// structures + +typedef struct +{ + + uint32_t frame_counter; + float frame_time_s; + float fixed_time_s; + float frame_rate; + double subtick; + +} TimeData; + +// functions prototypes + +void time_init(TimeData *time); +void time_setData(TimeData *time, bool paused); + +// functions implementations + +/* sets time data values to zero */ +void time_init(TimeData *time) +{ + + time->frame_counter = 0; + time->frame_rate = 0.0f; + time->subtick = 1; + + time->frame_time_s = 0.0f; + time->fixed_time_s = 0.04f; +} + +/* sets timing data */ +void time_setData(TimeData *time, bool paused) +{ + // Update timing values + if (!paused) + time->frame_counter++; + time->frame_rate = display_get_fps(); + time->subtick = (core_get_subtick() == 0) ? 1 : core_get_subtick(); + time->frame_time_s = display_get_delta_time(); + time->fixed_time_s = time->frame_time_s * (float)time->subtick; +} + +#endif \ No newline at end of file diff --git a/code/sb_halcyon/ui/ui.h b/code/sb_halcyon/ui/ui.h new file mode 100644 index 00000000..055431ef --- /dev/null +++ b/code/sb_halcyon/ui/ui.h @@ -0,0 +1,465 @@ +#ifndef UI_H +#define UI_H + +#include "ui_font.h" +#include "ui_colors.h" +#include "ui_sprite.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +// @TODO: Recreate button sprite tiling to remove these insanity checks. +#define aPressed 2 +#define aIdle 3 +#define aHeld 2 + +#define bPressed 0 +#define bIdle 1 +#define bHeld 3 + +#define cIdle 0 + enum cStates + { + C_UP, + C_DOWN, + C_LEFT, + C_RIGHT + }; + + enum MENU_TEXT + { + TEXT_DIFF, + TEXT_PLAYERS = +4, + TEXT_BOTS, + TEXT_CONTROLS, + TEXT_RUMBLE, + MENU_TEXT_COUNT + }; + + const char *uiMainMenuStrings[MENU_TEXT_COUNT] = { + "Difficulty: ", + "EASY", + "MEDIUM", + "HARD", + "Players: ", + "Bots: ", + " to Move\n to Jump", + "Insert Rumble Pak now!"}; + + const char *uiTipStrings[ACTOR_COUNT] = { + "Turn as many platforms\nto your player color\nas you can. GL HF!\nA winner is selected after\n3 players fall or\nthere's only 1 platform left.", + "Hold A to extend jump time.\nSee if you can make\na 2 platform gap.", + "Try to corner opponents\nand reducing their exit routes.", + "I should probably remove these placeholders"}; + + const char *uiCharacterSelectStrings[ACTOR_COUNT] = { + "s4ys", + "Wolfie", + "Mewde", + "Olli"}; + + // Positions + Vector2 textPositions; + Quaternion panelPositions; + + /* Declarations */ + + void ui_init(void); + void ui_syncText(void); + void ui_fps(float frame_rate, float x, float y); + void ui_printf(float x, float y, const char *txt, ...); + void ui_main_menu(ControllerData *control, int diff); + void ui_input_display(ControllerData *control); + void ui_textbox(void); + void ui_cleanup(void); + + /* Definitons */ + + void ui_init(void) + { + ui_fontRegister(); + ui_spriteLoad(); + textPositions = (Vector2){SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2}; // x,y + panelPositions = (Quaternion){SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT}; // x0,y0,x1,y1 + } + + // Optional RDPQ sync and set for text, to prevent bleeding if the autosync engine misses something. + void ui_syncText(void) + { + rdpq_sync_pipe(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_sync_tile(); + } + + void ui_fps(float frame_rate, float x, float y) + { + heap_stats_t heap_stats; + sys_get_heap_stats(&heap_stats); + ui_syncText(); + rdpq_text_printf(&txt_debugParms, ID_DEBUG, x, y, "FPS %.2f Mem: %d KiB", frame_rate, heap_stats.used / 1024); + } + + void ui_printf(float x, float y, const char *txt, ...) + { + ui_syncText(); + + va_list args; + va_start(args, txt); + rdpq_text_vprintf(&txt_debugParms, ID_DEBUG, x, y, txt, args); + } + + void ui_print_winner(int winner) + { + ui_spriteDrawPanel(TILE1, sprite_gloss, T_BLACK, panelPositions.x - 64, panelPositions.y - 18, panelPositions.w - 30, panelPositions.y + 10, 0, 0, 64, 64); + ui_syncText(); + if (winner != 5) // 5 signifies a Draw + { + rdpq_textparms_t winnerTextParms = txt_gameParms; + winnerTextParms.style_id = STYLE_PLAYER + winner - 1; + rdpq_text_printf(&winnerTextParms, ID_DEFAULT, textPositions.x - 50, textPositions.y, "Player %d Wins", winner); + } + else + { + rdpq_text_print(&txt_gameParms, ID_DEFAULT, textPositions.x - 20, textPositions.y, "TIE!"); + } + } + + void ui_print_playerNum(Player *player, Screen *screen) + { + Vector3 pos = player_getBillboard(player, &screen->gameplay_viewport); + ui_syncText(); + rdpq_textparms_t playerTextParms = txt_gameParms; + playerTextParms.style_id = STYLE_PLAYER + player->id; + rdpq_text_printf(&playerTextParms, ID_DEFAULT, pos.x, pos.z, "P%d", player->id + 1); + } + + void ui_playerScores(Player *player) + { + static Vector2 base = {32, 32}; + Vector2 position = base; + ui_syncText(); + rdpq_textparms_t playerTextParms = txt_gameParms; + playerTextParms.style_id = STYLE_PLAYER + player->id; + rdpq_text_printf(&playerTextParms, ID_DEFAULT, position.x + 75 * player->id, position.y, "P%d : %u", player->id + 1, player->score); + } + + void ui_countdown(int secondsLeft) + { + // Convert secondsLeft integer to a string + char countdownText[2]; + snprintf(countdownText, sizeof(countdownText), "%d", secondsLeft); + + ui_syncText(); + rdpq_text_printf(&txt_titleParms, ID_TITLE, textPositions.x - 10, textPositions.y - 10, "%s", countdownText); + + txt_gameParms.align = ALIGN_CENTER; + txt_gameParms.width = 320; + rdpq_text_printf(&txt_gameParms, ID_DEFAULT, 0, textPositions.y + 10, "%s", uiTipStrings[0]); + txt_gameParms.align = ALIGN_LEFT; + txt_gameParms.width = 0; + } + + // Controller data is passed here for visual feedback for the button press. + void ui_main_menu(ControllerData *control, int diff) + { + // Panels + ui_spriteDrawPanel(TILE2, sprite_gloss, T_BLUE, panelPositions.x - 70, panelPositions.y - 80, panelPositions.z - 90, panelPositions.w - 116, 0, 0, 64, 64); + ui_spriteDrawPanel(TILE3, sprite_tessalate, T_BLACK, panelPositions.x - 60, panelPositions.y - 75, panelPositions.z - 100, panelPositions.w - 126, 0, 0, 64, 64); + + // Buttons + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 1, textPositions.x + 10, textPositions.y - 30); + } + else + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 0, textPositions.x + 10, textPositions.y - 30); + } + ui_spriteDraw(TILE5, sprite_controlStick, 0, textPositions.x - 68, textPositions.y + 50); + int stickX = 92 + (control->input.stick_x / 15); + int stickY = 138 + (spriteHeight * 2) - (control->input.stick_y / 15); + ui_spriteDraw(TILE5, sprite_controlStick, 1, stickX, stickY); + if (control->pressed.a || control->held.a) + { + ui_spriteDraw(TILE6, sprite_faceButtons1, aHeld, textPositions.x - 68, textPositions.y + 66); + } + else + { + ui_spriteDraw(TILE6, sprite_faceButtons0, aIdle, textPositions.x - 68, textPositions.y + 66); + } + + int count = core_get_playercount(); + static int set = 0; + if (count == 4) + { + + if (control->pressed.b || control->held.b) + { + ui_spriteDraw(TILE4, sprite_faceButtons1, bHeld, textPositions.x - 86, textPositions.y + 8); + } + else + { + ui_spriteDraw(TILE4, sprite_faceButtons1, bIdle, textPositions.x - 86, textPositions.y + 8); + } + + set = diff; + } + else + { + set = core_get_aidifficulty(); + } + + // Text + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, textPositions.x - 54, textPositions.y - 56, " Halcyon\nHexagons"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, textPositions.x - 32, textPositions.y - 18, "Press"); + rdpq_text_printf(&txt_gameParms, ID_DEFAULT, textPositions.x - 68, textPositions.y + 20, + "%s %s\n" + "%s %d\n" + "%s %d\n" + "%s\n\n" + "%s\n", + uiMainMenuStrings[TEXT_DIFF], uiMainMenuStrings[TEXT_DIFF + set + 1], + uiMainMenuStrings[TEXT_PLAYERS], count, + uiMainMenuStrings[TEXT_BOTS], ACTOR_COUNT - count, + uiMainMenuStrings[TEXT_CONTROLS], + uiMainMenuStrings[TEXT_RUMBLE]); + } + + void ui_pause(ControllerData *control) + { + + ui_spriteDrawPanel(TILE2, sprite_gloss, T_BLUE, 90, 60, 230, 144, 0, 0, 64, 64); + ui_spriteDrawPanel(TILE3, sprite_tessalate, T_BLACK, 100, 65, 220, 134, 0, 0, 64, 64); + + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 1, 170, 110); + } + else + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 0, 170, 110); + } + + ui_spriteDraw(TILE5, sprite_dPadTriggers, 5, 160, 180); + + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, 106, 84, " Halcyon\nHexagons"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 128, 122, "Press\n\n\n PAUSED\n\nHold to\nQuit Game"); + } + + void ui_character_select(ControllerData *control, uint8_t selectedActor) + { + // Buttons + if (control->pressed.a || control->held.a) + { + ui_spriteDraw(TILE2, sprite_faceButtons1, aHeld, 104, 46); + } + else + { + ui_spriteDraw(TILE2, sprite_faceButtons0, aIdle, 104, 46); + } + + // Text + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, 70, 40, "Character Select"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 63, 58, "Press to Confirm Selection"); + rdpq_text_printf(&txt_titleParms, ID_DEFAULT, 84, 76, "Selected Actor: %s", uiCharacterSelectStrings[selectedActor]); + } + + // Time to crash test the RDP + void ui_input_display(ControllerData *control) + { + int s = 24; + int t = 164; + + // First row: Triggers + if (control->pressed.l || control->held.l) + ui_spriteDraw(TILE0, sprite_dPadTriggers, 6, s, t); + if (control->pressed.z || control->held.z) + ui_spriteDraw(TILE1, sprite_dPadTriggers, 5, s + spriteWidth, t); + if (control->pressed.r || control->held.r) + ui_spriteDraw(TILE2, sprite_dPadTriggers, 7, s + (spriteWidth * 2), t); + + // Second row: Face Buttons + if (control->pressed.a || control->held.a) + { + ui_spriteDraw(TILE3, sprite_faceButtons1, aHeld, s, t + spriteHeight); + } + else + { + ui_spriteDraw(TILE3, sprite_faceButtons0, aIdle, s, t + spriteHeight); + } + + if (control->pressed.b || control->held.b) + { + ui_spriteDraw(TILE4, sprite_faceButtons1, bHeld, s + spriteHeight, t + spriteHeight); + } + else + { + ui_spriteDraw(TILE4, sprite_faceButtons1, bIdle, s + spriteHeight, t + spriteHeight); + } + + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE5, sprite_faceButtons0, 1, s + (spriteHeight * 2), t + spriteHeight); + } + else + { + ui_spriteDraw(TILE5, sprite_faceButtons0, 0, s + (spriteHeight * 2), t + spriteHeight); + } + + // Third row: Sticks + ui_spriteDraw(TILE6, sprite_controlStick, 0, s, t + (spriteHeight * 2)); + int stickX = s + (control->input.stick_x / 15); + int stickY = t + (spriteHeight * 2) - (control->input.stick_y / 15); + ui_spriteDraw(TILE6, sprite_controlStick, 1, stickX, stickY); + if (control->pressed.c_up || control->held.c_up) + { + ui_spriteDraw(TILE7, sprite_cButtons1, C_UP, s + (spriteHeight * 2), t + (spriteHeight * 2)); + } + else if (control->pressed.c_down || control->held.c_down) + { + ui_spriteDraw(TILE7, sprite_cButtons1, C_DOWN, s + (spriteHeight * 2), t + (spriteHeight * 2)); + } + else if (control->pressed.c_left || control->held.c_left) + { + ui_spriteDraw(TILE7, sprite_cButtons1, C_LEFT, s + (spriteHeight * 2), t + (spriteHeight * 2)); + } + else if (control->pressed.c_right || control->held.c_right) + { + ui_spriteDraw(TILE7, sprite_cButtons1, C_RIGHT, s + (spriteHeight * 2), t + (spriteHeight * 2)); + } + else + { + ui_spriteDraw(TILE7, sprite_cButtons0, 0, s + (spriteHeight * 2), t + (spriteHeight * 2)); + } + } + + void ui_intro(ControllerData *control) + { + // Basic frame counter for timing + static uint32_t introTimer = 0; + introTimer++; + + // Animated text positions + static Vector2 topTextPosition = {93.0f, 0.0f}; + topTextPosition.y = topTextPosition.y + 2.0f; + if (topTextPosition.y > 56.0f) + topTextPosition.y = 56.0f; + + // Dynamic alpha from prim colors + uint32_t dynamicColorsPacked[3] = {0, 0, 0}; + color_t dynamicColors[3]; + dynamicColorsPacked[0] = ui_colorSetAlpha(COLORS[N_RED], introTimer * 45.0f * display_get_delta_time()); + dynamicColorsPacked[1] = ui_colorSetAlpha(COLORS[N_GREEN], introTimer * 45.0f * display_get_delta_time()); + dynamicColorsPacked[2] = ui_colorSetAlpha(COLORS[N_YELLOW], introTimer * 45.0f * display_get_delta_time()); + dynamicColors[0] = color_from_packed32(dynamicColorsPacked[0]); + dynamicColors[1] = color_from_packed32(dynamicColorsPacked[1]); + dynamicColors[2] = color_from_packed32(dynamicColorsPacked[2]); + + if (introTimer < 200) + { + + /* MADE WITH SCREEN */ + + // Panels + ui_spriteDrawDynamic(TILE1, sprite_libdragon, dynamicColors[0], 28, 76, 156, 140, 0, 0, 128, 64); + ui_spriteDrawDynamic(TILE2, sprite_t3d, dynamicColors[1], 160, 76, 288, 140, 0, 0, 64, 32); + ui_spriteDrawDynamic(TILE3, sprite_mixamo, dynamicColors[2], 96, 146, 224, 210, 0, 0, 128, 64); + + // Buttons + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 1, 132, 214); + } + else + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 0, 132, 214); + } + + // Text + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, topTextPosition.x, topTextPosition.y, "Made with"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 90, 226, "Press to Skip Intro"); + } + else if (introTimer < 300) + { + + /* STRAWBERRY SCREEN */ + + // Panels + ui_spriteDrawPanel(TILE1, sprite_strawberryTop, WHITE, 128, 80, 196, 112, 0, 0, 32, 16); + if (introTimer >= 240) + { + ui_spriteDrawPanel(TILE2, sprite_strawberry1, WHITE, 128, 112, 196, 144, 0, 0, 32, 16); + } + else + { + ui_spriteDrawPanel(TILE2, sprite_strawberry0, WHITE, 128, 112, 196, 144, 0, 0, 32, 16); + } + + // Buttons + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE3, sprite_faceButtons0, 1, 132, 214); + } + else + { + ui_spriteDraw(TILE3, sprite_faceButtons0, 0, 132, 214); + } + + // Text + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, 68, 56, "Strawberry Byte"); + if (introTimer >= 240) + rdpq_text_print(&txt_titleParms, ID_TITLE, 110, 190, "Presents"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 90, 226, "Press to Skip Intro"); + } + else + { + + // Buttons + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE3, sprite_faceButtons0, 1, 170, 66); + } + else + { + ui_spriteDraw(TILE3, sprite_faceButtons0, 0, 170, 66); + } + + // Text + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, 106, 40, " Halcyon\nHexagons"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 128, 78, "Press"); + rdpq_text_print(&txt_titleParms, ID_DEFAULT, 32, 94, "CREDITS:"); + + // @TODO: Probably should make this a rdpq_paragraph + rdpq_text_print(&txt_gameParms, ID_DEBUG, 32, 114, + "Programming: zoncabe, s4ys\n" + "Models: zoncabe, mewde, s4ys\n" + "- Original 'Olli' by FazanaJ\n" + "Strawberry Sprite by Sonika Rud\n" + "UI Sprites by Kenney\n" + "Music by Kaelin Stemmler\n" + "SFX obtained from Freesound"); + } + } + + void ui_cleanup(void) + { + ui_spriteCleanup(); + ui_fontUnregister(); + ui_fileCleanup(); + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_H \ No newline at end of file diff --git a/code/sb_halcyon/ui/ui_colors.h b/code/sb_halcyon/ui/ui_colors.h new file mode 100644 index 00000000..619c87a8 --- /dev/null +++ b/code/sb_halcyon/ui/ui_colors.h @@ -0,0 +1,102 @@ +#ifndef UI_COLORS_H +#define UI_COLORS_H + +#ifdef __cplusplus +extern "C" +{ +#endif + + enum COLOR_NAMES + { + // Standard ROYGBIV + RED, + ORANGE, + YELLOW, + GREEN, + BLUE, + INDIGO, + VIOLET, + // RGB 0 (full black) & 1 (full white) + BLACK, + WHITE, + // RGB 1 * (n*.25f) + LIGHT_GREY, // n = 3 + GREY, // n = 2 + DARK_GREY, // n = 1 + // Transparent Colors + TRANSPARENT, + T_RED, + T_ORANGE, + T_YELLOW, + T_GREEN, + T_BLUE, + T_INDIGO, + T_VIOLET, + T_BLACK, + T_WHITE, + T_GREY, + // Darker Variants + DARK_RED, + DARK_GREEN, + // N64 Logo Colors + N_RED, + N_YELLOW, + N_GREEN, + N_BLUE, + COLOR_COUNT + }; + + // FMT_RGBA32, 32-bit packed RGBA (8888) + const uint32_t COLORS[COLOR_COUNT] = + { + 0xD90000FF, // RED + 0xFF6822FF, // ORANGE + 0xFFDA21FF, // YELLOW + 0x33DD00FF, // GREEN + 0x1133CCFF, // BLUE + 0x220066FF, // INDIGO + 0x330044FF, // VIOLET + 0x000000FF, // BLACK + 0xFFFFFFFF, // WHITE + 0xC0C0C0FF, // LIGHT_GREY + 0x808080FF, // GREY + 0x404040FF, // DARK_GREY + 0x0000007F, // TRANSPARENT + 0xD90000C8, // T_RED + 0xFF6822C8, // T_ORANGE + 0xFFDA21C8, // T_YELLOW + 0x33DD00C8, // T_GREEN + 0x1133CCC8, // T_BLUE + 0x220066C8, // T_INDIGO + 0x330044C8, // T_VIOLET + 0x1F1F1FC8, // T_BLACK + 0xFFFFFFC8, // T_WHITE + 0xC0C0C0C8, // T_GREY + 0x820000FF, // DARK_RED + 0x006400FF, // DARK_GREEN + 0xE10916FF, // N_RED + 0xF5B201FF, // N_YELLOW + 0x319900FF, // N_GREEN + 0x01009AFF, // N_BLUE + }; + + inline color_t ui_color(int colorIdx); + uint32_t ui_colorSetAlpha(uint32_t color, uint8_t alpha); + + // Creates a color_t from one of the 32-bit packed COLORS. + inline color_t ui_color(int colorIdx) + { + return color_from_packed32(COLORS[colorIdx]); + } + + // Clears the alpha bits and sets them to the new value + uint32_t ui_colorSetAlpha(uint32_t color, uint8_t alpha) + { + return (color & 0xFFFFFF00) | (alpha & 0xFF); + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_COLORS_H \ No newline at end of file diff --git a/code/sb_halcyon/ui/ui_file.h b/code/sb_halcyon/ui/ui_file.h new file mode 100644 index 00000000..7f460909 --- /dev/null +++ b/code/sb_halcyon/ui/ui_file.h @@ -0,0 +1,139 @@ +#ifndef UI_FILE_H +#define UI_FILE_H + +#define NUM_FONTS 2 +#define NUM_SPRITES 6 + +#ifdef __cplusplus +extern "C" +{ +#endif + + // Base directory for UI assets. + const char *basePath = "rom:/strawberry_byte/ui/"; + + // Arrays for font file names and paths: + // - uiFontFileName: Array of pointers to store full paths to font files after initialization. + // - uiFontPaths: Constant array of relative paths for each font, appended to basePath at runtime. + const char *uiFontFileName[NUM_FONTS]; + const char *uiFontPaths[NUM_FONTS] = { + "fonts/TitanOne-Regular.font64", + "fonts/OilOnTheWater-ee5O.font64"}; + + // Arrays for button sprite file names and paths. + const char *uiSpriteButtonFileName[NUM_SPRITES]; + const char *uiSpriteButtonPath[NUM_SPRITES] = { + "buttons/control_stick.ia8.sprite", + "buttons/d_pad_triggers.ia8.sprite", + "buttons/c_buttons0.rgba32.sprite", + "buttons/c_buttons1.rgba32.sprite", + "buttons/face_buttons0.rgba32.sprite", + "buttons/face_buttons1.rgba32.sprite"}; + + // Arrays for button sprite file names and paths. + const char *uiSpritePanelFileName[3]; + const char *uiSpritePanelPath[3] = { + "panels/gloss.ia4.sprite", + "panels/pattern_tessalate.ia4.sprite", + "panels/clouds.ia8.sprite", + }; + + // Arrays for logo sprite file names and paths. See LICENSE.txt for attribution. + const char *uiSpriteLogoFileName[NUM_SPRITES]; + const char *uiSpriteLogoPath[NUM_SPRITES] = { + "logos/sb_b0.rgba32.sprite", + "logos/sb_b1.rgba32.sprite", + "logos/sb_top.rgba32.sprite", + "logos/t3d.ia8.sprite", + "logos/libdragon.ia4.sprite", + "logos/mixamo.ia4.sprite"}; + + /* Declarations */ + + char *ui_filePath(const char *fn); + void ui_fileFonts(void); + void ui_fileSprites(void); + void ui_fileLogos(void); + void ui_fileGet(void); + void ui_fileCleanup(void); + + /* Definitions */ + + // Concatenates basePath and fn, returning the full path (caller must free memory). + char *ui_filePath(const char *fn) + { + char *fullPath = (char *)malloc(256 * sizeof(char)); + if (!fullPath) + { + return NULL; + } + + sprintf(fullPath, "%s%s", basePath, fn); + + return fullPath; + } + + // Populates uiFontFileName with full paths for fonts. + void ui_fileFonts(void) + { + for (int i = 0; i < NUM_FONTS; ++i) + { + uiFontFileName[i] = ui_filePath(uiFontPaths[i]); + } + } + + // Populates uiSpriteButtonFileName and uiSpritePanelFileName with full paths for sprites. + void ui_fileSprites(void) + { + for (int i = 0; i < NUM_SPRITES; ++i) + { + uiSpriteButtonFileName[i] = ui_filePath(uiSpriteButtonPath[i]); + } + for (int i = 0; i < 3; ++i) + { + uiSpritePanelFileName[i] = ui_filePath(uiSpritePanelPath[i]); + } + } + + void ui_fileLogos(void) + { + for (int i = 0; i < NUM_SPRITES; ++i) + { + uiSpriteLogoFileName[i] = ui_filePath(uiSpriteLogoPath[i]); + } + } + + // Calls functions to initialize font and sprite file paths. + void ui_fileGet(void) + { + ui_fileFonts(); + ui_fileSprites(); + ui_fileLogos(); + } + + // Frees memory allocated for font and sprite file paths. + void ui_fileCleanup(void) + { + for (int i = 0; i < NUM_FONTS; i++) + { + free((char *)uiFontFileName[i]); + ; + } + + for (int i = 0; i < 3; i++) + { + free((char *)uiSpritePanelFileName[i]); + } + + for (int i = 0; i < NUM_SPRITES; i++) + { + free((char *)uiSpriteButtonFileName[i]); + free((char *)uiSpriteLogoFileName[i]); + } + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_FILE_H \ No newline at end of file diff --git a/code/sb_halcyon/ui/ui_font.h b/code/sb_halcyon/ui/ui_font.h new file mode 100644 index 00000000..6c9322ce --- /dev/null +++ b/code/sb_halcyon/ui/ui_font.h @@ -0,0 +1,154 @@ +#ifndef UI_FONT_H +#define UI_FONT_H + +#include "ui_colors.h" +#include "ui_file.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + enum FONT_IDS + { + ID_RESERVED, + ID_DEBUG, + ID_DEFAULT, + ID_TITLE, + ID_COUNT + }; + + // Array of pointers to rdpq_font_t, with each entry representing a font identified by an ID. + rdpq_font_t *font[ID_COUNT]; + + enum FONT_STYLES + { + STYLE_DEBUG, + STYLE_DEFAULT, + STYLE_TITLE, + STYLE_BRIGHT, + STYLE_GREEN, + STYLE_PLAYER, + STYLE_COUNT = STYLE_PLAYER + MAXPLAYERS + }; + + // RDPQ text parameters, used here primarily to set the following RDPQ font styles. + rdpq_textparms_t txt_debugParms; + rdpq_textparms_t txt_titleParms; + rdpq_textparms_t txt_gameParms; + + // RDPQ font styles, used here primarily to set text color. + rdpq_fontstyle_t txt_debug_fontStyle; + rdpq_fontstyle_t txt_player_fontStyle; + rdpq_fontstyle_t txt_title_fontStyle; + rdpq_fontstyle_t txt_game_fontStyle; + rdpq_fontstyle_t txt_bright_fontStyle; + rdpq_fontstyle_t txt_green_fontStyle; + + /* Declarations */ + + void ui_fontRegister(void); + void ui_fontUnregister(void); + + /* Definitions */ + + /* All in one font initialization. + - Loads and regsiters fonts. + - Assigns colors to font styles. + - Register font styles for each font. + - Assigns font styles to text parameters. + + Possible improvements would be to separate functionality, + to make fonts more flexible and modular. */ + void ui_fontRegister(void) + { + ui_fileFonts(); + + font[ID_DEBUG] = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_VAR); + font[ID_DEFAULT] = rdpq_font_load(uiFontFileName[0]); + font[ID_TITLE] = rdpq_font_load(uiFontFileName[1]); + + // Create and register font styles + txt_debug_fontStyle.color = ui_color(YELLOW); + txt_debug_fontStyle.outline_color = ui_color(BLACK); + + const color_t playerColors[MAXPLAYERS] = { + PLAYERCOLOR_1, + PLAYERCOLOR_2, + PLAYERCOLOR_3, + PLAYERCOLOR_4, + }; + + txt_player_fontStyle.outline_color = ui_color(BLACK); + + txt_game_fontStyle.color = ui_color(WHITE); + txt_game_fontStyle.outline_color = ui_color(BLACK); + + txt_title_fontStyle.color = ui_color(WHITE); + txt_title_fontStyle.outline_color = ui_color(N_BLUE); + + txt_bright_fontStyle.color = ui_color(YELLOW); + txt_bright_fontStyle.outline_color = ui_color(BLACK); + + txt_green_fontStyle.color = ui_color(GREEN); + txt_green_fontStyle.outline_color = ui_color(DARK_GREEN); + + for (int i = 1; i < ID_COUNT; i++) + { + rdpq_text_register_font(i, font[i]); + + rdpq_font_style( + font[i], + STYLE_DEFAULT, + &txt_game_fontStyle); + + rdpq_font_style( + font[i], + STYLE_TITLE, + &txt_title_fontStyle); + + rdpq_font_style( + font[i], + STYLE_BRIGHT, + &txt_bright_fontStyle); + + rdpq_font_style( + font[i], + STYLE_GREEN, + &txt_green_fontStyle); + + for (int p = 1; p <= MAXPLAYERS; p++) + { + txt_player_fontStyle.color = playerColors[p - 1]; + rdpq_font_style( + font[i], + STYLE_PLAYER + p - 1, + &txt_player_fontStyle); + } + } + + rdpq_font_style( + font[ID_DEBUG], + STYLE_DEBUG, + &txt_debug_fontStyle); + + txt_debugParms = (rdpq_textparms_t){.style_id = STYLE_DEBUG, .disable_aa_fix = true}; + txt_titleParms = (rdpq_textparms_t){.style_id = STYLE_TITLE, .disable_aa_fix = true, .align = ALIGN_CENTER, .valign = VALIGN_CENTER}; + txt_gameParms = (rdpq_textparms_t){.style_id = STYLE_BRIGHT, .disable_aa_fix = true}; + } + + // Unregisters and frees fonts for the next minigame. + void ui_fontUnregister(void) + { + for (int i = ID_DEBUG; i < ID_COUNT; ++i) + { + rdpq_text_unregister_font(i); + rdpq_font_free(font[i]); + } + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_FONT_H \ No newline at end of file diff --git a/code/sb_halcyon/ui/ui_sprite.h b/code/sb_halcyon/ui/ui_sprite.h new file mode 100644 index 00000000..5b971bb2 --- /dev/null +++ b/code/sb_halcyon/ui/ui_sprite.h @@ -0,0 +1,169 @@ +#ifndef UI_SPRITE_H +#define UI_SPRITE_H + +#include "ui_file.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + // Constant dimensions for sprites + const int spriteWidth = 16, spriteHeight = 16; + + // Static pointers to sprites representing different controller elements + static sprite_t *sprite_controlStick; + static sprite_t *sprite_dPadTriggers; + static sprite_t *sprite_cButtons0; + static sprite_t *sprite_cButtons1; + static sprite_t *sprite_faceButtons0; + static sprite_t *sprite_faceButtons1; + + // Static pointers to sprites for panels + static sprite_t *sprite_gloss; + static sprite_t *sprite_tessalate; + static sprite_t *sprite_clouds; + + // Static pointers to sprites for logos + static sprite_t *sprite_libdragon; + static sprite_t *sprite_mixamo; + static sprite_t *sprite_t3d; + static sprite_t *sprite_strawberryTop; + static sprite_t *sprite_strawberry0; + static sprite_t *sprite_strawberry1; + + // Surfaces for rendering UI elements + surface_t surf_UIpanels; + surface_t surf_UIsprites; + + /* Declarations */ + + void ui_spriteLoad(void); + void ui_syncSprite(int color); + void ui_spriteDraw(rdpq_tile_t tile, sprite_t *sprite, int idx, int x, int y); + void ui_spriteDrawPanel(rdpq_tile_t tile, sprite_t *sprite, int color, int x0, int y0, int x1, int y1, int s, int t, int s1, int t1); + void ui_spriteDrawDynamic(rdpq_tile_t tile, sprite_t *sprite, color_t color, int x0, int y0, int x1, int y1, int s, int t, int s1, int t1); + void ui_spriteCleanup(void); + + /* Definitions */ + + // Loads and assigns sprites to their corresponding pointers based on file paths set by ui_fileSprites. + void ui_spriteLoad(void) + { + ui_fileSprites(); + ui_fileLogos(); + + // Load IA format sprites (grayscale with alpha for UI overlays). + sprite_gloss = sprite_load(uiSpritePanelFileName[0]); + sprite_tessalate = sprite_load(uiSpritePanelFileName[1]); + sprite_clouds = sprite_load(uiSpritePanelFileName[2]); + + sprite_controlStick = sprite_load(uiSpriteButtonFileName[0]); + sprite_dPadTriggers = sprite_load(uiSpriteButtonFileName[1]); + sprite_libdragon = sprite_load(uiSpriteLogoFileName[4]); + sprite_mixamo = sprite_load(uiSpriteLogoFileName[5]); + sprite_t3d = sprite_load(uiSpriteLogoFileName[3]); + + // Load RGBA32 format sprites (full color with transparency for UI buttons). + sprite_cButtons0 = sprite_load(uiSpriteButtonFileName[2]); + sprite_cButtons1 = sprite_load(uiSpriteButtonFileName[3]); + sprite_faceButtons0 = sprite_load(uiSpriteButtonFileName[4]); + sprite_faceButtons1 = sprite_load(uiSpriteButtonFileName[5]); + sprite_strawberry0 = sprite_load(uiSpriteLogoFileName[0]); + sprite_strawberry1 = sprite_load(uiSpriteLogoFileName[1]); + sprite_strawberryTop = sprite_load(uiSpriteLogoFileName[2]); + } + + // Optional RDPQ sync and set for sprites. Similar to ui_syncText, but sets the combiner for textures and allows for primitive color to added. + void ui_syncSprite(int color) + { + rdpq_sync_pipe(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_TEX_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_set_prim_color(ui_color(color)); + rdpq_sync_tile(); + } + + // Draws a simple 16x16 sprite. + void ui_spriteDraw(rdpq_tile_t tile, sprite_t *sprite, int idx, int x, int y) + { + int s = 0, t = 0; + int idxCopy = idx; + + if (idx > 4) + { + idx = idx % 4; + s = spriteWidth * idx; + } + else + { + s = spriteWidth * idx; + } + + t = (idxCopy / 4) * spriteHeight; + + ui_syncSprite(WHITE); + + surf_UIsprites = sprite_get_pixels(sprite); + + rdpq_tex_upload_sub(tile, &surf_UIsprites, NULL, s, t, s + spriteWidth, t + spriteHeight); + rdpq_texture_rectangle(tile, x, y, x + spriteWidth, y + spriteHeight, s, t); + } + + // Draws a scalable sprite with predefined primitive color by index. + void ui_spriteDrawPanel(rdpq_tile_t tile, sprite_t *sprite, int colorIdx, int x0, int y0, int x1, int y1, int s, int t, int s1, int t1) + { + + ui_syncSprite(colorIdx); + + surf_UIpanels = sprite_get_pixels(sprite); + + rdpq_tex_upload(tile, &surf_UIpanels, NULL); + rdpq_texture_rectangle_scaled(tile, x0, y0, x1, y1, s, t, s1, t1); + } + + // Draws a scalable sprite with added primitive color. + void ui_spriteDrawDynamic(rdpq_tile_t tile, sprite_t *sprite, color_t color, int x0, int y0, int x1, int y1, int s, int t, int s1, int t1) + { + + rdpq_sync_pipe(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_TEX_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_set_prim_color(color); + rdpq_sync_tile(); + + surf_UIpanels = sprite_get_pixels(sprite); + + rdpq_tex_upload(tile, &surf_UIpanels, NULL); + rdpq_texture_rectangle_scaled(tile, x0, y0, x1, y1, s, t, s1, t1); + } + + // Frees static pointers to sprites. + void ui_spriteCleanup(void) + { + sprite_free(sprite_controlStick); + sprite_free(sprite_dPadTriggers); + sprite_free(sprite_cButtons0); + sprite_free(sprite_cButtons1); + sprite_free(sprite_faceButtons0); + sprite_free(sprite_faceButtons1); + sprite_free(sprite_gloss); + sprite_free(sprite_clouds); + sprite_free(sprite_tessalate); + sprite_free(sprite_libdragon); + sprite_free(sprite_mixamo); + sprite_free(sprite_t3d); + sprite_free(sprite_strawberryTop); + sprite_free(sprite_strawberry0); + sprite_free(sprite_strawberry1); + surface_free(&surf_UIpanels); + surface_free(&surf_UIsprites); + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_SPRITE_H \ No newline at end of file diff --git a/code/sb_holes/main.c b/code/sb_holes/main.c new file mode 100644 index 00000000..14be3c08 --- /dev/null +++ b/code/sb_holes/main.c @@ -0,0 +1,194 @@ +#include +#include "../../minigame.h" +#include "../../core.h" +#include +#include +#include +#include +#include +#include + +/** + * Simple clone of Hole.io + * Basically just pushing myself to make something in a shorter time. + */ + +const MinigameDef minigame_def = { + .gamename = "holes", + .developername = "Strawberry Byte: s4ys", + .description = "Clone of Hole.io", + .instructions = "Try to devour as much as possible!"}; + +// Global subsystems +#include "util.h" +#include "sound.h" + +// Locals +T3DModel *model; +T3DModel *modelCar; +T3DModel *modelBuilding; +T3DModel *modelHydrant; + +////////// MAP +#include "map.h" +map_data map; + +////////// CAMERA +camera_data cam[MAXPLAYERS]; + +////////// OBJECTS +#include "objects.h" +object_type objects[NUM_OBJ_TYPES]; +T3DVec3 gridPos[MAX_GRID_POINTS]; +size_t gridPointCount = 0; +T3DObject *buildings[2]; +bool spray[NUM_OBJECTS] = {false}; +bool stop[NUM_OBJECTS] = {false}; + +////////// PLAYERS +#include "player.h" +player_data players[MAXPLAYERS]; + +////////// TEXT +#include "ui.h" + +////////// VIEWPORTS +#include "screen.h" +surface_t *depthBuffer; +T3DViewport viewport[MAXPLAYERS]; + +////////// SCENE +#include "render.h" +#include "scene.h" +scene_data scenes[NUM_SCENES]; + +game_data game; + +void minigame_init(void) +{ + + display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE_ANTIALIAS); + depthBuffer = display_get_zbuf(); + +#ifdef DEBUG_RDP + rdpq_debug_start(); +#endif + + t3d_init((T3DInitParams){}); + + ui_init(); + + viewport_create(viewport); + + map_init(&map); + + ////////// OBJECTS + modelCar = t3d_model_load("rom:/sb_holes/car.t3dm"); + modelBuilding = t3d_model_load("rom:/sb_holes/building.t3dm"); + modelHydrant = t3d_model_load("rom:/sb_holes/hydrant.t3dm"); + + for (int i = 0; i < NUM_OBJ_TYPES; i++) + { + object_initBatch(&objects[i], i); + } + + ////////// PLAYERS + model = t3d_model_load("rom:/sb_holes/hole.t3dm"); + + T3DVec3 start_positions[] = { + (T3DVec3){{-50, 5, -52}}, + (T3DVec3){{52, 5, -52}}, + (T3DVec3){{-50, 5, 40}}, + (T3DVec3){{52, 5, 40}}, + }; + + float start_rotations[] = { + M_PI / 2, + 0, + 3 * M_PI / 2, + M_PI}; + + const color_t colors[] = { + PLAYERCOLOR_1, + PLAYERCOLOR_2, + PLAYERCOLOR_3, + PLAYERCOLOR_4, + }; + + for (size_t i = 0; i < MAXPLAYERS; i++) + { + player_init(&players[i], colors[i], start_positions[i], start_rotations[i]); + players[i].plynum = i; + cam[i].position = (T3DVec3){{players[i].playerPos.x, players[i].playerPos.y + 150.0f, players[i].playerPos.z + 100.0f}}; + cam[i].target = players[i].playerPos; + } + ////////// + + game.playerCount = core_get_playercount(); + game.introTimer = INTRO_DELAY; + game.countDownTimer = COUNTDOWN_DELAY; + game.isEnding = false; + game.endTimer = 0; + game.winner = 0; + game.syncPoint = 0; + scene_init(&game, scenes); + game.scene = INTRO; + + // Decide whether to limit FPS and/or disable background music + switch (game.playerCount) + { + case 1: + sound_load(true); + break; + case 2: + case 3: + sound_load(true); + display_set_fps_limit(display_get_refresh_rate() * 0.5f); + break; + case 4: + sound_load(false); + display_set_fps_limit(display_get_refresh_rate() * 0.5f); + break; + } +} + +void minigame_fixedloop(float deltaTime) +{ + scenes[game.scene].fixedLoop(&game, deltaTime); +} + +void minigame_loop(float deltaTime) +{ + scenes[game.scene].loop(&game, deltaTime); +} + +void minigame_cleanup(void) +{ +#ifdef DEBUG_RDP + rdpq_debug_stop(); +#endif + + sound_cleanup(); + + display_set_fps_limit(0); + + for (size_t i = 0; i < MAXPLAYERS; i++) + { + player_cleanup(&players[i]); + } + + for (int i = 0; i < NUM_OBJ_TYPES; i++) + { + object_destroyBatch(&objects[i]); + } + + t3d_model_free(model); + + map_cleanup(&map); + + ui_cleanup(); + + t3d_destroy(); + + display_close(); +} diff --git a/code/sb_holes/map.h b/code/sb_holes/map.h new file mode 100644 index 00000000..1406ba84 --- /dev/null +++ b/code/sb_holes/map.h @@ -0,0 +1,28 @@ +#ifndef MAP_H +#define MAP_H + +extern map_data map; + +void map_init(map_data *map); +void map_cleanup(map_data *map); + +// Allocates and sets matrix, loads model and creates RSPQ block +void map_init(map_data *map) +{ + map->mtxFP = malloc_uncached(sizeof(T3DMat4FP)); + t3d_mat4fp_from_srt_euler(map->mtxFP, (float[3]){0.3f, 0.3f, 0.3f}, (float[3]){0, 0, 0}, (float[3]){0, 0, -10}); + map->model = t3d_model_load("rom:/sb_holes/map.t3dm"); + rspq_block_begin(); + t3d_matrix_set(map->mtxFP, true); + rdpq_set_prim_color(RGBA32(255, 255, 255, 255)); + t3d_model_draw(map->model); + map->block = rspq_block_end(); +} + +void map_cleanup(map_data *map) +{ + rspq_block_free(map->block); + t3d_model_free(map->model); + free_uncached(map->mtxFP); +} +#endif // MAP_H \ No newline at end of file diff --git a/code/sb_holes/objects.h b/code/sb_holes/objects.h new file mode 100644 index 00000000..beb510af --- /dev/null +++ b/code/sb_holes/objects.h @@ -0,0 +1,308 @@ +#ifndef OBJECTS_H +#define OBJECTS_H + +#define GRID_SIZE 144 // BOX_SIZE plus 4 to be neatly divide by 9 +#define CELL_SIZE 96 +#define NUM_CELLS (GRID_SIZE * 2 / CELL_SIZE) +#define MAX_GRID_POINTS (NUM_CELLS * NUM_CELLS) + +extern T3DVec3 gridPos[MAX_GRID_POINTS]; +extern size_t gridPointCount; +extern T3DObject *buildings[2]; +extern bool spray[NUM_OBJECTS]; +extern bool stop[NUM_OBJECTS]; + +void generate_grid(void); +void object_init(object_data *object, uint8_t objectType, uint8_t ID, T3DVec3 position); +void object_initBatch(object_type *batch, uint8_t objectType); +void hydrant_water_spray(T3DVec3 position, T3DViewport *viewport); +void object_updateBatch(object_type *batch, T3DViewport *vp, player_data *player); +void object_cull(object_type *batch, T3DViewport *vp, int playercount); +void object_drawBatch(object_type *batch); +void object_destroyBatch(object_type *batch); + +// Checks whether an object is close enough to the player's frustum to enable rendering +void object_cull(object_type *batch, T3DViewport *vp, int playercount) +{ + for (int o = 0; o < NUM_OBJECTS; o++) + { + // We want to find a sweet spot between performance and minimizing pop-in + if (t3d_frustum_vs_sphere(&vp->viewFrustum, &gridPos[o], (60.0f * batch->collisionRadius) - (playercount * batch->collisionRadius))) + { + batch->objects[o].hide = false; + } + else + { + batch->objects[o].hide = true; + } + } +} + +// Populates gridPos according to predefined settings +void generate_grid(void) +{ + gridPointCount = 0; + + // Calculate half the grid size + int halfGridSize = GRID_SIZE; + + for (int i = 0; i < NUM_CELLS; i++) + { + for (int j = 0; j < NUM_CELLS; j++) + { + // Calculate cell center coordinates + int x = -halfGridSize + j * CELL_SIZE + CELL_SIZE / 2; + int z = -halfGridSize + i * CELL_SIZE + CELL_SIZE / 2; + + // Store the grid cell center position + gridPos[gridPointCount++] = (T3DVec3){{(float)x, 10.0f, (float)z}}; + } + } +} + +void object_init(object_data *object, uint8_t objectType, uint8_t ID, T3DVec3 position) +{ + object->ID = ID; + object->mtxFP = malloc_uncached(sizeof(T3DMat4FP)); + object->position = position; + object->texID = 0; + + switch (objectType) + { + case OBJ_CAR: + object->scale = (T3DVec3){{0.1f, 0.1f, 0.1f}}; + object->position.v[0] = fmaxf(-GRID_SIZE, fminf(GRID_SIZE, object->position.v[0] + (rand() % 20))); + object->position.v[2] = fmaxf(-GRID_SIZE, fminf(GRID_SIZE, object->position.v[2] + 45)); + object->yaw = T3D_DEG_TO_RAD(90.0f); + object->color = rand() % 2 == 0 ? color_from_packed32(0xE10916FF) : color_from_packed32(0x319900FF); + break; + case OBJ_BUILDING: + object->scale = (T3DVec3){{0.3f, 0.3f, 0.3f}}; + object->yaw = 0; + object->color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF); + break; + case OBJ_HYDRANT: + object->scale = (T3DVec3){{0.06f, 0.06f, 0.06f}}; + object->position.v[0] = fmaxf(-GRID_SIZE, fminf(GRID_SIZE, object->position.v[0] + 25)); + object->position.v[2] = fmaxf(-GRID_SIZE, fminf(GRID_SIZE, object->position.v[2] + 25)); + object->yaw = 0; + object->color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF); + break; + } + + object->visible = true; + object->hide = false; +} + +void object_initBatch(object_type *batch, uint8_t objectType) +{ + + batch->type = objectType; + + // Assign model to objects + switch (batch->type) + { + case OBJ_CAR: + batch->scoreValue = 2; + batch->model = modelCar; + batch->collisionRadius = 0.2f; + break; + case OBJ_BUILDING: + batch->scoreValue = 4; + batch->model = modelBuilding; + batch->collisionRadius = 0.4f; + buildings[0] = t3d_model_get_object_by_index(modelBuilding, 1); + buildings[1] = t3d_model_get_object_by_index(modelBuilding, 0); + break; + case OBJ_HYDRANT: + batch->scoreValue = 1; + batch->model = modelHydrant; + batch->collisionRadius = 0.125f; + break; + } + + // Initialize batch objects + generate_grid(); + for (size_t i = 0; i < NUM_OBJECTS; i++) + { + object_init(&batch->objects[i], batch->type, i, gridPos[i]); + } + + // Create model block + if (objectType == OBJ_BUILDING) + { + for (size_t i = 0; i < NUM_OBJECTS; i++) + { + rspq_block_begin(); + t3d_matrix_set(batch->objects[i].mtxFP, true); + t3d_model_draw_object(buildings[0], NULL); + + batch->objects[i].modelBlock = rspq_block_end(); + } + } + else + { + for (size_t i = 0; i < NUM_OBJECTS; i++) + { + rspq_block_begin(); + + t3d_matrix_set(batch->objects[i].mtxFP, true); + rdpq_set_prim_color(batch->objects[i].color); + t3d_model_draw(batch->model); + + batch->objects[i].modelBlock = rspq_block_end(); + } + } +} + +// Draws a transparent blue triangle with a randomized height +void hydrant_water_spray(T3DVec3 position, T3DViewport *viewport) +{ + + T3DVec3 screenPos; + t3d_viewport_calc_viewspace_pos(viewport, &screenPos, &position); + + int offset = 5; + float upward = screenPos.y - rand() % 30; + + float v1[] = {screenPos.x, screenPos.y - offset}; + float v2[] = {screenPos.x - offset, upward}; + float v3[] = {screenPos.x + offset, upward}; + + rdpq_sync_pipe(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_set_prim_color(RGBA32(40, 40, 240, 127)); + + rdpq_triangle(&TRIFMT_FILL, v1, v2, v3); +} + +// ALL object behaviors +void object_updateBatch(object_type *batch, T3DViewport *vp, player_data *player) +{ + for (size_t i = 0; i < NUM_OBJECTS; i++) + { + + if (!batch->objects[i].visible) + continue; // just skip the object update if not visible + + if (check_collision(&batch->objects[i].position, batch->collisionRadius, &player->playerPos, HITBOX_RADIUS * player->scale.x)) + { + if (player->scale.x < batch->collisionRadius) + continue; + + // Lower height according to batch's object size + batch->objects[i].position.v[1] -= 0.4f / batch->collisionRadius; + t3d_vec3_lerp(&batch->objects[i].scale, &batch->objects[i].scale, &(T3DVec3){{0.05f, 0.05f, 0.05f}}, fabsf(batch->objects[i].position.v[1]) * 0.001f); + + // SFX + if (batch->objects[i].position.v[1] == 0.0f) + { + switch (batch->type) + { + case OBJ_CAR: + sound_wavPlay(SFX_CAR, false); + break; + case OBJ_HYDRANT: + sound_wavPlay(SFX_HYDRANT, true); // @TODO: Why is this one not playing at all? + break; + case OBJ_BUILDING: + sound_wavPlay(SFX_BUILDING, false); + break; + } + } + + // Cars spin out and get pulled towards the center + if (batch->type == OBJ_CAR) + { + batch->objects[i].yaw -= .1f; + batch->objects[i].position.x = t3d_lerp(batch->objects[i].position.x, player->playerPos.x, 0.04f); + batch->objects[i].position.z = t3d_lerp(batch->objects[i].position.z, player->playerPos.z, 0.04f); + stop[i] = true; + } + + // Hydrants start spraying water after being collided with + if (batch->type == OBJ_HYDRANT) + { + if (!spray[i]) + spray[i] = true; + } + + // Buildings swap materials to create flickering illusion, shake and spin + if (batch->type == OBJ_BUILDING) + { + batch->objects[i].texID = (int)fm_floorf(batch->objects[i].position.v[1]) % 8 == 0 ? 0 : 1; + batch->objects[i].position.x = gridPos[i].x + 1.0f * fm_sinf(batch->objects[i].position.v[1]); + batch->objects[i].yaw -= 0.01f; + } + + // When the height is low enough, increment player score and disable object + while (batch->objects[i].position.v[1] <= -80.0f * batch->collisionRadius) + { + player->score += batch->scoreValue; + batch->objects[i].visible = false; + break; + } + } + else if (batch->type == OBJ_CAR) + { + + // Car go vroom vroom + if (batch->objects[i].position.x <= GRID_SIZE) + { + if (!stop[i]) + batch->objects[i].position.x += 0.2f + 0.1f * (rand() % 5); + } + else + { + batch->objects[i].position.x = -GRID_SIZE; + } + } + + // Update matrices + t3d_mat4fp_from_srt_euler( + batch->objects[i].mtxFP, + batch->objects[i].scale.v, + (float[3]){0, batch->objects[i].yaw, 0}, + batch->objects[i].position.v); + } +} + +void object_drawBatch(object_type *batch) +{ + if (batch->type == OBJ_BUILDING) + { + for (size_t i = 0; i < NUM_OBJECTS; i++) + { + // For the buildings we draw a material and object for each to allow each to flicker independently + t3d_model_draw_material(buildings[batch->objects[i].texID]->material, NULL); + if (batch->objects[i].visible && !batch->objects[i].hide) + { + rspq_block_run(batch->objects[i].modelBlock); + } + } + } + else + { + for (size_t i = 0; i < NUM_OBJECTS; i++) + { + if (batch->objects[i].visible && !batch->objects[i].hide) + rspq_block_run(batch->objects[i].modelBlock); + } + } + rspq_wait(); // RSPQ crashes if we don't wait for the objects to finish +} + +void object_destroyBatch(object_type *batch) +{ + for (size_t i = 0; i < NUM_OBJECTS; i++) + { + free_uncached(batch->objects[i].mtxFP); + rspq_block_free(batch->objects[i].modelBlock); + } + + t3d_model_free(batch->model); +} + +#endif // OBJECTS_H \ No newline at end of file diff --git a/code/sb_holes/player.h b/code/sb_holes/player.h new file mode 100644 index 00000000..fa69028e --- /dev/null +++ b/code/sb_holes/player.h @@ -0,0 +1,312 @@ +#ifndef PLAYER_H +#define PLAYER_H + +extern player_data players[MAXPLAYERS]; + +extern T3DModel *model; + +void player_init(player_data *player, color_t color, T3DVec3 position, float rotation); +void player_do_damage(player_data *player); +bool player_has_control(game_data *game, player_data *player); +void player_fixedloop(game_data *game, player_data *player, object_type *objects, float deltaTime, joypad_port_t port, bool is_human); +void player_loop(game_data *game, player_data *player, float deltaTime, joypad_port_t port, bool is_human); +void player_draw(player_data *player); +void player_cleanup(player_data *player); + +void player_init(player_data *player, color_t color, T3DVec3 position, float rotation) +{ + player->modelMatFP = malloc_uncached(sizeof(T3DMat4FP)); + + player->moveDir = (T3DVec3){{0, 0, 0}}; + player->playerPos = position; + player->scale = (T3DVec3){{0.125f, 0.125f, 0.125f}}; + player->score = 0; + memset(&player->btn, 0, sizeof(control_data)); + + rspq_block_begin(); + t3d_matrix_set(player->modelMatFP, true); + rdpq_set_prim_color(color); + t3d_model_draw(model); + player->dplHole = rspq_block_end(); + + player->rotY = rotation; + player->currSpeed = 0.0f; + player->isAlive = true; + player->ai_targetPlayer = rand() % MAXPLAYERS; + player->ai_targetObject = rand() % NUM_OBJECTS; + player->ai_reactionspeed = (2 - core_get_aidifficulty()) * 5 + rand() % ((3 - core_get_aidifficulty()) * 3); +} + +float ai_target_priority_object(player_data *player, object_data *object) +{ + float priority = 0.0f; + + if (!object->visible) + return 0.0f; + + if (player->score >= 20) + priority -= 0.3f; + + float distance = sqrtf(vec2_dist_squared(&object->position, &player->playerPos)); + + if (distance > 0 && object->visible) + priority += 0.5f / distance; + + return priority; +} + +float ai_target_priority_player(player_data *player, player_data *other) +{ + float priority = 0.0f; + + if (!other->isAlive) + return 0.0f; + + if (player->scale.x > 0.4f) + priority += 0.8f; + + if (other->scale.x < player->scale.x) + { + priority += 0.8f; + } + else if (other->scale.x > player->scale.x) + { + priority -= 0.3f; + } + + priority *= (1.0f + 0.1f * core_get_aidifficulty()); + + return priority; +} + +void player_do_damage(player_data *player) +{ + if (!player->isAlive) + { + // Prevent edge cases + return; + } + + for (size_t i = 0; i < MAXPLAYERS; i++) + { + player_data *other_player = &players[i]; + if (other_player == player || !other_player->isAlive || other_player->scale.x > player->scale.x) + continue; + + if (check_collision(&other_player->playerPos, 0, &player->playerPos, HITBOX_RADIUS * player->scale.x)) + { + if (other_player->score < player->score) + { + other_player->isAlive = false; + player->score += 5.0f; + } + } + } +} + +bool player_has_control(game_data *game, player_data *player) +{ + return player->isAlive && game->countDownTimer < 0.0f; +} + +void player_fixedloop(game_data *game, player_data *player, object_type *objects, float deltaTime, joypad_port_t port, bool is_human) +{ + float speed = 0.0f; + T3DVec3 newDir = {0}; + int deadzone = 3; + + if (player_has_control(game, player)) + { + if (is_human) + { + joypad_inputs_t joypad = joypad_get_inputs(port); + float moveX = 0; + float moveY = 0; + + /** D Pad inputs + * Why 4.0f? Since the control range is ~[-79,79], + * the max absolute input value being used, `fabsf(joypad.stick_ * 0.05f)`, + * would return ~4.0f + */ + if (joypad.btn.d_up) + moveY -= 4.0f; + if (joypad.btn.d_down) + moveY += 4.0f; + if (joypad.btn.d_left) + moveX -= 4.0f; + if (joypad.btn.d_right) + moveX += 4.0f; + + // Control Stick inputs + if (fabsf(joypad.stick_x) >= deadzone || fabsf(joypad.stick_y) >= deadzone) + { + moveX += (float)joypad.stick_x * 0.05f; + moveY -= (float)joypad.stick_y * 0.05f; + } + + newDir.v[0] = moveX; + newDir.v[2] = moveY; + speed = sqrtf(t3d_vec3_len2(&newDir)); + } + else + { + + player_data *targetPlayer = &player[player->ai_targetPlayer]; + object_data *targetObject = &objects->objects[player->ai_targetObject]; + + float playerPriority = ai_target_priority_player(player, targetPlayer); + float objectPriority = ai_target_priority_object(player, targetObject); + if (objectPriority > playerPriority) + { + + if (targetObject->visible) + { + float dist, norm; + newDir.v[0] = (targetObject->position.v[0] - player->playerPos.v[0]); + newDir.v[2] = (targetObject->position.v[2] - player->playerPos.v[2]); + dist = sqrtf(newDir.v[0] * newDir.v[0] + newDir.v[2] * newDir.v[2]); + if (dist == 0) + dist = 1; + norm = 1 / dist; + newDir.v[0] *= norm + (0.1f * core_get_aidifficulty()); + newDir.v[2] *= norm + (0.1f * core_get_aidifficulty()); + speed = 20; + } + else + { + player->ai_targetObject++; + } + } + else if (objectPriority < playerPriority) + { + if (targetPlayer->isAlive && player->plynum != targetPlayer->plynum) + { + float dist, norm; + newDir.v[0] = (targetPlayer->playerPos.v[0] - player->playerPos.v[0]); + newDir.v[2] = (targetPlayer->playerPos.v[2] - player->playerPos.v[2]); + dist = sqrtf(newDir.v[0] * newDir.v[0] + newDir.v[2] * newDir.v[2]); + if (dist == 0) + dist = 1; + norm = 1 / dist; + newDir.v[0] *= norm + (0.05f * core_get_aidifficulty()); + newDir.v[2] *= norm + (0.05f * core_get_aidifficulty()); + speed = 15; + } + else + { + player->ai_targetPlayer = 0; // (Attempt) to aquire a new target this frame + } + } + else + { + player->ai_targetObject = rand() % NUM_OBJECTS; + player->ai_targetPlayer = rand() % MAXPLAYERS; + } + } + } + + // Player movement + bool boost = game->playerCount > 1 ? true : false; + if (speed > 0.15f) + { + newDir.v[0] /= speed; + newDir.v[2] /= speed; + player->moveDir = newDir; + + float newAngle = atan2f(player->moveDir.v[0], player->moveDir.v[2]); + player->rotY = t3d_lerp_angle(player->rotY, newAngle, 0.5f); + if (boost) + { + player->currSpeed = t3d_lerp(player->currSpeed, speed * 0.2f, 0.2f); + } + else + { + player->currSpeed = t3d_lerp(player->currSpeed, speed * 0.1f, 0.2f); + } + } + else + { + player->currSpeed *= 0.6f; + } + + // ...and limit position inside the box + const float BOX_SIZE = 140.0f; + if (player->playerPos.v[0] < -BOX_SIZE) + player->playerPos.v[0] = -BOX_SIZE; + if (player->playerPos.v[0] > BOX_SIZE) + player->playerPos.v[0] = BOX_SIZE; + if (player->playerPos.v[2] < -BOX_SIZE) + player->playerPos.v[2] = -BOX_SIZE; + if (player->playerPos.v[2] > BOX_SIZE) + player->playerPos.v[2] = BOX_SIZE; + + player_do_damage(player); + + // Scaling based on score + if (player->score >= 2 && player->score < 6) + { + if (player->scale.v[0] < 0.3f) + player->scale.v[0] = t3d_lerp(player->scale.v[0], 0.25f, deltaTime); + if (player->scale.v[2] < 0.3f) + player->scale.v[2] = t3d_lerp(player->scale.v[2], 0.25f, deltaTime); + } + else if (player->score >= 6 && player->score < 10) + { + if (player->scale.v[0] < 0.6f) + player->scale.v[0] = t3d_lerp(player->scale.v[0], 0.5f, deltaTime); + if (player->scale.v[2] < 0.6f) + player->scale.v[2] = t3d_lerp(player->scale.v[2], 0.5f, deltaTime); + } + else if (player->score >= 10) + { + if (player->scale.v[0] < 0.9f) + player->scale.v[0] = t3d_lerp(player->scale.v[0], 0.8f, deltaTime); + if (player->scale.v[2] < 0.9f) + player->scale.v[2] = t3d_lerp(player->scale.v[2], 0.8f, deltaTime); + } +} + +void player_loop(game_data *game, player_data *player, float deltaTime, joypad_port_t port, bool is_human) +{ + if (is_human) + { + player->btn.pressed = joypad_get_buttons_pressed(port); + player->btn.held = joypad_get_buttons_held(port); + player->btn.released = joypad_get_buttons_released(port); + } + + // move player... + player->playerPos.v[0] += player->moveDir.v[0] * player->currSpeed; + player->playerPos.v[2] += player->moveDir.v[2] * player->currSpeed; + + // Update player matrix + if (player->isAlive) + { + t3d_mat4fp_from_srt_euler(player->modelMatFP, + player->scale.v, + (float[3]){0.0f, -player->rotY, 0}, + player->playerPos.v); + } + else + { + player->currSpeed = 0; + player->moveDir = (T3DVec3){{0}}; + } + + if (game->syncPoint) + rspq_syncpoint_wait(game->syncPoint); // wait for the RSP to process the previous frame +} + +void player_draw(player_data *player) +{ + if (player->isAlive) + rspq_block_run(player->dplHole); +} + +void player_cleanup(player_data *player) +{ + rspq_block_free(player->dplHole); + free_uncached(player->modelMatFP); +} + +#endif // PLAYER_H \ No newline at end of file diff --git a/code/sb_holes/render.h b/code/sb_holes/render.h new file mode 100644 index 00000000..e85d9cb7 --- /dev/null +++ b/code/sb_holes/render.h @@ -0,0 +1,157 @@ +#ifndef RENDER_H +#define RENDER_H + +void render_scene(game_data *game, scene_data *scene); + +void render_scene(game_data *game, scene_data *scene) +{ + // Attach display and color clear + rdpq_attach(display_get(), depthBuffer); + rdpq_clear(RGBA32(0, 0, 0, 255)); + + // For each human player + for (size_t i = 0; i < game->playerCount; i++) + { + + // If not alive, do nothing, only for 4 player though + if (!players[i].isAlive && game->playerCount == 4) + continue; + + // Reset render mode and attach player's viewport + t3d_frame_start(); + t3d_viewport_attach(&viewport[i]); + + // Set lights + t3d_light_set_ambient(scene->colorAmbient); + t3d_light_set_directional(0, scene->colorDir, &scene->lightDirVec); + t3d_light_set_count(1); + + /** Render Layout + * - Map + * - Players + * - Objects + * - UI + */ + + t3d_matrix_push_pos(1); + + // Disable Z buffer compare for first layer + rdpq_mode_zbuf(false, true); + rspq_block_run(map.block); + + // reset Z buffer testing for second layer + rdpq_sync_tile(); + rdpq_sync_pipe(); + rdpq_mode_zbuf(false, false); + for (size_t p = 0; p < MAXPLAYERS; p++) + { + player_draw(&players[p]); + } + + // Disable Z buffer write for last layer + rdpq_sync_tile(); + rdpq_sync_pipe(); + rdpq_mode_zbuf(true, false); + for (int j = 0; j < NUM_OBJ_TYPES; j++) + { + object_cull(&objects[j], &viewport[i], game->playerCount); + } + + object_drawBatch(&objects[OBJ_HYDRANT]); + for (size_t o = 0; o < NUM_OBJECTS; o++) + { + if (spray[o] && !objects[OBJ_HYDRANT].objects[o].hide) + hydrant_water_spray(objects[OBJ_HYDRANT].objects[o].position, &viewport[i]); + } + + // Reset render mode for 3D + t3d_frame_start(); + + object_drawBatch(&objects[OBJ_BUILDING]); + object_drawBatch(&objects[OBJ_CAR]); + + t3d_matrix_pop(1); + } + + game->syncPoint = rspq_syncpoint_new(); + + // ======== Draw (2D) ======== // + rdpq_sync_tile(); + rdpq_sync_pipe(); // Hardware crashes otherwise + + viewport_drawScissor(); + + if (game->scene == INTRO) + { + + players[0].btn.pressed = joypad_get_buttons_pressed(PLAYER_1); + players[0].btn.held = joypad_get_buttons_held(PLAYER_1); + players[0].btn.released = joypad_get_buttons_released(PLAYER_1); + ui_intro(&players[0].btn); + if (players[0].btn.pressed.start) + { + if (game->introTimer <= 0) + { + game->scene = GAMEPLAY; + } + else + { + game->introTimer = -1; + } + } + } + else if (game->scene == GAMEPLAY) + { + bool fps = false; + if (players[0].btn.held.r) + fps = true; + + ui_print(game, fps); + + if (game->playerCount == 4) + { + for (size_t out = 0; out < MAXPLAYERS; out++) + { + if (!players[out].isAlive) + ui_playerOut(&players[out]); + } + } + if (players[0].btn.pressed.start && game->countDownTimer < COUNTDOWN_DELAY) + game->scene = PAUSE; + } + else if (game->scene == PAUSE) + { + players[0].btn.pressed = joypad_get_buttons_pressed(PLAYER_1); + players[0].btn.held = joypad_get_buttons_held(PLAYER_1); + players[0].btn.released = joypad_get_buttons_released(PLAYER_1); + ui_pause(&players[0].btn); + if (players[0].btn.pressed.start) + game->scene = GAMEPLAY; + + // Hold Z to quit on the Pause screen + static int resetTimer = 0; + if (game->scene == PAUSE && players[0].btn.held.z) + { + resetTimer++; + if (resetTimer == 5) + { + sound_wavPlay(SFX_STOP, false); + } + else if (resetTimer >= 30) + { + game->isEnding = true; + minigame_end(); + } + } + if (game->scene == PAUSE && players[0].btn.released.z) + resetTimer = 0; + } + else // ENDING + { + ui_print(game, false); + } + + rdpq_detach_show(); +} + +#endif // RENDER_H \ No newline at end of file diff --git a/code/sb_holes/sb_holes.mk b/code/sb_holes/sb_holes.mk new file mode 100644 index 00000000..ed1eb40d --- /dev/null +++ b/code/sb_holes/sb_holes.mk @@ -0,0 +1,21 @@ + +ASSETS_LIST += \ + filesystem/sb_holes/map.t3dm \ + filesystem/sb_holes/building.t3dm \ + filesystem/sb_holes/car.t3dm \ + filesystem/sb_holes/hole.t3dm \ + filesystem/sb_holes/hydrant.t3dm \ + filesystem/sb_holes/hole.ia8.sprite \ + filesystem/sb_holes/hydrant.ci8.sprite \ + filesystem/sb_holes/street.ci4.sprite \ + filesystem/sb_holes/window.ci4.sprite \ + filesystem/sb_holes/window1.ci4.sprite \ + filesystem/sb_holes/car.i4.sprite \ + filesystem/sb_holes/car1.i4.sprite \ + filesystem/sb_holes/car.wav64 \ + filesystem/sb_holes/hydrant.wav64 \ + filesystem/sb_holes/TheMorningAfter.xm64 \ + + +filesystem/sb_holes/car.wav64: AUDIOCONV_FLAGS_FLAGS += --wav-compress 3 --wav-mono +filesystem/sb_holes/hydrant.wav64: AUDIOCONV_FLAGS_FLAGS += --wav-compress 3 --wav-mono diff --git a/code/sb_holes/scene.h b/code/sb_holes/scene.h new file mode 100644 index 00000000..4a9eb1af --- /dev/null +++ b/code/sb_holes/scene.h @@ -0,0 +1,240 @@ +#ifndef SCENE_H +#define SCENE_H + +extern scene_data scenes[NUM_SCENES]; + +void intro_fixedLoop(game_data *game, float deltaTime); +void intro_loop(game_data *game, float deltaTime); +void gameplay_fixedLoop(game_data *game, float deltaTime); +void gameplay_loop(game_data *game, float deltaTime); +void pause_fixedLoop(game_data *game, float deltaTime); +void pause_loop(game_data *game, float deltaTime); +void ending_fixedLoop(game_data *game, float deltaTime); +void ending_loop(game_data *game, float deltaTime); + +void intro_fixedLoop(game_data *game, float deltaTime) +{ + for (size_t i = 0; i < MAXPLAYERS; i++) + { + for (int j = 0; j < NUM_OBJ_TYPES; j++) + { + player_fixedloop(game, &players[i], &objects[j], deltaTime, core_get_playercontroller(i), i < game->playerCount); + } + } + sound_setChannels(); +} +void intro_loop(game_data *game, float deltaTime) +{ + + viewport_set(viewport, game->playerCount, cam); + + for (size_t i = 0; i < game->playerCount; i++) + { + + for (int j = 0; j < NUM_OBJ_TYPES; j++) + { + for (size_t p = 0; p < MAXPLAYERS; p++) + { + object_updateBatch(&objects[j], &viewport[i], &players[p]); + } + } + + cam[i].position = (T3DVec3){{players[i].playerPos.x, players[i].playerPos.y + 250.0f, players[i].playerPos.z + 100.0f}}; + cam[i].target = players[i].playerPos; + } + + for (size_t p = 0; p < MAXPLAYERS; p++) + { + player_loop(game, &players[p], deltaTime, core_get_playercontroller(p), p < game->playerCount); + } + + render_scene(game, &scenes[game->scene]); +} + +void gameplay_fixedLoop(game_data *game, float deltaTime) +{ + + ////////// PLAYERS + bool controlbefore = player_has_control(game, &players[0]); + for (size_t i = 0; i < MAXPLAYERS; i++) + { + for (int j = 0; j < NUM_OBJ_TYPES; j++) + { + player_fixedloop(game, &players[i], &objects[j], deltaTime, core_get_playercontroller(i), i < game->playerCount); + } + } + ////////// + + sound_setChannels(); + if (game->introTimer > 0) + { + game->introTimer -= deltaTime; + } + if (game->scene == GAMEPLAY && game->countDownTimer > -GO_DELAY) + { + float prevCountDown = game->countDownTimer; + game->countDownTimer -= deltaTime; + if ((int)prevCountDown != (int)game->countDownTimer && game->countDownTimer >= 0) + sound_wavPlay(SFX_COUNTDOWN, false); + } + if (!controlbefore && player_has_control(game, &players[0])) + sound_wavPlay(SFX_START, false); + + if (!game->isEnding) + { + // Determine if a player has won + uint32_t alivePlayers = 0; + PlyNum lastPlayer = 0; + for (size_t i = 0; i < MAXPLAYERS; i++) + { + if (players[i].isAlive) + { + alivePlayers++; + lastPlayer = i; + } + if (players[i].score >= 40) + { + alivePlayers = 1; + lastPlayer = i; + } + } + + if (alivePlayers == 1) + { + game->isEnding = true; + game->winner = lastPlayer; + if (game->playerCount != 4) + sound_xmStop(); + sound_wavPlay(SFX_STOP, false); + game->scene = ENDING; + } + } + else + { + float prevEndTime = game->endTimer; + game->endTimer += deltaTime; + if ((int)prevEndTime != (int)game->endTimer && (int)game->endTimer == WIN_SHOW_DELAY) + sound_wavPlay(SFX_WINNER, false); + if (game->endTimer > WIN_DELAY) + { + core_set_winner(game->winner); + minigame_end(); + } + } +} + +void gameplay_loop(game_data *game, float deltaTime) +{ + + viewport_set(viewport, game->playerCount, cam); + + if (game->scene == GAMEPLAY) + { + + for (size_t i = 0; i < game->playerCount; i++) + { + + for (int j = 0; j < NUM_OBJ_TYPES; j++) + { + for (size_t p = 0; p < MAXPLAYERS; p++) + { + object_updateBatch(&objects[j], &viewport[i], &players[p]); + } + } + + cam[i].position = (T3DVec3){{players[i].playerPos.x, players[i].playerPos.y + 250.0f, players[i].playerPos.z + 100.0f}}; + cam[i].target = players[i].playerPos; + } + + for (size_t p = 0; p < MAXPLAYERS; p++) + { + player_loop(game, &players[p], deltaTime, core_get_playercontroller(p), p < game->playerCount); + } + } + + render_scene(game, &scenes[game->scene]); +} + +void pause_fixedLoop(game_data *game, float deltaTime) +{ + sound_setChannels(); +} +void pause_loop(game_data *game, float deltaTime) +{ + render_scene(game, &scenes[game->scene]); +} + +void scene_init(game_data *game, scene_data *scene) +{ + const uint32_t colors[4] = { + 0xAA0000FF, + 0x00AA00FF, + 0x0000AAFF, + 0xAAAA00FF, + }; + for (size_t s = 0; s < NUM_SCENES; s++) + { + scene[s].ID = s; + + switch (s) + { + case INTRO: + uint8_t tempAmb1[4] = {54, 40, 47, 0xFF}; + uint8_t tempDir1[4] = {0, 0, 0, 0xFF}; + for (int i = 0; i < 4; i++) + { + scene[s].colorAmbient[i] = tempAmb1[i]; + scene[s].colorDir[i] = tempDir1[i]; + } + scene[s].lightDirVec = (T3DVec3){{0.0f, 1.0f, 0.0f}}; + scene[s].loop = intro_loop; + scene[s].fixedLoop = intro_fixedLoop; + break; + case GAMEPLAY: + uint8_t tempAmb2[4] = {54, 40, 47, 0xFF}; + uint8_t tempDir2[4] = {0xFF, 0xAA, 0xAA, 0xFF}; + for (int i = 0; i < 4; i++) + { + scene[s].colorAmbient[i] = tempAmb2[i]; + scene[s].colorDir[i] = tempDir2[i]; + } + scene[s].lightDirVec = (T3DVec3){{0.0f, 1.0f, 1.0f}}; + scene[s].loop = gameplay_loop; + scene[s].fixedLoop = gameplay_fixedLoop; + break; + case PAUSE: + uint8_t tempAmb3[4] = {54, 40, 47, 0xFF}; + uint8_t tempDir3[4] = {0xAA, 0, 0, 0xFF}; + for (int i = 0; i < 4; i++) + { + scene[s].colorAmbient[i] = tempAmb3[i]; + scene[s].colorDir[i] = tempDir3[i]; + } + scene[s].lightDirVec = (T3DVec3){{0.0f, -1.0f, 0.0f}}; + scene[s].loop = pause_loop; + scene[s].fixedLoop = pause_fixedLoop; + break; + case ENDING: + uint8_t tempAmb4[4] = {54, 40, 47, 0xFF}; + uint32_t winnerColor = colors[game->winner]; + uint8_t tempDir4[4] = { + (winnerColor >> 24) & 0xFF, + (winnerColor >> 16) & 0xFF, + (winnerColor >> 8) & 0xFF, + winnerColor & 0xFF}; + for (int i = 0; i < 4; i++) + { + scene[s].colorAmbient[i] = tempAmb4[i]; + scene[s].colorDir[i] = tempDir4[i]; + } + scene[s].lightDirVec = (T3DVec3){{1.0f, 1.0f, 0.0f}}; + scene[s].loop = gameplay_loop; + scene[s].fixedLoop = gameplay_fixedLoop; + break; + } + + t3d_vec3_norm(&scene[s].lightDirVec); + } +} + +#endif // SCENE_H \ No newline at end of file diff --git a/code/sb_holes/screen.h b/code/sb_holes/screen.h new file mode 100644 index 00000000..126190ba --- /dev/null +++ b/code/sb_holes/screen.h @@ -0,0 +1,104 @@ +#ifndef SCREEN_H +#define SCREEN_H + +extern T3DViewport viewport[MAXPLAYERS]; + +void viewport_create(T3DViewport *vp); +void viewport_set(T3DViewport *vp, int playercount, camera_data *cam); +void viewport_drawScissor(void); + +// Creates up to 4 viewports depending on playercount +void viewport_create(T3DViewport *vp) +{ + switch (core_get_playercount()) + { + case 1: + vp[0] = t3d_viewport_create(); + t3d_viewport_set_area(&vp[0], 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + break; + case 2: + vp[0] = t3d_viewport_create(); + vp[1] = t3d_viewport_create(); + t3d_viewport_set_area(&vp[0], 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2); + t3d_viewport_set_area(&vp[1], 0, SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT / 2); + break; + case 3: + vp[0] = t3d_viewport_create(); + vp[1] = t3d_viewport_create(); + vp[2] = t3d_viewport_create(); + t3d_viewport_set_area(&vp[0], 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2); + t3d_viewport_set_area(&vp[1], 0, SCREEN_HEIGHT / 2, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 2); + t3d_viewport_set_area(&vp[2], SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT / 2 - 2); + break; + case 4: + vp[0] = t3d_viewport_create(); + vp[1] = t3d_viewport_create(); + vp[2] = t3d_viewport_create(); + vp[3] = t3d_viewport_create(); + t3d_viewport_set_area(&vp[0], 0, 0, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2); + t3d_viewport_set_area(&vp[1], SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT / 2); + t3d_viewport_set_area(&vp[2], 0, SCREEN_HEIGHT / 2, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 2); + t3d_viewport_set_area(&vp[3], SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT / 2 - 2); + break; + } +} + +// Sets projection matrix and look at for each viewport +void viewport_set(T3DViewport *vp, int playercount, camera_data *cam) +{ + for (size_t i = 0; i < playercount; i++) + { + switch (playercount) + { + case 1: + case 4: + t3d_viewport_set_projection(&vp[i], T3D_DEG_TO_RAD(30.0f), 5.0f, 300.0f); + break; + case 2: + t3d_viewport_set_projection(&vp[i], T3D_DEG_TO_RAD(20.0f), 5.0f, 300.0f); + break; + case 3: + if (i > 0) + { + t3d_viewport_set_projection(&vp[i], T3D_DEG_TO_RAD(30.0f), 5.0f, 300.0f); + } + else + { + t3d_viewport_set_projection(&vp[i], T3D_DEG_TO_RAD(20.0f), 5.0f, 300.0f); + } + break; + } + + t3d_viewport_look_at(&vp[i], &cam[i].position, &cam[i].target, &(T3DVec3){{0, 1, 0}}); + } +} + +// Draws the lines for splitscreen +void viewport_drawScissor(void) +{ + int sizeX = SCREEN_WIDTH; + int sizeY = SCREEN_HEIGHT; + rdpq_set_scissor(0, 0, sizeX, sizeY); + rdpq_set_mode_standard(); + rdpq_set_mode_fill(RGBA32(0, 0, 0, 255)); + + // draw thick lines between the screens + switch (core_get_playercount()) + { + case 1: + break; + case 2: + rdpq_fill_rectangle(0, sizeY / 2 - 1, sizeX, sizeY / 2 + 1); + break; + case 3: + rdpq_fill_rectangle(0, sizeY / 2 - 1, sizeX, sizeY / 2 + 1); + rdpq_fill_rectangle(sizeX / 2 - 1, sizeY / 2, sizeX / 2 + 1, sizeY); + break; + case 4: + rdpq_fill_rectangle(0, sizeY / 2 - 1, sizeX, sizeY / 2 + 1); + rdpq_fill_rectangle(sizeX / 2 - 1, 0, sizeX / 2 + 1, sizeY); + break; + } +} + +#endif // SCREEN_H \ No newline at end of file diff --git a/code/sb_holes/sound.h b/code/sb_holes/sound.h new file mode 100644 index 00000000..7f092107 --- /dev/null +++ b/code/sb_holes/sound.h @@ -0,0 +1,242 @@ +#ifndef SOUND_H +#define SOUND_H + +#include + +// Core Definitions +#define MUSIC_CHANNEL 0 +#define SFX_CHANNEL 31 + +// XM sequences +enum BG_XM +{ + XM_TMA, + NUM_XM +}; + +xm64player_t xmPlayer; + +const char *xmFileNames[NUM_XM] = { + "rom:/sb_holes/TheMorningAfter.xm64", // https://github.com/DragonMinded/libdragon/blob/trunk/examples/audioplayer/assets/TheMorningAfter.xm +}; + +// WAV files +enum SFX_WAV +{ + SFX_BUILDING, + SFX_CAR, + SFX_HYDRANT, + SFX_START, + SFX_COUNTDOWN, + SFX_STOP, + SFX_WINNER, + NUM_WAV +}; + +// Each WAV must have its own structure +wav64_t soundEffects[NUM_WAV]; + +const char *wavFileNames[NUM_WAV] = { + "rom:/strawberry_byte/sound/stones-falling.wav64", + "rom:/sb_holes/car.wav64", + "rom:/sb_holes/hydrant.wav64", + "rom:/core/Start.wav64", + "rom:/core/Countdown.wav64", + "rom:/core/Stop.wav64", + "rom:/core/Winner.wav64", +}; + +/* Function Declarations */ +void sound_load(bool playXM); +void sound_xmSwitch(int songID, float volume, bool loop); +void sound_xmStop(void); +void sound_xmUpdate(float volume, bool loop); +void sound_wavPlay(int sfxID, bool loop); +void sound_wavClose(int sfxID); +void sound_wavCleanup(void); +void sound_cleanup(void); +void sound_update(void); +float sound_reverb(float volume, float mix); +void sound_setChannels(void); + +/* Function Definitions */ + +void sound_load(bool playXM) +{ + // Open all WAVs at boot + for (int w = 0; w < NUM_WAV; ++w) + wav64_open(&soundEffects[w], wavFileNames[w]); + + // Open and play first XM in the list + if (playXM) + { + xm64player_open(&xmPlayer, xmFileNames[0]); + xm64player_set_vol(&xmPlayer, 0.5f); + xm64player_play(&xmPlayer, MUSIC_CHANNEL); + } +} + +// Stops current XM, opens and plays requested module with set volume and whether to loop +void sound_xmSwitch(int songID, float volume, bool loop) +{ + xm64player_stop(&xmPlayer); + xm64player_close(&xmPlayer); + xm64player_open(&xmPlayer, xmFileNames[songID]); + xm64player_set_loop(&xmPlayer, loop); + xm64player_set_vol(&xmPlayer, volume); + xm64player_play(&xmPlayer, MUSIC_CHANNEL); +} + +// Stops and closes XM player +void sound_xmStop(void) +{ + xm64player_stop(&xmPlayer); +} + +// Adjusts volume and looping of current XM module +void sound_xmUpdate(float volume, bool loop) +{ + xm64player_set_loop(&xmPlayer, loop); + xm64player_set_vol(&xmPlayer, volume); +} + +// Plays requested WAV and whether to loop +void sound_wavPlay(int sfxID, bool loop) +{ + wav64_set_loop(&soundEffects[sfxID], loop); + wav64_play(&soundEffects[sfxID], SFX_CHANNEL - sfxID); +} + +void sound_wavClose(int sfxID) +{ + wav64_close(&soundEffects[sfxID]); +} + +void sound_wavCleanup(void) +{ + for (int w = 0; w < NUM_WAV; ++w) + wav64_close(&soundEffects[w]); +} + +void sound_cleanup(void) +{ + if (core_get_playercount() != 4) + xm64player_close(&xmPlayer); + sound_wavCleanup(); +} + +void sound_update(void) +{ + mixer_try_play(); +} + +////// Audio filters + +#define REVERB_BUFFER_SIZE 32000 // Size for the delay buffer +#define MAX_COMB_FILTERS 3 +#define MAX_ALLPASS_FILTERS 2 + +typedef struct +{ + float comb_delays[MAX_COMB_FILTERS]; + float comb_feedback; + float allpass_delays[MAX_ALLPASS_FILTERS]; + float allpass_feedback; + float sample_rate; +} ReverbParams; + +// Schroeder Reverberator Parameters +ReverbParams paramsSchroeder = { + {2.0f, 3.0f, 4.0f}, // Comb filter delays in frames + 0.5f, + {1.0f, 2.0f}, // All-pass filter delays in frames + 0.4f, + 16000.0f, +}; + +// Circular buffers for the comb and all-pass filters +float comb_delay_buffer[REVERB_BUFFER_SIZE]; +int comb_buffer_index = 0; + +float allpass_delay_buffer[REVERB_BUFFER_SIZE]; +int allpass_buffer_index = 0; + +// Comb Filter implementation +float comb_filter(float input, float delay_seconds, float feedback, float sample_rate) +{ + int delay_samples = (int)(delay_seconds * sample_rate); + int buffer_index = (comb_buffer_index + REVERB_BUFFER_SIZE - delay_samples) % REVERB_BUFFER_SIZE; + + float delayed_sample = comb_delay_buffer[buffer_index]; + float output = delayed_sample + input; + + // Store the output with feedback in the buffer + comb_delay_buffer[comb_buffer_index] = input + delayed_sample * feedback; + comb_buffer_index = (comb_buffer_index + 1) % REVERB_BUFFER_SIZE; + + return output; +} + +// All-Pass Filter implementation +float allpass_filter(float input, float delay_seconds, float feedback, float sample_rate) +{ + int delay_samples = (int)(delay_seconds * sample_rate); + int buffer_index = (allpass_buffer_index + REVERB_BUFFER_SIZE - delay_samples) % REVERB_BUFFER_SIZE; + + float delayed_sample = allpass_delay_buffer[buffer_index]; + float output = delayed_sample - (input * feedback) + input; + + // Store the new input into the delay buffer + allpass_delay_buffer[allpass_buffer_index] = input + delayed_sample * feedback; + allpass_buffer_index = (allpass_buffer_index + 1) % REVERB_BUFFER_SIZE; + + return output; +} + +// Applies reverb based on current volume and set mix +float sound_reverb(float volume, float mix) +{ + + if (volume < 0.2f) + volume = 0.2f; // Clamp volume to minimum + + // Apply comb filters + float reverb_volume = 0.0f; + for (int i = 0; i < MAX_COMB_FILTERS; i++) + reverb_volume += comb_filter(volume, paramsSchroeder.comb_delays[i], paramsSchroeder.comb_feedback, paramsSchroeder.sample_rate); + + // Apply all-pass filters + for (int i = 0; i < MAX_ALLPASS_FILTERS; i++) + reverb_volume = allpass_filter(reverb_volume, paramsSchroeder.allpass_delays[i], paramsSchroeder.allpass_feedback, paramsSchroeder.sample_rate); + + // Mix original sound with reverb + return volume * (1.0f - mix) + reverb_volume * mix; +} + +// Sets predefined values for each SFX mixer channel +void sound_setChannels(void) +{ + for (int i = 0; i < NUM_WAV; i++) + { + switch (i) + { + case SFX_WINNER: + mixer_ch_set_vol_pan(SFX_CHANNEL - i, 0.4f, 0.5f); + break; + case SFX_BUILDING: + case SFX_CAR: + mixer_ch_set_vol_pan(SFX_CHANNEL - i, sound_reverb(0.9f, 0.6f), 0.5f); + break; + case SFX_HYDRANT: + mixer_ch_set_vol_pan(SFX_CHANNEL - i, sound_reverb(0.9f, 0.2f), 0.5f); + break; + case SFX_START: + case SFX_COUNTDOWN: + case SFX_STOP: + mixer_ch_set_vol_pan(SFX_CHANNEL - i, sound_reverb(0.4f, 0.9f), 0.5f); + break; + } + } +} + +#endif // SOUND_H \ No newline at end of file diff --git a/code/sb_holes/ui.h b/code/sb_holes/ui.h new file mode 100644 index 00000000..4b313ba0 --- /dev/null +++ b/code/sb_holes/ui.h @@ -0,0 +1,290 @@ +#ifndef UI_H +#define UI_H + +#include "ui/ui_font.h" +#include "ui/ui_colors.h" +#include "ui/ui_sprite.h" + +enum cStates +{ + C_UP, + C_DOWN, + C_LEFT, + C_RIGHT +}; + +enum MENU_TEXT +{ + TEXT_DIFF, + TEXT_PLAYERS = +4, + TEXT_BOTS, + TEXT_CONTROLS, + TEXT_RUMBLE, + MENU_TEXT_COUNT +}; + +const char *uiMainMenuStrings[MENU_TEXT_COUNT] = { + "Difficulty: ", + "EASY", + "MEDIUM", + "HARD", + "Players: ", + "Bots: ", + " to Move", + "Insert Rumble Pak now!"}; + +float textPositions[2]; +float panelPositions[4]; + +void ui_init(void); +void ui_print(game_data *game, float fps); +void ui_cleanup(void); + +// Loads, registers and sets a font with style +void ui_init(void) +{ + ui_fontRegister(); + ui_spriteLoad(); + textPositions[0] = SCREEN_WIDTH / 2; // x + textPositions[1] = SCREEN_HEIGHT / 2; // y + panelPositions[0] = SCREEN_WIDTH / 2; // x0 + panelPositions[1] = SCREEN_HEIGHT / 2; // y0 + panelPositions[2] = SCREEN_WIDTH; // x1 + panelPositions[3] = SCREEN_HEIGHT; // y1 +} + +// Optional RDPQ sync and set for text, to prevent bleeding if the autosync engine misses something. +void ui_syncText(void) +{ + rdpq_sync_pipe(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_sync_tile(); +} + +void ui_fps(float frame_rate, float x, float y) +{ + heap_stats_t heap_stats; + sys_get_heap_stats(&heap_stats); + ui_syncText(); + rdpq_text_printf(&txt_debugParms, ID_DEBUG, x, y, "FPS %.2f Mem: %d KiB", frame_rate, heap_stats.used / 1024); +} + +void ui_printf(float x, float y, const char *txt, ...) +{ + ui_syncText(); + + va_list args; + va_start(args, txt); + rdpq_text_vprintf(&txt_debugParms, ID_DEBUG, x, y, txt, args); +} + +void ui_playerScores(player_data *player) +{ + static float base[] = {32, 32}; + float position[] = {base[0], base[1]}; + ui_syncText(); + rdpq_textparms_t playerTextParms = txt_gameParms; + playerTextParms.style_id = STYLE_PLAYER + player->plynum; + playerTextParms.align = ALIGN_LEFT; + rdpq_text_printf(&playerTextParms, ID_DEFAULT, position[0] + 75 * player->plynum, position[1], "P%d : %u", player->plynum + 1, player->score); +} + +void ui_playerOut(player_data *player) +{ + ui_syncText(); + rdpq_textparms_t playerTxtParms = txt_gameParms; + playerTxtParms.style_id = STYLE_PLAYER + player->plynum; + float xOffset = 80; + float yOffset = 60; + float x, y = 0; + + if (player->plynum == 0) + { + x = -xOffset; + y = textPositions[1] - yOffset; + } + else if (player->plynum == 1) + { + x = xOffset; + y = textPositions[1] - yOffset; + } + else if (player->plynum == 2) + { + x = -xOffset; + y = textPositions[1] + yOffset; + } + else // player->plynum == 3 + { + x = xOffset; + y = textPositions[1] + yOffset; + } + + rdpq_text_printf(&playerTxtParms, ID_DEFAULT, x, y, "Player %d Out", player->plynum + 1); +} + +void ui_print_winner(int winner) +{ + ui_spriteDrawPanel(TILE1, sprite_gloss, T_BLACK, panelPositions[0] - 64, panelPositions[1] - 18, panelPositions[0] + 60, panelPositions[1] + 10, 0, 0, 64, 64); + ui_syncText(); + if (winner != 5) // 5 signifies a Draw + { + rdpq_textparms_t winnerTextParms = txt_gameParms; + winnerTextParms.style_id = STYLE_PLAYER + winner; + winnerTextParms.align = ALIGN_CENTER; + rdpq_text_printf(&winnerTextParms, ID_DEFAULT, 0, textPositions[1], "Player %d Wins", winner + 1); + } + else + { + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 0, textPositions[1], "TIE!"); + } +} + +// Catch-all print function for Countdown, Score and Winner, with optional FPS print +void ui_print(game_data *game, float fps) +{ + if (game->countDownTimer > 0.0f) + { + rdpq_text_printf(&txt_titleParms, ID_TITLE, 0, 126, "%d", (int)ceilf(game->countDownTimer)); + } + else if (game->countDownTimer > -GO_DELAY) + { + rdpq_text_print(&txt_titleParms, ID_TITLE, 0, 126, "GO!"); + } + else if (game->isEnding && game->endTimer >= WIN_SHOW_DELAY) + { + ui_print_winner(game->winner); + } + else + { + for (size_t scores = 0; scores < MAXPLAYERS; scores++) + if (players[scores].isAlive) + ui_playerScores(&players[scores]); + } + + if (fps) + ui_fps(display_get_fps(), 20, 20); +} + +void ui_pause(control_data *control) +{ + + ui_spriteDrawPanel(TILE2, sprite_gloss, T_BLUE, 40, 60, 280, 144, 0, 0, 64, 64); + ui_spriteDrawPanel(TILE3, sprite_tessalate, T_BLACK, 50, 65, 270, 134, 0, 0, 64, 64); + + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 1, 142, 110); + } + else + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 0, 142, 110); + } + + ui_spriteDraw(TILE5, sprite_dPadTriggers, 5, 159, 180); + + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, 0, 84, "holes\nA Clone of Hole.io"); + txt_gameParms.align = ALIGN_LEFT; + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 98, 122, "Press to Return"); + txt_gameParms.align = ALIGN_CENTER; + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 0, 164, "PAUSED\n\nHold to\nQuit Game"); +} + +void ui_intro(control_data *control) +{ + // Basic frame counter for timing + static uint32_t introTimer = 0; + introTimer++; + + // Animated text positions + static float topTextPosition[] = {93.0f, 0.0f}; + topTextPosition[1] = topTextPosition[1] + 2.0f; + if (topTextPosition[1] > 56.0f) + topTextPosition[1] = 56.0f; + + if (introTimer < 100) + { + + /* STRAWBERRY SCREEN */ + + // Panels + ui_spriteDrawPanel(TILE1, sprite_strawberryTop, WHITE, 128, 80, 196, 112, 0, 0, 32, 16); + if (introTimer >= 50) + { + ui_spriteDrawPanel(TILE2, sprite_strawberry1, WHITE, 128, 112, 196, 144, 0, 0, 32, 16); + } + else + { + ui_spriteDrawPanel(TILE2, sprite_strawberry0, WHITE, 128, 112, 196, 144, 0, 0, 32, 16); + } + + // Buttons + if (control->pressed.start || control->held.start) + { + introTimer = 101; + ui_spriteDraw(TILE3, sprite_faceButtons0, 1, 130, 214); + } + else + { + ui_spriteDraw(TILE3, sprite_faceButtons0, 0, 130, 214); + } + + // Text + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, 0, 56, "Strawberry Byte"); + if (introTimer >= 50) + rdpq_text_print(&txt_titleParms, ID_TITLE, 0, 190, "Presents"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 0, 226, "Press to Skip Intro"); + } + else + { + + // Buttons + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE3, sprite_faceButtons0, 1, 152, 66); + } + else + { + ui_spriteDraw(TILE3, sprite_faceButtons0, 0, 152, 66); + } + + joypad_inputs_t joypad = joypad_get_inputs(PLAYER_1); + + ui_spriteDraw(TILE5, sprite_controlStick, 0, 134, 86); + int stickX = 134 + (joypad.stick_x / 15); + int stickY = 54 + (spriteHeight * 2) - (joypad.stick_y / 15); + ui_spriteDraw(TILE5, sprite_controlStick, 1, stickX, stickY); + + ui_spriteDraw(TILE6, sprite_dPadTriggers, 0, 168, 86); + + // Text + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, 0, 40, "holes\nA Clone of Hole.io"); + txt_gameParms.align = ALIGN_LEFT; + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 108, 78, "Press to Play"); + txt_gameParms.align = ALIGN_CENTER; + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 0, 100, "or\nto Move"); + rdpq_text_print(&txt_titleParms, ID_DEFAULT, 0, 136, "CREDITS:"); + + // @TODO: Probably should make this a rdpq_paragraph + rdpq_text_print(&txt_gameParms, ID_DEBUG, 0, 148, + "Programming: s4ys\n" + "Models: s4ys\n" + "Strawberry Sprite by Sonika Rud\n" + "UI Sprites by Kenney\n" + "Music: 'The Morning After' by Soft One\n" + "SFX obtained from Freesound"); + } +} + +void ui_cleanup(void) +{ + ui_spriteCleanup(); + ui_fontUnregister(); + ui_fileCleanup(); +} + +#endif // UI_H \ No newline at end of file diff --git a/code/sb_holes/ui/ui_colors.h b/code/sb_holes/ui/ui_colors.h new file mode 100644 index 00000000..619c87a8 --- /dev/null +++ b/code/sb_holes/ui/ui_colors.h @@ -0,0 +1,102 @@ +#ifndef UI_COLORS_H +#define UI_COLORS_H + +#ifdef __cplusplus +extern "C" +{ +#endif + + enum COLOR_NAMES + { + // Standard ROYGBIV + RED, + ORANGE, + YELLOW, + GREEN, + BLUE, + INDIGO, + VIOLET, + // RGB 0 (full black) & 1 (full white) + BLACK, + WHITE, + // RGB 1 * (n*.25f) + LIGHT_GREY, // n = 3 + GREY, // n = 2 + DARK_GREY, // n = 1 + // Transparent Colors + TRANSPARENT, + T_RED, + T_ORANGE, + T_YELLOW, + T_GREEN, + T_BLUE, + T_INDIGO, + T_VIOLET, + T_BLACK, + T_WHITE, + T_GREY, + // Darker Variants + DARK_RED, + DARK_GREEN, + // N64 Logo Colors + N_RED, + N_YELLOW, + N_GREEN, + N_BLUE, + COLOR_COUNT + }; + + // FMT_RGBA32, 32-bit packed RGBA (8888) + const uint32_t COLORS[COLOR_COUNT] = + { + 0xD90000FF, // RED + 0xFF6822FF, // ORANGE + 0xFFDA21FF, // YELLOW + 0x33DD00FF, // GREEN + 0x1133CCFF, // BLUE + 0x220066FF, // INDIGO + 0x330044FF, // VIOLET + 0x000000FF, // BLACK + 0xFFFFFFFF, // WHITE + 0xC0C0C0FF, // LIGHT_GREY + 0x808080FF, // GREY + 0x404040FF, // DARK_GREY + 0x0000007F, // TRANSPARENT + 0xD90000C8, // T_RED + 0xFF6822C8, // T_ORANGE + 0xFFDA21C8, // T_YELLOW + 0x33DD00C8, // T_GREEN + 0x1133CCC8, // T_BLUE + 0x220066C8, // T_INDIGO + 0x330044C8, // T_VIOLET + 0x1F1F1FC8, // T_BLACK + 0xFFFFFFC8, // T_WHITE + 0xC0C0C0C8, // T_GREY + 0x820000FF, // DARK_RED + 0x006400FF, // DARK_GREEN + 0xE10916FF, // N_RED + 0xF5B201FF, // N_YELLOW + 0x319900FF, // N_GREEN + 0x01009AFF, // N_BLUE + }; + + inline color_t ui_color(int colorIdx); + uint32_t ui_colorSetAlpha(uint32_t color, uint8_t alpha); + + // Creates a color_t from one of the 32-bit packed COLORS. + inline color_t ui_color(int colorIdx) + { + return color_from_packed32(COLORS[colorIdx]); + } + + // Clears the alpha bits and sets them to the new value + uint32_t ui_colorSetAlpha(uint32_t color, uint8_t alpha) + { + return (color & 0xFFFFFF00) | (alpha & 0xFF); + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_COLORS_H \ No newline at end of file diff --git a/code/sb_holes/ui/ui_file.h b/code/sb_holes/ui/ui_file.h new file mode 100644 index 00000000..7f460909 --- /dev/null +++ b/code/sb_holes/ui/ui_file.h @@ -0,0 +1,139 @@ +#ifndef UI_FILE_H +#define UI_FILE_H + +#define NUM_FONTS 2 +#define NUM_SPRITES 6 + +#ifdef __cplusplus +extern "C" +{ +#endif + + // Base directory for UI assets. + const char *basePath = "rom:/strawberry_byte/ui/"; + + // Arrays for font file names and paths: + // - uiFontFileName: Array of pointers to store full paths to font files after initialization. + // - uiFontPaths: Constant array of relative paths for each font, appended to basePath at runtime. + const char *uiFontFileName[NUM_FONTS]; + const char *uiFontPaths[NUM_FONTS] = { + "fonts/TitanOne-Regular.font64", + "fonts/OilOnTheWater-ee5O.font64"}; + + // Arrays for button sprite file names and paths. + const char *uiSpriteButtonFileName[NUM_SPRITES]; + const char *uiSpriteButtonPath[NUM_SPRITES] = { + "buttons/control_stick.ia8.sprite", + "buttons/d_pad_triggers.ia8.sprite", + "buttons/c_buttons0.rgba32.sprite", + "buttons/c_buttons1.rgba32.sprite", + "buttons/face_buttons0.rgba32.sprite", + "buttons/face_buttons1.rgba32.sprite"}; + + // Arrays for button sprite file names and paths. + const char *uiSpritePanelFileName[3]; + const char *uiSpritePanelPath[3] = { + "panels/gloss.ia4.sprite", + "panels/pattern_tessalate.ia4.sprite", + "panels/clouds.ia8.sprite", + }; + + // Arrays for logo sprite file names and paths. See LICENSE.txt for attribution. + const char *uiSpriteLogoFileName[NUM_SPRITES]; + const char *uiSpriteLogoPath[NUM_SPRITES] = { + "logos/sb_b0.rgba32.sprite", + "logos/sb_b1.rgba32.sprite", + "logos/sb_top.rgba32.sprite", + "logos/t3d.ia8.sprite", + "logos/libdragon.ia4.sprite", + "logos/mixamo.ia4.sprite"}; + + /* Declarations */ + + char *ui_filePath(const char *fn); + void ui_fileFonts(void); + void ui_fileSprites(void); + void ui_fileLogos(void); + void ui_fileGet(void); + void ui_fileCleanup(void); + + /* Definitions */ + + // Concatenates basePath and fn, returning the full path (caller must free memory). + char *ui_filePath(const char *fn) + { + char *fullPath = (char *)malloc(256 * sizeof(char)); + if (!fullPath) + { + return NULL; + } + + sprintf(fullPath, "%s%s", basePath, fn); + + return fullPath; + } + + // Populates uiFontFileName with full paths for fonts. + void ui_fileFonts(void) + { + for (int i = 0; i < NUM_FONTS; ++i) + { + uiFontFileName[i] = ui_filePath(uiFontPaths[i]); + } + } + + // Populates uiSpriteButtonFileName and uiSpritePanelFileName with full paths for sprites. + void ui_fileSprites(void) + { + for (int i = 0; i < NUM_SPRITES; ++i) + { + uiSpriteButtonFileName[i] = ui_filePath(uiSpriteButtonPath[i]); + } + for (int i = 0; i < 3; ++i) + { + uiSpritePanelFileName[i] = ui_filePath(uiSpritePanelPath[i]); + } + } + + void ui_fileLogos(void) + { + for (int i = 0; i < NUM_SPRITES; ++i) + { + uiSpriteLogoFileName[i] = ui_filePath(uiSpriteLogoPath[i]); + } + } + + // Calls functions to initialize font and sprite file paths. + void ui_fileGet(void) + { + ui_fileFonts(); + ui_fileSprites(); + ui_fileLogos(); + } + + // Frees memory allocated for font and sprite file paths. + void ui_fileCleanup(void) + { + for (int i = 0; i < NUM_FONTS; i++) + { + free((char *)uiFontFileName[i]); + ; + } + + for (int i = 0; i < 3; i++) + { + free((char *)uiSpritePanelFileName[i]); + } + + for (int i = 0; i < NUM_SPRITES; i++) + { + free((char *)uiSpriteButtonFileName[i]); + free((char *)uiSpriteLogoFileName[i]); + } + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_FILE_H \ No newline at end of file diff --git a/code/sb_holes/ui/ui_font.h b/code/sb_holes/ui/ui_font.h new file mode 100644 index 00000000..629c06f5 --- /dev/null +++ b/code/sb_holes/ui/ui_font.h @@ -0,0 +1,154 @@ +#ifndef UI_FONT_H +#define UI_FONT_H + +#include "ui_colors.h" +#include "ui_file.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + enum FONT_IDS + { + ID_RESERVED, + ID_DEBUG, + ID_DEFAULT, + ID_TITLE, + ID_COUNT + }; + + // Array of pointers to rdpq_font_t, with each entry representing a font identified by an ID. + rdpq_font_t *font[ID_COUNT]; + + enum FONT_STYLES + { + STYLE_DEBUG, + STYLE_DEFAULT, + STYLE_TITLE, + STYLE_BRIGHT, + STYLE_GREEN, + STYLE_PLAYER, + STYLE_COUNT = STYLE_PLAYER + MAXPLAYERS + }; + + // RDPQ text parameters, used here primarily to set the following RDPQ font styles. + rdpq_textparms_t txt_debugParms; + rdpq_textparms_t txt_titleParms; + rdpq_textparms_t txt_gameParms; + + // RDPQ font styles, used here primarily to set text color. + rdpq_fontstyle_t txt_debug_fontStyle; + rdpq_fontstyle_t txt_player_fontStyle; + rdpq_fontstyle_t txt_title_fontStyle; + rdpq_fontstyle_t txt_game_fontStyle; + rdpq_fontstyle_t txt_bright_fontStyle; + rdpq_fontstyle_t txt_green_fontStyle; + + /* Declarations */ + + void ui_fontRegister(void); + void ui_fontUnregister(void); + + /* Definitions */ + + /* All in one font initialization. + - Loads and regsiters fonts. + - Assigns colors to font styles. + - Register font styles for each font. + - Assigns font styles to text parameters. + + Possible improvements would be to separate functionality, + to make fonts more flexible and modular. */ + void ui_fontRegister(void) + { + ui_fileFonts(); + + font[ID_DEBUG] = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_VAR); + font[ID_DEFAULT] = rdpq_font_load(uiFontFileName[0]); + font[ID_TITLE] = rdpq_font_load(uiFontFileName[1]); + + // Create and register font styles + txt_debug_fontStyle.color = ui_color(YELLOW); + txt_debug_fontStyle.outline_color = ui_color(BLACK); + + const color_t playerColors[MAXPLAYERS] = { + PLAYERCOLOR_1, + PLAYERCOLOR_2, + PLAYERCOLOR_3, + PLAYERCOLOR_4, + }; + + txt_player_fontStyle.outline_color = ui_color(BLACK); + + txt_game_fontStyle.color = ui_color(WHITE); + txt_game_fontStyle.outline_color = ui_color(BLACK); + + txt_title_fontStyle.color = ui_color(WHITE); + txt_title_fontStyle.outline_color = ui_color(N_BLUE); + + txt_bright_fontStyle.color = ui_color(YELLOW); + txt_bright_fontStyle.outline_color = ui_color(BLACK); + + txt_green_fontStyle.color = ui_color(GREEN); + txt_green_fontStyle.outline_color = ui_color(DARK_GREEN); + + for (int i = 1; i < ID_COUNT; i++) + { + rdpq_text_register_font(i, font[i]); + + rdpq_font_style( + font[i], + STYLE_DEFAULT, + &txt_game_fontStyle); + + rdpq_font_style( + font[i], + STYLE_TITLE, + &txt_title_fontStyle); + + rdpq_font_style( + font[i], + STYLE_BRIGHT, + &txt_bright_fontStyle); + + rdpq_font_style( + font[i], + STYLE_GREEN, + &txt_green_fontStyle); + + for (int p = 1; p <= MAXPLAYERS; p++) + { + txt_player_fontStyle.color = playerColors[p - 1]; + rdpq_font_style( + font[i], + STYLE_PLAYER + p - 1, + &txt_player_fontStyle); + } + } + + rdpq_font_style( + font[ID_DEBUG], + STYLE_DEBUG, + &txt_debug_fontStyle); + + txt_debugParms = (rdpq_textparms_t){.style_id = STYLE_DEBUG, .disable_aa_fix = true}; + txt_titleParms = (rdpq_textparms_t){.style_id = STYLE_TITLE, .disable_aa_fix = true, .align = ALIGN_CENTER, .width = 320}; + txt_gameParms = (rdpq_textparms_t){.style_id = STYLE_BRIGHT, .disable_aa_fix = true, .align = ALIGN_CENTER, .width = 320}; + } + + // Unregisters and frees fonts for the next minigame. + void ui_fontUnregister(void) + { + for (int i = ID_DEBUG; i < ID_COUNT; ++i) + { + rdpq_text_unregister_font(i); + rdpq_font_free(font[i]); + } + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_FONT_H \ No newline at end of file diff --git a/code/sb_holes/ui/ui_sprite.h b/code/sb_holes/ui/ui_sprite.h new file mode 100644 index 00000000..5b971bb2 --- /dev/null +++ b/code/sb_holes/ui/ui_sprite.h @@ -0,0 +1,169 @@ +#ifndef UI_SPRITE_H +#define UI_SPRITE_H + +#include "ui_file.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + // Constant dimensions for sprites + const int spriteWidth = 16, spriteHeight = 16; + + // Static pointers to sprites representing different controller elements + static sprite_t *sprite_controlStick; + static sprite_t *sprite_dPadTriggers; + static sprite_t *sprite_cButtons0; + static sprite_t *sprite_cButtons1; + static sprite_t *sprite_faceButtons0; + static sprite_t *sprite_faceButtons1; + + // Static pointers to sprites for panels + static sprite_t *sprite_gloss; + static sprite_t *sprite_tessalate; + static sprite_t *sprite_clouds; + + // Static pointers to sprites for logos + static sprite_t *sprite_libdragon; + static sprite_t *sprite_mixamo; + static sprite_t *sprite_t3d; + static sprite_t *sprite_strawberryTop; + static sprite_t *sprite_strawberry0; + static sprite_t *sprite_strawberry1; + + // Surfaces for rendering UI elements + surface_t surf_UIpanels; + surface_t surf_UIsprites; + + /* Declarations */ + + void ui_spriteLoad(void); + void ui_syncSprite(int color); + void ui_spriteDraw(rdpq_tile_t tile, sprite_t *sprite, int idx, int x, int y); + void ui_spriteDrawPanel(rdpq_tile_t tile, sprite_t *sprite, int color, int x0, int y0, int x1, int y1, int s, int t, int s1, int t1); + void ui_spriteDrawDynamic(rdpq_tile_t tile, sprite_t *sprite, color_t color, int x0, int y0, int x1, int y1, int s, int t, int s1, int t1); + void ui_spriteCleanup(void); + + /* Definitions */ + + // Loads and assigns sprites to their corresponding pointers based on file paths set by ui_fileSprites. + void ui_spriteLoad(void) + { + ui_fileSprites(); + ui_fileLogos(); + + // Load IA format sprites (grayscale with alpha for UI overlays). + sprite_gloss = sprite_load(uiSpritePanelFileName[0]); + sprite_tessalate = sprite_load(uiSpritePanelFileName[1]); + sprite_clouds = sprite_load(uiSpritePanelFileName[2]); + + sprite_controlStick = sprite_load(uiSpriteButtonFileName[0]); + sprite_dPadTriggers = sprite_load(uiSpriteButtonFileName[1]); + sprite_libdragon = sprite_load(uiSpriteLogoFileName[4]); + sprite_mixamo = sprite_load(uiSpriteLogoFileName[5]); + sprite_t3d = sprite_load(uiSpriteLogoFileName[3]); + + // Load RGBA32 format sprites (full color with transparency for UI buttons). + sprite_cButtons0 = sprite_load(uiSpriteButtonFileName[2]); + sprite_cButtons1 = sprite_load(uiSpriteButtonFileName[3]); + sprite_faceButtons0 = sprite_load(uiSpriteButtonFileName[4]); + sprite_faceButtons1 = sprite_load(uiSpriteButtonFileName[5]); + sprite_strawberry0 = sprite_load(uiSpriteLogoFileName[0]); + sprite_strawberry1 = sprite_load(uiSpriteLogoFileName[1]); + sprite_strawberryTop = sprite_load(uiSpriteLogoFileName[2]); + } + + // Optional RDPQ sync and set for sprites. Similar to ui_syncText, but sets the combiner for textures and allows for primitive color to added. + void ui_syncSprite(int color) + { + rdpq_sync_pipe(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_TEX_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_set_prim_color(ui_color(color)); + rdpq_sync_tile(); + } + + // Draws a simple 16x16 sprite. + void ui_spriteDraw(rdpq_tile_t tile, sprite_t *sprite, int idx, int x, int y) + { + int s = 0, t = 0; + int idxCopy = idx; + + if (idx > 4) + { + idx = idx % 4; + s = spriteWidth * idx; + } + else + { + s = spriteWidth * idx; + } + + t = (idxCopy / 4) * spriteHeight; + + ui_syncSprite(WHITE); + + surf_UIsprites = sprite_get_pixels(sprite); + + rdpq_tex_upload_sub(tile, &surf_UIsprites, NULL, s, t, s + spriteWidth, t + spriteHeight); + rdpq_texture_rectangle(tile, x, y, x + spriteWidth, y + spriteHeight, s, t); + } + + // Draws a scalable sprite with predefined primitive color by index. + void ui_spriteDrawPanel(rdpq_tile_t tile, sprite_t *sprite, int colorIdx, int x0, int y0, int x1, int y1, int s, int t, int s1, int t1) + { + + ui_syncSprite(colorIdx); + + surf_UIpanels = sprite_get_pixels(sprite); + + rdpq_tex_upload(tile, &surf_UIpanels, NULL); + rdpq_texture_rectangle_scaled(tile, x0, y0, x1, y1, s, t, s1, t1); + } + + // Draws a scalable sprite with added primitive color. + void ui_spriteDrawDynamic(rdpq_tile_t tile, sprite_t *sprite, color_t color, int x0, int y0, int x1, int y1, int s, int t, int s1, int t1) + { + + rdpq_sync_pipe(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_TEX_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_set_prim_color(color); + rdpq_sync_tile(); + + surf_UIpanels = sprite_get_pixels(sprite); + + rdpq_tex_upload(tile, &surf_UIpanels, NULL); + rdpq_texture_rectangle_scaled(tile, x0, y0, x1, y1, s, t, s1, t1); + } + + // Frees static pointers to sprites. + void ui_spriteCleanup(void) + { + sprite_free(sprite_controlStick); + sprite_free(sprite_dPadTriggers); + sprite_free(sprite_cButtons0); + sprite_free(sprite_cButtons1); + sprite_free(sprite_faceButtons0); + sprite_free(sprite_faceButtons1); + sprite_free(sprite_gloss); + sprite_free(sprite_clouds); + sprite_free(sprite_tessalate); + sprite_free(sprite_libdragon); + sprite_free(sprite_mixamo); + sprite_free(sprite_t3d); + sprite_free(sprite_strawberryTop); + sprite_free(sprite_strawberry0); + sprite_free(sprite_strawberry1); + surface_free(&surf_UIpanels); + surface_free(&surf_UIsprites); + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_SPRITE_H \ No newline at end of file diff --git a/code/sb_holes/util.h b/code/sb_holes/util.h new file mode 100644 index 00000000..f0778130 --- /dev/null +++ b/code/sb_holes/util.h @@ -0,0 +1,164 @@ +#ifndef UTIL_H +#define UTIL_H + +// Global defines +// #define DEBUG_RDP + +#define HITBOX_RADIUS 40.f + +#define INTRO_DELAY 5.0f +#define COUNTDOWN_DELAY 3.0f +#define GO_DELAY 1.0f +#define WIN_DELAY 5.0f +#define WIN_SHOW_DELAY 2.0f + +#define NUM_OBJECTS 9 + +#define SCREEN_WIDTH display_get_width() +#define SCREEN_HEIGHT display_get_height() + +// STRUCTS +typedef struct +{ + T3DMat4FP *mtxFP; + rspq_block_t *block; + T3DModel *model; +} map_data; + +typedef struct +{ + T3DVec3 position; + T3DVec3 target; + +} camera_data; + +enum OBJ_TYPES +{ + OBJ_CAR, + OBJ_BUILDING, + OBJ_HYDRANT, + NUM_OBJ_TYPES +}; + +typedef struct +{ + uint8_t ID; + uint8_t texID; + T3DObject *model; + T3DMat4FP *mtxFP; + T3DVec3 position; + T3DVec3 scale; + float yaw; + bool visible; + bool hide; + color_t color; + rspq_block_t *modelBlock; +} object_data; + +typedef struct +{ + uint8_t type; + uint8_t scoreValue; + T3DModel *model; + float collisionRadius; + object_data objects[NUM_OBJECTS]; +} object_type; + +typedef struct +{ + joypad_buttons_t pressed; + joypad_buttons_t held; + joypad_buttons_t released; +} control_data; + +typedef struct +{ + PlyNum plynum; + T3DMat4FP *modelMatFP; + rspq_block_t *dplHole; + T3DVec3 moveDir; + T3DVec3 playerPos; + T3DVec3 scale; + float rotY; + float currSpeed; + bool isAlive; + PlyNum ai_targetPlayer; + PlyNum ai_targetObject; + int ai_reactionspeed; + uint8_t score; + control_data btn; +} player_data; + +typedef struct +{ + size_t playerCount; + float introTimer; + float countDownTimer; + bool isEnding; + float endTimer; + PlyNum winner; + uint8_t scene; + rspq_syncpoint_t syncPoint; +} game_data; + +typedef void (*SceneLoop)(game_data *, float); +typedef struct +{ + uint8_t ID; + uint8_t colorAmbient[4]; + uint8_t colorDir[4]; + T3DVec3 lightDirVec; + SceneLoop loop; + SceneLoop fixedLoop; +} scene_data; + +enum SCENE_ID +{ + INTRO, + GAMEPLAY, + PAUSE, + ENDING, + NUM_SCENES +}; + +// Global variables +extern camera_data cam[MAXPLAYERS]; + +// COLLISION + +float vec2_dist_squared(T3DVec3 *pos0, T3DVec3 *pos1); +bool check_collision(T3DVec3 *pos0, float radii0, T3DVec3 *pos1, float radii1); + +// Calculate squared distance between two positions' X and Z coordinates +float vec2_dist_squared(T3DVec3 *pos0, T3DVec3 *pos1) +{ + float dx = pos1->v[0] - pos0->v[0]; + float dy = pos1->v[2] - pos0->v[2]; + return dx * dx + dy * dy; +} + +// Return whether point is within a radius (radii0 == 0), or if 2 radii overlap (radii0 != 0) +bool check_collision(T3DVec3 *pos0, float radii0, T3DVec3 *pos1, float radii1) +{ + + float distSq = vec2_dist_squared(pos0, pos1); + + assert(radii0 >= 0.0f && radii1 >= 0.0f); + + if (radii0 != 0) + { + // Assume checking if radii overlap + float radiiSum = radii0 + radii1; + float radiiSumSq = radiiSum * radiiSum; + + return distSq <= radiiSumSq; + } + else + { + + // Assume checking if pos0 is within pos1's radius + return distSq <= radii1 * radii1; + } +} + +#endif // UTIL_H \ No newline at end of file diff --git a/code/sb_hot/.gitignore b/code/sb_hot/.gitignore new file mode 100644 index 00000000..28c69d0b --- /dev/null +++ b/code/sb_hot/.gitignore @@ -0,0 +1,5 @@ + +build/ +filesystem/ +.vscode/ +game.z64 diff --git a/code/sb_hot/actor/actor.h b/code/sb_hot/actor/actor.h new file mode 100644 index 00000000..d6931e32 --- /dev/null +++ b/code/sb_hot/actor/actor.h @@ -0,0 +1,212 @@ +#ifndef ACTOR_H +#define ACTOR_H + +// structures + +typedef struct +{ + + float idle_acceleration_rate; + float walk_acceleration_rate; + float run_acceleration_rate; + float roll_acceleration_rate; + float roll_acceleration_grip_rate; + float jump_acceleration_rate; + float aerial_control_rate; + + float walk_target_speed; + float run_target_speed; + float sprint_target_speed; + float idle_to_roll_target_speed; + float idle_to_roll_grip_target_speed; + float walk_to_roll_target_speed; + float run_to_roll_target_speed; + float sprint_to_roll_target_speed; + float jump_target_speed; + + float jump_timer_max; + + float fall_max_speed; + float jump_max_speed; + float jump_horizontal_boost; + +} ActorSettings; + +typedef struct +{ + + float stick_magnitude; + float stick_x; + float stick_y; + float jump_time_held; + float jump_time_buffer; + bool jump_hold; + bool jump_released; + +} Actorinput; + +typedef struct +{ + + T3DSkeleton main; + T3DSkeleton blend; + +} ActorArmature; + +typedef struct +{ + + T3DAnim breathing_idle; + T3DAnim running_left; + T3DAnim jump_left; + T3DAnim falling_left; + T3DAnim land_left; + +} AnimationSet; + +typedef struct +{ + + uint8_t previous; + uint8_t current; + + AnimationSet main; + AnimationSet blend; + + uint8_t change_delay; + float blending_ratio; + float speed_rate; + bool synced; + +} ActorAnimation; + +typedef struct +{ + + uint32_t id; + rspq_block_t *dl; + T3DMat4FP *modelMat; + T3DModel *model; + Vector3 scale; + + char model_path; + ActorArmature armature; + ActorAnimation animation; + + RigidBody body; + + float target_yaw; + float horizontal_target_speed; + Vector3 target_velocity; + + float horizontal_speed; + bool grounded; + float grounding_height; + + bool hasCollided; // Testing a collision boolean + + uint8_t locomotion_state; + uint8_t previous_state; + uint8_t state; + + Vector3 home; + + ActorSettings settings; + Actorinput input; + +} Actor; + +// function prototypes + +Actor actor_create(uint32_t id, const char *model_path); + +void actor_draw(Actor *actor); +void actor_delete(Actor *actor); + +// function implemenations + +Actor actor_create(uint32_t id, const char *model_path) +{ + Actor actor = { + + .id = id, + .model = t3d_model_load(model_path), + .modelMat = malloc_uncached(sizeof(T3DMat4FP)), + + .scale = {1.0f, 1.0f, 1.0f}, + + .state = 1, + .previous_state = 1, + .locomotion_state = 1, + + .body = { + .position = {0.0f, 0.0f, 0.0f}, + .velocity = {0.0f, 0.0f, 0.0f}, + .rotation = {0.0f, 0.0f, 0.0f}, + }, + + .grounding_height = -2000.0f, // magic number + + .settings = {.idle_acceleration_rate = 9, .walk_acceleration_rate = 4, .run_acceleration_rate = 10, .roll_acceleration_rate = 20, .roll_acceleration_grip_rate = 2, .jump_acceleration_rate = 50, .aerial_control_rate = 4.0, .walk_target_speed = 200, .run_target_speed = 700, .sprint_target_speed = 900, .idle_to_roll_target_speed = 300, .idle_to_roll_grip_target_speed = 50, .walk_to_roll_target_speed = 400, .run_to_roll_target_speed = 780, .sprint_to_roll_target_speed = 980, .jump_target_speed = 500, .jump_timer_max = 0.20, .fall_max_speed = -2650.0f, .jump_max_speed = 1000.0f, .jump_horizontal_boost = 100.0f}, + }; + + actor.armature.main = t3d_skeleton_create(actor.model); + // actor.armature.blend = t3d_skeleton_clone(&actor.armature.main, false); + + rspq_block_begin(); + t3d_matrix_set(actor.modelMat, true); + t3d_model_draw_skinned(actor.model, &actor.armature.main); + actor.dl = rspq_block_end(); + + t3d_mat4fp_identity(actor.modelMat); + + return actor; +} + +void actor_updateMat(Actor *actor) +{ + if (actor->state == 9) + return; // DEATH + + t3d_mat4fp_from_srt_euler(actor->modelMat, + (float[3]){actor->scale.x, actor->scale.y, actor->scale.z}, + (float[3]){rad(actor->body.rotation.x), rad(actor->body.rotation.y), rad(actor->body.rotation.z)}, + (float[3]){actor->body.position.x, actor->body.position.y, actor->body.position.z}); +} + +void actor_draw(Actor *actor) +{ + for (uint8_t i = 0; i < ACTOR_COUNT; i++) + { + + if (actor[i].state == 9) + continue; // DEATH + + rspq_block_run(actor[i].dl); + }; +} + +void actor_delete(Actor *actor) +{ + free_uncached(actor->modelMat); + + t3d_skeleton_destroy(&actor->armature.main); + t3d_anim_destroy(&actor->animation.main.breathing_idle); + t3d_anim_destroy(&actor->animation.main.running_left); + t3d_anim_destroy(&actor->animation.main.falling_left); + // t3d_anim_destroy(&actor->animation.main.jump_left); + // t3d_anim_destroy(&actor->animation.main.land_left); + + // t3d_skeleton_destroy(&actor->armature.blend); + // t3d_anim_destroy(&actor->animation.blend.breathing_idle); + // t3d_anim_destroy(&actor->animation.blend.running_left); + // t3d_anim_destroy(&actor->animation.blend.falling_left); + // t3d_anim_destroy(&actor->animation.blend.jump_left); + // t3d_anim_destroy(&actor->animation.blend.land_left); + + t3d_model_free(actor->model); + if (actor->dl != NULL) + rspq_block_free(actor->dl); +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/actor/actor_animation.h b/code/sb_hot/actor/actor_animation.h new file mode 100644 index 00000000..2b6bb634 --- /dev/null +++ b/code/sb_hot/actor/actor_animation.h @@ -0,0 +1,137 @@ +#ifndef ACTOR_ANIMATION_H +#define ACTOR_ANIMATION_H + +// function implemenations + +ActorAnimation actorAnimation_create(const Actor *actor) +{ + ActorAnimation animation; + animation.previous = STAND_IDLE; + animation.current = STAND_IDLE; + animation.blending_ratio = 0.0f; + return animation; +} + +void animationSet_init(const Actor *actor, AnimationSet *set) +{ + set->breathing_idle = t3d_anim_create(actor->model, "breathing-idle"); + set->running_left = t3d_anim_create(actor->model, "running-10-left"); + // set->jump_left = t3d_anim_create(actor->model, "jump-left"); + set->falling_left = t3d_anim_create(actor->model, "falling-idle-left"); + // set->land_left = t3d_anim_create(actor->model, "falling-to-landing-left"); +} + +void actorAnimation_init(const Actor *actor, ActorAnimation *animation) +{ + animationSet_init(actor, &animation->main); + // animationSet_init(actor, &animation->blend); + // attach main + t3d_anim_attach(&animation->main.breathing_idle, &actor->armature.main); + t3d_anim_attach(&animation->main.falling_left, &actor->armature.main); + t3d_anim_attach(&animation->main.running_left, &actor->armature.main); + + // attach blend + // t3d_anim_attach(&animation->blend.running_left, &actor->armature.blend); +} + +void actorAnimation_setStandIdle(Actor *actor, ActorAnimation *animation, const float frame_time, rspq_syncpoint_t *syncpoint) +{ + t3d_anim_update(&animation->main.breathing_idle, frame_time); +} + +void actorAnimation_setRunning(Actor *actor, ActorAnimation *animation, const float frame_time, rspq_syncpoint_t *syncpoint) +{ + // if (animation->previous == STAND_IDLE || animation->current == STAND_IDLE) { + // animation->blending_ratio = actor->horizontal_speed / 320; + // if (animation->blending_ratio > 1.0f) animation->blending_ratio = 1.0f; + // if (animation->current == STAND_IDLE) t3d_anim_set_time(&animation->blend.running_left, 0.0f); + // if(animation->blending_ratio > 0.0f && animation->blending_ratio < 1.0f) + // { + // t3d_anim_update(&animation->main.breathing_idle, frame_time); + // t3d_anim_set_speed(&animation->blend.running_left, animation->blending_ratio); + // t3d_anim_update(&animation->blend.running_left, frame_time); + // + // t3d_skeleton_blend(&actor->armature.main, &actor->armature.main, &actor->armature.blend, animation->blending_ratio); + // } + // } + // else + t3d_anim_update(&animation->main.running_left, frame_time); +} + +void actorAnimation_setJump(Actor *actor, ActorAnimation *animation, const float frame_time, rspq_syncpoint_t *syncpoint) +{ + t3d_anim_update(&animation->main.falling_left, frame_time); +} + +void actor_setAnimation(Actor *actor, ActorAnimation *animation, const float frame_time, rspq_syncpoint_t *syncpoint) +{ + switch (actor->state) + { + + case STAND_IDLE: + { + actorAnimation_setStandIdle(actor, animation, frame_time, syncpoint); + if (animation->current != STAND_IDLE) + { + animation->previous = animation->current; + animation->current = STAND_IDLE; + } + break; + } + + case RUNNING: + { + actorAnimation_setRunning(actor, animation, frame_time, syncpoint); + if (animation->current != RUNNING) + { + animation->previous = animation->current; + animation->current = RUNNING; + } + break; + } + + case JUMP: + { + actorAnimation_setJump(actor, animation, frame_time, syncpoint); + break; + } + case FALLING: + { + actorAnimation_setJump(actor, animation, frame_time, syncpoint); + break; + } + case DEATH: + { + actorAnimation_setJump(actor, animation, frame_time, syncpoint); + break; + } + } + + /* + * The original engine only accounts for 1 actor, + * but looping through the actor update for each + * means that the syncpoint would be updated + * 4 times, which is 3 too many. + */ + // if(syncpoint)rspq_syncpoint_wait(*syncpoint); + // t3d_skeleton_update(&actor->armature.main); +} + +// temporary place for this until i solve the circular dependency +void actor_init(Actor *actor) +{ + actor->animation = actorAnimation_create(actor); + actorAnimation_init(actor, &actor->animation); +} + +void actor_update(Actor *actor, ControllerData *control, TimeData *timing, float camera_angle_around, float camera_offset, rspq_syncpoint_t *syncpoint) +{ + if (control != NULL) + actor_setControlData(actor, control, timing->frame_time_s, camera_angle_around, camera_offset); + if (actor->previous_state != actor->state) + actor_setState(actor, actor->state); // Skip setting state if it hasn't changed + actor_setAnimation(actor, &actor->animation, timing->frame_time_s, syncpoint); + actor_setMotion(actor, timing->fixed_time_s); +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/actor/actor_control.h b/code/sb_hot/actor/actor_control.h new file mode 100644 index 00000000..dc2205b5 --- /dev/null +++ b/code/sb_hot/actor/actor_control.h @@ -0,0 +1,122 @@ +#ifndef ACTOR_CONTROLS_H +#define ACTOR_CONTROLS_H + +// function prototypes + +void actorControl_setJump(Actor *actor, ControllerData *control, float frame_time); +void actorControl_moveWithStick(Actor *actor, ControllerData *control, float camera_angle_around, float camera_offset); +void actor_setControlData(Actor *actor, ControllerData *control, float frame_time, float camera_angle_around, float camera_offset); + +// function implementations + +void actorControl_setJump(Actor *actor, ControllerData *control, float frame_time) +{ + bool canJump = false; + bool wantJump = false; + + switch (actor->state) + { + case FALLING: + if (control->held.a) + actor->input.jump_time_buffer += frame_time; + actor->input.jump_released = true; + actor->input.jump_hold = false; + canJump = false; + break; + case JUMP: + if (control->held.a) + actor->input.jump_time_held += frame_time; + actor->input.jump_time_buffer = 0; + canJump = false; + wantJump = false; + break; + case STAND_IDLE: + case RUNNING: + actor->input.jump_released = true; + actor->input.jump_hold = false; + canJump = true; + break; + } + + if (control->pressed.a) + wantJump = true; + if (actor->input.jump_time_buffer > 0.0f && actor->input.jump_time_buffer < 0.3f) + wantJump = true; + + if (wantJump && canJump) + { + + actor->body.velocity.z = actor->settings.jump_horizontal_boost; + actor->input.jump_hold = true; + actor->input.jump_released = false; + sound_wavPlay(SFX_JUMP, false); + actor_setState(actor, JUMP); + } + else + { + actor->input.jump_released = true; + actor->input.jump_hold = false; + } + + if (control->released.a) + actor->input.jump_time_buffer = 0; +} + +void actorControl_moveWithStick(Actor *actor, ControllerData *control, float camera_angle_around, float camera_offset) +{ + int deadzone = 2; + float stick_magnitude = 0; + + // Store previous camera angle and offset + static float prev_camera_angle = -1.0f; // Initialize with a value that will trigger the first calculation + static float prev_camera_offset = -1.0f; + + static float yaw = 0; + + // Check if the camera angle or offset has changed + bool camera_changed = fabsf(camera_angle_around - prev_camera_angle) > 0.001f || fabsf(camera_offset - prev_camera_offset) > 0.001f; + + do + { + // Update the previous camera angle and offset values + prev_camera_angle = camera_angle_around; + prev_camera_offset = camera_offset; + + // Only change yaw if the camera angle or offset has changed + if (fabsf(control->input.stick_x) >= deadzone || fabsf(control->input.stick_y) >= deadzone) + { + yaw = deg(fm_atan2f(control->input.stick_x, -control->input.stick_y) - rad(camera_angle_around - (0.5 * camera_offset))); + } + break; + + } while (camera_changed); + + actor->target_yaw = yaw; + + if (fabsf(control->input.stick_x) >= deadzone || fabsf(control->input.stick_y) >= deadzone) + { + Vector2 stick = {control->input.stick_x, control->input.stick_y}; + stick_magnitude = vector2_magnitude(&stick); + actor->horizontal_target_speed = stick_magnitude * 6; + } + + if (stick_magnitude == 0 && actor->state != JUMP && actor->state != FALLING) + { + actor->state = STAND_IDLE; + } + + else if (stick_magnitude > 0 && actor->state != JUMP && actor->state != FALLING) + { + actor->state = RUNNING; + } +} + +void actor_setControlData(Actor *actor, ControllerData *control, float frame_time, float camera_angle_around, float camera_offset) +{ + + actorControl_setJump(actor, control, frame_time); + + actorControl_moveWithStick(actor, control, camera_angle_around, camera_offset); +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/actor/actor_motion.h b/code/sb_hot/actor/actor_motion.h new file mode 100644 index 00000000..b66271e0 --- /dev/null +++ b/code/sb_hot/actor/actor_motion.h @@ -0,0 +1,184 @@ +#ifndef ACTOR_MOVEMENT_H +#define ACTOR_MOVEMENT_H + +#define ACTOR_GRAVITY -1500 + +// function prototypes + +void actorMotion_setHorizontalAcceleration(Actor *actor, float target_speed, float acceleration_rate); + +void actorMotion_setHorizontalInertiaAcceleration(Actor *actor, float target_speed, float acceleration_rate); + +void actorMotion_setStopingAcceleration(Actor *actor); + +void actorMotion_setJumpAcceleration(Actor *actor, float target_speed, float acceleration_rate); + +void actorMotion_integrate(Actor *actor, float frame_time); + +void actorMotion_setHorizontalAcceleration(Actor *actor, float target_speed, float acceleration_rate) +{ + actor->target_velocity.x = target_speed * fm_sinf(rad(actor->target_yaw)); + actor->target_velocity.y = target_speed * -fm_cosf(rad(actor->target_yaw)); + + actor->body.acceleration.x = acceleration_rate * (actor->target_velocity.x - actor->body.velocity.x); + actor->body.acceleration.y = acceleration_rate * (actor->target_velocity.y - actor->body.velocity.y); +} + +void actorMotion_setHorizontalInertiaAcceleration(Actor *actor, float target_speed, float acceleration_rate) +{ + actor->target_velocity.x = target_speed * fm_sinf(rad(actor->body.rotation.z)); + actor->target_velocity.y = target_speed * -fm_cosf(rad(actor->body.rotation.z)); + + actor->body.acceleration.x = acceleration_rate * (actor->target_velocity.x - actor->body.velocity.x); + actor->body.acceleration.y = acceleration_rate * (actor->target_velocity.y - actor->body.velocity.y); +} + +void actorMotion_setStopingAcceleration(Actor *actor) +{ + actor->body.acceleration.x = actor->settings.idle_acceleration_rate * (0 - actor->body.velocity.x); + actor->body.acceleration.y = actor->settings.idle_acceleration_rate * (0 - actor->body.velocity.y); +} + +void actorMotion_setJumpAcceleration(Actor *actor, float target_speed, float acceleration_rate) +{ + actor->body.acceleration.z = acceleration_rate * (target_speed - actor->body.velocity.z); +} + +void actorMotion_integrate(Actor *actor, float frame_time) +{ + + if (actor->body.acceleration.x != 0 || actor->body.acceleration.y != 0 || actor->body.acceleration.z != 0) + { + vector3_addScaledVector(&actor->body.velocity, &actor->body.acceleration, frame_time); + } + + if (fabsf(actor->body.velocity.x) < 10.0f && fabsf(actor->body.velocity.y) < 10.0f) + { + actor->body.velocity.x = 0; + actor->body.velocity.y = 0; + } + + if (actor->body.velocity.x != 0 || actor->body.velocity.y != 0 || actor->body.velocity.z != 0) + { + vector3_addScaledVector(&actor->body.position, &actor->body.velocity, frame_time); + if (actor->body.velocity.z < actor->settings.fall_max_speed) + actor->body.velocity.z = actor->settings.fall_max_speed; + if (actor->body.velocity.z > actor->settings.jump_max_speed) + actor->body.velocity.z = actor->settings.jump_max_speed; + } + + if (actor->body.velocity.x != 0 || actor->body.velocity.y != 0) + { + + actor->body.rotation.z = deg(fm_atan2f(-actor->body.velocity.x, -actor->body.velocity.y)); + + Vector2 horizontal_velocity = {actor->body.velocity.x, actor->body.velocity.y}; + actor->horizontal_speed = vector2_magnitude(&horizontal_velocity); + } +} + +void actorMotion_setIdle(Actor *actor) +{ + actorMotion_setStopingAcceleration(actor); + + if (fabsf(actor->body.velocity.x) < 1.0f && fabsf(actor->body.velocity.y) < 1.0f) + { + + vector3_init(&actor->body.velocity); + actor->horizontal_speed = 0; + actor->target_yaw = actor->body.rotation.z; + } +} + +void actorMotion_setRunning(Actor *actor) +{ + actorMotion_setHorizontalAcceleration(actor, actor->horizontal_target_speed, actor->settings.run_acceleration_rate); + // actorMotion_setHorizontalAcceleration (actor, actor->settings.run_target_speed, actor->settings.run_acceleration_rate); +} + +void actorMotion_setJump(Actor *actor) +{ + if (actor->input.jump_hold && !actor->input.jump_released && actor->input.jump_time_held < actor->settings.jump_timer_max) + { + // Scale horizontal boost based on how long the jump button is held + float hold_factor = (float)actor->input.jump_time_held / actor->settings.jump_timer_max; + float boosted_speed = actor->horizontal_speed + (actor->settings.jump_horizontal_boost * hold_factor); + + // Set horizontal acceleration to the boosted speed + actorMotion_setHorizontalAcceleration(actor, boosted_speed, actor->settings.aerial_control_rate); + + // Maintain vertical acceleration for the jump + actorMotion_setJumpAcceleration(actor, actor->settings.jump_target_speed, actor->settings.jump_acceleration_rate); + } + else if (actor->body.velocity.z > 0) + { + + // Scale horizontal boost based on how long the jump button is held + float hold_factor = (float)actor->input.jump_time_held / actor->settings.jump_timer_max; + float degraded_speed = actor->horizontal_speed - (hold_factor * 0.05f); + ; + + // Maintain aerial control while falling + actorMotion_setHorizontalAcceleration(actor, degraded_speed, actor->settings.aerial_control_rate); + actor->body.acceleration.z = ACTOR_GRAVITY; + } + else + { + // Transition to falling state + actor->state = FALLING; + actor->input.jump_time_held = 0; + return; + } +} + +void actorMotion_setFalling(Actor *actor) +{ + actor->grounded = 0; + actorMotion_setHorizontalAcceleration(actor, actor->horizontal_speed, actor->settings.aerial_control_rate); + actor->body.acceleration.z = ACTOR_GRAVITY; + + if (actor->body.position.z <= actor->grounding_height) + { + + actor->grounded = 1; + actor->body.acceleration.z = 0; + actor->body.velocity.z = 0; + actor->body.position.z = actor->grounding_height; + + actor->state = STAND_IDLE; + + return; + } +} + +void actor_setMotion(Actor *actor, float frame_time) +{ + switch (actor->state) + { + + case STAND_IDLE: + { + actorMotion_setIdle(actor); + break; + } + case RUNNING: + { + actorMotion_setRunning(actor); + break; + } + case JUMP: + { + actorMotion_setJump(actor); + break; + } + case FALLING: + { + actorMotion_setFalling(actor); + break; + } + } + + actorMotion_integrate(actor, frame_time); +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/actor/actor_states.h b/code/sb_hot/actor/actor_states.h new file mode 100644 index 00000000..ad855929 --- /dev/null +++ b/code/sb_hot/actor/actor_states.h @@ -0,0 +1,111 @@ +#ifndef ACTORSTATES_H +#define ACTORSTATES_H + +#define STAND_IDLE 1 +#define RUNNING 3 +#define JUMP 6 +#define FALLING 7 +#define LANDING 8 +#define DEATH 9 + +// function prototypes + +void actorState_setIdle(Actor *actor); + +void actorState_setRunning(Actor *actor); + +void actorState_setJump(Actor *actor); + +void actorState_setFalling(Actor *actor); + +void actorState_setDeath(Actor *actor); + +void actor_setState(Actor *actor, uint8_t state); + +void actorState_setIdle(Actor *actor) +{ + if (actor->state == STAND_IDLE) + return; + if (actor->previous_state != FALLING && actor->state != JUMP) + actor->previous_state = actor->state; + actor->state = STAND_IDLE; + actor->locomotion_state = STAND_IDLE; +} + +void actorState_setRunning(Actor *actor) +{ + if (actor->state == RUNNING) + return; + if (actor->previous_state != FALLING && actor->state != JUMP) + actor->previous_state = actor->state; + actor->state = RUNNING; + actor->locomotion_state = RUNNING; +} + +void actorState_setJump(Actor *actor) +{ + if (actor->state == JUMP) + return; + + if (actor->previous_state != FALLING && actor->state != JUMP) + actor->previous_state = actor->state; + actor->state = JUMP; + actor->grounded = 0; + actor->grounding_height = 0.0f; +} + +void actorState_setFalling(Actor *actor) +{ + if (actor->state == FALLING) + return; + + if (actor->state != FALLING && actor->state != JUMP) + actor->previous_state = actor->state; + actor->state = FALLING; + actor->grounding_height = -0.0f; +} + +void actorState_setDeath(Actor *actor) +{ + if (actor->state == DEATH) + return; + + actor->previous_state = actor->state; + actor->grounding_height = -50.0f; + actor->state = DEATH; +} + +void actor_setState(Actor *actor, uint8_t state) +{ + switch (state) + { + + case STAND_IDLE: + { + actorState_setIdle(actor); + break; + } + case RUNNING: + { + actorState_setRunning(actor); + break; + } + case JUMP: + { + actorState_setJump(actor); + break; + } + case FALLING: + { + actorState_setFalling(actor); + break; + } + case DEATH: + { + actorState_setDeath(actor); + break; + } + } +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/actor/collision/actor_collision_detection.h b/code/sb_hot/actor/collision/actor_collision_detection.h new file mode 100644 index 00000000..23c503db --- /dev/null +++ b/code/sb_hot/actor/collision/actor_collision_detection.h @@ -0,0 +1,163 @@ +#ifndef ACTOR_COLLISION_H +#define ACTOR_COLLISION_H + +// collision types + +/* +#define SPHERE 1 +#define AABB 2 +#define BOX 3 +#define PLANE 4 +#define RAY 5 +#define CAPSULE 6 +#define TERRAIN 7 +#define MESH 8 +*/ + +// structures + +typedef struct +{ + float body_radius; + float body_height; + // float sword_radius; + // float sword_lenght; + // float shield_radius; +} ActorColliderSettings; + +typedef struct +{ + Capsule body; + // Capsule sword; + // Sphere shield; + ActorColliderSettings settings; +} ActorCollider; + +typedef struct +{ + Vector3 axis_closest_to_point; // closest point in the capsule axis to the point of contact + Vector3 velocity_penetration; // penetration vector in the direction of the velocity + float slope; // angle of inclination of the the plane of contact + float angle_of_incidence; // angle between the velocity vector and the plane of contact + float displacement; // distance from the origin to the plane of contact + float ground_distance; // vertical distance from the actor's position to the nearest plane of contact + ContactData data; +} ActorContactData; + +void actorCollider_init(ActorCollider *collider) +{ + collider->body.radius = collider->settings.body_radius; + collider->body.length = collider->settings.body_height; +} + +void actorCollider_setVertical(ActorCollider *collider, Vector3 *position) +{ + capsule_setVertical(&collider->body, position); +} + +void actorCollider_set(ActorCollider *collider, Vector3 *position, Vector3 *rotation) +{ +} + +void actorContactData_clear(ActorContactData *contact) +{ + contact->axis_closest_to_point = (Vector3){0.0f, 0.0f, 0.0f}; + contact->velocity_penetration = (Vector3){0.0f, 0.0f, 0.0f}; + contact->slope = 1000.0f; // Set the slope to an out of range value to indicate no contact + contact->displacement = 0.0f; + contact->ground_distance = 1000.0f; + contactData_init(&contact->data); +} + +void actorContactData_setAxisClosestToPoint(ActorContactData *contact, const ActorCollider *collider) +{ + contact->axis_closest_to_point = segment_closestToPoint(&collider->body.start, &collider->body.end, &contact->data.point); +} + +void actorContactData_setSlope(ActorContactData *contact) +{ + float magnitude = vector3_magnitude(&contact->data.normal); + if (magnitude > 0) + { + float cos_slope = contact->data.normal.z / magnitude; // Calculate the cosine of the angle between the normal and the z-axis + float slope = acosf(cos_slope); + contact->slope = deg(slope); + } +} + +void actorContactData_setAngleOfIncidence(ActorContactData *contact, const Vector3 *velocity) +{ + contact->angle_of_incidence = -deg((M_PI * 0.5f) - acosf(vector3_returnDotProduct(velocity, &contact->data.normal) / vector3_magnitude(velocity))); +} + +void actorContactData_setDisplacement(ActorContactData *contact) +{ + contact->displacement = vector3_returnDotProduct(&contact->data.point, &contact->data.normal); +} + +void actorCollision_setGroundDistance(ActorContactData *contact, Vector3 *position) +{ + if (contact->data.normal.z == 0.0f) + contact->ground_distance = 1000.0; // arbitrary large value to indicate no grounding + else + contact->ground_distance = (contact->displacement - vector3_returnDotProduct(position, &contact->data.normal)) / -contact->data.normal.z; +} + +bool actorCollision_contactSphere(const ActorCollider *collider, const Sphere *sphere) +{ + return capsule_contactSphere(&collider->body, sphere); +} + +void actorCollision_contactSphereSetData(ActorContactData *contact, const ActorCollider *collider, const Sphere *sphere) +{ + capsule_contactSphereSetData(&contact->data, &collider->body, sphere); + actorContactData_setSlope(contact); + actorContactData_setDisplacement(contact); + actorContactData_setAxisClosestToPoint(contact, collider); +} + +bool actorCollision_contactAABB(const ActorCollider *collider, const AABB *aabb) +{ + return capsule_contactAABB(&collider->body, aabb); +} + +void actorCollision_contactAABBsetData(ActorContactData *contact, const ActorCollider *collider, const AABB *aabb) +{ + capsule_contactAABBSetData(&contact->data, &collider->body, aabb); + actorContactData_setSlope(contact); + actorContactData_setDisplacement(contact); + actorContactData_setAxisClosestToPoint(contact, collider); +} + +bool actorCollision_contactBox(const ActorCollider *collider, const Box *box) +{ + return capsule_contactBox(&collider->body, box); +} + +void actorCollision_contactBoxSetData(ActorContactData *contact, const ActorCollider *collider, const Box *box) +{ + capsule_contactBoxSetData(&contact->data, &collider->body, box); + actorContactData_setSlope(contact); + actorContactData_setDisplacement(contact); + actorContactData_setAxisClosestToPoint(contact, collider); +} + +bool actorCollision_contactPlane(const ActorCollider *collider, const Plane *plane) +{ + return capsule_contactPlane(&collider->body, plane); +} + +void actorCollision_contactPlaneSetData(ActorContactData *contact, const ActorCollider *collider, const Plane *plane) +{ + capsule_contactPlaneSetData(&contact->data, &collider->body, plane); + actorContactData_setSlope(contact); + contact->displacement = plane->displacement; + actorContactData_setAxisClosestToPoint(contact, collider); +} + +bool actorCollision_intersectionRay(const ActorCollider *collider, const Ray *ray) +{ + return capsule_intersectionRay(&collider->body, ray); +} + +#endif diff --git a/code/sb_hot/actor/collision/actor_collision_response.h b/code/sb_hot/actor/collision/actor_collision_response.h new file mode 100644 index 00000000..53170a0a --- /dev/null +++ b/code/sb_hot/actor/collision/actor_collision_response.h @@ -0,0 +1,234 @@ +#ifndef ACTOR_COLLISION_RESPONSE_H +#define ACTOR_COLLISION_RESPONSE_H + +void actorCollision_pushTowardsNormal(Actor *actor, ActorContactData *contact) +{ + // Calculate the necessary displacement vector in the direction of the contact normal + Vector3 displacement_vector = vector3_returnScaled(&contact->data.normal, -contact->data.penetration); + + // Apply the displacement to the actor's position + vector3_subtract(&actor->body.position, &displacement_vector); +} + +// lighter solution to use together with the push towards normal function. gives almost same results, for now i will use the correct algorithm +void actorCollision_projectAcceleration(Actor *actor, ActorContactData *contact) +{ + float t = vector3_returnDotProduct(&actor->body.acceleration, &contact->data.normal); + vector3_addScaledVector(&actor->body.acceleration, &contact->data.normal, -t); +} + +void actorCollision_projectVelocity(Actor *actor, ActorContactData *contact) +{ + float t = vector3_returnDotProduct(&actor->body.velocity, &contact->data.normal); + vector3_addScaledVector(&actor->body.velocity, &contact->data.normal, -t); +} + +void actorCollision_solvePenetration(Actor *actor, ActorContactData *contact, ActorCollider *collider) +{ + // Normalize the actor's velocity vector + Vector3 velocity_normal = vector3_returnNormalized(&actor->body.velocity); + + // Calculate the intersection of the ray (contact point + velocity normal) with the plane + float denominator = vector3_returnDotProduct(&velocity_normal, &contact->data.normal); + float numerator = contact->displacement + collider->body.radius - vector3_returnDotProduct(&contact->data.point, &contact->data.normal); + + float t; + if (fabsf(denominator) > 0.0001f) + t = numerator / denominator; + else + return; + + Vector3 axis_closest_at_contact = contact->data.point; + vector3_addScaledVector(&axis_closest_at_contact, &velocity_normal, t); + + Vector3 displacement_vector = axis_closest_at_contact; + vector3_subtract(&displacement_vector, &contact->axis_closest_to_point); + + contact->velocity_penetration = displacement_vector; + vector3_invert(&contact->velocity_penetration); + + vector3_add(&actor->body.position, &displacement_vector); +} + +void actorCollision_collideAndSlide(Actor *actor, ActorContactData *contact) +{ + float t = vector3_returnDotProduct(&contact->velocity_penetration, &contact->data.normal); + Vector3 projection = contact->velocity_penetration; + vector3_addScaledVector(&projection, &contact->data.normal, -t); + + vector3_add(&actor->body.position, &projection); +} + +void actorCollision_setGroundResponse(Actor *actor, ActorContactData *contact, ActorCollider *collider) +{ + actorCollision_pushTowardsNormal(actor, contact); + actor->grounded = true; + actor->body.acceleration.z = 0; + actor->body.velocity.z = 0; + actor->grounding_height = actor->body.position.z + 2.0f; + actor->state = actor->previous_state; + + // Lower the ground height slightly when on a slope + if (contact->slope > 7.0f && contact->slope < 50.0f && contact->ground_distance > 0.1f) + { + float slope_offset = 0.1f * contact->slope; + actor->grounding_height -= slope_offset; + if (actor->body.velocity.x != 0) + actor->body.position.z = actor->grounding_height; + } +} + +void actorCollision_setCeilingResponse(Actor *actor, ActorContactData *contact) +{ + if (actor->body.velocity.z > 0) + { + vector3_scale(&actor->body.velocity, 1 - (contact->angle_of_incidence * 0.01)); // angle of incidence can be up to 90 degrees + actor->body.velocity = vector3_reflect(&actor->body.velocity, &contact->data.normal); + actor->body.velocity.z = 0.0f; + } + else + { + actor->body.velocity.x = 0.0f; + actor->body.velocity.y = 0.0f; + } + + // Possible fix for Sloped Ceilings forcing Player downwards + if (!(actor->grounded)) + { + actor->state = FALLING; + actor->hasCollided = false; + } +} + +void actorCollision_setResponse(Actor *actor, ActorContactData *contact, ActorCollider *collider) +{ + actorContactData_setAngleOfIncidence(contact, &actor->body.velocity); + actorCollision_solvePenetration(actor, contact, collider); + + if (contact->slope > 0 && contact->slope < 50) + { + actorCollision_setGroundResponse(actor, contact, collider); + actorCollision_collideAndSlide(actor, contact); + } + else if (contact->slope > 95 && actor->grounded == false) + { + actorCollision_collideAndSlide(actor, contact); + actorCollision_setCeilingResponse(actor, contact); + } + else + { + actorCollision_setGroundResponse(actor, contact, collider); + actorCollision_collideAndSlide(actor, contact); + } + + actorCollider_setVertical(collider, &actor->body.position); +} + +void actorCollision_updateFalling(Actor *actor, ActorContactData *actor_contact, ActorCollider *actor_collider) +{ + actorContactData_clear(actor_contact); + actorCollider_setVertical(actor_collider, &actor->body.position); + + // Check if the actor is neither jumping nor falling + if (actor->body.position.z != LOWER_LIMIT_HEIGHT && actor->state != JUMP && actor->state != FALLING && actor->hasCollided == false) + { + + actor->state = FALLING; + actor->grounded = false; + actor->grounding_height = LOWER_LIMIT_HEIGHT; + } +} + +void actorCollision_collidePlatforms(Actor *actor, ActorContactData *actor_contact, ActorCollider *actor_collider, Platform *platforms) +{ + + actorCollision_updateFalling(actor, actor_contact, actor_collider); + + // Calculate the grid cell the actor is in + int xCell = (int)fm_floorf((actor->body.position.x + 775) / 350); + int yCell = (int)fm_floorf((actor->body.position.y + 775) / 350); + + if (xCell < 0 || xCell >= 7 || yCell < 0 || yCell >= 7) + { + // Actor is out of bounds; fall and skip collision + actor->state = FALLING; + actor->grounded = false; + actor->grounding_height = LOWER_LIMIT_HEIGHT; + if (actor->body.position.z <= DEATH_PLANE_HEIGHT) + actorState_setDeath(actor); + return; + } + + const float collisionRangeSq = 175.0f * 175.0f; + + // Reset actor's collision state + actor->hasCollided = false; + + // Iterate through platforms in the same and adjacent cells + for (int dx = -1; dx <= 1; dx++) + { + for (int dy = -1; dy <= 1; dy++) + { + int nx = xCell + dx; + int ny = yCell + dy; + + if (nx < 0 || nx >= 7 || ny < 0 || ny >= 7) + { + // Actor is out of bounds; fall and skip collision + actor->state = FALLING; + actor->grounded = false; + actor->grounding_height = LOWER_LIMIT_HEIGHT; + if (actor->body.position.z <= DEATH_PLANE_HEIGHT) + actorState_setDeath(actor); + continue; + } + + PlatformGridCell *cell = &platformGrid[nx][ny]; + for (size_t i = 0; i < cell->count; i++) + { + Platform *platform = &platforms[cell->platformIndices[i]]; + fm_vec3_t actorPos = Vector3_to_fast(actor->body.position); + fm_vec3_t platformPos = Vector3_to_fast(platform->position); + float distanceSq = fm_vec3_distance2(&actorPos, &platformPos); + if (distanceSq <= collisionRangeSq) + { + + // Check collision with each box in the platform's collider + for (int j = 0; j < 3; j++) + { + Box *box = &platform->collider.box[j]; + + // If the actor hits a box + if (actorCollision_contactBox(actor_collider, box)) + { + // Set collision response + actorCollision_contactBoxSetData(actor_contact, actor_collider, box); + actorCollision_collideAndSlide(actor, actor_contact); + actorCollision_setGroundResponse(actor, actor_contact, actor_collider); + + // If the actor is lower the top of the box (center.z+(size.z/2)), move there + if (actor->body.position.z < box->center.z + (box->size.z * 0.5f)) + actor->body.position.z = box->center.z + (box->size.z * 0.5f); + + // Set collided state parameter + actor->hasCollided = true; + + // Handle platform collision here instead again for the platforms + platform->contact = true; + + return; // Early exit if collision is detected + } + } + } + } + } + } + + // Call setState after processing collision responses + actor_setState(actor, actor->state); + + if (actor->body.position.z <= DEATH_PLANE_HEIGHT) + actorState_setDeath(actor); +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/camera/camera.h b/code/sb_hot/camera/camera.h new file mode 100644 index 00000000..22c50d12 --- /dev/null +++ b/code/sb_hot/camera/camera.h @@ -0,0 +1,194 @@ +#ifndef CAMERA_H +#define CAMERA_H + +#include "light.h" + +// structures + +typedef enum +{ + ORBITAL, + AIMING, + MINIGAME, + +} CameraState; + +typedef struct +{ + + float orbitational_acceleration_rate; + Vector2 orbitational_max_velocity; + + float zoom_acceleration_rate; + float zoom_deceleration_rate; + float zoom_max_speed; + + float distance_from_baricenter; + + float field_of_view; + float field_of_view_aim; + + float offset_acceleration_rate; + float offset_deceleration_rate; + float offset_max_speed; + + float offset_angle; + float offset_angle_aim; + + float max_pitch; + +} CameraSettings; + +typedef struct +{ + + T3DViewport viewport; + + Vector3 position; + float offset_height; + + float distance_from_barycenter; // the barycenter is choosen and it's the center of the orbitational movement + float angle_around_barycenter; + float pitch; + + float horizontal_barycenter_distance; + float vertical_barycenter_distance; + + Vector3 target; // target as in the place at which the camera must aim + float target_distance; + float horizontal_target_distance; + float vertical_target_distance; + + Vector2 orbitational_acceleration; + Vector2 orbitational_velocity; + Vector2 orbitational_target_velocity; // target as in intended velocity + + float offset_angle; + + float offset_acceleration; + float offset_speed; + int offset_direction; + + float field_of_view; + + float zoom_acceleration; + float zoom_speed; + int zoom_direction; + + float near_clipping; + float far_clipping; + + uint8_t cam_mode; + float lerpTime; + float camTime; + + CameraSettings settings; +} Camera; + +// functions prototypes + +Camera camera_create(); +void camera_getOrbitalPosition(Camera *camera, Vector3 barycenter, float frame_time); +void camera_set(Camera *camera, Screen *screen); + +// function implementations + +Camera camera_create() +{ + Camera camera = { + .position = (Vector3){0, -1200, 1200}, + .distance_from_barycenter = 500, + .target_distance = 200, + .angle_around_barycenter = 0, + .pitch = 25, + .offset_angle = 0, + .offset_height = 250, + .field_of_view = 80, + .near_clipping = 100, + .far_clipping = 3000, + .cam_mode = 0, + .lerpTime = 13.0f, + .camTime = 0, + }; + + return camera; +} + +void camera_getOrbitalPosition(Camera *camera, Vector3 barycenter, float frame_time) +{ + camera->orbitational_velocity.x += camera->orbitational_acceleration.x * frame_time; + camera->orbitational_velocity.y += camera->orbitational_acceleration.y * frame_time; + camera->zoom_speed += camera->zoom_acceleration * frame_time; + camera->offset_speed += camera->offset_acceleration * frame_time; + + if (fabsf(camera->orbitational_velocity.x) < 1.0f && fabsf(camera->orbitational_velocity.y) < 1.0f && fabsf(camera->zoom_speed) < 1.0f && fabsf(camera->offset_speed) < 1.0f) + { + camera->orbitational_velocity.x = 0; + camera->orbitational_velocity.y = 0; + camera->zoom_speed = 0; + camera->offset_speed = 0; + } + + camera->pitch += camera->orbitational_velocity.x * frame_time; + camera->angle_around_barycenter += camera->orbitational_velocity.y * frame_time; + + camera->field_of_view += camera->zoom_direction * camera->zoom_speed * frame_time; + camera->offset_angle += camera->offset_direction * camera->offset_speed * frame_time; + + if (camera->angle_around_barycenter > 360) + camera->angle_around_barycenter -= 360; + if (camera->angle_around_barycenter < 0) + camera->angle_around_barycenter += 360; + + if (camera->pitch > camera->settings.max_pitch) + camera->pitch = camera->settings.max_pitch; + if (camera->pitch < -camera->settings.max_pitch + 30) + camera->pitch = -camera->settings.max_pitch + 30; // this hard coded + 20 is for the near plane to not enter the actor geometry during "camera collision" + + camera->horizontal_barycenter_distance = camera->distance_from_barycenter * fm_cosf(rad(camera->pitch)); + camera->vertical_barycenter_distance = camera->distance_from_barycenter * fm_sinf(rad(camera->pitch)); + + camera->horizontal_target_distance = camera->target_distance * fm_cosf(rad(camera->pitch)); + camera->vertical_target_distance = camera->target_distance * fm_sinf(rad(camera->pitch + 180)); + + camera->position.x = barycenter.x - (camera->horizontal_barycenter_distance * fm_sinf(rad(camera->angle_around_barycenter - camera->offset_angle))); + camera->position.y = barycenter.y - (camera->horizontal_barycenter_distance * fm_cosf(rad(camera->angle_around_barycenter - camera->offset_angle))); + camera->position.z = barycenter.z + camera->offset_height + camera->vertical_barycenter_distance; + + /* this is a temporary brute force abomination to "collide" the camera with an horizontal plane at height 20 simulating the floor, + will be modyfied when camera collision happens */ + /* + */ + camera->distance_from_barycenter = camera->settings.distance_from_baricenter; + while (camera->position.z < 30) + { + camera->distance_from_barycenter--; + camera->horizontal_barycenter_distance = camera->distance_from_barycenter * fm_cosf(rad(camera->pitch)); + camera->vertical_barycenter_distance = camera->distance_from_barycenter * fm_sinf(rad(camera->pitch)); + + camera->position.x = barycenter.x - camera->horizontal_barycenter_distance * fm_sinf(rad(camera->angle_around_barycenter - camera->offset_angle)); + camera->position.y = barycenter.y - camera->horizontal_barycenter_distance * fm_cosf(rad(camera->angle_around_barycenter - camera->offset_angle)); + camera->position.z = barycenter.z + camera->offset_height + camera->vertical_barycenter_distance; + } + + camera->target.x = barycenter.x - camera->horizontal_target_distance * fm_sinf(rad(camera->angle_around_barycenter + 180)); + camera->target.y = barycenter.y - camera->horizontal_target_distance * fm_cosf(rad(camera->angle_around_barycenter + 180)); + camera->target.z = barycenter.z + camera->offset_height + camera->vertical_target_distance; +} + +void camera_set(Camera *camera, Screen *screen) +{ + t3d_viewport_set_projection( + &screen->gameplay_viewport, + T3D_DEG_TO_RAD(camera->field_of_view), + camera->near_clipping, + camera->far_clipping); + + t3d_viewport_look_at( + &screen->gameplay_viewport, + &(T3DVec3){{camera->position.x, camera->position.y, camera->position.z}}, + &(T3DVec3){{camera->target.x, camera->target.y, camera->target.z}}, + &(T3DVec3){{0, 0, 1}}); +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/camera/camera_control.h b/code/sb_hot/camera/camera_control.h new file mode 100644 index 00000000..6943304d --- /dev/null +++ b/code/sb_hot/camera/camera_control.h @@ -0,0 +1,119 @@ +#ifndef CAMERA_CONTROLS_H +#define CAMERA_CONTROLS_H + +/* CAMERA_CONTROLS.H +here are all the camera control related functions */ + +int input(int input); + +void camera_orbit_withStick(Camera *camera, ControllerData *data); +void camera_orbit_withCButtons(Camera *camera, ControllerData *data); +void camera_aim(Camera *camera, ControllerData *data); + +void cameraControl_setOrbitalMovement(Camera *camera, ControllerData *data); + +/* input + auxiliary function for 8 directional movement*/ + +int input(int input) +{ + if (input == 0) + { + return 0; + } + else + { + return 1; + } +} + +/* camera_move_stick +changes the camera variables depending on controller input*/ + +void camera_orbit_withStick(Camera *camera, ControllerData *data) +{ + int deadzone = 8; + float stick_x = 0; + float stick_y = 0; + + if (fabsf(data->input.stick_x) >= deadzone || fabsf(data->input.stick_y) >= deadzone) + { + stick_x = data->input.stick_x; + stick_y = data->input.stick_y; + } + + if (stick_x == 0 && stick_y == 0) + { + camera->orbitational_target_velocity.x = 0; + camera->orbitational_target_velocity.y = 0; + } + + else if (stick_x != 0 || stick_y != 0) + { + camera->orbitational_target_velocity.x = stick_y; + camera->orbitational_target_velocity.y = stick_x; + } +} + +void camera_orbit_withCButtons(Camera *camera, ControllerData *data) +{ + float input_x = 0; + float input_y = 0; + + if ((data->held.c_right) || (data->held.c_left) || (data->held.c_up) || (data->held.c_down)) + { + + input_x = input(data->held.c_right) - input(data->held.c_left); + input_y = input(data->held.c_up) - input(data->held.c_down); + } + + if (input_x == 0) + camera->orbitational_target_velocity.y = 0; + else + camera->orbitational_target_velocity.y = input_x * camera->settings.orbitational_max_velocity.y; + + if (input_y == 0) + camera->orbitational_target_velocity.x = 0; + else + camera->orbitational_target_velocity.x = input_y * camera->settings.orbitational_max_velocity.x; +} + +void cameraControl_freeCam(Camera *camera, ControllerData *data, float time) +{ + + const float speed = 500.0f; + + float input_x = 0; + float input_y = 0; + float input_z = 0; + + if ((data->held.c_right) || (data->held.c_left) || (data->held.c_up) || (data->held.c_down) || (data->held.a) || (data->held.b)) + { + + input_x = input(data->held.c_right) - input(data->held.c_left); + input_y = input(data->held.c_up) - input(data->held.c_down); + input_z = input(data->held.a) - input(data->held.b); + } + + camera->position.x += input_x * time * speed; + camera->position.y += input_y * time * speed; + camera->position.z += input_z * time * speed; +} + +void camera_aim(Camera *camera, ControllerData *data) +{ + if (data->held.z) + camera_setState(camera, AIMING); + else + camera_setState(camera, ORBITAL); +} + +void cameraControl_setOrbitalMovement(Camera *camera, ControllerData *data) +{ + // camera_orbit_withStick(camera, data_1); + camera_orbit_withCButtons(camera, data); + camera_setState(camera, MINIGAME); + // camera_aim(camera, data); +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/camera/camera_states.h b/code/sb_hot/camera/camera_states.h new file mode 100644 index 00000000..2687132d --- /dev/null +++ b/code/sb_hot/camera/camera_states.h @@ -0,0 +1,86 @@ +#ifndef CAMERA_STATES_H +#define CAMERA_STATES_H + +// function prototypes + +void cameraState_setOrbital(Camera *camera); +void cameraState_setAiming(Camera *camera); +void camera_setState(Camera *camera, CameraState new_state); + +// function implemetations + +void cameraState_setOrbital(Camera *camera) +{ + if (camera->field_of_view < camera->settings.field_of_view) + camera->zoom_acceleration = camera->settings.zoom_acceleration_rate * (camera->settings.zoom_max_speed - camera->zoom_speed); + + else + camera->zoom_acceleration = (camera->settings.zoom_deceleration_rate + 10) * (0 - camera->zoom_speed); + + camera->zoom_direction = 1; + + if (camera->offset_angle > camera->settings.offset_angle) + camera->offset_acceleration = camera->settings.offset_acceleration_rate * (camera->settings.offset_max_speed - camera->offset_speed); + + else + camera->offset_acceleration = camera->settings.offset_deceleration_rate * (0 - camera->offset_speed); + + camera->offset_direction = -1; + + camera->orbitational_acceleration.x = camera->settings.orbitational_acceleration_rate * (camera->orbitational_target_velocity.x - camera->orbitational_velocity.x); + camera->orbitational_acceleration.y = camera->settings.orbitational_acceleration_rate * (camera->orbitational_target_velocity.y - camera->orbitational_velocity.y); +} + +// the set aiming not ready for it's prime yet :( +void cameraState_setAiming(Camera *camera) +{ + if (camera->field_of_view > camera->settings.field_of_view_aim) + camera->zoom_acceleration = (camera->settings.zoom_acceleration_rate + 10) * (camera->settings.zoom_max_speed - camera->zoom_speed); + + else + camera->zoom_acceleration = camera->settings.zoom_deceleration_rate * (0 - camera->zoom_speed); + + camera->zoom_direction = -1; + + if (camera->offset_angle < camera->settings.offset_angle_aim) + camera->offset_acceleration = camera->settings.offset_acceleration_rate * (camera->settings.offset_max_speed - camera->offset_speed); + + else + camera->offset_acceleration = camera->settings.offset_deceleration_rate * (0 - camera->offset_speed); + + camera->offset_direction = 1; + + camera->orbitational_acceleration.x = camera->settings.orbitational_acceleration_rate * ((camera->orbitational_target_velocity.x / 2) - camera->orbitational_velocity.x); + camera->orbitational_acceleration.y = camera->settings.orbitational_acceleration_rate * ((camera->orbitational_target_velocity.y / 2) - camera->orbitational_velocity.y); +} + +void set_minigame(Camera *camera) +{ + camera->orbitational_acceleration.x = camera->settings.orbitational_acceleration_rate * (camera->orbitational_target_velocity.x - camera->orbitational_velocity.x); + camera->orbitational_acceleration.y = camera->settings.orbitational_acceleration_rate * (camera->orbitational_target_velocity.y - camera->orbitational_velocity.y); +} + +void camera_setState(Camera *camera, CameraState new_state) +{ + switch (new_state) + { + + case ORBITAL: + { + cameraState_setOrbital(camera); + break; + } + case AIMING: + { + cameraState_setAiming(camera); + break; + } + case MINIGAME: + { + set_minigame(camera); + break; + } + } +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/camera/light.h b/code/sb_hot/camera/light.h new file mode 100644 index 00000000..f1c38f3b --- /dev/null +++ b/code/sb_hot/camera/light.h @@ -0,0 +1,62 @@ +#ifndef LIGHT_H +#define LIGHT_H + +typedef struct +{ + + uint8_t ambient_color[4]; + uint8_t directional_color1[4]; + T3DVec3 direction1; + + uint8_t directional_color2[4]; + T3DVec3 direction2; + +} LightData; + +void light_set(LightData *light); +void light_setAmbient(LightData *light, uint8_t value); +void light_resetAmbient(LightData *light); + +LightData light_create() +{ + LightData light = { + .ambient_color = {0x32, 0x20, 0x06, 0xFF}, + .directional_color1 = {0xE3, 0x3B, 0x29, 0xFF}, + .direction1 = {{1.0f, 1.0f, 1.0f}}, + .directional_color2 = {0x32, 0x20, 0x06, 0xFF}, + .direction2 = {{0.0f, 100.0f, 1000.0f}}, + }; + + t3d_vec3_norm(&light.direction1); + + return light; +} + +/* set light +temporary function until i learn how the lights work */ +void light_set(LightData *light) +{ + t3d_light_set_ambient(light->ambient_color); + t3d_light_set_directional(0, light->directional_color1, &light->direction1); + t3d_light_set_directional(1, light->directional_color2, &light->direction2); + t3d_light_set_count(2); +} + +void light_setAmbient(LightData *light, uint8_t value) +{ + for (size_t i = 0; i < 3; i++) + { + light->ambient_color[i] = value; + } + light_set(light); +} + +void light_resetAmbient(LightData *light) +{ + light->ambient_color[0] = 0x32; + light->ambient_color[1] = 0x20; + light->ambient_color[2] = 0x06; + light_set(light); +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/control/controls.h b/code/sb_hot/control/controls.h new file mode 100644 index 00000000..3c8a54a3 --- /dev/null +++ b/code/sb_hot/control/controls.h @@ -0,0 +1,108 @@ +#ifndef CONTROLS_H +#define CONTROLS_H + +typedef struct +{ + + joypad_buttons_t pressed; + joypad_buttons_t held; + joypad_buttons_t released; + joypad_inputs_t input; + + // RUMBLE + bool rumble_active; + bool has_rumbled; + uint8_t rumble_time; + +} ControllerData; + +void controllerData_getInputs(ControllerData *data, uint8_t port); +void controllerData_rumbleStart(ControllerData *data, uint8_t port); +void controllerData_rumbleStop(ControllerData *data, uint8_t port); +void controllerData_rumbleFrames(ControllerData *data, uint8_t port, uint8_t frames); + +void controllerData_getInputs(ControllerData *data, uint8_t port) +{ + data->pressed = joypad_get_buttons_pressed(port); + data->held = joypad_get_buttons_held(port); + data->released = joypad_get_buttons_released(port); + data->input = joypad_get_inputs(port); + + // Check if the rumble pak has been unplugged + if (!joypad_get_rumble_supported(port)) + controllerData_rumbleStop(data, port); +} + +/* RUMBLE */ + +// Set rumble to active state +void controllerData_rumbleStart(ControllerData *data, uint8_t port) +{ + joypad_set_rumble_active(port, true); + data->rumble_time = 0; + data->rumble_active = true; +} + +// Reset rumble to idle state +void controllerData_rumbleStop(ControllerData *data, uint8_t port) +{ + joypad_set_rumble_active(port, false); + data->rumble_time = 0; + data->rumble_active = false; +} + +// Rumble controller for n number of frames +void controllerData_rumbleFrames(ControllerData *data, uint8_t port, uint8_t frames) +{ + + if (data->rumble_active == false) + { + controllerData_rumbleStart(data, port); + } + + data->rumble_time++; + + if (data->rumble_time >= frames) + controllerData_rumbleStop(data, port); +} + +///// 8 WAY ///// + +#define DEAD_ZONE 50 +#define INPUT_DELAY 0.3f + +// Treats joystick inputs as digital pad buttons, for menu navigation +void controllerData_8way(ControllerData *data) +{ + static float input_time = 0; + const float current_time = display_get_delta_time(); + + input_time -= current_time; + + if (input_time <= 0.0f) + { + if (data->input.stick_y > DEAD_ZONE) + { + data->pressed.d_up = 1; + input_time = INPUT_DELAY; + } + else if (data->input.stick_y < -DEAD_ZONE) + { + data->pressed.d_down = 1; + input_time = INPUT_DELAY; + } + + if (data->input.stick_x > DEAD_ZONE) + { + data->pressed.d_right = 1; + input_time = INPUT_DELAY; + } + else if (data->input.stick_x < -DEAD_ZONE) + { + data->pressed.d_left = 1; + input_time = INPUT_DELAY; + } + } +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/game/game.h b/code/sb_hot/game/game.h new file mode 100644 index 00000000..98dbc432 --- /dev/null +++ b/code/sb_hot/game/game.h @@ -0,0 +1,62 @@ +#ifndef GAME_H +#define GAME_H + +enum GAME_STATES +{ + INTRO, + MAIN_MENU, + CHARACTER_SELECT, + GAMEPLAY, + PAUSE, + GAME_OVER +}; + +typedef struct +{ + + uint8_t state; + Screen screen; + TimeData timing; + rspq_syncpoint_t syncPoint; + int diff; + int8_t winTimer; + uint8_t winnerID; + uint8_t countdownTimer; + uint8_t humanCount; + uint8_t deadPool; + bool actorSet; + bool winnerSet; + Scene scene; + +} Game; + +void game_init(Game *game); + +void game_init(Game *game) +{ + + screen_initDisplay(&game->screen); + screen_initT3dViewport(&game->screen); + + t3d_init((T3DInitParams){}); + + // TPX + ptx_init(&lavaBubbles); + + time_init(&game->timing); + + scene_init(&game->scene); + + // + ui_init(); + sound_load(); + // + + game->countdownTimer = 150; // Oops, forget to set this + game->syncPoint = 0; + game->state = INTRO; + game->humanCount = core_get_playercount(); + game->deadPool = 0; +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/game/game_control.h b/code/sb_hot/game/game_control.h new file mode 100644 index 00000000..05bd4415 --- /dev/null +++ b/code/sb_hot/game/game_control.h @@ -0,0 +1,33 @@ +#ifndef GAME_CONTROLS_H +#define GAME_CONTROLS_H + +void game_setControlData(Game *game, Player *player) +{ + for (uint8_t i = 0; i < PLAYER_COUNT; i++) + { + + if (player[i].control.pressed.start) + { + + switch (game->state) + { + case PAUSE: + game->state = GAMEPLAY; + break; + case GAMEPLAY: + game->state = PAUSE; + break; + case INTRO: + game->state = MAIN_MENU; + break; + case MAIN_MENU: + game->state = CHARACTER_SELECT; + break; + case CHARACTER_SELECT: + break; + } + } + } +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/game/game_states.h b/code/sb_hot/game/game_states.h new file mode 100644 index 00000000..573ee968 --- /dev/null +++ b/code/sb_hot/game/game_states.h @@ -0,0 +1,804 @@ +#ifndef GAME_STATES_H +#define GAME_STATES_H + +// Comment out to disable RSPQ Profiling +// #define PROFILING + +#ifdef PROFILING +#include "rspq_profile.h" +static rspq_profile_data_t profile_data; +#endif + +// function prototypes + +void gameState_setIntro(Game *game, Player *player, Scenery *scenery); +void gameState_setMainMenu(Game *game, Player *player, Actor *actor, Scenery *scenery); +void gameState_setCS(Game *game, Player *player, Actor *actor, Scenery *scenery); + +void gameState_setGameplay(Game *game, Player *player, AI *ai, Actor *actor, Scenery *scenery, ActorCollider *actor_collider, ActorContactData *actor_contact); +void gameState_setPause(Game *game, Player *player, Actor *actor, Scenery *scenery); + +void gameState_setGameOver(); + +void game_play(Game *game, Player *player, AI *ai, Actor *actor, Scenery *scenery, ActorCollider *actor_collider, ActorContactData *actor_contact); + +// new camera code //// + +void camera_getMinigamePosition(Camera *camera, Actor *actor, Player *player, Vector3 camera_distance) +{ + Vector3 camera_target; + + uint8_t average_count = 0; + + vector3_init(&camera->position); + vector3_init(&camera_target); + + for (uint8_t i = 0; i < ACTOR_COUNT; i++) + { + if (!player[i].died && player[i].isHuman) + { + + vector3_add(&camera_target, &actor[player[i].actor_id].body.position); + average_count++; + } + } + + if (average_count > 0) + vector3_divideByNumber(&camera_target, average_count); + camera_target.z = 200; + + if (camera_target.x != camera->target.x || camera_target.y != camera->target.y) + camera->target = vector3_lerp(&camera->target, &camera_target, 0.2f); + + camera->position = camera->target; + + vector3_add(&camera->position, &camera_distance); +} + +////////////////////// + +void gameState_setIntro(Game *game, Player *player, Scenery *scenery) +{ + + for (size_t j = 0; j < PLATFORM_COUNT; j++) + { + + platform_loop(&hexagons[j], NULL, 0); + } + + move_lava(scenery); + + // ======== Draw ======== // + + screen_clearDisplay(&game->screen); + screen_clearT3dViewport(&game->screen); + screen_applyColor_Depth(&game->screen, ui_color(DARK_RED), true); + + light_set(&game->scene.light); + + t3d_matrix_push_pos(1); + + room_draw(scenery); + + light_setAmbient(&game->scene.light, 0xBF); + platform_drawBatch(); + light_resetAmbient(&game->scene.light); + + t3d_matrix_pop(1); + + game->syncPoint = rspq_syncpoint_new(); + + // TPX + ptx_draw(&game->screen.gameplay_viewport, &lavaBubbles, 1, 1); + + ui_intro(&player[0].control); + + if (player[0].control.held.r) + { + ui_fps(game->timing.frame_rate, 20.0f, 20.0f); + if (core_get_playercount() == 1) + ui_input_display(&player[0].control); + } + + rdpq_detach_show(); + sound_update(); +} + +void gameState_setMainMenu(Game *game, Player *player, Actor *actor, Scenery *scenery) +{ + move_lava(scenery); + + // ======== Draw ======== // + + screen_clearDisplay(&game->screen); + screen_clearT3dViewport(&game->screen); + screen_applyColor_Depth(&game->screen, ui_color(DARK_RED), true); + + light_set(&game->scene.light); + // Instead drawing a dark transparent texture over the scene, just change the light direction + game->scene.light.direction1 = (T3DVec3){{-1, -1, -1}}; + + t3d_matrix_push_pos(1); + + room_draw(scenery); + + light_setAmbient(&game->scene.light, 0xFF); + platform_drawBatch(); + light_resetAmbient(&game->scene.light); + + t3d_matrix_pop(1); + + game->syncPoint = rspq_syncpoint_new(); + + // TPX + ptx_draw(&game->screen.gameplay_viewport, &lavaBubbles, 1, 1); + + if (core_get_playercount() == 4) + { + if (player[0].control.pressed.b) + { + if (game->diff <= 1) + { + game->diff++; + } + else + { + game->diff = 0; + } + } + } + ui_main_menu(&player[0].control, game->diff); + if (player[0].control.held.r) + { + ui_fps(game->timing.frame_rate, 20.0f, 20.0f); + if (core_get_playercount() == 1) + ui_input_display(&player[0].control); + } + + rdpq_detach_show(); + sound_update(); + +#ifdef PROFILING + rspq_profile_next_frame(); + if (game->timing.frame_counter > 29) + { + rspq_profile_dump(); + rspq_profile_reset(); + game->timing.frame_counter = 0; + } + rspq_profile_get_data(&profile_data); +#endif // PROFILING +} + +void gameState_setCS(Game *game, Player *player, Actor *actor, Scenery *scenery) +{ + + static uint8_t activePlayer = 0; + const uint8_t totalPlayers = core_get_playercount(); + static uint8_t selectedCharacter[ACTOR_COUNT] = {0}; + static bool actorSelected[ACTOR_COUNT] = {false}; + + if (activePlayer >= 4) + { + game->state = GAMEPLAY; + return; + } + + controllerData_8way(&player[activePlayer].control); + + if (player[activePlayer].control.pressed.d_right) + { + uint8_t initialSelection = selectedCharacter[activePlayer]; + do + { + selectedCharacter[activePlayer] = (selectedCharacter[activePlayer] + 1) % ACTOR_COUNT; + } while (actorSelected[selectedCharacter[activePlayer]] && selectedCharacter[activePlayer] != initialSelection); + } + + if (player[activePlayer].control.pressed.d_left) + { + uint8_t initialSelection = selectedCharacter[activePlayer]; + do + { + selectedCharacter[activePlayer] = (selectedCharacter[activePlayer] - 1 + ACTOR_COUNT) % ACTOR_COUNT; + } while (actorSelected[selectedCharacter[activePlayer]] && selectedCharacter[activePlayer] != initialSelection); + } + + if (player[activePlayer].control.pressed.a) + { + // @TODO: More of a bandaid than a fix + // Bugfix: Ensure selected actor is next available one + if (!actorSelected[selectedCharacter[activePlayer]]) + { + uint8_t selectedActorId = selectedCharacter[activePlayer]; + player[activePlayer].actor_id = selectedActorId; + player[activePlayer].isHuman = true; + actorSelected[selectedActorId] = true; + + // Visual feedback for selecting actor + actor[selectedActorId].body.rotation.z = actor[selectedActorId].body.rotation.z + 180.0f; + + activePlayer++; + } + else + { + // Audio feedback for selecting unavailable actor + sound_wavPlay(SFX_JUMP, false); + } + + // Automatically assign actors to AI players + if (activePlayer >= totalPlayers) + { + for (uint8_t i = totalPlayers; i < 4; i++) // AI players start after human players + { + for (uint8_t j = 0; j < ACTOR_COUNT; j++) + { + if (!actorSelected[j]) // Assign the first unselected actor + { + player[i].actor_id = j; + player[i].isHuman = false; + actorSelected[j] = true; + break; + } + } + } + activePlayer = 4; // Lock selection + } + + if (activePlayer >= 4) + { + game->state = GAMEPLAY; + return; + } + } + + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + actor_update(&actor[i], NULL, &game->timing, game->scene.camera.angle_around_barycenter, game->scene.camera.offset_angle, &game->syncPoint); + + // Reset non-selected actors + if (!actorSelected[i]) + { + actor[i].body.position = actor[i].home; + actor[i].body.rotation.z = 0; + } + + actor_updateMat(&actor[i]); + } + + // Sync RSPQ once, and then update each actors' skeleton + if (game->syncPoint) + rspq_syncpoint_wait(game->syncPoint); + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + t3d_skeleton_update(&actor[i].armature.main); + } + + move_lava(scenery); + + // ======== Draw ======== // + + screen_clearDisplay(&game->screen); + screen_clearT3dViewport(&game->screen); + screen_applyColor_Depth(&game->screen, ui_color(DARK_RED), true); + + light_set(&game->scene.light); + + // Change light direction to illuminate players + game->scene.light.direction1 = (T3DVec3){{0, -1, 0}}; + game->scene.light.directional_color1[0] = 200; + game->scene.light.directional_color1[1] = 200; + game->scene.light.directional_color1[2] = 200; + + t3d_matrix_push_pos(1); + + room_draw(scenery); + + light_setAmbient(&game->scene.light, 0xBF); + platform_drawBatch(); + light_resetAmbient(&game->scene.light); + + t3d_frame_start(); + actor_draw(actor); + + t3d_matrix_pop(1); + + game->syncPoint = rspq_syncpoint_new(); + + // TPX + lavaBubbles.count = 512; + ptx_draw(&game->screen.gameplay_viewport, &lavaBubbles, 1, 1); + + if (activePlayer < MAXPLAYERS) + { + player[activePlayer].position.x = (actor[selectedCharacter[activePlayer]].body.position.x * 3.6f) - 30.0f; + player[activePlayer].position.z = 125.0f; + ui_print_playerNum(&player[activePlayer], &game->screen); + } + + ui_character_select(&player[activePlayer].control, selectedCharacter[activePlayer]); + + if (player[0].control.held.r) + { + ui_fps(game->timing.frame_rate, 20.0f, 20.0f); + if (core_get_playercount() == 1) + ui_input_display(&player[0].control); + } + + rdpq_detach_show(); + sound_update(); +} + +void gameState_setGameplay(Game *game, Player *player, AI *ai, Actor *actor, Scenery *scenery, ActorCollider *actor_collider, ActorContactData *actor_contact) +{ + + if (!game->actorSet) + { + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + actor[i].body.position = hexagons[9].position; // Center Platform + actor[i].body.position.z = actor[i].body.position.z + 150.0f; // Adjust height to prevent spawning inside platform + actor[i].home = actor[i].body.position; + } + game->actorSet ^= 1; + } + + // ======== Countdown ======== // + if (game->countdownTimer > 0) + { + if (game->countdownTimer % 44 == 0) + sound_wavPlay(SFX_COUNTDOWN, false); + + if (game->countdownTimer == 3) + sound_wavPlay(SFX_START, false); + + move_lava(scenery); + + // ======== Draw ======== // + screen_clearDisplay(&game->screen); + screen_clearT3dViewport(&game->screen); + screen_applyColor_Depth(&game->screen, ui_color(DARK_RED), true); + + light_set(&game->scene.light); + + // Lerp light direction back to default during countdown + T3DVec3 dirLightPos; + t3d_vec3_lerp(&dirLightPos, &(T3DVec3){{1, 1, 1}}, &(T3DVec3){{0, -1, 0}}, (float)(game->countdownTimer) * 0.006f); + game->scene.light.direction1 = dirLightPos; + game->scene.light.directional_color1[0] = 200; + game->scene.light.directional_color1[1] = 100; + game->scene.light.directional_color1[2] = 50; + + t3d_matrix_push_pos(1); + + room_draw(scenery); + + light_setAmbient(&game->scene.light, 0xBF); + platform_drawBatch(); + light_resetAmbient(&game->scene.light); + + t3d_matrix_pop(1); + + game->syncPoint = rspq_syncpoint_new(); + + // TPX + ptx_draw(&game->screen.gameplay_viewport, &lavaBubbles, 1, 1); + + // Convert frames to seconds based on refresh rate + uint8_t secondsLeft = (game->countdownTimer / display_get_refresh_rate()) + 1; + ui_countdown(secondsLeft); + + game->countdownTimer--; + + rdpq_detach_show(); + sound_update(); + return; // Exit early until countdown finishes + } + + // ======== Gameplay ======== // + + // AI +#ifndef AI_BATTLE + for (size_t i = 0; i < AI_COUNT; i++) + { + if (player[i + PLAYER_COUNT].died) + continue; + if (game->winnerSet) + continue; + ai_generateControlData(&ai[i], &player[i + PLAYER_COUNT].control, &actor[player[i + PLAYER_COUNT].actor_id], hexagons, game->scene.camera.offset_angle); + } +#else + for (size_t i = 0; i < AI_COUNT; i++) + { + if (player[i].died) + continue; + if (game->winnerSet) + continue; + ai_generateControlData(&ai[i], &player[i].control, &actor[player[i].actor_id], hexagons, game->scene.camera.offset_angle); + } +#endif + + // Platforms + for (size_t j = 0; j < PLATFORM_COUNT; j++) + { + if (!game->winnerSet) + platform_loop(&hexagons[j], actor, game->diff); + } + + move_lava(scenery); + + // Actors + uint8_t loserCount = 0; + uint8_t aliveCount = 0; + uint8_t lastAlivePlayer = 0; + + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + // Use player[i].actor_id to identify the assigned actor + uint8_t actorIndex = player[i].actor_id; + Actor *currentActor = &actor[actorIndex]; + // Sync player's position with the actor + player[i].position = currentActor->body.position; + if (currentActor->state != DEATH) + { + if (!game->winnerSet) + { + aliveCount++; + lastAlivePlayer = i; // Track the last alive player + // Update the assigned actor using its actor ID + actor_update(currentActor, &player[i].control, &game->timing, game->scene.camera.angle_around_barycenter, game->scene.camera.offset_angle, &game->syncPoint); + // Update collision data for the assigned actor + actorCollision_collidePlatforms(currentActor, &actor_contact[actorIndex], &actor_collider[actorIndex], hexagons); + + // Update matrix + actor_updateMat(currentActor); + } + } + else + { + + // Bugfix: Center dead actor's position to not break camera + currentActor->body.position = (Vector3){0, 0, 250}; + static int8_t timer[MAXPLAYERS] = {0}; + if (player[i].isHuman) + { + if (!player[i].control.has_rumbled) + { + controllerData_rumbleFrames(&player[i].control, i, 5); + if (++timer[i] > 25) + player[i].control.has_rumbled = true; + } + else + { + controllerData_rumbleStop(&player[i].control, i); + } + } + player[i].died = true; + loserCount++; + } + } + + // Check if we have a winner (only one alive player left) + if (aliveCount == 1 && !game->winnerSet) + { + core_set_winner(lastAlivePlayer); // Set the winner to the last remaining player + game->winnerID = lastAlivePlayer; + game->winnerSet = true; + } + + // Sync RSPQ once, and then update each actors' skeleton + if (game->syncPoint) + rspq_syncpoint_wait(game->syncPoint); + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + t3d_skeleton_update(&actor[i].armature.main); + } + + // ======== Draw ======== // + + screen_clearDisplay(&game->screen); + screen_clearT3dViewport(&game->screen); + screen_applyColor(&game->screen, ui_color(DARK_RED), true); + + light_set(&game->scene.light); + + // Reset light direction to default in case players have paused + game->scene.light.direction1 = (T3DVec3){{1, 1, 1}}; + + t3d_matrix_push_pos(1); + + room_draw(scenery); + + light_setAmbient(&game->scene.light, 0xBF); + platform_drawBatch(); + light_resetAmbient(&game->scene.light); + + // Don't bother drawing shadows for the AI + for (size_t s = 0; s < PLAYER_COUNT; s++) + { + if (actor[player[s].actor_id].state == FALLING || actor[player[s].actor_id].state == JUMP) + player_drawShadow(actor[player[s].actor_id].body.position, &game->screen.gameplay_viewport); + } + + t3d_frame_start(); // reset after drawing shadows + + actor_draw(actor); + + t3d_matrix_pop(1); + + game->syncPoint = rspq_syncpoint_new(); + + // TPX + if (!game->winnerSet) + ptx_draw(&game->screen.gameplay_viewport, &lavaBubbles, 1, 1); + + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + if (!game->winnerSet) + ui_print_playerNum(&player[i], &game->screen); + } + + if (loserCount == 3) + { + if (game->winnerSet) + { + game->winTimer++; + if (game->winTimer == 3) + { + // XM takes up a lot of the buffer, have to free everything + xm64player_stop(&xmPlayer); + sound_wavClose(SFX_LAVA); + sound_wavClose(SFX_JUMP); + sound_wavClose(SFX_STONES); + wait_ticks(4); + sound_wavPlay(SFX_START, false); // Stop nor Winner will work, buffer too small + } + if (game->winTimer < 120) + ui_print_winner(game->winnerID + 1); + if (game->winTimer >= 118) + game->state = GAME_OVER; + } + } + else if (loserCount > 3) + { + game->winTimer++; + if (game->winTimer == 3) + { + sound_wavClose(SFX_JUMP); + sound_wavClose(SFX_STONES); + wait_ticks(4); + sound_wavPlay(SFX_START, false); + } + if (game->winTimer < 120) + ui_print_winner(5); + if (game->winTimer >= 118) + game->state = GAME_OVER; + } + + if (player[0].control.held.r) + { + ui_fps(game->timing.frame_rate, 20.0f, 20.0f); + if (core_get_playercount() == 1) + ui_input_display(&player[0].control); + } + + rdpq_detach_show(); + sound_update(); + +#ifdef PROFILING + rspq_profile_next_frame(); + if (game->timing.frame_counter > 29) + { + rspq_profile_dump(); + rspq_profile_reset(); + game->timing.frame_counter = 0; + } + rspq_profile_get_data(&profile_data); +#endif // PROFILING +} + +void gameState_setPause(Game *game, Player *player, Actor *actor, Scenery *scenery) +{ + + move_lava(scenery); + + // ======== Draw ======== // + + screen_clearDisplay(&game->screen); + screen_clearT3dViewport(&game->screen); + screen_applyColor(&game->screen, ui_color(BLACK), true); + + light_set(&game->scene.light); + + // Instead drawing a dark transparent texture over the scene, just change the light direction + game->scene.light.direction1 = (T3DVec3){{-1, -1, -1}}; + + t3d_matrix_push_pos(1); + + room_draw(scenery); + + light_setAmbient(&game->scene.light, 0xFF); + platform_drawBatch(); + light_resetAmbient(&game->scene.light); + + actor_draw(actor); + + t3d_matrix_pop(1); + + game->syncPoint = rspq_syncpoint_new(); + + // TPX + ptx_draw(&game->screen.gameplay_viewport, &lavaBubbles, 1, 1); + + if (player[0].control.held.r) + { + ui_fps(game->timing.frame_rate, 20.0f, 20.0f); + if (core_get_playercount() == 1) + ui_input_display(&player[0].control); + } + else + { + ui_pause(&player[0].control); + } + + rdpq_detach_show(); + sound_update(); + +#ifdef PROFILING + rspq_profile_next_frame(); + if (game->timing.frame_counter > 29) + { + rspq_profile_dump(); + rspq_profile_reset(); + game->timing.frame_counter = 0; + } + rspq_profile_get_data(&profile_data); +#endif // PROFILING +} + +void gameState_setGameOver() +{ + minigame_end(); +} + +void game_play(Game *game, Player *player, AI *ai, Actor *actor, Scenery *scenery, ActorCollider *actor_collider, ActorContactData *actor_contact) +{ + for (;;) + { + + // Tme + time_setData(&game->timing); + + // Controls + game_setControlData(game, player); + player_setControlData(player); + + // Hold Z to quit on the Pause screen + static int resetTimer = 0; + if (game->state == PAUSE && player[0].control.held.z) + { + resetTimer++; + if (resetTimer == 30) + { + sound_wavPlay(SFX_JUMP, false); + } + else if (resetTimer > 40) + { + game->state = GAME_OVER; + } + } + + if (game->state == PAUSE && player[0].control.released.z) + resetTimer = 0; + + //// CAMERA ///// + if (player[0].control.pressed.l) + game->scene.camera.cam_mode ^= 1; + + Vector3 introStartPos = (Vector3){0, 3000, 100}; + Vector3 centerHex = hexagons[10].home; + Vector3 csPos = (Vector3){0, -1000, 525}; + Vector3 gamePlayPos = (Vector3){0, -600, 1000}; + + game->scene.camera.camTime += game->timing.fixed_time_s; + + if (game->state == INTRO) + { + float t = game->scene.camera.camTime / game->scene.camera.lerpTime; + t = clamp(t, 0.0f, 1.0f); + Vector3 camPos = vector3_lerp(&introStartPos, ¢erHex, t); + camera_getMinigamePosition(&game->scene.camera, actor, player, camPos); + } + else + { + + if (game->scene.camera.cam_mode == 0) + { + camera_getOrbitalPosition(&game->scene.camera, csPos, game->timing.fixed_time_s); + } + else + { + float t = game->scene.camera.camTime / game->scene.camera.lerpTime; + t = clamp(t, 0.0f, 1.0f); + Vector3 camPos = vector3_lerp(&csPos, &gamePlayPos, t); + if (game->state == PAUSE) + { + cameraControl_freeCam(&game->scene.camera, &player[0].control, game->timing.fixed_time_s); + } + else + { + camera_getMinigamePosition(&game->scene.camera, actor, player, camPos); + } + } + } + camera_set(&game->scene.camera, &game->screen); + //// CAMERA ///// + + // Sound: reverb + mixer_ch_set_vol_pan(MUSIC_CHANNEL, sound_reverb(0.3f, 0.7f), 0.5f); + for (int i = 0; i < SFX_WINNER; i++) + { + if (i < SFX_COUNTDOWN) + { + mixer_ch_set_vol_pan(SFX_CHANNEL - i, sound_reverb(0.9f, 0.6f), 0.5f); + } + else if (i != SFX_LAVA) + { + mixer_ch_set_vol_pan(SFX_CHANNEL - i, sound_reverb(0.5f, 0.8f), 0.5f); + } + } + + switch (game->state) + { + case INTRO: + { + gameState_setIntro(game, player, scenery); + break; + } + case MAIN_MENU: + { + gameState_setMainMenu(game, player, actor, scenery); + break; + } + case CHARACTER_SELECT: + { + gameState_setCS(game, player, actor, scenery); + break; + } + case GAMEPLAY: + { + game->scene.camera.cam_mode = 1; + for (uint8_t p = 0; p < game->humanCount; p++) + { + if (player[p].isHuman && player[p].died && !player[p].deathCounted) + { + game->deadPool++; + player[p].deathCounted = true; + } + } + if (game->deadPool == game->humanCount) + { + display_set_fps_limit(0); + } + else + { + display_set_fps_limit((display_get_refresh_rate() / 4) * 2); + } + gameState_setGameplay(game, player, ai, actor, scenery, actor_collider, actor_contact); + break; + } + case PAUSE: + { + gameState_setPause(game, player, actor, scenery); + break; + } + case GAME_OVER: + { + gameState_setGameOver(); + return; + } + } + } +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/main.c b/code/sb_hot/main.c new file mode 100644 index 00000000..bfe7ebbe --- /dev/null +++ b/code/sb_hot/main.c @@ -0,0 +1,213 @@ +#include +#include +#include +#include +#include +#include +#include + +// May make this an easter egg +// #define AI_BATTLE + +// This define is to test if running the game loop +// in the fixed or the delta matters +#define FIXED + +#define ACTOR_COUNT 4 +#define PLAYER_COUNT core_get_playercount() + +#ifndef AI_BATTLE +#define AI_COUNT ACTOR_COUNT - PLAYER_COUNT +#else +#define AI_COUNT ACTOR_COUNT +#endif + +#define SCENERY_COUNT 1 +#define PLATFORM_COUNT 19 + +#define S4YS 0 +#define WOLFIE 1 +#define MEW 2 +#define DOGMAN 3 + +#include "../../core.h" +#include "../../minigame.h" + +#include "screen/screen.h" +#include "control/controls.h" +#include "time/time.h" + +#include "physics/physics.h" + +#include "camera/camera.h" +#include "camera/camera_states.h" +#include "camera/camera_control.h" + +#include "sound/sound.h" + +#include "actor/actor.h" +#include "actor/actor_states.h" +#include "actor/actor_motion.h" +#include "actor/actor_control.h" +#include "actor/actor_animation.h" + +#include "player/player.h" + +#include "scene/scene.h" +#include "scene/scenery.h" +#include "scene/platform.h" +#include "scene/room.h" + +#include "actor/collision/actor_collision_detection.h" +#include "actor/collision/actor_collision_response.h" + +#include "player/ai.h" + +#include "ui/ui.h" + +// TPX +#include +#include "scene/particles.h" + +#include "game/game.h" +#include "game/game_control.h" +#include "game/game_states.h" + +const MinigameDef minigame_def = { + .gamename = "Hot Hot Hexagons", + .developername = "Strawberry Byte: .zoncabe, s4ys, mewde, kaelin", + .description = "The floor is lava!", + .instructions = "Jump from platform to platform\nto avoid a terrible fate."}; + +Game minigame = { + .state = INTRO}; + +Player player[MAXPLAYERS]; + +AI aiPlayer[MAXPLAYERS]; + +Actor actors[ACTOR_COUNT]; + +ActorCollider actor_collider[ACTOR_COUNT]; + +ActorContactData actor_contact[ACTOR_COUNT]; + +Scenery scenery[SCENERY_COUNT]; + +void minigame_init() +{ + game_init(&minigame); +#ifdef PROFILING + // rdpq_debug_start(); + profile_data.frame_count = 0; + rspq_profile_start(); +#endif + + // As per Rasky's suggestion, limit the frame rate to 2 frames every 3 vblanks + display_set_fps_limit((display_get_refresh_rate() / 3) * 2); + + // actors + actors[S4YS] = actor_create(0, "rom:/strawberry_byte/s4ys.t3dm"); + actors[WOLFIE] = actor_create(1, "rom:/strawberry_byte/wolfie.t3dm"); + actors[MEW] = actor_create(2, "rom:/strawberry_byte/mew.t3dm"); + actors[DOGMAN] = actor_create(3, "rom:/strawberry_byte/dogman.t3dm"); + + for (uint8_t i = 0; i < ACTOR_COUNT; i++) + { + actor_init(&actors[i]); + actorCollider_init(&actor_collider[i]); + actor_collider[i].settings.body_radius = 35.0f; + actor_collider[i].settings.body_height = 190.f; + + actors[i].body.position.y = -800.0f; + actors[i].body.rotation.x = 25.0f; + actors[i].body.position.z = 450.0f; + + // Evenly space characters along x-axis + float spacing = 100.0f; // Distance between characters + actors[i].body.position.x = -((ACTOR_COUNT - 1) * spacing) / 2 + i * spacing; + + actors[i].home = actors[i].body.position; + + player_init(&player[i], i, i); + } + + // AI + for (uint8_t i = 0; i < AI_COUNT; i++) + { + ai_init(&aiPlayer[i], core_get_aidifficulty()); + } + + // scenery + scenery[0] = scenery_create(0, "rom:/strawberry_byte/lava.t3dm"); + + for (uint8_t i = 0; i < SCENERY_COUNT; i++) + { + scenery_set(&scenery[i]); + } + + // platforms + platform_hexagonGrid(hexagons, t3d_model_load("rom:/strawberry_byte/platform.t3dm"), 250.0f, ui_color(N_YELLOW)); + + // Sound: Play lava SFX + sound_wavPlayBG(SFX_LAVA); +} + +#ifdef FIXED +void minigame_fixedloop(float dt) +{ + minigame.timing.fixed_time_s = dt; + game_play(&minigame, player, aiPlayer, actors, scenery, actor_collider, actor_contact); +} +void minigame_loop(float dt) +{ + minigame.timing.frame_time_s = dt; +} +#else +void minigame_fixedloop(float dt) +{ + minigame.timing.fixed_time_s = dt; +} +void minigame_loop(float dt) +{ + minigame.timing.frame_time_s = dt; + game_play(&minigame, player, aiPlayer, actors, scenery, actor_collider, actor_contact); +} +#endif + +void minigame_cleanup() +{ + +#ifdef PROFILING + rspq_profile_stop(); +#endif + + // Step 1: Disable Frame Limiter + display_set_fps_limit(0); + + // Step 2: Clean up Subsystems + sound_cleanup(); + ui_cleanup(); + + // TPX + ptx_cleanup(&lavaBubbles); + + // Step 3: Destroy Tiny3D models, matrices, animations and RSPQ blocks + for (uint8_t i = 0; i < ACTOR_COUNT; i++) + { + + actor_delete(&actors[i]); + }; + + for (uint8_t i = 0; i < SCENERY_COUNT; i++) + { + + scenery_delete(&scenery[i]); + } + platform_destroy(hexagons); + t3d_destroy(); // Then destroy library + + // Step 4: Free allocated surface buffers + surface_free(&minigame.screen.depthBuffer); + display_close(); +} diff --git a/code/sb_hot/physics/body/rigid_body.h b/code/sb_hot/physics/body/rigid_body.h new file mode 100644 index 00000000..17d6853f --- /dev/null +++ b/code/sb_hot/physics/body/rigid_body.h @@ -0,0 +1,25 @@ +/** + * @file + * + * This file contains the definitions for the rigid body class, the + * basic building block of all the physics system. + */ + +#ifndef RIGID_BODY_H +#define RIGID_BODY_H + +typedef struct RigidBody +{ + + Vector3 acceleration; + Vector3 velocity; + Vector3 position; + Vector3 rotation; + + Vector3 previous_position; + + // Quaternion orientation; + +} RigidBody; + +#endif diff --git a/code/sb_hot/physics/collision/contact_data.h b/code/sb_hot/physics/collision/contact_data.h new file mode 100644 index 00000000..61ec9f1f --- /dev/null +++ b/code/sb_hot/physics/collision/contact_data.h @@ -0,0 +1,20 @@ +#ifndef CONTACT_DATA +#define CONTACT_DATA + +#define MAX_CONTACTS 3 + +typedef struct +{ + Vector3 point; + Vector3 normal; + float penetration; +} ContactData; + +void contactData_init(ContactData *contact) +{ + contact->point = (Vector3){0, 0, 0}; + contact->normal = (Vector3){0, 0, 0}; + contact->penetration = 0.0f; +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/physics/collision/shapes/AABB.h b/code/sb_hot/physics/collision/shapes/AABB.h new file mode 100644 index 00000000..0de6f9c7 --- /dev/null +++ b/code/sb_hot/physics/collision/shapes/AABB.h @@ -0,0 +1,322 @@ +#ifndef AABB_H +#define AABB_H + +// structures + +typedef struct AABB +{ + Vector3 minCoordinates; + Vector3 maxCoordinates; +} AABB; + +#define MAGIC_NUM 0 + +// function prototypes + +void aabb_setFromCenterAndSize(AABB *aabb, const Vector3 *center, const Vector3 *size); +void aabb_getCorners(const AABB *aabb, Vector3 corners[8]); + +Vector3 aabb_closestToPoint(const AABB *aabb, const Vector3 *point); +Vector3 aabb_closestToSegment(const AABB *aabb, const Vector3 *a, const Vector3 *b); + +bool aabb_containsPoint(const AABB *aabb, const Vector3 *point); +bool aabb_contactAABB(const AABB *a, const AABB *b); +void aabb_contactAABBsetData(ContactData *contact, const AABB *a, const AABB *b); +bool aabb_contactSphere(const AABB *aabb, const Sphere *sphere); +void aabb_contactSphereSetData(ContactData *contact, const AABB *aabb, const Sphere *sphere); + +// function implementations + +void aabb_setFromCenterAndSize(AABB *aabb, const Vector3 *center, const Vector3 *size) +{ + Vector3 halfSize = vector3_returnScaled(size, 0.5f); + aabb->minCoordinates = vector3_difference(center, &halfSize); + aabb->maxCoordinates = vector3_sum(center, &halfSize); +} + +void aabb_getCorners(const AABB *aabb, Vector3 corners[8]) +{ + corners[0] = (Vector3){aabb->minCoordinates.x, aabb->minCoordinates.y, aabb->minCoordinates.z}; + corners[1] = (Vector3){aabb->maxCoordinates.x, aabb->minCoordinates.y, aabb->minCoordinates.z}; + corners[2] = (Vector3){aabb->minCoordinates.x, aabb->maxCoordinates.y, aabb->minCoordinates.z}; + corners[3] = (Vector3){aabb->maxCoordinates.x, aabb->maxCoordinates.y, aabb->minCoordinates.z}; + corners[4] = (Vector3){aabb->minCoordinates.x, aabb->minCoordinates.y, aabb->maxCoordinates.z}; + corners[5] = (Vector3){aabb->maxCoordinates.x, aabb->minCoordinates.y, aabb->maxCoordinates.z}; + corners[6] = (Vector3){aabb->minCoordinates.x, aabb->maxCoordinates.y, aabb->maxCoordinates.z}; + corners[7] = (Vector3){aabb->maxCoordinates.x, aabb->maxCoordinates.y, aabb->maxCoordinates.z}; +} + +Vector3 aabb_closestToPoint(const AABB *aabb, const Vector3 *point) +{ + Vector3 closest; + closest.x = clamp(point->x, aabb->minCoordinates.x, aabb->maxCoordinates.x); + closest.y = clamp(point->y, aabb->minCoordinates.y, aabb->maxCoordinates.y); + closest.z = clamp(point->z, aabb->minCoordinates.z, aabb->maxCoordinates.z); + return closest; +} + +Vector3 aabb_getCenter(const AABB *aabb) +{ + Vector3 sum = vector3_sum(&aabb->minCoordinates, &aabb->maxCoordinates); + return vector3_returnScaled(&sum, 0.5f); +} + +Vector3 aabb_getHalfSize(const AABB *aabb) +{ + Vector3 difference = vector3_difference(&aabb->maxCoordinates, &aabb->minCoordinates); + return vector3_returnScaled(&difference, 0.5f); +} + +Vector3 aabb_closestToSegment(const AABB *aabb, const Vector3 *a, const Vector3 *b) +{ + Vector3 s, v, sign = {1, 1, 1}; + Vector3 aabb_center = aabb_getCenter(aabb); + Vector3 half_size = aabb_getHalfSize(aabb); + Vector3 tanchor = {2.0f, 2.0f, 2.0f}; // Initial large number for tanchor + Vector3 region = {0, 0, 0}; + Vector3 v2; + float t = 0; + + // Translate the segment's starting point relative to the AABB's center + s = vector3_difference(a, &aabb_center); + + // Calculate the direction vector of the segment + v = vector3_difference(b, a); + + // Mirror the line direction if necessary + if (v.x < 0) + { + s.x = -s.x; + v.x = -v.x; + sign.x = -1; + } + if (v.y < 0) + { + s.y = -s.y; + v.y = -v.y; + sign.y = -1; + } + if (v.z < 0) + { + s.z = -s.z; + v.z = -v.z; + sign.z = -1; + } + + // Calculate v^2 (the square of each component of v) + v2.x = v.x * v.x; + v2.y = v.y * v.y; + v2.z = v.z * v.z; + + // Determine regions and tanchor values for a + if (v.x > MAGIC_NUM) + { + if (s.x < -half_size.x) + { + region.x = -1; + tanchor.x = (-half_size.x - s.x) / v.x; + } + else if (s.x > half_size.x) + { + region.x = 1; + tanchor.x = (half_size.x - s.x) / v.x; + } + } + else + { + region.x = 0; + tanchor.x = 2; // this will never be a valid tanchor + } + + if (v.y > MAGIC_NUM) + { + if (s.y < -half_size.y) + { + region.y = -1; + tanchor.y = (-half_size.y - s.y) / v.y; + } + else if (s.y > half_size.y) + { + region.y = 1; + tanchor.y = (half_size.y - s.y) / v.y; + } + } + else + { + region.y = 0; + tanchor.y = 2; // this will never be a valid tanchor + } + if (v.z > MAGIC_NUM) + { + if (s.z < -half_size.z) + { + region.z = -1; + tanchor.z = (-half_size.z - s.z) / v.z; + } + else if (s.z > half_size.z) + { + region.z = 1; + tanchor.z = (half_size.z - s.z) / v.z; + } + } + else + { + region.z = 0; + tanchor.z = 2; // this will never be a valid tanchor + } + + // Check the initial dd2dt + float dd2dt = 0; + if (region.x != 0) + dd2dt -= v2.x * tanchor.x; + if (region.y != 0) + dd2dt -= v2.y * tanchor.y; + if (region.z != 0) + dd2dt -= v2.z * tanchor.z; + if (dd2dt >= 0) + goto finalize; + + // Iterate to find the smallest t + float next_t, next_dd2dt; + + do + { + + // find the point on the line that is at the next clip plane boundary + next_t = 1.0f; + next_t = (tanchor.x > t && tanchor.x < 1 && tanchor.x < next_t) ? tanchor.x : next_t; + next_t = (tanchor.y > t && tanchor.y < 1 && tanchor.y < next_t) ? tanchor.y : next_t; + next_t = (tanchor.z > t && tanchor.z < 1 && tanchor.z < next_t) ? tanchor.z : next_t; + + // compute d|d|^2/dt for the next t + next_dd2dt = 0; + next_dd2dt += (region.x ? v2.x : 0) * (next_t - tanchor.x); + next_dd2dt += (region.y ? v2.y : 0) * (next_t - tanchor.y); + next_dd2dt += (region.z ? v2.z : 0) * (next_t - tanchor.z); + + // if the sign of d|d|^2/dt has changed, solution = the crossover point + if (next_dd2dt >= 0) + { + float m = (next_dd2dt - dd2dt) / (next_t - t); + t -= dd2dt / m; + goto finalize; + } + + // advance to the next anchor point / region + if (tanchor.x == next_t) + { + tanchor.x = (half_size.x - s.x) / v.x; + region.x++; + } + if (tanchor.y == next_t) + { + tanchor.y = (half_size.y - s.y) / v.y; + region.y++; + } + if (tanchor.z == next_t) + { + tanchor.z = (half_size.z - s.z) / v.z; + region.z++; + } + + t = next_t; + dd2dt = next_dd2dt; + + } while (t < 1.0f); + + t = 1.0f; + +finalize: + + // Compute the closest point on the box + Vector3 tmp = {sign.x * (s.x + t * v.x), sign.y * (s.y + t * v.y), sign.z * (s.z + t * v.z)}; + + // Clamp tmp to the AABB's extents + if (tmp.x < -half_size.x) + tmp.x = -half_size.x; + else if (tmp.x > half_size.x) + tmp.x = half_size.x; + + if (tmp.y < -half_size.y) + tmp.y = -half_size.y; + else if (tmp.y > half_size.y) + tmp.y = half_size.y; + + if (tmp.z < -half_size.z) + tmp.z = -half_size.z; + else if (tmp.z > half_size.z) + tmp.z = half_size.z; + + return (Vector3){tmp.x + aabb_center.x, tmp.y + aabb_center.y, tmp.z + aabb_center.z}; +} + +bool aabb_containsPoint(const AABB *aabb, const Vector3 *point) +{ + return (point->x >= aabb->minCoordinates.x && point->x <= aabb->maxCoordinates.x && + point->y >= aabb->minCoordinates.y && point->y <= aabb->maxCoordinates.y && + point->z >= aabb->minCoordinates.z && point->z <= aabb->maxCoordinates.z); +} + +bool aabb_contactAABB(const AABB *a, const AABB *b) +{ + if (a->maxCoordinates.x < b->minCoordinates.x || b->maxCoordinates.x < a->minCoordinates.x) + return false; + if (a->maxCoordinates.y < b->minCoordinates.y || b->maxCoordinates.y < a->minCoordinates.y) + return false; + if (a->maxCoordinates.z < b->minCoordinates.z || b->maxCoordinates.z < a->minCoordinates.z) + return false; + return true; +} + +void aabb_contactAABBsetData(ContactData *contact, const AABB *a, const AABB *b) +{ + // Calculate overlap on each axis + float overlapX = min2(a->maxCoordinates.x, b->maxCoordinates.x) - max2(a->minCoordinates.x, b->minCoordinates.x); + float overlapY = min2(a->maxCoordinates.y, b->maxCoordinates.y) - max2(a->minCoordinates.y, b->minCoordinates.y); + float overlapZ = min2(a->maxCoordinates.z, b->maxCoordinates.z) - max2(a->minCoordinates.z, b->minCoordinates.z); + + // Find the axis of least penetration + if (overlapX < overlapY && overlapX < overlapZ) + { + contact->normal = (Vector3){a->maxCoordinates.x < b->maxCoordinates.x ? -1 : 1, 0, 0}; + } + else if (overlapY < overlapX && overlapY < overlapZ) + { + contact->normal = (Vector3){0, a->maxCoordinates.y < b->maxCoordinates.y ? -1 : 1, 0}; + } + else + { + contact->normal = (Vector3){0, 0, a->maxCoordinates.z < b->maxCoordinates.z ? -1 : 1}; + } + + // Calculate the contact point + contact->point = (Vector3){ + (max2(a->minCoordinates.x, b->minCoordinates.x) + min2(a->maxCoordinates.x, b->maxCoordinates.x)) / 2.0f, + (max2(a->minCoordinates.y, b->minCoordinates.y) + min2(a->maxCoordinates.y, b->maxCoordinates.y)) / 2.0f, + (max2(a->minCoordinates.z, b->minCoordinates.z) + min2(a->maxCoordinates.z, b->maxCoordinates.z)) / 2.0f}; +} + +bool aabb_contactSphere(const AABB *aabb, const Sphere *sphere) +{ + // Find the point on the AABB closest to the center of the sphere + Vector3 closestPoint = aabb_closestToPoint(aabb, &sphere->center); + + // Calculate the distance from the closest point to the center of the sphere + Vector3 difference = vector3_difference(&closestPoint, &sphere->center); + float distanceSquared = vector3_squaredMagnitude(&difference); + + // Check if the distance is less than or equal to the radius squared + return distanceSquared <= (sphere->radius * sphere->radius); +} + +void aabb_contactSphereSetData(ContactData *contact, const AABB *aabb, const Sphere *sphere) +{ + // the contact point is the point on the AABB closest to the center of the sphere + contact->point = aabb_closestToPoint(aabb, &sphere->center); + + // Calculate the vector from the sphere center to the closest point on the AABB + contact->normal = vector3_difference(&sphere->center, &contact->point); + vector3_normalize(&contact->normal); +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/physics/collision/shapes/box.h b/code/sb_hot/physics/collision/shapes/box.h new file mode 100644 index 00000000..aad6e547 --- /dev/null +++ b/code/sb_hot/physics/collision/shapes/box.h @@ -0,0 +1,77 @@ +#ifndef BOX_H +#define BOX_H + +// structures + +typedef struct +{ + Vector3 size; + Vector3 center; + Vector3 rotation; +} Box; + +// function prototypes + +AABB box_getLocalAABB(const Box *box); + +bool box_contactSphere(const Box *box, const Sphere *sphere); +void box_contactSphereSetData(ContactData *contact, const Box *box, const Sphere *sphere); + +void box_init(Box *box, Vector3 size, Vector3 center, Vector3 rotation, float scalar); + +// function implementations + +AABB box_getLocalAABB(const Box *box) +{ + AABB aabb; + aabb.minCoordinates = vector3_returnScaled(&box->size, -0.5f); + aabb.maxCoordinates = vector3_returnScaled(&box->size, 0.5f); + return aabb; +} + +bool box_contactSphere(const Box *box, const Sphere *sphere) +{ + // Transform the center of the sphere to the local space of the box + Vector3 local_sphere_center = sphere->center; + point_transformToLocalSpace(&local_sphere_center, &box->center, &box->rotation); + + // Get the local AABB of the box and local sphere + AABB local_aabb = box_getLocalAABB(box); + const Sphere local_sphere = { + center : local_sphere_center, + radius : sphere->radius + }; + + // Check for collision in the local space + return aabb_contactSphere(&local_aabb, &local_sphere); +} + +void box_contactSphereSetData(ContactData *contact, const Box *box, const Sphere *sphere) +{ + // Transform the center of the sphere to the local space of the box + Vector3 local_sphere_center = sphere->center; + point_transformToLocalSpace(&local_sphere_center, &box->center, &box->rotation); + + // Get the local AABB of the box + AABB local_aabb = box_getLocalAABB(box); + + // Find the point on the AABB closest to the center of the sphere + contact->point = aabb_closestToPoint(&local_aabb, &local_sphere_center); + + // Calculate the normal in local space pointing from the AABB towards the sphere + contact->normal = vector3_difference(&local_sphere_center, &contact->point); + vector3_normalize(&contact->normal); + + // Transform the closest point and normal back to global space + point_transformToGlobalSpace(&contact->point, &box->center, &box->rotation); + rotate_normal(&contact->normal, &box->rotation); // Rotate normal to global space +} + +void box_init(Box *box, Vector3 size, Vector3 center, Vector3 rotation, float scalar) +{ + box->size = vector3_returnScaled(&size, scalar); + box->center = vector3_returnScaled(¢er, scalar); + box->rotation = rotation; +} + +#endif diff --git a/code/sb_hot/physics/collision/shapes/capsule.h b/code/sb_hot/physics/collision/shapes/capsule.h new file mode 100644 index 00000000..b3566831 --- /dev/null +++ b/code/sb_hot/physics/collision/shapes/capsule.h @@ -0,0 +1,220 @@ +#ifndef CAPSULE_H +#define CAPSULE_H + +typedef struct +{ + Vector3 start; + Vector3 end; + float radius; + float length; +} Capsule; + +// Function prototypes + +void capsule_setVertical(Capsule *capsule, const Vector3 *position); + +bool capsule_contactSphere(const Capsule *capsule, const Sphere *sphere); +void capsule_contactSphereSetData(ContactData *contact, const Capsule *capsule, const Sphere *sphere); + +bool capsule_contactAABB(const Capsule *capsule, const AABB *aabb); +void capsule_contactAABBSetData(ContactData *contact, const Capsule *capsule, const AABB *aabb); + +bool capsule_contactBox(const Capsule *capsule, const Box *box); +void capsule_contactBoxSetData(ContactData *contact, const Capsule *capsule, const Box *box); + +bool capsule_contactPlane(const Capsule *capsule, const Plane *plane); + +bool capsule_intersectionRay(const Capsule *capsule, const Ray *ray); + +// Function implementations + +void capsule_setVertical(Capsule *capsule, const Vector3 *position) +{ + capsule->start = *position; + capsule->start.z += capsule->radius; + capsule->end = *position; + capsule->end.z = capsule->end.z + capsule->length - capsule->radius; +} + +bool capsule_contactSphere(const Capsule *capsule, const Sphere *sphere) +{ + // Calculate the closest point on the capsule segment to the sphere center + Vector3 closest_on_axis = segment_closestToPoint(&capsule->start, &capsule->end, &sphere->center); + + // Calculate the squared distance from the closest point to the sphere center + Vector3 difference = vector3_difference(&closest_on_axis, &sphere->center); + float distanceSquared = vector3_squaredMagnitude(&difference); + + // Compare the squared distance with the squared combined radius + float combinedRadius = capsule->radius + sphere->radius; + return distanceSquared <= combinedRadius * combinedRadius; +} + +void capsule_contactSphereSetData(ContactData *contact, const Capsule *capsule, const Sphere *sphere) +{ + // Calculate the closest point on the capsule segment to the sphere center + Vector3 closest_on_axis = segment_closestToPoint(&capsule->start, &capsule->end, &sphere->center); + + // Calculate the squared distance from the closest point to the sphere center + contact->normal = vector3_difference(&closest_on_axis, &sphere->center); + contact->penetration = capsule->radius + sphere->radius - fabsf(vector3_magnitude(&contact->normal)); + vector3_normalize(&contact->normal); + + // Calculate the contact point in reference to the sphere + contact->point = sphere->center; + vector3_addScaledVector(&contact->point, &contact->normal, sphere->radius); +} + +bool capsule_contactAABB(const Capsule *capsule, const AABB *aabb) +{ + Vector3 closest_point_on_aabb = aabb_closestToSegment(aabb, &capsule->end, &capsule->start); + Vector3 closest_point_on_axis = segment_closestToPoint(&capsule->end, &capsule->start, &closest_point_on_aabb); + Vector3 distance_vector = vector3_difference(&closest_point_on_axis, &closest_point_on_aabb); + + return vector3_squaredMagnitude(&distance_vector) <= capsule->radius * capsule->radius; +} + +void capsule_contactAABBSetData(ContactData *contact, const Capsule *capsule, const AABB *aabb) +{ + contact->point = aabb_closestToSegment(aabb, &capsule->end, &capsule->start); + + Vector3 closest_point_on_axis = segment_closestToPoint(&capsule->end, &capsule->start, &contact->point); + Vector3 distance_vector = vector3_difference(&closest_point_on_axis, &contact->point); + contact->penetration = capsule->radius - fabsf(vector3_magnitude(&distance_vector)); + contact->normal = distance_vector; + vector3_normalize(&contact->normal); +} + +bool capsule_contactBox(const Capsule *capsule, const Box *box) +{ + Capsule local_capsule = *capsule; + point_transformToLocalSpace(&local_capsule.start, &box->center, &box->rotation); + point_transformToLocalSpace(&local_capsule.end, &box->center, &box->rotation); + AABB local_aabb = box_getLocalAABB(box); + return capsule_contactAABB(&local_capsule, &local_aabb); +} + +void capsule_contactBoxSetData(ContactData *contact, const Capsule *capsule, const Box *box) +{ + Capsule local_capsule = *capsule; + point_transformToLocalSpace(&local_capsule.start, &box->center, &box->rotation); + point_transformToLocalSpace(&local_capsule.end, &box->center, &box->rotation); + AABB local_aabb = box_getLocalAABB(box); + + capsule_contactAABBSetData(contact, &local_capsule, &local_aabb); + point_transformToGlobalSpace(&contact->point, &box->center, &box->rotation); + rotate_normal(&contact->normal, &box->rotation); +} + +bool capsule_contactPlane(const Capsule *capsule, const Plane *plane) +{ + // Calculate the distances of the capsule endpoints from the plane + float distance_to_start = plane_distanceToPoint(plane, &capsule->start); + float distance_to_end = plane_distanceToPoint(plane, &capsule->end); + + // Check if either endpoint of the capsule is within the radius distance from the plane + if (fabsf(distance_to_start) <= capsule->radius || fabsf(distance_to_end) <= capsule->radius) + { + return true; + } + + // Check if the plane intersects the segment of the capsule + if ((distance_to_start > 0 && distance_to_end < 0) || (distance_to_start < 0 && distance_to_end > 0)) + { + return true; + } + + return false; +} + +void capsule_contactPlaneSetData(ContactData *contact, const Capsule *capsule, const Plane *plane) +{ + // Calculate the distances of the capsule endpoints from the plane + float distance_to_start = plane_distanceToPoint(plane, &capsule->start); + float distance_to_end = plane_distanceToPoint(plane, &capsule->end); + + // Set the contact normal as the plane's normal + contact->normal = plane->normal; + + // Determine the point of contact and penetration depth + if (fabsf(distance_to_start) <= capsule->radius) + { + // The start point of the capsule is within the radius distance from the plane + contact->penetration = capsule->radius - fabsf(distance_to_start); + contact->point = capsule->start; + vector3_addScaledVector(&contact->point, &contact->normal, -distance_to_start); + } + else if (fabsf(distance_to_end) <= capsule->radius) + { + // The end point of the capsule is within the radius distance from the plane + contact->penetration = capsule->radius - fabsf(distance_to_end); + contact->point = capsule->end; + vector3_addScaledVector(&contact->point, &contact->normal, -distance_to_end); + } +} + +bool capsule_intersectionRay(const Capsule *capsule, const Ray *ray) +{ + // Calculate the vector from the start to the end of the capsule + Vector3 ba = vector3_difference(&capsule->end, &capsule->start); + // Calculate the vector from the start of the capsule to the origin of the ray + Vector3 oa = vector3_difference(&ray->origin, &capsule->start); + + // Compute dot products used in the quadratic equation + float baba = vector3_returnDotProduct(&ba, &ba); + float bard = vector3_returnDotProduct(&ba, &ray->direction); + float baoa = vector3_returnDotProduct(&ba, &oa); + float rdoa = vector3_returnDotProduct(&ray->direction, &oa); + float oaoa = vector3_returnDotProduct(&oa, &oa); + + // Compute the coefficients of the quadratic equation + float a = baba - bard * bard; + float b = baba * rdoa - baoa * bard; + float c = baba * oaoa - baoa * baoa - capsule->radius * capsule->radius * baba; + float h = b * b - a * c; + + // Check if the discriminant is non-negative (real roots) + if (h >= 0.0f && a != 0.0f) + { + // Compute the smallest root of the quadratic equation + float t = (-b - sqrtf(h)) / a; + float y = baoa + t * bard; + // Check if the intersection is within the cylindrical body of the capsule + if (y > 0.0f && y < baba) + return true; + // Otherwise, check for intersections with the spherical caps + Vector3 oc = (y <= 0.0f) ? oa : vector3_difference(&ray->origin, &capsule->end); + b = vector3_returnDotProduct(&ray->direction, &oc); + c = vector3_returnDotProduct(&oc, &oc) - capsule->radius * capsule->radius; + h = b * b - c; + // Check if the intersection with the cap is valid + if (h > 0.0f) + return true; + } + // Return false if no intersection is found + return false; +} + +bool capsule_intersectsEdge(const Capsule *capsule, const Vector3 *edgeStart, const Vector3 *edgeEnd) +{ + // Step 1: Find the closest point on the capsule's segment to the edge + Vector3 closestPointOnCapsule = segment_closestToPoint(&capsule->start, &capsule->end, edgeStart); + Vector3 closestPointOnEdge = segment_closestToPoint(edgeStart, edgeEnd, &capsule->start); + + // Step 2: Calculate the distance vector between these two closest points + Vector3 distanceVector = vector3_difference(&closestPointOnCapsule, &closestPointOnEdge); + + // Step 3: Calculate the squared distance + float distanceSquared = vector3_squaredMagnitude(&distanceVector); + + // Step 4: Calculate the combined radius + float combinedRadius = capsule->radius; // Adjust if the edge has its own thickness + + // Step 5: Check for intersection + return distanceSquared <= combinedRadius * combinedRadius; +} + +/* + */ + +#endif \ No newline at end of file diff --git a/code/sb_hot/physics/collision/shapes/plane.h b/code/sb_hot/physics/collision/shapes/plane.h new file mode 100644 index 00000000..2d867f63 --- /dev/null +++ b/code/sb_hot/physics/collision/shapes/plane.h @@ -0,0 +1,76 @@ +#ifndef PLANE_H +#define PLANE_H + +// structures + +typedef struct +{ + Vector3 normal; // Normal vector of the plane + float displacement; // Displacement from the origin along the normal +} Plane; + +// function prototypes + +Vector3 plane_getNormalFromRotation(const Vector3 *rotation); +float plane_getDisplacement(const Vector3 *normal, const Vector3 *point); +void plane_setFromRotationAndPoint(Plane *plane, const Vector3 *rotation, const Vector3 *point); +void plane_setFromNormalAndPoint(Plane *plane, const Vector3 *normal, const Vector3 *point); +float plane_distanceToPoint(const Plane *plane, const Vector3 *point); + +// function implementations + +Vector3 plane_getNormalFromRotation(const Vector3 *rotation) +{ + Vector3 normal = {0.0f, 0.0f, 1.0f}; + Vector3 rad_rotation = vector3_degToRad(rotation); + Quaternion q_rotation = quaternion_getFromVector(&rad_rotation); + normal = vector3_rotateByQuaternion(&normal, &q_rotation); + + return normal; +} + +float plane_getDisplacement(const Vector3 *normal, const Vector3 *point) +{ + + return vector3_returnDotProduct(normal, point); +} + +void plane_setFromRotationAndPoint(Plane *plane, const Vector3 *rotation, const Vector3 *point) +{ + plane->normal = plane_getNormalFromRotation(rotation); + plane->displacement = plane_getDisplacement(&plane->normal, point); +} + +void plane_setFromNormalAndPoint(Plane *plane, const Vector3 *normal, const Vector3 *point) +{ + plane->normal = *normal; + plane->displacement = plane_getDisplacement(normal, point); +} + +float plane_distanceToPoint(const Plane *plane, const Vector3 *point) +{ + return vector3_returnDotProduct(&plane->normal, point) - plane->displacement; +} + +bool plane_contactSphere(const Plane *plane, const Sphere *sphere) +{ + // Project the center of the sphere onto the plane's normal vector + float distance = vector3_returnDotProduct(&plane->normal, &sphere->center) - plane->displacement; + + // Check if the distance is less than the radius of the sphere + return fabsf(distance) <= sphere->radius; +} + +void plane_contactSphereGetData(ContactData *contact, const Plane *plane, const Sphere *sphere) +{ + // Project the center of the sphere onto the plane's normal vector + float distance = vector3_returnDotProduct(&plane->normal, &sphere->center) - plane->displacement; + + contact->normal = plane->normal; + + // Calculate the contact point on the plane + Vector3 scaled_normal = vector3_returnScaled(&plane->normal, distance); + contact->point = vector3_difference(&sphere->center, &scaled_normal); +} + +#endif diff --git a/code/sb_hot/physics/collision/shapes/ray.h b/code/sb_hot/physics/collision/shapes/ray.h new file mode 100644 index 00000000..da6013f0 --- /dev/null +++ b/code/sb_hot/physics/collision/shapes/ray.h @@ -0,0 +1,290 @@ +#ifndef RAY_H +#define RAY_H + +// structures + +typedef struct +{ + Vector3 origin; + Vector3 direction; +} Ray; + +// function prototypes + +Vector3 ray_getDirectionFromRotation(const Vector3 *rotation); +void ray_setFromRotationAndPoint(Ray *ray, const Vector3 *origin, const Vector3 *rotation); + +bool ray_intersectionSphere(const Ray *ray, const Sphere *sphere); +void raycast_sphere(ContactData *contact, const Ray *ray, const Sphere *sphere); + +bool ray_intersectionAABB(const Ray *ray, const AABB *aabb); +void raycast_aabb(ContactData *contact, const Ray *ray, const AABB *aabb); + +bool ray_intersectionBox(const Ray *ray, const Box *box); +void raycast_box(ContactData *contact, const Ray *ray, const Box *box); + +// function implementations + +Vector3 ray_getDirectionFromRotation(const Vector3 *rotation) +{ + Vector3 direction = {0.0f, 1.0f, 0.0f}; + Vector3 rad_rotation = vector3_degToRad(rotation); + Quaternion q_rotation = quaternion_getFromVector(&rad_rotation); + direction = vector3_rotateByQuaternion(&direction, &q_rotation); + + return direction; +} + +void ray_setFromRotationAndPoint(Ray *ray, const Vector3 *origin, const Vector3 *rotation) +{ + ray->origin = *origin; + ray->direction = ray_getDirectionFromRotation(rotation); +} + +bool ray_intersectionSphere(const Ray *ray, const Sphere *sphere) +{ + // Vector from ray origin to sphere center + Vector3 oc = vector3_difference(&ray->origin, &sphere->center); + + // Compute dot products used in the quadratic equation + float a = vector3_returnDotProduct(&ray->direction, &ray->direction); + float b = 2.0f * vector3_returnDotProduct(&oc, &ray->direction); + float c = vector3_returnDotProduct(&oc, &oc) - sphere->radius * sphere->radius; + + // Compute the discriminant + float discriminant = b * b - 4 * a * c; + + // Check if the discriminant is non-negative (real roots) + return (discriminant >= 0); +} + +void raycast_sphere(ContactData *contact, const Ray *ray, const Sphere *sphere) +{ + // Vector from ray origin to sphere center + Vector3 oc = vector3_difference(&ray->origin, &sphere->center); + + // Compute dot products used in the quadratic equation + float a = vector3_returnDotProduct(&ray->direction, &ray->direction); + float b = 2.0f * vector3_returnDotProduct(&oc, &ray->direction); + float c = vector3_returnDotProduct(&oc, &oc) - sphere->radius * sphere->radius; + + // Compute the discriminant + float discriminant = b * b - 4 * a * c; + + // Calculate the nearest intersection point + float t = (-b - 1.0f / qi_sqrt(discriminant)) / (2.0f * a); + + // Calculate the contact point + contact->point = (Vector3){ + ray->origin.x + t * ray->direction.x, + ray->origin.y + t * ray->direction.y, + ray->origin.z + t * ray->direction.z}; + + // Calculate the normal at the contact point + contact->normal = vector3_difference(&contact->point, &sphere->center); + vector3_normalize(&contact->normal); +} + +bool ray_intersectionAABB(const Ray *ray, const AABB *aabb) +{ + Vector3 rayDirectionInverse = { + 1.0f / ray->direction.x, + 1.0f / ray->direction.y, + 1.0f / ray->direction.z}; + + // Calculate intersection times for the x-axis + float t1 = (aabb->minCoordinates.x - ray->origin.x) * rayDirectionInverse.x; + float t2 = (aabb->maxCoordinates.x - ray->origin.x) * rayDirectionInverse.x; + float tMin = min2(t1, t2); + float tMax = max2(t1, t2); + + // Calculate intersection times for the y-axis + float t1_y = (aabb->minCoordinates.y - ray->origin.y) * rayDirectionInverse.y; + float t2_y = (aabb->maxCoordinates.y - ray->origin.y) * rayDirectionInverse.y; + tMin = max2(tMin, min2(t1_y, t2_y)); + tMax = min2(tMax, max2(t1_y, t2_y)); + + // Calculate intersection times for the z-axis + float t1_z = (aabb->minCoordinates.z - ray->origin.z) * rayDirectionInverse.z; + float t2_z = (aabb->maxCoordinates.z - ray->origin.z) * rayDirectionInverse.z; + tMin = max2(tMin, min2(t1_z, t2_z)); + tMax = min2(tMax, max2(t1_z, t2_z)); + + // Check if there is a valid intersection + return tMax >= max2(tMin, 0.0f); +} + +void raycast_aabb(ContactData *contact, const Ray *ray, const AABB *aabb) +{ + float tMin = 0.0f; + float tMax = FLT_MAX; + const float epsilon = 0.00001f; + + float t1 = 0; + float t2 = 0; + float rayDirectionInverse = 0; + + // For x-axis + if (fabsf(ray->direction.x) < epsilon) + { + if (ray->origin.x < aabb->minCoordinates.x || ray->origin.x > aabb->maxCoordinates.x) + return; + } + else + { + rayDirectionInverse = 1.0f / ray->direction.x; + t1 = (aabb->minCoordinates.x - ray->origin.x) * rayDirectionInverse; + t2 = (aabb->maxCoordinates.x - ray->origin.x) * rayDirectionInverse; + if (t1 > t2) + { + float temp = t2; + t2 = t1; + t1 = temp; + } + tMin = max2(tMin, t1); + tMax = min2(tMax, t2); + if (tMin > tMax) + return; + } + + // For y-axis + if (fabsf(ray->direction.y) < epsilon) + { + if (ray->origin.y < aabb->minCoordinates.y || ray->origin.y > aabb->maxCoordinates.y) + return; + } + else + { + rayDirectionInverse = 1.0f / ray->direction.y; + t1 = (aabb->minCoordinates.y - ray->origin.y) * rayDirectionInverse; + t2 = (aabb->maxCoordinates.y - ray->origin.y) * rayDirectionInverse; + if (t1 > t2) + { + float temp = t2; + t2 = t1; + t1 = temp; + } + tMin = max2(tMin, t1); + tMax = min2(tMax, t2); + if (tMin > tMax) + return; + } + + // For z-axis + if (fabsf(ray->direction.z) < epsilon) + { + if (ray->origin.z < aabb->minCoordinates.z || ray->origin.z > aabb->maxCoordinates.z) + return; + } + else + { + rayDirectionInverse = 1.0f / ray->direction.z; + t1 = (aabb->minCoordinates.z - ray->origin.z) * rayDirectionInverse; + t2 = (aabb->maxCoordinates.z - ray->origin.z) * rayDirectionInverse; + if (t1 > t2) + { + float temp = t2; + t2 = t1; + t1 = temp; + } + tMin = max2(tMin, t1); + tMax = min2(tMax, t2); + if (tMin > tMax) + return; + } + + Vector3 temp = ray->direction; + vector3_scale(&temp, tMin); + vector3_add(&temp, &ray->origin); + contact->point = temp; + + // Assuming the normal should be calculated based on which side the intersection occurred + if (tMin == t1) + { + contact->normal = (Vector3){1, 0, 0}; + } + else if (tMin == t2) + { + contact->normal = (Vector3){-1, 0, 0}; + } + else if (tMin == t1) + { + contact->normal = (Vector3){0, 1, 0}; + } + else if (tMin == t2) + { + contact->normal = (Vector3){0, -1, 0}; + } + else if (tMin == t1) + { + contact->normal = (Vector3){0, 0, 1}; + } + else if (tMin == t2) + { + contact->normal = (Vector3){0, 0, -1}; + } +} + +bool ray_intersectionBox(const Ray *ray, const Box *box) +{ + // Transform the ray to the local space of the box + Ray local_ray = *ray; + point_transformToLocalSpace(&local_ray.origin, &box->center, &box->rotation); + rotate_normal(&local_ray.direction, &box->rotation); + + // Invert the direction of the local ray for the intersection test + vector3_invert(&local_ray.direction); + + // Get the local AABB of the box + AABB local_AABB = box_getLocalAABB(box); + + // Use the existing AABB intersection function + return ray_intersectionAABB(&local_ray, &local_AABB); +} + +void raycast_box(ContactData *contact, const Ray *ray, const Box *box) +{ + // Transform the ray to the local space of the box + Ray local_ray = *ray; + point_transformToLocalSpace(&local_ray.origin, &box->center, &box->rotation); + rotate_normal(&local_ray.direction, &box->rotation); + + // Invert the direction of the local ray for the intersection test + vector3_invert(&local_ray.direction); + + // Get the local AABB of the box + AABB local_aabb = box_getLocalAABB(box); + + // Perform the intersection using raycast_aabb + raycast_aabb(contact, &local_ray, &local_aabb); + + // Transform the hit point and normal back to global space + point_transformToGlobalSpace(&contact->point, &box->center, &box->rotation); + rotate_normal(&contact->normal, &box->rotation); +} + +bool ray_intersectionPlane(const Ray *ray, const Plane *plane) +{ + float denominator = vector3_returnDotProduct(&ray->direction, &plane->normal); + + // If the denominator is zero, the ray is parallel to the plane + if (fabsf(denominator) < 1e-6) + return false; + + // If t is negative, the ray intersects the plane in the opposite direction + float t = (plane->displacement - vector3_returnDotProduct(&ray->origin, &plane->normal)) / denominator; + if (t < 0) + return false; + + return true; +} + +void raycast_plane(ContactData *contact, const Ray *ray, const Plane *plane) +{ + contact->point = ray->origin; + vector3_addScaledVector(&contact->point, &ray->direction, (plane->displacement - vector3_returnDotProduct(&ray->origin, &plane->normal)) / vector3_returnDotProduct(&ray->direction, &plane->normal)); + + contact->normal = plane->normal; +} + +#endif diff --git a/code/sb_hot/physics/collision/shapes/sphere.h b/code/sb_hot/physics/collision/shapes/sphere.h new file mode 100644 index 00000000..56533770 --- /dev/null +++ b/code/sb_hot/physics/collision/shapes/sphere.h @@ -0,0 +1,38 @@ +#ifndef SPHERE_H +#define SPHERE_H + +// structures + +typedef struct +{ + Vector3 center; + float radius; +} Sphere; + +bool sphere_contactSphere(const Sphere *s, const Sphere *t); +void sphere_collisionTestSphere(ContactData *contact, const Sphere *s, const Sphere *t); + +bool sphere_contactSphere(const Sphere *s, const Sphere *t) +{ + float radiusSum = s->radius + t->radius; + + Vector3 diff = s->center; + vector3_subtract(&diff, &t->center); + // Check if the squared distance is less than or equal to the square of the sum of the radii + return vector3_squaredMagnitude(&diff) <= radiusSum * radiusSum; +} + +void sphere_collisionTestSphere(ContactData *contact, const Sphere *s, const Sphere *t) +{ + // Calculate the normal pointing towards the first sphere + contact->normal = vector3_difference(&t->center, &s->center); + vector3_normalize(&contact->normal); + + // Calculate the contact point in reference to the second sphere + contact->point = (Vector3){ + t->center.x - contact->normal.x * t->radius, + t->center.y - contact->normal.y * t->radius, + t->center.z - contact->normal.z * t->radius}; +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/physics/collision/shapes/triangle.h b/code/sb_hot/physics/collision/shapes/triangle.h new file mode 100644 index 00000000..c37583cd --- /dev/null +++ b/code/sb_hot/physics/collision/shapes/triangle.h @@ -0,0 +1,74 @@ +#ifndef TRIANGLE_H +#define TRIANGLE_H + +typedef struct +{ + Vector3 start; + Vector3 end; +} Edge; + +// Structure for a triangular plane +typedef struct Triangle +{ + Vector3 vertA; + Vector3 vertB; + Vector3 vertC; + Vector3 normal; + Edge edge; +} Triangle; + +void triangle_setVertices(Triangle *triangle, const Vector3 *vertexA, const Vector3 *vertexB, const Vector3 *vertexC); +bool triangle_containsPoint(const Triangle *triangle, const Vector3 *point); + +// Sets up the triangle plane and computes its normal +void triangle_setVertices(Triangle *triangle, const Vector3 *a, const Vector3 *b, const Vector3 *c) +{ + triangle->vertA = *a; + triangle->vertB = *b; + triangle->vertC = *c; + + // Calculate the normal by taking the cross product of two edges + Vector3 edge1 = vector3_difference(b, a); + Vector3 edge2 = vector3_difference(c, a); + Vector3 normal = vector3_returnCrossProduct(&edge1, &edge2); + vector3_normalize(&normal); + triangle->normal = normal; +} + +// Check if a point is on or near the plane of the triangle +bool triangle_containsPoint(const Triangle *triangle, const Vector3 *point) +{ + // Compute vector from point to one vertex + Vector3 point_to_a = vector3_difference(point, &triangle->vertA); + + // Compute dot product to find the distance to the plane along the normal + float distance = vector3_returnDotProduct(&point_to_a, &triangle->normal); + + // Consider the point on the plane if the distance is near zero (tolerance can be adjusted) + return fabsf(distance) < TOLERANCE; +} + +// Initialize edges of the hexagon from vertices +void hex_initEdges(Edge *edges, Vector3 *vertices) +{ + for (int i = 0; i < 6; ++i) + { + edges[i].start = vertices[i]; + edges[i].end = vertices[(i + 1) % 6]; // Loop back to the first vertex + } +} + +// Initialize each triangle in the hexagon +void hex_init(Triangle *hexagon, Vector3 *center, Vector3 *vertices) +{ + triangle_setVertices(&hexagon[0], center, &vertices[0], &vertices[1]); + triangle_setVertices(&hexagon[1], center, &vertices[1], &vertices[2]); + triangle_setVertices(&hexagon[2], center, &vertices[2], &vertices[3]); + triangle_setVertices(&hexagon[3], center, &vertices[3], &vertices[4]); + triangle_setVertices(&hexagon[4], center, &vertices[4], &vertices[5]); + triangle_setVertices(&hexagon[5], center, &vertices[5], &vertices[0]); + + hex_initEdges(&hexagon->edge, vertices); +} + +#endif // TRIANGLE_H diff --git a/code/sb_hot/physics/math/math_common.h b/code/sb_hot/physics/math/math_common.h new file mode 100644 index 00000000..e76bc4f1 --- /dev/null +++ b/code/sb_hot/physics/math/math_common.h @@ -0,0 +1,125 @@ +#ifndef PHYSICS_MATH_COMMON_H +#define PHYSICS_MATH_COMMON_H + +#define PI 3.141592653589f +#define PI_TIMES_2 6.28318530f + +#define TOLERANCE 1e-6f + +// ---------- Mathematics functions ---------- // + +float qi_sqrt(float number); + +float rad(float angle); +float deg(float rad); + +int clamp_int(int value, int lowerLimit, int upperLimit); +float clamp(float value, float lowerLimit, float upperLimit); + +float max2(float a, float b); +float min2(float a, float b); + +float min3(float a, float b, float c); +float max3(float a, float b, float c); + +bool sameSign(float a, float b); +bool approxEqual(float a, float b); + +bool isfinite(float x); + +/* quick inverse square root */ +inline float qi_sqrt(float number) +{ + float x2, y; + const float threehalfs = 1.5F; + + x2 = number * 0.5F; + y = number; + + union + { + float f; + long l; + } converter; + + converter.f = y; // Almacena el float en la unión + converter.l = 0x5f3759df - (converter.l >> 1); // Manipula los bits como long + y = converter.f; // Recupera el resultado como float + + y = y * (threehalfs - (x2 * y * y)); // 1st iteration + y = y * (threehalfs - (x2 * y * y)); // 2nd iteration (puede eliminarse) + + return y; +} + +/* degrees to radians */ +inline float rad(float angle) +{ + return PI / 180 * angle; +} + +/* radians to degrees */ +inline float deg(float rad) +{ + return 180 / PI * rad; +} + +/* return the result of the "value" clamped by "lowerLimit" and "upperLimit" */ +inline int clamp_int(int value, int lowerLimit, int upperLimit) +{ + assert(lowerLimit <= upperLimit); + return (value < lowerLimit) ? lowerLimit : (value > upperLimit) ? upperLimit + : value; +} + +/* return the result of the "value" clamped by "lowerLimit" and "upperLimit" */ +inline float clamp(float value, float lowerLimit, float upperLimit) +{ + assert(lowerLimit <= upperLimit); + return (value < lowerLimit) ? lowerLimit : (value > upperLimit) ? upperLimit + : value; +} + +/* return higher value*/ +inline float max2(float a, float b) +{ + return (a > b) ? a : b; +} + +/* return lower value*/ +inline float min2(float a, float b) +{ + return (a < b) ? a : b; +} + +/* return the minimum value among three values */ +inline float min3(float a, float b, float c) +{ + return (a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c); +} + +/* return the maximum value among three values */ +inline float max3(float a, float b, float c) +{ + return (a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c); +} + +/* return true if two values have the same sign */ +inline bool sameSign(float a, float b) +{ + return a * b >= 0.0f; +} + +/* Function to test if two real numbers are (almost) equal +We test if two numbers a and b are such that (a-b) are in [-EPSILON; EPSILON] */ +inline bool approxEqual(float a, float b) +{ + return (fabsf(a - b) < FLT_EPSILON); +} + +inline bool isfinite(float x) +{ + return (x == x) && (x != INFINITY) && (x != -INFINITY); +} + +#endif diff --git a/code/sb_hot/physics/math/math_functions.h b/code/sb_hot/physics/math/math_functions.h new file mode 100644 index 00000000..b22e1bf1 --- /dev/null +++ b/code/sb_hot/physics/math/math_functions.h @@ -0,0 +1,483 @@ +#ifndef MATH_FUNCTIONS_H +#define MATH_FUNCTIONS_H + +// function prototypes + +Vector3 vector3_multiplyByMatrix3x3(const Matrix3x3 *matrix, const Vector3 *vector); +Vector3 vector3_rotateByQuaternion(const Vector3 *v, const Quaternion *q); + +Vector3 vector3_transformToLocalSpace(const Vector3 *global_point, Vector3 local_center, Vector3 rotation); +Vector3 vector3_transformToGlobalSpace(const Vector3 *local_point, Vector3 local_center, Vector3 rotation); + +Vector3 vector3_reflect(const Vector3 *vector, const Vector3 *normal); + +Vector3 vector3_degToRad(const Vector3 *rotation); +Vector3 vector3_clamp(const Vector3 *vector, float maxLength); + +bool vector3_areParallel(const Vector3 *vector1, const Vector3 *vector2); +bool vector3_areOrthogonal(const Vector3 *vector1, const Vector3 *vector2); + +void point_rotateZYX(Vector3 *point, const Vector3 *rotation); +void point_rotateXYZ(Vector3 *point, const Vector3 *rotation); +void point_transformToLocalSpace(Vector3 *global_point, const Vector3 *local_center, const Vector3 *local_rotation); +void point_transformToGlobalSpace(Vector3 *local_point, const Vector3 *local_center, const Vector3 *local_rotation); + +Vector3 segment_closestToPoint(const Vector3 *seg_a, const Vector3 *seg_b, const Vector3 *point_c); +void segment_closestPointsWithSegment(const Vector3 *seg1_a, const Vector3 *seg1_b, const Vector3 *seg2_a, const Vector3 *seg2_b, Vector3 *closest_seg1, Vector3 *closest_seg2); +float segment_distanceToPoint(const Vector3 *a, const Vector3 *b, const Vector3 *p); + +float line_distanceToPoint(const Vector3 *linePointA, const Vector3 *linePointB, const Vector3 *point); + +float plane_intersectionWithSegment(const Vector3 *a, const Vector3 *b, float plane_displacement, const Vector3 *planeormal); + +void triangle_getBarycentricCoordinates(const Vector3 *a, const Vector3 *b, const Vector3 *c, const Vector3 *p, float *u, float *v, float *w); + +Matrix3x3 rotationMatrix_getFromEuler(const Vector3 *rotation); + +void rotate_normal(Vector3 *vector, const Vector3 *rotation); + +Vector3 vector3_fromQuaternion(Quaternion q); + +// function implementations + +inline Vector3 vector3_multiplyByMatrix3x3(const Matrix3x3 *matrix, const Vector3 *vector) +{ + return (Vector3){ + .x = matrix->row[0].x * vector->x + matrix->row[0].y * vector->y + matrix->row[0].z * vector->z, + .y = matrix->row[1].x * vector->x + matrix->row[1].y * vector->y + matrix->row[1].z * vector->z, + .z = matrix->row[2].x * vector->x + matrix->row[2].y * vector->y + matrix->row[2].z * vector->z}; +} + +inline Vector3 vector3_rotateByQuaternion(const Vector3 *v, const Quaternion *q) +{ + Vector3 u = {q->x, q->y, q->z}; + float s = q->w; + + Vector3 rv1 = vector3_returnScaled(&u, 2.0f * vector3_returnDotProduct(&u, v)); + Vector3 rv2 = vector3_returnScaled(v, (s * s - vector3_returnDotProduct(&u, &u))); + Vector3 crossProduct = vector3_returnCrossProduct(&u, v); + Vector3 rv3 = vector3_returnScaled(&crossProduct, 2.0f * s); + + Vector3 result = vector3_sum(&rv1, &rv2); + + return vector3_sum(&result, &rv3); +} + +inline Vector3 vector3_transformToLocalSpace(const Vector3 *global_point, Vector3 local_center, Vector3 rotation) +{ + // Translate point by the inverse of Box's center + Vector3 local_point = vector3_difference(global_point, &local_center); + + // Rotate the translated point by the inverse of Box's rotation + Vector3 inverse_rotation = vector3_getInverse(&rotation); + point_rotateXYZ(&local_point, &inverse_rotation); + + return local_point; +} + +inline Vector3 vector3_transformToGlobalSpace(const Vector3 *local_point, Vector3 local_center, Vector3 rotation) +{ + Vector3 global_point = *local_point; + // Apply rotation to the local point to get it in the global space orientation + point_rotateZYX(&global_point, &rotation); + + // Translate the rotated point by adding the Box's center + global_point = vector3_sum(&global_point, &local_center); + + return global_point; +} + +inline Vector3 vector3_degToRad(const Vector3 *rotation) +{ + Vector3 result; + result.x = rad(rotation->x); + result.y = rad(rotation->y); + result.z = rad(rotation->z); + return result; +} + +inline bool vector3_areParallel(const Vector3 *vector1, const Vector3 *vector2) +{ + Vector3 crossProduct = { + vector1->y * vector2->z - vector1->z * vector2->y, + vector1->z * vector2->x - vector1->x * vector2->z, + vector1->x * vector2->y - vector1->y * vector2->x}; + return (crossProduct.x * crossProduct.x + crossProduct.y * crossProduct.y + crossProduct.z * crossProduct.z) < 0.00001f; +} + +inline bool vector3_areOrthogonal(const Vector3 *vector1, const Vector3 *vector2) +{ + float dotProduct = vector1->x * vector2->x + vector1->y * vector2->y + vector1->z * vector2->z; + return fabsf(dotProduct) < 0.001f; +} + +/* clamp a vector to a maximum length */ +inline Vector3 vector3_clamp(const Vector3 *vector, float maxLength) +{ + float lengthSquare = vector->x * vector->x + vector->y * vector->y + vector->z * vector->z; + if (lengthSquare > maxLength * maxLength) + { + float length = sqrt(lengthSquare); + return (Vector3){vector->x * maxLength / length, vector->y * maxLength / length, vector->z * maxLength / length}; + } + return *vector; +} + +inline Vector3 vector3_reflect(const Vector3 *vector, const Vector3 *normal) +{ + Vector3 scaled_normal = vector3_returnScaled(normal, vector3_returnDotProduct(vector, normal) * 2.0f); + return vector3_difference(vector, &scaled_normal); +} + +/* compute and return a point on segment from "seg_a" and "seg_b" that is closest to point "point_c" */ +inline Vector3 segment_closestToPoint(const Vector3 *seg_a, const Vector3 *seg_b, const Vector3 *point_c) +{ + Vector3 ab = {seg_b->x - seg_a->x, seg_b->y - seg_a->y, seg_b->z - seg_a->z}; + Vector3 ac = {point_c->x - seg_a->x, point_c->y - seg_a->y, point_c->z - seg_a->z}; + float abLengthSquare = ab.x * ab.x + ab.y * ab.y + ab.z * ab.z; + + // If the segment has almost zero length + if (abLengthSquare < 1e-6f) + { + // Return one end-point of the segment as the closest point + return *seg_a; + } + + // Project point C onto "AB" line + float t = (ac.x * ab.x + ac.y * ab.y + ac.z * ab.z) / abLengthSquare; + + // If projected point onto the line is outside the segment, clamp it to the segment + if (t < 0.0f) + t = 0.0f; + if (t > 1.0f) + t = 1.0f; + + // Return the closest point on the segment + return (Vector3){seg_a->x + t * ab.x, seg_a->y + t * ab.y, seg_a->z + t * ab.z}; +} + +/* segment_closestPointInBetween +compute the closest points between two segments */ +inline void segment_closestPointsWithSegment(const Vector3 *seg1_a, const Vector3 *seg1_b, + const Vector3 *seg2_a, const Vector3 *seg2_b, + Vector3 *closest_seg1, Vector3 *closest_seg2) +{ + + Vector3 d1 = {seg1_b->x - seg1_a->x, seg1_b->y - seg1_a->y, seg1_b->z - seg1_a->z}; + Vector3 d2 = {seg2_b->x - seg2_a->x, seg2_b->y - seg2_a->y, seg2_b->z - seg2_a->z}; + Vector3 r = {seg1_a->x - seg2_a->x, seg1_a->y - seg2_a->y, seg1_a->z - seg2_a->z}; + float a = d1.x * d1.x + d1.y * d1.y + d1.z * d1.z; + float e = d2.x * d2.x + d2.y * d2.y + d2.z * d2.z; + float f = d2.x * r.x + d2.y * r.y + d2.z * r.z; + float s, t; + + // If both segments degenerate into points + if (a <= 1e-6f && e <= 1e-6f) + { + *closest_seg1 = *seg1_a; + *closest_seg2 = *seg2_a; + return; + } + if (a <= 1e-6f) + { // If first segment degenerates into a point + s = 0.0f; + // Compute the closest point on second segment + t = clamp(f / e, 0.0f, 1.0f); + } + else + { + float c = d1.x * r.x + d1.y * r.y + d1.z * r.z; + // If the second segment degenerates into a point + if (e <= 1e-6f) + { + t = 0.0f; + s = clamp(-c / a, 0.0f, 1.0f); + } + else + { + float b = d1.x * d2.x + d1.y * d2.y + d1.z * d2.z; + float denom = a * e - b * b; + // If the segments are not parallel + if (denom != 0.0f) + { + // Compute the closest point on line 1 to line 2 and + // clamp to first segment. + s = clamp((b * f - c * e) / denom, 0.0f, 1.0f); + } + else + { + // Pick an arbitrary point on first segment + s = 0.0f; + } + // Compute the point on line 2 closest to the closest point + // we have just found + t = (b * s + f) / e; + // If this closest point is inside second segment (t in [0, 1]), we are done. + // Otherwise, we clamp the point to the second segment and compute again the + // closest point on segment 1 + if (t < 0.0f) + { + t = 0.0f; + s = clamp(-c / a, 0.0f, 1.0f); + } + else if (t > 1.0f) + { + t = 1.0f; + s = clamp((b - c) / a, 0.0f, 1.0f); + } + } + } + + // Compute the closest points on both segments + *closest_seg1 = (Vector3){seg1_a->x + d1.x * s, seg1_a->y + d1.y * s, seg1_a->z + d1.z * s}; + *closest_seg2 = (Vector3){seg2_a->x + d2.x * t, seg2_a->y + d2.y * t, seg2_a->z + d2.z * t}; +} + +/* triangle_getBarycentricCoordinates +compute the barycentric coordinates u, v, w of a point p inside the triangle (a, b, c) */ +inline void triangle_getBarycentricCoordinates(const Vector3 *a, const Vector3 *b, const Vector3 *c, + const Vector3 *p, float *u, float *v, float *w) +{ + Vector3 v0 = {b->x - a->x, b->y - a->y, b->z - a->z}; + Vector3 v1 = {c->x - a->x, c->y - a->y, c->z - a->z}; + Vector3 v2 = {p->x - a->x, p->y - a->y, p->z - a->z}; + + float d00 = v0.x * v0.x + v0.y * v0.y + v0.z * v0.z; + float d01 = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z; + float d11 = v1.x * v1.x + v1.y * v1.y + v1.z * v1.z; + float d20 = v2.x * v0.x + v2.y * v0.y + v2.z * v0.z; + float d21 = v2.x * v1.x + v2.y * v1.y + v2.z * v1.z; + + float denom = d00 * d11 - d01 * d01; + *v = (d11 * d20 - d01 * d21) / denom; + *w = (d00 * d21 - d01 * d20) / denom; + *u = 1.0f - *v - *w; +} + +/* plane_intersectionWithSegment +compute the intersection between a plane and a segment */ +inline float plane_intersectionWithSegment(const Vector3 *a, const Vector3 *b, float plane_displacement, const Vector3 *planeormal) +{ + const float parallelEpsilon = 0.0001f; + float t = -1.0f; + + Vector3 ab = {b->x - a->x, b->y - a->y, b->z - a->z}; + float nDotAB = planeormal->x * ab.x + planeormal->y * ab.y + planeormal->z * ab.z; + + // If the segment is not parallel to the plane + if (fabsf(nDotAB) > parallelEpsilon) + { + t = (plane_displacement - (planeormal->x * a->x + planeormal->y * a->y + planeormal->z * a->z)) / nDotAB; + } + + return t; +} + +/* line_distanceToPoint +compute the distance between a point "point" and a line given by the points "linePointA" and "linePointB" */ +inline float line_distanceToPoint(const Vector3 *linePointA, const Vector3 *linePointB, const Vector3 *point) +{ + float distAB = sqrt((linePointB->x - linePointA->x) * (linePointB->x - linePointA->x) + + (linePointB->y - linePointA->y) * (linePointB->y - linePointA->y) + + (linePointB->z - linePointA->z) * (linePointB->z - linePointA->z)); + + if (distAB < 1e-6f) + { + return sqrt((point->x - linePointA->x) * (point->x - linePointA->x) + + (point->y - linePointA->y) * (point->y - linePointA->y) + + (point->z - linePointA->z) * (point->z - linePointA->z)); + } + + Vector3 crossProduct = {(point->y - linePointA->y) * (point->z - linePointB->z) - (point->z - linePointA->z) * (point->y - linePointB->y), + (point->z - linePointA->z) * (point->x - linePointB->x) - (point->x - linePointA->x) * (point->z - linePointB->z), + (point->x - linePointA->x) * (point->y - linePointB->y) - (point->y - linePointA->y) * (point->x - linePointB->x)}; + + float crossLength = sqrt(crossProduct.x * crossProduct.x + crossProduct.y * crossProduct.y + crossProduct.z * crossProduct.z); + + return crossLength / distAB; +} + +inline float segment_distanceToPoint(const Vector3 *a, const Vector3 *b, const Vector3 *p) +{ + // Calculate the vector from a to b and from a to p + Vector3 ab = vector3_difference(b, a); + Vector3 ap = vector3_difference(p, a); + + // Project the vector ap onto ab and clamp the value of t + float t = vector3_returnDotProduct(&ap, &ab) / vector3_returnDotProduct(&ab, &ab); + t = max2(0, min2(1, t)); // Clamp t to the range [0, 1] + + // Calculate the closest point on the segment + Vector3 scaled_ab = vector3_returnScaled(&ab, t); + Vector3 closestPoint = vector3_sum(a, &scaled_ab); + + // Calculate the distance from p to the closest point + Vector3 diff = vector3_difference(p, &closestPoint); + return vector3_magnitude(&diff); +} + +Matrix3x3 rotationMatrix_getFromEuler(const Vector3 *rotation) +{ + float rad_x = rad(rotation->x); + float cos_rad_x = fm_cosf(rad_x); + float sin_rad_x = fm_sinf(rad_x); + + float rad_y = rad(rotation->y); + float cos_rad_y = fm_cosf(rad_y); + float sin_rad_y = fm_sinf(rad_y); + + float rad_z = rad(rotation->z); + float cos_rad_z = fm_cosf(rad_z); + float sin_rad_z = fm_sinf(rad_z); + + Matrix3x3 R_x = { + .row = { + {1, 0, 0}, + {0, cos_rad_x, -sin_rad_x}, + {0, sin_rad_x, cos_rad_x}}}; + + Matrix3x3 R_y = { + .row = { + {cos_rad_y, 0, sin_rad_y}, + {0, 1, 0}, + {-sin_rad_y, 0, cos_rad_y}}}; + + Matrix3x3 R_z = { + .row = { + {cos_rad_z, -sin_rad_z, 0}, + {sin_rad_z, cos_rad_z, 0}, + {0, 0, 1}}}; + + Matrix3x3 R_temp = matrix3x3_multiply(&R_x, &R_y); + Matrix3x3 R = matrix3x3_multiply(&R_temp, &R_z); + + return R; +} + +// this function delivers an elegant simplification saving significant performance +// in comparison with the standard way to do it using matrix and vector operations; +// +// Matrix3x3 matrix = rotationMatrix_getFromEuler(rotation); +// point = matrix3x3_multiplyByVector(matrix, point); +// +// as with the rotation matrix algorithm, this does not work when the point lays on any axis +// for that you must use a quaternion rotation +void point_rotateZYX(Vector3 *point, const Vector3 *rotation) +{ + float rad_x = rad(rotation->x); + float cos_rad_x = fm_cosf(rad_x); + float sin_rad_x = fm_sinf(rad_x); + + float rad_y = rad(rotation->y); + float cos_rad_y = fm_cosf(rad_y); + float sin_rad_y = fm_sinf(rad_y); + + float rad_z = rad(rotation->z); + float cos_rad_z = fm_cosf(rad_z); + float sin_rad_z = fm_sinf(rad_z); + + // Rotate around Z axis + float xZ = point->x * cos_rad_z - point->y * sin_rad_z; + float yZ = point->x * sin_rad_z + point->y * cos_rad_z; + + // Rotate around Y axis + float xY = xZ * cos_rad_y + point->z * sin_rad_y; + float zY = -xZ * sin_rad_y + point->z * cos_rad_y; + + // Rotate around X axis + point->y = yZ * cos_rad_x - zY * sin_rad_x; + point->z = yZ * sin_rad_x + zY * cos_rad_x; + point->x = xY; +} + +void point_rotateXYZ(Vector3 *point, const Vector3 *rotation) +{ + float rad_x = rad(rotation->x); + float cos_rad_x = fm_cosf(rad_x); + float sin_rad_x = fm_sinf(rad_x); + + float rad_y = rad(rotation->y); + float cos_rad_y = fm_cosf(rad_y); + float sin_rad_y = fm_sinf(rad_y); + + float rad_z = rad(rotation->z); + float cos_rad_z = fm_cosf(rad_z); + float sin_rad_z = fm_sinf(rad_z); + + // Rotate around X axis (inverse order) + float yX = point->y * cos_rad_x + point->z * sin_rad_x; + float zX = -point->y * sin_rad_x + point->z * cos_rad_x; + + // Rotate around Y axis (inverse order) + float xY = point->x * cos_rad_y - zX * sin_rad_y; + float zY = point->x * sin_rad_y + zX * cos_rad_y; + + // Rotate around Z axis (inverse order) + float xZ = xY * cos_rad_z + yX * sin_rad_z; + float yZ = -xY * sin_rad_z + yX * cos_rad_z; + + point->x = xZ; + point->y = yZ; + point->z = zY; +} + +void point_transformToLocalSpace(Vector3 *global_point, const Vector3 *local_center, const Vector3 *local_rotation) +{ + // Translate point by the inverse of Box's center + vector3_subtract(global_point, local_center); + + // Rotate the translated point by the negative of Box's rotation + Vector3 inverse_rotation = vector3_getInverse(local_rotation); + point_rotateZYX(global_point, &inverse_rotation); +} + +void point_transformToGlobalSpace(Vector3 *local_point, const Vector3 *local_center, const Vector3 *local_rotation) +{ + // Apply rotation to the local point to get it in the global space orientation + Vector3 inverse_rotation = vector3_getInverse(local_rotation); + point_rotateXYZ(local_point, &inverse_rotation); + + // Translate the rotated point by adding the Box's center + vector3_add(local_point, local_center); +} + +// another very convenient and very difficult to figure out algorithm +void rotate_normal(Vector3 *vector, const Vector3 *rotation) +{ + Vector3 rad_rotation = vector3_degToRad(rotation); + Quaternion q_rotation = quaternion_getFromVector(&rad_rotation); + *vector = vector3_rotateByQuaternion(vector, &q_rotation); +} + +void rotate_vector(Vector3 *vector, const Vector3 *rotation) +{ + Vector3 rad_rotation = vector3_degToRad(rotation); + Quaternion q_rotation = quaternion_getFromVector(&rad_rotation); + *vector = vector3_rotateByQuaternion(vector, &q_rotation); +} + +inline Vector3 vector3_fromQuaternion(Quaternion q) +{ + Vector3 euler; + + // Roll (X-axis rotation) + float sinr_cosp = 2 * (q.w * q.x + q.y * q.z); + float cosr_cosp = 1 - 2 * (q.x * q.x + q.y * q.y); + euler.x = atan2f(sinr_cosp, cosr_cosp); + + // Pitch (Y-axis rotation) + float sinp = 2 * (q.w * q.y - q.z * q.x); + if (fabs(sinp) >= 1) + euler.y = copysignf(M_PI / 2, sinp); // Use 90 degrees if out of range + else + euler.y = asinf(sinp); + + // Yaw (Z-axis rotation) + float siny_cosp = 2 * (q.w * q.z + q.x * q.y); + float cosy_cosp = 1 - 2 * (q.y * q.y + q.z * q.z); + euler.z = atan2f(siny_cosp, cosy_cosp); + + return euler; +} + +#endif diff --git a/code/sb_hot/physics/math/matrix2x2.h b/code/sb_hot/physics/math/matrix2x2.h new file mode 100644 index 00000000..133b24a3 --- /dev/null +++ b/code/sb_hot/physics/math/matrix2x2.h @@ -0,0 +1,254 @@ +#ifndef MATRIX2X2_H +#define MATRIX2X2_H + +// Libraries +#include +#include +#include +#include "vector2.h" // Asegúrate de que esto incluya la definición de Vector2 + +typedef struct +{ + Vector2 row[2]; +} Matrix2x2; + +// Function prototypes +void matrix2x2_init(Matrix2x2 *matrix); +void matrix2x2_clear(Matrix2x2 *matrix); + +void matrix2x2_set(Matrix2x2 *matrix, float a1, float a2, float b1, float b2); +void matrix2x2_setWithValue(Matrix2x2 *matrix, float value); + +Vector2 matrix2x2_returnColumn(const Matrix2x2 *matrix, int i); +Vector2 matrix2x2_returnRow(const Matrix2x2 *matrix, int i); + +Matrix2x2 matrix2x2_sum(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2); +void matrix2x2_add(Matrix2x2 *matrix1, const Matrix2x2 *matrix2); + +Matrix2x2 matrix2x2_difference(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2); +void matrix2x2_subtract(Matrix2x2 *matrix1, const Matrix2x2 *matrix2); + +Matrix2x2 matrix2x2_returnScaled(const Matrix2x2 *matrix, float scalar); +void matrix2x2_scale(Matrix2x2 *matrix, float scalar); + +Matrix2x2 matrix2x2_returnProduct(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2); +Vector2 matrix2x2_returnProductByVector(const Matrix2x2 *matrix, const Vector2 *vector); + +Matrix2x2 matrix2x2_returnNegative(const Matrix2x2 *matrix); +Matrix2x2 matrix2x2_returnTranspose(const Matrix2x2 *matrix); +float matrix2x2_returnDeterminant(const Matrix2x2 *matrix); +float matrix2x2_returnTrace(const Matrix2x2 *matrix); +Matrix2x2 matrix2x2_returnInverse(const Matrix2x2 *matrix); +Matrix2x2 matrix2x2_returnAbsoluteMatrix(const Matrix2x2 *matrix); + +void matrix2x2_setIdentity(Matrix2x2 *matrix); +Matrix2x2 matrix2x2_returnIdentity(); + +int matrix2x2_equals(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2); +int matrix2x2_notEquals(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2); + +// Implementations + +/* Initializes all values in the matrix to zero. */ +void matrix2x2_init(Matrix2x2 *matrix) +{ + matrix2x2_set(matrix, 0.0f, 0.0f, 0.0f, 0.0f); +} + +/* Initializes the matrix with a given value. */ +void matrix2x2_setWithValue(Matrix2x2 *matrix, float value) +{ + matrix2x2_set(matrix, value, value, value, value); +} + +/* Sets all values in the matrix. */ +void matrix2x2_set(Matrix2x2 *matrix, float a1, float a2, float b1, float b2) +{ + matrix->row[0].x = a1; + matrix->row[0].y = a2; + matrix->row[1].x = b1; + matrix->row[1].y = b2; +} + +/* Sets the matrix to zero. */ +void matrix2x2_clear(Matrix2x2 *matrix) +{ + matrix2x2_set(matrix, 0.0f, 0.0f, 0.0f, 0.0f); +} + +/* Returns a column of the matrix. */ +Vector2 matrix2x2_returnColumn(const Matrix2x2 *matrix, int i) +{ + assert(i >= 0 && i < 2); + return (Vector2){matrix->row[0].x, matrix->row[1].x}; +} + +/* Returns a row of the matrix. */ +Vector2 matrix2x2_returnRow(const Matrix2x2 *matrix, int i) +{ + assert(i >= 0 && i < 2); + return matrix->row[i]; +} + +/* Returns the transpose of the matrix. */ +Matrix2x2 matrix2x2_returnTranspose(const Matrix2x2 *matrix) +{ + return (Matrix2x2){ + .row = { + {matrix->row[0].x, matrix->row[1].x}, + {matrix->row[0].y, matrix->row[1].y}}}; +} + +/* Returns the determinant of the matrix. */ +float matrix2x2_returnDeterminant(const Matrix2x2 *matrix) +{ + return matrix->row[0].x * matrix->row[1].y - matrix->row[1].x * matrix->row[0].y; +} + +/* Returns the trace of the matrix. */ +float matrix2x2_returnTrace(const Matrix2x2 *matrix) +{ + return matrix->row[0].x + matrix->row[1].y; +} + +/* Sets the matrix to the identity matrix. */ +void matrix2x2_setIdentity(Matrix2x2 *matrix) +{ + matrix2x2_set(matrix, 1.0f, 0.0f, 0.0f, 1.0f); +} + +/* Returns the 2x2 identity matrix. */ +Matrix2x2 matrix2x2_returnIdentity() +{ + Matrix2x2 identityMatrix; + matrix2x2_setIdentity(&identityMatrix); + return identityMatrix; +} + +/* Returns the 2x2 zero matrix. */ +Matrix2x2 matrix2x2_zero() +{ + Matrix2x2 zeroMatrix; + matrix2x2_clear(&zeroMatrix); + return zeroMatrix; +} + +/* Returns the inverse of the matrix. */ +Matrix2x2 matrix2x2_returnInverse(const Matrix2x2 *matrix) +{ + float determinant = matrix2x2_returnDeterminant(matrix); + assert(determinant > FLT_EPSILON); + + float invDeterminant = 1.0f / determinant; + + return (Matrix2x2){ + .row = { + {matrix->row[1].y * invDeterminant, -matrix->row[0].y * invDeterminant}, + {-matrix->row[1].x * invDeterminant, matrix->row[0].x * invDeterminant}}}; +} + +/* Returns the matrix with absolute values. */ +Matrix2x2 matrix2x2_returnAbsoluteMatrix(const Matrix2x2 *matrix) +{ + return (Matrix2x2){ + .row = { + {fabsf(matrix->row[0].x), fabsf(matrix->row[0].y)}, + {fabsf(matrix->row[1].x), fabsf(matrix->row[1].y)}}}; +} + +/* Adds two matrices. */ +Matrix2x2 matrix2x2_sum(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + return (Matrix2x2){ + .row = { + {matrix1->row[0].x + matrix2->row[0].x, matrix1->row[0].y + matrix2->row[0].y}, + {matrix1->row[1].x + matrix2->row[1].x, matrix1->row[1].y + matrix2->row[1].y}}}; +} + +/* Subtracts matrix2 from matrix1. */ +Matrix2x2 matrix2x2_difference(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + return (Matrix2x2){ + .row = { + {matrix1->row[0].x - matrix2->row[0].x, matrix1->row[0].y - matrix2->row[0].y}, + {matrix1->row[1].x - matrix2->row[1].x, matrix1->row[1].y - matrix2->row[1].y}}}; +} + +/* Returns the negative of the matrix. */ +Matrix2x2 matrix2x2_returnNegative(const Matrix2x2 *matrix) +{ + return (Matrix2x2){ + .row = { + {-matrix->row[0].x, -matrix->row[0].y}, + {-matrix->row[1].x, -matrix->row[1].y}}}; +} + +/* Multiplies the matrix by a scalar. */ +Matrix2x2 matrix2x2_returnScaled(const Matrix2x2 *matrix, float scalar) +{ + return (Matrix2x2){ + .row = { + {matrix->row[0].x * scalar, matrix->row[0].y * scalar}, + {matrix->row[1].x * scalar, matrix->row[1].y * scalar}}}; +} + +/* Multiplies two matrices. */ +Matrix2x2 matrix2x2_returnProduct(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + return (Matrix2x2){ + .row = { + {matrix1->row[0].x * matrix2->row[0].x + matrix1->row[0].y * matrix2->row[1].x, + matrix1->row[0].x * matrix2->row[0].y + matrix1->row[0].y * matrix2->row[1].y}, + {matrix1->row[1].x * matrix2->row[0].x + matrix1->row[1].y * matrix2->row[1].x, + matrix1->row[1].x * matrix2->row[0].y + matrix1->row[1].y * matrix2->row[1].y}}}; +} + +/* Multiplies the matrix by a vector. */ +Vector2 matrix2x2_returnProductByVector(const Matrix2x2 *matrix, const Vector2 *vector) +{ + return (Vector2){ + .x = matrix->row[0].x * vector->x + matrix->row[0].y * vector->y, + .y = matrix->row[1].x * vector->x + matrix->row[1].y * vector->y}; +} + +/* Checks if two matrices are equal. */ +int matrix2x2_equals(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + return (matrix1->row[0].x == matrix2->row[0].x && matrix1->row[0].y == matrix2->row[0].y && + matrix1->row[1].x == matrix2->row[1].x && matrix1->row[1].y == matrix2->row[1].y); +} + +/* Checks if two matrices are not equal. */ +int matrix2x2_notEquals(const Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + return !matrix2x2_equals(matrix1, matrix2); +} + +/* Adds matrix2 to matrix1 and assigns the result to matrix1. */ +void matrix2x2_add(Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + matrix1->row[0].x += matrix2->row[0].x; + matrix1->row[0].y += matrix2->row[0].y; + matrix1->row[1].x += matrix2->row[1].x; + matrix1->row[1].y += matrix2->row[1].y; +} + +/* Subtracts matrix2 from matrix1 and assigns the result to matrix1. */ +void matrix2x2_subtract(Matrix2x2 *matrix1, const Matrix2x2 *matrix2) +{ + matrix1->row[0].x -= matrix2->row[0].x; + matrix1->row[0].y -= matrix2->row[0].y; + matrix1->row[1].x -= matrix2->row[1].x; + matrix1->row[1].y -= matrix2->row[1].y; +} + +/* Multiplies matrix1 by a scalar and assigns the result to matrix1. */ +void matrix2x2_scale(Matrix2x2 *matrix, float scalar) +{ + matrix->row[0].x *= scalar; + matrix->row[0].y *= scalar; + matrix->row[1].x *= scalar; + matrix->row[1].y *= scalar; +} + +#endif // MATRIX2X2_H diff --git a/code/sb_hot/physics/math/matrix3x3.h b/code/sb_hot/physics/math/matrix3x3.h new file mode 100644 index 00000000..c8a09f7b --- /dev/null +++ b/code/sb_hot/physics/math/matrix3x3.h @@ -0,0 +1,312 @@ +/** + * @file + * + * holds a 3x3 matrix. + */ + +#ifndef MATRIX3X3_H +#define MATRIX3X3_H + +#include "vector3.h" + +typedef struct +{ + Vector3 row[3]; +} Matrix3x3; + +// Function prototypes +void matrix3x3_init(Matrix3x3 *matrix); +void matrix3x3_clear(Matrix3x3 *matrix); + +void matrix3x3_set(Matrix3x3 *matrix, float a1, float a2, float a3, float b1, float b2, float b3, float c1, float c2, float c3); +void matrix3x3_setWithValue(Matrix3x3 *matrix, float value); + +Vector3 matrix3x3_returnColumn(const Matrix3x3 *matrix, int i); +Vector3 matrix3x3_returnRow(const Matrix3x3 *matrix, int i); + +void matrix3x3_add(Matrix3x3 *matrix1, const Matrix3x3 *matrix2); +Matrix3x3 matrix3x3_sum(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2); + +void matrix3x3_subtract(Matrix3x3 *matrix1, const Matrix3x3 *matrix2); +Matrix3x3 matrix3x3_difference(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2); + +Matrix3x3 matrix3x3_returnScaled(const Matrix3x3 *matrix, float scalar); +void matrix3x3_scale(Matrix3x3 *matrix, float scalar); + +Matrix3x3 matrix3x3_multiply(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2); +Vector3 matrix3x3_multiplyByVector(const Matrix3x3 *matrix, const Vector3 *vector); + +Matrix3x3 matrix3x3_returnNegative(const Matrix3x3 *matrix); +Matrix3x3 matrix3x3_returnTranspose(const Matrix3x3 *matrix); +float matrix3x3_returnDeterminant(const Matrix3x3 *matrix); +float matrix3x3_returnTrace(const Matrix3x3 *matrix); +Matrix3x3 matrix3x3_returnInverse(const Matrix3x3 *matrix); +Matrix3x3 matrix3x3_returnAbsoluteMatrix(const Matrix3x3 *matrix); + +void matrix3x3_setIdentity(Matrix3x3 *matrix); +Matrix3x3 matrix3x3_returnIdentity(); + +Matrix3x3 matrix3x3_computeSkewSymmetricMatrixForCrossProduct(const Vector3 *vector); + +int matrix3x3_equals(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2); +int matrix3x3_notEquals(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2); + +// Implementations + +/* Initializes the matrix to zero. */ +void matrix3x3_init(Matrix3x3 *matrix) +{ + matrix3x3_set(matrix, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); +} + +/* Initializes the matrix with a given value. */ +void matrix3x3_setWithValue(Matrix3x3 *matrix, float value) +{ + matrix3x3_set(matrix, value, value, value, value, value, value, value, value, value); +} + +/* Sets all values in the matrix. */ +void matrix3x3_set(Matrix3x3 *matrix, float a1, float a2, float a3, + float b1, float b2, float b3, float c1, float c2, float c3) +{ + matrix->row[0].x = a1; + matrix->row[0].y = a2; + matrix->row[0].z = a3; + matrix->row[1].x = b1; + matrix->row[1].y = b2; + matrix->row[1].z = b3; + matrix->row[2].x = c1; + matrix->row[2].y = c2; + matrix->row[2].z = c3; +} + +/* Sets the matrix to zero. */ +void matrix3x3_clear(Matrix3x3 *matrix) +{ + vector3_set(&matrix->row[0], 0.0f, 0.0f, 0.0f); + vector3_set(&matrix->row[1], 0.0f, 0.0f, 0.0f); + vector3_set(&matrix->row[2], 0.0f, 0.0f, 0.0f); +} + +/* Returns a column of the matrix. */ +Vector3 matrix3x3_returnColumn(const Matrix3x3 *matrix, int i) +{ + assert(i >= 0 && i < 3); + return (Vector3){matrix->row[0].x, matrix->row[1].x, matrix->row[2].x}; +} + +/* Returns a row of the matrix. */ +Vector3 matrix3x3_returnRow(const Matrix3x3 *matrix, int i) +{ + assert(i >= 0 && i < 3); + return matrix->row[i]; +} + +/* Returns the transpose of the matrix. */ +Matrix3x3 matrix3x3_returnTranspose(const Matrix3x3 *matrix) +{ + return (Matrix3x3){ + .row = { + {matrix->row[0].x, matrix->row[1].x, matrix->row[2].x}, + {matrix->row[0].y, matrix->row[1].y, matrix->row[2].y}, + {matrix->row[0].z, matrix->row[1].z, matrix->row[2].z}}}; +} + +/* Returns the determinant of the matrix. */ +float matrix3x3_returnDeterminant(const Matrix3x3 *matrix) +{ + return (matrix->row[0].x * (matrix->row[1].y * matrix->row[2].z - matrix->row[2].y * matrix->row[1].z) - + matrix->row[0].y * (matrix->row[1].x * matrix->row[2].z - matrix->row[2].x * matrix->row[1].z) + + matrix->row[0].z * (matrix->row[1].x * matrix->row[2].y - matrix->row[2].x * matrix->row[1].y)); +} + +/* Returns the trace of the matrix. */ +float matrix3x3_returnTrace(const Matrix3x3 *matrix) +{ + return (matrix->row[0].x + matrix->row[1].y + matrix->row[2].z); +} + +/* Returns the inverse of the matrix. */ +Matrix3x3 matrix3x3_returnInverse(const Matrix3x3 *matrix) +{ + float determinant = matrix3x3_returnDeterminant(matrix); + // Check if the determinant is equal to zero + assert(determinant != 0.0f); + + float invDeterminant = 1.0f / determinant; + + Matrix3x3 tempMatrix; + matrix3x3_set(&tempMatrix, + (matrix->row[1].y * matrix->row[2].z - matrix->row[2].y * matrix->row[1].z), + -(matrix->row[0].y * matrix->row[2].z - matrix->row[2].y * matrix->row[0].z), + (matrix->row[0].y * matrix->row[1].z - matrix->row[0].z * matrix->row[1].y), + -(matrix->row[1].x * matrix->row[2].z - matrix->row[2].x * matrix->row[1].z), + (matrix->row[0].x * matrix->row[2].z - matrix->row[2].x * matrix->row[0].z), + -(matrix->row[0].x * matrix->row[1].z - matrix->row[1].x * matrix->row[0].z), + (matrix->row[1].x * matrix->row[2].y - matrix->row[2].x * matrix->row[1].y), + -(matrix->row[0].x * matrix->row[2].y - matrix->row[2].x * matrix->row[0].y), + (matrix->row[0].x * matrix->row[1].y - matrix->row[0].y * matrix->row[1].x)); + + // Return the inverse matrix + return matrix3x3_returnScaled(&tempMatrix, invDeterminant); +} + +/* Returns the matrix with absolute values. */ +Matrix3x3 matrix3x3_returnAbsoluteMatrix(const Matrix3x3 *matrix) +{ + return (Matrix3x3){ + .row = { + {fabsf(matrix->row[0].x), fabsf(matrix->row[0].y), fabsf(matrix->row[0].z)}, + {fabsf(matrix->row[1].x), fabsf(matrix->row[1].y), fabsf(matrix->row[1].z)}, + {fabsf(matrix->row[2].x), fabsf(matrix->row[2].y), fabsf(matrix->row[2].z)}}}; +} + +/* Sets the matrix to the identity matrix. */ +void matrix3x3_setIdentity(Matrix3x3 *matrix) +{ + matrix3x3_set(matrix, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f); +} + +/* Returns the 3x3 identity matrix. */ +Matrix3x3 matrix3x3_returnIdentity() +{ + Matrix3x3 identityMatrix; + matrix3x3_setIdentity(&identityMatrix); + return identityMatrix; +} + +/* Returns a skew-symmetric matrix for cross product. */ +Matrix3x3 matrix3x3_computeSkewSymmetricMatrixForCrossProduct(const Vector3 *vector) +{ + return (Matrix3x3){ + .row = { + {0.0f, -vector->z, vector->y}, + {vector->z, 0.0f, -vector->x}, + {-vector->y, vector->x, 0.0f}}}; +} + +/* Adds two matrices. */ +Matrix3x3 matrix3x3_sum(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + return (Matrix3x3){ + .row = { + {matrix1->row[0].x + matrix2->row[0].x, matrix1->row[0].y + matrix2->row[0].y, matrix1->row[0].z + matrix2->row[0].z}, + {matrix1->row[1].x + matrix2->row[1].x, matrix1->row[1].y + matrix2->row[1].y, matrix1->row[1].z + matrix2->row[1].z}, + {matrix1->row[2].x + matrix2->row[2].x, matrix1->row[2].y + matrix2->row[2].y, matrix1->row[2].z + matrix2->row[2].z}}}; +} + +/* Subtracts matrix2 from matrix1. */ +Matrix3x3 matrix3x3_difference(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + return (Matrix3x3){ + .row = { + {matrix1->row[0].x - matrix2->row[0].x, matrix1->row[0].y - matrix2->row[0].y, matrix1->row[0].z - matrix2->row[0].z}, + {matrix1->row[1].x - matrix2->row[1].x, matrix1->row[1].y - matrix2->row[1].y, matrix1->row[1].z - matrix2->row[1].z}, + {matrix1->row[2].x - matrix2->row[2].x, matrix1->row[2].y - matrix2->row[2].y, matrix1->row[2].z - matrix2->row[2].z}}}; +} + +/* Returns the negative of the matrix. */ +Matrix3x3 matrix3x3_returnNegative(const Matrix3x3 *matrix) +{ + return (Matrix3x3){ + .row = { + {-matrix->row[0].x, -matrix->row[0].y, -matrix->row[0].z}, + {-matrix->row[1].x, -matrix->row[1].y, -matrix->row[1].z}, + {-matrix->row[2].x, -matrix->row[2].y, -matrix->row[2].z}}}; +} + +/* Multiplies the matrix by a scalar. */ +Matrix3x3 matrix3x3_returnScaled(const Matrix3x3 *matrix, float scalar) +{ + return (Matrix3x3){ + .row = { + {matrix->row[0].x * scalar, matrix->row[0].y * scalar, matrix->row[0].z * scalar}, + {matrix->row[1].x * scalar, matrix->row[1].y * scalar, matrix->row[1].z * scalar}, + {matrix->row[2].x * scalar, matrix->row[2].y * scalar, matrix->row[2].z * scalar}}}; +} + +/* Multiplies two matrices. */ +Matrix3x3 matrix3x3_multiply(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + Matrix3x3 result; + result.row[0].x = matrix1->row[0].x * matrix2->row[0].x + matrix1->row[0].y * matrix2->row[1].x + matrix1->row[0].z * matrix2->row[2].x; + result.row[0].y = matrix1->row[0].x * matrix2->row[0].y + matrix1->row[0].y * matrix2->row[1].y + matrix1->row[0].z * matrix2->row[2].y; + result.row[0].z = matrix1->row[0].x * matrix2->row[0].z + matrix1->row[0].y * matrix2->row[1].z + matrix1->row[0].z * matrix2->row[2].z; + + result.row[1].x = matrix1->row[1].x * matrix2->row[0].x + matrix1->row[1].y * matrix2->row[1].x + matrix1->row[1].z * matrix2->row[2].x; + result.row[1].y = matrix1->row[1].x * matrix2->row[0].y + matrix1->row[1].y * matrix2->row[1].y + matrix1->row[1].z * matrix2->row[2].y; + result.row[1].z = matrix1->row[1].x * matrix2->row[0].z + matrix1->row[1].y * matrix2->row[1].z + matrix1->row[1].z * matrix2->row[2].z; + + result.row[2].x = matrix1->row[2].x * matrix2->row[0].x + matrix1->row[2].y * matrix2->row[1].x + matrix1->row[2].z * matrix2->row[2].x; + result.row[2].y = matrix1->row[2].x * matrix2->row[0].y + matrix1->row[2].y * matrix2->row[1].y + matrix1->row[2].z * matrix2->row[2].y; + result.row[2].z = matrix1->row[2].x * matrix2->row[0].z + matrix1->row[2].y * matrix2->row[1].z + matrix1->row[2].z * matrix2->row[2].z; + + return result; +} + +/* Multiplies the matrix by a vector. */ +Vector3 matrix3x3_multiplyByVector(const Matrix3x3 *matrix, const Vector3 *vector) +{ + return (Vector3){ + .x = matrix->row[0].x * vector->x + matrix->row[0].y * vector->y + matrix->row[0].z * vector->z, + .y = matrix->row[1].x * vector->x + matrix->row[1].y * vector->y + matrix->row[1].z * vector->z, + .z = matrix->row[2].x * vector->x + matrix->row[2].y * vector->y + matrix->row[2].z * vector->z}; +} + +/* Checks if two matrices are equal. */ +int matrix3x3_equals(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + return (matrix1->row[0].x == matrix2->row[0].x && matrix1->row[0].y == matrix2->row[0].y && matrix1->row[0].z == matrix2->row[0].z && + matrix1->row[1].x == matrix2->row[1].x && matrix1->row[1].y == matrix2->row[1].y && matrix1->row[1].z == matrix2->row[1].z && + matrix1->row[2].x == matrix2->row[2].x && matrix1->row[2].y == matrix2->row[2].y && matrix1->row[2].z == matrix2->row[2].z); +} + +/* Checks if two matrices are not equal. */ +int matrix3x3_notEquals(const Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + return !matrix3x3_equals(matrix1, matrix2); +} + +/* Adds matrix2 to matrix1 and assigns the result to matrix1. */ +void matrix3x3_add(Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + matrix1->row[0].x += matrix2->row[0].x; + matrix1->row[0].y += matrix2->row[0].y; + matrix1->row[0].z += matrix2->row[0].z; + matrix1->row[1].x += matrix2->row[1].x; + matrix1->row[1].y += matrix2->row[1].y; + matrix1->row[1].z += matrix2->row[1].z; + matrix1->row[2].x += matrix2->row[2].x; + matrix1->row[2].y += matrix2->row[2].y; + matrix1->row[2].z += matrix2->row[2].z; +} + +/* Subtracts matrix2 from matrix1 and assigns the result to matrix1. */ +void matrix3x3_subtract(Matrix3x3 *matrix1, const Matrix3x3 *matrix2) +{ + matrix1->row[0].x -= matrix2->row[0].x; + matrix1->row[0].y -= matrix2->row[0].y; + matrix1->row[0].z -= matrix2->row[0].z; + matrix1->row[1].x -= matrix2->row[1].x; + matrix1->row[1].y -= matrix2->row[1].y; + matrix1->row[1].z -= matrix2->row[1].z; + matrix1->row[2].x -= matrix2->row[2].x; + matrix1->row[2].y -= matrix2->row[2].y; + matrix1->row[2].z -= matrix2->row[2].z; +} + +/* Multiplies matrix1 by a scalar and assigns the result to matrix1. */ +void matrix3x3_scale(Matrix3x3 *matrix, float scalar) +{ + matrix->row[0].x *= scalar; + matrix->row[0].y *= scalar; + matrix->row[0].z *= scalar; + matrix->row[1].x *= scalar; + matrix->row[1].y *= scalar; + matrix->row[1].z *= scalar; + matrix->row[2].x *= scalar; + matrix->row[2].y *= scalar; + matrix->row[2].z *= scalar; +} + +#endif // MATRIX3X3_H diff --git a/code/sb_hot/physics/math/physics_math.h b/code/sb_hot/physics/math/physics_math.h new file mode 100644 index 00000000..299f295e --- /dev/null +++ b/code/sb_hot/physics/math/physics_math.h @@ -0,0 +1,23 @@ +#ifndef PHYSICS_MATH_H +#define PHYSICS_MATH_H + +// Libraries + +#include +#include + +#include "math_common.h" + +#include "vector2.h" +#include "vector3.h" + +#include "matrix3x3.h" +#include "matrix2x2.h" + +#include "quaternion.h" + +#include "transform.h" + +#include "math_functions.h" + +#endif \ No newline at end of file diff --git a/code/sb_hot/physics/math/quaternion.h b/code/sb_hot/physics/math/quaternion.h new file mode 100644 index 00000000..ffd13789 --- /dev/null +++ b/code/sb_hot/physics/math/quaternion.h @@ -0,0 +1,473 @@ +/** + * @file + * + * Holds a quaternion. + */ + +#ifndef QUATERNION_H +#define QUATERNION_H + +typedef struct +{ + float x; + float y; + float z; + float w; +} Quaternion; + +// Function prototypes +void quaternion_set(Quaternion *q, float x, float y, float z, float w); +void quaternion_init(Quaternion *q); +void quaternion_clear(Quaternion *q); + +void quaternion_setWithVector(Quaternion *q, float w, const Vector3 *v); + +Quaternion quaternion_sum(const Quaternion *q, const Quaternion *r); +Quaternion quaternion_difference(const Quaternion *q, const Quaternion *r); +Quaternion quaternion_returnScaled(const Quaternion *q, float scalar); +Quaternion quaternion_returnProduct(const Quaternion *q, const Quaternion *r); +Vector3 quaternion_getVectorProduct(const Quaternion *q, const Vector3 *vector); + +void quaternion_setIdentity(Quaternion *q); +Vector3 quaternion_returnVectorV(const Quaternion *q); +float quaternion_magnitude(const Quaternion *q); +float quaternion_squaredMagnitude(const Quaternion *q); +void quaternion_normalize(Quaternion *q); +void quaternion_invert(Quaternion *q); + +Quaternion quaternion_returnUnit(const Quaternion *q); + +Quaternion quaternion_getConjugate(const Quaternion *q); +Quaternion quaternion_getInverse(const Quaternion *q); +float quaternion_dotProduct(const Quaternion *q, const Quaternion *r); + +bool quaternion_isFinite(const Quaternion *q); +bool quaternion_isUnit(const Quaternion *q); +bool quaternion_isValid(const Quaternion *q); +bool quaternion_equals(const Quaternion *q, const Quaternion *r); + +void quaternion_setFromEulerAngles(Quaternion *quaternion, float angleX, float angleY, float angleZ); +Quaternion quaternion_getFromEulerAngles(float angleX, float angleY, float angleZ); +Quaternion quaternion_getFromVector(const Vector3 *rotation); +Quaternion quaternion_getFromMatrix(const Matrix3x3 *matrix); + +void quaternion_setRotationAngleAxis(Quaternion *quaternion, float *angle, Vector3 *axis); +Matrix3x3 quaternion_getMatrix(const Quaternion *quaternion); + +Quaternion quaternion_slerp(const Quaternion *q, const Quaternion *r, float t); + +Quaternion quat_from_array(float arr[4]); + +// Implementations + +/* Initializes the quaternion to zero. */ +void quaternion_init(Quaternion *q) +{ + quaternion_set(q, 0.0f, 0.0f, 0.0f, 0.0f); +} + +/* Initializes the quaternion with the component w and the vector v=(x y z). */ +void quaternion_setWithVector(Quaternion *q, float w, const Vector3 *v) +{ + quaternion_set(q, v->x, v->y, v->z, w); +} + +/* Sets all values in the quaternion. */ +void quaternion_set(Quaternion *q, float x, float y, float z, float w) +{ + q->x = x; + q->y = y; + q->z = z; + q->w = w; +} + +/* Sets the quaternion to zero. */ +void quaternion_clear(Quaternion *q) +{ + quaternion_set(q, 0.0f, 0.0f, 0.0f, 0.0f); +} + +/* Sets the quaternion to the identity quaternion. */ +void quaternion_setIdentity(Quaternion *q) +{ + quaternion_set(q, 0.0f, 0.0f, 0.0f, 1.0f); +} + +/* Returns the vector v=(x y z) of the quaternion. */ +Vector3 quaternion_returnVectorV(const Quaternion *q) +{ + return (Vector3){q->x, q->y, q->z}; +} + +/* Returns the length of the quaternion. */ +float quaternion_magnitude(const Quaternion *q) +{ + return sqrtf(q->x * q->x + q->y * q->y + q->z * q->z + q->w * q->w); +} + +/* Returns the square of the length of the quaternion. */ +float quaternion_squaredMagnitude(const Quaternion *q) +{ + return q->x * q->x + q->y * q->y + q->z * q->z + q->w * q->w; +} + +/* Normalizes the quaternion. */ +void quaternion_normalize(Quaternion *q) +{ + float l = quaternion_magnitude(q); + assert(l > TOLERANCE); + q->x /= l; + q->y /= l; + q->z /= l; + q->w /= l; +} + +/* Inverses the quaternion. */ +void quaternion_invert(Quaternion *q) +{ + q->x = -q->x; + q->y = -q->y; + q->z = -q->z; +} + +/* Returns the unit quaternion. */ +Quaternion quaternion_returnUnit(const Quaternion *q) +{ + float ql = quaternion_magnitude(q); + assert(ql > TOLERANCE); + return (Quaternion){q->x / ql, q->y / ql, q->z / ql, q->w / ql}; +} + +/* Returns the identity quaternion. */ +Quaternion quaternion_identity() +{ + return (Quaternion){0.0f, 0.0f, 0.0f, 1.0f}; +} + +/* Returns the conjugate of the quaternion. */ +Quaternion quaternion_getConjugate(const Quaternion *q) +{ + return (Quaternion){-q->x, -q->y, -q->z, q->w}; +} + +/* Returns the inverse of the quaternion. */ +Quaternion quaternion_getInverse(const Quaternion *q) +{ + return quaternion_getConjugate(q); +} + +/* Computes the dot product between two quaternions. */ +float quaternion_dotProduct(const Quaternion *q, const Quaternion *r) +{ + return q->x * r->x + q->y * r->y + q->z * r->z + q->w * r->w; +} + +/* Checks if the quaternion's values are finite. */ +bool quaternion_isFinite(const Quaternion *q) +{ + return isfinite(q->x) && isfinite(q->y) && isfinite(q->z) && isfinite(q->w); +} + +/* Checks if the quaternion is a unit quaternion. */ +bool quaternion_isUnit(const Quaternion *q) +{ + float length = quaternion_magnitude(q); + return fabsf(length - 1.0f) < 1e-5f; +} + +/* Checks if the quaternion is valid. */ +bool quaternion_isValid(const Quaternion *q) +{ + return quaternion_isFinite(q) && quaternion_isUnit(q); +} + +/* Adds two quaternions. */ +Quaternion quaternion_sum(const Quaternion *q, const Quaternion *r) +{ + return (Quaternion){q->x + r->x, q->y + r->y, q->z + r->z, q->w + r->w}; +} + +/* Subtracts r from q. */ +Quaternion quaternion_difference(const Quaternion *q, const Quaternion *r) +{ + return (Quaternion){q->x - r->x, q->y - r->y, q->z - r->z, q->w - r->w}; +} + +/* Multiplies the quaternion by a scalar. */ +Quaternion quaternion_returnScaled(const Quaternion *q, float scalar) +{ + return (Quaternion){q->x * scalar, q->y * scalar, q->z * scalar, q->w * scalar}; +} + +/* Multiplies two quaternions. */ +Quaternion quaternion_returnProduct(const Quaternion *q, const Quaternion *r) +{ + return (Quaternion){ + q->w * r->x + r->w * q->x + q->y * r->z - q->z * r->y, + q->w * r->y + r->w * q->y + q->z * r->x - q->x * r->z, + q->w * r->z + r->w * q->z + q->x * r->y - q->y * r->x, + q->w * r->w - q->x * r->x - q->y * r->y - q->z * r->z}; +} + +/* Multiplies the quaternion by a vector. */ +Vector3 quaternion_getVectorProduct(const Quaternion *q, const Vector3 *vector) +{ + float prodX = q->w * vector->x + q->y * vector->z - q->z * vector->y; + float prodY = q->w * vector->y + q->z * vector->x - q->x * vector->z; + float prodZ = q->w * vector->z + q->x * vector->y - q->y * vector->x; + float prodW = -q->x * vector->x - q->y * vector->y - q->z * vector->z; + + return (Vector3){ + q->w * prodX - prodY * q->z + prodZ * q->y - prodW * q->x, + q->w * prodY - prodZ * q->x + prodX * q->z - prodW * q->y, + q->w * prodZ - prodX * q->y + prodY * q->x - prodW * q->z}; +} + +/* Checks if two quaternions are equal. */ +bool quaternion_equals(const Quaternion *q, const Quaternion *r) +{ + return (q->x == r->x && q->y == r->y && q->z == r->z && q->w == r->w); +} + +/* Initializes the quaternion using Euler angles. */ +void quaternion_setFromEulerAngles(Quaternion *quaternion, float angleX, float angleY, float angleZ) +{ + float angle = angleX * 0.5f; + float sinX = fm_sinf(angle); + float cosX = fm_cosf(angle); + + angle = angleY * 0.5f; + float sinY = fm_sinf(angle); + float cosY = fm_cosf(angle); + + angle = angleZ * 0.5f; + float sinZ = fm_sinf(angle); + float cosZ = fm_cosf(angle); + + float cosYcosZ = cosY * cosZ; + float sinYcosZ = sinY * cosZ; + float cosYsinZ = cosY * sinZ; + float sinYsinZ = sinY * sinZ; + + quaternion->x = sinX * cosYcosZ - cosX * sinYsinZ; + quaternion->y = cosX * sinYcosZ + sinX * cosYsinZ; + quaternion->z = cosX * cosYsinZ - sinX * sinYcosZ; + quaternion->w = cosX * cosYcosZ + sinX * sinYsinZ; + + /* Normalize the quaternion */ + // quaternion_normalize(quaternion); +} + +/* Returns a quaternion constructed from Euler angles (in radians). */ +Quaternion quaternion_getFromEulerAngles(float angleX, float angleY, float angleZ) +{ + Quaternion quaternion; + quaternion_setFromEulerAngles(&quaternion, angleX, angleY, angleZ); + return quaternion; +} + +/* Returns a quaternion constructed from Euler angles (in radians). */ +Quaternion quaternion_getFromVector(const Vector3 *rotation) +{ + Quaternion quaternion; + quaternion_setFromEulerAngles(&quaternion, rotation->x, rotation->y, rotation->z); + return quaternion; +} + +/* Creates a unit quaternion from a rotation matrix. */ +Quaternion quaternion_getFromMatrix(const Matrix3x3 *matrix) +{ + Quaternion quaternion; + + /* Get the trace of the matrix */ + float trace = matrix->row[0].x + matrix->row[1].y + matrix->row[2].z; + + float r; + float s; + + if (trace < 0.0f) + { + if (matrix->row[1].y > matrix->row[0].x) + { + if (matrix->row[2].z > matrix->row[1].y) + { + r = sqrtf(matrix->row[2].z - matrix->row[0].x - matrix->row[1].y + 1.0f); + s = 0.5f / r; + + /* Compute the quaternion */ + quaternion.x = (matrix->row[2].x + matrix->row[0].z) * s; + quaternion.y = (matrix->row[1].z + matrix->row[2].y) * s; + quaternion.z = 0.5f * r; + quaternion.w = (matrix->row[1].x - matrix->row[0].y) * s; + } + else + { + r = sqrtf(matrix->row[1].y - matrix->row[2].z - matrix->row[0].x + 1.0f); + s = 0.5f / r; + + /* Compute the quaternion */ + quaternion.x = (matrix->row[0].y + matrix->row[1].x) * s; + quaternion.y = 0.5f * r; + quaternion.z = (matrix->row[1].z + matrix->row[2].y) * s; + quaternion.w = (matrix->row[0].z - matrix->row[2].x) * s; + } + } + else if (matrix->row[2].z > matrix->row[0].x) + { + r = sqrtf(matrix->row[2].z - matrix->row[0].x - matrix->row[1].y + 1.0f); + s = 0.5f / r; + + /* Compute the quaternion */ + quaternion.x = (matrix->row[2].x + matrix->row[0].z) * s; + quaternion.y = (matrix->row[1].z + matrix->row[2].y) * s; + quaternion.z = 0.5f * r; + quaternion.w = (matrix->row[1].x - matrix->row[0].y) * s; + } + else + { + r = sqrtf(matrix->row[0].x - matrix->row[1].y - matrix->row[2].z + 1.0f); + s = 0.5f / r; + + /* Compute the quaternion */ + quaternion.x = 0.5f * r; + quaternion.y = (matrix->row[0].y + matrix->row[1].x) * s; + quaternion.z = (matrix->row[2].x + matrix->row[0].z) * s; + quaternion.w = (matrix->row[2].y - matrix->row[1].z) * s; + } + } + else + { + r = sqrtf(trace + 1.0f); + s = 0.5f / r; + + /* Compute the quaternion */ + quaternion.x = (matrix->row[2].y - matrix->row[1].z) * s; + quaternion.y = (matrix->row[0].z - matrix->row[2].x) * s; + quaternion.z = (matrix->row[1].x - matrix->row[0].y) * s; + quaternion.w = 0.5f * r; + } + + /* Normalize the quaternion to ensure it represents a valid rotation */ + quaternion_normalize(&quaternion); + return quaternion; +} + +/* Computes the rotation angle (in radians) and the rotation axis. */ +void quaternion_setRotationAngleAxis(Quaternion *quaternion, float *angle, Vector3 *axis) +{ + /* Compute the rotation angle */ + *angle = acosf(quaternion->w) * 2.0f; + + /* Compute the 3D rotation axis */ + Vector3 rotationAxis = {quaternion->x, quaternion->y, quaternion->z}; + + /* Normalize the rotation axis */ + vector3_normalize(&rotationAxis); + + /* Set the rotation axis values */ + vector3_set(axis, rotationAxis.x, rotationAxis.y, rotationAxis.z); +} + +/* Returns the orientation matrix corresponding to this quaternion. */ +Matrix3x3 quaternion_getMatrix(const Quaternion *quaternion) +{ + float nQ = quaternion->x * quaternion->x + quaternion->y * quaternion->y + quaternion->z * quaternion->z + quaternion->w * quaternion->w; + float s = 0.0f; + + if (nQ > 0.0f) + { + s = 2.0f / nQ; + } + + /* Computations used for optimization (less multiplications) */ + float xs = quaternion->x * s; + float ys = quaternion->y * s; + float zs = quaternion->z * s; + float wxs = quaternion->w * xs; + float wys = quaternion->w * ys; + float wzs = quaternion->w * zs; + float xxs = quaternion->x * xs; + float xys = quaternion->x * ys; + float xzs = quaternion->x * zs; + float yys = quaternion->y * ys; + float yzs = quaternion->y * zs; + float zzs = quaternion->z * zs; + + /* Create the matrix corresponding to the quaternion */ + return (Matrix3x3){ + .row = { + {1.0f - yys - zzs, xys - wzs, xzs + wys}, + {xys + wzs, 1.0f - xxs - zzs, yzs - wxs}, + {xzs - wys, yzs + wxs, 1.0f - xxs - yys}}}; +} + +Quaternion quaternion_slerp(const Quaternion *q, const Quaternion *r, float t) +{ + assert(t >= 0.0f && t <= 1.0f); + + float invert = 1.0f; + + /* Compute cos(theta) using the quaternion scalar product */ + float cosineTheta = quaternion_dotProduct(q, r); + + /* Take care of the sign of cosineTheta */ + if (cosineTheta < 0.0f) + { + cosineTheta = -cosineTheta; + invert = -1.0f; + } + + /* Because of precision, if cos(theta) is nearly 1, + therefore theta is nearly 0 and we can write + sin((1-t)*theta) as (1-t) and sin(t*theta) as t */ + float epsilon = 0.00001f; + if (1.0f - cosineTheta < epsilon) + { + Quaternion q1 = quaternion_returnScaled(q, 1.0f - t); + Quaternion q2 = quaternion_returnScaled(r, t * invert); + return quaternion_sum(&q1, &q2); + } + + /* Compute the theta angle */ + float theta = acosf(cosineTheta); + + /* Compute sin(theta) */ + float sineTheta = fm_sinf(theta); + + /* Compute the two coefficients that are in the spherical linear interpolation formula */ + float coeff1 = fm_sinf((1.0f - t) * theta) / sineTheta; + float coeff2 = fm_sinf(t * theta) / sineTheta * invert; + + /* Compute and return the interpolated quaternion */ + Quaternion q1 = quaternion_returnScaled(q, coeff1); + Quaternion q2 = quaternion_returnScaled(r, coeff2); + return quaternion_sum(&q1, &q2); +} + +Vector3 quaternion_rotateVector(Vector3 v, Quaternion q) +{ + Vector3 u = {q.x, q.y, q.z}; + float s = q.w; + + Vector3 rv1 = vector3_returnScaled(&u, 2.0f * vector3_returnDotProduct(&u, &v)); + Vector3 rv2 = vector3_returnScaled(&v, (s * s - vector3_returnDotProduct(&u, &u))); + Vector3 crossProduct = vector3_returnCrossProduct(&u, &v); + Vector3 rv3 = vector3_returnScaled(&crossProduct, 2.0f * s); + + Vector3 result = vector3_sum(&rv1, &rv2); + result = vector3_sum(&result, &rv3); + + return result; +} + +inline Quaternion quat_from_array(float arr[4]) +{ + Quaternion result; + result.x = arr[0]; + result.y = arr[1]; + result.z = arr[2]; + result.w = arr[3]; + return result; +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/physics/math/transform.h b/code/sb_hot/physics/math/transform.h new file mode 100644 index 00000000..1f1fb1ad --- /dev/null +++ b/code/sb_hot/physics/math/transform.h @@ -0,0 +1,155 @@ +/** + * @file + * + * Holds the transform matrix. + */ + +#ifndef TRANSFORM_H +#define TRANSFORM_H + +typedef struct +{ + Vector3 position; + Quaternion orientation; +} Transform; + +void transform_init(Transform *transform); +void transform_initWithMatrix(Transform *transform, const Vector3 *position, const Matrix3x3 *orientation); +void transform_initWithQuaternion(Transform *transform, const Vector3 *position, const Quaternion *orientation); + +Vector3 transform_getPosition(const Transform *transform); +void transform_setPosition(Transform *transform, const Vector3 *position); +Quaternion transform_getOrientation(const Transform *transform); +void transform_setOrientation(Transform *transform, const Quaternion *orientation); + +void transform_setIdentity(Transform *transform); +Transform transform_returnIdentity(); + +Transform transform_getInverse(const Transform *transform); +Transform transform_getInterpolated(const Transform *oldTransform, const Transform *newTransform, float factor); + +Vector3 transform_getProductVector(const Transform *transform, const Vector3 *vector); +Transform transform_product(const Transform *t1, const Transform *t2); + +bool transform_isValid(const Transform *transform); +bool transform_equals(const Transform *t1, const Transform *t2); +bool transform_notEquals(const Transform *t1, const Transform *t2); +void transform_toString(const Transform *transform, char *buffer, size_t bufferSize); + +/* Initializes the transform to the identity transform. */ +void transform_init(Transform *transform) +{ + vector3_clear(&transform->position); + quaternion_setIdentity(&transform->orientation); +} + +/* Initializes the transform with a given position and orientation matrix. */ +void transform_initWithMatrix(Transform *transform, const Vector3 *position, const Matrix3x3 *orientation) +{ + transform->position = *position; + transform->orientation = quaternion_getFromMatrix(orientation); +} + +/* Initializes the transform with a given position and orientation quaternion. */ +void transform_initWithQuaternion(Transform *transform, const Vector3 *position, const Quaternion *orientation) +{ + transform->position = *position; + transform->orientation = *orientation; +} + +/* Returns the position of the transform. */ +Vector3 transform_getPosition(const Transform *transform) +{ + return transform->position; +} + +/* Sets the position of the transform. */ +void transform_setPosition(Transform *transform, const Vector3 *position) +{ + transform->position = *position; +} + +/* Returns the orientation quaternion of the transform. */ +Quaternion transform_getOrientation(const Transform *transform) +{ + return transform->orientation; +} + +/* Sets the orientation quaternion of the transform. */ +void transform_setOrientation(Transform *transform, const Quaternion *orientation) +{ + transform->orientation = *orientation; +} + +/* Sets the transform to the identity transform. */ +void transform_setIdentity(Transform *transform) +{ + vector3_set(&transform->position, 0.0f, 0.0f, 0.0f); + quaternion_setIdentity(&transform->orientation); +} + +/* Returns the inverse of the transform. */ +Transform transform_getInverse(const Transform *transform) +{ + Quaternion invQuaternion = quaternion_getInverse(&transform->orientation); + Vector3 invertedPosition = vector3_getInverse(&transform->position); + Vector3 invPosition = quaternion_getVectorProduct(&invQuaternion, &invertedPosition); + Transform inverseTransform = {invPosition, invQuaternion}; + return inverseTransform; +} + +/* Returns an interpolated transform between two transforms. */ +Transform transform_getInterpolated(const Transform *oldTransform, const Transform *newTransform, float factor) +{ + Vector3 scaledOldPosition = vector3_returnScaled(&oldTransform->position, 1.0f - factor); + Vector3 scaledNewPosition = vector3_returnScaled(&newTransform->position, factor); + Vector3 interPosition = vector3_sum(&scaledOldPosition, &scaledNewPosition); + Quaternion interOrientation = quaternion_slerp(&oldTransform->orientation, &newTransform->orientation, factor); + Transform interpolatedTransform = {interPosition, interOrientation}; + return interpolatedTransform; +} + +/* Returns the identity transform. */ +Transform transform_returnIdentity() +{ + Transform identityTransform; + transform_setIdentity(&identityTransform); + return identityTransform; +} + +/* Returns true if the transform is valid. */ +bool transform_isValid(const Transform *transform) +{ + return vector3_isFinite(&transform->position) && quaternion_isValid(&transform->orientation); +} + +/* Returns the transformed vector. */ +Vector3 transform_getProductVector(const Transform *transform, const Vector3 *vector) +{ + Vector3 rotatedVector = quaternion_getVectorProduct(&transform->orientation, vector); + return vector3_sum(&rotatedVector, &transform->position); +} + +/* Multiplies two transforms. */ +Transform transform_product(const Transform *t1, const Transform *t2) +{ + Vector3 rotatedPosition = quaternion_getVectorProduct(&t1->orientation, &t2->position); + Vector3 newPosition = vector3_sum(&t1->position, &rotatedPosition); + Quaternion newOrientation = quaternion_returnProduct(&t1->orientation, &t2->orientation); + Transform result = {newPosition, newOrientation}; + return result; +} + +/* Returns true if the two transforms are equal. */ +bool transform_equals(const Transform *t1, const Transform *t2) +{ + return vector3_equals(&t1->position, &t2->position) && quaternion_equals(&t1->orientation, &t2->orientation); +} + +/* Returns true if the two transforms are different. */ +bool transform_notEquals(const Transform *t1, const Transform *t2) +{ + return !transform_equals(t1, t2); +} + +#endif diff --git a/code/sb_hot/physics/math/vector2.h b/code/sb_hot/physics/math/vector2.h new file mode 100644 index 00000000..5fc7528f --- /dev/null +++ b/code/sb_hot/physics/math/vector2.h @@ -0,0 +1,234 @@ +/** + * @file + * + * holds a vector in 2 dimensions. + */ + +#ifndef VECTOR2_H +#define VECTOR2_H + +// Vector2 structure +typedef struct +{ + float x; + float y; +} Vector2; + +// Function prototypes + +void vector2_init(Vector2 *v); +void vector2_clear(Vector2 *v); +void vector2_set(Vector2 *v, float x, float y); + +void vector2_setValue(Vector2 *vector, int index, float value); +float vector2_returnValue(const Vector2 *vector, int index); + +void vector2_add(Vector2 *v, const Vector2 *vector2); +void vector2_subtract(Vector2 *v, const Vector2 *vector2); +void vector2_scale(Vector2 *vector, float number); +void vector2_divideByNumber(Vector2 *vector, float number); + +float vector2_magnitude(const Vector2 *vector); +float vector2_squaredMagnitude(const Vector2 *vector); + +Vector2 vector2_returnUnit(const Vector2 *vector); +Vector2 vector2_returnUnitOrthogonalVector(const Vector2 *vector); + +float vector2_dotProduct(const Vector2 *v, const Vector2 *vector2); +void vector2_normalize(Vector2 *vector); +Vector2 vector2_returnAbsoluteVector(const Vector2 *vector); + +Vector2 vector2_min(const Vector2 *v, const Vector2 *vector2); +Vector2 vector2_max(const Vector2 *v, const Vector2 *vector2); + +int vector2_returnMinAxis(const Vector2 *vector); +int vector2_returnMaxAxis(const Vector2 *vector); + +bool vector2_isUnit(const Vector2 *vector); +bool vector2_isFinite(const Vector2 *vector); +bool vector2_isZero(const Vector2 *vector); + +bool vector2_equals(const Vector2 *v, const Vector2 *vector2); +bool vector2_notEquals(const Vector2 *v, const Vector2 *vector2); +bool vector2_lessThan(const Vector2 *v, const Vector2 *vector2); +bool vector2_approxEqual(const Vector2 *v, const Vector2 *w, float epsilon); + +// Function implementations + +/* Initializes all components of the vector to zero. */ +void vector2_init(Vector2 *v) +{ + v->x = 0.0f; + v->y = 0.0f; +} + +/* Clears all components of the vector to zero. */ +void vector2_clear(Vector2 *v) +{ + vector2_init(v); +} + +/* Sets the components of the vector to the specified values. */ +void vector2_set(Vector2 *v, float x, float y) +{ + v->x = x; + v->y = y; +} + +float vector2_magnitude(const Vector2 *v) +{ + return sqrtf(v->x * v->x + v->y * v->y); +} + +float vector2_squaredMagnitude(const Vector2 *v) +{ + return v->x * v->x + v->y * v->y; +} + +Vector2 vector2_returnUnit(const Vector2 *v) +{ + Vector2 vector = {0.0f, 0.0f}; + float l = vector2_magnitude(v); + + if (l < FLT_EPSILON) + return vector; + + vector2_set(&vector, v->x / l, v->y / l); + + return vector; +} + +Vector2 vector2_returnUnitOrthogonalVector(const Vector2 *v) +{ + float l = vector2_magnitude(v); + assert(l > FLT_EPSILON); + // Assuming clockwise rotation for orthogonal vector + Vector2 vector = {-v->y / l, v->x / l}; + return vector; +} + +bool vector2_isUnit(const Vector2 *vector) +{ + return fabs(vector2_squaredMagnitude(vector) - 1.0) < FLT_EPSILON; +} + +bool vector2_isFinite(const Vector2 *vector) +{ + return isfinite(vector->x) && isfinite(vector->y); +} + +bool vector2_isZero(const Vector2 *vector) +{ + return vector2_squaredMagnitude(vector) < FLT_EPSILON; +} + +float vector2_dotProduct(const Vector2 *v, const Vector2 *w) +{ + return v->x * w->x + v->y * w->y; +} + +void vector2_normalize(Vector2 *v) +{ + float l = vector2_magnitude(v); + if (l < FLT_EPSILON) + return; + v->x /= l; + v->y /= l; +} + +Vector2 vector2_returnAbsoluteVector(const Vector2 *v) +{ + Vector2 result; + result.x = fabsf(v->x); + result.y = fabsf(v->y); + return result; +} + +int vector2_returnMinAxis(const Vector2 *v) +{ + return (v->x < v->y ? 0 : 1); +} + +int vector2_returnMaxAxis(const Vector2 *v) +{ + return (v->x < v->y ? 1 : 0); +} + +bool vector2_equals(const Vector2 *v, const Vector2 *w) +{ + return (v->x == w->x && v->y == w->y); +} + +bool vector2_notEquals(const Vector2 *v, const Vector2 *w) +{ + return !vector2_equals(v, w); +} + +bool vector2_lessThan(const Vector2 *v, const Vector2 *w) +{ + return (v->x == w->x ? v->y < w->y : v->x < w->x); +} + +bool vector2_approxEqual(const Vector2 *v, const Vector2 *w, float epsilon) +{ + return approxEqual(v->x, w->x) && approxEqual(v->y, w->y); +} + +void vector2_add(Vector2 *v, const Vector2 *w) +{ + v->x += w->x; + v->y += w->y; +} + +void vector2_subtract(Vector2 *v, const Vector2 *w) +{ + v->x -= w->x; + v->y -= w->y; +} + +void vector2_scale(Vector2 *v, float scalar) +{ + v->x *= scalar; + v->y *= scalar; +} + +void vector2_divideByNumber(Vector2 *v, float number) +{ + assert(number > FLT_EPSILON); + v->x /= number; + v->y /= number; +} + +float vector2_returnValue(const Vector2 *v, int index) +{ + if (index == 0) + return v->x; + else + return v->y; +} + +void vector2_setValue(Vector2 *v, int index, float value) +{ + if (index == 0) + v->x = value; + else + v->y = value; +} + +Vector2 vector2_min(const Vector2 *v, const Vector2 *w) +{ + Vector2 result; + result.x = min2(v->x, w->x); + result.y = min2(v->y, w->y); + return result; +} + +Vector2 vector2_max(const Vector2 *v, const Vector2 *w) +{ + Vector2 result; + result.x = max2(v->x, w->x); + result.y = max2(v->y, w->y); + return result; +} + +#endif // VECTOR2_H diff --git a/code/sb_hot/physics/math/vector3.h b/code/sb_hot/physics/math/vector3.h new file mode 100644 index 00000000..42b26b02 --- /dev/null +++ b/code/sb_hot/physics/math/vector3.h @@ -0,0 +1,385 @@ +#ifndef VECTOR_3_H +#define VECTOR_3_H + +// structures + +typedef struct Vector3 +{ + float x; + float y; + float z; +} Vector3; + +// Macros to use t3dmath if necessary +#define T3DVec3_to_Vector3(t3dVec) ((Vector3){(t3dVec).v[0], (t3dVec).v[1], (t3dVec).v[2]}) +#define Vector3_to_T3DVec3(vec) ((T3DVec3){{(vec).x, (vec).y, (vec).z}}) +#define Vector3_to_fast(vec) ((fm_vec3_t){{(vec).x, (vec).y, (vec).z}}) +#define fast_to_Vector3(fast) ((Vector3){{(fast).x, (fast).y, (fast).z}}) + +// function prototypes + +void vector3_init(Vector3 *v); +void vector3_clear(Vector3 *v); +void vector3_set(Vector3 *v, float x, float y, float z); +void vector3_setElement(Vector3 *v, int index, float value); +float vector3_returnElement(const Vector3 *v, int index); + +void vector3_invert(Vector3 *v); +Vector3 vector3_getInverse(const Vector3 *v); + +void vector3_add(Vector3 *v, const Vector3 *w); +Vector3 vector3_sum(const Vector3 *v, const Vector3 *w); +void vector3_subtract(Vector3 *v, const Vector3 *w); +Vector3 vector3_difference(const Vector3 *v, const Vector3 *w); + +void vector3_scale(Vector3 *v, float scalar); +Vector3 vector3_returnScaled(const Vector3 *v, float scalar); +void vector3_divideByNumber(Vector3 *v, float number); +Vector3 vector3_returnQuotientByNumber(const Vector3 *v, float number); +Vector3 vector3_returnQuotientByVector(const Vector3 *v, const Vector3 *w); + +void vector3_componentProduct(Vector3 *v, const Vector3 *w); +Vector3 vector3_returnComponentProduct(const Vector3 *v, const Vector3 *w); +void vector3_crossProduct(Vector3 *v, const Vector3 *w); +Vector3 vector3_returnCrossProduct(const Vector3 *v, const Vector3 *w); +float vector3_returnDotProduct(const Vector3 *v, const Vector3 *w); +void vector3_addScaledVector(Vector3 *v, const Vector3 *w, float scalar); + +float vector3_magnitude(const Vector3 *v); +float vector3_squaredMagnitude(const Vector3 *v); +void vector3_normalize(Vector3 *v); +Vector3 vector3_returnNormalized(const Vector3 *v); +Vector3 vector3_returnAbsoluteVector(const Vector3 *v); + +Vector3 vector3_min(const Vector3 *v, const Vector3 *w); +Vector3 vector3_max(const Vector3 *v, const Vector3 *w); +float vector3_returnMinValue(const Vector3 *v); +float vector3_returnMaxValue(const Vector3 *v); +int vector3_returnMinAxis(const Vector3 *v); +int vector3_returnMaxAxis(const Vector3 *v); + +bool vector3_isUnit(const Vector3 *v); +bool vector3_isFinite(const Vector3 *v); +bool vector3_isZero(const Vector3 *v); +bool vector3_equals(const Vector3 *v, const Vector3 *w); +bool vector3_notEquals(const Vector3 *v, const Vector3 *w); +bool vector3_lessThan(const Vector3 *v, const Vector3 *w); +bool vector3_approxEquals(const Vector3 *v, const Vector3 *w); +Vector3 vector3_lerp(const Vector3 *v1, const Vector3 *v2, float t); + +Vector3 vector3_from_array(float arr[3]); +Vector3 vector3_flip_coords(Vector3 vec); +Vector3 vector3_flip_up(Vector3 vec); +float vector3_squaredDistance(const Vector3 *v, const Vector3 *w); +float vector3_distance(const Vector3 *v, const Vector3 *w); +Vector3 vector3_average4(const Vector3 *v1, const Vector3 *v2, const Vector3 *v3, const Vector3 *v4); + +inline void vector3_init(Vector3 *v) +{ + v->x = 0.0f; + v->y = 0.0f; + v->z = 0.0f; +} + +inline void vector3_clear(Vector3 *v) +{ + vector3_init(v); +} + +inline void vector3_set(Vector3 *v, float x, float y, float z) +{ + v->x = x; + v->y = y; + v->z = z; +} + +inline void vector3_invert(Vector3 *v) +{ + v->x = -v->x; + v->y = -v->y; + v->z = -v->z; +} + +inline Vector3 vector3_getInverse(const Vector3 *v) +{ + + return (Vector3){-v->x, -v->y, -v->z}; +} + +inline void vector3_copy(Vector3 *v, const Vector3 *w) +{ + v->x = w->x; + v->y = w->y; + v->z = w->z; +} + +inline void vector3_add(Vector3 *v, const Vector3 *w) +{ + v->x += w->x; + v->y += w->y; + v->z += w->z; +} + +inline Vector3 vector3_sum(const Vector3 *v, const Vector3 *w) +{ + return (Vector3){v->x + w->x, v->y + w->y, v->z + w->z}; +} + +inline void vector3_subtract(Vector3 *v, const Vector3 *w) +{ + v->x -= w->x; + v->y -= w->y; + v->z -= w->z; +} + +inline Vector3 vector3_difference(const Vector3 *v, const Vector3 *w) +{ + return (Vector3){v->x - w->x, v->y - w->y, v->z - w->z}; +} + +inline void vector3_scale(Vector3 *v, float scalar) +{ + v->x *= scalar; + v->y *= scalar; + v->z *= scalar; +} + +inline Vector3 vector3_returnScaled(const Vector3 *v, float scalar) +{ + return (Vector3){v->x * scalar, v->y * scalar, v->z * scalar}; +} + +void vector3_divideByNumber(Vector3 *v, float number) +{ + assert(number > FLT_EPSILON); + v->x /= number; + v->y /= number; + v->z /= number; +} + +Vector3 vector3_returnQuotientByNumber(const Vector3 *v, float number) +{ + assert(number > FLT_EPSILON); + return (Vector3){v->x / number, v->y / number, v->z / number}; +} + +Vector3 vector3_returnQuotientByVector(const Vector3 *v, const Vector3 *w) +{ + assert(w->x > FLT_EPSILON); + assert(w->y > FLT_EPSILON); + assert(w->z > FLT_EPSILON); + return (Vector3){v->x / w->x, v->y / w->y, v->z / w->z}; +} + +inline void vector3_componentProduct(Vector3 *v, const Vector3 *w) +{ + v->x *= w->x; + v->y *= w->y; + v->z *= w->z; +} + +inline Vector3 vector3_returnComponentProduct(const Vector3 *v, const Vector3 *w) +{ + return (Vector3){v->x * w->x, v->y * w->y, v->z * w->z}; +} + +inline void vector3_crossProduct(Vector3 *v, const Vector3 *w) +{ + v->x = v->y * w->z - v->z * w->y; + v->y = v->z * w->x - v->x * w->z; + v->z = v->x * w->y - v->y * w->x; +} + +Vector3 vector3_returnCrossProduct(const Vector3 *v, const Vector3 *w) +{ + return (Vector3){v->y * w->z - v->z * w->y, v->z * w->x - v->x * w->z, v->x * w->y - v->y * w->x}; +} + +inline float vector3_returnDotProduct(const Vector3 *v, const Vector3 *w) +{ + return v->x * w->x + v->y * w->y + v->z * w->z; +} + +inline void vector3_addScaledVector(Vector3 *v, const Vector3 *w, float scalar) +{ + v->x += w->x * scalar; + v->y += w->y * scalar; + v->z += w->z * scalar; +} + +inline float vector3_magnitude(const Vector3 *v) +{ + return sqrtf(v->x * v->x + v->y * v->y + v->z * v->z); +} + +inline float vector3_squaredMagnitude(const Vector3 *v) +{ + return v->x * v->x + v->y * v->y + v->z * v->z; +} + +void vector3_normalize(Vector3 *v) +{ + float m = vector3_magnitude(v); + if (m > 0) + { + m = 1.0f / m; + vector3_scale(v, m); + } +} + +Vector3 vector3_returnNormalized(const Vector3 *v) +{ + Vector3 result = *v; + vector3_normalize(&result); + return result; +} + +Vector3 vector3_returnAbsoluteVector(const Vector3 *v) +{ + return (Vector3){fabsf(v->x), fabsf(v->y), fabsf(v->z)}; +} + +inline int vector3_returnMinAxis(const Vector3 *v) +{ + return (v->x < v->y ? (v->x < v->z ? 0 : 2) : (v->y < v->z ? 1 : 2)); +} + +inline int vector3_returnMaxAxis(const Vector3 *v) +{ + return (v->x < v->y ? (v->y < v->z ? 2 : 1) : (v->x < v->z ? 2 : 0)); +} + +bool vector3_isUnit(const Vector3 *v) +{ + return approxEqual(vector3_squaredMagnitude(v), 1.0f); +} + +bool vector3_isFinite(const Vector3 *v) +{ + return isfinite(v->x) && isfinite(v->y) && isfinite(v->z); +} + +bool vector3_isZero(const Vector3 *v) +{ + return approxEqual(vector3_squaredMagnitude(v), 0.0f); +} + +inline bool vector3_equals(const Vector3 *v, const Vector3 *w) +{ + return (v->x == w->x && v->y == w->y && v->z == w->z); +} + +inline bool vector3_notEquals(const Vector3 *v, const Vector3 *w) +{ + return !vector3_equals(v, w); +} + +inline bool vector3_lessThan(const Vector3 *v, const Vector3 *w) +{ + return (v->x == w->x ? (v->y == w->y ? v->z < w->z : v->y < w->y) : v->x < w->x); +} + +bool vector3_approxEquals(const Vector3 *v, const Vector3 *w) +{ + return approxEqual(v->x, w->x) && approxEqual(v->y, w->y) && approxEqual(v->z, w->z); +} + +inline float vector3_returnElement(const Vector3 *v, int index) +{ + return ((const float *)v)[index]; +} + +inline void vector3_setElement(Vector3 *v, int index, float value) +{ + ((float *)v)[index] = value; +} + +inline Vector3 vector3_min(const Vector3 *v, const Vector3 *w) +{ + return (Vector3){min2(v->x, w->x), min2(v->y, w->y), min2(v->z, w->z)}; +} + +inline Vector3 vector3_max(const Vector3 *v, const Vector3 *w) +{ + return (Vector3){max2(v->x, w->x), max2(v->y, w->y), max2(v->z, w->z)}; +} + +inline float vector3_returnMinValue(const Vector3 *v) +{ + return min2(min2(v->x, v->y), v->z); +} + +inline float vector3_returnMaxValue(const Vector3 *v) +{ + return max3(v->x, v->y, v->z); +} + +Vector3 vector3_lerp(const Vector3 *v1, const Vector3 *v2, float t) +{ + Vector3 result; + result.x = v1->x + (v2->x - v1->x) * t; + result.y = v1->y + (v2->y - v1->y) * t; + result.z = v1->z + (v2->z - v1->z) * t; + return result; +} + +inline Vector3 vector3_from_array(float arr[3]) +{ + Vector3 result; + result.x = arr[0]; + result.y = arr[1]; + result.z = arr[2]; + return result; +} + +// Function to convert a vector from a right-handed Y-up coordinate system to a left-handed Z-up coordinate system +inline Vector3 vector3_flip_coords(Vector3 vec) +{ + Vector3 result; + result.x = vec.x; + result.y = vec.z; + result.z = -vec.y; + return result; +} + +// Function to convert a vector from a right-handed Y-up coordinate system to a right-handed Z-up coordinate system +inline Vector3 vector3_flip_up(Vector3 vec) +{ + Vector3 result; + result.x = vec.x; + result.y = -vec.z; + result.z = vec.y; + return result; +} + +// Function to convert T3D AABB coordinates to engine's format +inline Vector3 vector3_from_int16(const int16_t int_arr[3]) +{ + Vector3 vec; + vec.x = (float)int_arr[0]; + vec.y = -(float)int_arr[2]; + vec.z = (float)int_arr[1]; + return vec; +} + +float vector3_squaredDistance(const Vector3 *v, const Vector3 *w) +{ + Vector3 diff = vector3_difference(v, w); + return vector3_returnDotProduct(&diff, &diff); +} + +float vector3_distance(const Vector3 *v, const Vector3 *w) +{ + return sqrtf(vector3_squaredDistance(v, w)); +} + +Vector3 vector3_average4(const Vector3 *v1, const Vector3 *v2, const Vector3 *v3, const Vector3 *v4) +{ + Vector3 avg; + avg.x = (v1->x + v2->x + v3->x + v4->x) / 4.0f; + avg.y = (v1->y + v2->y + v3->y + v4->y) / 4.0f; + avg.z = (v1->z + v2->z + v3->z + v4->z) / 4.0f; + return avg; +} + +#endif diff --git a/code/sb_hot/physics/physics.h b/code/sb_hot/physics/physics.h new file mode 100644 index 00000000..8b597fbf --- /dev/null +++ b/code/sb_hot/physics/physics.h @@ -0,0 +1,17 @@ +#ifndef PHYSICS_H +#define PHYSICS_H + +#include "math/physics_math.h" + +#include "body/rigid_body.h" + +#include "collision/contact_data.h" +#include "collision/shapes/sphere.h" +#include "collision/shapes/AABB.h" +#include "collision/shapes/box.h" +#include "collision/shapes/plane.h" +#include "collision/shapes/ray.h" +#include "collision/shapes/capsule.h" +#include "collision/shapes/triangle.h" + +#endif \ No newline at end of file diff --git a/code/sb_hot/physics/physics_config.h b/code/sb_hot/physics/physics_config.h new file mode 100644 index 00000000..203c1323 --- /dev/null +++ b/code/sb_hot/physics/physics_config.h @@ -0,0 +1,15 @@ +#ifndef PHYSICS_CONFIG_H +#define PHYSICS_CONFIG_H + +// ---------- Constants ---------- // + +/* Pi constant */ +#define PI 3.141592653589f + +/* 2*Pi constant */ +#define PI_TIMES_2 6.28318530f + +/* A numeric tolerance value used to handle floating-point precision issues. */ +#define TOLERANCE 1e-3f + +#endif diff --git a/code/sb_hot/player/ai.h b/code/sb_hot/player/ai.h new file mode 100644 index 00000000..d35cbbbe --- /dev/null +++ b/code/sb_hot/player/ai.h @@ -0,0 +1,163 @@ +#ifndef AI_H +#define AI_H + +typedef struct +{ + float jump_threshold; + float safe_height; + uint8_t difficulty; + uint8_t error_margin; + uint8_t reaction_delay; + uint8_t max_reaction_delay; +} AI; + +void ai_init(AI *ai, uint8_t difficulty); +void ai_generateControlData(AI *ai, ControllerData *control, Actor *actor, Platform *platforms, float camera_angle); + +void ai_init(AI *ai, uint8_t difficulty) +{ + + // Reset reaction delay for fresh initialization + ai->reaction_delay = 0; + + // Set other fields based on difficulty + switch (difficulty) + { + case DIFF_EASY: + ai->jump_threshold = 400.0f; + ai->safe_height = 245.0f; + ai->difficulty = DIFF_MEDIUM; + ai->error_margin = 12; + ai->max_reaction_delay = 6; + break; + case DIFF_MEDIUM: + ai->jump_threshold = 385.0f; + ai->safe_height = 240.0f; + ai->difficulty = DIFF_MEDIUM; + ai->error_margin = 10; + ai->max_reaction_delay = 4; + break; + case DIFF_HARD: + ai->jump_threshold = 375.0f; + ai->safe_height = 235.0f; + ai->difficulty = DIFF_MEDIUM; + ai->error_margin = 8; + ai->max_reaction_delay = 2; + break; + } +} + +// Helper function to rotate input by camera angle +void ai_updateCam(ControllerData *control, float camera_angle) +{ + // Convert angle to radians + float angle_rad = camera_angle * (T3D_PI / 180.0f); + int8_t original_x = control->input.stick_x; + int8_t original_y = control->input.stick_y; + + // Rotate stick_x and stick_y based on camera angle + control->input.stick_x = (int8_t)(original_x * fm_cosf(angle_rad) - original_y * fm_sinf(angle_rad)); + control->input.stick_y = (int8_t)(original_x * fm_sinf(angle_rad) + original_y * fm_cosf(angle_rad)); +} + +// Function to find the nearest platform at a safe height +Platform *find_nearest_safe_platform(AI *ai, Actor *actor, Platform *platforms) +{ + Platform *nearest_platform = NULL; + float min_distance_sq = FLT_MAX; // Store squared distance to avoid square root computation + const float current_platform_threshold_sq = 0.02f * 0.02f; // Squared threshold to ignore the current platform + + // Calculate grid cell for the actor's current position + int xCell = (int)fm_floorf((actor->body.position.x + 700) / GRID_SIZE); + int yCell = (int)fm_floorf((actor->body.position.y + 700) / GRID_SIZE); + + // Iterate through platforms in the same and adjacent grid cells + for (int dx = -1; dx <= 1; dx++) + { + for (int dy = -1; dy <= 1; dy++) + { + int nx = xCell + dx; + int ny = yCell + dy; + + // Check if the cell is within bounds + if (nx >= 0 && nx < MAX_GRID_CELLS && ny >= 0 && ny < MAX_GRID_CELLS) + { + PlatformGridCell *cell = &platformGrid[nx][ny]; + for (size_t i = 0; i < cell->count; i++) + { + size_t platformIndex = cell->platformIndices[i]; + Platform *platform = &platforms[platformIndex]; + + // Skip platforms not at a safe height + if (platform->position.z <= ai->safe_height) + continue; + + // Calculate squared distance using fast math vector3 + fm_vec3_t actorPos = Vector3_to_fast(actor->body.position); + fm_vec3_t platformPos = Vector3_to_fast(platform->position); + float distance_sq = fm_vec3_distance2(&platformPos, &actorPos); + + // Ignore the current platform the AI is standing on + if (distance_sq < current_platform_threshold_sq) + continue; + + // Check if this platform is the nearest valid one + if (distance_sq < min_distance_sq) + { + min_distance_sq = distance_sq; + nearest_platform = platform; + } + } + } + } + } + + return nearest_platform; +} + +// Generate control data for the AI +void ai_generateControlData(AI *ai, ControllerData *control, Actor *actor, Platform *platforms, float camera_angle) +{ + // Initialize control data to zero for each frame + memset(control, 0, sizeof(ControllerData)); + + // Set difficulty-based reaction delay + if (ai->reaction_delay < ai->max_reaction_delay) + { + ai->reaction_delay++; + return; // Skip processing this frame to simulate slower reaction + } + + // Find the nearest safe platform + Platform *target_platform = find_nearest_safe_platform(ai, actor, platforms); + if (target_platform == NULL) + return; // No valid platform found, do nothing + + // Calculate direction towards the target platform + fm_vec3_t direction_to_target = {{ + target_platform->position.x - actor->body.position.x, + target_platform->position.y - actor->body.position.y, + target_platform->position.z - actor->body.position.z, + }}; + fm_vec3_norm(&direction_to_target, &direction_to_target); + + // Set movement based on target distance + control->input.stick_x = (int8_t)(direction_to_target.x * 127); // Max joystick range + control->input.stick_y = (int8_t)(direction_to_target.y * 127); + + // Add slight randomness to mimic human error, increasing with easier difficulties + control->input.stick_x += (rand() % ai->error_margin) - (ai->error_margin / 2); + control->input.stick_y += (rand() % ai->error_margin) - (ai->error_margin / 2); + + // Check if the actor should jump when horizontal speed is greater than 10 + if (actor->horizontal_speed > ai->jump_threshold && actor->state != FALLING) + { + control->pressed.a = 1; // Press jump button + control->held.a = 1; // Hold jump button + } + + // Adjust for camera angle if needed + ai_updateCam(control, camera_angle); +} + +#endif // AI_H \ No newline at end of file diff --git a/code/sb_hot/player/player.h b/code/sb_hot/player/player.h new file mode 100644 index 00000000..b6024c66 --- /dev/null +++ b/code/sb_hot/player/player.h @@ -0,0 +1,99 @@ +#ifndef PLAYER_H +#define PLAYER_H + +typedef struct +{ + + Vector3 position; + uint8_t id; + uint8_t actor_id; + bool died; + bool isHuman; + bool deathCounted; + ControllerData control; + +} Player; + +void player_init(Player *player, uint8_t id, uint8_t actor_id); + +void player_init(Player *player, uint8_t id, uint8_t actor_id) +{ + player->position = (Vector3){0, 0, 0}; + player->id = id; + player->actor_id = actor_id; + player->died = false; + player->isHuman = true; + player->deathCounted = false; +} + +void player_setControlData(Player *player) +{ + joypad_poll(); + + for (int i = 0; i < PLAYER_COUNT; i++) + { + if (i != 0 && player[i].died) + continue; + controllerData_getInputs(&player[i].control, i); + } +} + +// Modified from `player_draw_billboard` in Snake3D example +Vector3 player_getBillboard(Player *player, T3DViewport *viewport) +{ + Vector3 result = (Vector3){0, 0, 0}; + if (player->died) + return result; + + Vector3 billboardPos = (Vector3){ + player->position.x, + player->position.y, + player->position.z + 200.0f}; + + T3DVec3 billboardPosConvert = Vector3_to_T3DVec3(billboardPos); + + T3DVec3 billboardScreenPos; + t3d_viewport_calc_viewspace_pos(viewport, &billboardScreenPos, &billboardPosConvert); + + int x = fm_floorf(billboardScreenPos.v[0]); + int y = fm_floorf(billboardScreenPos.v[2]); + int z = fm_floorf(billboardScreenPos.v[1]); + + result = (Vector3){x, y, z}; + return result; +} + +void player_drawShadow(Vector3 position, T3DViewport *viewport) +{ + + Vector3 shadowPos = (Vector3){ + position.x, + position.y, + 310 // @TODO: in a bigger game, I'd probably just raycasst for the floor Z + }; + + T3DVec3 playerPosConvert = Vector3_to_T3DVec3(position); + T3DVec3 shadowPosConvert = Vector3_to_T3DVec3(shadowPos); + + T3DVec3 playerScreenPos; + T3DVec3 shadowScreenPos; + t3d_viewport_calc_viewspace_pos(viewport, &playerScreenPos, &playerPosConvert); + t3d_viewport_calc_viewspace_pos(viewport, &shadowScreenPos, &shadowPosConvert); + + int offset = 3; + float v1[] = {playerScreenPos.x, playerScreenPos.y - offset}; + float v2[] = {shadowScreenPos.x - offset, shadowScreenPos.y}; + float v3[] = {shadowScreenPos.x + offset, shadowScreenPos.y}; + float v4[] = {shadowScreenPos.x, shadowScreenPos.y + offset}; + + rdpq_sync_pipe(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_set_prim_color(RGBA32(0, 0, 0, 127)); + + // Draw rhombus shape with 2 tris + rdpq_triangle(&TRIFMT_FILL, v1, v2, v3); + rdpq_triangle(&TRIFMT_FILL, v3, v4, v2); +} +#endif \ No newline at end of file diff --git a/code/sb_hot/scene/particles.h b/code/sb_hot/scene/particles.h new file mode 100644 index 00000000..6d8e0fc4 --- /dev/null +++ b/code/sb_hot/scene/particles.h @@ -0,0 +1,159 @@ +/* + * This file includes code from Tiny3D. + * Tiny3D is licensed under the MIT License. + * + * Original code by Max Bebök + * Adapted by s4ys + * November 2024 + * + * Description of changes or adaptations made: + * - Generate positions randomly within an AABB + * - Utilize `gradient_fire` in more of a random distribution + * + * + * Original source: https://github.com/HailToDodongo/tiny3d/tree/main/examples/18_particles + */ + +#ifndef PARTICLES_H +#define PARTICLES_H + +typedef struct +{ + uint32_t count; + uint32_t bufSize; + TPXParticle *buf; + T3DMat4FP *mat; + +} Particles; + +Particles lavaBubbles; + +void ptx_init(Particles *ptx) +{ + tpx_init((TPXInitParams){}); + ptx->count = 2000; + ptx->bufSize = sizeof(TPXParticle) * (ptx->count + 2); + ptx->buf = malloc_uncached(ptx->bufSize); + ptx->mat = malloc_uncached(sizeof(T3DMat4FP)); +} + +// Fire color: white -> yellow/orange -> red -> black +void gradient_fire(uint8_t *color, float t) +{ + t = fminf(1.0f, fmaxf(0.0f, t)); + t = 0.8f - t; + t *= t; + + if (t < 0.25f) + { // Dark red to bright red + color[0] = (uint8_t)(200 * (t / 0.25f)) + 55; + color[1] = 0; + color[2] = 0; + } + else if (t < 0.5f) + { // Bright red to yellow + color[0] = 255; + color[1] = (uint8_t)(255 * ((t - 0.25f) / 0.25f)); + color[2] = 0; + } + else if (t < 0.75f) + { // Yellow to white (optional, if you want a bright white center) + color[0] = 255; + color[1] = 255; + color[2] = (uint8_t)(255 * ((t - 0.5f) / 0.25f)); + } + else + { // White to black + color[0] = (uint8_t)(255 * (1.0f - (t - 0.75f) / 0.25f)); + color[1] = (uint8_t)(255 * (1.0f - (t - 0.75f) / 0.25f)); + color[2] = (uint8_t)(255 * (1.0f - (t - 0.75f) / 0.25f)); + } +} + +void ptx_randomPos(Particles *ptx, AABB aabb, T3DViewport *vp) +{ + for (int i = 0; i < ptx->count; i++) + { + int p = i / 2; + int8_t *ptxPos = (i % 2 == 0) ? ptx->buf[p].posA : ptx->buf[p].posB; + + // Assign random sizes + ptx->buf[p].sizeA = 2 + (rand() % 5); + ptx->buf[p].sizeB = 2 + (rand() % 5); + + // Random positions within the bounding box + T3DVec3 randomPos; + randomPos.v[0] = aabb.minCoordinates.x + ((float)rand() / aabb.maxCoordinates.x) * (aabb.maxCoordinates.x - aabb.minCoordinates.x); + randomPos.v[1] = aabb.minCoordinates.y; + randomPos.v[2] = aabb.minCoordinates.z + ((float)rand() / aabb.maxCoordinates.z) * (aabb.maxCoordinates.z - aabb.minCoordinates.z); + + // Calculate from view space + T3DVec3 screenPos; + t3d_viewport_calc_viewspace_pos(vp, &screenPos, &randomPos); + + // Move particles upwards and oscillate + float frequency = 0.1f; + float amplitude = 0.5f; + float t = (float)i / ptx->count; // Vary by particle index + screenPos.v[1] += t * (aabb.maxCoordinates.y - aabb.minCoordinates.y); // Move upward + screenPos.v[0] += amplitude * fm_sinf(t * frequency * 2 * T3D_PI); + screenPos.v[2] += amplitude * fm_cosf(t * frequency * 2 * T3D_PI); + + // Clamp final values to fit within int8_t range + ptxPos[0] = fm_floorf(screenPos.v[0]); + ptxPos[1] = fm_floorf(screenPos.v[1]); + ptxPos[2] = fm_floorf(screenPos.v[2]); + + gradient_fire(ptx->buf[p].colorA, (ptxPos[0] + 127) / 250.0f); + gradient_fire(ptx->buf[p].colorB, (ptxPos[0] + 127) / 250.0f); + } +} + +void ptx_draw(T3DViewport *vp, Particles *ptx, float x, float y) +{ + + static int frameCounter = 0; + const int updateInterval = 6; + + // Prepare the RDPQ + rdpq_sync_pipe(); + rdpq_sync_tile(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER1((PRIM, 0, ENV, 0), (0, 0, 0, 1))); + rdpq_set_env_color(ui_color(WHITE)); + + AABB aabb = (AABB){ + .minCoordinates = {-127.9f, -127.9f, -127.9f}, + .maxCoordinates = {126.9f, 126.9f, 126.9f}}; + + if (frameCounter % updateInterval == 0) + { + ptx_randomPos(ptx, aabb, vp); + } + frameCounter++; + + if (frameCounter >= 255) + frameCounter = 0; // clamp the int so it doesn't overflow + + t3d_mat4fp_from_srt_euler( + ptx->mat, + (float[3]){50, 25, 50}, + (float[3]){0, 0, 0}, + (float[3]){0, 0, 0}); + + tpx_state_from_t3d(); + + tpx_matrix_push(ptx->mat); + tpx_state_set_scale(x, y); + tpx_particle_draw(ptx->buf, ptx->count); + tpx_matrix_pop(1); +} + +void ptx_cleanup(Particles *ptx) +{ + free_uncached(ptx->mat); + free_uncached(ptx->buf); + tpx_destroy(); +} + +#endif // PARTICLES_H \ No newline at end of file diff --git a/code/sb_hot/scene/platform.h b/code/sb_hot/scene/platform.h new file mode 100644 index 00000000..d4399212 --- /dev/null +++ b/code/sb_hot/scene/platform.h @@ -0,0 +1,368 @@ +#ifndef PLATFORM_H +#define PLATFORM_H + +typedef struct +{ + + Box box[3]; + +} PlatformCollider; + +typedef struct +{ + + uint32_t id; + T3DMat4FP *mat; + T3DObject *obj; + Vector3 position; + Vector3 home; + PlatformCollider collider; + color_t color; + uint32_t platformTimer; + bool contact; + +} Platform; + +#define OFFSET 350 +#define GRID_SIZE OFFSET // Size of each grid cell +#define MAX_GRID_CELLS 7 // Adjust based on level size + +typedef struct +{ + size_t platformIndices[PLATFORM_COUNT]; // Indices of platforms in this cell + size_t count; // Number of platforms in this cell +} PlatformGridCell; + +PlatformGridCell platformGrid[MAX_GRID_CELLS][MAX_GRID_CELLS]; + +Platform hexagons[PLATFORM_COUNT]; + +// Forward Declarations + +void platform_init(Platform *platform, T3DModel *model, Vector3 position, color_t color); +void platform_loop(Platform *platform, Actor *actor, int diff); +void platform_drawBatch(void); +void platform_hexagonGrid(Platform *platform, T3DModel *model, float z, color_t color); +void platform_destroy(Platform *platform); + +// Definitions + +void platform_init(Platform *platform, T3DModel *model, Vector3 position, color_t color) +{ + + static uint32_t platformIdx = 0; + + platform->id = platformIdx; + platform->mat = malloc_uncached(sizeof(T3DMat4FP)); // needed for t3d + platform->position = position; + platform->home = position; + + // Initialize the three boxes for collision within each hexagon + for (int j = 0; j < 3; j++) + { + platform->collider.box[j] = (Box){ + .size = {200.0f, 300.0f, 75.0f}, + .center = platform->position, + .rotation = { + // Set only Z rotation explicitly + 0.0f, + 0.0f, + (j == 0) ? 0.0f : (j == 1) ? 30.0f + : -30.0f // Z rotation for boxes[0], boxes[1], and boxes[2] + }}; + } + + platform->color = color; // Set color + + platform->platformTimer = 0; + + platform->contact = false; + + platformIdx++; +} + +void platform_assignGrid(Platform *platforms) +{ + memset(platformGrid, 0, sizeof(platformGrid)); // Clear the grid + + for (size_t i = 0; i < PLATFORM_COUNT; i++) + { + int xCell = (int)fm_floorf((platforms[i].position.x + (OFFSET * 2)) / GRID_SIZE); + int yCell = (int)fm_floorf((platforms[i].position.y + (OFFSET * 2)) / GRID_SIZE); + + if (xCell >= 0 && xCell < MAX_GRID_CELLS && yCell >= 0 && yCell < MAX_GRID_CELLS) + { + PlatformGridCell *cell = &platformGrid[xCell][yCell]; + if (cell->count < PLATFORM_COUNT) // Ensure we don't exceed bounds + cell->platformIndices[cell->count++] = i; + } + } +} + +//// BEHAVIORS ~ Start //// + +// Example behavior: Oscillate platform x position to simulate shake +void platform_shake(Platform *platform, float time) +{ + float amplitude = 10.0f; // Maximum oscillation distance from home + float baseX = platform->home.x; // Use a stored "home" position for the base + + // Oscillate `platform->position.x` around `baseX` + platform->position.x = baseX + amplitude * fm_sinf(time); +} + +// Example behavior: Lower platform over time +void platform_updateHeight(Platform *platform, float time) +{ + if (platform->position.z > -150.0f) + platform->position.z = platform->position.z - time; +} + +void platform_collideCheck(Platform *platform, Actor *actor) +{ + if (platform->contact) + return; // If already in collided state, do nothing + + const float squaredDistanceThreshold = 150.0f * 150.0f; + + for (size_t i = 0; i < ACTOR_COUNT; i++) + { + // Skip actors that are not grounded (not relevant for collision) + if (!actor[i].grounded) + continue; + + // Calculate the difference vector + Vector3 diff = vector3_difference(&platform->position, &actor[i].body.position); + + // Calculate squared distance + float distanceSq = vector3_squaredMagnitude(&diff); + + if (distanceSq <= squaredDistanceThreshold) + { + platform->contact = true; + return; // Exit early on first collision + } + } +} + +void platform_collideCheckOptimized(Platform *platforms, Actor *actor) +{ + int xCell = (int)floorf((actor->body.position.x + (OFFSET * 2)) / GRID_SIZE); + int yCell = (int)floorf((actor->body.position.y + (OFFSET * 2)) / GRID_SIZE); + + if (xCell < 0 || xCell >= MAX_GRID_CELLS || yCell < 0 || yCell >= MAX_GRID_CELLS) + { + return; // Actor is out of bounds + } + + const float collisionRangeSq = 150.0f * 150.0f; + + // Check platforms in the same and adjacent cells + for (int dx = -1; dx <= 1; dx++) + { + for (int dy = -1; dy <= 1; dy++) + { + int nx = xCell + dx; + int ny = yCell + dy; + + if (nx >= 0 && nx < MAX_GRID_CELLS && ny >= 0 && ny < MAX_GRID_CELLS) + { + PlatformGridCell *cell = &platformGrid[nx][ny]; + for (size_t i = 0; i < cell->count; i++) + { + size_t platformIndex = cell->platformIndices[i]; + float distanceSq = vector3_squaredDistance(&actor->body.position, &platforms[platformIndex].position); + if (distanceSq <= collisionRangeSq) + { + platforms[platformIndex].contact = true; + return; // Early exit on collision + } + } + } + } + } +} + +void platform_loop(Platform *platform, Actor *actor, int diff) +{ + int difficulty = (core_get_playercount() == 4) ? diff : DIFF_EASY; + + // Translate collision + for (int j = 0; j < 3; j++) + platform->collider.box[j].center = platform->position; + + // Run behaviors + // if(actor != NULL) platform_collideCheck(platform, actor); + if (platform->contact) + { + platform->platformTimer++; + + // Action 1: Play sound + if (platform->platformTimer > 0 && platform->platformTimer < 2) + { + sound_wavPlay(SFX_STONES, false); + + // Action 2: Shake platform + } + else if (platform->platformTimer < 60) + { + platform_shake(platform, platform->platformTimer); + + // Action 3: Drop platform + } + else if (platform->platformTimer > 60 && platform->platformTimer < 240) + { + platform_updateHeight(platform, 2.0f + difficulty); + + // Action 4: Reset to idle + } + else if (platform->platformTimer > (200 * (difficulty + 1))) + { + platform->contact = false; + } + } + else + { + + // Reset to initial position + platform->platformTimer = 0; + if (platform->position.z < platform->home.z) + platform->position.z = platform->position.z + 1.0f + difficulty; + } + + // Update matrix + t3d_mat4fp_from_srt_euler( + platform->mat, + (float[3]){1.0f, 1.0f, 1.0f}, + (float[3]){0.0f, 0.0f, 0.0f}, + (float[3]){platform->position.x, platform->position.y, platform->position.z}); +} + +//// BEHAVIORS ~ End //// + +//// RENDERING ~ Start //// + +// T3D MODEL DRAW BATCHING +#define BATCH_LIMIT 19 // Number of objects per rspq block +#define BLOCK_COUNT 1 // Pre-calculated block count + +T3DModel *batchModel = NULL; +rspq_block_t *rspqBlocks[BLOCK_COUNT] = {NULL}; // Static array of rspq block pointers +rspq_block_t *materialBlock = NULL; // Block for single material load and draw + +void platform_createBatch(Platform *platform, T3DModel *model) +{ + // Load model once for the entire batch if not already loaded + if (batchModel == NULL) + { + batchModel = model; + } + + // Create T3DObjects from batch for each platform + for (size_t i = 0; i < PLATFORM_COUNT; i++) + { + platform[i].obj = t3d_model_get_object_by_index(batchModel, 0); + } + + // Create material RSPQ block to run before drawing objects + rspq_block_begin(); + t3d_model_draw_material(platform[0].obj->material, NULL); + materialBlock = rspq_block_end(); + + // Initialize the rspq block index and start a new rspq block + size_t blockIndex = 0; + rspq_block_begin(); + + for (size_t i = 0; i < PLATFORM_COUNT; i++) + { + + // Set the model matrix and draw + t3d_matrix_set(platform[i].mat, true); + rdpq_set_prim_color(platform[i].color); + t3d_model_draw_object(platform[i].obj, NULL); + + // End the current rspq block and start a new one every n objects + if ((i + 1) % BATCH_LIMIT == 0 || i == PLATFORM_COUNT - 1) + { + rspqBlocks[blockIndex] = rspq_block_end(); // Store the completed rspq block + blockIndex++; + if (i < PLATFORM_COUNT - 1) + rspq_block_begin(); // Start a new rspq block if more objects remain + } + } +} + +// Iterate through and run RSPQ blocks +void platform_drawBatch(void) +{ + + // Draw material once per batch + rspq_block_run(materialBlock); + + for (size_t i = 0; i < BLOCK_COUNT; i++) + { + if (rspqBlocks[i] != NULL) // Check for NULL before running + { + rspq_block_run(rspqBlocks[i]); + } + } +} + +// Generate a hexagonal grid of 19 platforms at desired height, with desired model and color +void platform_hexagonGrid(Platform *platform, T3DModel *model, float z, color_t color) +{ + float x_offset = OFFSET; // Horizontal distance between centers of adjacent columns + float y_offset = OFFSET; // Vertical distance between centers of adjacent rows + float start_x = 0.0f; // Starting X coordinate for the first row + float start_y = -500.0f; // Starting Y coordinate for the first row + + int rows[] = {3, 4, 5, 4, 3}; // Number of hexagons per row + int hexagon_index = 0; + + for (int row_index = 0; row_index < 5; row_index++) + { + int hex_count = rows[row_index]; + float row_start_x = start_x - (hex_count - 1) * x_offset / 2.0f; + + for (int i = 0; i < hex_count; i++) + { + platform[hexagon_index].position.x = row_start_x + i * x_offset; + platform[hexagon_index].position.y = start_y + row_index * y_offset; + platform[hexagon_index].position.z = z; + hexagon_index++; + } + } + + // Initialize the platforms + for (size_t p = 0; p < PLATFORM_COUNT; p++) + { + platform_init(&platform[p], model, platform[p].position, color); + } + + platform_assignGrid(platform); + + platform_createBatch(platform, model); +} + +// Frees T3D model, matrices and RSPQ Blocks used for rendering +void platform_destroy(Platform *platform) +{ + rspq_block_free(materialBlock); + + for (size_t b = 0; b < BLOCK_COUNT; b++) + { + if (rspqBlocks[b] != NULL) + rspq_block_free(rspqBlocks[b]); + } + + for (size_t p = 0; p < PLATFORM_COUNT; p++) + { + if (platform[p].mat != NULL) + free_uncached(platform[p].mat); + } + + if (batchModel != NULL) + t3d_model_free(batchModel); +} + +#endif // PLATFORM_H \ No newline at end of file diff --git a/code/sb_hot/scene/room.h b/code/sb_hot/scene/room.h new file mode 100644 index 00000000..f65a93b7 --- /dev/null +++ b/code/sb_hot/scene/room.h @@ -0,0 +1,114 @@ +/* + * This file includes code from Tiny3D. + * Tiny3D is licensed under the MIT License. + * + * Original code by Max Bebök + * Adapted by s4ys + * November 2024 + * + * Description of changes or adaptations made: + * - Modify water like wobble to use a sum of sines instead a single sine wave + * + * + * Original source: https://github.com/HailToDodongo/tiny3d/tree/main/examples/04_dynamic + */ + +#ifndef ROOM_H +#define ROOM_H + +#define DEATH_PLANE_HEIGHT -50.0f +#define LOWER_LIMIT_HEIGHT -200.0f + +#define NUM_HARMONICS 4 + +typedef struct +{ + float amplitude[NUM_HARMONICS]; + float frequency[NUM_HARMONICS]; + float phase[NUM_HARMONICS]; +} HarmonicData; + +// Precomputed harmonic data for the wobble effect +static const HarmonicData wobbleData = { + .amplitude = {10.0f, 5.0f, 3.0f, 1.0f}, + .frequency = {4.5f, 9.0f, 13.5f, 18.0f}, + .phase = {0.0f, 0.5f, 1.0f, 1.5f}}; + +// Hook/callback to modify tile settings set by t3d_model_draw +void tile_scroll(void *userData, rdpq_texparms_t *tileParams, rdpq_tile_t tile) +{ + float offset = *(float *)userData; + + if (tile == TILE0) + { + tileParams->s.translate = offset * 0.5f; + tileParams->t.translate = offset * 0.8f; + + tileParams->s.translate = fm_fmodf(tileParams->s.translate, 32.0f); + tileParams->t.translate = fm_fmodf(tileParams->t.translate, 32.0f); + } +} + +void move_lava(Scenery *scenery) +{ + + scenery[0].transform_offset += 0.008f; + scenery[0].tile_offset += 0.1f; + + // returns the global vertex buffer for a model. + // If you have multiple models and want to only update one, you have to manually iterate over the objects. + // see the implementation of t3d_model_draw_custom in that case. + T3DVertPacked *verts = t3d_model_get_vertices(scenery[0].model); + float globalHeight = 0; + + // Calculate globalHeight as the sum of the harmonics + for (int i = 0; i < NUM_HARMONICS; ++i) + { + globalHeight += wobbleData.amplitude[i] * + fm_sinf(scenery[0].transform_offset * wobbleData.frequency[i] + wobbleData.phase[i]); + } + + for (uint16_t i = 0; i < scenery[0].model->totalVertCount; ++i) + { + int16_t *pos = t3d_vertbuffer_get_pos(verts, i); + + // Water-like wobble effect using a sum of sines + float height = 0.0f; + for (int j = 0; j < NUM_HARMONICS; ++j) + { + height += wobbleData.amplitude[j] * + fm_sinf( + scenery[0].transform_offset * wobbleData.frequency[j] + + wobbleData.phase[j] + pos[0] * (30.1f + j) + pos[1] * (20.1f + j)); + } + + pos[2] = height + globalHeight; + + // Adjust color more subtly based on height + float colorVariation = height * 0.02f; // Reduced scaling factor + float baseIntensity = 0.75f; // Higher base intensity for consistent lighting + float color = baseIntensity + colorVariation; + + uint8_t *rgba = t3d_vertbuffer_get_rgba(verts, i); + rgba[0] = fminf(color, 0.9f) * 255; // Clamp to avoid overflow + rgba[1] = fminf(color, 0.9f) * 200; + rgba[2] = fminf(color, 0.9f) * 200; + rgba[3] = 0xFF; + } + + // Don't forget to flush the cache again! (or use an uncached buffer in the first place) + data_cache_hit_writeback(verts, sizeof(T3DVertPacked) * scenery[0].model->totalVertCount / 2); +} + +void room_draw(Scenery *scenery) +{ + + rdpq_mode_zbuf(false, true); + t3d_model_draw_custom(scenery[0].model, (T3DModelDrawConf){ + .userData = &scenery[0].tile_offset, + .tileCb = tile_scroll, + }); + rdpq_mode_zbuf(true, true); +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/scene/scene.h b/code/sb_hot/scene/scene.h new file mode 100644 index 00000000..b43b0ae0 --- /dev/null +++ b/code/sb_hot/scene/scene.h @@ -0,0 +1,17 @@ +#ifndef SCENE_H +#define SCENE_H + +typedef struct +{ + Camera camera; + LightData light; + +} Scene; + +void scene_init(Scene *scene) +{ + scene->camera = camera_create(); + scene->light = light_create(); +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/scene/scenery.h b/code/sb_hot/scene/scenery.h new file mode 100644 index 00000000..47e3b76c --- /dev/null +++ b/code/sb_hot/scene/scenery.h @@ -0,0 +1,79 @@ +#ifndef SCENERY_H +#define SCENERY_H + +// structures + +typedef struct +{ + + uint32_t id; + rspq_block_t *dl; + T3DMat4FP *modelMat; + T3DModel *model; + + float tile_offset; + float transform_offset; + + Vector3 scale; + Vector3 position; + Vector3 rotation; + +} Scenery; + +// function prototypes + +Scenery scenery_create(uint32_t id, const char *model_path); +void scenery_set(Scenery *scenery); +void scenery_draw(Scenery *scenery); +void scenery_delete(Scenery *scenery); + +// function implementations + +Scenery scenery_create(uint32_t id, const char *model_path) +{ + Scenery scenery = { + .id = id, + .model = t3d_model_load(model_path), + .modelMat = malloc_uncached(sizeof(T3DMat4FP)), // needed for t3d + + .scale = {1.0f, 1.0f, 1.0f}, + .position = {0.0f, 0.0f, 0.0f}, + .rotation = {0.0f, 0.0f, 0.0f}, + }; + + rspq_block_begin(); + t3d_matrix_set(scenery.modelMat, true); + t3d_model_draw(scenery.model); + scenery.dl = rspq_block_end(); + + t3d_mat4fp_identity(scenery.modelMat); + + return scenery; +} + +void scenery_set(Scenery *scenery) +{ + + t3d_mat4fp_from_srt_euler(scenery->modelMat, + (float[3]){scenery->scale.x, scenery->scale.y, scenery->scale.z}, + (float[3]){rad(scenery->rotation.x), rad(scenery->rotation.y), rad(scenery->rotation.z)}, + (float[3]){scenery->position.x, scenery->position.y, scenery->position.z}); +} + +void scenery_draw(Scenery *scenery) +{ + for (int i = 0; i < SCENERY_COUNT; i++) + { + + rspq_block_run(scenery[i].dl); + }; +} + +void scenery_delete(Scenery *scenery) +{ + free_uncached(scenery->modelMat); + t3d_model_free(scenery->model); + rspq_block_free(scenery->dl); +} + +#endif diff --git a/code/sb_hot/screen/screen.h b/code/sb_hot/screen/screen.h new file mode 100644 index 00000000..0091c96a --- /dev/null +++ b/code/sb_hot/screen/screen.h @@ -0,0 +1,65 @@ +#ifndef SCREEN_H +#define SCREEN_H + +typedef struct +{ + + surface_t depthBuffer; + T3DViewport gameplay_viewport; + +} Screen; + +void screen_initDisplay(Screen *screen); +void screen_clearDisplay(Screen *screen); + +void screen_initDisplay(Screen *screen) +{ + display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE); + screen->depthBuffer = surface_alloc(FMT_RGBA16, display_get_width(), display_get_height()); +} + +void screen_clearDisplay(Screen *screen) +{ + rdpq_attach(display_get(), &screen->depthBuffer); + t3d_frame_start(); +} + +void screen_initT3dViewport(Screen *screen) +{ + screen->gameplay_viewport = t3d_viewport_create(); +} + +void screen_clearT3dViewport(Screen *screen) +{ + t3d_viewport_attach(&screen->gameplay_viewport); +} + +void screen_applyColor_Depth(Screen *screen, color_t color, bool fog) +{ + if (fog) + { + rdpq_mode_fog(RDPQ_FOG_STANDARD); + rdpq_set_fog_color(color); + } + + t3d_screen_clear_color(color); + t3d_screen_clear_depth(); + + if (fog) + t3d_fog_set_range(750.0f, 900.0f); +} + +void screen_applyColor(Screen *screen, color_t color, bool fog) +{ + if (fog) + { + rdpq_mode_fog(RDPQ_FOG_STANDARD); + rdpq_set_fog_color(color); + } + t3d_screen_clear_color(color); + + if (fog) + t3d_fog_set_range(750.0f, 900.0f); +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/sound/sound.h b/code/sb_hot/sound/sound.h new file mode 100644 index 00000000..2365392c --- /dev/null +++ b/code/sb_hot/sound/sound.h @@ -0,0 +1,298 @@ +#ifndef SOUND_H +#define SOUND_H + +#include + +// Core Definitions +#define MUSIC_CHANNEL 0 +#define SFX_CHANNEL 31 + +// XM sequences +enum BG_XM +{ + XM_FF, + NUM_XM +}; + +xm64player_t xmPlayer; + +// const char* xmFileNames[NUM_XM] = { +// "rom:/strawberry_byte/sound/tribeof.xm64", +// }; + +// WAV files +enum SFX_WAV +{ + SFX_JUMP, + SFX_STONES, + SFX_LAVA, + SFX_START, + SFX_COUNTDOWN, + SFX_STOP, + SFX_WINNER, + NUM_WAV +}; + +// Each WAV must have its own structure +wav64_t soundEffects[NUM_WAV]; + +const char *wavFileNames[NUM_WAV] = { + "rom:/strawberry_byte/sound/grunt-01.wav64", + "rom:/strawberry_byte/sound/stones-falling.wav64", + "rom:/strawberry_byte/sound/hexagone.wav64", + "rom:/core/Start.wav64", + "rom:/core/Countdown.wav64", + "rom:/core/Stop.wav64", + "rom:/core/Winner.wav64", +}; + +/* Function Declarations */ +void sound_load(void); +void sound_xmSwitch(int songID, float volume, bool loop); +void sound_xmStop(void); +void sound_xmUpdate(float volume, bool loop); +void sound_wavPlay(int sfxID, bool loop); +void sound_wavPlayBG(int sfxID); +void sound_wavClose(int sfxID); +void sound_wavCleanup(void); +void sound_cleanup(void); +void sound_update(void); + +/* Function Definitions */ + +void sound_load(void) +{ + // Open all WAVs at boot + for (int w = 0; w < NUM_WAV; ++w) + wav64_open(&soundEffects[w], wavFileNames[w]); + + mixer_ch_set_vol_pan(MUSIC_CHANNEL, 0.5f, 0.5f); + // Open and play first XM in the list + // xm64player_open(&xmPlayer, xmFileNames[0]); + // xm64player_set_vol(&xmPlayer, 0.5f); + // xm64player_play(&xmPlayer, MUSIC_CHANNEL); +} + +// Stops current XM, opens and plays requested module with set volume and whether to loop +void sound_xmSwitch(int songID, float volume, bool loop) +{ + xm64player_stop(&xmPlayer); + xm64player_close(&xmPlayer); + // xm64player_open(&xmPlayer, xmFileNames[songID]); + xm64player_set_loop(&xmPlayer, loop); + xm64player_set_vol(&xmPlayer, volume); + xm64player_play(&xmPlayer, MUSIC_CHANNEL); +} + +// Stops and closes XM player +void sound_xmStop(void) +{ + xm64player_stop(&xmPlayer); +} + +// Adjusts volume and looping of current XM module +void sound_xmUpdate(float volume, bool loop) +{ + xm64player_set_loop(&xmPlayer, loop); + xm64player_set_vol(&xmPlayer, volume); +} + +// Plays requested WAV and whether to loop +void sound_wavPlay(int sfxID, bool loop) +{ + wav64_set_loop(&soundEffects[sfxID], loop); + wav64_play(&soundEffects[sfxID], SFX_CHANNEL - sfxID); +} + +void sound_wavPlayBG(int sfxID) +{ + wav64_set_loop(&soundEffects[sfxID], true); + wav64_play(&soundEffects[sfxID], MUSIC_CHANNEL); +} + +void sound_wavClose(int sfxID) +{ + wav64_close(&soundEffects[sfxID]); +} + +void sound_wavCleanup(void) +{ + for (int w = 0; w < NUM_WAV; ++w) + wav64_close(&soundEffects[w]); +} + +void sound_cleanup(void) +{ + sound_wavCleanup(); +} + +void sound_update(void) +{ + mixer_try_play(); +} + +// Function to calculate forward facing direction of the camera based on its target +Vector3 calculate_camera_forward(const Camera *camera) +{ + Vector3 direction = { + camera->target.x - camera->position.x, + camera->target.y - camera->position.y, + camera->target.z - camera->position.z}; + vector3_normalize(&direction); + return direction; +} + +////// Audio filters + +#define REVERB_BUFFER_SIZE 32000 // Size for the delay buffer +#define MAX_COMB_FILTERS 3 +#define MAX_ALLPASS_FILTERS 2 + +typedef struct +{ + float comb_delays[MAX_COMB_FILTERS]; + float comb_feedback; + float allpass_delays[MAX_ALLPASS_FILTERS]; + float allpass_feedback; + float sample_rate; +} ReverbParams; + +// Schroeder Reverberator Parameters +ReverbParams paramsSchroeder = { + {2.0f, 3.0f, 4.0f}, // Comb filter delays in frames + 0.4f, + {1.0f, 2.0f}, // All-pass filter delays in frames + 0.3f, + 16000.0f, +}; + +// Circular buffers for the comb and all-pass filters +float comb_delay_buffer[REVERB_BUFFER_SIZE]; +int comb_buffer_index = 0; + +float allpass_delay_buffer[REVERB_BUFFER_SIZE]; +int allpass_buffer_index = 0; + +// Comb Filter implementation +float comb_filter(float input, float delay_seconds, float feedback, float sample_rate) +{ + int delay_samples = (int)(delay_seconds * sample_rate); + int buffer_index = (comb_buffer_index + REVERB_BUFFER_SIZE - delay_samples) % REVERB_BUFFER_SIZE; + + float delayed_sample = comb_delay_buffer[buffer_index]; + float output = delayed_sample + input; + + // Store the output with feedback in the buffer + comb_delay_buffer[comb_buffer_index] = input + delayed_sample * feedback; + comb_buffer_index = (comb_buffer_index + 1) % REVERB_BUFFER_SIZE; + + return output; +} + +// All-Pass Filter implementation +float allpass_filter(float input, float delay_seconds, float feedback, float sample_rate) +{ + int delay_samples = (int)(delay_seconds * sample_rate); + int buffer_index = (allpass_buffer_index + REVERB_BUFFER_SIZE - delay_samples) % REVERB_BUFFER_SIZE; + + float delayed_sample = allpass_delay_buffer[buffer_index]; + float output = delayed_sample - (input * feedback) + input; + + // Store the new input into the delay buffer + allpass_delay_buffer[allpass_buffer_index] = input + delayed_sample * feedback; + allpass_buffer_index = (allpass_buffer_index + 1) % REVERB_BUFFER_SIZE; + + return output; +} + +// Applies reverb based on current volume and set mix +float sound_reverb(float volume, float mix) +{ + + if (volume < 0.2f) + volume = 0.2f; // Clamp volume to minimum + + // Apply comb filters + float reverb_volume = 0.0f; + for (int i = 0; i < MAX_COMB_FILTERS; i++) + reverb_volume += comb_filter(volume, paramsSchroeder.comb_delays[i], paramsSchroeder.comb_feedback, paramsSchroeder.sample_rate); + + // Apply all-pass filters + for (int i = 0; i < MAX_ALLPASS_FILTERS; i++) + reverb_volume = allpass_filter(reverb_volume, paramsSchroeder.allpass_delays[i], paramsSchroeder.allpass_feedback, paramsSchroeder.sample_rate); + + // Mix original sound with reverb + return volume * (1.0f - mix) + reverb_volume * mix; +} + +////// Spatial 3D sound experiment +void sound_spatial(const Vector3 *spawner, + const Vector3 *player, + const Camera *camera) +{ + + float pan = 0.5f; + float volume = 1.0f; + + // Calculate distance vector for volume attenuation + Vector3 diff = vector3_difference(spawner, player); + float distance = vector3_magnitude(&diff); + + // If close enough to the sound spawner, skip unnecessary spatial calculations + if (distance > 250.0f) + { + // Volume attenuation (inverse-square or linear falloff) + float max_distance = 8000.0f; // Maximum distance for audible sound + volume = 1.0f - (distance / max_distance); + volume = fmaxf(0.1f, volume); // Clamp volume to minimum + + // Calculate the horizontal angle (azimuth) for panning + Vector3 horizontal_diff = {diff.x, diff.y, 0.0f}; + float horizontal_distance = vector3_magnitude(&horizontal_diff); + + // Avoid division by zero + if (horizontal_distance > 0.0f) + { + Vector3 player_forward = calculate_camera_forward(camera); + float cos_angle = vector3_returnDotProduct(&horizontal_diff, &player_forward) / horizontal_distance; + float angle = acosf(cos_angle); // Angle in radians + + // Determine if sound is to the left or right + pan = 0.5f * (1.0f - angle / M_PI); // Centered at 0.5, where 0 is left and 1 is right + + // Flip pan if the sound source is on the right side + if (diff.x * player_forward.y - diff.y * player_forward.x > 0) + pan = 1.0f - pan; + + // Cap the pan + pan = fminf(0.8f, fmaxf(0.2f, pan)); + } + else + { + // If horizontal_distance is zero, keep the pan centered + pan = 0.5f; + } + + // Apply a vertical attenuation if sound changes by height + float vertical_attenuation = fminf(1.0f, fmaxf(0.1f, 1.0f - fabsf(diff.z) / max_distance)); + + // Volume adjustment with vertical attenuation + volume *= vertical_attenuation; + + // Calculate reverb mix based on distance (0.3 at 250, 1.0 at max_distance) + float min_reverb_distance = 250.0f; + float mix = (distance - min_reverb_distance) / (max_distance - min_reverb_distance); + mix = fminf(1.0f, fmaxf(0.3f, mix)); // Clamp mix to [0.0, 1.0] + + // Apply reverb + volume = sound_reverb(volume, mix); + } + + // Set the volume and panning for each SFX channel + for (int i = 0; i < NUM_WAV; i++) + { + mixer_ch_set_vol_pan(SFX_CHANNEL - i, volume, pan); + } +} + +#endif // SOUND_H \ No newline at end of file diff --git a/code/sb_hot/time/time.h b/code/sb_hot/time/time.h new file mode 100644 index 00000000..244fb67c --- /dev/null +++ b/code/sb_hot/time/time.h @@ -0,0 +1,43 @@ +#ifndef TIME_H +#define TIME_H + +// structures + +typedef struct +{ + + uint32_t frame_counter; + float frame_time_s; + float fixed_time_s; + float frame_rate; + double subtick; + +} TimeData; + +// functions prototypes + +void time_init(TimeData *time); +void time_setData(TimeData *time); + +// functions implementations + +/* sets time data values to zero */ +void time_init(TimeData *time) +{ + time->frame_counter = 0; + time->frame_time_s = 0.0f; + time->fixed_time_s = 0.04f; // Hardcoded + time->frame_rate = 0.0f; + time->subtick = 0.0; +} + +/* sets timing data */ +void time_setData(TimeData *time) +{ + // Update timing values + time->frame_rate = display_get_fps(); // Retrieve current frames per second + time->subtick = core_get_subtick(); // Retrieve subtick of the frame + time->frame_counter++; +} + +#endif \ No newline at end of file diff --git a/code/sb_hot/ui/ui.h b/code/sb_hot/ui/ui.h new file mode 100644 index 00000000..193f745f --- /dev/null +++ b/code/sb_hot/ui/ui.h @@ -0,0 +1,455 @@ +#ifndef UI_H +#define UI_H + +#include "ui_font.h" +#include "ui_colors.h" +#include "ui_sprite.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +// @TODO: Recreate button sprite tiling to remove these insanity checks. +#define aPressed 2 +#define aIdle 3 +#define aHeld 2 + +#define bPressed 0 +#define bIdle 1 +#define bHeld 3 + +#define cIdle 0 + enum cStates + { + C_UP, + C_DOWN, + C_LEFT, + C_RIGHT + }; + + enum MENU_TEXT + { + TEXT_DIFF, + TEXT_PLAYERS = +4, + TEXT_BOTS, + TEXT_CONTROLS, + TEXT_RUMBLE, + MENU_TEXT_COUNT + }; + + const char *uiMainMenuStrings[MENU_TEXT_COUNT] = { + "Difficulty: ", + "EASY", + "MEDIUM", + "HARD", + "Players: ", + "Bots: ", + " to Move\n to Jump", + "Insert Rumble Pak now!"}; + + const char *uiTipStrings[ACTOR_COUNT] = { + "Platforms will fall when stood on.\nHurry to the closest safe one.", + "Hold A to extend jump time.\nSee if you can make\na 2 platform gap.", + "Try to corner opponents\nand reducing their exit routes.", + "Remember to roll 10d10\nwhen you fall in the lava."}; + + const char *uiCharacterSelectStrings[ACTOR_COUNT] = { + "s4ys", + "Wolfie", + "Mewde", + "Olli"}; + + /* Declarations */ + + void ui_init(void); + void ui_syncText(void); + void ui_fps(float frame_rate, float x, float y); + void ui_printf(float x, float y, const char *txt, ...); + void ui_main_menu(ControllerData *control, int diff); + void ui_input_display(ControllerData *control); + void ui_textbox(void); + void ui_cleanup(void); + + /* Definitons */ + + void ui_init(void) + { + ui_fontRegister(); + ui_spriteLoad(); + } + + // Optional RDPQ sync and set for text, to prevent bleeding if the autosync engine misses something. + void ui_syncText(void) + { + rdpq_sync_pipe(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_sync_tile(); + } + + void ui_fps(float frame_rate, float x, float y) + { + heap_stats_t heap_stats; + sys_get_heap_stats(&heap_stats); + ui_syncText(); + rdpq_text_printf(&txt_debugParms, ID_DEBUG, x, y, "FPS %.2f Mem: %d KiB", frame_rate, heap_stats.used / 1024); + } + + void ui_printf(float x, float y, const char *txt, ...) + { + ui_syncText(); + + va_list args; + va_start(args, txt); + rdpq_text_vprintf(&txt_debugParms, ID_DEBUG, x, y, txt, args); + } + + void ui_print_winner(int winner) + { + ui_spriteDrawPanel(TILE1, sprite_gloss, T_BLACK, 96, 102, 210, 130, 0, 0, 64, 64); + ui_syncText(); + if (winner != 5) // 5 signifies a Draw + { + rdpq_textparms_t winnerTextParms = txt_gameParms; + winnerTextParms.style_id = STYLE_PLAYER + winner - 1; + rdpq_text_printf(&winnerTextParms, ID_DEFAULT, 110, 120, "Player %d Wins", winner); + } + else + { + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 132, 120, "DRAW!"); + } + } + + void ui_print_playerNum(Player *player, Screen *screen) + { + Vector3 pos = player_getBillboard(player, &screen->gameplay_viewport); + ui_syncText(); + rdpq_textparms_t playerTextParms = txt_gameParms; + playerTextParms.style_id = STYLE_PLAYER + player->id; + rdpq_text_printf(&playerTextParms, ID_DEFAULT, pos.x, pos.z, "P%d", player->id + 1); + } + + void ui_countdown(int secondsLeft) + { + // Convert secondsLeft integer to a string + char countdownText[2]; + snprintf(countdownText, sizeof(countdownText), "%d", secondsLeft); + + ui_syncText(); + rdpq_text_printf(&txt_titleParms, ID_TITLE, 150, 110, "%s", countdownText); + + // One of the four tips at random during countdown + static int randomTip = -1; + if (randomTip == -1) + randomTip = rand() % 4; + txt_titleParms.align = ALIGN_CENTER; + txt_titleParms.width = 320; + rdpq_text_printf(&txt_titleParms, ID_DEFAULT, 0, 150, "%s", uiTipStrings[randomTip]); + txt_titleParms.align = ALIGN_LEFT; + txt_titleParms.width = 0; + } + + // Controller data is passed here for visual feedback for the button press. + void ui_main_menu(ControllerData *control, int diff) + { + // Panels + ui_spriteDrawPanel(TILE2, sprite_gloss, T_RED, 90, 40, 230, 124, 0, 0, 64, 64); + ui_spriteDrawPanel(TILE3, sprite_tessalate, T_BLACK, 100, 45, 220, 114, 0, 0, 64, 64); + + // Buttons + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 1, 170, 90); + } + else + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 0, 170, 90); + } + ui_spriteDraw(TILE5, sprite_controlStick, 0, 92, 170); + int stickX = 92 + (control->input.stick_x / 15); + int stickY = 138 + (spriteHeight * 2) - (control->input.stick_y / 15); + ui_spriteDraw(TILE5, sprite_controlStick, 1, stickX, stickY); + if (control->pressed.a || control->held.a) + { + ui_spriteDraw(TILE6, sprite_faceButtons1, aHeld, 92, 186); + } + else + { + ui_spriteDraw(TILE6, sprite_faceButtons0, aIdle, 92, 186); + } + + int count = core_get_playercount(); + static int set = 0; + if (count == 4) + { + + if (control->pressed.b || control->held.b) + { + ui_spriteDraw(TILE4, sprite_faceButtons1, bHeld, 74, 128); + } + else + { + ui_spriteDraw(TILE4, sprite_faceButtons1, bIdle, 74, 128); + } + + set = diff; + } + else + { + set = core_get_aidifficulty(); + } + + // Text + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, 106, 64, " Hot Hot\nHexagons"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 128, 102, "Press"); + rdpq_text_printf(&txt_gameParms, ID_DEFAULT, 92, 140, + "%s %s\n" + "%s %d\n" + "%s %d\n" + "%s\n\n" + "%s\n", + uiMainMenuStrings[TEXT_DIFF], uiMainMenuStrings[TEXT_DIFF + set + 1], + uiMainMenuStrings[TEXT_PLAYERS], count, + uiMainMenuStrings[TEXT_BOTS], ACTOR_COUNT - count, + uiMainMenuStrings[TEXT_CONTROLS], + uiMainMenuStrings[TEXT_RUMBLE]); + } + + void ui_pause(ControllerData *control) + { + + ui_spriteDrawPanel(TILE2, sprite_gloss, T_RED, 90, 60, 230, 144, 0, 0, 64, 64); + ui_spriteDrawPanel(TILE3, sprite_tessalate, T_BLACK, 100, 65, 220, 134, 0, 0, 64, 64); + + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 1, 170, 110); + } + else + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 0, 170, 110); + } + + ui_spriteDraw(TILE5, sprite_dPadTriggers, 5, 160, 180); + + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, 106, 84, " Hot Hot\nHexagons"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 128, 122, "Press\n\n\n PAUSED\n\nHold to\nQuit Game"); + } + + void ui_character_select(ControllerData *control, uint8_t selectedActor) + { + // Buttons + if (control->pressed.a || control->held.a) + { + ui_spriteDraw(TILE2, sprite_faceButtons1, aHeld, 104, 46); + } + else + { + ui_spriteDraw(TILE2, sprite_faceButtons0, aIdle, 104, 46); + } + + // Text + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, 70, 40, "Character Select"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 63, 58, "Press to Confirm Selection"); + rdpq_text_printf(&txt_titleParms, ID_DEFAULT, 84, 76, "Selected Actor: %s", uiCharacterSelectStrings[selectedActor]); + } + + // Time to crash test the RDP + void ui_input_display(ControllerData *control) + { + int s = 24; + int t = 164; + + // First row: Triggers + if (control->pressed.l || control->held.l) + ui_spriteDraw(TILE0, sprite_dPadTriggers, 6, s, t); + if (control->pressed.z || control->held.z) + ui_spriteDraw(TILE1, sprite_dPadTriggers, 5, s + spriteWidth, t); + if (control->pressed.r || control->held.r) + ui_spriteDraw(TILE2, sprite_dPadTriggers, 7, s + (spriteWidth * 2), t); + + // Second row: Face Buttons + if (control->pressed.a || control->held.a) + { + ui_spriteDraw(TILE3, sprite_faceButtons1, aHeld, s, t + spriteHeight); + } + else + { + ui_spriteDraw(TILE3, sprite_faceButtons0, aIdle, s, t + spriteHeight); + } + + if (control->pressed.b || control->held.b) + { + ui_spriteDraw(TILE4, sprite_faceButtons1, bHeld, s + spriteHeight, t + spriteHeight); + } + else + { + ui_spriteDraw(TILE4, sprite_faceButtons1, bIdle, s + spriteHeight, t + spriteHeight); + } + + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE5, sprite_faceButtons0, 1, s + (spriteHeight * 2), t + spriteHeight); + } + else + { + ui_spriteDraw(TILE5, sprite_faceButtons0, 0, s + (spriteHeight * 2), t + spriteHeight); + } + + // Third row: Sticks + ui_spriteDraw(TILE6, sprite_controlStick, 0, s, t + (spriteHeight * 2)); + int stickX = s + (control->input.stick_x / 15); + int stickY = t + (spriteHeight * 2) - (control->input.stick_y / 15); + ui_spriteDraw(TILE6, sprite_controlStick, 1, stickX, stickY); + if (control->pressed.c_up || control->held.c_up) + { + ui_spriteDraw(TILE7, sprite_cButtons1, C_UP, s + (spriteHeight * 2), t + (spriteHeight * 2)); + } + else if (control->pressed.c_down || control->held.c_down) + { + ui_spriteDraw(TILE7, sprite_cButtons1, C_DOWN, s + (spriteHeight * 2), t + (spriteHeight * 2)); + } + else if (control->pressed.c_left || control->held.c_left) + { + ui_spriteDraw(TILE7, sprite_cButtons1, C_LEFT, s + (spriteHeight * 2), t + (spriteHeight * 2)); + } + else if (control->pressed.c_right || control->held.c_right) + { + ui_spriteDraw(TILE7, sprite_cButtons1, C_RIGHT, s + (spriteHeight * 2), t + (spriteHeight * 2)); + } + else + { + ui_spriteDraw(TILE7, sprite_cButtons0, 0, s + (spriteHeight * 2), t + (spriteHeight * 2)); + } + } + + void ui_intro(ControllerData *control) + { + // Basic frame counter for timing + static uint32_t introTimer = 0; + const float refreshRate = display_get_fps(); + introTimer++; + + // Animated text positions + static Vector2 topTextPosition = {93.0f, 0.0f}; + topTextPosition.y = topTextPosition.y + 2.0f; + if (topTextPosition.y > 56.0f) + topTextPosition.y = 56.0f; + + // Dynamic alpha from prim colors + uint32_t dynamicColorsPacked[3] = {0, 0, 0}; + color_t dynamicColors[3]; + dynamicColorsPacked[0] = ui_colorSetAlpha(COLORS[N_RED], introTimer * refreshRate * display_get_delta_time()); + dynamicColorsPacked[1] = ui_colorSetAlpha(COLORS[N_GREEN], introTimer * refreshRate * display_get_delta_time()); + dynamicColorsPacked[2] = ui_colorSetAlpha(COLORS[N_YELLOW], introTimer * refreshRate * display_get_delta_time()); + dynamicColors[0] = color_from_packed32(dynamicColorsPacked[0]); + dynamicColors[1] = color_from_packed32(dynamicColorsPacked[1]); + dynamicColors[2] = color_from_packed32(dynamicColorsPacked[2]); + + if (introTimer < refreshRate * 6.0f) + { + + /* MADE WITH SCREEN */ + + // Panels + ui_spriteDrawDynamic(TILE1, sprite_libdragon, dynamicColors[0], 28, 76, 156, 140, 0, 0, 128, 64); + ui_spriteDrawDynamic(TILE2, sprite_t3d, dynamicColors[1], 160, 76, 288, 140, 0, 0, 64, 32); + ui_spriteDrawDynamic(TILE3, sprite_mixamo, dynamicColors[2], 96, 146, 224, 210, 0, 0, 128, 64); + + // Buttons + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 1, 132, 214); + } + else + { + ui_spriteDraw(TILE4, sprite_faceButtons0, 0, 132, 214); + } + + // Text + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, topTextPosition.x, topTextPosition.y, "Made with"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 90, 226, "Press to Skip Intro"); + } + else if (introTimer < refreshRate * 10.0f) + { + + /* STRAWBERRY SCREEN */ + + // Panels + ui_spriteDrawPanel(TILE1, sprite_strawberryTop, WHITE, 128, 80, 196, 112, 0, 0, 32, 16); + if (introTimer >= refreshRate * 8.0f) + { + ui_spriteDrawPanel(TILE2, sprite_strawberry1, WHITE, 128, 112, 196, 144, 0, 0, 32, 16); + } + else + { + ui_spriteDrawPanel(TILE2, sprite_strawberry0, WHITE, 128, 112, 196, 144, 0, 0, 32, 16); + } + + // Buttons + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE3, sprite_faceButtons0, 1, 132, 214); + } + else + { + ui_spriteDraw(TILE3, sprite_faceButtons0, 0, 132, 214); + } + + // Text + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, 68, 56, "Strawberry Byte"); + if (introTimer >= refreshRate * 8.0f) + rdpq_text_print(&txt_titleParms, ID_TITLE, 110, 190, "Presents"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 90, 226, "Press to Skip Intro"); + } + else + { + + // Buttons + if (control->pressed.start || control->held.start) + { + ui_spriteDraw(TILE3, sprite_faceButtons0, 1, 170, 66); + } + else + { + ui_spriteDraw(TILE3, sprite_faceButtons0, 0, 170, 66); + } + + // Text + ui_syncText(); + rdpq_text_print(&txt_titleParms, ID_TITLE, 106, 40, " Hot Hot\nHexagons"); + rdpq_text_print(&txt_gameParms, ID_DEFAULT, 128, 78, "Press"); + rdpq_text_print(&txt_titleParms, ID_DEFAULT, 32, 94, "CREDITS:"); + + // @TODO: Probably should make this a rdpq_paragraph + rdpq_text_print(&txt_gameParms, ID_DEBUG, 32, 114, + "Programming: zoncabe, s4ys\n" + "Models: zoncabe, mewde, s4ys\n" + "- Lava model by HailToDodongo\n" + "- Original 'Olli' by FazanaJ\n" + "Strawberry Sprite by Sonika Rud\n" + "UI Sprites by Kenney\n" + "Music by Kaelin Stemmler\n" + "SFX obtained from Freesound"); + } + } + + void ui_cleanup(void) + { + ui_spriteCleanup(); + ui_fontUnregister(); + ui_fileCleanup(); + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_H \ No newline at end of file diff --git a/code/sb_hot/ui/ui_colors.h b/code/sb_hot/ui/ui_colors.h new file mode 100644 index 00000000..619c87a8 --- /dev/null +++ b/code/sb_hot/ui/ui_colors.h @@ -0,0 +1,102 @@ +#ifndef UI_COLORS_H +#define UI_COLORS_H + +#ifdef __cplusplus +extern "C" +{ +#endif + + enum COLOR_NAMES + { + // Standard ROYGBIV + RED, + ORANGE, + YELLOW, + GREEN, + BLUE, + INDIGO, + VIOLET, + // RGB 0 (full black) & 1 (full white) + BLACK, + WHITE, + // RGB 1 * (n*.25f) + LIGHT_GREY, // n = 3 + GREY, // n = 2 + DARK_GREY, // n = 1 + // Transparent Colors + TRANSPARENT, + T_RED, + T_ORANGE, + T_YELLOW, + T_GREEN, + T_BLUE, + T_INDIGO, + T_VIOLET, + T_BLACK, + T_WHITE, + T_GREY, + // Darker Variants + DARK_RED, + DARK_GREEN, + // N64 Logo Colors + N_RED, + N_YELLOW, + N_GREEN, + N_BLUE, + COLOR_COUNT + }; + + // FMT_RGBA32, 32-bit packed RGBA (8888) + const uint32_t COLORS[COLOR_COUNT] = + { + 0xD90000FF, // RED + 0xFF6822FF, // ORANGE + 0xFFDA21FF, // YELLOW + 0x33DD00FF, // GREEN + 0x1133CCFF, // BLUE + 0x220066FF, // INDIGO + 0x330044FF, // VIOLET + 0x000000FF, // BLACK + 0xFFFFFFFF, // WHITE + 0xC0C0C0FF, // LIGHT_GREY + 0x808080FF, // GREY + 0x404040FF, // DARK_GREY + 0x0000007F, // TRANSPARENT + 0xD90000C8, // T_RED + 0xFF6822C8, // T_ORANGE + 0xFFDA21C8, // T_YELLOW + 0x33DD00C8, // T_GREEN + 0x1133CCC8, // T_BLUE + 0x220066C8, // T_INDIGO + 0x330044C8, // T_VIOLET + 0x1F1F1FC8, // T_BLACK + 0xFFFFFFC8, // T_WHITE + 0xC0C0C0C8, // T_GREY + 0x820000FF, // DARK_RED + 0x006400FF, // DARK_GREEN + 0xE10916FF, // N_RED + 0xF5B201FF, // N_YELLOW + 0x319900FF, // N_GREEN + 0x01009AFF, // N_BLUE + }; + + inline color_t ui_color(int colorIdx); + uint32_t ui_colorSetAlpha(uint32_t color, uint8_t alpha); + + // Creates a color_t from one of the 32-bit packed COLORS. + inline color_t ui_color(int colorIdx) + { + return color_from_packed32(COLORS[colorIdx]); + } + + // Clears the alpha bits and sets them to the new value + uint32_t ui_colorSetAlpha(uint32_t color, uint8_t alpha) + { + return (color & 0xFFFFFF00) | (alpha & 0xFF); + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_COLORS_H \ No newline at end of file diff --git a/code/sb_hot/ui/ui_file.h b/code/sb_hot/ui/ui_file.h new file mode 100644 index 00000000..923a904c --- /dev/null +++ b/code/sb_hot/ui/ui_file.h @@ -0,0 +1,133 @@ +#ifndef UI_FILE_H +#define UI_FILE_H + +#define NUM_FONTS 2 +#define NUM_SPRITES 6 + +#ifdef __cplusplus +extern "C" +{ +#endif + + // Base directory for UI assets. + const char *basePath = "rom:/strawberry_byte/ui/"; + + // Arrays for font file names and paths: + // - uiFontFileName: Array of pointers to store full paths to font files after initialization. + // - uiFontPaths: Constant array of relative paths for each font, appended to basePath at runtime. + const char *uiFontFileName[NUM_FONTS]; + const char *uiFontPaths[NUM_FONTS] = { + "fonts/TitanOne-Regular.font64", + "fonts/OilOnTheWater-ee5O.font64"}; + + // Arrays for button sprite file names and paths. + const char *uiSpriteButtonFileName[NUM_SPRITES]; + const char *uiSpriteButtonPath[NUM_SPRITES] = { + "buttons/control_stick.ia8.sprite", + "buttons/d_pad_triggers.ia8.sprite", + "buttons/c_buttons0.rgba32.sprite", + "buttons/c_buttons1.rgba32.sprite", + "buttons/face_buttons0.rgba32.sprite", + "buttons/face_buttons1.rgba32.sprite"}; + + // Arrays for button sprite file names and paths. + const char *uiSpritePanelFileName[NUM_FONTS]; + const char *uiSpritePanelPath[NUM_FONTS] = { + "panels/gloss.ia4.sprite", + "panels/pattern_tessalate.ia4.sprite", + }; + + // Arrays for logo sprite file names and paths. See LICENSE.txt for attribution. + const char *uiSpriteLogoFileName[NUM_SPRITES]; + const char *uiSpriteLogoPath[NUM_SPRITES] = { + "logos/sb_b0.rgba32.sprite", + "logos/sb_b1.rgba32.sprite", + "logos/sb_top.rgba32.sprite", + "logos/t3d.ia8.sprite", + "logos/libdragon.ia4.sprite", + "logos/mixamo.ia4.sprite"}; + + /* Declarations */ + + char *ui_filePath(const char *fn); + void ui_fileFonts(void); + void ui_fileSprites(void); + void ui_fileLogos(void); + void ui_fileGet(void); + void ui_fileCleanup(void); + + /* Definitions */ + + // Concatenates basePath and fn, returning the full path (caller must free memory). + char *ui_filePath(const char *fn) + { + char *fullPath = (char *)malloc(256 * sizeof(char)); + if (!fullPath) + { + return NULL; + } + + sprintf(fullPath, "%s%s", basePath, fn); + + return fullPath; + } + + // Populates uiFontFileName with full paths for fonts. + void ui_fileFonts(void) + { + for (int i = 0; i < NUM_FONTS; ++i) + { + uiFontFileName[i] = ui_filePath(uiFontPaths[i]); + } + } + + // Populates uiSpriteButtonFileName and uiSpritePanelFileName with full paths for sprites. + void ui_fileSprites(void) + { + for (int i = 0; i < NUM_SPRITES; ++i) + { + uiSpriteButtonFileName[i] = ui_filePath(uiSpriteButtonPath[i]); + } + for (int i = 0; i < NUM_FONTS; ++i) + { + uiSpritePanelFileName[i] = ui_filePath(uiSpritePanelPath[i]); + } + } + + void ui_fileLogos(void) + { + for (int i = 0; i < NUM_SPRITES; ++i) + { + uiSpriteLogoFileName[i] = ui_filePath(uiSpriteLogoPath[i]); + } + } + + // Calls functions to initialize font and sprite file paths. + void ui_fileGet(void) + { + ui_fileFonts(); + ui_fileSprites(); + ui_fileLogos(); + } + + // Frees memory allocated for font and sprite file paths. + void ui_fileCleanup(void) + { + for (int i = 0; i < NUM_FONTS; i++) + { + free((char *)uiFontFileName[i]); + free((char *)uiSpritePanelFileName[i]); + } + + for (int i = 0; i < NUM_SPRITES; i++) + { + free((char *)uiSpriteButtonFileName[i]); + free((char *)uiSpriteLogoFileName[i]); + } + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_FILE_H \ No newline at end of file diff --git a/code/sb_hot/ui/ui_font.h b/code/sb_hot/ui/ui_font.h new file mode 100644 index 00000000..64b95f69 --- /dev/null +++ b/code/sb_hot/ui/ui_font.h @@ -0,0 +1,154 @@ +#ifndef UI_FONT_H +#define UI_FONT_H + +#include "ui_colors.h" +#include "ui_file.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + enum FONT_IDS + { + ID_RESERVED, + ID_DEBUG, + ID_DEFAULT, + ID_TITLE, + ID_COUNT + }; + + // Array of pointers to rdpq_font_t, with each entry representing a font identified by an ID. + rdpq_font_t *font[ID_COUNT]; + + enum FONT_STYLES + { + STYLE_DEBUG, + STYLE_DEFAULT, + STYLE_TITLE, + STYLE_BRIGHT, + STYLE_GREEN, + STYLE_PLAYER, + STYLE_COUNT = STYLE_PLAYER + MAXPLAYERS + }; + + // RDPQ text parameters, used here primarily to set the following RDPQ font styles. + rdpq_textparms_t txt_debugParms; + rdpq_textparms_t txt_titleParms; + rdpq_textparms_t txt_gameParms; + + // RDPQ font styles, used here primarily to set text color. + rdpq_fontstyle_t txt_debug_fontStyle; + rdpq_fontstyle_t txt_player_fontStyle; + rdpq_fontstyle_t txt_title_fontStyle; + rdpq_fontstyle_t txt_game_fontStyle; + rdpq_fontstyle_t txt_bright_fontStyle; + rdpq_fontstyle_t txt_green_fontStyle; + + /* Declarations */ + + void ui_fontRegister(void); + void ui_fontUnregister(void); + + /* Definitions */ + + /* All in one font initialization. + - Loads and regsiters fonts. + - Assigns colors to font styles. + - Register font styles for each font. + - Assigns font styles to text parameters. + + Possible improvements would be to separate functionality, + to make fonts more flexible and modular. */ + void ui_fontRegister(void) + { + ui_fileFonts(); + + font[ID_DEBUG] = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_VAR); + font[ID_DEFAULT] = rdpq_font_load(uiFontFileName[0]); + font[ID_TITLE] = rdpq_font_load(uiFontFileName[1]); + + // Create and register font styles + txt_debug_fontStyle.color = ui_color(YELLOW); + txt_debug_fontStyle.outline_color = ui_color(BLACK); + + const color_t playerColors[MAXPLAYERS] = { + PLAYERCOLOR_1, + PLAYERCOLOR_2, + PLAYERCOLOR_3, + PLAYERCOLOR_4, + }; + + txt_player_fontStyle.outline_color = ui_color(BLACK); + + txt_game_fontStyle.color = ui_color(WHITE); + txt_game_fontStyle.outline_color = ui_color(T_BLACK); + + txt_title_fontStyle.color = ui_color(YELLOW); + txt_title_fontStyle.outline_color = ui_color(DARK_RED); + + txt_bright_fontStyle.color = ui_color(YELLOW); + txt_bright_fontStyle.outline_color = ui_color(T_BLACK); + + txt_green_fontStyle.color = ui_color(GREEN); + txt_green_fontStyle.outline_color = ui_color(DARK_GREEN); + + for (int i = 1; i < ID_COUNT; i++) + { + rdpq_text_register_font(i, font[i]); + + rdpq_font_style( + font[i], + STYLE_DEFAULT, + &txt_game_fontStyle); + + rdpq_font_style( + font[i], + STYLE_TITLE, + &txt_title_fontStyle); + + rdpq_font_style( + font[i], + STYLE_BRIGHT, + &txt_bright_fontStyle); + + rdpq_font_style( + font[i], + STYLE_GREEN, + &txt_green_fontStyle); + + for (int p = 1; p <= MAXPLAYERS; p++) + { + txt_player_fontStyle.color = playerColors[p - 1]; + rdpq_font_style( + font[i], + STYLE_PLAYER + p - 1, + &txt_player_fontStyle); + } + } + + rdpq_font_style( + font[ID_DEBUG], + STYLE_DEBUG, + &txt_debug_fontStyle); + + txt_debugParms = (rdpq_textparms_t){.style_id = STYLE_DEBUG, .disable_aa_fix = true}; + txt_titleParms = (rdpq_textparms_t){.style_id = STYLE_TITLE, .disable_aa_fix = true}; + txt_gameParms = (rdpq_textparms_t){.style_id = STYLE_BRIGHT, .disable_aa_fix = true}; + } + + // Unregisters and frees fonts for the next minigame. + void ui_fontUnregister(void) + { + for (int i = ID_DEBUG; i < ID_COUNT; ++i) + { + rdpq_text_unregister_font(i); + rdpq_font_free(font[i]); + } + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_FONT_H \ No newline at end of file diff --git a/code/sb_hot/ui/ui_sprite.h b/code/sb_hot/ui/ui_sprite.h new file mode 100644 index 00000000..139d7b90 --- /dev/null +++ b/code/sb_hot/ui/ui_sprite.h @@ -0,0 +1,165 @@ +#ifndef UI_SPRITE_H +#define UI_SPRITE_H + +#include "ui_file.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + // Constant dimensions for sprites + const int spriteWidth = 16, spriteHeight = 16; + + // Static pointers to sprites representing different controller elements + static sprite_t *sprite_controlStick; + static sprite_t *sprite_dPadTriggers; + static sprite_t *sprite_cButtons0; + static sprite_t *sprite_cButtons1; + static sprite_t *sprite_faceButtons0; + static sprite_t *sprite_faceButtons1; + + // Static pointers to sprites for panels + static sprite_t *sprite_gloss; + static sprite_t *sprite_tessalate; + + // Static pointers to sprites for logos + static sprite_t *sprite_libdragon; + static sprite_t *sprite_mixamo; + static sprite_t *sprite_t3d; + static sprite_t *sprite_strawberryTop; + static sprite_t *sprite_strawberry0; + static sprite_t *sprite_strawberry1; + + // Surfaces for rendering UI elements + surface_t surf_UIpanels; + surface_t surf_UIsprites; + + /* Declarations */ + + void ui_spriteLoad(void); + void ui_syncSprite(int color); + void ui_spriteDraw(rdpq_tile_t tile, sprite_t *sprite, int idx, int x, int y); + void ui_spriteDrawPanel(rdpq_tile_t tile, sprite_t *sprite, int color, int x0, int y0, int x1, int y1, int s, int t, int s1, int t1); + void ui_spriteDrawDynamic(rdpq_tile_t tile, sprite_t *sprite, color_t color, int x0, int y0, int x1, int y1, int s, int t, int s1, int t1); + void ui_spriteCleanup(void); + + /* Definitions */ + + // Loads and assigns sprites to their corresponding pointers based on file paths set by ui_fileSprites. + void ui_spriteLoad(void) + { + ui_fileSprites(); + ui_fileLogos(); + + // Load IA format sprites (grayscale with alpha for UI overlays). + sprite_gloss = sprite_load(uiSpritePanelFileName[0]); + sprite_tessalate = sprite_load(uiSpritePanelFileName[1]); + sprite_controlStick = sprite_load(uiSpriteButtonFileName[0]); + sprite_dPadTriggers = sprite_load(uiSpriteButtonFileName[1]); + sprite_libdragon = sprite_load(uiSpriteLogoFileName[4]); + sprite_mixamo = sprite_load(uiSpriteLogoFileName[5]); + sprite_t3d = sprite_load(uiSpriteLogoFileName[3]); + + // Load RGBA32 format sprites (full color with transparency for UI buttons). + sprite_cButtons0 = sprite_load(uiSpriteButtonFileName[2]); + sprite_cButtons1 = sprite_load(uiSpriteButtonFileName[3]); + sprite_faceButtons0 = sprite_load(uiSpriteButtonFileName[4]); + sprite_faceButtons1 = sprite_load(uiSpriteButtonFileName[5]); + sprite_strawberry0 = sprite_load(uiSpriteLogoFileName[0]); + sprite_strawberry1 = sprite_load(uiSpriteLogoFileName[1]); + sprite_strawberryTop = sprite_load(uiSpriteLogoFileName[2]); + } + + // Optional RDPQ sync and set for sprites. Similar to ui_syncText, but sets the combiner for textures and allows for primitive color to added. + void ui_syncSprite(int color) + { + rdpq_sync_pipe(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_TEX_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_set_prim_color(ui_color(color)); + rdpq_sync_tile(); + } + + // Draws a simple 16x16 sprite. + void ui_spriteDraw(rdpq_tile_t tile, sprite_t *sprite, int idx, int x, int y) + { + int s = 0, t = 0; + int idxCopy = idx; + + if (idx > 4) + { + idx = idx % 4; + s = spriteWidth * idx; + } + else + { + s = spriteWidth * idx; + } + + t = (idxCopy / 4) * spriteHeight; + + ui_syncSprite(WHITE); + + surf_UIsprites = sprite_get_pixels(sprite); + + rdpq_tex_upload_sub(tile, &surf_UIsprites, NULL, s, t, s + spriteWidth, t + spriteHeight); + rdpq_texture_rectangle(tile, x, y, x + spriteWidth, y + spriteHeight, s, t); + } + + // Draws a scalable sprite with predefined primitive color by index. + void ui_spriteDrawPanel(rdpq_tile_t tile, sprite_t *sprite, int colorIdx, int x0, int y0, int x1, int y1, int s, int t, int s1, int t1) + { + + ui_syncSprite(colorIdx); + + surf_UIpanels = sprite_get_pixels(sprite); + + rdpq_tex_upload(tile, &surf_UIpanels, NULL); + rdpq_texture_rectangle_scaled(tile, x0, y0, x1, y1, s, t, s1, t1); + } + + // Draws a scalable sprite with added primitive color. + void ui_spriteDrawDynamic(rdpq_tile_t tile, sprite_t *sprite, color_t color, int x0, int y0, int x1, int y1, int s, int t, int s1, int t1) + { + + rdpq_sync_pipe(); + rdpq_set_mode_standard(); + rdpq_mode_combiner(RDPQ_COMBINER_TEX_FLAT); + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + rdpq_set_prim_color(color); + rdpq_sync_tile(); + + surf_UIpanels = sprite_get_pixels(sprite); + + rdpq_tex_upload(tile, &surf_UIpanels, NULL); + rdpq_texture_rectangle_scaled(tile, x0, y0, x1, y1, s, t, s1, t1); + } + + // Frees static pointers to sprites. + void ui_spriteCleanup(void) + { + sprite_free(sprite_controlStick); + sprite_free(sprite_dPadTriggers); + sprite_free(sprite_cButtons0); + sprite_free(sprite_cButtons1); + sprite_free(sprite_faceButtons0); + sprite_free(sprite_faceButtons1); + sprite_free(sprite_gloss); + sprite_free(sprite_tessalate); + sprite_free(sprite_libdragon); + sprite_free(sprite_mixamo); + sprite_free(sprite_t3d); + sprite_free(sprite_strawberryTop); + sprite_free(sprite_strawberry0); + sprite_free(sprite_strawberry1); + surface_free(&surf_UIpanels); + surface_free(&surf_UIsprites); + } + +#ifdef __cplusplus +} +#endif + +#endif // UI_SPRITE_H \ No newline at end of file diff --git a/config.h b/config.h index e6935b8c..b5ba1017 100644 --- a/config.h +++ b/config.h @@ -25,4 +25,4 @@ // The current minigame you want to test #define MINIGAME_TO_TEST "examplegame" -#endif \ No newline at end of file +#endif