Codename Hamster - For olcCodeJam 2024
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
hamster/src/Hamster.cpp

1069 lines
41 KiB

#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "HamsterGame.h"
#include "Hamster.h"
#include "util.h"
#include <ranges>
#include "AnimationState.h"
#include "FloatingText.h"
#include "HamsterAI.h"
std::vector<Hamster>Hamster::HAMSTER_LIST;
const uint8_t Hamster::MAX_HAMSTER_COUNT{100U};
const uint8_t Hamster::NPC_HAMSTER_COUNT{5U};
const std::vector<std::string>Hamster::NPC_HAMSTER_IMAGES{
"hamster1.png",
"hamster2.png",
"hamster3.png",
"hamster4.png",
"hamster5.png",
"hamster6.png",
"hamster7.png",
"hamster8.png",
};
std::string Hamster::PLAYER_HAMSTER_IMAGE{"hamster1.png"};
std::optional<Hamster*>Hamster::playerHamster;
Hamster::Hamster(const vf2d spawnPos,const std::string&img,const PlayerControlled IsPlayerControlled)
:pos(spawnPos),IsPlayerControlled(IsPlayerControlled),randomId(util::random()),colorFilename(img){
animations=HamsterGame::GetAnimations(img);
animations.ChangeState(internalAnimState,AnimationState::DEFAULT);
}
void Hamster::UpdateHamsters(const float fElapsedTime){
for(Hamster&h:HAMSTER_LIST){
h.lastSafeLocationTimer=std::max(0.f,h.lastSafeLocationTimer-fElapsedTime);
h.animations.UpdateState(h.internalAnimState,fElapsedTime);
h.aiNodeTime+=fElapsedTime;
h.frictionEnabled=true;
h.bumpTimer-=fElapsedTime;
h.boostTimer=std::max(0.f,h.boostTimer-fElapsedTime);
h.canCollectWheelPowerupTimer=std::max(0.f,h.canCollectWheelPowerupTimer-fElapsedTime);
h.lastCollisionSound+=fElapsedTime;
h.HandleCollision();
switch(h.state){
case NORMAL:{
if(h.CanMove()){
if(h.IsPlayerControlled){
h.HandlePlayerControls();
}else{
h.HandleAIControls();
}
}
}break;
case BUMPED:{
if(h.bumpTimer<=0.f){
h.SetState(NORMAL);
}
}break;
case DROWNING:{
if(h.imgScale>0.f){
h.shrinkEffectColor=BLACK;
h.imgScale=std::max(0.f,h.imgScale-0.5f*fElapsedTime);
}
else{
h.waitTimer=4.f;
HamsterGame::PlaySFX(h.pos,"drown_burn.wav");
h.SetState(WAIT);
}
}break;
case WAIT:{
h.waitTimer=std::max(0.f,h.waitTimer-fElapsedTime);
if(h.waitTimer<=0.f){
h.imgScale=1.f;
h.drownTimer=0.f;
if(!h.lastSafeLocation.has_value()){
h.lastSafeLocation=h.GetNearestSafeLocation();
}
if(h.IsPlayerControlled)h.SetPos(h.lastSafeLocation.value());
else{
if(h.ai.GetCurrentAction().has_value())h.SetPos(h.ai.GetCurrentAction().value().get().pos);
else h.SetPos(h.lastSafeLocation.value());
}
h.SetState(NORMAL);
if(h.IsPlayerControlled)h.RemoveAllPowerups();
}
}break;
case BURNING:{
if(h.imgScale>0.f){
h.shrinkEffectColor=RED;
h.imgScale=std::max(0.f,h.imgScale-0.5f*fElapsedTime);
}else{
h.waitTimer=4.f;
HamsterGame::PlaySFX(h.pos,"drown_burn.wav");
h.SetState(WAIT);
}
}break;
case KNOCKOUT:{
h.knockoutTimer-=fElapsedTime;
if(h.knockoutTimer<=0.f){
h.SetState(NORMAL);
h.animations.ChangeState(h.internalAnimState,AnimationState::DEFAULT);
}
}break;
case BURROWING:{
h.burrowTimer-=fElapsedTime;
h.burrowImgShrinkTimer-=fElapsedTime;
h.shrinkEffectColor=BLACK;
h.imgScale=std::max(0.f,util::lerp(0,1,h.burrowImgShrinkTimer*2.f));
if(h.burrowTimer<=0.f){
h.burrowTimer=3.f;
h.SetState(BURROW_WAIT);
}
}break;
case BURROW_WAIT:{
h.burrowTimer-=fElapsedTime;
const Tunnel&enteredTunnel{HamsterGame::Game().GetTunnels().at(h.enteredTunnel)};
const vf2d destinationTunnelPos{HamsterGame::Game().GetTunnels().at(enteredTunnel.linkedTo).worldPos+vi2d{8,8}};
if(h.burrowTimer<=0.f){
h.burrowTimer=1.f;
h.SetState(SURFACING);
h.imgScale=0.f;
h.burrowImgShrinkTimer=0.5f;
switch(HamsterGame::Game().GetTileFacingDirection(destinationTunnelPos)){
case Terrain::NORTH:{
h.targetRot=-geom2d::pi/2;
}break;
case Terrain::EAST:{
h.targetRot=0.f;
}break;
case Terrain::SOUTH:{
h.targetRot=geom2d::pi/2;
}break;
case Terrain::WEST:{
h.targetRot=geom2d::pi;
}break;
}
}
h.pos=destinationTunnelPos.lerp(enteredTunnel.worldPos+vi2d{8,8},h.burrowTimer/3.f);
}break;
case SURFACING:{
h.burrowTimer-=fElapsedTime;
h.burrowImgShrinkTimer-=fElapsedTime;
h.imgScale=std::min(1.f,util::lerp(1,0,h.burrowImgShrinkTimer*2.f));
vf2d targetDirVec{0.f,-16.f};
const Tunnel&enteredTunnel{HamsterGame::Game().GetTunnels().at(h.enteredTunnel)};
const vf2d destinationTunnelPos{HamsterGame::Game().GetTunnels().at(enteredTunnel.linkedTo).worldPos+vi2d{8,8}};
switch(HamsterGame::Game().GetTileFacingDirection(destinationTunnelPos)){
case Terrain::EAST:{
targetDirVec={16.f,0.f};
}break;
case Terrain::SOUTH:{
targetDirVec={0.f,16.f};
}break;
case Terrain::WEST:{
targetDirVec={-16.f,0.f};
}break;
}
const vf2d walkOutTunnelDest{destinationTunnelPos+targetDirVec};
h.pos=walkOutTunnelDest.lerp(destinationTunnelPos,h.burrowTimer);
if(h.burrowTimer<=0.f){
h.SetState(Hamster::NORMAL);
h.imgScale=1.f;
}
}break;
}
if(h.state!=FLYING){
if((h.GetTerrainStandingOn()==Terrain::OCEAN||h.GetTerrainStandingOn()==Terrain::VOID||!h.HasPowerup(Powerup::SWAMP)&&h.GetTerrainStandingOn()==Terrain::SWAMP)&&h.state!=DROWNING&&h.state!=WAIT)h.drownTimer+=fElapsedTime;
else if((!h.HasPowerup(Powerup::LAVA)&&h.GetTerrainStandingOn()==Terrain::LAVA)&&h.state!=BURNING&&h.state!=WAIT)h.burnTimer+=fElapsedTime;
else if(h.lastSafeLocationTimer<=0.f&&h.state==NORMAL&&!h.IsLethalTerrain(h.GetPos())){
h.lastSafeLocationTimer=0.5f;
h.drownTimer=0.f;
h.burnTimer=0.f;
h.lastSafeLocation=h.GetPos();
}
if(h.drownTimer>=h.DEFAULT_DROWN_TIME&&h.state!=DROWNING&&h.state!=WAIT){
h.SetState(DROWNING);
}
if(h.burnTimer>=h.DEFAULT_BURN_TIME&&h.state!=BURNING&&h.state!=WAIT){
h.SetState(BURNING);
}
}
if(h.hamsterJet.has_value())h.hamsterJet.value().Update(fElapsedTime);
h.TurnTowardsTargetDirection();
h.MoveHamster();
if(h.IsPlayerControlled){
h.readyFlashTimer+=fElapsedTime;
h.jetFuelDisplayAmt+=(h.jetFuel-h.jetFuelDisplayAmt)*4.f*fElapsedTime;
}
if(h.CollectedAllCheckpoints()){
h.animations.ChangeState(h.internalAnimState,AnimationState::SIDE_VIEW);
h.raceFinishAnimTimer+=fElapsedTime;
}
}
}
void Hamster::CreateHamsters(const HamsterGame::GameMode mode){
HAMSTER_LIST.clear();
playerHamster.reset();
HAMSTER_LIST.reserve(MAX_HAMSTER_COUNT);
if(NPC_HAMSTER_COUNT+1>MAX_HAMSTER_COUNT)throw std::runtime_error{std::format("WARNING! Max hamster count is too high! Please expand the MAX_HAMSTER_COUNT if you want more hamsters. Requested {} hamsters.",MAX_HAMSTER_COUNT)};
playerHamster=&HAMSTER_LIST.emplace_back(vf2d{},HamsterGame::Game().ColorToHamsterImage(HamsterGame::Game().hamsterColor),PLAYER_CONTROLLED);
std::vector<std::string>hamsterColorChoices{NPC_HAMSTER_IMAGES};
std::erase(hamsterColorChoices,HamsterGame::Game().ColorToHamsterImage(HamsterGame::Game().hamsterColor));
for(int i:std::ranges::iota_view(0U,NPC_HAMSTER_COUNT)){
std::string colorChoice{hamsterColorChoices.at(util::random()%hamsterColorChoices.size())};
std::erase(hamsterColorChoices,colorChoice);
Hamster&npcHamster{HAMSTER_LIST.emplace_back(vf2d{},colorChoice,NPC)};
HamsterAI::AIType typeChosen{HamsterAI::DUMB};
switch(mode){
case HamsterGame::GameMode::GRAND_PRIX_1:{
int randPct{util::random()%100};
if(randPct<=25)typeChosen=HamsterAI::DUMB;
else if(randPct<=75)typeChosen=HamsterAI::NORMAL;
else typeChosen=HamsterAI::SMART;
}break;
case HamsterGame::GameMode::GRAND_PRIX_2:{
int randPct{util::random()%100};
if(randPct<=10)typeChosen=HamsterAI::DUMB;
else if(randPct<=2)typeChosen=HamsterAI::NORMAL;
else typeChosen=HamsterAI::SMART;
}break;
case HamsterGame::GameMode::GRAND_PRIX_3:{
int randPct{util::random()%100};
if(randPct<=1)typeChosen=HamsterAI::DUMB;
else if(randPct<=20)typeChosen=HamsterAI::NORMAL;
else typeChosen=HamsterAI::SMART;
}break;
case HamsterGame::GameMode::SINGLE_RACE:{
int randPct{util::random()%100};
switch(HamsterGame::Game().GetMapDifficulty()){
case Difficulty::EASY:{
if(randPct<=40&&randPct>20)typeChosen=HamsterAI::NORMAL;
else if(randPct<=20)typeChosen=HamsterAI::SMART;
}break;
case Difficulty::MEDIUM:{
typeChosen=HamsterAI::NORMAL;
if(randPct<=100&&randPct>80)typeChosen=HamsterAI::DUMB;
else if(randPct<=20)typeChosen=HamsterAI::SMART;
}break;
case Difficulty::HARD:{
typeChosen=HamsterAI::SMART;
if(randPct<=20)typeChosen=HamsterAI::NORMAL;
}break;
}
}break;
case HamsterGame::GameMode::MARATHON:{
int randPct{util::random()%100};
if(randPct<=1)typeChosen=HamsterAI::DUMB;
else if(randPct<=20)typeChosen=HamsterAI::NORMAL;
else typeChosen=HamsterAI::SMART;
}break;
}
npcHamster.aiLevel=typeChosen;
}
}
void Hamster::MoveHamstersToSpawn(const geom2d::rect<int>startingLoc){
if(HAMSTER_LIST.size()==0)Hamster::CreateHamsters(HamsterGame::Game().GetGameMode());
int MAX_AI_FILES{100};
int aiFileCount{0};
while(MAX_AI_FILES>0){
if(!std::filesystem::exists(std::format("{}{}.{}",HamsterGame::ASSETS_DIR,HamsterGame::Game().GetCurrentMapName(),aiFileCount)))break;
aiFileCount++;
MAX_AI_FILES--;
}
struct HamsterPersistentData{
HamsterAI::AIType aiLevel;
std::string colorFilename;
Hamster::PlayerControlled IsPlayerControlled;
int points;
};
std::vector<HamsterPersistentData>persistentData;
size_t previousHamsterCount{HAMSTER_LIST.size()};
for(int i:std::ranges::iota_view(0U,HAMSTER_LIST.size())){
Hamster&hamster{HAMSTER_LIST[i]};
//Keep persistent data available and reset Hamster.
persistentData.emplace_back(hamster.aiLevel,hamster.colorFilename,hamster.IsPlayerControlled,hamster.points);
}
HAMSTER_LIST.clear();
for(HamsterPersistentData&data:persistentData){
Hamster&newHamster{HAMSTER_LIST.emplace_back(vf2d{},data.colorFilename,data.IsPlayerControlled)};
newHamster.points=data.points;
newHamster.aiLevel=data.aiLevel;
}
for(Hamster&hamster:HAMSTER_LIST){
hamster.SetPos(vf2d{util::random_range(startingLoc.pos.x,startingLoc.pos.x+startingLoc.size.x),util::random_range(startingLoc.pos.y,startingLoc.pos.y+startingLoc.size.y)});
std::vector<int>possibleAIs{};
for(int i:std::ranges::iota_view(0,aiFileCount)){
if(i%3==hamster.aiLevel)possibleAIs.emplace_back(i);
}
if(possibleAIs.size()==0){
std::cout<<"WARNING! No AI files for AI Type "<<int(hamster.aiLevel)<<" currently exist! Consider adding them! Rolling with whatever exists..."<<std::endl;
for(int i:std::ranges::iota_view(0,aiFileCount)){
possibleAIs.emplace_back(i);
}
}
if(possibleAIs.size()>0)hamster.ai.LoadAI(HamsterGame::Game().GetCurrentMapName(),possibleAIs[util::random()%possibleAIs.size()]);
}
}
void Hamster::DrawHamsters(TransformedView&tv){
for(Hamster&h:HAMSTER_LIST){
const Animate2D::FrameSequence&anim{h.animations.GetFrames(h.internalAnimState)};
const Animate2D::FrameSequence&wheelTopAnim{h.animations.GetFrames(AnimationState::WHEEL_TOP)};
const Animate2D::FrameSequence&wheelBottomAnim{h.animations.GetFrames(AnimationState::WHEEL_BOTTOM)};
const Animate2D::Frame&img{h.animations.GetState(h.internalAnimState)==AnimationState::DEFAULT?anim.GetFrame(h.distanceTravelled/100.f):h.GetCurrentAnimation()};
const Animate2D::Frame&wheelTopImg{wheelTopAnim.GetFrame(h.distanceTravelled/80.f)};
const Animate2D::Frame&wheelBottomImg{wheelBottomAnim.GetFrame(h.distanceTravelled/80.f)};
if(h.CollectedAllCheckpoints()){
float animCycle{fmod(h.raceFinishAnimTimer,1.5f)};
float facingXScale{fmod(h.raceFinishAnimTimer,3.f)>=1.5f?-1.f:1.f};
float yHopAmt{0.f};
if(animCycle>=0.8f){
yHopAmt=-abs(sin(geom2d::pi*(animCycle/0.35f)))*12.f;
}
Pixel blendCol{PixelLerp(h.shrinkEffectColor,WHITE,h.imgScale)};
if(h.boostTimer!=0.f&&fmod(HamsterGame::Game().GetRuntime(),0.5f)<0.25f)blendCol=RED;
if(h.hamsterJet.has_value())h.hamsterJet.value().Draw(blendCol);
HamsterGame::Game().SetZ(h.z+0.02f);
tv.DrawRotatedDecal(h.pos+vf2d{0.f,h.drawingOffsetY},HamsterGame::GetGFX("shadow.png").Decal(),0.f,HamsterGame::GetGFX("shadow.png").Sprite()->Size()/2);
HamsterGame::Game().SetZ(h.z+0.025f);
tv.DrawPartialRotatedDecal(h.pos+vf2d{0.f,h.drawingOffsetY+yHopAmt},img.GetSourceImage()->Decal(),0.f,img.GetSourceRect().size/2,img.GetSourceRect().pos,img.GetSourceRect().size,vf2d{1.f,1.f}*h.imgScale*vf2d{facingXScale,1.f},blendCol);
HamsterGame::Game().SetZ(0.f);
}else{
Pixel blendCol{PixelLerp(h.shrinkEffectColor,WHITE,h.imgScale)};
if(h.boostTimer!=0.f&&fmod(HamsterGame::Game().GetRuntime(),0.5f)<0.25f)blendCol=RED;
if(h.hamsterJet.has_value())h.hamsterJet.value().Draw(blendCol);
HamsterGame::Game().SetZ(h.z+0.02f);
if(h.HasPowerup(Powerup::WHEEL))tv.DrawPartialRotatedDecal(h.pos+vf2d{0.f,h.drawingOffsetY},wheelBottomImg.GetSourceImage()->Decal(),h.rot,wheelBottomImg.GetSourceRect().size/2,wheelBottomImg.GetSourceRect().pos,wheelBottomImg.GetSourceRect().size,vf2d{1.f,1.f}*h.imgScale,PixelLerp(h.shrinkEffectColor,blendCol,h.imgScale));
tv.DrawPartialRotatedDecal(h.pos+vf2d{0.f,h.drawingOffsetY},img.GetSourceImage()->Decal(),h.rot,img.GetSourceRect().size/2,img.GetSourceRect().pos,img.GetSourceRect().size,vf2d{1.f,1.f}*h.imgScale,PixelLerp(h.shrinkEffectColor,blendCol,h.imgScale));
HamsterGame::Game().SetZ(h.z+0.025f);
if(h.HasPowerup(Powerup::WHEEL))tv.DrawPartialRotatedDecal(h.pos+vf2d{0.f,h.drawingOffsetY},wheelTopImg.GetSourceImage()->Decal(),h.rot,wheelTopImg.GetSourceRect().size/2,wheelTopImg.GetSourceRect().pos,wheelTopImg.GetSourceRect().size,vf2d{1.f,1.f}*h.imgScale,PixelLerp(h.shrinkEffectColor,{blendCol.r,blendCol.g,blendCol.b,192},h.imgScale));
HamsterGame::Game().SetZ(0.f);
}
}
}
void Hamster::DrawOverlay(){
if(GetPlayer().hamsterJet.has_value())GetPlayer().hamsterJet.value().DrawOverlay();
const vf2d jetDisplayOffset{HamsterGame::SCREEN_FRAME.pos+vf2d{HamsterGame::SCREEN_FRAME.size.x,0.f}};
Pixel jetDisplayCol{VERY_DARK_GREY};
if(!GetPlayer().hamsterJet.has_value()){
if(GetPlayer().HasPowerup(Powerup::JET))jetDisplayCol=WHITE;
const Animate2D::FrameSequence&lightAnim{HamsterGame::Game().GetAnimation("hamster_jet.png",AnimationState::JET_LIGHTS)};
const Animate2D::Frame&lightFrame{lightAnim.GetFrame(HamsterGame::Game().GetRuntime())};
const Animate2D::FrameSequence&jetAnim{HamsterGame::Game().GetAnimation("hamster_jet.png",AnimationState::JET)};
const Animate2D::Frame&jetFrame{jetAnim.GetFrame(HamsterGame::Game().GetRuntime())};
HamsterGame::Game().DrawPartialRotatedDecal(jetDisplayOffset+vf2d{48.f,80.f},jetFrame.GetSourceImage()->Decal(),0.f,jetFrame.GetSourceRect().size/2,jetFrame.GetSourceRect().pos,jetFrame.GetSourceRect().size,{2.f,2.f},jetDisplayCol);
HamsterGame::Game().DrawPartialRotatedDecal(jetDisplayOffset+vf2d{48.f,80.f},lightFrame.GetSourceImage()->Decal(),0.f,lightFrame.GetSourceRect().size/2,lightFrame.GetSourceRect().pos,jetFrame.GetSourceRect().size,{2.f,2.f},jetDisplayCol);
}
if(GetPlayer().HasPowerup(Powerup::JET)&&!GetPlayer().hamsterJet.has_value()){
const std::string readyText{"READY!"};
const vi2d textSize{HamsterGame::Game().GetTextSize(readyText)};
HamsterGame::Game().DrawShadowRotatedStringDecal(jetDisplayOffset+vf2d{48.f,116.f},readyText,0.f,textSize/2,GREEN,fmod(GetPlayer().readyFlashTimer,1.5f)<=0.75f?DARK_RED:BLACK);
HamsterGame::Game().DrawDecal(HamsterGame::SCREEN_FRAME.pos+vf2d{HamsterGame::SCREEN_FRAME.size.x,0.f},HamsterGame::GetGFX("fuelmeter.png").Decal());
const std::string launchText{"(SPACE)x2\nto Launch!"};
const vi2d launchTextSize{HamsterGame::Game().GetTextSize(launchText)};
HamsterGame::Game().DrawShadowRotatedStringDecal(jetDisplayOffset+vf2d{48.f,224.f},launchText,0.f,launchTextSize/2,WHITE,BLACK);
}else{
HamsterGame::Game().DrawPartialDecal(HamsterGame::SCREEN_FRAME.pos+vf2d{HamsterGame::SCREEN_FRAME.size.x,0.f},HamsterGame::GetGFX("fuelmeter.png").Decal(),{0,0},{96,200});
}
const float jetFuelBarHeight{float(HamsterGame::GetGFX("fuelbar.png").Sprite()->height)};
HamsterGame::Game().DrawPartialDecal(jetDisplayOffset+vf2d{24.f,139.f}+vf2d{0.f,jetFuelBarHeight*(1.f-GetPlayer().jetFuelDisplayAmt)},HamsterGame::GetGFX("fuelbar.png").Decal(),{0.f,jetFuelBarHeight*(1.f-GetPlayer().jetFuelDisplayAmt)},{float(HamsterGame::GetGFX("fuelbar.png").Sprite()->width),jetFuelBarHeight*(GetPlayer().jetFuelDisplayAmt)});
if(GetPlayer().hamsterJet.has_value()){
const Terrain::FuelDamage jetFuelLandingCost{std::min(GetPlayer().jetFuelDisplayAmt,Terrain::GetFuelDamageTakenAndKnockoutEffect(GetPlayer().GetTerrainHoveringOver(),GetPlayer().hamsterJet.value().GetLandingSpeed()).first)};
HamsterGame::Game().DrawPartialDecal(jetDisplayOffset+vf2d{24.f,139.f}+vf2d{0.f,jetFuelBarHeight*(1.f-GetPlayer().jetFuelDisplayAmt)},HamsterGame::GetGFX("fuelbar.png").Decal(),{0.f,jetFuelBarHeight*(1.f-GetPlayer().jetFuelDisplayAmt)},{float(HamsterGame::GetGFX("fuelbar.png").Sprite()->width),jetFuelBarHeight*jetFuelLandingCost},{1.f,1.f},{255,0,0,192});
}
if(GetPlayer().HasPowerup(Powerup::JET))HamsterGame::Game().DrawDecal(jetDisplayOffset+vf2d{22.f,137.f},HamsterGame::GetGFX("fuelbar_outline.png").Decal(),{1.f,1.f},GetPlayer().jetFuel<=0.2f?(fmod(GetPlayer().readyFlashTimer,1.f)<=0.5f?RED:BLACK):BLACK);
if(GetPlayer().HasPowerup(Powerup::WHEEL)){
for(int i:std::ranges::iota_view(0,3)){
if(fmod(HamsterGame::Game().GetRuntime(),2.f)<1.f&&GetPlayer().boostCounter>i)HamsterGame::Game().DrawDecal(HamsterGame::SCREEN_FRAME.pos+vf2d{i*16.f+4.f,HamsterGame::SCREEN_FRAME.size.y-18.f},HamsterGame::GetGFX("boost_outline.png").Decal(),{0.125f,0.125f},GetPlayer().boostCounter>i?WHITE:BLACK);
else HamsterGame::Game().DrawDecal(HamsterGame::SCREEN_FRAME.pos+vf2d{i*16.f+4.f,HamsterGame::SCREEN_FRAME.size.y-18.f},HamsterGame::GetGFX("boost.png").Decal(),{0.125f,0.125f},GetPlayer().boostCounter>i?WHITE:BLACK);
}
HamsterGame::Game().DrawShadowStringDecal(HamsterGame::SCREEN_FRAME.pos+vf2d{3*16.f+8.f,HamsterGame::SCREEN_FRAME.size.y-12.f},"\"R\" - BOOST",YELLOW,fmod(HamsterGame::Game().GetRuntime(),2.f)<1.f?RED:BLACK);
}
if(HamsterGame::Game().collectedPowerupTimer>0.f){
vf2d powerupInfoStrSize{HamsterGame::Game().GetTextSizeProp(HamsterGame::Game().powerupHelpDisplay)};
uint8_t alpha{255U};
if(HamsterGame::Game().collectedPowerupTimer<1.f)alpha=uint8_t(util::lerp(0,255,HamsterGame::Game().collectedPowerupTimer));
HamsterGame::Game().DrawShadowRotatedStringPropDecal(HamsterGame::SCREEN_FRAME.middle()+vf2d{0.f,-HamsterGame::SCREEN_FRAME.size.y/2+powerupInfoStrSize.y+4.f},HamsterGame::Game().powerupHelpDisplay,0.f,powerupInfoStrSize/2,{255,255,255,alpha},{0,0,0,alpha});
}
}
const Animate2D::Frame&Hamster::GetCurrentAnimation()const{
return animations.GetFrame(internalAnimState);
}
const Hamster&Hamster::GetPlayer(){
if(!playerHamster.has_value())throw std::runtime_error{std::format("WARNING! Player is not created at this time! There should not be any code referencing the player at this moment!")};
return *playerHamster.value();
}
const vf2d&Hamster::GetPos()const{
return pos;
}
void Hamster::HandlePlayerControls(){
lastTappedSpace+=HamsterGame::Game().GetElapsedTime();
vf2d aimingDir{};
if(HamsterGame::Game().GetKey(W).bHeld||HamsterGame::Game().GetKey(UP).bHeld){
aimingDir+=vf2d{0,-1};
}
if(HamsterGame::Game().GetKey(D).bHeld||HamsterGame::Game().GetKey(RIGHT).bHeld){
aimingDir+=vf2d{1,0};
}
if(HamsterGame::Game().GetKey(S).bHeld||HamsterGame::Game().GetKey(DOWN).bHeld){
aimingDir+=vf2d{0,1};
}
if(HamsterGame::Game().GetKey(A).bHeld||HamsterGame::Game().GetKey(LEFT).bHeld){
aimingDir+=vf2d{-1,0};
}
if(aimingDir!=vf2d{}){
targetRot=aimingDir.norm().polar().y;
const vf2d currentVel{vel};
vel=vf2d{currentVel.polar().x+((GetMaxSpeed()/GetTimeToMaxSpeed())*HamsterGame::Game().GetElapsedTime()),rot}.cart();
vel=vf2d{std::min(GetMaxSpeed(),vel.polar().x),vel.polar().y}.cart();
frictionEnabled=false;
}
if(HamsterGame::Game().GetKey(R).bPressed&&boostCounter>0){
boostCounter--;
boostTimer=1.f;
HamsterGame::PlaySFX(pos,"wheel_boost.wav");
if(IsPlayerControlled)HamsterAI::OnBoost(this->pos);
}
if(HamsterGame::Game().GetKey(SPACE).bPressed){
if(lastTappedSpace<=0.6f&&HasPowerup(Powerup::JET)){
SetState(FLYING);
lastSafeLocation.reset();
if(IsPlayerControlled)HamsterAI::OnJetLaunch(this->pos);
hamsterJet.emplace(*this);
}
lastTappedSpace=0.f;
}
}
void Hamster::TurnTowardsTargetDirection(){
util::turn_towards_direction(rot,targetRot,GetTurnSpeed()*HamsterGame::Game().GetElapsedTime());
}
void Hamster::MoveHamster(){
if(IsBurrowed())return;
SetPos(GetPos()+vel*HamsterGame::Game().GetElapsedTime());
distanceTravelled+=vel.mag()*HamsterGame::Game().GetElapsedTime();
if(state==FLYING){
jetFuel=std::max(0.f,jetFuel-vel.mag()*HamsterGame::Game().GetElapsedTime()/100000.f);
}
#pragma region Handle Friction
if(frictionEnabled){
const vf2d currentVel{vel};
vel=vf2d{std::max(0.f,currentVel.polar().x-GetFriction()*HamsterGame::Game().GetElapsedTime()),currentVel.polar().y}.cart();
}
#pragma endregion
if(hamsterJet.has_value()&&hamsterJet.value().GetState()==HamsterJet::HAMSTER_CONTROL)hamsterJet.value().SetPos(GetPos()); //Hamster Jet lagging behind hack fix.
}
void Hamster::HandleCollision(){
if(IsBurrowed())return;
for(Hamster&h:HAMSTER_LIST){
if(this==&h)continue;
if(h.IsBurrowed())continue;
if(abs(z-h.z)<=0.1f&&geom2d::overlaps(geom2d::circle<float>(GetPos(),GetRadius()),geom2d::circle<float>(h.GetPos(),h.GetRadius()))){
if(geom2d::line<float>(GetPos(),h.GetPos()).length()==0.f){ //Push these two in random directions, they are on top of each other!
float randDir{util::random(2*geom2d::pi)};
vf2d collisionResolve1{GetPos()+vf2d{GetRadius(),randDir}.cart()};
vf2d collisionResolve2{h.GetPos()+vf2d{h.GetRadius(),float(randDir+geom2d::pi)}.cart()};
SetPos(collisionResolve1);
h.SetPos(collisionResolve2);
vel+=vf2d{GetBumpAmount(),randDir}.cart();
vel=vf2d{std::min(GetMaxSpeed(),vel.polar().x),vel.polar().y}.cart();
h.vel+=vf2d{h.GetBumpAmount(),float(randDir+geom2d::pi)}.cart();
h.vel=vf2d{std::min(h.GetMaxSpeed(),h.vel.polar().x),h.vel.polar().y}.cart();
}else{
geom2d::line<float>collisionLine{geom2d::line<float>(GetPos(),h.GetPos())};
float distance{collisionLine.length()};
float totalRadii{GetRadius()+h.GetRadius()};
float bumpDistance{totalRadii-distance};
vf2d collisionResolve1{GetPos()+vf2d{bumpDistance/2.f,float(collisionLine.vector().polar().y+geom2d::pi)}.cart()};
vf2d collisionResolve2{h.GetPos()+vf2d{bumpDistance/2.f,collisionLine.vector().polar().y}.cart()};
SetPos(collisionResolve1);
h.SetPos(collisionResolve2);
vel+=vf2d{GetBumpAmount(),float(collisionLine.vector().polar().y+geom2d::pi)}.cart();
vel=vf2d{std::min(GetMaxSpeed(),vel.polar().x),vel.polar().y}.cart();
h.vel+=vf2d{h.GetBumpAmount(),collisionLine.vector().polar().y}.cart();
h.vel=vf2d{std::min(h.GetMaxSpeed(),h.vel.polar().x),h.vel.polar().y}.cart();
}
if(h.lastCollisionSound>1.f){
if(util::random()%2==0)HamsterGame::PlaySFX(pos,"hit_hamster.wav");
else HamsterGame::PlaySFX(pos,"hit_hamster_2.wav");
lastCollisionSound=h.lastCollisionSound=0.f;
}
bumpTimer=h.bumpTimer=0.12f;
}
}
for(Powerup&powerup:Powerup::GetPowerups()){
if(z<=0.1f&&
(!HasPowerup(powerup.GetType())||HasPowerup(Powerup::JET)&&powerup.GetType()==Powerup::JET&&jetFuel!=1.f||powerup.GetType()==Powerup::WHEEL&&HasPowerup(Powerup::WHEEL)&&boostCounter<3&&canCollectWheelPowerupTimer==0.f)
&&geom2d::overlaps(geom2d::circle<float>(GetPos(),collisionRadius),geom2d::circle<float>(powerup.GetPos(),20.f))){
ObtainPowerup(powerup.GetType());
if(IsPlayerControlled)HamsterAI::OnPowerupCollection(this->pos);
if(powerup.GetType()==Powerup::JET)HamsterGame::PlaySFX(pos,"obtain_jet.wav");
else HamsterGame::PlaySFX(pos,"collect_powerup.wav");
powerup.OnPowerupObtain(*this);
if(IsPlayerControlled){
HamsterGame::Game().collectedPowerupTimer=5.f;
HamsterGame::Game().powerupHelpDisplay=Powerup::powerupInfo[powerup.GetType()];
}
}
}
for(Checkpoint&checkpoint:Checkpoint::GetCheckpoints()){
if(z<=0.1f&&geom2d::overlaps(geom2d::rect<float>(checkpoint.GetPos()-vf2d{62,60},{122.f,113.f}),geom2d::circle<float>(GetPos(),collisionRadius))&&!checkpointsCollected.count(checkpoint.GetPos())){
checkpointsCollected.insert(checkpoint.GetPos());
FloatingText::CreateFloatingText(pos,std::format("{} / {}",checkpointsCollected.size(),Checkpoint::GetCheckpoints().size()),{WHITE,GREEN},{1.5f,2.f});
if(IsPlayerControlled)HamsterAI::OnCheckpointCollected(this->pos);
if(IsPlayerControlled)checkpoint.OnCheckpointCollect();
if(CollectedAllCheckpoints()){
if(IsPlayerControlled||!GetPlayer().CollectedAllCheckpoints())finishedRaceTime=HamsterGame::Game().GetRaceTime();
else if(!IsPlayerControlled){finishedRaceTime=GetPlayer().GetFinishedTime()+HamsterGame::Game().GetPlayerDifferentialTime();}
if(IsPlayerControlled)HamsterGame::PlaySFX("winneris.ogg");
if(IsPlayerControlled)HamsterGame::Game().OnPlayerFinishedRace();
}else HamsterGame::PlaySFX(pos,"checkpoint_collection.wav");
lastObtainedCheckpointPos=checkpoint.GetPos();
}
}
if(GetState()==NORMAL){
for(const auto&[id,tunnel]:HamsterGame::Game().GetTunnels()){
if(geom2d::overlaps(geom2d::circle<float>(GetPos(),4),geom2d::rect<float>(tunnel.worldPos,{16,16}))){
SetState(Hamster::BURROWING);
burrowTimer=1.f;
enteredTunnel=id;
burrowImgShrinkTimer=0.5f;
switch(HamsterGame::Game().GetTileFacingDirection(tunnel.worldPos)){
case Terrain::NORTH:{
targetRot=geom2d::pi/2;
}break;
case Terrain::EAST:{
targetRot=geom2d::pi;
}break;
case Terrain::SOUTH:{
targetRot=-geom2d::pi/2;
}break;
case Terrain::WEST:{
targetRot=0.;
}break;
}
if(IsPlayerControlled)HamsterAI::OnTunnelEnter(this->pos);
}
}
}
}
const float Hamster::GetRadius()const{
return collisionRadius;
}
const Terrain::TerrainType Hamster::GetTerrainStandingOn()const{
if(FlyingInTheAir())return Terrain::ROCK;
return HamsterGame::Game().GetTerrainTypeAtPos(GetPos());
}
const Terrain::TerrainType Hamster::GetTerrainHoveringOver()const{
return HamsterGame::Game().GetTerrainTypeAtPos(GetPos());
}
const bool Hamster::IsTerrainStandingOnSolid()const{
return HamsterGame::Game().IsTerrainSolid(GetPos());
}
const float Hamster::GetTimeToMaxSpeed()const{
float finalTimeToMaxSpd{DEFAULT_TIME_TO_MAX_SPD};
if(!HasPowerup(Powerup::ICE)&&GetTerrainStandingOn()==Terrain::ICE)finalTimeToMaxSpd*=3;
else if(!HasPowerup(Powerup::SWAMP)&&GetTerrainStandingOn()==Terrain::SWAMP)finalTimeToMaxSpd*=1.25;
if(hamsterJet.has_value()){
if(hamsterJet.value().GetState()==HamsterJet::LANDING)finalTimeToMaxSpd*=2.f;
else if(FlyingInTheAir())finalTimeToMaxSpd*=3.f;
}
return finalTimeToMaxSpd;
}
const float Hamster::GetMaxSpeed()const{
float finalMaxSpd{DEFAULT_MAX_SPD};
switch(GetTerrainStandingOn()){
case Terrain::GRASS:{
if(!HasPowerup(Powerup::GRASS))finalMaxSpd*=0.80f;
}break;
case Terrain::SAND:{
if(!HasPowerup(Powerup::SAND))finalMaxSpd*=0.60f;
}break;
case Terrain::SWAMP:{
if(HasPowerup(Powerup::SWAMP))finalMaxSpd*=0.80f;
else finalMaxSpd*=0.50f;
}break;
case Terrain::SHORE:{
finalMaxSpd*=0.80f;
}break;
case Terrain::OCEAN:{
finalMaxSpd*=0.10f;
}break;
case Terrain::LAVA:{
if(HasPowerup(Powerup::LAVA))finalMaxSpd*=1.f;
else finalMaxSpd*=0.6f;
}break;
case Terrain::FOREST:{
if(!HasPowerup(Powerup::FOREST))finalMaxSpd*=0.50f;
}break;
case Terrain::VOID:{
finalMaxSpd*=0.10f;
}break;
}
if(HasPowerup(Powerup::WHEEL))finalMaxSpd*=1.5f;
if(boostTimer!=0.f)finalMaxSpd*=1.5f;
if(hamsterJet.has_value()){
if(hamsterJet.value().GetState()==HamsterJet::LANDING)finalMaxSpd*=1.5f;
else if(FlyingInTheAir())finalMaxSpd*=8.f;
}
if(!IsPlayerControlled){
if(aiNodeTime>0.3f)finalMaxSpd*=0.8f; //Slow down a bit...
if(temporaryNode.has_value()||aiNodeTime>3.f)finalMaxSpd*=0.5f; //Slow down a bit...
//if((temporaryNode.has_value()||aiNodeTime>3.f)&&hamsterJet.has_value())finalMaxSpd*=0.5f; //Slow down A LOT...
switch(ai.GetAIType()){
case HamsterAI::SMART:{
finalMaxSpd*=0.99f;
}break;
case HamsterAI::NORMAL:{
finalMaxSpd*=0.92f;
}break;
case HamsterAI::DUMB:{
finalMaxSpd*=0.85f;
}break;
}
}
if(!HamsterGame::Game().RaceCountdownCompleted())finalMaxSpd*=0.f;
return finalMaxSpd;
}
const float Hamster::GetFriction()const{
float finalFriction{DEFAULT_FRICTION};
if(!HasPowerup(Powerup::ICE)&&GetTerrainStandingOn()==Terrain::ICE)finalFriction*=0.1f;
else if(!HasPowerup(Powerup::SWAMP)&&GetTerrainStandingOn()==Terrain::SWAMP)finalFriction*=0.6f;
if(hamsterJet.has_value()){
if(hamsterJet.value().GetState()==HamsterJet::LANDING)finalFriction*=1.5f;
else if(FlyingInTheAir())finalFriction*=8.f;
}
return finalFriction;
}
const float Hamster::GetTurnSpeed()const{
float finalTurnSpd{DEFAULT_TURN_SPD};
if(!HasPowerup(Powerup::ICE)&&GetTerrainStandingOn()==Terrain::ICE)finalTurnSpd*=0.6f;
return finalTurnSpd;
}
const float Hamster::GetBumpAmount()const{
float finalBumpAmt{DEFAULT_BUMP_AMT};
return finalBumpAmt*GetMaxSpeed()/DEFAULT_MAX_SPD;
}
void Hamster::ObtainPowerup(const Powerup::PowerupType powerup){
if(powerup==Powerup::WHEEL){
boostCounter=3;
canCollectWheelPowerupTimer=5.f;
}
powerups.insert(powerup);
}
const bool Hamster::HasPowerup(const Powerup::PowerupType powerup)const{
return powerups.count(powerup);
}
void Hamster::RemoveAllPowerups(){
powerups.clear();
}
const bool Hamster::IsLethalTerrain(const vf2d pos)const{
return HamsterGame::Game().GetTerrainTypeAtPos(pos)==Terrain::LAVA||HamsterGame::Game().GetTerrainTypeAtPos(pos)==Terrain::OCEAN||HamsterGame::Game().GetTerrainTypeAtPos(pos)==Terrain::SWAMP||HamsterGame::Game().GetTerrainTypeAtPos(pos)==Terrain::VOID;
}
const bool Hamster::IsDrowning()const{
return drownTimer>0.f;
}
const bool Hamster::IsBurning()const{
return burnTimer>0.f;
}
const float Hamster::GetDrownRatio()const{
return drownTimer/DEFAULT_DROWN_TIME;
}
const float Hamster::GetBurnRatio()const{
return burnTimer/DEFAULT_BURN_TIME;
}
const float&Hamster::GetZ()const{
return z;
}
void Hamster::SetPos(const vf2d pos){
bool movedY{false};
bool movedX{false};
if(state==FLYING&&HamsterGame::Game().IsInBounds(vf2d{this->pos.x,pos.y})||!HamsterGame::Game().IsTerrainSolid(vf2d{this->pos.x,pos.y})){
this->pos=vf2d{this->pos.x,pos.y};
movedY=true;
}
if(state==FLYING&&HamsterGame::Game().IsInBounds(vf2d{pos.x,this->pos.y})||!HamsterGame::Game().IsTerrainSolid(vf2d{pos.x,this->pos.y})){
this->pos=vf2d{pos.x,this->pos.y};
movedX=true;
}
if (!movedY&&(state==FLYING&&HamsterGame::Game().IsInBounds(vf2d{this->pos.x,pos.y})||!HamsterGame::Game().IsTerrainSolid(vf2d{this->pos.x,pos.y}))){
this->pos=vf2d{this->pos.x,pos.y};
movedY=true;
}
Terrain::TerrainType terrainAtPos{HamsterGame::Game().GetTerrainTypeAtPos(this->pos)};
if(state!=FLYING&&distanceTravelled-lastFootstep>32.f){
lastFootstep=distanceTravelled;
if(terrainAtPos==Terrain::ROCK)HamsterGame::PlaySFX(pos,"footsteps_rock.wav");
else if(terrainAtPos==Terrain::SAND||terrainAtPos==Terrain::SWAMP||terrainAtPos==Terrain::FOREST)HamsterGame::PlaySFX(pos,"sfx_movement_footsteps1b.wav");
else HamsterGame::PlaySFX(pos,"sfx_movement_footsteps1a.wav");
}
if(IsPlayerControlled&&(movedX||movedY)&&terrainAtPos!=Terrain::TUNNEL&&state!=FLYING)HamsterAI::OnMove(this->pos);
}
void Hamster::SetZ(const float z){
this->z=z;
}
void Hamster::OnUserDestroy(){
HAMSTER_LIST.clear();
}
void Hamster::SetDrawingOffsetY(const float offsetY){
drawingOffsetY=offsetY;
}
const vf2d Hamster::GetNearestSafeLocation()const{
using TilePos=vi2d;
using TileDistance=int;
const vi2d playerTile{GetPos()/16};
geom2d::rect<int>searchRect{{-1,-1},{3,3}};
std::optional<std::pair<TilePos,TileDistance>>closestTile;
const auto DetermineAndUpdateClosestTile=[this,&playerTile,&closestTile](const vi2d&tile){
if(!IsLethalTerrain(tile*16)&&!IsSolidTerrain(tile*16)){
std::pair<TilePos,TileDistance>closest{closestTile.value_or(std::pair<TilePos,TileDistance>{{},std::numeric_limits<int>::max()})};
int tileDist{abs(playerTile.x-tile.x)+abs(playerTile.y-tile.y)};
if(tileDist<=closest.second)closestTile.emplace(std::pair<TilePos,TileDistance>{tile,tileDist});
}
};
while(!closestTile.has_value()){
#pragma region Top Outline Check
{
for(int offsetX:std::ranges::iota_view(searchRect.pos.x,searchRect.size.x)){
const vi2d checkTile{playerTile+vi2d{offsetX,searchRect.top().end.y}};
DetermineAndUpdateClosestTile(checkTile);
}
}
#pragma endregion
#pragma region Bottom Outline Check
{
for(int offsetX:std::ranges::iota_view(searchRect.pos.x,searchRect.size.x)){
const vi2d checkTile{playerTile+vi2d{offsetX,searchRect.bottom().end.y}};
DetermineAndUpdateClosestTile(checkTile);
}
}
#pragma endregion
#pragma region Right Outline Check
{
for(int offsetY:std::ranges::iota_view(searchRect.pos.y+1,searchRect.size.y-2+1)){
const vi2d checkTile{playerTile+vi2d{searchRect.right().end.x,offsetY}};
DetermineAndUpdateClosestTile(checkTile);
}
}
#pragma endregion
#pragma region Left Outline Check
{
for(int offsetY:std::ranges::iota_view(searchRect.pos.y+1,searchRect.size.y-2+1)){
const vi2d checkTile{playerTile+vi2d{searchRect.left().end.x,offsetY}};
DetermineAndUpdateClosestTile(checkTile);
}
}
#pragma endregion
searchRect.pos-=1;
searchRect.size+=2;
}
return closestTile.value().first*16+8;
}
const bool Hamster::IsSolidTerrain(const vf2d pos)const{
return HamsterGame::Game().IsTerrainSolid(pos);
}
void Hamster::SetJetFuel(const float amt){
jetFuel=amt;
}
void Hamster::Knockout(){
SetState(KNOCKOUT);
knockoutTimer=4.f;
animations.ChangeState(internalAnimState,AnimationState::KNOCKOUT);
}
const float Hamster::GetSpeed()const{
return vel.mag();
}
void Hamster::SetState(const HamsterState state){
this->state=state;
}
const bool Hamster::CollectedAllCheckpoints()const{
return checkpointsCollected.size()==Checkpoint::GetCheckpoints().size();
}
const bool Hamster::HasCollectedCheckpoint(const Checkpoint&cp)const{
return checkpointsCollected.contains(cp.GetPos());
}
std::vector<Hamster>&Hamster::GetHamsters(){
return HAMSTER_LIST;
}
const Hamster::HamsterState&Hamster::GetState()const{
return state;
}
const bool Hamster::BurnedOrDrowned()const{
return GetState()==WAIT;
}
const bool Hamster::CanMove()const{
return !CollectedAllCheckpoints()&&!IsBurrowed();
}
const bool Hamster::FlyingInTheAir()const{
return GetState()==FLYING&&hamsterJet.value().GetZ()>0.5f&&GetZ()>0.5f;
}
void Hamster::HandleAIControls(){
vf2d aimingDir{};
const HamsterAI::ActionOptRef&currentAction{ai.GetCurrentAction()};
HamsterAI::Action action;
if(!currentAction.has_value()){temporaryNode=ai.GetPreviousAction().value().get().pos;}
else action=currentAction.value().get();
if(aiNodeTime>GetAIAdjustNodeTime()){
geom2d::line<float>playerToHamster{GetPlayer().GetPos(),GetPos()};
const float screenDistance{playerToHamster.length()*(1.325f/(HamsterGame::Game().GetCameraZ()))};
const float playerScreenDistanceToNewNode{geom2d::line<float>(GetPlayer().GetPos(),action.pos).length()*(1.325f/(HamsterGame::Game().GetCameraZ()))};
if(screenDistance>226&&playerScreenDistanceToNewNode>226){
//Let's cheat, hehe.
pos=action.pos;
temporaryNode.reset();
}else{
int MAX_SEARCH_AMT{100};
while(MAX_SEARCH_AMT>0){
temporaryNode=GetPos()+vf2d{util::random_range(-SEARCH_RANGE*16,SEARCH_RANGE*16),util::random_range(-SEARCH_RANGE*16,SEARCH_RANGE*16)};
randomId=util::random(); //Shuffle this in hopes it sprouts better RNG.
if(!HamsterGame::Game().IsTerrainSolid(temporaryNode.value()))break;
SEARCH_RANGE+=1.f;
SEARCH_RANGE=std::min(64.f,SEARCH_RANGE);
MAX_SEARCH_AMT--;
}
}
aiNodeTime=0.f;
}
vf2d targetLoc{action.pos};
if(action.type==HamsterAI::Action::MOVE)targetLoc+=GetAINodePositionVariance();
if(temporaryNode.has_value())targetLoc=temporaryNode.value();
vf2d diff{targetLoc-GetPos()};
float variance{GetAINodeDistanceVariance()};
if(action.type!=HamsterAI::Action::MOVE)variance=12.f;
if(diff.mag()<variance){
if(action.type==HamsterAI::Action::LAUNCH_JET){
if(true||HasPowerup(Powerup::JET)){ //Currently ignoring whether we have a jet or not...We can cheat!
SetState(FLYING);
lastSafeLocation.reset();
if(IsPlayerControlled)HamsterAI::OnJetLaunch(this->pos);
hamsterJet.emplace(*this);
}else{
//TODO
//If we don't have a Jet and it's required at this point, we must backtrack nodes until we get to a location where one is placed...
//This will be a separate behavioral AI node later on...
}
}else
if(action.type==HamsterAI::Action::BOOST){
if(boostCounter>0){
boostCounter--;
boostTimer=1.f;
}
}
ai.AdvanceToNextAction();
const HamsterAI::ActionOptRef&nextAction{ai.GetCurrentAction()};
if(nextAction.has_value()){
const HamsterAI::Action&futureAction{nextAction.value().get()};
if(futureAction.type==HamsterAI::Action::MOVE){
switch(ai.GetAIType()){
case HamsterAI::SMART:{
if(util::random()%100<2)ai.AdvanceToNextAction();
}break;
case HamsterAI::NORMAL:{
if(util::random()%100<25)ai.AdvanceToNextAction();
}break;
case HamsterAI::DUMB:{
if(util::random()%100<50)ai.AdvanceToNextAction();
}break;
}
}
}
temporaryNode.reset();
SEARCH_RANGE=1.f;
aiNodeTime=0.f;
}
const float moveThreshold{4.f};
if(diff.y<-moveThreshold){
aimingDir+=vf2d{0,-1};
}
if(diff.x>moveThreshold){
aimingDir+=vf2d{1,0};
}
if(diff.y>moveThreshold){
aimingDir+=vf2d{0,1};
}
if(diff.x<-moveThreshold){
aimingDir+=vf2d{-1,0};
}
if(aimingDir!=vf2d{}){
targetRot=aimingDir.norm().polar().y;
vf2d currentVel{vel};
vel=vf2d{currentVel.polar().x+((GetMaxSpeed()/GetTimeToMaxSpeed())*HamsterGame::Game().GetElapsedTime()),rot}.cart();
vel=vf2d{std::min(GetMaxSpeed(),vel.polar().x),vel.polar().y}.cart();
frictionEnabled=false;
}
}
const float Hamster::GetAILandingSpeed()const{
switch(ai.GetAIType()){
case HamsterAI::SMART:{
return 4.9f;
}break;
case HamsterAI::NORMAL:{
return 2.9f;
}break;
case HamsterAI::DUMB:{
return 1.2f;
}break;
}
return 2.f;
}
const float Hamster::GetAIAdjustNodeTime()const{
switch(ai.GetAIType()){
case HamsterAI::SMART:{
return 1.f;
}break;
case HamsterAI::NORMAL:{
return 2.f;
}break;
case HamsterAI::DUMB:{
return 3.5f;
}break;
}
return 3.5f;
}
const bool Hamster::IsBurrowed()const{
return GetState()==BURROWING||GetState()==BURROW_WAIT||GetState()==SURFACING;
}
const float Hamster::GetAINodeDistanceVariance()const{
switch(ai.GetAIType()){
case HamsterAI::SMART:{
return 12.f*float(randomId%100)/100.f+12.f;
}break;
case HamsterAI::NORMAL:{
return 18.f*float(randomId%100)/100.f+18.f;
}break;
case HamsterAI::DUMB:{
return 24.f*float(randomId%100)/100.f+24.f;
}break;
}
return 12.f*float(randomId%100)/100.f+12.f;
}
const vf2d Hamster::GetAINodePositionVariance()const{
vf2d finalOffset{};
float seedX{float(randomId%100)/100.f};
float seedY{float((randomId*457)%100)/100.f};
switch(ai.GetAIType()){
case HamsterAI::SMART:{
finalOffset.x=seedX*16.f-8.f;
finalOffset.y=seedY*16.f-8.f;
}break;
case HamsterAI::NORMAL:{
finalOffset.x=seedX*32.f-16.f;
finalOffset.y=seedY*32.f-16.f;
}break;
case HamsterAI::DUMB:{
finalOffset.x=seedX*48.f-24.f;
finalOffset.y=seedY*48.f-24.f;
}break;
}
return finalOffset;
}
const size_t Hamster::GetCheckpointsCollectedCount()const{
return checkpointsCollected.size();
}
const std::optional<vf2d>Hamster::GetLastCollectedCheckpoint()const{
return lastObtainedCheckpointPos;
}
const int Hamster::GetFinishedTime()const{
return finishedRaceTime.value_or(std::numeric_limits<int>::max());
}
const std::string&Hamster::GetHamsterImage()const{
return colorFilename;
}
void Hamster::ClearHamsters(){
HAMSTER_LIST.clear();
}