#include "pch.h" #include "TPinballTable.h" #include "control.h" #include "loader.h" #include "midi.h" #include "pb.h" #include "render.h" #include "TBall.h" #include "TBlocker.h" #include "TBumper.h" #include "TComponentGroup.h" #include "TDemo.h" #include "TFlagSpinner.h" #include "TGate.h" #include "THole.h" #include "timer.h" #include "TKickback.h" #include "TKickout.h" #include "TLight.h" #include "TLightBargraph.h" #include "TLightGroup.h" #include "TLightRollover.h" #include "TOneway.h" #include "TPopupTarget.h" #include "TRamp.h" #include "TRollover.h" #include "TSink.h" #include "TSoloTarget.h" #include "TSound.h" #include "TTableLayer.h" #include "TTextBox.h" #include "TTimer.h" #include "TTripwire.h" #include "TWall.h" #include "TPlunger.h" #include "TFlipper.h" #include "TDrain.h" #include "translations.h" int TPinballTable::score_multipliers[5] = {1, 2, 3, 5, 10}; TPinballTable::TPinballTable(): TPinballComponent(nullptr, -1, false) { int shortArrLength; CurScoreStruct = nullptr; ScoreBallcount = nullptr; ScorePlayerNumber1 = nullptr; BallInDrainFlag = 0; ActiveFlag = 1; TiltLockFlag = 0; EndGameTimeoutTimer = 0; LightShowTimer = 0; ReplayTimer = 0; TiltTimeoutTimer = 0; MultiballFlag = false; PlayerCount = 0; auto ball = AddBall({0.0f, 0.0f}); ball->Disable(); new TTableLayer(this); LightGroup = new TLightGroup(this, 0); auto score1 = score::create("score1", render::background_bitmap); CurScoreStruct = score1; PlayerScores[0].ScoreStruct = score1; for (int scoreIndex = 1; scoreIndex < 4; scoreIndex++) { PlayerScores[scoreIndex].ScoreStruct = score::dup(CurScoreStruct, scoreIndex); } CurrentPlayer = 0; MaxBallCount = 3; ScoreBallcount = score::create("ballcount1", render::background_bitmap); ScorePlayerNumber1 = score::create("player_number1", render::background_bitmap); int groupIndexObjects = loader::query_handle("table_objects"); short* shortArr = loader::query_iattribute(groupIndexObjects, 1025, &shortArrLength); if (shortArrLength > 0) { for (int i = 0; i < shortArrLength / 2; ++i) { int objectType = *shortArr; short* shortArrPlus1 = shortArr + 1; int groupIndex = *shortArrPlus1; shortArr = shortArrPlus1 + 1; switch (objectType) { case 1000: case 1010: new TWall(this, groupIndex); break; case 1001: Plunger = new TPlunger(this, groupIndex); break; case 1002: LightGroup->List.push_back(new TLight(this, groupIndex)); break; case 1003: FlipperL = new TFlipper(this, groupIndex); break; case 1004: FlipperR = new TFlipper(this, groupIndex); break; case 1005: new TBumper(this, groupIndex); break; case 1006: new TPopupTarget(this, groupIndex); break; case 1007: Drain = new TDrain(this, groupIndex); break; case 1011: new TBlocker(this, groupIndex); break; case 1012: new TKickout(this, groupIndex, true); break; case 1013: new TGate(this, groupIndex); break; case 1014: new TKickback(this, groupIndex); break; case 1015: new TRollover(this, groupIndex); break; case 1016: new TOneway(this, groupIndex); break; case 1017: new TSink(this, groupIndex); break; case 1018: new TFlagSpinner(this, groupIndex); break; case 1019: new TSoloTarget(this, groupIndex); break; case 1020: new TLightRollover(this, groupIndex); break; case 1021: new TRamp(this, groupIndex); break; case 1022: new THole(this, groupIndex); break; case 1023: new TDemo(this, groupIndex); break; case 1024: new TTripwire(this, groupIndex); break; case 1026: new TLightGroup(this, groupIndex); break; case 1028: new TComponentGroup(this, groupIndex); break; case 1029: new TKickout(this, groupIndex, false); break; case 1030: new TLightBargraph(this, groupIndex); break; case 1031: new TSound(this, groupIndex); break; case 1032: new TTimer(this, groupIndex); break; case 1033: new TTextBox(this, groupIndex); break; default: continue; } } } render::build_occlude_list(); pb::InfoTextBox = dynamic_cast(find_component("info_text_box")); pb::MissTextBox = dynamic_cast(find_component("mission_text_box")); control::make_links(this); } TPinballTable::~TPinballTable() { for (auto& PlayerScore : PlayerScores) { delete PlayerScore.ScoreStruct; } if (ScorePlayerNumber1) { delete ScorePlayerNumber1; ScorePlayerNumber1 = nullptr; } if (ScoreBallcount) { delete ScoreBallcount; ScoreBallcount = nullptr; } delete LightGroup; while (!ComponentList.empty()) { // Component destructor removes it from the list. delete ComponentList[0]; } control::ClearLinks(); pb::InfoTextBox = pb::MissTextBox = nullptr; } TPinballComponent* TPinballTable::find_component(LPCSTR componentName) { for (auto component : ComponentList) { const char* groupName = component->GroupName; if (groupName && !strcmp(groupName, componentName)) { return component; } } pb::ShowMessageBox(SDL_MESSAGEBOX_WARNING, "Table cant find:", componentName); return nullptr; } TPinballComponent* TPinballTable::find_component(int groupIndex) { char Buffer[40]{}; for (auto component : ComponentList) { if (component->GroupIndex == groupIndex) return component; } snprintf(Buffer, sizeof Buffer, "%d", groupIndex); pb::ShowMessageBox(SDL_MESSAGEBOX_WARNING, "Table cant find (lh):", Buffer); return nullptr; } int TPinballTable::AddScore(int score) { if (JackpotScoreFlag) { JackpotScore += score; const auto jackpotLimit = !pb::FullTiltMode ? 5000000 : 10000000; if (JackpotScore > jackpotLimit) JackpotScore = jackpotLimit; } if (BonusScoreFlag) { BonusScore += score; if (BonusScore > 5000000) BonusScore = 5000000; } int addedScore = ScoreAdded + score * score_multipliers[ScoreMultiplier]; CurScore += addedScore; if (CurScore > 1000000000) { ++CurScoreE9; CurScore = CurScore - 1000000000; } score::set(CurScoreStruct, CurScore); return addedScore; } void TPinballTable::ChangeBallCount(int count) { BallCount = count; if (count <= 0) { score::erase(ScoreBallcount, 1); } else { score::set(ScoreBallcount, MaxBallCount - count + 1); score::update(ScoreBallcount); } } void TPinballTable::tilt(float time) { if (!TiltLockFlag && !BallInDrainFlag) { pb::InfoTextBox->Clear(); pb::MissTextBox->Clear(); pb::InfoTextBox->Display(pb::get_rc_string(Msg::STRING136), -1.0); loader::play_sound(SoundIndex3, nullptr, "TPinballTable1"); TiltTimeoutTimer = timer::set(30.0, this, tilt_timeout); for (auto component : ComponentList) { component->Message(MessageCode::SetTiltLock, time); } LightGroup->Message(MessageCode::TLightTurnOffTimed, 0); TiltLockFlag = 1; control::table_control_handler(MessageCode::SetTiltLock); } } void TPinballTable::port_draw() { for (auto component : ComponentList) { component->port_draw(); } } int TPinballTable::Message(MessageCode code, float value) { const char* rc_text; switch (code) { case MessageCode::LeftFlipperInputPressed: if (!TiltLockFlag) { FlipperL->Message(MessageCode::TFlipperExtend, value); } break; case MessageCode::LeftFlipperInputReleased: if (!TiltLockFlag) { FlipperL->Message(MessageCode::TFlipperRetract, value); } break; case MessageCode::RightFlipperInputPressed: if (!TiltLockFlag) { FlipperR->Message(MessageCode::TFlipperExtend, value); } break; case MessageCode::RightFlipperInputReleased: if (!TiltLockFlag) { FlipperR->Message(MessageCode::TFlipperRetract, value); } break; case MessageCode::PlungerInputPressed: case MessageCode::PlungerInputReleased: Plunger->Message(code, value); break; case MessageCode::Pause: case MessageCode::Resume: case MessageCode::LooseFocus: for (auto component : ComponentList) { component->Message(code, value); } break; case MessageCode::ClearTiltLock: LightGroup->Message(MessageCode::TLightResetTimed, 0.0); if (TiltLockFlag) { TiltLockFlag = 0; if (TiltTimeoutTimer) timer::kill(TiltTimeoutTimer); TiltTimeoutTimer = 0; } break; case MessageCode::StartGamePlayer1: LightGroup->Message(MessageCode::TLightGroupReset, 0.0); LightGroup->Message(MessageCode::TLightResetAndTurnOff, 0.0); Plunger->Message(MessageCode::PlungerStartFeedTimer, 0.0); if (Demo && Demo->ActiveFlag) rc_text = pb::get_rc_string(Msg::STRING131); else rc_text = pb::get_rc_string(Msg::STRING127); pb::InfoTextBox->Display(rc_text, -1.0); if (Demo) Demo->Message(MessageCode::NewGame, 0.0); break; case MessageCode::NewGame: if (EndGameTimeoutTimer) { timer::kill(EndGameTimeoutTimer); EndGame_timeout(0, this); pb::mode_change(GameModes::InGame); } if (LightShowTimer) { timer::kill(LightShowTimer); LightShowTimer = 0; Message(MessageCode::StartGamePlayer1, 0.0); } else { // Some of the control cheats persist across games. // Was this loose anti-cheat by design? CheatsUsed = 0; Message(MessageCode::Reset, 0.0); auto ball = BallList[0]; ball->Position.Y = 0.0; ball->Position.X = 0.0; ball->Position.Z = -0.8f; auto playerCount = static_cast(floor(value)); PlayerCount = playerCount; if (playerCount >= 1) { if (playerCount > 4) PlayerCount = 4; } else { PlayerCount = 1; } auto plr1Score = PlayerScores[0].ScoreStruct; CurrentPlayer = 0; CurScoreStruct = plr1Score; CurScore = 0; score::set(plr1Score, 0); ScoreMultiplier = 0; for (int plrIndex = 1; plrIndex < PlayerCount; ++plrIndex) { auto scorePtr = &PlayerScores[plrIndex]; score::set(scorePtr->ScoreStruct, 0); scorePtr->Score = 0; scorePtr->ScoreE9Part = 0; scorePtr->BallCount = MaxBallCount; scorePtr->ExtraBalls = ExtraBalls; scorePtr->BallLockedCounter = BallLockedCounter; scorePtr->JackpotScore = JackpotScore; } BallCount = MaxBallCount; ChangeBallCount(BallCount); score::set(ScorePlayerNumber1, CurrentPlayer + 1); score::update(ScorePlayerNumber1); for (auto scoreIndex = 4 - PlayerCount; scoreIndex > 0; scoreIndex--) { score::set(PlayerScores[scoreIndex].ScoreStruct, -1); } JackpotScoreFlag = false; BonusScoreFlag = false; UnknownP71 = 0; pb::InfoTextBox->Clear(); pb::MissTextBox->Clear(); LightGroup->Message(MessageCode::TLightGroupLightShowAnimation, 0.2f); auto time = loader::play_sound(SoundIndex1, nullptr, "TPinballTable2"); if (time < 0) time = 5.0f; LightShowTimer = timer::set(time, this, LightShow_timeout); } if (pb::FullTiltMode) { // Multi-ball is FT exclusive feature, at least for now. MultiballFlag = true; JackpotScore = 500000; } midi::play_track(MidiTracks::Track1, true); break; case MessageCode::PlungerRelaunchBall: if (ReplayTimer) timer::kill(ReplayTimer); ReplayTimer = timer::set(floor(value), this, replay_timer_callback); ReplayActiveFlag = 1; break; case MessageCode::SwitchToNextPlayer: { if (PlayerCount <= 1) { const char* textboxText; if (Demo->ActiveFlag) textboxText = pb::get_rc_string(Msg::STRING131); else textboxText = pb::get_rc_string(Msg::STRING127); pb::InfoTextBox->Display(textboxText, -1.0); break; } auto nextPlayer = (CurrentPlayer + 1) % PlayerCount; auto nextScorePtr = &PlayerScores[nextPlayer]; if (nextScorePtr->BallCount <= 0) break; PlayerScores[CurrentPlayer].Score = CurScore; PlayerScores[CurrentPlayer].ScoreE9Part = CurScoreE9; PlayerScores[CurrentPlayer].BallCount = BallCount; PlayerScores[CurrentPlayer].ExtraBalls = ExtraBalls; PlayerScores[CurrentPlayer].BallLockedCounter = BallLockedCounter; PlayerScores[CurrentPlayer].JackpotScore = JackpotScore; CurScore = nextScorePtr->Score; CurScoreE9 = nextScorePtr->ScoreE9Part; BallCount = nextScorePtr->BallCount; ExtraBalls = nextScorePtr->ExtraBalls; BallLockedCounter = nextScorePtr->BallLockedCounter; JackpotScore = nextScorePtr->JackpotScore; CurScoreStruct = nextScorePtr->ScoreStruct; score::set(CurScoreStruct, CurScore); CurScoreStruct->DirtyFlag = true; ChangeBallCount(BallCount); score::set(ScorePlayerNumber1, nextPlayer + 1); score::update(ScorePlayerNumber1); for (auto component : ComponentList) { component->Message(MessageCode::PlayerChanged, static_cast(nextPlayer)); } const char* textboxText = nullptr; switch (nextPlayer) { case 0: if (Demo->ActiveFlag) textboxText = pb::get_rc_string(Msg::STRING131); else textboxText = pb::get_rc_string(Msg::STRING127); break; case 1: if (Demo->ActiveFlag) textboxText = pb::get_rc_string(Msg::STRING132); else textboxText = pb::get_rc_string(Msg::STRING128); break; case 2: if (Demo->ActiveFlag) textboxText = pb::get_rc_string(Msg::STRING133); else textboxText = pb::get_rc_string(Msg::STRING129); break; case 3: if (Demo->ActiveFlag) textboxText = pb::get_rc_string(Msg::STRING134); else textboxText = pb::get_rc_string(Msg::STRING130); break; default: break; } if (textboxText != nullptr) pb::InfoTextBox->Display(textboxText, -1); JackpotScoreFlag = false; BonusScoreFlag = false; UnknownP71 = 0; CurrentPlayer = nextPlayer; } break; case MessageCode::GameOver: loader::play_sound(SoundIndex2, nullptr, "TPinballTable3"); pb::MissTextBox->Clear(); pb::InfoTextBox->Display(pb::get_rc_string(Msg::STRING135), -1.0); EndGameTimeoutTimer = timer::set(3.0, this, EndGame_timeout); break; case MessageCode::Reset: for (auto component : ComponentList) { component->Message(MessageCode::Reset, 0); } if (ReplayTimer) timer::kill(ReplayTimer); ReplayTimer = 0; if (LightShowTimer) { timer::kill(LightShowTimer); LightGroup->Message(MessageCode::TLightGroupReset, 0.0); } LightShowTimer = 0; ScoreMultiplier = 0; ScoreAdded = 0; ReflexShotScore = 0; BonusScore = 10000; BonusScoreFlag = false; JackpotScore = 20000; JackpotScoreFlag = false; UnknownP71 = 0; ExtraBalls = 0; MultiballCount = 0; BallLockedCounter = 0; MultiballFlag = false; UnknownP78 = 0; ReplayActiveFlag = 0; ReplayTimer = 0; TiltLockFlag = 0; break; default: break; } control::table_control_handler(code); return 0; } TBall* TPinballTable::AddBall(vector2 position) { TBall* ball = nullptr; for (auto curBall : BallList) { if (!curBall->ActiveFlag) { ball = curBall; break; } } if (ball != nullptr) { ball->ActiveFlag = 1; ball->Position.Z = ball->Radius; ball->Direction = {}; ball->Speed = 0; ball->TimeDelta = 0; ball->EdgeCollisionCount = 0; ball->CollisionFlag = 0; ball->CollisionMask = 1; ball->CollisionComp = nullptr; } else { if (BallList.size() >= 20) return nullptr; ball = new TBall(this, -1); BallList.push_back(ball); } ball->Position.X = position.X; ball->Position.Y = position.Y; ball->PrevPosition = ball->Position; ball->StuckCounter = 0; ball->LastActiveTime = pb::time_ticks; return ball; } int TPinballTable::BallCountInRect(const RectF& rect) { int count = 0; for (const auto ball : BallList) { if (ball->ActiveFlag && ball->Position.X >= rect.XMin && ball->Position.Y >= rect.YMin && ball->Position.X <= rect.XMax && ball->Position.Y <= rect.YMax) { count++; } } return count; } int TPinballTable::BallCountInRect(const vector2& pos, float margin) { RectF rect{}; rect.XMin = pos.X - margin; rect.XMax = pos.X + margin; rect.YMin = pos.Y - margin; rect.YMax = pos.Y + margin; return BallCountInRect(rect); } void TPinballTable::EndGame_timeout(int timerId, void* caller) { auto table = static_cast(caller); table->EndGameTimeoutTimer = 0; pb::end_game(); for (auto component : table->ComponentList) { component->Message(MessageCode::GameOver, 0); } if (table->Demo) table->Demo->Message(MessageCode::GameOver, 0.0); control::handler(MessageCode::ControlMissionStarted, pb::MissTextBox); pb::InfoTextBox->Display(pb::get_rc_string(Msg::STRING125), -1.0); } void TPinballTable::LightShow_timeout(int timerId, void* caller) { auto table = static_cast(caller); table->LightShowTimer = 0; table->Message(MessageCode::StartGamePlayer1, 0.0); } void TPinballTable::replay_timer_callback(int timerId, void* caller) { auto table = static_cast(caller); table->ReplayActiveFlag = 0; table->ReplayTimer = 0; } void TPinballTable::tilt_timeout(int timerId, void* caller) { auto table = static_cast(caller); vector2 vec{}; table->TiltTimeoutTimer = 0; if (table->TiltLockFlag) { for (auto ball : table->BallList) { table->Drain->Collision(ball, &vec, &vec, 0.0, nullptr); } } }