Vendor SpaceCadetPinball, build for Apple Silicon
Reverse-engineered reimplementation of 3D Pinball for Windows - Space Cadet from k4zmu2a/SpaceCadetPinball (MIT), upstream cb9b7b8, vendored rather than submoduled so local patches are just edits. - CMakeLists: stop hard-setting CMAKE_OSX_ARCHITECTURES so the documented -D override works; brew's SDL2 is arm64-only and universal linking failed. - gitignore original .DAT/.MID assets, which are not ours to redistribute. - CLAUDE.md: build line, upstream deviation, and where game data goes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
b671975742
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
||||
23
.github/workflows/ReleaseBuilds.yml
vendored
Normal file
23
.github/workflows/ReleaseBuilds.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
name: Make Release Builds
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
build-macos:
|
||||
runs-on: macos-12
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: 13
|
||||
|
||||
- run: bash build-mac-app.sh
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mac-build
|
||||
path: SpaceCadetPinball-*-mac.dmg
|
||||
308
.gitignore
vendored
Normal file
308
.gitignore
vendored
Normal file
@ -0,0 +1,308 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
ARM/
|
||||
ARM64/
|
||||
win32/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
#*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
/Ida
|
||||
/Export
|
||||
/DrMem
|
||||
/Doc private
|
||||
# Windows and macOS local libraries
|
||||
/Libs
|
||||
|
||||
#CMake generated
|
||||
out/
|
||||
/cmake-build-debug
|
||||
|
||||
# Windows XP stuff
|
||||
DebugWinXp/
|
||||
ReleaseWinXp/
|
||||
|
||||
### CMake ###
|
||||
CMakeLists.txt.user
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
CMakeScripts
|
||||
Testing
|
||||
Makefile
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
CTestTestfile.cmake
|
||||
_deps
|
||||
|
||||
### CMake Patch ###
|
||||
# External projects
|
||||
*-prefix/
|
||||
|
||||
### Ninja ###
|
||||
.ninja_deps
|
||||
.ninja_log
|
||||
build.ninja
|
||||
# Build directory
|
||||
/build
|
||||
|
||||
# Original Microsoft/Maxis game assets — never commit these, they are not ours to redistribute
|
||||
*.DAT
|
||||
*.dat
|
||||
*.MID
|
||||
*.mid
|
||||
20
BuildForWindows.ps1
Normal file
20
BuildForWindows.ps1
Normal file
@ -0,0 +1,20 @@
|
||||
#Run this script from Developer Command Prompt for VS *
|
||||
$artefacts = ".\bin\Release\SpaceCadetPinball.exe", ".\bin\Release\SDL2.dll", ".\bin\Release\SDL2_mixer.dll"
|
||||
|
||||
#X86 build
|
||||
Remove-Item -Path .\build\CMakeCache.txt -ErrorAction SilentlyContinue
|
||||
cmake -S . -B build -A Win32 -DCMAKE_WIN32_EXECUTABLE:BOOL=1
|
||||
cmake --build build --config Release
|
||||
Compress-Archive -Path $artefacts -DestinationPath ".\bin\SpaceCadetPinballx86Win.zip" -Force
|
||||
|
||||
#X64 build
|
||||
Remove-Item -Path .\build\CMakeCache.txt
|
||||
cmake -S . -B build -A x64 -DCMAKE_WIN32_EXECUTABLE:BOOL=1
|
||||
cmake --build build --config Release
|
||||
Compress-Archive -Path $artefacts -DestinationPath ".\bin\SpaceCadetPinballx64Win.zip" -Force
|
||||
|
||||
#86 XP build, requires special XP MSVC toolset
|
||||
Remove-Item -Path .\build\CMakeCache.txt
|
||||
cmake -S . -B build -A Win32 -DCMAKE_WIN32_EXECUTABLE:BOOL=1 -T v141_xp
|
||||
cmake --build build --config Release
|
||||
Compress-Archive -Path $artefacts -DestinationPath ".\bin\SpaceCadetPinballx86WinXP.zip" -Force
|
||||
42
CLAUDE.md
Normal file
42
CLAUDE.md
Normal file
@ -0,0 +1,42 @@
|
||||
# macrosoft3dpinball
|
||||
|
||||
`3D Pinball for Windows – Space Cadet` on macOS. Vendored copy of
|
||||
[k4zmu2a/SpaceCadetPinball](https://github.com/k4zmu2a/SpaceCadetPinball) (MIT) at upstream
|
||||
commit `cb9b7b8`, not a submodule — patch it in place, pull upstream by re-vendoring.
|
||||
|
||||
## Build (Apple Silicon)
|
||||
|
||||
```bash
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 \
|
||||
-DCMAKE_PREFIX_PATH="$(brew --prefix sdl2);$(brew --prefix sdl2_mixer)"
|
||||
cmake --build build -j8
|
||||
```
|
||||
|
||||
Binary lands in `bin/SpaceCadetPinball`. Needs `brew install sdl2 sdl2_mixer`.
|
||||
|
||||
Do **not** use upstream `build-mac-app.sh` — it downloads SDL2 `.framework` DMGs and builds a
|
||||
signed universal `.app`+`.dmg`. Unnecessary here; brew SDL2 is already installed.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `CMakeLists.txt` line ~27 upstream hard-set `CMAKE_OSX_ARCHITECTURES "arm64;x86_64"`, which
|
||||
silently overrode the `-D` flag the README tells you to pass. Universal linking then fails
|
||||
because brew SDL2 is arm64-only. Patched to only set the default when not given on the
|
||||
command line. Re-apply this when re-vendoring upstream.
|
||||
- Homebrew resolves `sdl2` to `sdl2-compat` (SDL2 API on top of SDL3). Links and runs fine.
|
||||
- Deployment target is 10.11, so the link emits "built for newer version 26.0" warnings against
|
||||
the brew dylibs. Harmless.
|
||||
|
||||
## Game data (not in this repo)
|
||||
|
||||
The engine ships no assets. It needs the original data files, which are Microsoft's and are
|
||||
**not redistributable** — supply them from your own media (Windows XP or earlier install disc
|
||||
for `PINBALL.DAT`; the retail `Full Tilt! Pinball` CD for `CADET.DAT`). `.DAT`/`.MID` are
|
||||
gitignored so they never land in a commit.
|
||||
|
||||
Drop them in any of these; first match wins in the order `CADET.DAT`, `PINBALL.DAT`, `DEMO.DAT`:
|
||||
|
||||
- the working directory, or `bin/` next to the binary
|
||||
- `~/Library/Application Support/SpaceCadetPinball/` ← preferred, keeps the repo clean
|
||||
|
||||
Without them the binary exits with "Could not load game data" and lists these paths.
|
||||
250
CMakeLists.txt
Normal file
250
CMakeLists.txt
Normal file
@ -0,0 +1,250 @@
|
||||
cmake_minimum_required(VERSION 3.0...3.5)
|
||||
project(SpaceCadetPinball)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/bin)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/CMakeModules")
|
||||
|
||||
# On Windows, set paths to SDL-devel packages here
|
||||
if(WIN32)
|
||||
list(APPEND SDL2_PATH "${CMAKE_CURRENT_LIST_DIR}/Libs/SDL2")
|
||||
list(APPEND SDL2_MIXER_PATH "${CMAKE_CURRENT_LIST_DIR}/Libs/SDL2_mixer")
|
||||
endif()
|
||||
|
||||
# Link mingw libs static
|
||||
if(MINGW)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static")
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
set(MACOSX_RPATH)
|
||||
set(CMAKE_BUILD_WITH_INSTALL_RPATH true)
|
||||
set(CMAKE_INSTALL_RPATH "@executable_path/../Frameworks")
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.11")
|
||||
# Universal by default, but let -DCMAKE_OSX_ARCHITECTURES win (brew SDL2 is single-arch).
|
||||
if(NOT DEFINED CACHE{CMAKE_OSX_ARCHITECTURES})
|
||||
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
|
||||
endif()
|
||||
list(APPEND SDL2_PATH "${CMAKE_CURRENT_LIST_DIR}/Libs")
|
||||
list(APPEND SDL2_MIXER_PATH "${CMAKE_CURRENT_LIST_DIR}/Libs")
|
||||
endif()
|
||||
|
||||
# SDL2main is not needed
|
||||
set(SDL2_BUILDING_LIBRARY ON)
|
||||
|
||||
find_package(SDL2 REQUIRED)
|
||||
FIND_PACKAGE(SDL2_mixer REQUIRED)
|
||||
|
||||
include_directories(${SDL2_INCLUDE_DIR} ${SDL2_MIXER_INCLUDE_DIR})
|
||||
get_property(dirs DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
|
||||
foreach(dir ${dirs})
|
||||
message(STATUS "Include dir='${dir}'")
|
||||
endforeach()
|
||||
|
||||
|
||||
|
||||
set(SOURCE_FILES
|
||||
SpaceCadetPinball/control.cpp
|
||||
SpaceCadetPinball/control.h
|
||||
SpaceCadetPinball/EmbeddedData.cpp
|
||||
SpaceCadetPinball/EmbeddedData.h
|
||||
SpaceCadetPinball/font_selection.cpp
|
||||
SpaceCadetPinball/font_selection.h
|
||||
SpaceCadetPinball/fullscrn.cpp
|
||||
SpaceCadetPinball/fullscrn.h
|
||||
SpaceCadetPinball/gdrv.cpp
|
||||
SpaceCadetPinball/gdrv.h
|
||||
SpaceCadetPinball/GroupData.cpp
|
||||
SpaceCadetPinball/GroupData.h
|
||||
SpaceCadetPinball/high_score.cpp
|
||||
SpaceCadetPinball/high_score.h
|
||||
SpaceCadetPinball/loader.cpp
|
||||
SpaceCadetPinball/loader.h
|
||||
SpaceCadetPinball/maths.cpp
|
||||
SpaceCadetPinball/maths.h
|
||||
SpaceCadetPinball/midi.cpp
|
||||
SpaceCadetPinball/midi.h
|
||||
SpaceCadetPinball/nudge.cpp
|
||||
SpaceCadetPinball/nudge.h
|
||||
SpaceCadetPinball/options.cpp
|
||||
SpaceCadetPinball/options.h
|
||||
SpaceCadetPinball/partman.cpp
|
||||
SpaceCadetPinball/partman.h
|
||||
SpaceCadetPinball/pb.cpp
|
||||
SpaceCadetPinball/pb.h
|
||||
SpaceCadetPinball/pch.h
|
||||
SpaceCadetPinball/proj.cpp
|
||||
SpaceCadetPinball/proj.h
|
||||
SpaceCadetPinball/render.cpp
|
||||
SpaceCadetPinball/render.h
|
||||
SpaceCadetPinball/score.cpp
|
||||
SpaceCadetPinball/score.h
|
||||
SpaceCadetPinball/Sound.cpp
|
||||
SpaceCadetPinball/Sound.h
|
||||
SpaceCadetPinball/SpaceCadetPinball.cpp
|
||||
SpaceCadetPinball/TBall.cpp
|
||||
SpaceCadetPinball/TBall.h
|
||||
SpaceCadetPinball/TBlocker.cpp
|
||||
SpaceCadetPinball/TBlocker.h
|
||||
SpaceCadetPinball/TBumper.cpp
|
||||
SpaceCadetPinball/TBumper.h
|
||||
SpaceCadetPinball/TCircle.cpp
|
||||
SpaceCadetPinball/TCircle.h
|
||||
SpaceCadetPinball/TCollisionComponent.cpp
|
||||
SpaceCadetPinball/TCollisionComponent.h
|
||||
SpaceCadetPinball/TComponentGroup.cpp
|
||||
SpaceCadetPinball/TComponentGroup.h
|
||||
SpaceCadetPinball/TDemo.cpp
|
||||
SpaceCadetPinball/TDemo.h
|
||||
SpaceCadetPinball/TDrain.cpp
|
||||
SpaceCadetPinball/TDrain.h
|
||||
SpaceCadetPinball/TEdgeBox.h
|
||||
SpaceCadetPinball/TEdgeManager.cpp
|
||||
SpaceCadetPinball/TEdgeManager.h
|
||||
SpaceCadetPinball/TEdgeSegment.cpp
|
||||
SpaceCadetPinball/TEdgeSegment.h
|
||||
SpaceCadetPinball/TFlagSpinner.cpp
|
||||
SpaceCadetPinball/TFlagSpinner.h
|
||||
SpaceCadetPinball/TFlipper.cpp
|
||||
SpaceCadetPinball/TFlipper.h
|
||||
SpaceCadetPinball/TFlipperEdge.cpp
|
||||
SpaceCadetPinball/TFlipperEdge.h
|
||||
SpaceCadetPinball/TGate.cpp
|
||||
SpaceCadetPinball/TGate.h
|
||||
SpaceCadetPinball/THole.cpp
|
||||
SpaceCadetPinball/THole.h
|
||||
SpaceCadetPinball/timer.cpp
|
||||
SpaceCadetPinball/timer.h
|
||||
SpaceCadetPinball/TKickback.cpp
|
||||
SpaceCadetPinball/TKickback.h
|
||||
SpaceCadetPinball/TKickout.cpp
|
||||
SpaceCadetPinball/TKickout.h
|
||||
SpaceCadetPinball/TLight.cpp
|
||||
SpaceCadetPinball/TLight.h
|
||||
SpaceCadetPinball/TLightBargraph.cpp
|
||||
SpaceCadetPinball/TLightBargraph.h
|
||||
SpaceCadetPinball/TLightGroup.cpp
|
||||
SpaceCadetPinball/TLightGroup.h
|
||||
SpaceCadetPinball/TLightRollover.cpp
|
||||
SpaceCadetPinball/TLightRollover.h
|
||||
SpaceCadetPinball/TLine.cpp
|
||||
SpaceCadetPinball/TLine.h
|
||||
SpaceCadetPinball/TOneway.cpp
|
||||
SpaceCadetPinball/TOneway.h
|
||||
SpaceCadetPinball/TPinballComponent.cpp
|
||||
SpaceCadetPinball/TPinballComponent.h
|
||||
SpaceCadetPinball/TPinballTable.cpp
|
||||
SpaceCadetPinball/TPinballTable.h
|
||||
SpaceCadetPinball/TPlunger.cpp
|
||||
SpaceCadetPinball/TPlunger.h
|
||||
SpaceCadetPinball/TPopupTarget.cpp
|
||||
SpaceCadetPinball/TPopupTarget.h
|
||||
SpaceCadetPinball/TRamp.cpp
|
||||
SpaceCadetPinball/TRamp.h
|
||||
SpaceCadetPinball/translations.cpp
|
||||
SpaceCadetPinball/translations.h
|
||||
SpaceCadetPinball/TRollover.cpp
|
||||
SpaceCadetPinball/TRollover.h
|
||||
SpaceCadetPinball/TSink.cpp
|
||||
SpaceCadetPinball/TSink.h
|
||||
SpaceCadetPinball/TSoloTarget.cpp
|
||||
SpaceCadetPinball/TSoloTarget.h
|
||||
SpaceCadetPinball/TSound.cpp
|
||||
SpaceCadetPinball/TSound.h
|
||||
SpaceCadetPinball/TTableLayer.cpp
|
||||
SpaceCadetPinball/TTableLayer.h
|
||||
SpaceCadetPinball/TTextBox.cpp
|
||||
SpaceCadetPinball/TTextBox.h
|
||||
SpaceCadetPinball/TTextBoxMessage.cpp
|
||||
SpaceCadetPinball/TTextBoxMessage.h
|
||||
SpaceCadetPinball/TTimer.cpp
|
||||
SpaceCadetPinball/TTimer.h
|
||||
SpaceCadetPinball/TTripwire.cpp
|
||||
SpaceCadetPinball/TTripwire.h
|
||||
SpaceCadetPinball/TWall.cpp
|
||||
SpaceCadetPinball/TWall.h
|
||||
SpaceCadetPinball/winmain.cpp
|
||||
SpaceCadetPinball/winmain.h
|
||||
SpaceCadetPinball/zdrv.cpp
|
||||
SpaceCadetPinball/zdrv.h
|
||||
SpaceCadetPinball/imconfig.h
|
||||
SpaceCadetPinball/imgui_internal.h
|
||||
SpaceCadetPinball/imgui.cpp
|
||||
SpaceCadetPinball/imgui.h
|
||||
SpaceCadetPinball/imgui_sdl.cpp
|
||||
SpaceCadetPinball/imgui_sdl.h
|
||||
SpaceCadetPinball/imgui_draw.cpp
|
||||
SpaceCadetPinball/imgui_widgets.cpp
|
||||
SpaceCadetPinball/imgui_tables.cpp
|
||||
SpaceCadetPinball/imgui_demo.cpp
|
||||
SpaceCadetPinball/imgui_impl_sdl.cpp
|
||||
SpaceCadetPinball/imgui_impl_sdl.h
|
||||
SpaceCadetPinball/imgui_impl_sdlrenderer.h
|
||||
SpaceCadetPinball/imgui_impl_sdlrenderer.cpp
|
||||
SpaceCadetPinball/imstb_textedit.h
|
||||
SpaceCadetPinball/imstb_rectpack.h
|
||||
SpaceCadetPinball/imstb_truetype.h
|
||||
SpaceCadetPinball/DebugOverlay.cpp
|
||||
SpaceCadetPinball/DebugOverlay.h
|
||||
)
|
||||
|
||||
# On Windows, include resource file with the icon
|
||||
if(WIN32)
|
||||
set_source_files_properties(SpaceCadetPinball/SpaceCadetPinball.rc LANGUAGE RC)
|
||||
list(APPEND SOURCE_FILES SpaceCadetPinball/SpaceCadetPinball.rc)
|
||||
endif(WIN32)
|
||||
|
||||
add_executable(SpaceCadetPinball ${SOURCE_FILES})
|
||||
|
||||
# Skip pch on foreign code
|
||||
set_source_files_properties(
|
||||
SpaceCadetPinball/imgui.cpp
|
||||
SpaceCadetPinball/imgui_sdl.cpp
|
||||
SpaceCadetPinball/imgui_draw.cpp
|
||||
SpaceCadetPinball/imgui_widgets.cpp
|
||||
SpaceCadetPinball/imgui_tables.cpp
|
||||
SpaceCadetPinball/imgui_demo.cpp
|
||||
SpaceCadetPinball/imgui_impl_sdl.cpp
|
||||
SpaceCadetPinball/imgui_impl_sdlrenderer.cpp
|
||||
PROPERTIES SKIP_PRECOMPILE_HEADERS 1
|
||||
)
|
||||
|
||||
if(${CMAKE_VERSION} VERSION_GREATER "3.16.0" OR ${CMAKE_VERSION} VERSION_EQUAL "3.16.0")
|
||||
target_precompile_headers(SpaceCadetPinball
|
||||
PUBLIC
|
||||
SpaceCadetPinball/pch.h
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(SpaceCadetPinball ${SDL2_LIBRARY} ${SDL2_MIXER_LIBRARY})
|
||||
|
||||
# On Windows, copy DLL to output
|
||||
if(WIN32)
|
||||
list(GET SDL2_LIBRARY -1 SDL2_DLL_PATH)
|
||||
list(GET SDL2_MIXER_LIBRARY -1 SDL2_MIXER_DLL_PATH)
|
||||
get_filename_component(SDL2_DLL_PATH ${SDL2_DLL_PATH} DIRECTORY)
|
||||
get_filename_component(SDL2_MIXER_DLL_PATH ${SDL2_MIXER_DLL_PATH} DIRECTORY)
|
||||
if(MINGW)
|
||||
string(REGEX REPLACE "lib$" "bin" SDL2_DLL_PATH ${SDL2_DLL_PATH})
|
||||
string(REGEX REPLACE "lib$" "bin" SDL2_MIXER_DLL_PATH ${SDL2_MIXER_DLL_PATH})
|
||||
endif()
|
||||
message(STATUS "copy paths='${SDL2_DLL_PATH}' '${SDL2_MIXER_DLL_PATH}'")
|
||||
add_custom_command(TARGET SpaceCadetPinball POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${SDL2_DLL_PATH}/SDL2.dll" $<TARGET_FILE_DIR:SpaceCadetPinball>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${SDL2_MIXER_DLL_PATH}/SDL2_mixer.dll" $<TARGET_FILE_DIR:SpaceCadetPinball>
|
||||
)
|
||||
endif()
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS "${PROJECT_NAME}" RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
|
||||
install(FILES "${CMAKE_SOURCE_DIR}/Platform/Linux/${PROJECT_NAME}.desktop" DESTINATION "share/applications")
|
||||
install(FILES "${CMAKE_SOURCE_DIR}/Platform/Linux/${PROJECT_NAME}.metainfo.xml" DESTINATION "share/metainfo")
|
||||
foreach(S 16 32 48 128 192)
|
||||
install(FILES "${CMAKE_SOURCE_DIR}/${PROJECT_NAME}/Icon_${S}x${S}.png" DESTINATION
|
||||
"share/icons/hicolor/${S}x${S}/apps" RENAME "${PROJECT_NAME}.png")
|
||||
endforeach(S)
|
||||
endif()
|
||||
173
CMakeModules/FindSDL2.cmake
Normal file
173
CMakeModules/FindSDL2.cmake
Normal file
@ -0,0 +1,173 @@
|
||||
|
||||
# This module defines
|
||||
# SDL2_LIBRARY, the name of the library to link against
|
||||
# SDL2_FOUND, if false, do not try to link to SDL2
|
||||
# SDL2_INCLUDE_DIR, where to find SDL.h
|
||||
#
|
||||
# This module responds to the the flag:
|
||||
# SDL2_BUILDING_LIBRARY
|
||||
# If this is defined, then no SDL2main will be linked in because
|
||||
# only applications need main().
|
||||
# Otherwise, it is assumed you are building an application and this
|
||||
# module will attempt to locate and set the the proper link flags
|
||||
# as part of the returned SDL2_LIBRARY variable.
|
||||
#
|
||||
# Don't forget to include SDLmain.h and SDLmain.m your project for the
|
||||
# OS X framework based version. (Other versions link to -lSDL2main which
|
||||
# this module will try to find on your behalf.) Also for OS X, this
|
||||
# module will automatically add the -framework Cocoa on your behalf.
|
||||
#
|
||||
#
|
||||
# Additional Note: If you see an empty SDL2_LIBRARY_TEMP in your configuration
|
||||
# and no SDL2_LIBRARY, it means CMake did not find your SDL2 library
|
||||
# (SDL2.dll, libsdl2.so, SDL2.framework, etc).
|
||||
# Set SDL2_LIBRARY_TEMP to point to your SDL2 library, and configure again.
|
||||
# Similarly, if you see an empty SDL2MAIN_LIBRARY, you should set this value
|
||||
# as appropriate. These values are used to generate the final SDL2_LIBRARY
|
||||
# variable, but when these values are unset, SDL2_LIBRARY does not get created.
|
||||
#
|
||||
#
|
||||
# $SDL2DIR is an environment variable that would
|
||||
# correspond to the ./configure --prefix=$SDL2DIR
|
||||
# used in building SDL2.
|
||||
# l.e.galup 9-20-02
|
||||
#
|
||||
# Modified by Eric Wing.
|
||||
# Added code to assist with automated building by using environmental variables
|
||||
# and providing a more controlled/consistent search behavior.
|
||||
# Added new modifications to recognize OS X frameworks and
|
||||
# additional Unix paths (FreeBSD, etc).
|
||||
# Also corrected the header search path to follow "proper" SDL guidelines.
|
||||
# Added a search for SDL2main which is needed by some platforms.
|
||||
# Added a search for threads which is needed by some platforms.
|
||||
# Added needed compile switches for MinGW.
|
||||
#
|
||||
# On OSX, this will prefer the Framework version (if found) over others.
|
||||
# People will have to manually change the cache values of
|
||||
# SDL2_LIBRARY to override this selection or set the CMake environment
|
||||
# CMAKE_INCLUDE_PATH to modify the search paths.
|
||||
#
|
||||
# Note that the header path has changed from SDL2/SDL.h to just SDL.h
|
||||
# This needed to change because "proper" SDL convention
|
||||
# is #include "SDL.h", not <SDL2/SDL.h>. This is done for portability
|
||||
# reasons because not all systems place things in SDL2/ (see FreeBSD).
|
||||
|
||||
#=============================================================================
|
||||
# Copyright 2003-2009 Kitware, Inc.
|
||||
#
|
||||
# Distributed under the OSI-approved BSD License (the "License");
|
||||
# see accompanying file Copyright.txt for details.
|
||||
#
|
||||
# This software is distributed WITHOUT ANY WARRANTY; without even the
|
||||
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the License for more information.
|
||||
#=============================================================================
|
||||
# (To distribute this file outside of CMake, substitute the full
|
||||
# License text for the above reference.)
|
||||
|
||||
# message("<FindSDL2.cmake>")
|
||||
|
||||
SET(SDL2_SEARCH_PATHS
|
||||
~/Library/Frameworks
|
||||
/Library/Frameworks
|
||||
/usr/local
|
||||
/usr
|
||||
/sw # Fink
|
||||
/opt/local # DarwinPorts
|
||||
/opt/csw # Blastwave
|
||||
/opt
|
||||
${SDL2_PATH}
|
||||
)
|
||||
|
||||
FIND_PATH(SDL2_INCLUDE_DIR SDL.h
|
||||
HINTS
|
||||
$ENV{SDL2DIR}
|
||||
PATH_SUFFIXES include/SDL2 include SDL2
|
||||
PATHS ${SDL2_SEARCH_PATHS}
|
||||
)
|
||||
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(PATH_SUFFIXES lib64 lib/x64 lib)
|
||||
else()
|
||||
set(PATH_SUFFIXES lib/x86 lib)
|
||||
endif()
|
||||
|
||||
FIND_LIBRARY(SDL2_LIBRARY_TEMP
|
||||
NAMES SDL2
|
||||
HINTS
|
||||
$ENV{SDL2DIR}
|
||||
PATH_SUFFIXES ${PATH_SUFFIXES}
|
||||
PATHS ${SDL2_SEARCH_PATHS}
|
||||
)
|
||||
|
||||
IF(NOT SDL2_BUILDING_LIBRARY)
|
||||
IF(NOT ${SDL2_INCLUDE_DIR} MATCHES ".framework")
|
||||
# Non-OS X framework versions expect you to also dynamically link to
|
||||
# SDL2main. This is mainly for Windows and OS X. Other (Unix) platforms
|
||||
# seem to provide SDL2main for compatibility even though they don't
|
||||
# necessarily need it.
|
||||
FIND_LIBRARY(SDL2MAIN_LIBRARY
|
||||
NAMES SDL2main
|
||||
HINTS
|
||||
$ENV{SDL2DIR}
|
||||
PATH_SUFFIXES ${PATH_SUFFIXES}
|
||||
PATHS ${SDL2_SEARCH_PATHS}
|
||||
)
|
||||
ENDIF(NOT ${SDL2_INCLUDE_DIR} MATCHES ".framework")
|
||||
ENDIF(NOT SDL2_BUILDING_LIBRARY)
|
||||
|
||||
# SDL2 may require threads on your system.
|
||||
# The Apple build may not need an explicit flag because one of the
|
||||
# frameworks may already provide it.
|
||||
# But for non-OSX systems, I will use the CMake Threads package.
|
||||
IF(NOT APPLE)
|
||||
FIND_PACKAGE(Threads)
|
||||
ENDIF(NOT APPLE)
|
||||
|
||||
# MinGW needs an additional link flag, -mwindows
|
||||
# It's total link flags should look like -lmingw32 -lSDL2main -lSDL2 -mwindows
|
||||
IF(MINGW)
|
||||
SET(MINGW32_LIBRARY mingw32 "-mwindows" CACHE STRING "mwindows for MinGW")
|
||||
ENDIF(MINGW)
|
||||
|
||||
IF(SDL2_LIBRARY_TEMP)
|
||||
# For SDL2main
|
||||
IF(NOT SDL2_BUILDING_LIBRARY)
|
||||
IF(SDL2MAIN_LIBRARY)
|
||||
SET(SDL2_LIBRARY_TEMP ${SDL2MAIN_LIBRARY} ${SDL2_LIBRARY_TEMP})
|
||||
ENDIF(SDL2MAIN_LIBRARY)
|
||||
ENDIF(NOT SDL2_BUILDING_LIBRARY)
|
||||
|
||||
# For OS X, SDL2 uses Cocoa as a backend so it must link to Cocoa.
|
||||
# CMake doesn't display the -framework Cocoa string in the UI even
|
||||
# though it actually is there if I modify a pre-used variable.
|
||||
# I think it has something to do with the CACHE STRING.
|
||||
# So I use a temporary variable until the end so I can set the
|
||||
# "real" variable in one-shot.
|
||||
IF(APPLE)
|
||||
SET(SDL2_LIBRARY_TEMP ${SDL2_LIBRARY_TEMP} "-framework Cocoa")
|
||||
ENDIF(APPLE)
|
||||
|
||||
# For threads, as mentioned Apple doesn't need this.
|
||||
# In fact, there seems to be a problem if I used the Threads package
|
||||
# and try using this line, so I'm just skipping it entirely for OS X.
|
||||
IF(NOT APPLE)
|
||||
SET(SDL2_LIBRARY_TEMP ${CMAKE_THREAD_LIBS_INIT} ${SDL2_LIBRARY_TEMP})
|
||||
ENDIF(NOT APPLE)
|
||||
|
||||
# For MinGW library
|
||||
IF(MINGW)
|
||||
SET(SDL2_LIBRARY_TEMP ${MINGW32_LIBRARY} ${SDL2_LIBRARY_TEMP})
|
||||
ENDIF(MINGW)
|
||||
|
||||
# Set the final string here so the GUI reflects the final state.
|
||||
SET(SDL2_LIBRARY ${SDL2_LIBRARY_TEMP} CACHE STRING "Where the SDL2 Library can be found")
|
||||
# Set the temp variable to INTERNAL so it is not seen in the CMake GUI
|
||||
SET(SDL2_LIBRARY_TEMP "${SDL2_LIBRARY_TEMP}" CACHE INTERNAL "")
|
||||
ENDIF(SDL2_LIBRARY_TEMP)
|
||||
|
||||
# message("</FindSDL2.cmake>")
|
||||
|
||||
INCLUDE(FindPackageHandleStandardArgs)
|
||||
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2 REQUIRED_VARS SDL2_LIBRARY SDL2_INCLUDE_DIR)
|
||||
100
CMakeModules/FindSDL2_mixer.cmake
Normal file
100
CMakeModules/FindSDL2_mixer.cmake
Normal file
@ -0,0 +1,100 @@
|
||||
# Locate SDL_MIXER library
|
||||
#
|
||||
# This module defines:
|
||||
#
|
||||
# ::
|
||||
#
|
||||
# SDL2_MIXER_LIBRARIES, the name of the library to link against
|
||||
# SDL2_MIXER_INCLUDE_DIRS, where to find the headers
|
||||
# SDL2_MIXER_FOUND, if false, do not try to link against
|
||||
# SDL2_MIXER_VERSION_STRING - human-readable string containing the version of SDL_MIXER
|
||||
#
|
||||
#
|
||||
#
|
||||
# For backward compatibility the following variables are also set:
|
||||
#
|
||||
# ::
|
||||
#
|
||||
# SDLMIXER_LIBRARY (same value as SDL2_MIXER_LIBRARIES)
|
||||
# SDLMIXER_INCLUDE_DIR (same value as SDL2_MIXER_INCLUDE_DIRS)
|
||||
# SDLMIXER_FOUND (same value as SDL2_MIXER_FOUND)
|
||||
#
|
||||
#
|
||||
#
|
||||
# $SDLDIR is an environment variable that would correspond to the
|
||||
# ./configure --prefix=$SDLDIR used in building SDL.
|
||||
#
|
||||
# Created by Eric Wing. This was influenced by the FindSDL.cmake
|
||||
# module, but with modifications to recognize OS X frameworks and
|
||||
# additional Unix paths (FreeBSD, etc).
|
||||
|
||||
#=============================================================================
|
||||
# Copyright 2005-2009 Kitware, Inc.
|
||||
# Copyright 2012 Benjamin Eikel
|
||||
#
|
||||
# Distributed under the OSI-approved BSD License (the "License");
|
||||
# see accompanying file Copyright.txt for details.
|
||||
#
|
||||
# This software is distributed WITHOUT ANY WARRANTY; without even the
|
||||
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the License for more information.
|
||||
#=============================================================================
|
||||
# (To distribute this file outside of CMake, substitute the full
|
||||
# License text for the above reference.)
|
||||
|
||||
find_path(SDL2_MIXER_INCLUDE_DIR SDL_mixer.h
|
||||
HINTS
|
||||
ENV SDL2MIXERDIR
|
||||
ENV SDL2DIR
|
||||
PATH_SUFFIXES SDL2
|
||||
# path suffixes to search inside ENV{SDLDIR}
|
||||
include/SDL2 include
|
||||
PATHS ${SDL2_MIXER_PATH}
|
||||
)
|
||||
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(VC_LIB_PATH_SUFFIX lib/x64)
|
||||
else()
|
||||
set(VC_LIB_PATH_SUFFIX lib/x86)
|
||||
endif()
|
||||
|
||||
find_library(SDL2_MIXER_LIBRARY
|
||||
NAMES SDL2_mixer
|
||||
HINTS
|
||||
ENV SDL2MIXERDIR
|
||||
ENV SDL2DIR
|
||||
PATH_SUFFIXES lib bin ${VC_LIB_PATH_SUFFIX}
|
||||
PATHS ${SDL2_MIXER_PATH}
|
||||
)
|
||||
|
||||
if(SDL2_MIXER_INCLUDE_DIR AND EXISTS "${SDL2_MIXER_INCLUDE_DIR}/SDL_mixer.h")
|
||||
file(STRINGS "${SDL2_MIXER_INCLUDE_DIR}/SDL_mixer.h" SDL2_MIXER_VERSION_MAJOR_LINE REGEX "^#define[ \t]+SDL_MIXER_MAJOR_VERSION[ \t]+[0-9]+$")
|
||||
file(STRINGS "${SDL2_MIXER_INCLUDE_DIR}/SDL_mixer.h" SDL2_MIXER_VERSION_MINOR_LINE REGEX "^#define[ \t]+SDL_MIXER_MINOR_VERSION[ \t]+[0-9]+$")
|
||||
file(STRINGS "${SDL2_MIXER_INCLUDE_DIR}/SDL_mixer.h" SDL2_MIXER_VERSION_PATCH_LINE REGEX "^#define[ \t]+SDL_MIXER_PATCHLEVEL[ \t]+[0-9]+$")
|
||||
string(REGEX REPLACE "^#define[ \t]+SDL_MIXER_MAJOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_MIXER_VERSION_MAJOR "${SDL2_MIXER_VERSION_MAJOR_LINE}")
|
||||
string(REGEX REPLACE "^#define[ \t]+SDL_MIXER_MINOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_MIXER_VERSION_MINOR "${SDL2_MIXER_VERSION_MINOR_LINE}")
|
||||
string(REGEX REPLACE "^#define[ \t]+SDL_MIXER_PATCHLEVEL[ \t]+([0-9]+)$" "\\1" SDL2_MIXER_VERSION_PATCH "${SDL2_MIXER_VERSION_PATCH_LINE}")
|
||||
set(SDL2_MIXER_VERSION_STRING ${SDL2_MIXER_VERSION_MAJOR}.${SDL2_MIXER_VERSION_MINOR}.${SDL2_MIXER_VERSION_PATCH})
|
||||
unset(SDL2_MIXER_VERSION_MAJOR_LINE)
|
||||
unset(SDL2_MIXER_VERSION_MINOR_LINE)
|
||||
unset(SDL2_MIXER_VERSION_PATCH_LINE)
|
||||
unset(SDL2_MIXER_VERSION_MAJOR)
|
||||
unset(SDL2_MIXER_VERSION_MINOR)
|
||||
unset(SDL2_MIXER_VERSION_PATCH)
|
||||
endif()
|
||||
|
||||
set(SDL2_MIXER_LIBRARIES ${SDL2_MIXER_LIBRARY})
|
||||
set(SDL2_MIXER_INCLUDE_DIRS ${SDL2_MIXER_INCLUDE_DIR})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2_mixer
|
||||
REQUIRED_VARS SDL2_MIXER_LIBRARIES SDL2_MIXER_INCLUDE_DIRS
|
||||
VERSION_VAR SDL2_MIXER_VERSION_STRING)
|
||||
|
||||
# for backward compatibility
|
||||
set(SDLMIXER_LIBRARY ${SDL2_MIXER_LIBRARIES})
|
||||
set(SDLMIXER_INCLUDE_DIR ${SDL2_MIXER_INCLUDE_DIRS})
|
||||
set(SDLMIXER_FOUND ${SDL2_MIXER_FOUND})
|
||||
|
||||
mark_as_advanced(SDL2_MIXER_LIBRARY SDL2_MIXER_INCLUDE_DIR)
|
||||
74
CMakeSettings.json
Normal file
74
CMakeSettings.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "x64-Debug",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "Debug",
|
||||
"inheritEnvironments": [ "msvc_x64_x64" ],
|
||||
"buildRoot": "${projectDir}\\out\\build\\${name}",
|
||||
"installRoot": "${projectDir}\\out\\install\\${name}",
|
||||
"cmakeCommandArgs": "",
|
||||
"buildCommandArgs": "",
|
||||
"ctestCommandArgs": "",
|
||||
"addressSanitizerEnabled": false
|
||||
},
|
||||
{
|
||||
"name": "x86-Debug",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "Debug",
|
||||
"buildRoot": "${projectDir}\\out\\build\\${name}",
|
||||
"installRoot": "${projectDir}\\out\\install\\${name}",
|
||||
"cmakeCommandArgs": "",
|
||||
"buildCommandArgs": "",
|
||||
"ctestCommandArgs": "",
|
||||
"inheritEnvironments": [ "msvc_x86" ],
|
||||
"addressSanitizerEnabled": false
|
||||
},
|
||||
{
|
||||
"name": "x86-Release",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "Release",
|
||||
"buildRoot": "${projectDir}\\out\\build\\${name}",
|
||||
"installRoot": "${projectDir}\\out\\install\\${name}",
|
||||
"cmakeCommandArgs": "-DCMAKE_WIN32_EXECUTABLE:BOOL=1",
|
||||
"buildCommandArgs": "",
|
||||
"ctestCommandArgs": "",
|
||||
"inheritEnvironments": [ "msvc_x86" ]
|
||||
},
|
||||
{
|
||||
"name": "x64-Release",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "Release",
|
||||
"buildRoot": "${projectDir}\\out\\build\\${name}",
|
||||
"installRoot": "${projectDir}\\out\\install\\${name}",
|
||||
"cmakeCommandArgs": "-DCMAKE_WIN32_EXECUTABLE:BOOL=1",
|
||||
"buildCommandArgs": "",
|
||||
"ctestCommandArgs": "",
|
||||
"inheritEnvironments": [ "msvc_x64_x64" ]
|
||||
},
|
||||
{
|
||||
"name": "x86-Release-WinXP",
|
||||
"generator": "Visual Studio 16 2019",
|
||||
"configurationType": "Release",
|
||||
"buildRoot": "${projectDir}\\out\\build\\${name}",
|
||||
"installRoot": "${projectDir}\\out\\install\\${name}",
|
||||
"cmakeCommandArgs": "-DCMAKE_WIN32_EXECUTABLE:BOOL=1 -T v141_xp",
|
||||
"buildCommandArgs": "",
|
||||
"ctestCommandArgs": "",
|
||||
"inheritEnvironments": [ "msvc_x86" ]
|
||||
},
|
||||
{
|
||||
"name": "WSL-Clang-Debug",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "Debug",
|
||||
"buildRoot": "${projectDir}\\out\\build\\${name}",
|
||||
"installRoot": "${projectDir}\\out\\install\\${name}",
|
||||
"cmakeExecutable": "cmake",
|
||||
"cmakeCommandArgs": "",
|
||||
"buildCommandArgs": "",
|
||||
"ctestCommandArgs": "",
|
||||
"inheritEnvironments": [ "linux_clang_x64" ],
|
||||
"wslPath": "${defaultWSLPath}"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
CONTRIBUTING.md
Normal file
11
CONTRIBUTING.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Issues
|
||||
Don’t forget to mention game version, which release or branch it came from.\
|
||||
Source port issues are handled in their respective repositories.
|
||||
|
||||
# Pull request
|
||||
No source ports in main repository.\
|
||||
I have no way to test and maintain most of them.\
|
||||
The best I can do is to add a link.
|
||||
|
||||
There is no guarantee that any particular PR will be accepted.\
|
||||
If you are unsure, ask first, make PR second.
|
||||
5084
Doc/.dat dump.txt
Normal file
5084
Doc/.dat dump.txt
Normal file
File diff suppressed because it is too large
Load Diff
133
Doc/.dat file format.txt
Normal file
133
Doc/.dat file format.txt
Normal file
@ -0,0 +1,133 @@
|
||||
+-----------------------------------------------------+
|
||||
| 3D Pinball for Windows ("PARTOUT") .DAT file format |
|
||||
| AdrienTD |
|
||||
+-----------------------------------------------------+
|
||||
|
||||
//-- Header --//
|
||||
|
||||
+0x00: File signature BYTE*21
|
||||
+0x15: App name BYTE*50
|
||||
+0x47: Description BYTE*100
|
||||
+0xAB: File size DWORD
|
||||
+0xAF: Number of files/groups WORD
|
||||
+0xB1: Size of body DWORD
|
||||
+0xB5: Unknown (0) WORD
|
||||
|
||||
In 3D Pinball, the file signature is "PARTOUT(4.0)RESOURCE". The rest is
|
||||
filled with 0.
|
||||
|
||||
//-- Body --//
|
||||
|
||||
The body is constitued of groups. Every group begins with a byte that tells
|
||||
how many entries are in the group.
|
||||
|
||||
Every entry begins with a byte that specifies the type of the entry. A DWORD that
|
||||
tells the size of the coming data follows the byte. Then there's some data for the
|
||||
entry. However, if the type is 0, the type byte is followed by a short value,
|
||||
and that's all.
|
||||
|
||||
Existing types:
|
||||
|
||||
Type Meaning/comments
|
||||
---------------------------------------------------
|
||||
0 ?, does not have the 32bits size value, but a 16bits value (see above).
|
||||
1 8 bpp bitmap
|
||||
3 Group name
|
||||
5 Palette (1 color is 1 DWORD, only present 1 time in PINBALL.DAT
|
||||
with a data size of 1024 bytes for 256 colors. Some colors are 0
|
||||
because their indexes are reserved by Windows.)
|
||||
9 String (content)
|
||||
10 Array of 16bits integer values
|
||||
11 Array of 32bits floating point values (collision box, ...)
|
||||
12 16 bpp bitmap (zMap)
|
||||
|
||||
|
||||
//-- 8bpp bitmap data header --//
|
||||
+0: Resolution BYTE 0=640x480, 1=800x600, 2=1024x768, -1=Load in all resolutions
|
||||
+1: Width WORD
|
||||
+3: Height WORD
|
||||
+5: X position WORD
|
||||
+7 Y position WORD
|
||||
+9: Size of bitmap DWORD
|
||||
+13: Flags BYTE bit0=Raw bmp align; bit1=DibBitmap, raw when 0; bit2=Spliced bitmap (aka skipline), combines bmp and zMap in RLE-like way
|
||||
+14: Bitmap data BYTE*(DWORD@+9)
|
||||
|
||||
|
||||
//-- 16bpp zMap data header --//
|
||||
+0: Width WORD
|
||||
+2: Height WORD
|
||||
+4: Pitch/2 WORD
|
||||
+6: Unknown (0) DWORD
|
||||
+10: Unknown (0) WORD
|
||||
+12: Unknown (80) WORD
|
||||
+14: Bitmap data BYTE*(DWORD@+9)
|
||||
|
||||
//-- 16bpp zMap data header full tilt --//
|
||||
+0: Resolution BYTE 0=640x480, 1=800x600, 2=1024x768, -1=Load in all resolutions
|
||||
+1: Width WORD
|
||||
+3: Height WORD
|
||||
+5: Pitch/2 WORD
|
||||
+7: Unknown (0) DWORD
|
||||
+11: Unknown (0) WORD
|
||||
+13: Unknown (80) WORD
|
||||
+15: Bitmap data BYTE*(DWORD@+9)
|
||||
|
||||
|
||||
//-- Pinball 3D remarkable groups --//
|
||||
|
||||
-- table_size
|
||||
Entry type 10 contains 2 16-bit integers: first is width, second is height.
|
||||
|
||||
-- table_objects
|
||||
In entry type 10, the first integer is unknown, but then comes a series of
|
||||
16-bits integer pairs. Every pair represents an object of the table. The
|
||||
first integer is the type of object, and the second is the number of
|
||||
a group/resource in this data file.
|
||||
|
||||
Types of object (3D Pinball Space Cadet for Windows):
|
||||
1000: ?
|
||||
1001: Plunger
|
||||
1002: Light (lite###)
|
||||
1003: Left flipper
|
||||
1004: Right flipper
|
||||
1005: Bumper
|
||||
1006: Yellow target
|
||||
1007: Drain (no bmp)
|
||||
1010: ? (no bmp)
|
||||
1011: Bloc (dot between the flippers)
|
||||
1012: kout (no bmp) (?)
|
||||
1013: Gate
|
||||
1014: Kicker
|
||||
1015: Roll
|
||||
1016: One way (no bmp) (?)
|
||||
1017: Sink (no bmp) (?)
|
||||
1018: Flag
|
||||
1019: Red target
|
||||
1020: Roll (the green circle that makes a weird sound when rolling on it)
|
||||
1021: Ramp (no bmp)
|
||||
1022: Ramp hole (no bmp)
|
||||
1023: Demo (no bmp)
|
||||
1024: Trip (no bmp)
|
||||
1026: Lights (no bmp, list?)
|
||||
1028: Bumpers list (one for attack, one for launch)
|
||||
1029: kout (no bmp) (?, similar to 1012?)
|
||||
1030: Fuel bargraph (list?)
|
||||
1031: Sound
|
||||
1033: Text box (score, ball counter, player number)
|
||||
|
||||
//-- Information --//
|
||||
|
||||
There's an article of the format at http://rewiki.regengedanken.de/.
|
||||
I haven't read it completely, but some informations helped me a bit.
|
||||
|
||||
Another thing: There's nearly nothing about hacking the game on the web (even
|
||||
though the Space Cadet game bundled with Windows is very popular).
|
||||
What I've only seen when doing a (Google) web search is the 'hidden test'
|
||||
cheat code. [as of 2017 or even less]
|
||||
|
||||
|
||||
//-- Contact --//
|
||||
|
||||
Any questions? Contact me at [see my GitHub profile @AdrienTD for contacts] .
|
||||
|
||||
AdrienTD :)
|
||||
12193
Doc/.pdb dump.txt
Normal file
12193
Doc/.pdb dump.txt
Normal file
File diff suppressed because it is too large
Load Diff
BIN
Doc/FuncStats.xlsx
Normal file
BIN
Doc/FuncStats.xlsx
Normal file
Binary file not shown.
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2021 Andrey Muzychenko
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
25
Platform/Linux/BUILD.md
Normal file
25
Platform/Linux/BUILD.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Building
|
||||
|
||||
Install devel packages for `SDL2` and `SDL2_mixer`.\
|
||||
|
||||
Compile with CMake; tested with GCC 10, Clang 11.\
|
||||
To cross-compile for Windows, install a 64-bit version of mingw and its `SDL2` and `SDL2_mixer` distributions, then use the `mingwcc.cmake` toolchain.
|
||||
|
||||
```bash
|
||||
# On Debian and Ubuntu
|
||||
sudo apt install cmake build-essential ninja-build libsdl2-dev libsdl2-mixer-dev libsdl2-mixer-2.0-0 libsdl2-2.0-0 fluidsynth
|
||||
|
||||
# On Fedora
|
||||
sudo dnf install cmake ninja-build SDL2 SDL2-devel SDL2_mixer SDL2_mixer-devel fluidsynth fluidsynth-libs mscore-fonts g++
|
||||
|
||||
# Build
|
||||
cmake -GNinja .
|
||||
ninja
|
||||
```
|
||||
|
||||
**If you can't hear the background music**: You need to export the following variable before launching the game like this:
|
||||
`export SDL_SOUNDFONT=/path/to/your/soundfont`
|
||||
|
||||
Fluidsynth will install a default soundfont in the following path:
|
||||
On Fedora: `/usr/share/soundfonts/FluidR3_GM.sf2`
|
||||
On Ubuntu/Debian: `/usr/share/sounds/sf2/FluidR3_GM.sf2`
|
||||
10
Platform/Linux/SpaceCadetPinball.desktop
Normal file
10
Platform/Linux/SpaceCadetPinball.desktop
Normal file
@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Space Cadet Pinball
|
||||
Comment=Decompilation of 3D Pinball for Windows - Space Cadet
|
||||
Exec=SpaceCadetPinball
|
||||
StartupNotify=true
|
||||
Terminal=false
|
||||
Icon=SpaceCadetPinball
|
||||
Type=Application
|
||||
Categories=Game;ArcadeGame;
|
||||
95
Platform/Linux/SpaceCadetPinball.metainfo.xml
Normal file
95
Platform/Linux/SpaceCadetPinball.metainfo.xml
Normal file
@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>com.github.k4zmu2a.spacecadetpinball</id>
|
||||
<name>Space Cadet Pinball</name>
|
||||
<display_name>Space Cadet Pinball</display_name>
|
||||
<summary>Game engine for Space Cadet Pinball</summary>
|
||||
<url type="homepage">https://github.com/k4zmu2a/SpaceCadetPinball</url>
|
||||
<url type="bugtracker">https://github.com/k4zmu2a/SpaceCadetPinball/issues</url>
|
||||
<description>
|
||||
<p>
|
||||
Reverse engineering of '3D Pinball for Windows - Space Cadet', a game bundled with Windows.
|
||||
</p>
|
||||
</description>
|
||||
<categories>
|
||||
<category>Game</category>
|
||||
<category>ArcadeGame</category>
|
||||
</categories>
|
||||
<launchable type="desktop-id">SpaceCadetPinball.desktop</launchable>
|
||||
<provides>
|
||||
<binary>SpaceCadetPinball</binary>
|
||||
</provides>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://raw.githubusercontent.com/k4zmu2a/SpaceCadetPinball/master/Screenshots/screenshot-1.jpg</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://raw.githubusercontent.com/k4zmu2a/SpaceCadetPinball/master/Screenshots/screenshot-2.jpg</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="2.1.0" date="2023-10-16">
|
||||
<description>
|
||||
<p>Feature release focusing on FT compatibility.</p>
|
||||
<p>Main highlights:</p>
|
||||
<ul>
|
||||
<li>FT: collision system changes, table control changes, multiball, three MIDI tracks.</li>
|
||||
<li>Work in progress: localization support. Depends on external font selection for most languages.</li>
|
||||
<li>HW accelerated ImGui renderer.</li>
|
||||
<li>Experimental stereo sound support.</li>
|
||||
<li>UX improvements.</li>
|
||||
<li>Bug fixes.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.0.1" date="2021-12-29">
|
||||
<description>
|
||||
<p>First bug fix release of cross-platform port.</p>
|
||||
<p>Main highlights:</p>
|
||||
<ul>
|
||||
<li>Improved Linux support: install target, icons, desktop shortcut, alternative data paths.</li>
|
||||
<li>Improved game controller support.</li>
|
||||
<li>High precision sleep mode for ~0 frame time jitter.</li>
|
||||
<li>Integer image scaling mode.</li>
|
||||
<li>3DPB <-> FT data switch option, for easier data-set changes when both are available.</li>
|
||||
<li>FT demo data support.</li>
|
||||
<li>Bug fixes.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.0.0" date="2021-10-30">
|
||||
<description>
|
||||
<p>First release of cross-platform port.</p>
|
||||
<p>Main highlights:</p>
|
||||
<ul>
|
||||
<li>Windows dependencies removed.</li>
|
||||
<li>Graphics and sounds rendered with SDL.</li>
|
||||
<li>Dear ImGui GUI.</li>
|
||||
<li>CMake build script, compiles on a lot of platforms.</li>
|
||||
<li>A number of 3rd party source ports.</li>
|
||||
</ul>
|
||||
<p>All features of v1.x are present, except for text translations.</p>
|
||||
<p>
|
||||
Development and releases of v1.x will continue, don’t confuse the two.
|
||||
All source ports are technically v2.0RC, not v1.x.
|
||||
</p>
|
||||
|
||||
<p>There are no Linux or macOS binaries attached, compile them yourself.</p>
|
||||
|
||||
<p>
|
||||
This release was made better with the help of contributors.
|
||||
I thank them for their interest in the project.
|
||||
</p>
|
||||
</description>
|
||||
</release>
|
||||
</releases>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<developer_name>Andrey Muzychenko</developer_name>
|
||||
<project_license>MIT</project_license>
|
||||
<keywords>
|
||||
<keyword>pinball</keyword>
|
||||
<keyword>space</keyword>
|
||||
<keyword>cadet</keyword>
|
||||
</keywords>
|
||||
<content_rating type="oars-1.1" />
|
||||
</component>
|
||||
36
Platform/macOS/Info.plist
Normal file
36
Platform/macOS/Info.plist
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>SpaceCadetPinball</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>SpaceCadetPinball.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.github.k4zmu2a.spacecadetpinball</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleLongVersionString</key>
|
||||
<string></string>
|
||||
<key>CFBundleName</key>
|
||||
<string>SpaceCadetPinball</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>CHANGEME_SW_VERSION</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.11</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Platform/macOS/SpaceCadetPinball.icns
Normal file
BIN
Platform/macOS/SpaceCadetPinball.icns
Normal file
Binary file not shown.
123
README.md
Normal file
123
README.md
Normal file
@ -0,0 +1,123 @@
|
||||
<!-- markdownlint-disable-file MD033 -->
|
||||
|
||||
# SpaceCadetPinball
|
||||
|
||||
## Summary
|
||||
|
||||
Reverse engineering of `3D Pinball for Windows - Space Cadet`, a game bundled with Windows.
|
||||
|
||||
## How to play
|
||||
|
||||
Place compiled executable into a folder containing original game resources (not included).\
|
||||
Supports data files from Windows and Full Tilt versions of the game.
|
||||
|
||||
## Known source ports
|
||||
|
||||
| Platform | Author | URL |
|
||||
| ------------------ | --------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| PS Vita | Axiom | <https://github.com/suicvne/SpaceCadetPinball_Vita> |
|
||||
| Emscripten | alula | <https://github.com/alula/SpaceCadetPinball> <br> Play online: <https://alula.github.io/SpaceCadetPinball> |
|
||||
| Nintendo Switch | averne | <https://github.com/averne/SpaceCadetPinball-NX> |
|
||||
| webOS TV | mariotaku | <https://github.com/webosbrew/SpaceCadetPinball> |
|
||||
| Android (WIP) | Iscle | https://github.com/Iscle/SpaceCadetPinball |
|
||||
| Nintendo Wii | MaikelChan | https://github.com/MaikelChan/SpaceCadetPinball |
|
||||
| Nintendo 3DS | MaikelChan | https://github.com/MaikelChan/SpaceCadetPinball/tree/3ds |
|
||||
| Nintendo DS | Headshotnoby | https://github.com/headshot2017/3dpinball-nds |
|
||||
| Nintendo Wii U | IntriguingTiles | https://github.com/IntriguingTiles/SpaceCadetPinball-WiiU |
|
||||
| PlayStation 2 | Headshotnoby | https://github.com/headshot2017/3dpinball-ps2 |
|
||||
| Sega Dreamcast | Headshotnoby | https://github.com/headshot2017/3dpinball-dc |
|
||||
| MorphOS | BeWorld | https://www.morphos-storage.net/?id=1688897 |
|
||||
| AmigaOS 4 | rjd324 | http://aminet.net/package/game/actio/spacecadetpinball-aos4 |
|
||||
| Android (WIP) | fexed | https://github.com/fexed/Pinball-on-Android |
|
||||
|
||||
Platforms covered by this project: desktop Windows, Linux and macOS.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## Source
|
||||
|
||||
* `pinball.exe` from `Windows XP` (SHA-1 `2A5B525E0F631BB6107639E2A69DF15986FB0D05`) and its public PDB
|
||||
* `CADET.EXE` 32bit version from `Full Tilt! Pinball` (SHA-1 `3F7B5699074B83FD713657CD94671F2156DBEDC4`)
|
||||
|
||||
## Tools used
|
||||
|
||||
`Ghidra`, `Ida`, `Visual Studio`
|
||||
|
||||
## What was done
|
||||
|
||||
* All structures were populated, globals and locals named.
|
||||
* All subs were decompiled, C pseudo code was converted to compilable C++. Loose (namespace?) subs were assigned to classes.
|
||||
|
||||
## Compiling
|
||||
|
||||
Project uses `C++11` and depends on `SDL2` libs.
|
||||
|
||||
### On Windows
|
||||
|
||||
Download and unpack devel packages for `SDL2` and `SDL2_mixer`.\
|
||||
Set paths to them in `CMakeLists.txt`, see suggested placement in `/Libs`.\
|
||||
Compile with Visual Studio; tested with 2019.
|
||||
|
||||
### On Linux
|
||||
|
||||
Install devel packages for `SDL2` and `SDL2_mixer`.\
|
||||
Compile with CMake; tested with GCC 10, Clang 11.\
|
||||
To cross-compile for Windows, install a 64-bit version of mingw and its `SDL2` and `SDL2_mixer` distributions, then use the `mingwcc.cmake` toolchain.
|
||||
|
||||
[](https://repology.org/project/spacecadetpinball/versions)
|
||||
|
||||
Some distributions provide a package in their repository. You can use those for easier dependency management and updates.
|
||||
|
||||
This project is available as Flatpak on [Flathub](https://flathub.org/apps/details/com.github.k4zmu2a.spacecadetpinball).
|
||||
|
||||
### On macOS
|
||||
|
||||
Install XCode (or at least Xcode Command Line Tools with `xcode-select --install`) and CMake.
|
||||
|
||||
**HomeBrew**
|
||||
|
||||
You can easily install the build artifact by using `brew`.
|
||||
|
||||
```sh
|
||||
brew tap draftbrew/tap
|
||||
brew install --no-quarantine space-cadet-pinball
|
||||
```
|
||||
|
||||
Be aware that the flag `--no-quarantime` will disable macOS's Gatekeeper during installation.
|
||||
|
||||
**Manual compilation:**
|
||||
|
||||
* **Homebrew**: Install the `SDL2`, `SDL2_mixer` homebrew packages.
|
||||
* **MacPorts**: Install the `libSDL2`, `libSDL2_mixer` macports packages.
|
||||
|
||||
Compile with CMake. Ensure that `CMAKE_OSX_ARCHITECTURES` variable is set for either `x86_64` Apple Intel or `arm64` for Apple Silicon.
|
||||
|
||||
Tested with: macOS Big Sur (Intel) with Xcode 13 & macOS Montery Beta (Apple Silicon) with Xcode 13.
|
||||
|
||||
**Automated compilation:**
|
||||
|
||||
Run the `build-mac-app.sh` script from the root of the repository. The app will be available in a DMG file named `SpaceCadetPinball-<version>-mac.dmg`.
|
||||
|
||||
Tested with: macOS Ventura (Apple Silicon) with Xcode Command Line Tools 14 & macOS Big Sur on GitHub Runner (Intel) with XCode 13.
|
||||
|
||||
## Plans
|
||||
|
||||
* ~~Decompile original game~~
|
||||
* ~~Resizable window, scaled graphics~~
|
||||
* ~~Loader for high-res sprites from CADET.DAT~~
|
||||
* ~~Cross-platform port using SDL2, SDL2_mixer, ImGui~~
|
||||
* Full Tilt Cadet features
|
||||
* Localization support
|
||||
* Maybe: Support for the other two tables - Dragon and Pirate
|
||||
* Maybe: Game data editor
|
||||
|
||||
## On 64-bit bug that killed the game
|
||||
|
||||
I did not find it, decompiled game worked in x64 mode on the first try.\
|
||||
It was either lost in decompilation or introduced in x64 port/not present in x86 build.\
|
||||
Based on public description of the bug (no ball collision), I guess that the bug was in `TEdgeManager::TestGridBox`
|
||||
358
SpaceCadetPinball/DebugOverlay.cpp
Normal file
358
SpaceCadetPinball/DebugOverlay.cpp
Normal file
@ -0,0 +1,358 @@
|
||||
#include "pch.h"
|
||||
#include "DebugOverlay.h"
|
||||
|
||||
#include "loader.h"
|
||||
#include "maths.h"
|
||||
#include "proj.h"
|
||||
#include "winmain.h"
|
||||
#include "TFlipperEdge.h"
|
||||
#include "TFlipper.h"
|
||||
#include "pb.h"
|
||||
#include "TLine.h"
|
||||
#include "TCircle.h"
|
||||
#include "TPinballTable.h"
|
||||
#include "TEdgeBox.h"
|
||||
#include "TTableLayer.h"
|
||||
#include "TBall.h"
|
||||
#include "render.h"
|
||||
#include "options.h"
|
||||
#include "Sound.h"
|
||||
|
||||
|
||||
gdrv_bitmap8* DebugOverlay::dbScreen = nullptr;
|
||||
|
||||
static int SDL_RenderDrawCircle(SDL_Renderer* renderer, int x, int y, int radius)
|
||||
{
|
||||
SDL_Point points[256];
|
||||
int pointCount = 0;
|
||||
int offsetx, offsety, d;
|
||||
int status;
|
||||
|
||||
offsetx = 0;
|
||||
offsety = radius;
|
||||
d = radius - 1;
|
||||
status = 0;
|
||||
|
||||
while (offsety >= offsetx)
|
||||
{
|
||||
if (pointCount + 8 > 256)
|
||||
{
|
||||
status = SDL_RenderDrawPoints(renderer, points, pointCount);
|
||||
pointCount = 0;
|
||||
|
||||
if (status < 0) {
|
||||
status = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
points[pointCount++] = { x + offsetx, y + offsety };
|
||||
points[pointCount++] = { x + offsety, y + offsetx };
|
||||
points[pointCount++] = { x - offsetx, y + offsety };
|
||||
points[pointCount++] = { x - offsety, y + offsetx };
|
||||
points[pointCount++] = { x + offsetx, y - offsety };
|
||||
points[pointCount++] = { x + offsety, y - offsetx };
|
||||
points[pointCount++] = { x - offsetx, y - offsety };
|
||||
points[pointCount++] = { x - offsety, y - offsetx };
|
||||
|
||||
if (d >= 2 * offsetx) {
|
||||
d -= 2 * offsetx + 1;
|
||||
offsetx += 1;
|
||||
}
|
||||
else if (d < 2 * (radius - offsety)) {
|
||||
d += 2 * offsety - 1;
|
||||
offsety -= 1;
|
||||
}
|
||||
else {
|
||||
d += 2 * (offsety - offsetx - 1);
|
||||
offsety -= 1;
|
||||
offsetx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (pointCount > 0)
|
||||
status = SDL_RenderDrawPoints(renderer, points, pointCount);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
void DebugOverlay::UnInit()
|
||||
{
|
||||
delete dbScreen;
|
||||
dbScreen = nullptr;
|
||||
}
|
||||
|
||||
void DebugOverlay::DrawOverlay()
|
||||
{
|
||||
if (dbScreen == nullptr)
|
||||
{
|
||||
dbScreen = new gdrv_bitmap8(render::vscreen->Width, render::vscreen->Height, false, false);
|
||||
dbScreen->CreateTexture("nearest", SDL_TEXTUREACCESS_TARGET);
|
||||
SDL_SetTextureBlendMode(dbScreen->Texture, SDL_BLENDMODE_BLEND);
|
||||
}
|
||||
|
||||
// Setup overlay rendering
|
||||
Uint8 initialR, initialG, initialB, initialA;
|
||||
auto initialRenderTarget = SDL_GetRenderTarget(winmain::Renderer);
|
||||
SDL_GetRenderDrawColor(winmain::Renderer, &initialR, &initialG, &initialB, &initialA);
|
||||
SDL_SetRenderTarget(winmain::Renderer, dbScreen->Texture);
|
||||
SDL_SetRenderDrawColor(winmain::Renderer, 0, 0, 0, 0);
|
||||
SDL_RenderClear(winmain::Renderer);
|
||||
|
||||
// Draw EdgeManager box grid
|
||||
if (options::Options.DebugOverlayGrid)
|
||||
DrawBoxGrid();
|
||||
|
||||
// Draw bounding boxes around sprites
|
||||
if (options::Options.DebugOverlaySprites)
|
||||
DrawAllSprites();
|
||||
|
||||
// Draw all edges registered in TCollisionComponent.EdgeList + flippers
|
||||
if (options::Options.DebugOverlayAllEdges)
|
||||
DrawAllEdges();
|
||||
|
||||
// Draw ball collision info
|
||||
if (options::Options.DebugOverlayBallPosition || options::Options.DebugOverlayBallEdges)
|
||||
DrawBallInfo();
|
||||
|
||||
// Draw positions associated with currently playing sound channels
|
||||
if (options::Options.DebugOverlaySounds)
|
||||
DrawSoundPositions();
|
||||
|
||||
// Draw ball depth cutoff steps that determine sprite size.
|
||||
if (options::Options.DebugOverlayBallDepthGrid)
|
||||
DrawBallDepthSteps();
|
||||
|
||||
// Draw AABB of collision components
|
||||
if (options::Options.DebugOverlayAabb)
|
||||
DrawComponentAabb();
|
||||
|
||||
// Restore render target
|
||||
SDL_SetRenderTarget(winmain::Renderer, initialRenderTarget);
|
||||
SDL_SetRenderDrawColor(winmain::Renderer,
|
||||
initialR, initialG, initialB, initialA);
|
||||
|
||||
// Copy overlay with alpha blending
|
||||
SDL_BlendMode blendMode;
|
||||
SDL_GetRenderDrawBlendMode(winmain::Renderer, &blendMode);
|
||||
SDL_SetRenderDrawBlendMode(winmain::Renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_RenderCopy(winmain::Renderer, dbScreen->Texture, nullptr, &render::DestinationRect);
|
||||
SDL_SetRenderDrawBlendMode(winmain::Renderer, blendMode);
|
||||
}
|
||||
|
||||
void DebugOverlay::DrawBoxGrid()
|
||||
{
|
||||
auto& edgeMan = *TTableLayer::edge_manager;
|
||||
|
||||
SDL_SetRenderDrawColor(winmain::Renderer, 0, 255, 0, 255);
|
||||
for (int x = 0; x <= edgeMan.MaxBoxX; x++)
|
||||
{
|
||||
vector2 boxPt{ x * edgeMan.AdvanceX + edgeMan.MinX , edgeMan.MinY };
|
||||
auto pt1 = proj::xform_to_2d(boxPt);
|
||||
boxPt.Y = edgeMan.MaxBoxY * edgeMan.AdvanceY + edgeMan.MinY;
|
||||
auto pt2 = proj::xform_to_2d(boxPt);
|
||||
|
||||
SDL_RenderDrawLine(winmain::Renderer, pt1.X, pt1.Y, pt2.X, pt2.Y);
|
||||
}
|
||||
for (int y = 0; y <= edgeMan.MaxBoxY; y++)
|
||||
{
|
||||
vector2 boxPt{ edgeMan.MinX, y * edgeMan.AdvanceY + edgeMan.MinY };
|
||||
auto pt1 = proj::xform_to_2d(boxPt);
|
||||
boxPt.X = edgeMan.MaxBoxX * edgeMan.AdvanceX + edgeMan.MinX;
|
||||
auto pt2 = proj::xform_to_2d(boxPt);
|
||||
|
||||
SDL_RenderDrawLine(winmain::Renderer, pt1.X, pt1.Y, pt2.X, pt2.Y);
|
||||
}
|
||||
}
|
||||
|
||||
void DebugOverlay::DrawAllEdges()
|
||||
{
|
||||
SDL_SetRenderDrawColor(winmain::Renderer, 0, 200, 200, 255);
|
||||
for (auto cmp : pb::MainTable->ComponentList)
|
||||
{
|
||||
auto collCmp = dynamic_cast<TCollisionComponent*>(cmp);
|
||||
if (collCmp)
|
||||
{
|
||||
for (auto edge : collCmp->EdgeList)
|
||||
{
|
||||
DrawEdge(edge);
|
||||
}
|
||||
}
|
||||
auto flip = dynamic_cast<TFlipper*>(cmp);
|
||||
if (flip)
|
||||
{
|
||||
DrawEdge(flip->FlipperEdge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DebugOverlay::DrawBallInfo()
|
||||
{
|
||||
auto& edgeMan = *TTableLayer::edge_manager;
|
||||
for (auto ball : pb::MainTable->BallList)
|
||||
{
|
||||
if (ball->ActiveFlag)
|
||||
{
|
||||
vector2 ballPosition = { ball->Position.X, ball->Position.Y };
|
||||
|
||||
if (options::Options.DebugOverlayBallEdges)
|
||||
{
|
||||
SDL_SetRenderDrawColor(winmain::Renderer, 255, 0, 0, 255);
|
||||
auto x = edgeMan.box_x(ballPosition.X), y = edgeMan.box_y(ballPosition.Y);
|
||||
auto& box = edgeMan.BoxArray[x + y * edgeMan.MaxBoxX];
|
||||
for (auto edge : box.EdgeList)
|
||||
{
|
||||
DrawEdge(edge);
|
||||
}
|
||||
}
|
||||
|
||||
if (options::Options.DebugOverlayBallPosition)
|
||||
{
|
||||
SDL_SetRenderDrawColor(winmain::Renderer, 0, 0, 255, 255);
|
||||
|
||||
auto pt1 = proj::xform_to_2d(ballPosition);
|
||||
vector2 radVec1 = { 0, ballPosition.Y }, radVec2 = { ball->Radius, ballPosition.Y };
|
||||
auto radVec1I = proj::xform_to_2d(radVec1), radVec2I = proj::xform_to_2d(radVec2);
|
||||
auto radI = std::sqrt(maths::magnitudeSq(vector2i{ radVec1I.X - radVec2I.X ,radVec1I.Y - radVec2I.Y }));
|
||||
SDL_RenderDrawCircle(winmain::Renderer, pt1.X, pt1.Y, static_cast<int>(std::round(radI)));
|
||||
|
||||
auto nextPos = ballPosition;
|
||||
maths::vector_add(nextPos, maths::vector_mul(ball->Direction, ball->Speed / 10.0f));
|
||||
auto pt2 = proj::xform_to_2d(nextPos);
|
||||
SDL_RenderDrawLine(winmain::Renderer, pt1.X, pt1.Y, pt2.X, pt2.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DebugOverlay::DrawAllSprites()
|
||||
{
|
||||
SDL_SetRenderDrawColor(winmain::Renderer, 200, 200, 0, 255);
|
||||
for (auto cmp : pb::MainTable->ComponentList)
|
||||
{
|
||||
if (cmp->RenderSprite)
|
||||
{
|
||||
auto& bmpR = cmp->RenderSprite->BmpRect;
|
||||
if (bmpR.Width != 0 && bmpR.Height != 0)
|
||||
{
|
||||
SDL_Rect rect{ bmpR.XPosition, bmpR.YPosition, bmpR.Width, bmpR.Height };
|
||||
SDL_RenderDrawRect(winmain::Renderer, &rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DebugOverlay::DrawSoundPositions()
|
||||
{
|
||||
auto& edgeMan = *TTableLayer::edge_manager;
|
||||
SDL_SetRenderDrawColor(winmain::Renderer, 200, 0, 200, 255);
|
||||
|
||||
for (auto& posNorm : Sound::Channels)
|
||||
{
|
||||
auto pos3D = edgeMan.DeNormalizeBox(posNorm.Position);
|
||||
auto pos2D = proj::xform_to_2d(pos3D);
|
||||
SDL_RenderDrawCircle(winmain::Renderer, pos2D.X, pos2D.Y, 7);
|
||||
}
|
||||
}
|
||||
|
||||
void DebugOverlay::DrawBallDepthSteps()
|
||||
{
|
||||
auto& edgeMan = *TTableLayer::edge_manager;
|
||||
SDL_SetRenderDrawColor(winmain::Renderer, 200, 100, 0, 255);
|
||||
|
||||
for (auto ball : pb::MainTable->BallList)
|
||||
{
|
||||
auto visualCount = loader::query_visual_states(ball->GroupIndex);
|
||||
for (auto index = 0; index < visualCount; ++index)
|
||||
{
|
||||
auto depthPt = reinterpret_cast<vector3*>(loader::query_float_attribute(ball->GroupIndex, index, 501));
|
||||
auto pt = proj::xform_to_2d(*depthPt);
|
||||
|
||||
// Snap X coordinate to edge box sides
|
||||
auto x1 = proj::xform_to_2d(vector2{edgeMan.MinX, depthPt->Y}).X;
|
||||
auto x2 = proj::xform_to_2d(vector2{edgeMan.MaxBoxX * edgeMan.AdvanceX + edgeMan.MinX, depthPt->Y}).X;
|
||||
auto ff = proj::xform_to_2d(vector2{ edgeMan.MaxBoxX * edgeMan.AdvanceX + edgeMan.MinX, depthPt->Y });
|
||||
SDL_RenderDrawLine(winmain::Renderer, x1, pt.Y, x2, pt.Y);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DebugOverlay::DrawComponentAabb()
|
||||
{
|
||||
SDL_SetRenderDrawColor(winmain::Renderer, 0, 50, 200, 255);
|
||||
for (auto cmp : pb::MainTable->ComponentList)
|
||||
{
|
||||
auto collCmp = dynamic_cast<TCollisionComponent*>(cmp);
|
||||
if (collCmp)
|
||||
{
|
||||
const auto& aabb = collCmp->AABB;
|
||||
auto pt1 = proj::xform_to_2d(vector2{ aabb.XMax, aabb.YMax });
|
||||
auto pt2 = proj::xform_to_2d(vector2{ aabb.XMin, aabb.YMin });
|
||||
SDL_Rect rect{ pt2.X,pt2.Y, pt1.X - pt2.X , pt1.Y - pt2.Y };
|
||||
SDL_RenderDrawRect(winmain::Renderer, &rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DebugOverlay::DrawCicleType(circle_type& circle)
|
||||
{
|
||||
vector2 linePt{ circle.Center.X + sqrt(circle.RadiusSq), circle.Center.Y };
|
||||
auto pt1 = proj::xform_to_2d(circle.Center);
|
||||
auto pt2 = proj::xform_to_2d(linePt);
|
||||
auto radius = abs(pt2.X - pt1.X);
|
||||
|
||||
SDL_RenderDrawCircle(winmain::Renderer, pt1.X, pt1.Y, radius);
|
||||
}
|
||||
|
||||
void DebugOverlay::DrawLineType(line_type& line)
|
||||
{
|
||||
auto pt1 = proj::xform_to_2d(line.Origin);
|
||||
auto pt2 = proj::xform_to_2d(line.End);
|
||||
|
||||
SDL_RenderDrawLine(winmain::Renderer, pt1.X, pt1.Y, pt2.X, pt2.Y);
|
||||
}
|
||||
|
||||
void DebugOverlay::DrawEdge(TEdgeSegment* edge)
|
||||
{
|
||||
if (options::Options.DebugOverlayCollisionMask)
|
||||
{
|
||||
TBall* refBall = nullptr;
|
||||
for (auto ball : pb::MainTable->BallList)
|
||||
{
|
||||
if (ball->ActiveFlag)
|
||||
{
|
||||
refBall = ball;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (refBall != nullptr && (refBall->CollisionMask & edge->CollisionGroup) == 0)
|
||||
return;
|
||||
}
|
||||
|
||||
auto line = dynamic_cast<TLine*>(edge);
|
||||
if (line)
|
||||
{
|
||||
DrawLineType(line->Line);
|
||||
return;
|
||||
}
|
||||
|
||||
auto circle = dynamic_cast<TCircle*>(edge);
|
||||
if (circle)
|
||||
{
|
||||
DrawCicleType(circle->Circle);
|
||||
return;
|
||||
}
|
||||
|
||||
auto flip = dynamic_cast<TFlipperEdge*>(edge);
|
||||
if (flip)
|
||||
{
|
||||
if (flip->ControlPointDirtyFlag)
|
||||
flip->set_control_points(flip->CurrentAngle);
|
||||
|
||||
DrawLineType(flip->LineA);
|
||||
DrawLineType(flip->LineB);
|
||||
DrawCicleType(flip->circlebase);
|
||||
DrawCicleType(flip->circleT1);
|
||||
}
|
||||
}
|
||||
26
SpaceCadetPinball/DebugOverlay.h
Normal file
26
SpaceCadetPinball/DebugOverlay.h
Normal file
@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
struct gdrv_bitmap8;
|
||||
struct circle_type;
|
||||
struct line_type;
|
||||
class TEdgeSegment;
|
||||
|
||||
class DebugOverlay
|
||||
{
|
||||
public:
|
||||
static void UnInit();
|
||||
static void DrawOverlay();
|
||||
private:
|
||||
static gdrv_bitmap8* dbScreen;
|
||||
|
||||
static void DrawCicleType(circle_type& circle);
|
||||
static void DrawLineType(line_type& line);
|
||||
static void DrawEdge(TEdgeSegment* edge);
|
||||
static void DrawBoxGrid();
|
||||
static void DrawAllEdges();
|
||||
static void DrawBallInfo();
|
||||
static void DrawAllSprites();
|
||||
static void DrawSoundPositions();
|
||||
static void DrawBallDepthSteps();
|
||||
static void DrawComponentAabb();
|
||||
};
|
||||
1380
SpaceCadetPinball/EmbeddedData.cpp
Normal file
1380
SpaceCadetPinball/EmbeddedData.cpp
Normal file
File diff suppressed because it is too large
Load Diff
10
SpaceCadetPinball/EmbeddedData.h
Normal file
10
SpaceCadetPinball/EmbeddedData.h
Normal file
@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
class EmbeddedData
|
||||
{
|
||||
public:
|
||||
static const char PB_MSGFT_bin_compressed_data_base85[5380 + 1];
|
||||
static const unsigned int SDL_GameControllerDB_compressed_data[63392 / 4];
|
||||
static const unsigned int SDL_GameControllerDB_compressed_size;
|
||||
};
|
||||
361
SpaceCadetPinball/GroupData.cpp
Normal file
361
SpaceCadetPinball/GroupData.cpp
Normal file
@ -0,0 +1,361 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include "GroupData.h"
|
||||
|
||||
#include "EmbeddedData.h"
|
||||
#include "fullscrn.h"
|
||||
#include "gdrv.h"
|
||||
#include "pb.h"
|
||||
#include "zdrv.h"
|
||||
|
||||
|
||||
EntryData::~EntryData()
|
||||
{
|
||||
if (Buffer)
|
||||
{
|
||||
if (EntryType == FieldTypes::Bitmap8bit)
|
||||
{
|
||||
delete reinterpret_cast<gdrv_bitmap8*>(Buffer);
|
||||
}
|
||||
else if (EntryType == FieldTypes::Bitmap16bit)
|
||||
{
|
||||
delete reinterpret_cast<zmap_header_type*>(Buffer);
|
||||
}
|
||||
else
|
||||
delete[] Buffer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
GroupData::GroupData(int groupId)
|
||||
{
|
||||
GroupId = groupId;
|
||||
}
|
||||
|
||||
void GroupData::AddEntry(EntryData* entry)
|
||||
{
|
||||
auto addEntry = true;
|
||||
switch (entry->EntryType)
|
||||
{
|
||||
case FieldTypes::GroupName:
|
||||
GroupName = entry->Buffer;
|
||||
break;
|
||||
case FieldTypes::Bitmap8bit:
|
||||
{
|
||||
auto srcBmp = reinterpret_cast<gdrv_bitmap8*>(entry->Buffer);
|
||||
if (srcBmp->BitmapType == BitmapTypes::Spliced)
|
||||
{
|
||||
// Get rid of spliced bitmap early on, to simplify render pipeline
|
||||
auto bmp = new gdrv_bitmap8(srcBmp->Width, srcBmp->Height, true);
|
||||
auto zMap = new zmap_header_type(srcBmp->Width, srcBmp->Height, srcBmp->Width);
|
||||
SplitSplicedBitmap(*srcBmp, *bmp, *zMap);
|
||||
|
||||
NeedsSort = true;
|
||||
addEntry = false;
|
||||
AddEntry(new EntryData(FieldTypes::Bitmap8bit, reinterpret_cast<char*>(bmp)));
|
||||
AddEntry(new EntryData(FieldTypes::Bitmap16bit, reinterpret_cast<char*>(zMap)));
|
||||
delete entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetBitmap(srcBmp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FieldTypes::Bitmap16bit:
|
||||
{
|
||||
SetZMap(reinterpret_cast<zmap_header_type*>(entry->Buffer));
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (addEntry)
|
||||
Entries.push_back(entry);
|
||||
}
|
||||
|
||||
void GroupData::FinalizeGroup()
|
||||
{
|
||||
if (NeedsSort)
|
||||
{
|
||||
// Entries within a group are sorted by EntryType, in ascending order.
|
||||
// Dat files follow this rule, zMaps inserted in the middle break it.
|
||||
NeedsSort = false;
|
||||
std::sort(Entries.begin(), Entries.end(), [](const EntryData* lhs, const EntryData* rhs)
|
||||
{
|
||||
return lhs->EntryType < rhs->EntryType;
|
||||
});
|
||||
Entries.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
gdrv_bitmap8* GroupData::GetBitmap(int resolution) const
|
||||
{
|
||||
return Bitmaps[resolution];
|
||||
}
|
||||
|
||||
zmap_header_type* GroupData::GetZMap(int resolution) const
|
||||
{
|
||||
return ZMaps[resolution];
|
||||
}
|
||||
|
||||
void GroupData::SplitSplicedBitmap(const gdrv_bitmap8& srcBmp, gdrv_bitmap8& bmp, zmap_header_type& zMap)
|
||||
{
|
||||
assertm(srcBmp.BitmapType == BitmapTypes::Spliced, "GroupData: wrong bitmap type");
|
||||
|
||||
std::memset(bmp.IndexedBmpPtr, 0xff, bmp.Stride * bmp.Height);
|
||||
bmp.XPosition = srcBmp.XPosition;
|
||||
bmp.YPosition = srcBmp.YPosition;
|
||||
bmp.Resolution = srcBmp.Resolution;
|
||||
|
||||
zdrv::fill(&zMap, zMap.Width, zMap.Height, 0, 0, 0xFFFF);
|
||||
zMap.Resolution = srcBmp.Resolution;
|
||||
|
||||
auto tableWidth = fullscrn::resolution_array[srcBmp.Resolution].TableWidth;
|
||||
auto src = reinterpret_cast<uint16_t*>(srcBmp.IndexedBmpPtr);
|
||||
auto srcChar = reinterpret_cast<char**>(&src);
|
||||
for (int dstInd = 0;;)
|
||||
{
|
||||
auto stride = static_cast<int16_t>(*src++);
|
||||
if (stride < 0)
|
||||
break;
|
||||
|
||||
// Stride is in terms of dst stride, hardcoded to match vScreen width in current resolution
|
||||
if (stride > bmp.Width)
|
||||
{
|
||||
stride += bmp.Width - tableWidth;
|
||||
assertm(stride >= 0, "Spliced bitmap: negative computed stride");
|
||||
}
|
||||
|
||||
dstInd += stride;
|
||||
for (auto count = *src++; count; count--)
|
||||
{
|
||||
auto depth = *src++;
|
||||
bmp.IndexedBmpPtr[dstInd] = **srcChar;
|
||||
zMap.ZPtr1[dstInd] = depth;
|
||||
|
||||
(*srcChar)++;
|
||||
dstInd++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupData::SetBitmap(gdrv_bitmap8* bmp)
|
||||
{
|
||||
assertm(Bitmaps[bmp->Resolution] == nullptr, "GroupData: bitmap override");
|
||||
Bitmaps[bmp->Resolution] = bmp;
|
||||
|
||||
auto zMap = ZMaps[bmp->Resolution];
|
||||
if (zMap)
|
||||
{
|
||||
assertm(bmp->Width == zMap->Width && bmp->Height == zMap->Height,
|
||||
"GroupData: mismatched bitmap/zMap dimensions");
|
||||
}
|
||||
}
|
||||
|
||||
void GroupData::SetZMap(zmap_header_type* zMap)
|
||||
{
|
||||
// Flip zMap to match with flipped non-indexed bitmaps
|
||||
zdrv::FlipZMapHorizontally(*zMap);
|
||||
|
||||
assertm(ZMaps[zMap->Resolution] == nullptr, "GroupData: zMap override");
|
||||
ZMaps[zMap->Resolution] = zMap;
|
||||
|
||||
auto bmp = Bitmaps[zMap->Resolution];
|
||||
if (bmp)
|
||||
{
|
||||
assertm(bmp->Width == zMap->Width && bmp->Height == zMap->Height,
|
||||
"GroupData: mismatched bitmap/zMap dimensions");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DatFile::~DatFile()
|
||||
{
|
||||
for (auto group : Groups)
|
||||
{
|
||||
if (!group)
|
||||
continue;
|
||||
|
||||
for (const auto entry : group->GetEntries())
|
||||
{
|
||||
delete entry;
|
||||
}
|
||||
delete group;
|
||||
}
|
||||
}
|
||||
|
||||
char* DatFile::field(int groupIndex, FieldTypes targetEntryType)
|
||||
{
|
||||
assertm(targetEntryType != FieldTypes::Bitmap8bit && targetEntryType != FieldTypes::Bitmap16bit,
|
||||
"partman: Use specific get for bitmaps");
|
||||
|
||||
auto group = Groups[groupIndex];
|
||||
for (const auto entry : group->GetEntries())
|
||||
{
|
||||
if (entry->EntryType == targetEntryType)
|
||||
{
|
||||
return entry->Buffer;
|
||||
}
|
||||
if (entry->EntryType > targetEntryType)
|
||||
break;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
char* DatFile::field_nth(int groupIndex, FieldTypes targetEntryType, int skipFirstN)
|
||||
{
|
||||
assertm(targetEntryType != FieldTypes::Bitmap8bit && targetEntryType != FieldTypes::Bitmap16bit,
|
||||
"partman: Use specific get for bitmaps");
|
||||
|
||||
auto group = Groups[groupIndex];
|
||||
auto skipCount = 0;
|
||||
for (const auto entry : group->GetEntries())
|
||||
{
|
||||
if (entry->EntryType > targetEntryType)
|
||||
break;
|
||||
if (entry->EntryType == targetEntryType)
|
||||
if (skipCount++ == skipFirstN)
|
||||
return entry->Buffer;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int DatFile::field_size_nth(int groupIndex, FieldTypes targetEntryType, int skipFirstN)
|
||||
{
|
||||
auto group = Groups[groupIndex];
|
||||
auto skipCount = 0;
|
||||
for (const auto entry : group->GetEntries())
|
||||
{
|
||||
if (entry->EntryType > targetEntryType)
|
||||
return 0;
|
||||
if (entry->EntryType == targetEntryType)
|
||||
if (skipCount++ == skipFirstN)
|
||||
return entry->FieldSize;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int DatFile::field_size(int groupIndex, FieldTypes targetEntryType)
|
||||
{
|
||||
return field_size_nth(groupIndex, targetEntryType, 0);
|
||||
}
|
||||
|
||||
int DatFile::record_labeled(LPCSTR targetGroupName)
|
||||
{
|
||||
auto targetLength = strlen(targetGroupName);
|
||||
for (int groupIndex = static_cast<int>(Groups.size()) - 1; groupIndex >= 0; --groupIndex)
|
||||
{
|
||||
auto groupName = field(groupIndex, FieldTypes::GroupName);
|
||||
if (!groupName)
|
||||
continue;
|
||||
|
||||
auto index = 0u;
|
||||
for (; index < targetLength; index++)
|
||||
if (targetGroupName[index] != groupName[index])
|
||||
break;
|
||||
if (index == targetLength && !targetGroupName[index] && !groupName[index])
|
||||
return groupIndex;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* DatFile::field_labeled(LPCSTR lpString, FieldTypes fieldType)
|
||||
{
|
||||
auto groupIndex = record_labeled(lpString);
|
||||
return groupIndex < 0 ? nullptr : field(groupIndex, fieldType);
|
||||
}
|
||||
|
||||
gdrv_bitmap8* DatFile::GetBitmap(int groupIndex)
|
||||
{
|
||||
auto group = Groups[groupIndex];
|
||||
return group->GetBitmap(fullscrn::GetResolution());
|
||||
}
|
||||
|
||||
zmap_header_type* DatFile::GetZMap(int groupIndex)
|
||||
{
|
||||
auto group = Groups[groupIndex];
|
||||
return group->GetZMap(fullscrn::GetResolution());
|
||||
}
|
||||
|
||||
void DatFile::Finalize()
|
||||
{
|
||||
if (!pb::FullTiltMode)
|
||||
{
|
||||
int groupIndex = record_labeled("pbmsg_ft");
|
||||
assertm(groupIndex < 0, "DatFile: pbmsg_ft is already in .dat");
|
||||
|
||||
// Load 3DPB font into dat to simplify pipeline
|
||||
auto rcData = reinterpret_cast<MsgFont*>(ImFontAtlas::DecompressCompressedBase85Data(
|
||||
EmbeddedData::PB_MSGFT_bin_compressed_data_base85));
|
||||
AddMsgFont(rcData, "pbmsg_ft");
|
||||
IM_FREE(rcData);
|
||||
|
||||
// PINBALL2.MID is an alternative font provided in 3DPB data
|
||||
// Scaled down because it is too large for top text box
|
||||
/*auto file = pb::make_path_name("PINBALL2.MID");
|
||||
auto fileHandle = fopenu(file.c_str(), "rb");
|
||||
fseek(fileHandle, 0, SEEK_END);
|
||||
auto fileSize = static_cast<uint32_t>(ftell(fileHandle));
|
||||
auto rcData = reinterpret_cast<MsgFont*>(new uint8_t[fileSize]);
|
||||
fseek(fileHandle, 0, SEEK_SET);
|
||||
fread(rcData, 1, fileSize, fileHandle);
|
||||
fclose(fileHandle);
|
||||
auto groupId = Groups.back()->GroupId + 1u;
|
||||
AddMsgFont(rcData, "pbmsg_ft");
|
||||
delete[] rcData;
|
||||
for (auto i = groupId; i < Groups.size(); i++)
|
||||
Groups[i]->GetBitmap(0)->ScaleIndexed(0.84f, 0.84f);*/
|
||||
}
|
||||
|
||||
for (auto group : Groups)
|
||||
{
|
||||
group->FinalizeGroup();
|
||||
}
|
||||
}
|
||||
|
||||
void DatFile::AddMsgFont(MsgFont* font, const std::string& fontName)
|
||||
{
|
||||
auto groupId = Groups.back()->GroupId + 1;
|
||||
auto ptrToData = reinterpret_cast<char*>(font->Data);
|
||||
for (auto charInd = 32; charInd < 128; charInd++, groupId++)
|
||||
{
|
||||
auto curChar = reinterpret_cast<MsgFontChar*>(ptrToData);
|
||||
assertm(curChar->Width == font->CharWidths[charInd], "Score: mismatched font width");
|
||||
ptrToData += curChar->Width * font->Height + 1;
|
||||
|
||||
auto bmp = new gdrv_bitmap8(curChar->Width, font->Height, true);
|
||||
auto srcPtr = curChar->Data;
|
||||
auto dstPtr = &bmp->IndexedBmpPtr[bmp->Stride * (bmp->Height - 1)];
|
||||
for (auto y = 0; y < font->Height; ++y)
|
||||
{
|
||||
memcpy(dstPtr, srcPtr, curChar->Width);
|
||||
srcPtr += curChar->Width;
|
||||
dstPtr -= bmp->Stride;
|
||||
}
|
||||
|
||||
auto group = new GroupData(groupId);
|
||||
group->AddEntry(new EntryData(FieldTypes::Bitmap8bit, reinterpret_cast<char*>(bmp)));
|
||||
if (charInd == 32)
|
||||
{
|
||||
// First font group holds font name and gap width
|
||||
auto groupName = new char[fontName.length() + 1];
|
||||
strcpy(groupName, fontName.c_str());
|
||||
group->AddEntry(new EntryData(FieldTypes::GroupName, groupName));
|
||||
|
||||
auto gaps = new char[2];
|
||||
*reinterpret_cast<int16_t*>(gaps) = font->GapWidth;
|
||||
group->AddEntry(new EntryData(FieldTypes::ShortArray, gaps));
|
||||
}
|
||||
else
|
||||
{
|
||||
auto groupName = new char[30];
|
||||
sprintf(groupName, "char %d='%c'", charInd, charInd);
|
||||
group->AddEntry(new EntryData(FieldTypes::GroupName, groupName));
|
||||
}
|
||||
|
||||
Groups.push_back(group);
|
||||
}
|
||||
}
|
||||
128
SpaceCadetPinball/GroupData.h
Normal file
128
SpaceCadetPinball/GroupData.h
Normal file
@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
struct zmap_header_type;
|
||||
struct gdrv_bitmap8;
|
||||
|
||||
|
||||
enum class FieldTypes : int16_t
|
||||
{
|
||||
// One 16 bit signed integer
|
||||
ShortValue = 0,
|
||||
|
||||
// Sprite bitmap, 8bpp, indexed color
|
||||
Bitmap8bit = 1,
|
||||
|
||||
Unknown2 = 2,
|
||||
|
||||
// Group name, char[]. Not all groups have names.
|
||||
GroupName = 3,
|
||||
|
||||
Unknown4 = 4,
|
||||
|
||||
// Palette, contains 256 RBGA 4-byte colors.
|
||||
Palette = 5,
|
||||
|
||||
Unknown6 = 6,
|
||||
|
||||
Unknown7 = 7,
|
||||
|
||||
Unknown8 = 8,
|
||||
|
||||
// String, char[]
|
||||
String = 9,
|
||||
|
||||
// Array of 16 bit signed integers
|
||||
ShortArray = 10,
|
||||
|
||||
// Array of 32 bit floats
|
||||
FloatArray = 11,
|
||||
|
||||
// Sprite depth map, 16bpp, unsigned
|
||||
Bitmap16bit = 12,
|
||||
};
|
||||
|
||||
struct EntryData
|
||||
{
|
||||
EntryData() = default;
|
||||
|
||||
EntryData(FieldTypes entryType, char* buffer): EntryType(entryType), FieldSize(-1), Buffer(buffer)
|
||||
{
|
||||
}
|
||||
|
||||
~EntryData();
|
||||
FieldTypes EntryType{};
|
||||
int FieldSize{};
|
||||
char* Buffer{};
|
||||
};
|
||||
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct MsgFontChar
|
||||
{
|
||||
uint8_t Width;
|
||||
char Data[1];
|
||||
};
|
||||
|
||||
struct MsgFont
|
||||
{
|
||||
int16_t GapWidth;
|
||||
int16_t Unknown1;
|
||||
int16_t Height;
|
||||
uint8_t CharWidths[128];
|
||||
MsgFontChar Data[1];
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(MsgFont) == 136, "Wrong size of MsgFont");
|
||||
|
||||
|
||||
class GroupData
|
||||
{
|
||||
public:
|
||||
int GroupId;
|
||||
std::string GroupName;
|
||||
|
||||
GroupData(int groupId);
|
||||
void AddEntry(EntryData* entry);
|
||||
void FinalizeGroup();
|
||||
const std::vector<EntryData*>& GetEntries() const { return Entries; }
|
||||
const EntryData* GetEntry(size_t index) const { return Entries[index]; }
|
||||
size_t EntryCount() const { return Entries.size(); }
|
||||
void ReserveEntries(size_t count) { Entries.reserve(count); }
|
||||
gdrv_bitmap8* GetBitmap(int resolution) const;
|
||||
zmap_header_type* GetZMap(int resolution) const;
|
||||
|
||||
private:
|
||||
std::vector<EntryData*> Entries;
|
||||
gdrv_bitmap8* Bitmaps[3]{};
|
||||
zmap_header_type* ZMaps[3]{};
|
||||
bool NeedsSort = false;
|
||||
|
||||
static void SplitSplicedBitmap(const gdrv_bitmap8& srcBmp, gdrv_bitmap8& bmp, zmap_header_type& zMap);
|
||||
|
||||
void SetBitmap(gdrv_bitmap8* bmp);
|
||||
void SetZMap(zmap_header_type* zMap);
|
||||
};
|
||||
|
||||
|
||||
class DatFile
|
||||
{
|
||||
public:
|
||||
std::string AppName;
|
||||
std::string Description;
|
||||
std::vector<GroupData*> Groups;
|
||||
|
||||
~DatFile();
|
||||
char* field_nth(int groupIndex, FieldTypes targetEntryType, int skipFirstN);
|
||||
char* field(int groupIndex, FieldTypes entryType);
|
||||
int field_size_nth(int groupIndex, FieldTypes targetEntryType, int skipFirstN);
|
||||
int field_size(int groupIndex, FieldTypes targetEntryType);
|
||||
int record_labeled(LPCSTR targetGroupName);
|
||||
char* field_labeled(LPCSTR lpString, FieldTypes fieldType);
|
||||
gdrv_bitmap8* GetBitmap(int groupIndex);
|
||||
zmap_header_type* GetZMap(int groupIndex);
|
||||
void Finalize();
|
||||
|
||||
private:
|
||||
void AddMsgFont(MsgFont* font, const std::string& fontName);
|
||||
};
|
||||
BIN
SpaceCadetPinball/Icon_1.ico
Normal file
BIN
SpaceCadetPinball/Icon_1.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
BIN
SpaceCadetPinball/Icon_128x128.png
Normal file
BIN
SpaceCadetPinball/Icon_128x128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
SpaceCadetPinball/Icon_16x16.png
Normal file
BIN
SpaceCadetPinball/Icon_16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 972 B |
BIN
SpaceCadetPinball/Icon_192x192.png
Normal file
BIN
SpaceCadetPinball/Icon_192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
SpaceCadetPinball/Icon_32x32.png
Normal file
BIN
SpaceCadetPinball/Icon_32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
SpaceCadetPinball/Icon_48x48.png
Normal file
BIN
SpaceCadetPinball/Icon_48x48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
16
SpaceCadetPinball/NatvisFile.natvis
Normal file
16
SpaceCadetPinball/NatvisFile.natvis
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
|
||||
<Type Name="ColorRgba">
|
||||
<DisplayString>{{ Color:{Color} }}</DisplayString>
|
||||
<Expand>
|
||||
<Item Name="[Alpha]" ExcludeView="simple">Color>>alphaOffset & 255</Item>
|
||||
<Item Name="[Red]" ExcludeView="simple">Color>>redOffset & 255</Item>
|
||||
<Item Name="[Green]" ExcludeView="simple">Color>>greenOffset & 255</Item>
|
||||
<Item Name="[Blue]" ExcludeView="simple">Color>>blueOffset & 255</Item>
|
||||
<Item Name="[Color]" ExcludeView="simple">Color</Item>
|
||||
</Expand>
|
||||
</Type>
|
||||
<Type Name="vector3">
|
||||
<DisplayString>{{ X={X} Y={Y} Z={Z} }}</DisplayString>
|
||||
</Type>
|
||||
</AutoVisualizer>
|
||||
BIN
SpaceCadetPinball/PB_MSGFT.bin
Normal file
BIN
SpaceCadetPinball/PB_MSGFT.bin
Normal file
Binary file not shown.
154
SpaceCadetPinball/Sound.cpp
Normal file
154
SpaceCadetPinball/Sound.cpp
Normal file
@ -0,0 +1,154 @@
|
||||
#include "pch.h"
|
||||
#include "options.h"
|
||||
#include "Sound.h"
|
||||
#include "maths.h"
|
||||
|
||||
int Sound::num_channels;
|
||||
bool Sound::enabled_flag = false;
|
||||
std::vector<ChannelInfo> Sound::Channels{};
|
||||
int Sound::Volume = MIX_MAX_VOLUME;
|
||||
bool Sound::MixOpen = false;
|
||||
|
||||
void Sound::Init(bool mixOpen, int channels, bool enableFlag, int volume)
|
||||
{
|
||||
MixOpen = mixOpen;
|
||||
Volume = volume;
|
||||
SetChannels(channels);
|
||||
Enable(enableFlag);
|
||||
}
|
||||
|
||||
void Sound::Enable(bool enableFlag)
|
||||
{
|
||||
enabled_flag = enableFlag;
|
||||
if (MixOpen && !enableFlag)
|
||||
Mix_HaltChannel(-1);
|
||||
}
|
||||
|
||||
void Sound::Activate()
|
||||
{
|
||||
if (MixOpen)
|
||||
Mix_Resume(-1);
|
||||
}
|
||||
|
||||
void Sound::Deactivate()
|
||||
{
|
||||
if (MixOpen)
|
||||
Mix_Pause(-1);
|
||||
}
|
||||
|
||||
void Sound::Close()
|
||||
{
|
||||
Enable(false);
|
||||
Channels.clear();
|
||||
}
|
||||
|
||||
void Sound::PlaySound(Mix_Chunk* wavePtr, int time, TPinballComponent* soundSource, const char* info)
|
||||
{
|
||||
if (MixOpen && wavePtr && enabled_flag)
|
||||
{
|
||||
if (Mix_Playing(-1) == num_channels)
|
||||
{
|
||||
auto cmp = [](const ChannelInfo& a, const ChannelInfo& b)
|
||||
{
|
||||
return a.TimeStamp < b.TimeStamp;
|
||||
};
|
||||
auto min = std::min_element(Channels.begin(), Channels.end(), cmp);
|
||||
auto oldestChannel = static_cast<int>(std::distance(Channels.begin(), min));
|
||||
Mix_HaltChannel(oldestChannel);
|
||||
}
|
||||
|
||||
auto channel = Mix_PlayChannel(-1, wavePtr, 0);
|
||||
if (channel != -1)
|
||||
{
|
||||
Channels[channel].TimeStamp = time;
|
||||
if (options::Options.SoundStereo)
|
||||
{
|
||||
// Positional audio uses collision grid 2D coordinates normalized to [0, 1]
|
||||
// Point (0, 0) is bottom left table corner; point (1, 1) is top right table corner.
|
||||
// Z is defined as: 0 at table level, positive axis goes up from table surface.
|
||||
|
||||
// Get the source sound position.
|
||||
// Sound without position are assumed to be at the center top of the table.
|
||||
vector3 soundPos{};
|
||||
if (soundSource)
|
||||
{
|
||||
auto soundPos2D = soundSource->get_coordinates();
|
||||
soundPos = {soundPos2D.X, soundPos2D.Y, 0.0f};
|
||||
}
|
||||
else
|
||||
{
|
||||
soundPos = {0.5f, 1.0f, 0.0f};
|
||||
}
|
||||
Channels[channel].Position = soundPos;
|
||||
|
||||
// Listener is positioned at the bottom center of the table,
|
||||
// at 0.5 height, so roughly a table half - length.
|
||||
vector3 playerPos = {0.5f, 0.0f, 0.5f};
|
||||
auto soundDir = maths::vector_sub(soundPos, playerPos);
|
||||
|
||||
// Find sound angle from positive Y axis in clockwise direction with atan2
|
||||
// Remap atan2 output from (-Pi, Pi] to [0, 2 * Pi)
|
||||
auto angle = fmodf(atan2(soundDir.X, soundDir.Y) + Pi * 2, Pi * 2);
|
||||
auto angleDeg = angle * 180.0f / Pi;
|
||||
auto angleSdl = static_cast<Sint16>(angleDeg);
|
||||
|
||||
// Distance from listener to the sound position is roughly in the [0, ~1.22] range.
|
||||
// Remap to [0, 122] by multiplying by 100 and cast to an integer.
|
||||
auto distance = static_cast<Uint8>(100.0f * maths::magnitude(soundDir));
|
||||
|
||||
// Mix_SetPosition expects an angle in (Sint16)degrees, where
|
||||
// angle 0 is due north, and rotates clockwise as the value increases.
|
||||
// Mix_SetPosition expects a (Uint8)distance from 0 (near) to 255 (far).
|
||||
Mix_SetPosition(channel, angleSdl, distance);
|
||||
|
||||
// Output position of each sound emitted so we can verify
|
||||
// the sanity of the implementation.
|
||||
/*printf("X: %3.3f Y: %3.3f Angle: %3.3f Distance: %3d, Object: %s\n",
|
||||
soundPos.X,
|
||||
soundPos.Y,
|
||||
angleDeg,
|
||||
distance,
|
||||
info
|
||||
);*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Mix_Chunk* Sound::LoadWaveFile(const std::string& lpName)
|
||||
{
|
||||
if (!MixOpen)
|
||||
return nullptr;
|
||||
|
||||
auto wavFile = fopenu(lpName.c_str(), "r");
|
||||
if (!wavFile)
|
||||
return nullptr;
|
||||
fclose(wavFile);
|
||||
|
||||
return Mix_LoadWAV(lpName.c_str());
|
||||
}
|
||||
|
||||
void Sound::FreeSound(Mix_Chunk* wave)
|
||||
{
|
||||
if (MixOpen && wave)
|
||||
Mix_FreeChunk(wave);
|
||||
}
|
||||
|
||||
void Sound::SetChannels(int channels)
|
||||
{
|
||||
if (channels <= 0)
|
||||
channels = 8;
|
||||
|
||||
num_channels = channels;
|
||||
Channels.resize(num_channels);
|
||||
if (MixOpen)
|
||||
Mix_AllocateChannels(num_channels);
|
||||
SetVolume(Volume);
|
||||
}
|
||||
|
||||
void Sound::SetVolume(int volume)
|
||||
{
|
||||
Volume = volume;
|
||||
if (MixOpen)
|
||||
Mix_Volume(-1, volume);
|
||||
}
|
||||
31
SpaceCadetPinball/Sound.h
Normal file
31
SpaceCadetPinball/Sound.h
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "maths.h"
|
||||
#include "TPinballComponent.h"
|
||||
|
||||
struct ChannelInfo
|
||||
{
|
||||
int TimeStamp;
|
||||
vector2 Position;
|
||||
};
|
||||
|
||||
class Sound
|
||||
{
|
||||
public:
|
||||
static std::vector<ChannelInfo> Channels;
|
||||
|
||||
static void Init(bool mixOpen, int channels, bool enableFlag, int volume);
|
||||
static void Enable(bool enableFlag);
|
||||
static void Activate();
|
||||
static void Deactivate();
|
||||
static void Close();
|
||||
static void PlaySound(Mix_Chunk* wavePtr, int time, TPinballComponent *soundSource, const char* info);
|
||||
static Mix_Chunk* LoadWaveFile(const std::string& lpName);
|
||||
static void FreeSound(Mix_Chunk* wave);
|
||||
static void SetChannels(int channels);
|
||||
static void SetVolume(int volume);
|
||||
private:
|
||||
static int num_channels;
|
||||
static bool enabled_flag;
|
||||
static int Volume;
|
||||
static bool MixOpen;
|
||||
};
|
||||
55
SpaceCadetPinball/SpaceCadetPinball.cpp
Normal file
55
SpaceCadetPinball/SpaceCadetPinball.cpp
Normal file
@ -0,0 +1,55 @@
|
||||
#include "pch.h"
|
||||
#include "winmain.h"
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
std::string cmdLine;
|
||||
for (int i = 1; i < argc; i++)
|
||||
{
|
||||
if (i > 1)
|
||||
cmdLine += " ";
|
||||
cmdLine += argv[i];
|
||||
}
|
||||
|
||||
return winmain::WinMain(cmdLine.c_str());
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
#include <windows.h>
|
||||
|
||||
// Windows subsystem main
|
||||
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
|
||||
{
|
||||
return winmain::WinMain(lpCmdLine);
|
||||
}
|
||||
|
||||
// fopen to _wfopen adapter, for UTF-8 paths
|
||||
FILE* fopenu(const char* path, const char* opt)
|
||||
{
|
||||
wchar_t* wideArgs[2]{};
|
||||
for (auto& arg : wideArgs)
|
||||
{
|
||||
auto src = wideArgs[0] ? opt : path;
|
||||
auto length = MultiByteToWideChar(CP_UTF8, 0, src, -1, nullptr, 0);
|
||||
arg = new wchar_t[length];
|
||||
MultiByteToWideChar(CP_UTF8, 0, src, -1, arg, length);
|
||||
}
|
||||
|
||||
auto fileHandle = _wfopen(wideArgs[0], wideArgs[1]);
|
||||
for (auto arg : wideArgs)
|
||||
delete[] arg;
|
||||
|
||||
return fileHandle;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Run program: Ctrl + F5 or Debug > Start Without Debugging menu
|
||||
// Debug program: F5 or Debug > Start Debugging menu
|
||||
|
||||
// Tips for Getting Started:
|
||||
// 1. Use the Solution Explorer window to add/manage files
|
||||
// 2. Use the Team Explorer window to connect to source control
|
||||
// 3. Use the Output window to see build output and other messages
|
||||
// 4. Use the Error List window to view errors
|
||||
// 5. Go to Project > Add New Item to create new code files, or Project > Add Existing Item to add existing code files to the project
|
||||
// 6. In the future, to open this project again, go to File > Open > Project and select the .sln file
|
||||
1
SpaceCadetPinball/SpaceCadetPinball.rc
Normal file
1
SpaceCadetPinball/SpaceCadetPinball.rc
Normal file
@ -0,0 +1 @@
|
||||
ICON_1 ICON "Icon_1.ico"
|
||||
197
SpaceCadetPinball/TBall.cpp
Normal file
197
SpaceCadetPinball/TBall.cpp
Normal file
@ -0,0 +1,197 @@
|
||||
#include "pch.h"
|
||||
#include "TBall.h"
|
||||
|
||||
|
||||
#include "fullscrn.h"
|
||||
#include "loader.h"
|
||||
#include "maths.h"
|
||||
#include "pb.h"
|
||||
#include "proj.h"
|
||||
#include "render.h"
|
||||
#include "TPinballTable.h"
|
||||
#include "TTableLayer.h"
|
||||
|
||||
TBall::TBall(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false),
|
||||
TEdgeSegment(this, &ActiveFlag, 0)
|
||||
{
|
||||
visualStruct visual{};
|
||||
char ballGroupName[10]{"ball"};
|
||||
|
||||
RayMaxDistance = 0.0;
|
||||
ActiveFlag = 1;
|
||||
CollisionComp = nullptr;
|
||||
EdgeCollisionCount = 0;
|
||||
TimeDelta = 0.0;
|
||||
CollisionFlag = 0;
|
||||
Speed = 0.0;
|
||||
Direction.Y = 0.0;
|
||||
Direction.X = 0.0;
|
||||
ListBitmap = new std::vector<SpriteData>();
|
||||
|
||||
if (groupIndex == -1)
|
||||
{
|
||||
HasGroupFlag = false;
|
||||
Position = {0, 0, 0};
|
||||
CollisionMask = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
HasGroupFlag = true;
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
CollisionMask = visual.CollisionGroup;
|
||||
auto floatArr = loader::query_float_attribute(groupIndex, 0, 408);
|
||||
Position = {floatArr[0], floatArr[1], floatArr[3]};
|
||||
}
|
||||
|
||||
/*Full tilt: ball is ballN, where N[0,2] resolution*/
|
||||
groupIndex = loader::query_handle(ballGroupName);
|
||||
if (groupIndex < 0)
|
||||
{
|
||||
ballGroupName[4] = '0' + fullscrn::GetResolution();
|
||||
groupIndex = loader::query_handle(ballGroupName);
|
||||
}
|
||||
|
||||
Radius = *loader::query_float_attribute(groupIndex, 0, 500);
|
||||
|
||||
auto visualCount = loader::query_visual_states(groupIndex);
|
||||
for (auto index = 0; index < visualCount; ++index)
|
||||
{
|
||||
loader::query_visual(groupIndex, index, &visual);
|
||||
ListBitmap->push_back(visual.Bitmap);
|
||||
auto visVec = reinterpret_cast<vector3*>(loader::query_float_attribute(groupIndex, index, 501));
|
||||
auto zDepth = proj::z_distance(*visVec);
|
||||
VisualZArray[index] = zDepth;
|
||||
}
|
||||
RenderSprite = new render_sprite(VisualTypes::Ball, nullptr, nullptr, 0, 0, nullptr);
|
||||
PinballTable->CollisionCompOffset = Radius;
|
||||
Position.Z = Radius;
|
||||
GroupIndex = groupIndex;
|
||||
}
|
||||
|
||||
void TBall::Repaint()
|
||||
{
|
||||
if (CollisionFlag)
|
||||
{
|
||||
Position.Z =
|
||||
CollisionOffset.X * Position.X +
|
||||
CollisionOffset.Y * Position.Y +
|
||||
Radius + CollisionOffset.Z;
|
||||
}
|
||||
|
||||
auto pos2D = proj::xform_to_2d(Position);
|
||||
auto zDepth = proj::z_distance(Position);
|
||||
|
||||
auto index = 0u;
|
||||
for (; index < ListBitmap->size() - 1; ++index)
|
||||
{
|
||||
if (VisualZArray[index] <= zDepth) break;
|
||||
}
|
||||
|
||||
SpriteSetBall(index, pos2D, zDepth);
|
||||
}
|
||||
|
||||
void TBall::not_again(TEdgeSegment* edge)
|
||||
{
|
||||
if (EdgeCollisionCount < 16)
|
||||
{
|
||||
Collisions[EdgeCollisionCount] = edge;
|
||||
++EdgeCollisionCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
Collisions[i] = Collisions[i + 8];
|
||||
Collisions[8] = edge;
|
||||
EdgeCollisionCount = 9;
|
||||
}
|
||||
EdgeCollisionResetFlag = true;
|
||||
}
|
||||
|
||||
bool TBall::already_hit(const TEdgeSegment& edge) const
|
||||
{
|
||||
for (int i = 0; i < EdgeCollisionCount; i++)
|
||||
{
|
||||
if (Collisions[i] == &edge)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int TBall::Message(MessageCode code, float value)
|
||||
{
|
||||
if (code == MessageCode::Reset)
|
||||
{
|
||||
SpriteSetBall(-1, {0, 0}, 0.0f);
|
||||
Position.X = 0.0;
|
||||
CollisionComp = nullptr;
|
||||
Position.Y = 0.0;
|
||||
ActiveFlag = 0;
|
||||
CollisionFlag = 0;
|
||||
CollisionMask = 1;
|
||||
Direction.Y = 0.0;
|
||||
Position.Z = Radius;
|
||||
Direction.X = 0.0;
|
||||
Speed = 0.0;
|
||||
RayMaxDistance = 0.0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TBall::throw_ball(vector3* direction, float angleMult, float speedMult1, float speedMult2)
|
||||
{
|
||||
CollisionComp = nullptr;
|
||||
Direction = *direction;
|
||||
float rnd = RandFloat();
|
||||
float angle = (1.0f - (rnd + rnd)) * angleMult;
|
||||
maths::RotateVector(Direction, angle);
|
||||
rnd = RandFloat();
|
||||
Speed = (1.0f - (rnd + rnd)) * (speedMult1 * speedMult2) + speedMult1;
|
||||
}
|
||||
|
||||
void TBall::EdgeCollision(TBall* ball, float distance)
|
||||
{
|
||||
ball->CollisionDisabledFlag = true;
|
||||
ball->Position.X += ball->Direction.X * distance;
|
||||
ball->Position.Y += ball->Direction.Y * distance;
|
||||
ball->Direction.X *= ball->Speed;
|
||||
ball->Direction.Y *= ball->Speed;
|
||||
Direction.X *= Speed;
|
||||
Direction.Y *= Speed;
|
||||
|
||||
// AB - vector from ball to this, BA - from this to ball; collision direction
|
||||
vector2 AB{ball->Position.X - Position.X, ball->Position.Y - Position.Y};
|
||||
maths::normalize_2d(AB);
|
||||
vector2 BA{-AB.X, -AB.Y};
|
||||
|
||||
// Projection = difference between ball directions and collision direction
|
||||
auto projAB = -maths::DotProduct(ball->Direction, AB);
|
||||
auto projBA = -maths::DotProduct(Direction, BA);
|
||||
vector2 delta{AB.X * projAB - BA.X * projBA, AB.Y * projAB - BA.Y * projBA};
|
||||
|
||||
ball->Direction.X += delta.X;
|
||||
ball->Direction.Y += delta.Y;
|
||||
ball->Speed = maths::normalize_2d(ball->Direction);
|
||||
Direction.X -= delta.X;
|
||||
Direction.Y -= delta.Y;
|
||||
Speed = maths::normalize_2d(Direction);
|
||||
}
|
||||
|
||||
float TBall::FindCollisionDistance(const ray_type& ray)
|
||||
{
|
||||
// Original inherits TCircle and aliases position.
|
||||
const circle_type ballCircle{{Position.X, Position.Y}, Radius * Radius * 4.0f};
|
||||
return maths::ray_intersect_circle(ray, ballCircle);
|
||||
}
|
||||
|
||||
vector2 TBall::get_coordinates()
|
||||
{
|
||||
return TTableLayer::edge_manager->NormalizeBox(Position);
|
||||
}
|
||||
|
||||
void TBall::Disable()
|
||||
{
|
||||
ActiveFlag = false;
|
||||
CollisionDisabledFlag = true;
|
||||
SpriteSet(-1);
|
||||
}
|
||||
41
SpaceCadetPinball/TBall.h
Normal file
41
SpaceCadetPinball/TBall.h
Normal file
@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include "maths.h"
|
||||
#include "TCollisionComponent.h"
|
||||
#include "TEdgeSegment.h"
|
||||
|
||||
class TBall : public TCollisionComponent, public TEdgeSegment
|
||||
{
|
||||
public :
|
||||
TBall(TPinballTable* table, int groupIndex);
|
||||
void Repaint();
|
||||
void not_again(TEdgeSegment* edge);
|
||||
bool already_hit(const TEdgeSegment& edge) const;
|
||||
int Message(MessageCode code, float value) override;
|
||||
vector2 get_coordinates() override;
|
||||
void Disable();
|
||||
void throw_ball(vector3* direction, float angleMult, float speedMult1, float speedMult2);
|
||||
void place_in_grid(RectF* aabb) override {}
|
||||
void EdgeCollision(TBall* ball, float distance) override;
|
||||
float FindCollisionDistance(const ray_type& ray) override;
|
||||
|
||||
vector3 Position{};
|
||||
vector3 PrevPosition{};
|
||||
vector3 Direction{};
|
||||
float Speed;
|
||||
float RayMaxDistance;
|
||||
float TimeDelta;
|
||||
vector2 RampFieldForce{};
|
||||
TCollisionComponent* CollisionComp;
|
||||
int CollisionMask;
|
||||
TEdgeSegment* Collisions[16]{};
|
||||
int EdgeCollisionCount;
|
||||
bool EdgeCollisionResetFlag{};
|
||||
vector3 CollisionOffset{};
|
||||
int CollisionFlag;
|
||||
float Radius;
|
||||
bool HasGroupFlag;
|
||||
int StuckCounter = 0;
|
||||
int LastActiveTime{};
|
||||
float VisualZArray[50]{};
|
||||
bool CollisionDisabledFlag{};
|
||||
};
|
||||
72
SpaceCadetPinball/TBlocker.cpp
Normal file
72
SpaceCadetPinball/TBlocker.cpp
Normal file
@ -0,0 +1,72 @@
|
||||
#include "pch.h"
|
||||
#include "TBlocker.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "render.h"
|
||||
#include "timer.h"
|
||||
|
||||
TBlocker::TBlocker(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
|
||||
{
|
||||
visualStruct visual{};
|
||||
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
SoundIndex4 = visual.SoundIndex4;
|
||||
SoundIndex3 = visual.SoundIndex3;
|
||||
InitialDuration = 55;
|
||||
ExtendedDuration = 5;
|
||||
Threshold = 1000000000.0f;
|
||||
Timer = 0;
|
||||
MessageField = 0;
|
||||
ActiveFlag = 0;
|
||||
SpriteSet(-1);
|
||||
}
|
||||
|
||||
int TBlocker::Message(MessageCode code, float value)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::SetTiltLock:
|
||||
case MessageCode::PlayerChanged:
|
||||
case MessageCode::Reset:
|
||||
case MessageCode::TBlockerDisable:
|
||||
if (Timer)
|
||||
{
|
||||
timer::kill(Timer);
|
||||
Timer = 0;
|
||||
}
|
||||
MessageField = 0;
|
||||
ActiveFlag = 0;
|
||||
SpriteSet(-1);
|
||||
if (code == MessageCode::TBlockerDisable)
|
||||
loader::play_sound(SoundIndex3, this, "TBlocker1");
|
||||
break;
|
||||
case MessageCode::TBlockerEnable:
|
||||
ActiveFlag = 1;
|
||||
loader::play_sound(SoundIndex4, this, "TBlocker2");
|
||||
SpriteSet(0);
|
||||
if (Timer)
|
||||
timer::kill(Timer);
|
||||
Timer = 0;
|
||||
if (value >= 0)
|
||||
Timer = timer::set(value, this, TimerExpired);
|
||||
break;
|
||||
case MessageCode::TBlockerRestartTimeout:
|
||||
if (Timer)
|
||||
timer::kill(Timer);
|
||||
Timer = timer::set(std::max(value, 0.0f), this, TimerExpired);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TBlocker::TimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto blocker = static_cast<TBlocker*>(caller);
|
||||
blocker->Timer = 0;
|
||||
control::handler(MessageCode::ControlTimerExpired, blocker);
|
||||
}
|
||||
18
SpaceCadetPinball/TBlocker.h
Normal file
18
SpaceCadetPinball/TBlocker.h
Normal file
@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
class TBlocker :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TBlocker(TPinballTable* table, int groupIndex);
|
||||
int Message(MessageCode code, float value) override;
|
||||
|
||||
static void TimerExpired(int timerId, void* caller);
|
||||
|
||||
int InitialDuration;
|
||||
int ExtendedDuration;
|
||||
int Timer;
|
||||
int SoundIndex4;
|
||||
int SoundIndex3;
|
||||
};
|
||||
123
SpaceCadetPinball/TBumper.cpp
Normal file
123
SpaceCadetPinball/TBumper.cpp
Normal file
@ -0,0 +1,123 @@
|
||||
#include "pch.h"
|
||||
#include "TBumper.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "render.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TBumper::TBumper(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
|
||||
{
|
||||
visualStruct visual{};
|
||||
|
||||
BmpIndex = 0;
|
||||
Timer = 0;
|
||||
TimerTime = *loader::query_float_attribute(groupIndex, 0, 407);
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
SoundIndex4 = visual.SoundIndex4;
|
||||
SoundIndex3 = visual.SoundIndex3;
|
||||
OriginalThreshold = Threshold;
|
||||
}
|
||||
|
||||
int TBumper::Message(MessageCode code, float value)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::TBumperSetBmpIndex:
|
||||
{
|
||||
auto nextBmp = static_cast<int>(floor(value));
|
||||
auto maxBmp = static_cast<int>(ListBitmap->size()) - 1;
|
||||
if (2 * nextBmp > maxBmp)
|
||||
nextBmp = maxBmp / 2;
|
||||
if (nextBmp < 0)
|
||||
nextBmp = 0;
|
||||
if (nextBmp != BmpIndex)
|
||||
{
|
||||
if (nextBmp >= BmpIndex)
|
||||
loader::play_sound(SoundIndex4, this, "TBumper1");
|
||||
if (nextBmp < BmpIndex)
|
||||
loader::play_sound(SoundIndex3, this, "TBumper2");
|
||||
BmpIndex = nextBmp;
|
||||
Fire();
|
||||
control::handler(MessageCode::TBumperSetBmpIndex, this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCode::TBumperIncBmpIndex:
|
||||
{
|
||||
auto nextBmp = BmpIndex + 1;
|
||||
auto maxBmp = static_cast<int>(ListBitmap->size()) - 1;
|
||||
if (2 * nextBmp > maxBmp)
|
||||
nextBmp = maxBmp / 2;
|
||||
TBumper::Message(MessageCode::TBumperSetBmpIndex, static_cast<float>(nextBmp));
|
||||
break;
|
||||
}
|
||||
case MessageCode::TBumperDecBmpIndex:
|
||||
{
|
||||
auto nextBmp = BmpIndex - 1;
|
||||
if (nextBmp < 0)
|
||||
nextBmp = 0;
|
||||
TBumper::Message(MessageCode::TBumperSetBmpIndex, static_cast<float>(nextBmp));
|
||||
break;
|
||||
}
|
||||
case MessageCode::PlayerChanged:
|
||||
{
|
||||
auto playerPtr = &PlayerData[PinballTable->CurrentPlayer];
|
||||
playerPtr->BmpIndex = BmpIndex;
|
||||
playerPtr->MessageField = MessageField;
|
||||
|
||||
playerPtr = &PlayerData[static_cast<int>(floor(value))];
|
||||
BmpIndex = playerPtr->BmpIndex;
|
||||
MessageField = playerPtr->MessageField;
|
||||
TBumper::Message(MessageCode::TBumperSetBmpIndex, static_cast<float>(BmpIndex));
|
||||
break;
|
||||
}
|
||||
case MessageCode::Reset:
|
||||
{
|
||||
if (Timer)
|
||||
{
|
||||
timer::kill(Timer);
|
||||
TimerExpired(Timer, this);
|
||||
}
|
||||
BmpIndex = 0;
|
||||
MessageField = 0;
|
||||
for (auto& playerPtr : PlayerData)
|
||||
{
|
||||
playerPtr.BmpIndex = 0;
|
||||
playerPtr.MessageField = 0;
|
||||
}
|
||||
TimerExpired(0, this);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TBumper::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
|
||||
{
|
||||
if (DefaultCollision(ball, nextPosition, direction))
|
||||
{
|
||||
Fire();
|
||||
control::handler(MessageCode::ControlCollision, this);
|
||||
}
|
||||
}
|
||||
|
||||
void TBumper::TimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto bump = static_cast<TBumper*>(caller);
|
||||
bump->SpriteSet(bump->BmpIndex * 2);
|
||||
bump->Timer = 0;
|
||||
bump->Threshold = bump->OriginalThreshold;
|
||||
}
|
||||
|
||||
void TBumper::Fire()
|
||||
{
|
||||
SpriteSet(2 * BmpIndex + 1);
|
||||
Timer = timer::set(TimerTime, this, TimerExpired);
|
||||
Threshold = 1000000000.0;
|
||||
}
|
||||
30
SpaceCadetPinball/TBumper.h
Normal file
30
SpaceCadetPinball/TBumper.h
Normal file
@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
struct TBumper_player_backup
|
||||
{
|
||||
int MessageField;
|
||||
int BmpIndex;
|
||||
};
|
||||
|
||||
class TBumper :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TBumper(TPinballTable* table, int groupIndex);
|
||||
~TBumper() override = default;
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
void Fire();
|
||||
|
||||
static void TimerExpired(int timerId, void* caller);
|
||||
|
||||
int BmpIndex;
|
||||
int Timer;
|
||||
float TimerTime;
|
||||
float OriginalThreshold;
|
||||
int SoundIndex4;
|
||||
int SoundIndex3;
|
||||
TBumper_player_backup PlayerData[4]{};
|
||||
};
|
||||
44
SpaceCadetPinball/TCircle.cpp
Normal file
44
SpaceCadetPinball/TCircle.cpp
Normal file
@ -0,0 +1,44 @@
|
||||
#include "pch.h"
|
||||
#include "TCircle.h"
|
||||
|
||||
#include "TBall.h"
|
||||
#include "TCollisionComponent.h"
|
||||
#include "TTableLayer.h"
|
||||
|
||||
TCircle::TCircle(TCollisionComponent* collComp, char* activeFlag, unsigned collisionGroup, vector2* center,
|
||||
float radius): TEdgeSegment(collComp, activeFlag, collisionGroup)
|
||||
{
|
||||
Circle.RadiusSq = radius * radius;
|
||||
Circle.Center = *center;
|
||||
}
|
||||
|
||||
float TCircle::FindCollisionDistance(const ray_type& ray)
|
||||
{
|
||||
return maths::ray_intersect_circle(ray, Circle);
|
||||
}
|
||||
|
||||
void TCircle::EdgeCollision(TBall* ball, float distance)
|
||||
{
|
||||
vector2 direction{}, nextPosition{};
|
||||
|
||||
nextPosition.X = distance * ball->Direction.X + ball->Position.X;
|
||||
nextPosition.Y = distance * ball->Direction.Y + ball->Position.Y;
|
||||
direction.X = nextPosition.X - Circle.Center.X;
|
||||
direction.Y = nextPosition.Y - Circle.Center.Y;
|
||||
maths::normalize_2d(direction);
|
||||
CollisionComponent->Collision(ball, &nextPosition, &direction, distance, this);
|
||||
}
|
||||
|
||||
void TCircle::place_in_grid(RectF* aabb)
|
||||
{
|
||||
if(aabb)
|
||||
{
|
||||
const auto radius = sqrt(Circle.RadiusSq);
|
||||
aabb->Merge({
|
||||
Circle.Center.X + radius, Circle.Center.Y + radius,
|
||||
Circle.Center.X - radius, Circle.Center.Y - radius
|
||||
});
|
||||
}
|
||||
|
||||
TTableLayer::edges_insert_circle(&Circle, this, nullptr);
|
||||
}
|
||||
16
SpaceCadetPinball/TCircle.h
Normal file
16
SpaceCadetPinball/TCircle.h
Normal file
@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include "maths.h"
|
||||
#include "TEdgeSegment.h"
|
||||
|
||||
class TCircle :
|
||||
public TEdgeSegment
|
||||
{
|
||||
public:
|
||||
circle_type Circle{};
|
||||
|
||||
TCircle(TCollisionComponent* collComp, char* activeFlag, unsigned int collisionGroup, vector2* center,
|
||||
float radius);
|
||||
float FindCollisionDistance(const ray_type& ray) override;
|
||||
void EdgeCollision(TBall* ball, float distance) override;
|
||||
void place_in_grid(RectF* aabb) override;
|
||||
};
|
||||
103
SpaceCadetPinball/TCollisionComponent.cpp
Normal file
103
SpaceCadetPinball/TCollisionComponent.cpp
Normal file
@ -0,0 +1,103 @@
|
||||
#include "pch.h"
|
||||
#include "TCollisionComponent.h"
|
||||
#include "loader.h"
|
||||
#include "maths.h"
|
||||
#include "TEdgeSegment.h"
|
||||
#include "TPinballTable.h"
|
||||
#include "TBall.h"
|
||||
|
||||
|
||||
TCollisionComponent::TCollisionComponent(TPinballTable* table, int groupIndex, bool createWall) :
|
||||
TPinballComponent(table, groupIndex, true)
|
||||
{
|
||||
visualStruct visual{};
|
||||
|
||||
ActiveFlag = 1;
|
||||
AABB = { -10000, -10000, 10000, 10000 };
|
||||
if (GroupName != nullptr)
|
||||
UnusedBaseFlag = 1;
|
||||
if (groupIndex <= 0)
|
||||
{
|
||||
loader::default_vsi(&visual);
|
||||
}
|
||||
else
|
||||
{
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
if (createWall)
|
||||
{
|
||||
float offset = table->CollisionCompOffset;
|
||||
float* floatArr = loader::query_float_attribute(groupIndex, 0, 600);
|
||||
TEdgeSegment::install_wall(floatArr, this, &ActiveFlag, visual.CollisionGroup, offset, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Threshold = visual.Kicker.Threshold;
|
||||
Elasticity = visual.Elasticity;
|
||||
Smoothness = visual.Smoothness;
|
||||
Boost = visual.Kicker.Boost;
|
||||
HardHitSoundId = visual.Kicker.HardHitSoundId;
|
||||
SoftHitSoundId = visual.SoftHitSoundId;
|
||||
GroupIndex = groupIndex;
|
||||
}
|
||||
|
||||
TCollisionComponent::~TCollisionComponent()
|
||||
{
|
||||
for (auto edge : EdgeList)
|
||||
delete edge;
|
||||
}
|
||||
|
||||
|
||||
void TCollisionComponent::port_draw()
|
||||
{
|
||||
for (auto edge : EdgeList)
|
||||
edge->port_draw();
|
||||
}
|
||||
|
||||
bool TCollisionComponent::DefaultCollision(TBall* ball, vector2* nextPosition, vector2* direction)
|
||||
{
|
||||
if (PinballTable->TiltLockFlag)
|
||||
{
|
||||
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, 1000000000.0, 0.0);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool collision = false;
|
||||
auto projSpeed = maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, Threshold, Boost);
|
||||
if (projSpeed > Threshold)
|
||||
{
|
||||
loader::play_sound(HardHitSoundId, ball, "TCollisionComponent1");
|
||||
collision = true;
|
||||
}
|
||||
else if (projSpeed > 0.2f)
|
||||
loader::play_sound(SoftHitSoundId, ball, "TCollisionComponent2");
|
||||
|
||||
return collision;
|
||||
}
|
||||
|
||||
void TCollisionComponent::Collision(TBall* ball, vector2* nextPosition, vector2* direction,
|
||||
float distance, TEdgeSegment* edge)
|
||||
{
|
||||
if (PinballTable->TiltLockFlag)
|
||||
{
|
||||
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, 1000000000.0, 0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
auto projSpeed = maths::basic_collision(
|
||||
ball,
|
||||
nextPosition,
|
||||
direction,
|
||||
Elasticity,
|
||||
Smoothness,
|
||||
Threshold,
|
||||
Boost);
|
||||
if (projSpeed > Threshold)
|
||||
loader::play_sound(HardHitSoundId, ball, "TCollisionComponent3");
|
||||
else if (projSpeed > 0.2f)
|
||||
loader::play_sound(SoftHitSoundId, ball, "TCollisionComponent4");
|
||||
}
|
||||
|
||||
int TCollisionComponent::FieldEffect(TBall* ball, vector2* vecDst)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
28
SpaceCadetPinball/TCollisionComponent.h
Normal file
28
SpaceCadetPinball/TCollisionComponent.h
Normal file
@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include "maths.h"
|
||||
#include "TPinballComponent.h"
|
||||
|
||||
struct vector2;
|
||||
class TEdgeSegment;
|
||||
class TBall;
|
||||
|
||||
class TCollisionComponent : public TPinballComponent
|
||||
{
|
||||
public:
|
||||
std::vector<TEdgeSegment*> EdgeList;
|
||||
float Elasticity;
|
||||
float Smoothness;
|
||||
float Boost;
|
||||
float Threshold;
|
||||
int SoftHitSoundId;
|
||||
int HardHitSoundId;
|
||||
RectF AABB;
|
||||
|
||||
TCollisionComponent(TPinballTable* table, int groupIndex, bool createWall);
|
||||
~TCollisionComponent() override;
|
||||
void port_draw() override;
|
||||
virtual void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge);
|
||||
virtual int FieldEffect(TBall* ball, vector2* vecDst);
|
||||
bool DefaultCollision(TBall* ball, vector2* nextPosition, vector2* direction);
|
||||
};
|
||||
64
SpaceCadetPinball/TComponentGroup.cpp
Normal file
64
SpaceCadetPinball/TComponentGroup.cpp
Normal file
@ -0,0 +1,64 @@
|
||||
#include "pch.h"
|
||||
#include "TComponentGroup.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TComponentGroup::TComponentGroup(TPinballTable* table, int groupIndex) : TPinballComponent(table, groupIndex, false)
|
||||
{
|
||||
Timer = 0;
|
||||
if (groupIndex > 0)
|
||||
{
|
||||
int attrCount;
|
||||
auto shortArr = loader::query_iattribute(groupIndex, 1027, &attrCount);
|
||||
auto shortArrPtr = shortArr;
|
||||
for (auto index = 0; index < attrCount; ++index, ++shortArrPtr)
|
||||
{
|
||||
auto component = table->find_component(*shortArrPtr);
|
||||
if (component)
|
||||
List.push_back(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TComponentGroup::~TComponentGroup()
|
||||
{
|
||||
if (Timer)
|
||||
{
|
||||
timer::kill(Timer);
|
||||
Timer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int TComponentGroup::Message(MessageCode code, float value)
|
||||
{
|
||||
if (code == MessageCode::TComponentGroupResetNotifyTimer)
|
||||
{
|
||||
if (this->Timer)
|
||||
{
|
||||
timer::kill(this->Timer);
|
||||
this->Timer = 0;
|
||||
}
|
||||
if (value > 0.0f)
|
||||
this->Timer = timer::set(value, this, NotifyTimerExpired);
|
||||
}
|
||||
else if (code < MessageCode::Pause || (code > MessageCode::SetTiltLock &&
|
||||
code != MessageCode::PlayerChanged && code != MessageCode::GameOver))
|
||||
{
|
||||
for (auto component : List)
|
||||
{
|
||||
component->Message(code, value);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TComponentGroup::NotifyTimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto compGroup = static_cast<TComponentGroup*>(caller);
|
||||
compGroup->Timer = 0;
|
||||
control::handler(MessageCode::ControlNotifyTimerExpired, compGroup);
|
||||
}
|
||||
16
SpaceCadetPinball/TComponentGroup.h
Normal file
16
SpaceCadetPinball/TComponentGroup.h
Normal file
@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include "TPinballComponent.h"
|
||||
|
||||
|
||||
class TComponentGroup :
|
||||
public TPinballComponent
|
||||
{
|
||||
public:
|
||||
TComponentGroup(TPinballTable* table, int groupIndex);
|
||||
~TComponentGroup() override;
|
||||
int Message(MessageCode code, float value) override;
|
||||
static void NotifyTimerExpired(int timerId, void* caller);
|
||||
|
||||
std::vector<TPinballComponent*> List;
|
||||
int Timer;
|
||||
};
|
||||
203
SpaceCadetPinball/TDemo.cpp
Normal file
203
SpaceCadetPinball/TDemo.cpp
Normal file
@ -0,0 +1,203 @@
|
||||
#include "pch.h"
|
||||
#include "TDemo.h"
|
||||
|
||||
|
||||
#include "loader.h"
|
||||
#include "pb.h"
|
||||
#include "TEdgeSegment.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
#include "TBall.h"
|
||||
|
||||
TDemo::TDemo(TPinballTable* table, int groupIndex)
|
||||
: TCollisionComponent(table, groupIndex, false)
|
||||
{
|
||||
visualStruct visual{};
|
||||
|
||||
RestartGameTimer = 0;
|
||||
PlungerFlag = 0;
|
||||
FlipLeftTimer = 0;
|
||||
FlipRightTimer = 0;
|
||||
MessageField = 0;
|
||||
UnusedBaseFlag = 0;
|
||||
ActiveFlag = 0;
|
||||
FlipRightFlag = 0;
|
||||
FlipLeftFlag = 0;
|
||||
table->Demo = this;
|
||||
auto floatArr1 = loader::query_float_attribute(groupIndex, 0, 407);
|
||||
if (floatArr1)
|
||||
{
|
||||
FlipTimerTime1 = floatArr1[0];
|
||||
FlipTimerTime2 = floatArr1[1];
|
||||
UnFlipTimerTime1 = floatArr1[2];
|
||||
UnFlipTimerTime2 = floatArr1[3];
|
||||
}
|
||||
else
|
||||
{
|
||||
FlipTimerTime1 = 0.2f;
|
||||
FlipTimerTime2 = 0.1f;
|
||||
UnFlipTimerTime1 = 0.2f;
|
||||
UnFlipTimerTime2 = 0.1f;
|
||||
}
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
auto v5 = loader::query_float_attribute(groupIndex, 0, 1400);
|
||||
Edge1 = TEdgeSegment::install_wall(v5, this, &ActiveFlag, visual.CollisionGroup, 0.0, 1400);
|
||||
|
||||
auto v6 = loader::query_float_attribute(groupIndex, 0, 1401);
|
||||
TEdgeSegment::install_wall(v6, this, &ActiveFlag, visual.CollisionGroup, 0.0, 1401);
|
||||
|
||||
auto v7 = loader::query_float_attribute(groupIndex, 0, 1402);
|
||||
Edge2 = TEdgeSegment::install_wall(v7, this, &ActiveFlag, visual.CollisionGroup, 0.0, 1402);
|
||||
|
||||
auto v8 = loader::query_float_attribute(groupIndex, 0, 1403);
|
||||
TEdgeSegment::install_wall(v8, this, &ActiveFlag, visual.CollisionGroup, 0.0, 1403);
|
||||
|
||||
auto v9 = loader::query_float_attribute(groupIndex, 0, 1404);
|
||||
Edge3 = TEdgeSegment::install_wall(v9, this, &ActiveFlag, visual.CollisionGroup, table->CollisionCompOffset, 1404);
|
||||
}
|
||||
|
||||
int TDemo::Message(MessageCode code, float value)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::NewGame:
|
||||
if (RestartGameTimer)
|
||||
timer::kill(RestartGameTimer);
|
||||
RestartGameTimer = 0;
|
||||
break;
|
||||
case MessageCode::GameOver:
|
||||
if (RestartGameTimer)
|
||||
timer::kill(RestartGameTimer);
|
||||
RestartGameTimer = 0;
|
||||
if (ActiveFlag != 0)
|
||||
RestartGameTimer = timer::set(5.0, this, NewGameRestartTimer);
|
||||
break;
|
||||
case MessageCode::Reset:
|
||||
if (FlipLeftTimer)
|
||||
timer::kill(FlipLeftTimer);
|
||||
FlipLeftTimer = 0;
|
||||
if (FlipRightTimer)
|
||||
timer::kill(FlipRightTimer);
|
||||
FlipRightTimer = 0;
|
||||
|
||||
if (FlipLeftFlag != 0)
|
||||
UnFlipLeft(0, this);
|
||||
if (FlipRightFlag)
|
||||
UnFlipRight(0, this);
|
||||
if (PlungerFlag)
|
||||
PlungerRelease(0, this);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TDemo::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
|
||||
{
|
||||
ball->not_again(edge);
|
||||
ball->Position.X = nextPosition->X;
|
||||
ball->Position.Y = nextPosition->Y;
|
||||
ball->RayMaxDistance -= distance;
|
||||
|
||||
switch (reinterpret_cast<size_t>(edge->WallValue))
|
||||
{
|
||||
case 1400:
|
||||
if (!FlipLeftTimer && !FlipLeftFlag)
|
||||
{
|
||||
float time = FlipTimerTime1 + FlipTimerTime2 - RandFloat() * (FlipTimerTime2 + FlipTimerTime2);
|
||||
FlipLeftTimer = timer::set(time, this, FlipLeft);
|
||||
}
|
||||
break;
|
||||
case 1401:
|
||||
FlipLeft(0, this);
|
||||
break;
|
||||
case 1402:
|
||||
if (!FlipRightTimer && !FlipRightFlag)
|
||||
{
|
||||
float time = FlipTimerTime1 + FlipTimerTime2 - RandFloat() * (FlipTimerTime2 + FlipTimerTime2);
|
||||
FlipRightTimer = timer::set(time, this, FlipRight);
|
||||
}
|
||||
break;
|
||||
case 1403:
|
||||
FlipRight(0, this);
|
||||
break;
|
||||
case 1404:
|
||||
if (!PlungerFlag)
|
||||
{
|
||||
PinballTable->Message(MessageCode::PlungerInputPressed, 0);
|
||||
float time = RandFloat() + 2.0f;
|
||||
PlungerFlag = timer::set(time, this, PlungerRelease);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TDemo::PlungerRelease(int timerId, void* caller)
|
||||
{
|
||||
auto demo = static_cast<TDemo*>(caller);
|
||||
demo->PlungerFlag = 0;
|
||||
demo->PinballTable->Message(MessageCode::PlungerInputReleased, pb::time_next);
|
||||
}
|
||||
|
||||
void TDemo::UnFlipRight(int timerId, void* caller)
|
||||
{
|
||||
auto demo = static_cast<TDemo*>(caller);
|
||||
if (demo->FlipRightFlag)
|
||||
demo->PinballTable->Message(MessageCode::RightFlipperInputReleased, pb::time_next);
|
||||
demo->FlipRightFlag = 0;
|
||||
}
|
||||
|
||||
void TDemo::UnFlipLeft(int timerId, void* caller)
|
||||
{
|
||||
auto demo = static_cast<TDemo*>(caller);
|
||||
if (demo->FlipLeftFlag)
|
||||
demo->PinballTable->Message(MessageCode::LeftFlipperInputReleased, pb::time_next);
|
||||
demo->FlipLeftFlag = 0;
|
||||
}
|
||||
|
||||
void TDemo::FlipRight(int timerId, void* caller)
|
||||
{
|
||||
auto demo = static_cast<TDemo*>(caller);
|
||||
if (!demo->FlipRightFlag)
|
||||
{
|
||||
if (demo->FlipRightTimer)
|
||||
{
|
||||
timer::kill(demo->FlipRightTimer);
|
||||
demo->FlipRightTimer = 0;
|
||||
}
|
||||
demo->PinballTable->Message(MessageCode::RightFlipperInputPressed, pb::time_next);
|
||||
demo->FlipRightFlag = 1;
|
||||
float time = demo->UnFlipTimerTime1 + demo->UnFlipTimerTime2 - RandFloat() *
|
||||
(demo->UnFlipTimerTime2 + demo->UnFlipTimerTime2);
|
||||
timer::set(time, demo, UnFlipRight);
|
||||
}
|
||||
}
|
||||
|
||||
void TDemo::FlipLeft(int timerId, void* caller)
|
||||
{
|
||||
auto demo = static_cast<TDemo*>(caller);
|
||||
if (!demo->FlipLeftFlag)
|
||||
{
|
||||
if (demo->FlipLeftTimer)
|
||||
{
|
||||
timer::kill(demo->FlipLeftTimer);
|
||||
demo->FlipLeftTimer = 0;
|
||||
}
|
||||
demo->PinballTable->Message(MessageCode::LeftFlipperInputPressed, pb::time_next);
|
||||
demo->FlipLeftFlag = 1;
|
||||
float time = demo->UnFlipTimerTime1 + demo->UnFlipTimerTime2 - RandFloat() *
|
||||
(demo->UnFlipTimerTime2 + demo->UnFlipTimerTime2);
|
||||
timer::set(time, demo, UnFlipLeft);
|
||||
}
|
||||
}
|
||||
|
||||
void TDemo::NewGameRestartTimer(int timerId, void* caller)
|
||||
{
|
||||
auto demo = static_cast<TDemo*>(caller);
|
||||
pb::replay_level(true);
|
||||
demo->PinballTable->Message(MessageCode::NewGame, static_cast<float>(demo->PinballTable->PlayerCount));
|
||||
demo->RestartGameTimer = 0;
|
||||
}
|
||||
33
SpaceCadetPinball/TDemo.h
Normal file
33
SpaceCadetPinball/TDemo.h
Normal file
@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
class TDemo :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TDemo(TPinballTable* table, int groupIndex);
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
|
||||
static void PlungerRelease(int timerId, void* caller);
|
||||
static void UnFlipRight(int timerId, void* caller);
|
||||
static void UnFlipLeft(int timerId, void* caller);
|
||||
static void FlipRight(int timerId, void* caller);
|
||||
static void FlipLeft(int timerId, void* caller);
|
||||
static void NewGameRestartTimer(int timerId, void* caller);
|
||||
|
||||
float FlipTimerTime1;
|
||||
float FlipTimerTime2;
|
||||
float UnFlipTimerTime1;
|
||||
float UnFlipTimerTime2;
|
||||
int FlipLeftFlag;
|
||||
int FlipRightFlag;
|
||||
int FlipLeftTimer;
|
||||
int FlipRightTimer;
|
||||
int PlungerFlag;
|
||||
int RestartGameTimer;
|
||||
TEdgeSegment* Edge1;
|
||||
TEdgeSegment* Edge2;
|
||||
TEdgeSegment* Edge3;
|
||||
};
|
||||
48
SpaceCadetPinball/TDrain.cpp
Normal file
48
SpaceCadetPinball/TDrain.cpp
Normal file
@ -0,0 +1,48 @@
|
||||
#include "pch.h"
|
||||
#include "TDrain.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "TBall.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TDrain::TDrain(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
|
||||
{
|
||||
Timer = 0;
|
||||
TimerTime = *loader::query_float_attribute(groupIndex, 0, 407);
|
||||
}
|
||||
|
||||
int TDrain::Message(MessageCode code, float value)
|
||||
{
|
||||
if (code == MessageCode::Reset)
|
||||
{
|
||||
if (Timer)
|
||||
{
|
||||
timer::kill(Timer);
|
||||
Timer = 0;
|
||||
}
|
||||
PinballTable->BallInDrainFlag = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TDrain::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
|
||||
{
|
||||
ball->Disable();
|
||||
--PinballTable->MultiballCount;
|
||||
if (PinballTable->MultiballCount <= 0)
|
||||
{
|
||||
PinballTable->MultiballCount = 0;
|
||||
PinballTable->BallInDrainFlag = 1;
|
||||
Timer = timer::set(TimerTime, this, TimerCallback);
|
||||
}
|
||||
control::handler(MessageCode::ControlCollision, this);
|
||||
}
|
||||
|
||||
void TDrain::TimerCallback(int timerId, void* caller)
|
||||
{
|
||||
auto drain = static_cast<TDrain*>(caller);
|
||||
control::handler(MessageCode::ControlTimerExpired, drain);
|
||||
}
|
||||
17
SpaceCadetPinball/TDrain.h
Normal file
17
SpaceCadetPinball/TDrain.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
class TDrain :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TDrain(TPinballTable* table, int groupIndex);
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
|
||||
static void TimerCallback(int timerId, void* caller);
|
||||
|
||||
float TimerTime;
|
||||
int Timer;
|
||||
};
|
||||
11
SpaceCadetPinball/TEdgeBox.h
Normal file
11
SpaceCadetPinball/TEdgeBox.h
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
struct field_effect_type;
|
||||
class TEdgeSegment;
|
||||
|
||||
class TEdgeBox
|
||||
{
|
||||
public:
|
||||
std::vector<TEdgeSegment*> EdgeList{};
|
||||
std::vector<field_effect_type*> FieldList{};
|
||||
};
|
||||
231
SpaceCadetPinball/TEdgeManager.cpp
Normal file
231
SpaceCadetPinball/TEdgeManager.cpp
Normal file
@ -0,0 +1,231 @@
|
||||
#include "pch.h"
|
||||
#include "TEdgeManager.h"
|
||||
|
||||
|
||||
#include "maths.h"
|
||||
#include "TBall.h"
|
||||
#include "TEdgeBox.h"
|
||||
#include "TEdgeSegment.h"
|
||||
#include "TTableLayer.h"
|
||||
|
||||
TEdgeManager::TEdgeManager(float xMin, float yMin, float width, float height)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
MinX = xMin;
|
||||
MinY = yMin;
|
||||
MaxX = MinX + width;
|
||||
MaxY = MinY + height;
|
||||
MaxBoxX = 10;
|
||||
MaxBoxY = 15;
|
||||
AdvanceX = width / static_cast<float>(MaxBoxX);
|
||||
AdvanceY = height / static_cast<float>(MaxBoxY);
|
||||
BoxArray = new TEdgeBox[MaxBoxX * MaxBoxY];
|
||||
}
|
||||
|
||||
TEdgeManager::~TEdgeManager()
|
||||
{
|
||||
delete[] BoxArray;
|
||||
}
|
||||
|
||||
int TEdgeManager::box_x(float x)
|
||||
{
|
||||
return std::max(0, std::min(static_cast<int>(floor((x - MinX) / AdvanceX)), MaxBoxX - 1));
|
||||
}
|
||||
|
||||
int TEdgeManager::box_y(float y)
|
||||
{
|
||||
return std::max(0, std::min(static_cast<int>(floor((y - MinY) / AdvanceY)), MaxBoxY - 1));
|
||||
}
|
||||
|
||||
int TEdgeManager::increment_box_x(int x)
|
||||
{
|
||||
return std::min(x + 1, MaxBoxX - 1);
|
||||
}
|
||||
|
||||
int TEdgeManager::increment_box_y(int y)
|
||||
{
|
||||
return std::min(y + 1, MaxBoxY - 1);
|
||||
}
|
||||
|
||||
void TEdgeManager::add_edge_to_box(int x, int y, TEdgeSegment* edge)
|
||||
{
|
||||
assertm((unsigned)x < (unsigned)MaxBoxX && (unsigned)y < (unsigned)MaxBoxY, "Box coordinates out of range");
|
||||
|
||||
auto& list = BoxArray[x + y * MaxBoxX].EdgeList;
|
||||
assertm(std::find(list.begin(), list.end(), edge) == list.end(), "Duplicate inserted into box");
|
||||
list.push_back(edge);
|
||||
}
|
||||
|
||||
void TEdgeManager::add_field_to_box(int x, int y, field_effect_type* field)
|
||||
{
|
||||
assertm((unsigned)x < (unsigned)MaxBoxX && (unsigned)y < (unsigned)MaxBoxY, "Box coordinates out of range");
|
||||
|
||||
auto& list = BoxArray[x + y * MaxBoxX].FieldList;
|
||||
assertm(std::find(list.begin(), list.end(), field) == list.end(), "Duplicate inserted into box");
|
||||
list.push_back(field);
|
||||
}
|
||||
|
||||
int TEdgeManager::TestGridBox(int x, int y, float* distPtr, TEdgeSegment** edgeDst, ray_type* ray, TBall* ball,
|
||||
int edgeIndex)
|
||||
{
|
||||
if (x >= 0 && x < MaxBoxX && y >= 0 && y < MaxBoxY)
|
||||
{
|
||||
TEdgeBox* edgeBox = &BoxArray[x + y * MaxBoxX];
|
||||
TEdgeSegment** edgePtr = &EdgeArray[edgeIndex];
|
||||
for (auto it = edgeBox->EdgeList.rbegin(); it != edgeBox->EdgeList.rend(); ++it)
|
||||
{
|
||||
auto edge = *it;
|
||||
if (!edge->ProcessedFlag && *edge->ActiveFlagPtr && (edge->CollisionGroup & ray->CollisionMask) != 0)
|
||||
{
|
||||
if (!ball->already_hit(*edge))
|
||||
{
|
||||
++edgeIndex;
|
||||
*edgePtr = edge;
|
||||
++edgePtr;
|
||||
edge->ProcessedFlag = 1;
|
||||
auto dist = edge->FindCollisionDistance(*ray);
|
||||
if (dist < *distPtr)
|
||||
{
|
||||
*distPtr = dist;
|
||||
*edgeDst = edge;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return edgeIndex;
|
||||
}
|
||||
|
||||
void TEdgeManager::FieldEffects(TBall* ball, vector2* dstVec)
|
||||
{
|
||||
vector2 vec{};
|
||||
TEdgeBox* edgeBox = &BoxArray[box_x(ball->Position.X) + box_y(ball->Position.Y) *
|
||||
MaxBoxX];
|
||||
|
||||
for (auto it = edgeBox->FieldList.rbegin(); it != edgeBox->FieldList.rend(); ++it)
|
||||
{
|
||||
auto field = *it;
|
||||
if (*field->ActiveFlag && ball->CollisionMask & field->CollisionGroup)
|
||||
{
|
||||
if (field->CollisionComp->FieldEffect(ball, &vec))
|
||||
{
|
||||
maths::vector_add(*dstVec, vec);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float TEdgeManager::FindCollisionDistance(ray_type* ray, TBall* ball, TEdgeSegment** edge)
|
||||
{
|
||||
auto distance = 1000000000.0f;
|
||||
auto edgeIndex = 0;
|
||||
|
||||
auto x0 = ray->Origin.X;
|
||||
auto y0 = ray->Origin.Y;
|
||||
auto x1 = ray->Direction.X * ray->MaxDistance + ray->Origin.X;
|
||||
auto y1 = ray->Direction.Y * ray->MaxDistance + ray->Origin.Y;
|
||||
|
||||
auto xBox0 = box_x(x0);
|
||||
auto yBox0 = box_y(y0);
|
||||
auto xBox1 = box_x(x1);
|
||||
auto yBox1 = box_y(y1);
|
||||
|
||||
auto dirX = x0 >= x1 ? -1 : 1;
|
||||
auto dirY = y0 >= y1 ? -1 : 1;
|
||||
|
||||
if (yBox0 == yBox1)
|
||||
{
|
||||
if (dirX == 1)
|
||||
{
|
||||
for (auto indexX = xBox0; indexX <= xBox1; indexX++)
|
||||
{
|
||||
edgeIndex = TestGridBox(indexX, yBox0, &distance, edge, ray, ball, edgeIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto indexX = xBox0; indexX >= xBox1; indexX--)
|
||||
{
|
||||
edgeIndex = TestGridBox(indexX, yBox0, &distance, edge, ray, ball, edgeIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (xBox0 == xBox1)
|
||||
{
|
||||
if (dirY == 1)
|
||||
{
|
||||
for (auto indexY = yBox0; indexY <= yBox1; indexY++)
|
||||
{
|
||||
edgeIndex = TestGridBox(xBox0, indexY, &distance, edge, ray, ball, edgeIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto indexY = yBox0; indexY >= yBox1; indexY--)
|
||||
{
|
||||
edgeIndex = TestGridBox(xBox0, indexY, &distance, edge, ray, ball, edgeIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
edgeIndex = TestGridBox(xBox0, yBox0, &distance, edge, ray, ball, 0);
|
||||
|
||||
// Bresenham line formula: y = dYdX * (x - x0) + y0; dYdX = (y0 - y1) / (x0 - x1)
|
||||
auto dyDx = (y0 - y1) / (x0 - x1);
|
||||
// Precompute constant part: dYdX * (-x0) + y0
|
||||
auto precomp = -x0 * dyDx + y0;
|
||||
// X and Y indexes are offset by one when going forwards, not sure why
|
||||
auto xBias = dirX == 1 ? 1 : 0, yBias = dirY == 1 ? 1 : 0;
|
||||
|
||||
for (auto indexX = xBox0, indexY = yBox0; indexX != xBox1 || indexY != yBox1;)
|
||||
{
|
||||
// Calculate y from indexY and from line formula
|
||||
auto yDiscrete = (indexY + yBias) * AdvanceY + MinY;
|
||||
auto ylinear = ((indexX + xBias) * AdvanceX + MinX) * dyDx + precomp;
|
||||
if (dirY == 1 ? ylinear >= yDiscrete : ylinear <= yDiscrete)
|
||||
{
|
||||
// Advance indexY when discrete value is ahead/behind
|
||||
// Advance indexX when discrete value matches linear value
|
||||
indexY += dirY;
|
||||
if (ylinear == yDiscrete)
|
||||
indexX += dirX;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Advance indexX otherwise
|
||||
indexX += dirX;
|
||||
}
|
||||
edgeIndex = TestGridBox(indexX, indexY, &distance, edge, ray, ball, edgeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (auto edgePtr = EdgeArray; edgeIndex > 0; --edgeIndex, ++edgePtr)
|
||||
{
|
||||
(*edgePtr)->ProcessedFlag = 0;
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
vector2 TEdgeManager::NormalizeBox(vector2 pt) const
|
||||
{
|
||||
// Standard PB Box ranges: X [-8, 8]; Y [-14, 15]; Top right corner: (-8, -14)
|
||||
// Bring them to: X [0, 16]; Y [0, 29]; Top right corner: (0, 0)
|
||||
auto x = Clamp(pt.X, MinX, MaxX) + abs(MinX);
|
||||
auto y = Clamp(pt.Y, MinY, MaxY) + abs(MinY);
|
||||
|
||||
// Normalize and invert to: X [0, 1]; Y [0, 1]; Top right corner: (1, 1)
|
||||
x /= Width; y /= Height;
|
||||
return vector2{ 1 - x, 1 - y };
|
||||
}
|
||||
|
||||
vector2 TEdgeManager::DeNormalizeBox(vector2 pt) const
|
||||
{
|
||||
// Undo normalization by applying steps in reverse
|
||||
auto x = (1 - pt.X) * Width - abs(MinX);
|
||||
auto y = (1 - pt.Y) * Height - abs(MinY);
|
||||
return vector2{ x, y };
|
||||
}
|
||||
43
SpaceCadetPinball/TEdgeManager.h
Normal file
43
SpaceCadetPinball/TEdgeManager.h
Normal file
@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
struct ray_type;
|
||||
class TEdgeBox;
|
||||
|
||||
struct field_effect_type
|
||||
{
|
||||
char* ActiveFlag;
|
||||
int CollisionGroup;
|
||||
TCollisionComponent* CollisionComp;
|
||||
};
|
||||
|
||||
class TEdgeManager
|
||||
{
|
||||
public:
|
||||
TEdgeManager(float xMin, float yMin, float width, float height);
|
||||
~TEdgeManager();
|
||||
void FieldEffects(TBall* ball, struct vector2* dstVec);
|
||||
int box_x(float x);
|
||||
int box_y(float y);
|
||||
int increment_box_x(int x);
|
||||
int increment_box_y(int y);
|
||||
void add_edge_to_box(int x, int y, TEdgeSegment* edge);
|
||||
void add_field_to_box(int x, int y, field_effect_type* field);
|
||||
int TestGridBox(int x, int y, float* distPtr, TEdgeSegment** edgeDst, ray_type* ray, TBall* ball, int edgeIndex);
|
||||
float FindCollisionDistance(ray_type* ray, TBall* ball, TEdgeSegment** edge);
|
||||
vector2 NormalizeBox(vector2 pt) const;
|
||||
vector2 DeNormalizeBox(vector2 pt) const;
|
||||
|
||||
float AdvanceX;
|
||||
float AdvanceY;
|
||||
int MaxBoxX;
|
||||
int MaxBoxY;
|
||||
float MinX;
|
||||
float MinY;
|
||||
float MaxX;
|
||||
float MaxY;
|
||||
float Width;
|
||||
float Height;
|
||||
TEdgeBox* BoxArray;
|
||||
TEdgeSegment* EdgeArray[1000]{};
|
||||
};
|
||||
116
SpaceCadetPinball/TEdgeSegment.cpp
Normal file
116
SpaceCadetPinball/TEdgeSegment.cpp
Normal file
@ -0,0 +1,116 @@
|
||||
#include "pch.h"
|
||||
#include "TEdgeSegment.h"
|
||||
|
||||
#include "TCircle.h"
|
||||
#include "TCollisionComponent.h"
|
||||
#include "TLine.h"
|
||||
|
||||
TEdgeSegment::TEdgeSegment(TCollisionComponent* collComp, char* activeFlag, unsigned collisionGroup)
|
||||
{
|
||||
CollisionComponent = collComp;
|
||||
ActiveFlagPtr = activeFlag;
|
||||
CollisionGroup = collisionGroup;
|
||||
ProcessedFlag = 0;
|
||||
}
|
||||
|
||||
void TEdgeSegment::port_draw()
|
||||
{
|
||||
}
|
||||
|
||||
TEdgeSegment* TEdgeSegment::install_wall(float* floatArr, TCollisionComponent* collComp, char* activeFlagPtr,
|
||||
unsigned int collisionGroup, float offset, size_t wallValue)
|
||||
{
|
||||
vector2 center{}, start{}, end{}, prevCenter{};
|
||||
vector3 vec1{}, vec2{}, dstVec{};
|
||||
TEdgeSegment* edge = nullptr;
|
||||
|
||||
wall_type wallType = static_cast<wall_type>(static_cast<int>(floor(*floatArr) - 1.0f));
|
||||
switch (wallType)
|
||||
{
|
||||
case wall_type::Circle:
|
||||
{
|
||||
center.X = floatArr[1];
|
||||
center.Y = floatArr[2];
|
||||
auto radius = offset + floatArr[3];
|
||||
auto circle = new TCircle(collComp, activeFlagPtr, collisionGroup, ¢er, radius);
|
||||
circle->WallValue = reinterpret_cast<void*>(wallValue);
|
||||
circle->place_in_grid(&collComp->AABB);
|
||||
collComp->EdgeList.push_back(circle);
|
||||
edge = circle;
|
||||
break;
|
||||
}
|
||||
case wall_type::Line:
|
||||
{
|
||||
start.X = floatArr[1];
|
||||
start.Y = floatArr[2];
|
||||
end.X = floatArr[3];
|
||||
end.Y = floatArr[4];
|
||||
auto line = new TLine(collComp, activeFlagPtr, collisionGroup, start, end);
|
||||
line->WallValue = reinterpret_cast<void*>(wallValue);
|
||||
line->Offset(offset);
|
||||
line->place_in_grid(&collComp->AABB);
|
||||
collComp->EdgeList.push_back(line);
|
||||
edge = line;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
int wallTypeI = static_cast<int>(wallType);
|
||||
auto floatArrPtr = floatArr + 1;
|
||||
prevCenter.X = floatArr[2 * wallTypeI - 1];
|
||||
prevCenter.Y = floatArr[2 * wallTypeI];
|
||||
|
||||
for (int index = 0; index < wallTypeI; index++, floatArrPtr += 2)
|
||||
{
|
||||
float centerX2, centerY2;
|
||||
if (index >= wallTypeI - 1)
|
||||
{
|
||||
centerX2 = floatArr[1];
|
||||
centerY2 = floatArr[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
centerX2 = floatArrPtr[2];
|
||||
centerY2 = floatArrPtr[3];
|
||||
}
|
||||
auto centerX1 = floatArrPtr[0];
|
||||
auto centerY1 = floatArrPtr[1];
|
||||
|
||||
center.X = centerX1;
|
||||
center.Y = centerY1;
|
||||
if (offset != 0.0f)
|
||||
{
|
||||
vec1.X = centerX1 - prevCenter.X;
|
||||
vec1.Y = center.Y - prevCenter.Y;
|
||||
vec2.X = centerX2 - centerX1;
|
||||
vec2.Y = centerY2 - center.Y;
|
||||
maths::cross(vec1, vec2, dstVec);
|
||||
if ((dstVec.Z > 0.0f && offset > 0.0f) ||
|
||||
(dstVec.Z < 0.0f && offset < 0.0f))
|
||||
{
|
||||
float radius = offset * 1.001f;
|
||||
auto circle = new TCircle(collComp, activeFlagPtr, collisionGroup, ¢er, radius);
|
||||
circle->WallValue = reinterpret_cast<void*>(wallValue);
|
||||
circle->place_in_grid(&collComp->AABB);
|
||||
collComp->EdgeList.push_back(circle);
|
||||
}
|
||||
}
|
||||
|
||||
start.X = floatArrPtr[0];
|
||||
start.Y = floatArrPtr[1];
|
||||
end.X = floatArrPtr[2];
|
||||
end.Y = floatArrPtr[3];
|
||||
auto line = new TLine(collComp, activeFlagPtr, collisionGroup, start, end);
|
||||
line->WallValue = reinterpret_cast<void*>(wallValue);
|
||||
line->Offset(offset);
|
||||
line->place_in_grid(&collComp->AABB);
|
||||
collComp->EdgeList.push_back(line);
|
||||
|
||||
edge = line;
|
||||
prevCenter = center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return edge;
|
||||
}
|
||||
33
SpaceCadetPinball/TEdgeSegment.h
Normal file
33
SpaceCadetPinball/TEdgeSegment.h
Normal file
@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
class TBall;
|
||||
class TCollisionComponent;
|
||||
struct ray_type;
|
||||
struct RectF;
|
||||
|
||||
enum class wall_type : int
|
||||
{
|
||||
Circle = 0,
|
||||
Line = 1,
|
||||
};
|
||||
|
||||
class TEdgeSegment
|
||||
{
|
||||
public:
|
||||
TCollisionComponent* CollisionComponent;
|
||||
char* ActiveFlagPtr;
|
||||
char ProcessedFlag;
|
||||
void* WallValue{};
|
||||
unsigned int CollisionGroup;
|
||||
|
||||
TEdgeSegment(TCollisionComponent* collComp, char* activeFlag, unsigned int collisionGroup);
|
||||
virtual ~TEdgeSegment() = default;
|
||||
|
||||
virtual void EdgeCollision(TBall* ball, float distance) = 0;
|
||||
virtual void port_draw();
|
||||
virtual void place_in_grid(RectF* aabb) = 0;
|
||||
virtual float FindCollisionDistance(const ray_type& ray) = 0;
|
||||
|
||||
static TEdgeSegment* install_wall(float* floatArr, TCollisionComponent* collComp, char* activeFlagPtr,
|
||||
unsigned int collisionGroup, float offset, size_t wallValue);
|
||||
};
|
||||
120
SpaceCadetPinball/TFlagSpinner.cpp
Normal file
120
SpaceCadetPinball/TFlagSpinner.cpp
Normal file
@ -0,0 +1,120 @@
|
||||
#include "pch.h"
|
||||
#include "TFlagSpinner.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "render.h"
|
||||
#include "TBall.h"
|
||||
#include "timer.h"
|
||||
#include "TLine.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TFlagSpinner::TFlagSpinner(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false)
|
||||
{
|
||||
visualStruct visual{};
|
||||
vector2 end{}, start{};
|
||||
|
||||
Timer = 0;
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
end.X = visual.FloatArr[0];
|
||||
end.Y = visual.FloatArr[1];
|
||||
start.X = visual.FloatArr[2];
|
||||
start.Y = visual.FloatArr[3];
|
||||
auto line = new TLine(this, &ActiveFlag, visual.CollisionGroup, start, end);
|
||||
if (line)
|
||||
{
|
||||
line->place_in_grid(&AABB);
|
||||
EdgeList.push_back(line);
|
||||
}
|
||||
|
||||
line = new TLine(this, &ActiveFlag, visual.CollisionGroup, end, start);
|
||||
PrevCollider = line;
|
||||
if (line)
|
||||
{
|
||||
line->place_in_grid(&AABB);
|
||||
EdgeList.push_back(line);
|
||||
}
|
||||
|
||||
SpeedDecrement = 0.64999998f;
|
||||
MaxSpeed = 50000.0f;
|
||||
MinSpeed = 5.0f;
|
||||
auto speedDec = loader::query_float_attribute(groupIndex, 0, 1202);
|
||||
if (speedDec)
|
||||
SpeedDecrement = *speedDec;
|
||||
auto maxSpeed = loader::query_float_attribute(groupIndex, 0, 1200);
|
||||
if (maxSpeed)
|
||||
MaxSpeed = *maxSpeed;
|
||||
auto minSpeed = loader::query_float_attribute(groupIndex, 0, 1201);
|
||||
if (minSpeed)
|
||||
MinSpeed = *minSpeed;
|
||||
}
|
||||
|
||||
int TFlagSpinner::Message(MessageCode code, float value)
|
||||
{
|
||||
if (code == MessageCode::Reset)
|
||||
{
|
||||
if (Timer)
|
||||
{
|
||||
timer::kill(Timer);
|
||||
Timer = 0;
|
||||
}
|
||||
BmpIndex = 0;
|
||||
SpriteSet(BmpIndex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TFlagSpinner::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge)
|
||||
{
|
||||
ball->Position.X = nextPosition->X;
|
||||
ball->Position.Y = nextPosition->Y;
|
||||
ball->RayMaxDistance = ball->RayMaxDistance - distance;
|
||||
ball->not_again(edge);
|
||||
|
||||
SpinDirection = 2 * (PrevCollider != edge) - 1;
|
||||
if (ball->Speed == 0.0f)
|
||||
Speed = MinSpeed;
|
||||
else
|
||||
Speed = ball->Speed * 20.0f;
|
||||
if (Speed < MinSpeed)
|
||||
Speed = MinSpeed;
|
||||
if (Speed > MaxSpeed)
|
||||
Speed = MaxSpeed;
|
||||
NextFrame();
|
||||
}
|
||||
|
||||
void TFlagSpinner::NextFrame()
|
||||
{
|
||||
BmpIndex += SpinDirection;
|
||||
int bmpIndex = BmpIndex;
|
||||
int bmpCount = static_cast<int>(ListBitmap->size());
|
||||
if (bmpIndex >= bmpCount)
|
||||
BmpIndex = 0;
|
||||
else if (bmpIndex < 0)
|
||||
BmpIndex = bmpCount - 1;
|
||||
|
||||
if (!PinballTable->TiltLockFlag)
|
||||
{
|
||||
control::handler(MessageCode::ControlCollision, this);
|
||||
if (SoftHitSoundId)
|
||||
loader::play_sound(SoftHitSoundId, this, "TFlagSpinner");
|
||||
if (!BmpIndex)
|
||||
control::handler(MessageCode::ControlSpinnerLoopReset, this);
|
||||
}
|
||||
|
||||
SpriteSet(BmpIndex);
|
||||
Speed *= SpeedDecrement;
|
||||
if (Speed >= MinSpeed)
|
||||
{
|
||||
timer::set(1.0f / Speed, this, SpinTimer);
|
||||
}
|
||||
}
|
||||
|
||||
void TFlagSpinner::SpinTimer(int timerId, void* caller)
|
||||
{
|
||||
auto spinner = static_cast<TFlagSpinner*>(caller);
|
||||
spinner->Timer = 0;
|
||||
spinner->NextFrame();
|
||||
}
|
||||
25
SpaceCadetPinball/TFlagSpinner.h
Normal file
25
SpaceCadetPinball/TFlagSpinner.h
Normal file
@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
class TFlagSpinner :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TFlagSpinner(TPinballTable* table, int groupIndex);
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
void NextFrame();
|
||||
|
||||
static void SpinTimer(int timerId, void* caller);
|
||||
|
||||
float Speed{};
|
||||
float MaxSpeed;
|
||||
float MinSpeed;
|
||||
float SpeedDecrement;
|
||||
int SpinDirection{};
|
||||
int BmpIndex{};
|
||||
int Timer;
|
||||
TEdgeSegment* PrevCollider;
|
||||
};
|
||||
|
||||
203
SpaceCadetPinball/TFlipper.cpp
Normal file
203
SpaceCadetPinball/TFlipper.cpp
Normal file
@ -0,0 +1,203 @@
|
||||
#include "pch.h"
|
||||
#include "TFlipper.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "pb.h"
|
||||
#include "render.h"
|
||||
#include "TBall.h"
|
||||
#include "TFlipperEdge.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TFlipper::TFlipper(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false)
|
||||
{
|
||||
visualStruct visual{};
|
||||
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
HardHitSoundId = visual.SoundIndex4;
|
||||
SoftHitSoundId = visual.SoundIndex3;
|
||||
Elasticity = visual.Elasticity;
|
||||
Smoothness = visual.Smoothness;
|
||||
|
||||
auto collMult = *loader::query_float_attribute(groupIndex, 0, 803);
|
||||
auto retractTime = *loader::query_float_attribute(groupIndex, 0, 805);
|
||||
auto extendTime = *loader::query_float_attribute(groupIndex, 0, 804);
|
||||
auto vecT2 = reinterpret_cast<vector3*>(loader::query_float_attribute(groupIndex, 0, 802));
|
||||
auto vecT1 = reinterpret_cast<vector3*>(loader::query_float_attribute(groupIndex, 0, 801));
|
||||
auto origin = reinterpret_cast<vector3*>(loader::query_float_attribute(groupIndex, 0, 800));
|
||||
auto flipperEdge = new TFlipperEdge(
|
||||
this,
|
||||
&ActiveFlag,
|
||||
visual.CollisionGroup,
|
||||
table,
|
||||
origin,
|
||||
vecT1,
|
||||
vecT2,
|
||||
extendTime,
|
||||
retractTime,
|
||||
collMult,
|
||||
Elasticity,
|
||||
Smoothness);
|
||||
flipperEdge->place_in_grid(&AABB);
|
||||
|
||||
FlipperEdge = flipperEdge;
|
||||
BmpIndex = 0;
|
||||
if (table)
|
||||
table->FlipperList.push_back(this);
|
||||
}
|
||||
|
||||
TFlipper::~TFlipper()
|
||||
{
|
||||
delete FlipperEdge;
|
||||
if (PinballTable)
|
||||
{
|
||||
auto& flippers = PinballTable->FlipperList;
|
||||
auto position = std::find(flippers.begin(), flippers.end(), this);
|
||||
if (position != flippers.end())
|
||||
flippers.erase(position);
|
||||
}
|
||||
}
|
||||
|
||||
int TFlipper::Message(MessageCode code, float value)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::TFlipperExtend:
|
||||
case MessageCode::TFlipperRetract:
|
||||
case MessageCode::Resume:
|
||||
case MessageCode::LooseFocus:
|
||||
case MessageCode::SetTiltLock:
|
||||
case MessageCode::GameOver:
|
||||
if (code == MessageCode::TFlipperExtend)
|
||||
{
|
||||
control::handler(MessageCode::TFlipperExtend, this);
|
||||
loader::play_sound(HardHitSoundId, this, "TFlipper1");
|
||||
}
|
||||
else if (code == MessageCode::TFlipperRetract)
|
||||
{
|
||||
loader::play_sound(SoftHitSoundId, this, "TFlipper2");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Retract for all non-input messages
|
||||
code = MessageCode::TFlipperRetract;
|
||||
}
|
||||
|
||||
MessageField = FlipperEdge->SetMotion(code);
|
||||
break;
|
||||
case MessageCode::PlayerChanged:
|
||||
case MessageCode::Reset:
|
||||
if (MessageField)
|
||||
{
|
||||
FlipperEdge->CurrentAngle = 0;
|
||||
FlipperEdge->set_control_points(0);
|
||||
MessageField = 0;
|
||||
FlipperEdge->SetMotion(MessageCode::Reset);
|
||||
UpdateSprite();
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TFlipper::port_draw()
|
||||
{
|
||||
FlipperEdge->port_draw();
|
||||
}
|
||||
|
||||
void TFlipper::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
|
||||
{
|
||||
}
|
||||
|
||||
void TFlipper::UpdateSprite()
|
||||
{
|
||||
auto bmpCountSub1 = static_cast<int>(ListBitmap->size()) - 1;
|
||||
auto newBmpIndex = static_cast<int>(floor(FlipperEdge->CurrentAngle / FlipperEdge->AngleMax * bmpCountSub1 + 0.5f));
|
||||
newBmpIndex = Clamp(newBmpIndex, 0, bmpCountSub1);
|
||||
if (BmpIndex == newBmpIndex)
|
||||
return;
|
||||
|
||||
BmpIndex = newBmpIndex;
|
||||
SpriteSet(BmpIndex);
|
||||
}
|
||||
|
||||
int TFlipper::GetFlipperStepAngle(float dt, float* dst) const
|
||||
{
|
||||
if (!MessageField)
|
||||
return 0;
|
||||
|
||||
auto deltaAngle = FlipperEdge->flipper_angle_delta(dt);
|
||||
auto step = std::fabs(std::ceil(FlipperEdge->DistanceDiv * deltaAngle * FlipperEdge->InvT1Radius));
|
||||
if (step > 3.0f)
|
||||
step = 3.0f;
|
||||
if (step >= 2.0f)
|
||||
{
|
||||
*dst = deltaAngle / step;
|
||||
return static_cast<int>(step);
|
||||
}
|
||||
|
||||
*dst = deltaAngle;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void TFlipper::FlipperCollision(float deltaAngle)
|
||||
{
|
||||
if (!MessageField)
|
||||
return;
|
||||
|
||||
ray_type ray{}, rayDst{};
|
||||
ray.MinDistance = 0.002f;
|
||||
bool collisionFlag = false;
|
||||
for (auto ball : pb::MainTable->BallList)
|
||||
{
|
||||
if ((FlipperEdge->CollisionGroup & ball->CollisionMask) != 0 &&
|
||||
FlipperEdge->YMax >= ball->Position.Y && FlipperEdge->YMin <= ball->Position.Y &&
|
||||
FlipperEdge->XMax >= ball->Position.X && FlipperEdge->XMin <= ball->Position.X)
|
||||
{
|
||||
if (FlipperEdge->ControlPointDirtyFlag)
|
||||
FlipperEdge->set_control_points(FlipperEdge->CurrentAngle);
|
||||
ray.CollisionMask = ball->CollisionMask;
|
||||
ray.Origin = ball->Position;
|
||||
|
||||
float sin, cos;
|
||||
auto ballPosRot = ray.Origin;
|
||||
maths::SinCos(-deltaAngle, sin, cos);
|
||||
maths::RotatePt(ballPosRot, sin, cos, FlipperEdge->RotOrigin);
|
||||
ray.Direction.X = ballPosRot.X - ray.Origin.X;
|
||||
ray.Direction.Y = ballPosRot.Y - ray.Origin.Y;
|
||||
ray.MaxDistance = maths::normalize_2d(ray.Direction);
|
||||
auto distance = maths::distance_to_flipper(FlipperEdge, ray, rayDst);
|
||||
if (distance < 1e9f)
|
||||
{
|
||||
FlipperEdge->NextBallPosition = ball->Position;
|
||||
FlipperEdge->CollisionDirection = rayDst.Direction;
|
||||
FlipperEdge->EdgeCollision(ball, distance);
|
||||
collisionFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (collisionFlag)
|
||||
{
|
||||
auto angleAdvance = deltaAngle / (std::fabs(FlipperEdge->MoveSpeed) * 5.0f);
|
||||
FlipperEdge->CurrentAngle -= angleAdvance;
|
||||
FlipperEdge->AngleRemainder += std::fabs(angleAdvance);
|
||||
}
|
||||
else
|
||||
{
|
||||
FlipperEdge->CurrentAngle += deltaAngle;
|
||||
FlipperEdge->AngleRemainder -= std::fabs(deltaAngle);
|
||||
}
|
||||
|
||||
if (FlipperEdge->AngleRemainder <= 0.0001f)
|
||||
{
|
||||
FlipperEdge->CurrentAngle = FlipperEdge->AngleDst;
|
||||
FlipperEdge->FlipperFlag = MessageCode::TFlipperNull;
|
||||
MessageField = 0;
|
||||
}
|
||||
FlipperEdge->ControlPointDirtyFlag = true;
|
||||
}
|
||||
22
SpaceCadetPinball/TFlipper.h
Normal file
22
SpaceCadetPinball/TFlipper.h
Normal file
@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
class TFlipperEdge;
|
||||
|
||||
class TFlipper :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TFlipper(TPinballTable* table, int groupIndex);
|
||||
~TFlipper() override;
|
||||
int Message(MessageCode code, float value) override;
|
||||
void port_draw() override;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
void UpdateSprite();
|
||||
int GetFlipperStepAngle(float dt, float* dst) const;
|
||||
void FlipperCollision(float deltaAngle);
|
||||
|
||||
int BmpIndex;
|
||||
TFlipperEdge* FlipperEdge;
|
||||
};
|
||||
273
SpaceCadetPinball/TFlipperEdge.cpp
Normal file
273
SpaceCadetPinball/TFlipperEdge.cpp
Normal file
@ -0,0 +1,273 @@
|
||||
#include "pch.h"
|
||||
#include "TFlipperEdge.h"
|
||||
|
||||
|
||||
#include "pb.h"
|
||||
#include "TLine.h"
|
||||
#include "TPinballTable.h"
|
||||
#include "TTableLayer.h"
|
||||
|
||||
|
||||
TFlipperEdge::TFlipperEdge(TCollisionComponent* collComp, char* activeFlag, unsigned int collisionGroup,
|
||||
TPinballTable* table,
|
||||
vector3* origin, vector3* vecT1, vector3* vecT2, float extendSpeed, float retractSpeed,
|
||||
float collMult, float elasticity, float smoothness): TEdgeSegment(
|
||||
collComp, activeFlag, collisionGroup)
|
||||
{
|
||||
vector3 crossProd{}, vecOriginT1{}, vecOriginT2{};
|
||||
|
||||
Elasticity = elasticity;
|
||||
Smoothness = smoothness;
|
||||
CollisionMult = collMult;
|
||||
|
||||
T1Src = static_cast<vector2>(*vecT1);
|
||||
T2Src = static_cast<vector2>(*vecT2);
|
||||
RotOrigin.X = origin->X;
|
||||
RotOrigin.Y = origin->Y;
|
||||
|
||||
CirclebaseRadius = origin->Z + table->CollisionCompOffset;
|
||||
CirclebaseRadiusMSq = CirclebaseRadius * 1.01f * (CirclebaseRadius * 1.01f);
|
||||
CirclebaseRadiusSq = CirclebaseRadius * CirclebaseRadius;
|
||||
|
||||
CircleT1Radius = vecT1->Z + table->CollisionCompOffset;
|
||||
CircleT1RadiusMSq = CircleT1Radius * 1.01f * (CircleT1Radius * 1.01f);
|
||||
CircleT1RadiusSq = CircleT1Radius * CircleT1Radius;
|
||||
|
||||
vecOriginT1.X = vecT1->X - origin->X;
|
||||
vecOriginT1.Y = vecT1->Y - origin->Y;
|
||||
vecOriginT1.Z = 0.0;
|
||||
maths::normalize_2d(vecOriginT1);
|
||||
|
||||
vecOriginT2.X = vecT2->X - origin->X;
|
||||
vecOriginT2.Y = vecT2->Y - origin->Y;
|
||||
vecOriginT2.Z = 0.0;
|
||||
maths::normalize_2d(vecOriginT2);
|
||||
|
||||
AngleMax = acos(maths::DotProduct(vecOriginT1, vecOriginT2));
|
||||
maths::cross(vecOriginT1, vecOriginT2, crossProd);
|
||||
if (crossProd.Z < 0.0f)
|
||||
AngleMax = -AngleMax;
|
||||
|
||||
FlipperFlag = MessageCode::TFlipperNull;
|
||||
AngleDst = 0.0;
|
||||
|
||||
// 3DPB and FT have different formats for flipper speed:
|
||||
// 3DPB: Time it takes for flipper to go from source to destination, in sec.
|
||||
// FT: Flipper movement speed, in radians per sec.
|
||||
if (!pb::FullTiltMode)
|
||||
{
|
||||
auto angleMax = std::abs(AngleMax);
|
||||
retractSpeed = angleMax / retractSpeed;
|
||||
extendSpeed = angleMax / extendSpeed;
|
||||
}
|
||||
ExtendSpeed = extendSpeed;
|
||||
RetractSpeed = retractSpeed;
|
||||
|
||||
const vector2 perpOriginT1Cc = {-vecOriginT1.Y, vecOriginT1.X};
|
||||
A2Src.X = perpOriginT1Cc.X * CirclebaseRadius + origin->X;
|
||||
A2Src.Y = perpOriginT1Cc.Y * CirclebaseRadius + origin->Y;
|
||||
A1Src.X = perpOriginT1Cc.X * CircleT1Radius + vecT1->X;
|
||||
A1Src.Y = perpOriginT1Cc.Y * CircleT1Radius + vecT1->Y;
|
||||
|
||||
const vector2 perpOriginT1C = {vecOriginT1.Y, -vecOriginT1.X};
|
||||
B1Src.X = perpOriginT1C.X * CirclebaseRadius + origin->X;
|
||||
B1Src.Y = perpOriginT1C.Y * CirclebaseRadius + origin->Y;
|
||||
B2Src.X = perpOriginT1C.X * CircleT1Radius + vecT1->X;
|
||||
B2Src.Y = perpOriginT1C.Y * CircleT1Radius + vecT1->Y;
|
||||
|
||||
if (AngleMax < 0.0f)
|
||||
{
|
||||
std::swap(A1Src, B1Src);
|
||||
std::swap(A2Src, B2Src);
|
||||
}
|
||||
|
||||
auto dx = vecT1->X - RotOrigin.X;
|
||||
auto dy = vecT1->Y - RotOrigin.Y;
|
||||
auto distance1 = sqrt(dy * dy + dx * dx) + table->CollisionCompOffset + vecT1->Z;
|
||||
DistanceDiv = distance1;
|
||||
DistanceDivSq = distance1 * distance1;
|
||||
InvT1Radius = 1.0f / CircleT1Radius * 1.5f;
|
||||
|
||||
if (AngleMax <= 0.0f)
|
||||
{
|
||||
ExtendSpeed = -ExtendSpeed;
|
||||
}
|
||||
else
|
||||
{
|
||||
RetractSpeed = -RetractSpeed;
|
||||
}
|
||||
set_control_points(CurrentAngle);
|
||||
}
|
||||
|
||||
void TFlipperEdge::port_draw()
|
||||
{
|
||||
set_control_points(CurrentAngle);
|
||||
}
|
||||
|
||||
float TFlipperEdge::FindCollisionDistance(const ray_type& ray)
|
||||
{
|
||||
ray_type dstRay{};
|
||||
if (ControlPointDirtyFlag)
|
||||
set_control_points(CurrentAngle);
|
||||
auto distance = maths::distance_to_flipper(this, ray, dstRay);
|
||||
if (distance >= 1e9f)
|
||||
return 1e9f;
|
||||
|
||||
NextBallPosition = dstRay.Origin;
|
||||
CollisionDirection = dstRay.Direction;
|
||||
return distance;
|
||||
}
|
||||
|
||||
void TFlipperEdge::EdgeCollision(TBall* ball, float distance)
|
||||
{
|
||||
if (FlipperFlag == MessageCode::TFlipperNull)
|
||||
{
|
||||
maths::basic_collision(
|
||||
ball,
|
||||
&NextBallPosition,
|
||||
&CollisionDirection,
|
||||
Elasticity,
|
||||
Smoothness,
|
||||
1e9f,
|
||||
0);
|
||||
return;
|
||||
}
|
||||
|
||||
vector2 t1NextPos{NextBallPosition.X - T1.X, NextBallPosition.Y - T1.Y},
|
||||
t1RotOrigin{RotOrigin.X - T1.X, RotOrigin.Y - T1.Y};
|
||||
auto crossProduct = maths::cross(t1RotOrigin, t1NextPos);
|
||||
|
||||
bool frontCollision = false;
|
||||
if (crossProduct <= 0)
|
||||
{
|
||||
if (AngleMax > 0)
|
||||
frontCollision = true;
|
||||
}
|
||||
else if (AngleMax <= 0)
|
||||
{
|
||||
frontCollision = true;
|
||||
}
|
||||
|
||||
if (FlipperFlag == MessageCode::TFlipperRetract)
|
||||
{
|
||||
frontCollision ^= true;
|
||||
CollisionLinePerp = LineB.PerpendicularC;
|
||||
}
|
||||
else
|
||||
{
|
||||
CollisionLinePerp = LineA.PerpendicularC;
|
||||
}
|
||||
|
||||
auto dx = NextBallPosition.X - RotOrigin.X;
|
||||
auto dy = NextBallPosition.Y - RotOrigin.Y;
|
||||
auto distanceSq = dy * dy + dx * dx;
|
||||
if (frontCollision)
|
||||
{
|
||||
float boost = 0;
|
||||
if (circlebase.RadiusSq * 1.01f < distanceSq)
|
||||
{
|
||||
auto v21 = std::fabs(MoveSpeed) * std::sqrt(distanceSq / DistanceDivSq);
|
||||
auto dot1 = maths::DotProduct(CollisionLinePerp, CollisionDirection);
|
||||
if (dot1 >= 0)
|
||||
boost = CollisionMult * dot1 * v21;
|
||||
}
|
||||
|
||||
auto threshold = boost <= 0.0f ? 1e9f : -1.0f;
|
||||
maths::basic_collision(
|
||||
ball,
|
||||
&NextBallPosition,
|
||||
&CollisionDirection,
|
||||
Elasticity,
|
||||
Smoothness,
|
||||
threshold,
|
||||
boost);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto elasticity = Elasticity;
|
||||
if (circlebase.RadiusSq * 1.01f < distanceSq)
|
||||
elasticity = (1.0f - std::sqrt(distanceSq / DistanceDivSq)) * Elasticity;
|
||||
maths::basic_collision(ball, &NextBallPosition, &CollisionDirection, elasticity, Smoothness, 1e9f, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
void TFlipperEdge::place_in_grid(RectF* aabb)
|
||||
{
|
||||
auto xMax = std::max(std::max(T2Src.X + CircleT1Radius, T1Src.X + CircleT1Radius), RotOrigin.X + CirclebaseRadius);
|
||||
auto yMax = std::max(std::max(T2Src.Y + CircleT1Radius, T1Src.Y + CircleT1Radius), RotOrigin.Y + CirclebaseRadius);
|
||||
auto xMin = std::min(std::min(T2Src.X - CircleT1Radius, T1Src.X - CircleT1Radius), RotOrigin.X - CirclebaseRadius);
|
||||
auto yMin = std::min(std::min(T2Src.Y - CircleT1Radius, T1Src.Y - CircleT1Radius), RotOrigin.Y - CirclebaseRadius);
|
||||
|
||||
if (aabb)
|
||||
{
|
||||
aabb->Merge({xMax, yMax, xMin, yMin});
|
||||
}
|
||||
|
||||
TTableLayer::edges_insert_square(yMin, xMin, yMax, xMax, this, nullptr);
|
||||
|
||||
auto offset = 1.0f / InvT1Radius + pb::BallHalfRadius;
|
||||
XMin = xMin - offset;
|
||||
YMin = yMin - offset;
|
||||
XMax = xMax + offset;
|
||||
YMax = yMax + offset;
|
||||
}
|
||||
|
||||
void TFlipperEdge::set_control_points(float angle)
|
||||
{
|
||||
float sin, cos;
|
||||
maths::SinCos(angle, sin, cos);
|
||||
A1 = A1Src;
|
||||
A2 = A2Src;
|
||||
B1 = B1Src;
|
||||
B2 = B2Src;
|
||||
T1 = T1Src;
|
||||
maths::RotatePt(A1, sin, cos, RotOrigin);
|
||||
maths::RotatePt(A2, sin, cos, RotOrigin);
|
||||
maths::RotatePt(T1, sin, cos, RotOrigin);
|
||||
maths::RotatePt(B1, sin, cos, RotOrigin);
|
||||
maths::RotatePt(B2, sin, cos, RotOrigin);
|
||||
maths::line_init(LineA, A1.X, A1.Y, A2.X, A2.Y);
|
||||
maths::line_init(LineB, B1.X, B1.Y, B2.X, B2.Y);
|
||||
circlebase = {RotOrigin, CirclebaseRadiusSq};
|
||||
circleT1 = {T1, CircleT1RadiusSq};
|
||||
ControlPointDirtyFlag = false;
|
||||
}
|
||||
|
||||
float TFlipperEdge::flipper_angle_delta(float timeDelta)
|
||||
{
|
||||
if (FlipperFlag == MessageCode::TFlipperNull)
|
||||
return 0.0f;
|
||||
|
||||
const auto deltaAngle = MoveSpeed * timeDelta;
|
||||
if (std::fabs(deltaAngle) > AngleRemainder)
|
||||
return AngleDst - CurrentAngle;
|
||||
return deltaAngle;
|
||||
}
|
||||
|
||||
int TFlipperEdge::SetMotion(MessageCode code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::TFlipperExtend:
|
||||
AngleRemainder = std::fabs(AngleMax - CurrentAngle);
|
||||
AngleDst = AngleMax;
|
||||
MoveSpeed = ExtendSpeed;
|
||||
break;
|
||||
case MessageCode::TFlipperRetract:
|
||||
AngleRemainder = std::fabs(CurrentAngle);
|
||||
AngleDst = 0.0f;
|
||||
MoveSpeed = RetractSpeed;
|
||||
break;
|
||||
case MessageCode::Reset:
|
||||
AngleRemainder = 0.0f;
|
||||
AngleDst = 0.0f;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (AngleRemainder == 0.0f)
|
||||
code = MessageCode::TFlipperNull;
|
||||
|
||||
FlipperFlag = code;
|
||||
return static_cast<int>(code);
|
||||
}
|
||||
56
SpaceCadetPinball/TFlipperEdge.h
Normal file
56
SpaceCadetPinball/TFlipperEdge.h
Normal file
@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
#include "maths.h"
|
||||
#include "TEdgeSegment.h"
|
||||
#include "TPinballComponent.h"
|
||||
|
||||
class TPinballTable;
|
||||
|
||||
class TFlipperEdge : public TEdgeSegment
|
||||
{
|
||||
public:
|
||||
TFlipperEdge(TCollisionComponent* collComp, char* activeFlag, unsigned int collisionGroup, TPinballTable* table,
|
||||
vector3* origin, vector3* vecT1, vector3* vecT2, float extendSpeed, float retractSpeed, float collMult,
|
||||
float elasticity, float smoothness);
|
||||
void port_draw() override;
|
||||
float FindCollisionDistance(const ray_type& ray) override;
|
||||
void EdgeCollision(TBall* ball, float distance) override;
|
||||
void place_in_grid(RectF* aabb) override;
|
||||
void set_control_points(float angle);
|
||||
float flipper_angle_delta(float timeDelta);
|
||||
int SetMotion(MessageCode code);
|
||||
|
||||
MessageCode FlipperFlag{};
|
||||
float Elasticity;
|
||||
float Smoothness;
|
||||
vector2 RotOrigin{};
|
||||
float CirclebaseRadius;
|
||||
float CircleT1Radius;
|
||||
float CirclebaseRadiusSq;
|
||||
float CircleT1RadiusSq;
|
||||
float CirclebaseRadiusMSq;
|
||||
float CircleT1RadiusMSq;
|
||||
float AngleMax;
|
||||
float AngleRemainder{};
|
||||
float AngleDst;
|
||||
float CurrentAngle{};
|
||||
vector2 CollisionLinePerp{};
|
||||
vector2 A1Src{};
|
||||
vector2 A2Src{};
|
||||
vector2 B1Src{};
|
||||
vector2 B2Src{};
|
||||
float CollisionMult;
|
||||
vector2 T1Src{};
|
||||
vector2 T2Src{};
|
||||
float DistanceDiv, DistanceDivSq;
|
||||
vector2 CollisionDirection{};
|
||||
float ExtendSpeed;
|
||||
float RetractSpeed;
|
||||
float MoveSpeed;
|
||||
vector2 NextBallPosition{};
|
||||
vector2 A1, A2, B1, B2, T1;
|
||||
line_type LineA, LineB;
|
||||
circle_type circlebase, circleT1;
|
||||
float InvT1Radius;
|
||||
float YMin, YMax, XMin, XMax;
|
||||
bool ControlPointDirtyFlag{};
|
||||
};
|
||||
42
SpaceCadetPinball/TGate.cpp
Normal file
42
SpaceCadetPinball/TGate.cpp
Normal file
@ -0,0 +1,42 @@
|
||||
#include "pch.h"
|
||||
#include "TGate.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "render.h"
|
||||
|
||||
TGate::TGate(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
|
||||
{
|
||||
visualStruct visual{};
|
||||
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
SoundIndex4 = visual.SoundIndex4;
|
||||
SoundIndex3 = visual.SoundIndex3;
|
||||
ActiveFlag = 1;
|
||||
SpriteSet(0);
|
||||
control::handler(MessageCode::Reset, this);
|
||||
}
|
||||
|
||||
int TGate::Message(MessageCode code, float value)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::TGateDisable:
|
||||
ActiveFlag = 0;
|
||||
SpriteSet(-1);
|
||||
loader::play_sound(SoundIndex3, this, "TGate1");
|
||||
break;
|
||||
case MessageCode::Reset:
|
||||
case MessageCode::TGateEnable:
|
||||
ActiveFlag = 1;
|
||||
SpriteSet(0);
|
||||
if (code == MessageCode::TGateEnable)
|
||||
loader::play_sound(SoundIndex4, this, "TGate2");
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
control::handler(code, this);
|
||||
return 0;
|
||||
}
|
||||
13
SpaceCadetPinball/TGate.h
Normal file
13
SpaceCadetPinball/TGate.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
class TGate :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TGate(TPinballTable* table, int groupIndex);
|
||||
int Message(MessageCode code, float value) override;
|
||||
|
||||
int SoundIndex4;
|
||||
int SoundIndex3;
|
||||
};
|
||||
148
SpaceCadetPinball/THole.cpp
Normal file
148
SpaceCadetPinball/THole.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
#include "pch.h"
|
||||
#include "THole.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "pb.h"
|
||||
#include "TBall.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
#include "TTableLayer.h"
|
||||
|
||||
THole::THole(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false)
|
||||
{
|
||||
visualStruct visual{};
|
||||
circle_type circle{};
|
||||
|
||||
Unknown4 = 0.050000001f;
|
||||
MessageField = 0;
|
||||
Timer = 0;
|
||||
BallCapturedFlag = false;
|
||||
Unknown3 = loader::query_float_attribute(groupIndex, 0, 407, 0.25f);
|
||||
GravityMult = loader::query_float_attribute(groupIndex, 0, 701, 0.2f);
|
||||
GravityPull = *loader::query_float_attribute(groupIndex, 0, 305);
|
||||
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
Circle.Center.X = visual.FloatArr[0];
|
||||
Circle.Center.Y = visual.FloatArr[1];
|
||||
Circle.RadiusSq = *loader::query_float_attribute(groupIndex, 0, 306) * visual.FloatArr[2];
|
||||
if (Circle.RadiusSq == 0.0f)
|
||||
Circle.RadiusSq = 0.001f;
|
||||
|
||||
auto tCircle = new TCircle(this, &ActiveFlag, visual.CollisionGroup,
|
||||
reinterpret_cast<vector3*>(visual.FloatArr),
|
||||
Circle.RadiusSq);
|
||||
tCircle->place_in_grid(&AABB);
|
||||
EdgeList.push_back(tCircle);
|
||||
|
||||
ZSetValue = loader::query_float_attribute(groupIndex, 0, 408)[2];
|
||||
CollisionMask = static_cast<int>(floor(*loader::query_float_attribute(groupIndex, 0, 1304)));
|
||||
|
||||
// CollisionMask difference: 3DPB - value given as is, FT: mask = 1 << value
|
||||
if (pb::FullTiltMode)
|
||||
CollisionMask = 1 << CollisionMask;
|
||||
|
||||
Circle.RadiusSq = visual.FloatArr[2] * visual.FloatArr[2];
|
||||
circle.RadiusSq = Circle.RadiusSq;
|
||||
circle.Center.X = Circle.Center.X;
|
||||
circle.Center.Y = Circle.Center.Y;
|
||||
|
||||
Field.ActiveFlag = &ActiveFlag;
|
||||
Field.CollisionComp = this;
|
||||
Field.CollisionGroup = visual.CollisionGroup;
|
||||
TTableLayer::edges_insert_circle(&circle, nullptr, &Field);
|
||||
}
|
||||
|
||||
int THole::Message(MessageCode code, float value)
|
||||
{
|
||||
if (code == MessageCode::Reset && BallCapturedFlag)
|
||||
{
|
||||
if (Timer)
|
||||
timer::kill(Timer);
|
||||
Timer = 0;
|
||||
BallCapturedSecondStage = true;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void THole::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
|
||||
{
|
||||
if (!BallCapturedFlag)
|
||||
{
|
||||
BallCapturedSecondStage = false;
|
||||
Threshold = 1e9f;
|
||||
BallCapturedFlag = true;
|
||||
ball->CollisionComp = this;
|
||||
ball->Position.X = Circle.Center.X;
|
||||
ball->Position.Y = Circle.Center.Y;
|
||||
ball->Direction.Z = 0.0;
|
||||
ball->CollisionDisabledFlag = true;
|
||||
|
||||
// Ramp hole has no delay in FT.
|
||||
auto captureTime = pb::FullTiltMode ? 0 : 0.5f;
|
||||
Timer = timer::set(captureTime, this, TimerExpired);
|
||||
|
||||
if (!PinballTable->TiltLockFlag)
|
||||
{
|
||||
loader::play_sound(HardHitSoundId, ball, "THole1");
|
||||
control::handler(MessageCode::ControlBallCaptured, this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DefaultCollision(ball, nextPosition, direction);
|
||||
}
|
||||
}
|
||||
|
||||
int THole::FieldEffect(TBall* ball, vector2* vecDst)
|
||||
{
|
||||
int result;
|
||||
vector2 direction{};
|
||||
|
||||
if (BallCapturedFlag)
|
||||
{
|
||||
if (BallCapturedSecondStage)
|
||||
{
|
||||
ball->Direction.Z -= PinballTable->GravityDirVectMult * ball->TimeDelta * GravityMult;
|
||||
ball->Position.Z += ball->Direction.Z;
|
||||
if (ball->Position.Z <= ZSetValue)
|
||||
{
|
||||
BallCapturedFlag = false;
|
||||
BallCapturedSecondStage = false;
|
||||
ball->Position.Z = ZSetValue;
|
||||
ball->CollisionMask = CollisionMask;
|
||||
ball->CollisionComp = nullptr;
|
||||
ball->Direction = {};
|
||||
ball->Speed = 0.0f;
|
||||
loader::play_sound(SoftHitSoundId, ball, "THole2");
|
||||
control::handler(MessageCode::ControlBallReleased, this);
|
||||
}
|
||||
}
|
||||
result = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
direction.X = Circle.Center.X - ball->Position.X;
|
||||
direction.Y = Circle.Center.Y - ball->Position.Y;
|
||||
if (direction.X * direction.X + direction.Y * direction.Y <= Circle.RadiusSq)
|
||||
{
|
||||
maths::normalize_2d(direction);
|
||||
vecDst->X = direction.X * GravityPull - ball->Direction.X * ball->Speed;
|
||||
vecDst->Y = direction.Y * GravityPull - ball->Direction.Y * ball->Speed;
|
||||
result = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = 0;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void THole::TimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto hole = static_cast<THole*>(caller);
|
||||
hole->Timer = 0;
|
||||
hole->BallCapturedSecondStage = true;
|
||||
}
|
||||
29
SpaceCadetPinball/THole.h
Normal file
29
SpaceCadetPinball/THole.h
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include "TCircle.h"
|
||||
#include "TCollisionComponent.h"
|
||||
#include "TEdgeManager.h"
|
||||
|
||||
class THole :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
THole(TPinballTable* table, int groupIndex);
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
int FieldEffect(TBall* ball, vector2* vecDst) override;
|
||||
|
||||
static void TimerExpired(int timerId, void* caller);
|
||||
|
||||
bool BallCapturedFlag{};
|
||||
bool BallCapturedSecondStage{};
|
||||
int Timer;
|
||||
float Unknown3;
|
||||
float Unknown4;
|
||||
float GravityMult;
|
||||
float ZSetValue;
|
||||
int CollisionMask;
|
||||
float GravityPull;
|
||||
circle_type Circle{};
|
||||
field_effect_type Field{};
|
||||
};
|
||||
73
SpaceCadetPinball/TKickback.cpp
Normal file
73
SpaceCadetPinball/TKickback.cpp
Normal file
@ -0,0 +1,73 @@
|
||||
#include "pch.h"
|
||||
#include "TKickback.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "maths.h"
|
||||
#include "render.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TKickback::TKickback(TPinballTable* table, int groupIndex): TCollisionComponent(table, groupIndex, true)
|
||||
{
|
||||
MessageField = 0;
|
||||
Timer = 0;
|
||||
KickActiveFlag = 0;
|
||||
TimerTime = 0.7f;
|
||||
TimerTime2 = 0.1f;
|
||||
Threshold = 1000000000.0f;
|
||||
}
|
||||
|
||||
int TKickback::Message(MessageCode code, float value)
|
||||
{
|
||||
if ((code == MessageCode::SetTiltLock || code == MessageCode::Reset) && Timer)
|
||||
{
|
||||
timer::kill(Timer);
|
||||
SpriteSet(-1);
|
||||
Timer = 0;
|
||||
KickActiveFlag = 0;
|
||||
Threshold = 1000000000.0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TKickback::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge)
|
||||
{
|
||||
if (PinballTable->TiltLockFlag)
|
||||
{
|
||||
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, 0.0,
|
||||
Boost);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!KickActiveFlag)
|
||||
{
|
||||
Threshold = 1000000000.0;
|
||||
KickActiveFlag = 1;
|
||||
Timer = timer::set(TimerTime, this, TimerExpired);
|
||||
}
|
||||
if (DefaultCollision(ball, nextPosition, direction))
|
||||
KickActiveFlag = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void TKickback::TimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto kick = static_cast<TKickback*>(caller);
|
||||
|
||||
if (kick->KickActiveFlag)
|
||||
{
|
||||
kick->Threshold = 0.0;
|
||||
kick->Timer = timer::set(kick->TimerTime2, kick, TimerExpired);
|
||||
loader::play_sound(kick->HardHitSoundId, kick, "TKickback");
|
||||
kick->SpriteSet(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
kick->SpriteSet(0);
|
||||
kick->Timer = 0;
|
||||
control::handler(MessageCode::ControlTimerExpired, kick);
|
||||
}
|
||||
}
|
||||
19
SpaceCadetPinball/TKickback.h
Normal file
19
SpaceCadetPinball/TKickback.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
class TKickback :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TKickback(TPinballTable* table, int groupIndex);
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
|
||||
static void TimerExpired(int timerId, void* caller);
|
||||
|
||||
float TimerTime;
|
||||
float TimerTime2;
|
||||
int Timer;
|
||||
int KickActiveFlag;
|
||||
};
|
||||
169
SpaceCadetPinball/TKickout.cpp
Normal file
169
SpaceCadetPinball/TKickout.cpp
Normal file
@ -0,0 +1,169 @@
|
||||
#include "pch.h"
|
||||
#include "TKickout.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "pb.h"
|
||||
#include "TBall.h"
|
||||
#include "TCircle.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
#include "TTableLayer.h"
|
||||
|
||||
TKickout::TKickout(TPinballTable* table, int groupIndex, bool someFlag): TCollisionComponent(
|
||||
table, groupIndex, false)
|
||||
{
|
||||
visualStruct visual{};
|
||||
circle_type circle{};
|
||||
|
||||
NotSomeFlag = !someFlag;
|
||||
if (!someFlag)
|
||||
ActiveFlag = 0;
|
||||
TimerTime1 = 1.5;
|
||||
TimerTime2 = 0.05f;
|
||||
MessageField = 0;
|
||||
Timer = 0;
|
||||
BallCaputeredFlag = 0;
|
||||
FieldMult = *loader::query_float_attribute(groupIndex, 0, 305);
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
SoftHitSoundId = visual.SoftHitSoundId;
|
||||
HardHitSoundId = visual.Kicker.HardHitSoundId;
|
||||
|
||||
Circle.Center.X = visual.FloatArr[0];
|
||||
Circle.Center.Y = visual.FloatArr[1];
|
||||
Circle.RadiusSq = *loader::query_float_attribute(groupIndex, 0, 306) * visual.FloatArr[2];
|
||||
if (Circle.RadiusSq == 0.0f)
|
||||
Circle.RadiusSq = 0.001f;
|
||||
auto tCircle = new TCircle(this, &ActiveFlag, visual.CollisionGroup,
|
||||
reinterpret_cast<vector3*>(visual.FloatArr), Circle.RadiusSq);
|
||||
if (tCircle)
|
||||
{
|
||||
tCircle->place_in_grid(&AABB);
|
||||
EdgeList.push_back(tCircle);
|
||||
}
|
||||
|
||||
Circle.RadiusSq = visual.FloatArr[2] * visual.FloatArr[2];
|
||||
auto zAttr = loader::query_float_attribute(groupIndex, 0, 408);
|
||||
CollisionBallSetZ = pb::FullTiltMode && !pb::FullTiltDemoMode ? zAttr[3] : zAttr[2];
|
||||
ThrowSpeedMult2 = visual.Kicker.ThrowBallMult * 0.01f;
|
||||
BallThrowDirection = visual.Kicker.ThrowBallDirection;
|
||||
ThrowAngleMult = visual.Kicker.ThrowBallAngleMult;
|
||||
ThrowSpeedMult1 = visual.Kicker.Boost;
|
||||
|
||||
circle.RadiusSq = Circle.RadiusSq;
|
||||
circle.Center.X = Circle.Center.X;
|
||||
circle.Center.Y = Circle.Center.Y;
|
||||
Field.ActiveFlag = &ActiveFlag;
|
||||
Field.CollisionComp = this;
|
||||
Field.CollisionGroup = visual.CollisionGroup;
|
||||
TTableLayer::edges_insert_circle(&circle, nullptr, &Field);
|
||||
}
|
||||
|
||||
int TKickout::Message(MessageCode code, float value)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::TKickoutRestartTimer:
|
||||
if (BallCaputeredFlag)
|
||||
{
|
||||
if (value < 0.0f)
|
||||
value = TimerTime1;
|
||||
Timer = timer::set(value, this, TimerExpired);
|
||||
}
|
||||
break;
|
||||
case MessageCode::SetTiltLock:
|
||||
if (NotSomeFlag)
|
||||
ActiveFlag = 0;
|
||||
break;
|
||||
case MessageCode::Reset:
|
||||
if (BallCaputeredFlag)
|
||||
{
|
||||
if (Timer)
|
||||
timer::kill(Timer);
|
||||
TimerExpired(0, this);
|
||||
}
|
||||
if (NotSomeFlag)
|
||||
ActiveFlag = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TKickout::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
|
||||
{
|
||||
if (!BallCaputeredFlag)
|
||||
{
|
||||
Ball = ball;
|
||||
Threshold = 1000000000.0;
|
||||
BallCaputeredFlag = 1;
|
||||
ball->CollisionComp = this;
|
||||
ball->Position.X = Circle.Center.X;
|
||||
ball->Position.Y = Circle.Center.Y;
|
||||
OriginalBallZ = ball->Position.Z;
|
||||
ball->Position.Z = CollisionBallSetZ;
|
||||
ball->CollisionDisabledFlag = true;
|
||||
if (PinballTable->TiltLockFlag)
|
||||
{
|
||||
Message(MessageCode::TKickoutRestartTimer, 0.1f);
|
||||
}
|
||||
else
|
||||
{
|
||||
loader::play_sound(SoftHitSoundId, ball, "TKickout1");
|
||||
control::handler(MessageCode::ControlCollision, this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ball->Position.X = nextPosition->X;
|
||||
ball->Position.Y = nextPosition->Y;
|
||||
ball->RayMaxDistance -= distance;
|
||||
ball->not_again(edge);
|
||||
}
|
||||
}
|
||||
|
||||
int TKickout::FieldEffect(TBall* ball, vector2* dstVec)
|
||||
{
|
||||
vector2 direction{};
|
||||
|
||||
if (BallCaputeredFlag)
|
||||
return 0;
|
||||
direction.X = Circle.Center.X - ball->Position.X;
|
||||
direction.Y = Circle.Center.Y - ball->Position.Y;
|
||||
if (direction.Y * direction.Y + direction.X * direction.X > Circle.RadiusSq)
|
||||
return 0;
|
||||
maths::normalize_2d(direction);
|
||||
dstVec->X = direction.X * FieldMult - ball->Direction.X * ball->Speed;
|
||||
dstVec->Y = direction.Y * FieldMult - ball->Direction.Y * ball->Speed;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void TKickout::TimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto kick = static_cast<TKickout*>(caller);
|
||||
if (kick->BallCaputeredFlag)
|
||||
{
|
||||
kick->BallCaputeredFlag = 0;
|
||||
kick->Timer = timer::set(kick->TimerTime2, kick, ResetTimerExpired);
|
||||
if (kick->Ball)
|
||||
{
|
||||
loader::play_sound(kick->HardHitSoundId, kick->Ball, "TKickout2");
|
||||
kick->Ball->Position.Z = kick->OriginalBallZ;
|
||||
kick->Ball->throw_ball(&kick->BallThrowDirection, kick->ThrowAngleMult, kick->ThrowSpeedMult1,
|
||||
kick->ThrowSpeedMult2);
|
||||
kick->ActiveFlag = 0;
|
||||
kick->Ball = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TKickout::ResetTimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto kick = static_cast<TKickout*>(caller);
|
||||
if (!kick->NotSomeFlag)
|
||||
kick->ActiveFlag = 1;
|
||||
kick->Timer = 0;
|
||||
}
|
||||
34
SpaceCadetPinball/TKickout.h
Normal file
34
SpaceCadetPinball/TKickout.h
Normal file
@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
#include "maths.h"
|
||||
#include "TCollisionComponent.h"
|
||||
#include "TEdgeManager.h"
|
||||
|
||||
class TKickout :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TKickout(TPinballTable* table, int groupIndex, bool someFlag);
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
int FieldEffect(TBall* ball, vector2* vecDst) override;
|
||||
|
||||
static void TimerExpired(int timerId, void* caller);
|
||||
static void ResetTimerExpired(int timerId, void* caller);
|
||||
|
||||
int BallCaputeredFlag;
|
||||
int NotSomeFlag;
|
||||
int Timer;
|
||||
float TimerTime1;
|
||||
float TimerTime2;
|
||||
float CollisionBallSetZ;
|
||||
TBall* Ball{};
|
||||
float FieldMult;
|
||||
circle_type Circle{};
|
||||
float OriginalBallZ{};
|
||||
vector3 BallThrowDirection{};
|
||||
float ThrowAngleMult;
|
||||
float ThrowSpeedMult1;
|
||||
float ThrowSpeedMult2;
|
||||
field_effect_type Field{};
|
||||
};
|
||||
349
SpaceCadetPinball/TLight.cpp
Normal file
349
SpaceCadetPinball/TLight.cpp
Normal file
@ -0,0 +1,349 @@
|
||||
#include "pch.h"
|
||||
#include "TLight.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "render.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TLight::TLight(TPinballTable* table, int groupIndex) : TPinballComponent(table, groupIndex, true)
|
||||
{
|
||||
TimeoutTimer = 0;
|
||||
FlasherOnFlag = false;
|
||||
UndoOverrideTimer = 0;
|
||||
FlashTimer = 0;
|
||||
Reset();
|
||||
float* floatArr1 = loader::query_float_attribute(groupIndex, 0, 900);
|
||||
FlashDelay[0] = *floatArr1;
|
||||
SourceDelay[0] = *floatArr1;
|
||||
float* floatArr2 = loader::query_float_attribute(groupIndex, 0, 901);
|
||||
FlashDelay[1] = *floatArr2;
|
||||
SourceDelay[1] = *floatArr2;
|
||||
}
|
||||
|
||||
int TLight::Message(MessageCode code, float value)
|
||||
{
|
||||
int bmpIndex;
|
||||
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::Reset:
|
||||
Reset();
|
||||
for (auto index = 0; index < PinballTable->PlayerCount; ++index)
|
||||
{
|
||||
auto playerPtr = &PlayerData[index];
|
||||
playerPtr->FlasherOnFlag = FlasherOnFlag;
|
||||
playerPtr->LightOnBmpIndex = LightOnBmpIndex;
|
||||
playerPtr->LightOnFlag = LightOnFlag;
|
||||
playerPtr->MessageField = MessageField;
|
||||
}
|
||||
break;
|
||||
case MessageCode::PlayerChanged:
|
||||
{
|
||||
auto playerPtr = &PlayerData[PinballTable->CurrentPlayer];
|
||||
playerPtr->FlasherOnFlag = FlasherOnFlag;
|
||||
playerPtr->LightOnBmpIndex = LightOnBmpIndex;
|
||||
playerPtr->LightOnFlag = LightOnFlag;
|
||||
playerPtr->MessageField = MessageField;
|
||||
|
||||
Reset();
|
||||
|
||||
playerPtr = &PlayerData[static_cast<int>(floor(value))];
|
||||
FlasherOnFlag = playerPtr->FlasherOnFlag;
|
||||
LightOnBmpIndex = playerPtr->LightOnBmpIndex;
|
||||
LightOnFlag = playerPtr->LightOnFlag;
|
||||
MessageField = playerPtr->MessageField;
|
||||
if (LightOnBmpIndex)
|
||||
{
|
||||
Message(MessageCode::TLightSetOnStateBmpIndex, static_cast<float>(LightOnBmpIndex));
|
||||
}
|
||||
if (LightOnFlag)
|
||||
Message(MessageCode::TLightTurnOn, 0.0);
|
||||
if (FlasherOnFlag)
|
||||
Message(MessageCode::TLightFlasherStart, 0.0);
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightTurnOff:
|
||||
LightOnFlag = false;
|
||||
if (!FlasherOnFlag && !ToggledOffFlag && !ToggledOnFlag)
|
||||
SetSpriteBmp(BmpArr[0]);
|
||||
break;
|
||||
case MessageCode::TLightTurnOn:
|
||||
LightOnFlag = true;
|
||||
if (!FlasherOnFlag && !ToggledOffFlag && !ToggledOnFlag)
|
||||
SetSpriteBmp(BmpArr[1]);
|
||||
break;
|
||||
case MessageCode::TLightGetLightOnFlag:
|
||||
return LightOnFlag;
|
||||
case MessageCode::TLightGetFlasherOnFlag:
|
||||
return FlasherOnFlag;
|
||||
case MessageCode::TLightFlasherStart:
|
||||
schedule_timeout(0.0);
|
||||
if (!FlasherOnFlag || !FlashTimer)
|
||||
{
|
||||
FlasherOnFlag = true;
|
||||
ToggledOnFlag = false;
|
||||
ToggledOffFlag = false;
|
||||
TurnOffAfterFlashingFg = false;
|
||||
flasher_start(LightOnFlag);
|
||||
}
|
||||
break;
|
||||
case MessageCode::TLightApplyMultDelay:
|
||||
FlashDelay[0] = value * SourceDelay[0];
|
||||
FlashDelay[1] = value * SourceDelay[1];
|
||||
break;
|
||||
case MessageCode::TLightApplyDelay:
|
||||
FlashDelay[0] = SourceDelay[0];
|
||||
FlashDelay[1] = SourceDelay[1];
|
||||
break;
|
||||
case MessageCode::TLightFlasherStartTimed:
|
||||
if (!FlasherOnFlag)
|
||||
flasher_start(LightOnFlag);
|
||||
FlasherOnFlag = true;
|
||||
ToggledOnFlag = false;
|
||||
TurnOffAfterFlashingFg = false;
|
||||
ToggledOffFlag = false;
|
||||
schedule_timeout(value);
|
||||
break;
|
||||
case MessageCode::TLightTurnOffTimed:
|
||||
if (!ToggledOffFlag)
|
||||
{
|
||||
if (FlasherOnFlag)
|
||||
{
|
||||
flasher_stop(0);
|
||||
FlasherOnFlag = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetSpriteBmp(BmpArr[0]);
|
||||
}
|
||||
ToggledOffFlag = true;
|
||||
ToggledOnFlag = false;
|
||||
}
|
||||
schedule_timeout(value);
|
||||
break;
|
||||
case MessageCode::TLightTurnOnTimed:
|
||||
if (!ToggledOnFlag)
|
||||
{
|
||||
if (FlasherOnFlag)
|
||||
{
|
||||
flasher_stop(1);
|
||||
FlasherOnFlag = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetSpriteBmp(BmpArr[1]);
|
||||
}
|
||||
ToggledOnFlag = true;
|
||||
ToggledOffFlag = false;
|
||||
}
|
||||
schedule_timeout(value);
|
||||
break;
|
||||
case MessageCode::TLightSetOnStateBmpIndex:
|
||||
LightOnBmpIndex = Clamp(static_cast<int>(floor(value)), 0, static_cast<int>(ListBitmap->size()) - 1);
|
||||
BmpArr[0] = -1;
|
||||
BmpArr[1] = LightOnBmpIndex;
|
||||
if (!FlasherOnFlag)
|
||||
{
|
||||
if (ToggledOffFlag)
|
||||
bmpIndex = 0;
|
||||
else if (ToggledOnFlag)
|
||||
bmpIndex = 1;
|
||||
else
|
||||
bmpIndex = LightOnFlag;
|
||||
}
|
||||
else
|
||||
{
|
||||
bmpIndex = FlashLightOnFlag;
|
||||
}
|
||||
SetSpriteBmp(BmpArr[bmpIndex]);
|
||||
break;
|
||||
case MessageCode::TLightIncOnStateBmpIndex:
|
||||
bmpIndex = LightOnBmpIndex + 1;
|
||||
if (bmpIndex >= static_cast<int>(ListBitmap->size()))
|
||||
bmpIndex = static_cast<int>(ListBitmap->size()) - 1;
|
||||
Message(MessageCode::TLightSetOnStateBmpIndex, static_cast<float>(bmpIndex));
|
||||
break;
|
||||
case MessageCode::TLightDecOnStateBmpIndex:
|
||||
bmpIndex = LightOnBmpIndex - 1;
|
||||
if (bmpIndex < 0)
|
||||
bmpIndex = 0;
|
||||
Message(MessageCode::TLightSetOnStateBmpIndex, static_cast<float>(bmpIndex));
|
||||
break;
|
||||
case MessageCode::TLightResetTimed:
|
||||
if (TimeoutTimer)
|
||||
timer::kill(TimeoutTimer);
|
||||
TimeoutTimer = 0;
|
||||
if (FlasherOnFlag)
|
||||
flasher_stop(-1);
|
||||
FlasherOnFlag = false;
|
||||
ToggledOffFlag = false;
|
||||
ToggledOnFlag = false;
|
||||
SetSpriteBmp(BmpArr[LightOnFlag]);
|
||||
break;
|
||||
case MessageCode::TLightFlasherStartTimedThenStayOn:
|
||||
TurnOffAfterFlashingFg = false;
|
||||
if (UndoOverrideTimer)
|
||||
timer::kill(UndoOverrideTimer);
|
||||
UndoOverrideTimer = 0;
|
||||
Message(MessageCode::TLightTurnOn, 0.0);
|
||||
Message(MessageCode::TLightFlasherStartTimed, value);
|
||||
break;
|
||||
case MessageCode::TLightFlasherStartTimedThenStayOff:
|
||||
if (UndoOverrideTimer)
|
||||
timer::kill(UndoOverrideTimer);
|
||||
UndoOverrideTimer = 0;
|
||||
Message(MessageCode::TLightFlasherStartTimed, value);
|
||||
TurnOffAfterFlashingFg = true;
|
||||
break;
|
||||
case MessageCode::TLightToggleValue:
|
||||
Message(static_cast<int>(floor(value)) ? MessageCode::TLightTurnOn : MessageCode::TLightTurnOff, 0.0);
|
||||
return LightOnFlag;
|
||||
case MessageCode::TLightResetAndToggleValue:
|
||||
Message(MessageCode::TLightToggleValue, value);
|
||||
Message(MessageCode::TLightResetTimed, 0.0);
|
||||
return LightOnFlag;
|
||||
case MessageCode::TLightResetAndTurnOn:
|
||||
Message(MessageCode::TLightTurnOn, 0.0);
|
||||
Message(MessageCode::TLightResetTimed, 0.0);
|
||||
break;
|
||||
case MessageCode::TLightResetAndTurnOff:
|
||||
Message(MessageCode::TLightTurnOff, 0.0);
|
||||
Message(MessageCode::TLightResetTimed, 0.0);
|
||||
break;
|
||||
case MessageCode::TLightToggle:
|
||||
Message(MessageCode::TLightToggleValue, !LightOnFlag);
|
||||
return LightOnFlag;
|
||||
case MessageCode::TLightResetAndToggle:
|
||||
Message(MessageCode::TLightResetAndToggleValue, !LightOnFlag);
|
||||
return LightOnFlag;
|
||||
case MessageCode::TLightSetMessageField:
|
||||
MessageField = static_cast<int>(floor(value));
|
||||
break;
|
||||
case MessageCode::TLightFtTmpOverrideOn:
|
||||
case MessageCode::TLightFtTmpOverrideOff:
|
||||
// FT codes in negative to avoid overlap with 3DPB TLightGroup codes
|
||||
SpriteSet(BmpArr[code == MessageCode::TLightFtTmpOverrideOn]);
|
||||
if (UndoOverrideTimer)
|
||||
timer::kill(UndoOverrideTimer);
|
||||
UndoOverrideTimer = 0;
|
||||
if (value > 0)
|
||||
{
|
||||
TemporaryOverrideFlag = true;
|
||||
UndoOverrideTimer = timer::set(value, this, UndoTmpOverride);
|
||||
}
|
||||
break;
|
||||
case MessageCode::TLightFtResetOverride:
|
||||
if (UndoOverrideTimer)
|
||||
timer::kill(UndoOverrideTimer);
|
||||
UndoOverrideTimer = 0;
|
||||
TemporaryOverrideFlag = false;
|
||||
SpriteSet(PreviousBitmap);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TLight::Reset()
|
||||
{
|
||||
if (TimeoutTimer)
|
||||
timer::kill(TimeoutTimer);
|
||||
if (UndoOverrideTimer)
|
||||
timer::kill(UndoOverrideTimer);
|
||||
if (FlasherOnFlag)
|
||||
flasher_stop(-1);
|
||||
TimeoutTimer = 0;
|
||||
UndoOverrideTimer = 0;
|
||||
LightOnFlag = false;
|
||||
LightOnBmpIndex = 0;
|
||||
ToggledOffFlag = false;
|
||||
ToggledOnFlag = false;
|
||||
FlasherOnFlag = false;
|
||||
TemporaryOverrideFlag = false;
|
||||
TurnOffAfterFlashingFg = false;
|
||||
PreviousBitmap = -1;
|
||||
BmpArr[0] = -1;
|
||||
BmpArr[1] = 0;
|
||||
SetSpriteBmp(BmpArr[0]);
|
||||
MessageField = 0;
|
||||
}
|
||||
|
||||
void TLight::schedule_timeout(float time)
|
||||
{
|
||||
FlashDelay[0] = SourceDelay[0];
|
||||
FlashDelay[1] = SourceDelay[1];
|
||||
if (TimeoutTimer)
|
||||
timer::kill(TimeoutTimer);
|
||||
TimeoutTimer = 0;
|
||||
if (time > 0.0f)
|
||||
TimeoutTimer = timer::set(time, this, TimerExpired);
|
||||
}
|
||||
|
||||
void TLight::TimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto light = static_cast<TLight*>(caller);
|
||||
if (light->FlasherOnFlag)
|
||||
light->flasher_stop(-1);
|
||||
light->SetSpriteBmp(light->BmpArr[light->LightOnFlag]);
|
||||
light->ToggledOffFlag = false;
|
||||
light->ToggledOnFlag = false;
|
||||
light->FlasherOnFlag = false;
|
||||
light->TimeoutTimer = 0;
|
||||
if (light->TurnOffAfterFlashingFg)
|
||||
{
|
||||
light->TurnOffAfterFlashingFg = false;
|
||||
light->Message(MessageCode::TLightResetAndTurnOff, 0.0);
|
||||
}
|
||||
if (light->Control)
|
||||
control::handler(MessageCode::ControlTimerExpired, light);
|
||||
}
|
||||
|
||||
|
||||
void TLight::flasher_stop(int bmpIndex)
|
||||
{
|
||||
if (FlashTimer)
|
||||
timer::kill(FlashTimer);
|
||||
FlashTimer = 0;
|
||||
if (bmpIndex >= 0)
|
||||
{
|
||||
FlashLightOnFlag = bmpIndex;
|
||||
SetSpriteBmp(BmpArr[FlashLightOnFlag]);
|
||||
}
|
||||
}
|
||||
|
||||
void TLight::flasher_start(bool bmpIndex)
|
||||
{
|
||||
FlashLightOnFlag = bmpIndex;
|
||||
flasher_callback(0, this);
|
||||
}
|
||||
|
||||
void TLight::SetSpriteBmp(int index)
|
||||
{
|
||||
PreviousBitmap = index;
|
||||
if (!TemporaryOverrideFlag)
|
||||
SpriteSet(index);
|
||||
}
|
||||
|
||||
void TLight::flasher_callback(int timerId, void* caller)
|
||||
{
|
||||
auto light = static_cast<TLight*>(caller);
|
||||
light->FlashLightOnFlag ^= true;
|
||||
light->SetSpriteBmp(light->BmpArr[light->FlashLightOnFlag]);
|
||||
light->FlashTimer = timer::set(light->FlashDelay[light->FlashLightOnFlag], light, flasher_callback);
|
||||
}
|
||||
|
||||
void TLight::UndoTmpOverride(int timerId, void* caller)
|
||||
{
|
||||
auto light = static_cast<TLight*>(caller);
|
||||
light->Message(MessageCode::TLightFtResetOverride, 0.0f);
|
||||
}
|
||||
|
||||
bool TLight::light_on() const
|
||||
{
|
||||
return LightOnFlag || ToggledOnFlag || FlasherOnFlag;
|
||||
}
|
||||
46
SpaceCadetPinball/TLight.h
Normal file
46
SpaceCadetPinball/TLight.h
Normal file
@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#include "TPinballComponent.h"
|
||||
|
||||
struct TLight_player_backup
|
||||
{
|
||||
int MessageField;
|
||||
bool LightOnFlag;
|
||||
int LightOnBmpIndex;
|
||||
bool FlasherOnFlag;
|
||||
};
|
||||
|
||||
|
||||
class TLight :
|
||||
public TPinballComponent
|
||||
{
|
||||
public:
|
||||
TLight(TPinballTable* table, int groupIndex);
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Reset();
|
||||
void schedule_timeout(float time);
|
||||
void flasher_stop(int bmpIndex);
|
||||
void flasher_start(bool bmpIndex);
|
||||
void SetSpriteBmp(int index);
|
||||
bool light_on() const;
|
||||
|
||||
static void TimerExpired(int timerId, void* caller);
|
||||
static void flasher_callback(int timerId, void* caller);
|
||||
static void UndoTmpOverride(int timerId, void* caller);
|
||||
|
||||
int BmpArr[2]{-1};
|
||||
float FlashDelay[2]{};
|
||||
int FlashTimer;
|
||||
bool FlashLightOnFlag{};
|
||||
bool LightOnFlag{};
|
||||
bool FlasherOnFlag;
|
||||
bool ToggledOffFlag{};
|
||||
bool ToggledOnFlag{};
|
||||
bool TurnOffAfterFlashingFg{};
|
||||
int LightOnBmpIndex{};
|
||||
float SourceDelay[2]{};
|
||||
int TimeoutTimer;
|
||||
int UndoOverrideTimer;
|
||||
bool TemporaryOverrideFlag{};
|
||||
int PreviousBitmap = -1;
|
||||
TLight_player_backup PlayerData[4]{};
|
||||
};
|
||||
130
SpaceCadetPinball/TLightBargraph.cpp
Normal file
130
SpaceCadetPinball/TLightBargraph.cpp
Normal file
@ -0,0 +1,130 @@
|
||||
#include "pch.h"
|
||||
#include "TLightBargraph.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TLightBargraph::TLightBargraph(TPinballTable* table, int groupIndex) : TLightGroup(table, groupIndex)
|
||||
{
|
||||
TimerTimeArray = nullptr;
|
||||
TLightBargraph::Reset();
|
||||
if (groupIndex > 0)
|
||||
{
|
||||
float* floatArr = loader::query_float_attribute(groupIndex, 0, 904);
|
||||
if (floatArr)
|
||||
{
|
||||
auto count = 2 * List.size();
|
||||
TimerTimeArray = new float[count];
|
||||
if (TimerTimeArray)
|
||||
{
|
||||
for (auto i = 0u; i < count; ++floatArr)
|
||||
TimerTimeArray[i++] = *floatArr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TLightBargraph::~TLightBargraph()
|
||||
{
|
||||
delete[] TimerTimeArray;
|
||||
}
|
||||
|
||||
int TLightBargraph::Message(MessageCode code, float value)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::TLightGroupGetOnCount:
|
||||
return TimeIndex;
|
||||
case MessageCode::TLightGroupToggleSplitIndex:
|
||||
{
|
||||
if (TimerBargraph)
|
||||
{
|
||||
timer::kill(TimerBargraph);
|
||||
TimerBargraph = 0;
|
||||
}
|
||||
auto timeIndex = static_cast<int>(floor(value));
|
||||
auto maxCount = static_cast<int>(List.size()) * 2;
|
||||
if (timeIndex >= maxCount)
|
||||
timeIndex = maxCount - 1;
|
||||
if (timeIndex >= 0)
|
||||
{
|
||||
TLightGroup::Message(MessageCode::TLightGroupToggleSplitIndex, static_cast<float>(timeIndex / 2));
|
||||
if (!(timeIndex & 1))
|
||||
TLightGroup::Message(MessageCode::TLightGroupStartFlasher, 0.0);
|
||||
if (TimerTimeArray)
|
||||
TimerBargraph = timer::set(TimerTimeArray[timeIndex], this, BargraphTimerExpired);
|
||||
TimeIndex = timeIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
TLightGroup::Message(MessageCode::TLightResetAndTurnOff, 0.0);
|
||||
TimeIndex = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCode::SetTiltLock:
|
||||
Reset();
|
||||
break;
|
||||
case MessageCode::PlayerChanged:
|
||||
if (TimerBargraph)
|
||||
{
|
||||
timer::kill(TimerBargraph);
|
||||
TimerBargraph = 0;
|
||||
}
|
||||
PlayerTimerIndexBackup[PinballTable->CurrentPlayer] = TimeIndex;
|
||||
Reset();
|
||||
TimeIndex = PlayerTimerIndexBackup[static_cast<int>(floor(value))];
|
||||
if (TimeIndex)
|
||||
{
|
||||
TLightBargraph::Message(MessageCode::TLightGroupToggleSplitIndex, static_cast<float>(TimeIndex));
|
||||
}
|
||||
break;
|
||||
case MessageCode::Reset:
|
||||
{
|
||||
Reset();
|
||||
int* playerPtr = PlayerTimerIndexBackup;
|
||||
for (auto index = 0; index < PinballTable->PlayerCount; ++index)
|
||||
{
|
||||
*playerPtr = TimeIndex;
|
||||
|
||||
++playerPtr;
|
||||
}
|
||||
TLightGroup::Message(MessageCode::Reset, value);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
TLightGroup::Message(code, value);
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TLightBargraph::Reset()
|
||||
{
|
||||
if (TimerBargraph)
|
||||
{
|
||||
timer::kill(TimerBargraph);
|
||||
TimerBargraph = 0;
|
||||
}
|
||||
TimeIndex = 0;
|
||||
TLightGroup::Reset();
|
||||
}
|
||||
|
||||
void TLightBargraph::BargraphTimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto bar = static_cast<TLightBargraph*>(caller);
|
||||
bar->TimerBargraph = 0;
|
||||
if (bar->TimeIndex)
|
||||
{
|
||||
bar->Message(MessageCode::TLightGroupToggleSplitIndex, static_cast<float>(bar->TimeIndex - 1));
|
||||
control::handler(MessageCode::ControlTimerExpired, bar);
|
||||
}
|
||||
else
|
||||
{
|
||||
bar->Message(MessageCode::TLightResetAndTurnOff, 0.0);
|
||||
control::handler(MessageCode::TLightGroupCountdownEnded, bar);
|
||||
}
|
||||
}
|
||||
19
SpaceCadetPinball/TLightBargraph.h
Normal file
19
SpaceCadetPinball/TLightBargraph.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include "TLightGroup.h"
|
||||
|
||||
class TLightBargraph :
|
||||
public TLightGroup
|
||||
{
|
||||
public:
|
||||
TLightBargraph(TPinballTable* table, int groupIndex);
|
||||
~TLightBargraph() override;
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Reset() override;
|
||||
|
||||
static void BargraphTimerExpired(int timerId, void* caller);
|
||||
|
||||
float* TimerTimeArray;
|
||||
int TimerBargraph{};
|
||||
int TimeIndex{};
|
||||
int PlayerTimerIndexBackup[4]{};
|
||||
};
|
||||
464
SpaceCadetPinball/TLightGroup.cpp
Normal file
464
SpaceCadetPinball/TLightGroup.cpp
Normal file
@ -0,0 +1,464 @@
|
||||
#include "pch.h"
|
||||
#include "TLightGroup.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "timer.h"
|
||||
#include "TLight.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TLightGroup::TLightGroup(TPinballTable* table, int groupIndex) : TPinballComponent(table, groupIndex, false)
|
||||
{
|
||||
Timer = 0;
|
||||
NotifyTimer = 0;
|
||||
TLightGroup::Reset();
|
||||
if (groupIndex > 0)
|
||||
{
|
||||
int count;
|
||||
Timer1TimeDefault = *loader::query_float_attribute(groupIndex, 0, 903);
|
||||
int16_t* groupIndArr = loader::query_iattribute(groupIndex, 1027, &count);
|
||||
for (int index = 0; index < count; ++groupIndArr)
|
||||
{
|
||||
auto comp = dynamic_cast<TLight*>(table->find_component(*groupIndArr));
|
||||
if (comp)
|
||||
List.push_back(comp);
|
||||
++index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int TLightGroup::Message(MessageCode code, float value)
|
||||
{
|
||||
auto const count = static_cast<int>(List.size());
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::SetTiltLock:
|
||||
case MessageCode::GameOver:
|
||||
break;
|
||||
case MessageCode::PlayerChanged:
|
||||
{
|
||||
auto playerPtr = &PlayerData[PinballTable->CurrentPlayer];
|
||||
playerPtr->MessageField = MessageField;
|
||||
playerPtr->MessageField2 = MessageField2;
|
||||
playerPtr->Timer1Time = Timer1Time;
|
||||
|
||||
Reset();
|
||||
|
||||
playerPtr = &PlayerData[static_cast<int>(floor(value))];
|
||||
MessageField = playerPtr->MessageField;
|
||||
MessageField2 = playerPtr->MessageField2;
|
||||
Timer1Time = playerPtr->Timer1Time;
|
||||
if (!(MessageField == 0))
|
||||
TimerExpired(0, this);
|
||||
break;
|
||||
}
|
||||
case MessageCode::Reset:
|
||||
Reset();
|
||||
for (auto index = 0; index < PinballTable->PlayerCount; index++)
|
||||
{
|
||||
auto playerPtr = &PlayerData[index];
|
||||
playerPtr->MessageField = MessageField;
|
||||
playerPtr->MessageField2 = MessageField2;
|
||||
playerPtr->Timer1Time = Timer1Time;
|
||||
}
|
||||
break;
|
||||
case MessageCode::TLightGroupStepBackward:
|
||||
{
|
||||
auto lastLight = List.at(count - 1);
|
||||
if (lastLight->FlasherOnFlag || lastLight->ToggledOnFlag || lastLight->ToggledOffFlag)
|
||||
break;
|
||||
if (MessageField2 != MessageCode::TLightGroupNull)
|
||||
{
|
||||
TLightGroup::Message(MessageCode::TLightGroupReset, 0.0);
|
||||
}
|
||||
AnimationFlag = 1;
|
||||
MessageField2 = code;
|
||||
auto lastMessage = lastLight->MessageField;
|
||||
auto lastStatus = lastLight->LightOnFlag;
|
||||
for (auto index = count - 1; index > 0; --index)
|
||||
{
|
||||
auto lightCur = List.at(index);
|
||||
auto lightPrev = List.at(index - 1);
|
||||
lightCur->Message(lightPrev->LightOnFlag ? MessageCode::TLightTurnOn : MessageCode::TLightTurnOff, 0.0);
|
||||
lightCur->MessageField = lightPrev->MessageField;
|
||||
}
|
||||
auto firstLight = List.at(0);
|
||||
firstLight->Message(lastStatus ? MessageCode::TLightTurnOn : MessageCode::TLightTurnOff, 0.0);
|
||||
firstLight->MessageField = lastMessage;
|
||||
reschedule_animation(value);
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupStepForward:
|
||||
{
|
||||
auto lastLight = List.at(count - 1);
|
||||
if (lastLight->FlasherOnFlag || lastLight->ToggledOnFlag || lastLight->ToggledOffFlag)
|
||||
break;
|
||||
if (MessageField2 != MessageCode::TLightGroupNull)
|
||||
{
|
||||
TLightGroup::Message(MessageCode::TLightGroupReset, 0.0);
|
||||
}
|
||||
auto firstLight = List.at(0);
|
||||
AnimationFlag = 1;
|
||||
MessageField2 = code;
|
||||
auto firstMessage = firstLight->MessageField;
|
||||
auto firstStatus = firstLight->LightOnFlag;
|
||||
for (auto index = 0; index < count - 1; index++)
|
||||
{
|
||||
auto lightCur = List.at(index);
|
||||
auto lightNext = List.at(index + 1);
|
||||
lightCur->Message(lightNext->LightOnFlag ? MessageCode::TLightTurnOn : MessageCode::TLightTurnOff, 0.0);
|
||||
lightCur->MessageField = lightNext->MessageField;
|
||||
}
|
||||
lastLight->Message(firstStatus ? MessageCode::TLightTurnOn : MessageCode::TLightTurnOff, 0.0);
|
||||
lastLight->MessageField = firstMessage;
|
||||
reschedule_animation(value);
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupAnimationBackward:
|
||||
{
|
||||
if (AnimationFlag || MessageField2 == MessageCode::TLightGroupNull)
|
||||
start_animation();
|
||||
MessageField2 = code;
|
||||
AnimationFlag = 0;
|
||||
auto lastLight = List.at(count - 1);
|
||||
auto lastStatus = lastLight->ToggledOnFlag;
|
||||
for (auto i = count - 1; i > 0; --i)
|
||||
{
|
||||
auto lightCur = List.at(i);
|
||||
auto lightPrev = List.at(i - 1);
|
||||
lightCur->Message(lightPrev->ToggledOnFlag ? MessageCode::TLightTurnOnTimed : MessageCode::TLightTurnOffTimed, 0.0);
|
||||
}
|
||||
auto firstLight = List.at(0);
|
||||
firstLight->Message(lastStatus ? MessageCode::TLightTurnOnTimed : MessageCode::TLightTurnOffTimed, 0);
|
||||
reschedule_animation(value);
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupAnimationForward:
|
||||
{
|
||||
if (AnimationFlag || MessageField2 == MessageCode::TLightGroupNull)
|
||||
start_animation();
|
||||
MessageField2 = code;
|
||||
AnimationFlag = 0;
|
||||
auto firstLight = List.at(0);
|
||||
auto firstStatus = firstLight->ToggledOnFlag;
|
||||
for (auto i = 0; i < count - 1; i++)
|
||||
{
|
||||
auto lightCur = List.at(i);
|
||||
auto lightNext = List.at(i + 1);
|
||||
lightCur->Message(lightNext->ToggledOnFlag ? MessageCode::TLightTurnOnTimed : MessageCode::TLightTurnOffTimed, 0.0);
|
||||
}
|
||||
auto lastLight = List.at(count - 1);
|
||||
lastLight->Message(firstStatus ? MessageCode::TLightTurnOnTimed : MessageCode::TLightTurnOffTimed, 0);
|
||||
reschedule_animation(value);
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupLightShowAnimation:
|
||||
{
|
||||
if (AnimationFlag || MessageField2 == MessageCode::TLightGroupNull)
|
||||
start_animation();
|
||||
MessageField2 = code;
|
||||
AnimationFlag = 0;
|
||||
for (auto light : List)
|
||||
{
|
||||
if (rand() % 100 > 70)
|
||||
{
|
||||
auto randVal = RandFloat() * value * 3.0f + 0.1f;
|
||||
light->Message(MessageCode::TLightTurnOnTimed, randVal);
|
||||
}
|
||||
}
|
||||
reschedule_animation(value);
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupGameOverAnimation:
|
||||
{
|
||||
if (AnimationFlag || MessageField2 == MessageCode::TLightGroupNull)
|
||||
start_animation();
|
||||
MessageField2 = code;
|
||||
AnimationFlag = 0;
|
||||
for (auto light : List)
|
||||
{
|
||||
auto randVal = static_cast<float>(rand() % 100 > 70);
|
||||
light->Message(MessageCode::TLightResetAndToggleValue, randVal);
|
||||
}
|
||||
reschedule_animation(value);
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupRandomAnimationSaturation:
|
||||
{
|
||||
auto noBmpInd1Count = 0;
|
||||
for (auto light : List)
|
||||
{
|
||||
if (!light->LightOnFlag)
|
||||
++noBmpInd1Count;
|
||||
}
|
||||
if (!noBmpInd1Count)
|
||||
break;
|
||||
|
||||
auto randModCount = rand() % noBmpInd1Count;
|
||||
for (auto it = List.rbegin(); it != List.rend(); ++it)
|
||||
{
|
||||
auto light = *it;
|
||||
if (!light->LightOnFlag && randModCount-- == 0)
|
||||
{
|
||||
light->Message(MessageCode::TLightTurnOn, 0.0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (MessageField2 != MessageCode::TLightGroupNull)
|
||||
start_animation();
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupRandomAnimationDesaturation:
|
||||
{
|
||||
auto bmpInd1Count = 0;
|
||||
for (auto light : List)
|
||||
{
|
||||
if (light->LightOnFlag)
|
||||
++bmpInd1Count;
|
||||
}
|
||||
if (!bmpInd1Count)
|
||||
break;
|
||||
|
||||
auto randModCount = rand() % bmpInd1Count;
|
||||
for (auto it = List.rbegin(); it != List.rend(); ++it)
|
||||
{
|
||||
auto light = *it;
|
||||
if (light->LightOnFlag && randModCount-- == 0)
|
||||
{
|
||||
light->Message(MessageCode::TLightTurnOff, 0.0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (MessageField2 != MessageCode::TLightGroupNull)
|
||||
start_animation();
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupOffsetAnimationForward:
|
||||
{
|
||||
auto index = next_light_up();
|
||||
if (index < 0)
|
||||
break;
|
||||
List.at(index)->Message(MessageCode::TLightTurnOn, 0.0);
|
||||
if (MessageField2 != MessageCode::TLightGroupNull)
|
||||
start_animation();
|
||||
return 1;
|
||||
}
|
||||
case MessageCode::TLightGroupOffsetAnimationBackward:
|
||||
{
|
||||
auto index = next_light_down();
|
||||
if (index < 0)
|
||||
break;
|
||||
List.at(index)->Message(MessageCode::TLightTurnOff, 0.0);
|
||||
if (MessageField2 != MessageCode::TLightGroupNull)
|
||||
start_animation();
|
||||
return 1;
|
||||
}
|
||||
case MessageCode::TLightGroupReset:
|
||||
{
|
||||
if (Timer)
|
||||
timer::kill(Timer);
|
||||
Timer = 0;
|
||||
if (MessageField2 == MessageCode::TLightGroupAnimationBackward ||
|
||||
MessageField2 == MessageCode::TLightGroupAnimationForward || MessageField2 == MessageCode::TLightGroupLightShowAnimation)
|
||||
TLightGroup::Message(MessageCode::TLightResetTimed, 0.0);
|
||||
MessageField2 = MessageCode::TLightGroupNull;
|
||||
AnimationFlag = 0;
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupTurnOnAtIndex:
|
||||
{
|
||||
auto index = static_cast<int>(floor(value));
|
||||
if (index >= count || index < 0)
|
||||
break;
|
||||
|
||||
auto light = List.at(index);
|
||||
light->Message(MessageCode::TLightTurnOn, 0.0);
|
||||
if (MessageField2 != MessageCode::TLightGroupNull)
|
||||
start_animation();
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupTurnOffAtIndex:
|
||||
{
|
||||
auto index = static_cast<int>(floor(value));
|
||||
if (index >= count || index < 0)
|
||||
break;
|
||||
|
||||
auto light = List.at(index);
|
||||
light->Message(MessageCode::TLightTurnOff, 0.0);
|
||||
if (MessageField2 != MessageCode::TLightGroupNull)
|
||||
start_animation();
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupGetOnCount:
|
||||
{
|
||||
auto bmp1Count = 0;
|
||||
for (auto light : List)
|
||||
{
|
||||
if (light->LightOnFlag)
|
||||
++bmp1Count;
|
||||
}
|
||||
return bmp1Count;
|
||||
}
|
||||
case MessageCode::TLightGroupGetLightCount:
|
||||
return count;
|
||||
case MessageCode::TLightGroupGetMessage2:
|
||||
return static_cast<int>(MessageField2);
|
||||
case MessageCode::TLightGroupGetAnimationFlag:
|
||||
return AnimationFlag;
|
||||
case MessageCode::TLightGroupResetAndTurnOn:
|
||||
{
|
||||
auto index = next_light_up();
|
||||
if (index < 0)
|
||||
break;
|
||||
if (MessageField2 != MessageCode::TLightGroupNull || AnimationFlag)
|
||||
TLightGroup::Message(MessageCode::TLightGroupReset, 0.0);
|
||||
List.at(index)->Message(MessageCode::TLightFlasherStartTimedThenStayOn, value);
|
||||
return 1;
|
||||
}
|
||||
case MessageCode::TLightGroupResetAndTurnOff:
|
||||
{
|
||||
auto index = next_light_down();
|
||||
if (index < 0)
|
||||
break;
|
||||
if (MessageField2 != MessageCode::TLightGroupNull || AnimationFlag)
|
||||
TLightGroup::Message(MessageCode::TLightGroupReset, 0.0);
|
||||
List.at(index)->Message(MessageCode::TLightFlasherStartTimedThenStayOff, value);
|
||||
return 1;
|
||||
}
|
||||
case MessageCode::TLightGroupRestartNotifyTimer:
|
||||
if (NotifyTimer)
|
||||
timer::kill(NotifyTimer);
|
||||
NotifyTimer = 0;
|
||||
if (value > 0.0f)
|
||||
NotifyTimer = timer::set(value, this, NotifyTimerExpired);
|
||||
break;
|
||||
case MessageCode::TLightGroupFlashWhenOn:
|
||||
{
|
||||
for (auto it = List.rbegin(); it != List.rend(); ++it)
|
||||
{
|
||||
auto light = *it;
|
||||
if (light->LightOnFlag)
|
||||
{
|
||||
light->Message(MessageCode::TLightTurnOff, 0.0);
|
||||
light->Message(MessageCode::TLightFlasherStartTimedThenStayOff, value);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupToggleSplitIndex:
|
||||
{
|
||||
control::handler(code, this);
|
||||
auto index = static_cast<int>(floor(value));
|
||||
if (index >= 0 && index < count)
|
||||
{
|
||||
// Turn off lights (index, end]
|
||||
for (auto i = count - 1; i > index; i--)
|
||||
{
|
||||
List.at(i)->Message(MessageCode::TLightResetAndTurnOff, 0.0);
|
||||
}
|
||||
|
||||
// Turn on lights [begin, index]
|
||||
for (auto i = index; i >= 0; i--)
|
||||
{
|
||||
List.at(i)->Message(MessageCode::TLightResetAndTurnOn, 0.0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCode::TLightGroupStartFlasher:
|
||||
{
|
||||
auto index = next_light_down();
|
||||
if (index >= 0)
|
||||
{
|
||||
List.at(index)->Message(MessageCode::TLightFlasherStart, 0.0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
for (auto it = List.rbegin(); it != List.rend(); ++it)
|
||||
{
|
||||
(*it)->Message(code, value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TLightGroup::Reset()
|
||||
{
|
||||
if (Timer)
|
||||
timer::kill(Timer);
|
||||
Timer = 0;
|
||||
if (NotifyTimer)
|
||||
timer::kill(NotifyTimer);
|
||||
NotifyTimer = 0;
|
||||
MessageField2 = MessageCode::TLightGroupNull;
|
||||
AnimationFlag = 0;
|
||||
Timer1Time = Timer1TimeDefault;
|
||||
}
|
||||
|
||||
void TLightGroup::reschedule_animation(float time)
|
||||
{
|
||||
if (Timer)
|
||||
timer::kill(Timer);
|
||||
Timer = 0;
|
||||
if (time == 0)
|
||||
{
|
||||
MessageField2 = MessageCode::TLightGroupNull;
|
||||
AnimationFlag = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
Timer1Time = time > 0.0f ? time : Timer1TimeDefault;
|
||||
Timer = timer::set(Timer1Time, this, TimerExpired);
|
||||
}
|
||||
|
||||
void TLightGroup::start_animation()
|
||||
{
|
||||
for (auto it = List.rbegin(); it != List.rend(); ++it)
|
||||
{
|
||||
auto light = *it;
|
||||
if (light->LightOnFlag)
|
||||
light->Message(MessageCode::TLightTurnOnTimed, 0.0);
|
||||
else
|
||||
light->Message(MessageCode::TLightTurnOffTimed, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
int TLightGroup::next_light_up()
|
||||
{
|
||||
for (auto index = 0u; index < List.size(); ++index)
|
||||
{
|
||||
if (!List[index]->LightOnFlag)
|
||||
return static_cast<int>(index);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int TLightGroup::next_light_down()
|
||||
{
|
||||
for (auto index = static_cast<int>(List.size()) - 1; index >= 0; --index)
|
||||
{
|
||||
if (List.at(index)->LightOnFlag)
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void TLightGroup::TimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto group = static_cast<TLightGroup*>(caller);
|
||||
group->Timer = 0;
|
||||
group->Message(group->MessageField2, group->Timer1Time);
|
||||
}
|
||||
|
||||
void TLightGroup::NotifyTimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto group = static_cast<TLightGroup*>(caller);
|
||||
group->NotifyTimer = 0;
|
||||
control::handler(MessageCode::ControlNotifyTimerExpired, group);
|
||||
}
|
||||
40
SpaceCadetPinball/TLightGroup.h
Normal file
40
SpaceCadetPinball/TLightGroup.h
Normal file
@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include "TPinballComponent.h"
|
||||
|
||||
|
||||
class TLight;
|
||||
|
||||
struct TLightGroup_player_backup
|
||||
{
|
||||
int MessageField;
|
||||
MessageCode MessageField2;
|
||||
float Timer1Time;
|
||||
int Unknown3;
|
||||
};
|
||||
|
||||
|
||||
class TLightGroup :
|
||||
public TPinballComponent
|
||||
{
|
||||
public:
|
||||
TLightGroup(TPinballTable* table, int groupIndex);
|
||||
~TLightGroup() override = default;
|
||||
int Message(MessageCode code, float value) override;
|
||||
virtual void Reset();
|
||||
void reschedule_animation(float time);
|
||||
void start_animation();
|
||||
int next_light_up();
|
||||
int next_light_down();
|
||||
|
||||
static void TimerExpired(int timerId, void* caller);
|
||||
static void NotifyTimerExpired(int timerId, void* caller);
|
||||
|
||||
std::vector<TLight*> List;
|
||||
float Timer1Time{};
|
||||
float Timer1TimeDefault;
|
||||
MessageCode MessageField2{};
|
||||
int AnimationFlag{};
|
||||
int NotifyTimer;
|
||||
int Timer;
|
||||
TLightGroup_player_backup PlayerData[4]{};
|
||||
};
|
||||
67
SpaceCadetPinball/TLightRollover.cpp
Normal file
67
SpaceCadetPinball/TLightRollover.cpp
Normal file
@ -0,0 +1,67 @@
|
||||
#include "pch.h"
|
||||
#include "TLightRollover.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "render.h"
|
||||
#include "TBall.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TLightRollover::TLightRollover(TPinballTable* table, int groupIndex) : TRollover(table, groupIndex, false)
|
||||
{
|
||||
RolloverFlag = 0;
|
||||
Timer = 0;
|
||||
SpriteSet(-1);
|
||||
build_walls(groupIndex);
|
||||
FloatArr = *loader::query_float_attribute(groupIndex, 0, 407);
|
||||
}
|
||||
|
||||
int TLightRollover::Message(MessageCode code, float value)
|
||||
{
|
||||
if (code == MessageCode::Reset)
|
||||
{
|
||||
ActiveFlag = 1;
|
||||
RolloverFlag = 0;
|
||||
if (Timer)
|
||||
timer::kill(Timer);
|
||||
Timer = 0;
|
||||
SpriteSet(-1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TLightRollover::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge)
|
||||
{
|
||||
ball->Position.X = nextPosition->X;
|
||||
ball->Position.Y = nextPosition->Y;
|
||||
ball->RayMaxDistance -= distance;
|
||||
ball->not_again(edge);
|
||||
if (!PinballTable->TiltLockFlag)
|
||||
{
|
||||
if (RolloverFlag)
|
||||
{
|
||||
timer::set(0.1f, this, TimerExpired);
|
||||
ActiveFlag = 0;
|
||||
RolloverFlag = RolloverFlag == 0;
|
||||
if (Timer == 0)
|
||||
Timer = timer::set(FloatArr, this, delay_expired);
|
||||
}
|
||||
else
|
||||
{
|
||||
loader::play_sound(SoftHitSoundId, this, "TLightRollover");
|
||||
control::handler(MessageCode::ControlCollision, this);
|
||||
RolloverFlag = RolloverFlag == 0;
|
||||
SpriteSet(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TLightRollover::delay_expired(int timerId, void* caller)
|
||||
{
|
||||
auto roll = static_cast<TLightRollover*>(caller);
|
||||
roll->SpriteSet(-1);
|
||||
roll->Timer = 0;
|
||||
}
|
||||
18
SpaceCadetPinball/TLightRollover.h
Normal file
18
SpaceCadetPinball/TLightRollover.h
Normal file
@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include "TRollover.h"
|
||||
|
||||
class TLightRollover :
|
||||
public TRollover
|
||||
{
|
||||
public:
|
||||
TLightRollover(TPinballTable* table, int groupIndex);
|
||||
~TLightRollover() override = default;
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
|
||||
static void delay_expired(int timerId, void* caller);
|
||||
|
||||
float FloatArr;
|
||||
int Timer;
|
||||
};
|
||||
131
SpaceCadetPinball/TLine.cpp
Normal file
131
SpaceCadetPinball/TLine.cpp
Normal file
@ -0,0 +1,131 @@
|
||||
#include "pch.h"
|
||||
#include "TLine.h"
|
||||
|
||||
#include "TTableLayer.h"
|
||||
|
||||
|
||||
TLine::TLine(TCollisionComponent* collCmp, char* activeFlag, unsigned int collisionGroup, float x0, float y0, float x1,
|
||||
float y1): TEdgeSegment(collCmp, activeFlag, collisionGroup)
|
||||
{
|
||||
X0 = x0;
|
||||
Y0 = y0;
|
||||
X1 = x1;
|
||||
Y1 = y1;
|
||||
maths::line_init(Line, x0, y0, x1, y1);
|
||||
}
|
||||
|
||||
TLine::TLine(TCollisionComponent* collCmp, char* activeFlag, unsigned int collisionGroup, const vector2& start,
|
||||
const vector2& end) : TEdgeSegment(collCmp, activeFlag, collisionGroup)
|
||||
{
|
||||
X0 = start.X;
|
||||
Y0 = start.Y;
|
||||
X1 = end.X;
|
||||
Y1 = end.Y;
|
||||
maths::line_init(Line, X0, Y0, X1, Y1);
|
||||
}
|
||||
|
||||
void TLine::Offset(float offset)
|
||||
{
|
||||
float offX = offset * Line.PerpendicularC.X;
|
||||
float offY = offset * Line.PerpendicularC.Y;
|
||||
|
||||
X0 += offX;
|
||||
Y0 += offY;
|
||||
X1 += offX;
|
||||
Y1 += offY;
|
||||
maths::line_init(Line, X0, Y0, X1, Y1);
|
||||
}
|
||||
|
||||
float TLine::FindCollisionDistance(const ray_type& ray)
|
||||
{
|
||||
return maths::ray_intersect_line(ray, Line);
|
||||
}
|
||||
|
||||
void TLine::EdgeCollision(TBall* ball, float distance)
|
||||
{
|
||||
CollisionComponent->Collision(
|
||||
ball,
|
||||
&Line.RayIntersect,
|
||||
&Line.PerpendicularC,
|
||||
distance,
|
||||
this);
|
||||
}
|
||||
|
||||
void TLine::place_in_grid(RectF* aabb)
|
||||
{
|
||||
if (aabb)
|
||||
{
|
||||
aabb->Merge({
|
||||
std::max(X0, X1), std::max(Y0, Y1),
|
||||
std::min(X0, X1), std::min(Y0, Y1)
|
||||
});
|
||||
}
|
||||
|
||||
auto edgeMan = TTableLayer::edge_manager;
|
||||
auto xBox0 = edgeMan->box_x(X0);
|
||||
auto yBox0 = edgeMan->box_y(Y0);
|
||||
auto xBox1 = edgeMan->box_x(X1);
|
||||
auto yBox1 = edgeMan->box_y(Y1);
|
||||
|
||||
int dirX = X0 >= X1 ? -1 : 1;
|
||||
int dirY = Y0 >= Y1 ? -1 : 1;
|
||||
|
||||
if (yBox0 == yBox1)
|
||||
{
|
||||
if (dirX == 1)
|
||||
{
|
||||
while (xBox0 <= xBox1)
|
||||
edgeMan->add_edge_to_box(xBox0++, yBox0, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
while (xBox0 >= xBox1)
|
||||
edgeMan->add_edge_to_box(xBox0--, yBox0, this);
|
||||
}
|
||||
}
|
||||
else if (xBox0 == xBox1)
|
||||
{
|
||||
if (dirY == 1)
|
||||
{
|
||||
while (yBox0 <= yBox1)
|
||||
edgeMan->add_edge_to_box(xBox0, yBox0++, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
while (yBox0 >= yBox1)
|
||||
edgeMan->add_edge_to_box(xBox0, yBox0--, this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
edgeMan->add_edge_to_box(xBox0, yBox0, this);
|
||||
|
||||
// Bresenham line formula: y = dYdX * (x - x0) + y0; dYdX = (y0 - y1) / (x0 - x1)
|
||||
auto dyDx = (Y0 - Y1) / (X0 - X1);
|
||||
// Precompute constant part: dYdX * (-x0) + y0
|
||||
auto precomp = -X0 * dyDx + Y0;
|
||||
// X and Y indexes are offset by one when going forwards, not sure why
|
||||
auto xBias = dirX == 1 ? 1 : 0, yBias = dirY == 1 ? 1 : 0;
|
||||
|
||||
for (auto indexX = xBox0, indexY = yBox0; indexX != xBox1 || indexY != yBox1;)
|
||||
{
|
||||
// Calculate y from indexY and from line formula
|
||||
auto yDiscrete = (indexY + yBias) * edgeMan->AdvanceY + edgeMan->MinY;
|
||||
auto ylinear = ((indexX + xBias) * edgeMan->AdvanceX + edgeMan->MinX) * dyDx + precomp;
|
||||
if (dirY == 1 ? ylinear >= yDiscrete : ylinear <= yDiscrete)
|
||||
{
|
||||
// Advance indexY when discrete value is ahead/behind
|
||||
// Advance indexX when discrete value matches linear value
|
||||
indexY += dirY;
|
||||
if (ylinear == yDiscrete)
|
||||
indexX += dirX;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Advance indexX otherwise
|
||||
indexX += dirX;
|
||||
}
|
||||
edgeMan->add_edge_to_box(indexX, indexY, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
SpaceCadetPinball/TLine.h
Normal file
17
SpaceCadetPinball/TLine.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include "maths.h"
|
||||
#include "TEdgeSegment.h"
|
||||
|
||||
class TLine :
|
||||
public TEdgeSegment
|
||||
{
|
||||
public:
|
||||
line_type Line{};
|
||||
float X0, Y0, X1, Y1;
|
||||
TLine(TCollisionComponent* collCmp, char* activeFlag, unsigned int collisionGroup, float x0, float y0, float x1, float y1);
|
||||
TLine(TCollisionComponent* collCmp, char* activeFlag, unsigned int collisionGroup, const vector2& start, const vector2& end);
|
||||
void Offset(float offset);
|
||||
float FindCollisionDistance(const ray_type& ray) override;
|
||||
void EdgeCollision(TBall* ball, float distance) override;
|
||||
void place_in_grid(RectF* aabb) override;
|
||||
};
|
||||
74
SpaceCadetPinball/TOneway.cpp
Normal file
74
SpaceCadetPinball/TOneway.cpp
Normal file
@ -0,0 +1,74 @@
|
||||
#include "pch.h"
|
||||
#include "TOneway.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "TBall.h"
|
||||
#include "TLine.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TOneway::TOneway(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false)
|
||||
{
|
||||
visualStruct visual{};
|
||||
vector2 linePt1{}, linePt2{};
|
||||
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
if (visual.FloatArrCount == 2)
|
||||
{
|
||||
linePt2.X = visual.FloatArr[0];
|
||||
linePt2.Y = visual.FloatArr[1];
|
||||
linePt1.X = visual.FloatArr[2];
|
||||
linePt1.Y = visual.FloatArr[3];
|
||||
|
||||
auto line = new TLine(this, &ActiveFlag, visual.CollisionGroup, linePt2, linePt1);
|
||||
if (line)
|
||||
{
|
||||
line->Offset(table->CollisionCompOffset);
|
||||
line->place_in_grid(&AABB);
|
||||
EdgeList.push_back(line);
|
||||
}
|
||||
|
||||
line = new TLine(this, &ActiveFlag, visual.CollisionGroup, linePt1, linePt2);
|
||||
Line = line;
|
||||
if (line)
|
||||
{
|
||||
line->Offset(-table->CollisionCompOffset * 0.8f);
|
||||
Line->place_in_grid(&AABB);
|
||||
EdgeList.push_back(Line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TOneway::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
|
||||
{
|
||||
if (edge == Line)
|
||||
{
|
||||
ball->not_again(edge);
|
||||
ball->Position.X = nextPosition->X;
|
||||
ball->Position.Y = nextPosition->Y;
|
||||
ball->RayMaxDistance -= distance;
|
||||
if (!PinballTable->TiltLockFlag)
|
||||
{
|
||||
if (HardHitSoundId)
|
||||
loader::play_sound(HardHitSoundId, ball, "TOneway1");
|
||||
control::handler(MessageCode::ControlCollision, this);
|
||||
}
|
||||
}
|
||||
else if (PinballTable->TiltLockFlag)
|
||||
{
|
||||
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, 1000000000.0, 0.0);
|
||||
}
|
||||
else if (maths::basic_collision(
|
||||
ball,
|
||||
nextPosition,
|
||||
direction,
|
||||
Elasticity,
|
||||
Smoothness,
|
||||
Threshold,
|
||||
Boost) > 0.2f)
|
||||
{
|
||||
if (SoftHitSoundId)
|
||||
loader::play_sound(SoftHitSoundId, ball, "TOneway2");
|
||||
}
|
||||
}
|
||||
15
SpaceCadetPinball/TOneway.h
Normal file
15
SpaceCadetPinball/TOneway.h
Normal file
@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
class TLine;
|
||||
|
||||
class TOneway : public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TOneway(TPinballTable* table, int groupIndex);
|
||||
~TOneway() override = default;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
|
||||
TLine* Line;
|
||||
};
|
||||
165
SpaceCadetPinball/TPinballComponent.cpp
Normal file
165
SpaceCadetPinball/TPinballComponent.cpp
Normal file
@ -0,0 +1,165 @@
|
||||
#include "pch.h"
|
||||
#include "TPinballComponent.h"
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "proj.h"
|
||||
#include "render.h"
|
||||
#include "TPinballTable.h"
|
||||
#include "TTableLayer.h"
|
||||
|
||||
TPinballComponent::TPinballComponent(TPinballTable* table, int groupIndex, bool loadVisuals)
|
||||
{
|
||||
visualStruct visual{};
|
||||
|
||||
MessageField = 0;
|
||||
UnusedBaseFlag = 0;
|
||||
ActiveFlag = 0;
|
||||
PinballTable = table;
|
||||
RenderSprite = nullptr;
|
||||
ListBitmap = nullptr;
|
||||
GroupName = nullptr;
|
||||
Control = nullptr;
|
||||
VisualPosNormX= -1.0f;
|
||||
VisualPosNormY = -1.0f;
|
||||
GroupIndex = groupIndex;
|
||||
if (table)
|
||||
table->ComponentList.push_back(this);
|
||||
if (groupIndex >= 0)
|
||||
GroupName = loader::query_name(groupIndex);
|
||||
|
||||
if (loadVisuals && groupIndex >= 0)
|
||||
{
|
||||
int visualCount = loader::query_visual_states(groupIndex);
|
||||
for (int index = 0; index < visualCount; ++index)
|
||||
{
|
||||
loader::query_visual(groupIndex, index, &visual);
|
||||
if (visual.Bitmap.Bmp)
|
||||
{
|
||||
assertm(visual.Bitmap.ZMap, "Bitmap/zMap pairing is mandatory");
|
||||
if (!ListBitmap)
|
||||
ListBitmap = new std::vector<SpriteData>();
|
||||
ListBitmap->push_back(visual.Bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
if (ListBitmap)
|
||||
{
|
||||
rectangle_type bmp1Rect{}, tmpRect{};
|
||||
const auto rootSprite = ListBitmap->at(0);
|
||||
const auto rootBmp = rootSprite.Bmp;
|
||||
|
||||
bmp1Rect.XPosition = rootBmp->XPosition - table->XOffset;
|
||||
bmp1Rect.YPosition = rootBmp->YPosition - table->YOffset;
|
||||
bmp1Rect.Width = rootBmp->Width;
|
||||
bmp1Rect.Height = rootBmp->Height;
|
||||
|
||||
for (auto index = 1u; index < ListBitmap->size(); index++)
|
||||
{
|
||||
auto bmp = ListBitmap->at(index).Bmp;
|
||||
tmpRect.XPosition = bmp->XPosition - table->XOffset;
|
||||
tmpRect.YPosition = bmp->YPosition - table->YOffset;
|
||||
tmpRect.Width = bmp->Width;
|
||||
tmpRect.Height = bmp->Height;
|
||||
maths::enclosing_box(bmp1Rect, tmpRect, bmp1Rect);
|
||||
}
|
||||
|
||||
RenderSprite = new render_sprite(
|
||||
VisualTypes::Sprite,
|
||||
rootBmp,
|
||||
rootSprite.ZMap,
|
||||
rootBmp->XPosition - table->XOffset,
|
||||
rootBmp->YPosition - table->YOffset,
|
||||
&bmp1Rect);
|
||||
|
||||
// Sound position = center of root visual, reverse-projected, normalized.
|
||||
auto& rect = RenderSprite->BmpRect;
|
||||
vector2i pos2D{ rect.XPosition + rect.Width / 2, rect.YPosition + rect.Height / 2 };
|
||||
auto pos3D = proj::ReverseXForm(pos2D);
|
||||
auto posNorm = TTableLayer::edge_manager->NormalizeBox(pos3D);
|
||||
VisualPosNormX = posNorm.X;
|
||||
VisualPosNormY = posNorm.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TPinballComponent::~TPinballComponent()
|
||||
{
|
||||
if (PinballTable)
|
||||
{
|
||||
// ComponentList contains one reference to each component.
|
||||
auto& components = PinballTable->ComponentList;
|
||||
auto position = std::find(components.begin(), components.end(), this);
|
||||
if (position != components.end())
|
||||
components.erase(position);
|
||||
}
|
||||
|
||||
delete ListBitmap;
|
||||
}
|
||||
|
||||
|
||||
int TPinballComponent::Message(MessageCode code, float value)
|
||||
{
|
||||
MessageField = static_cast<int>(code);
|
||||
if (code == MessageCode::Reset)
|
||||
MessageField = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TPinballComponent::port_draw()
|
||||
{
|
||||
}
|
||||
|
||||
int TPinballComponent::get_scoring(unsigned int index) const
|
||||
{
|
||||
return Control == nullptr || index >= Control->ScoreCount ? 0 : Control->Scores[index];
|
||||
}
|
||||
|
||||
vector2 TPinballComponent::get_coordinates()
|
||||
{
|
||||
return {VisualPosNormX, VisualPosNormY};
|
||||
}
|
||||
|
||||
void TPinballComponent::SpriteSet(int index) const
|
||||
{
|
||||
if (!ListBitmap)
|
||||
return;
|
||||
|
||||
int xPos, yPos;
|
||||
gdrv_bitmap8* bmp;
|
||||
zmap_header_type* zMap;
|
||||
if (index >= 0)
|
||||
{
|
||||
auto& spriteData = ListBitmap->at(index);
|
||||
bmp = spriteData.Bmp;
|
||||
zMap = spriteData.ZMap;
|
||||
xPos = bmp->XPosition - PinballTable->XOffset;
|
||||
yPos = bmp->YPosition - PinballTable->YOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
bmp = nullptr;
|
||||
zMap = nullptr;
|
||||
xPos = RenderSprite->BmpRect.XPosition;
|
||||
yPos = RenderSprite->BmpRect.YPosition;
|
||||
}
|
||||
|
||||
RenderSprite->set(bmp, zMap, xPos, yPos);
|
||||
}
|
||||
|
||||
void TPinballComponent::SpriteSetBall(int index, vector2i pos, float depth) const
|
||||
{
|
||||
if (ListBitmap)
|
||||
{
|
||||
gdrv_bitmap8* bmp = nullptr;
|
||||
if (index >= 0)
|
||||
{
|
||||
bmp = ListBitmap->at(index).Bmp;
|
||||
pos.X -= bmp->Width / 2;
|
||||
pos.Y -= bmp->Height / 2;
|
||||
}
|
||||
|
||||
RenderSprite->ball_set(bmp, depth, pos.X, pos.Y);
|
||||
}
|
||||
}
|
||||
158
SpaceCadetPinball/TPinballComponent.h
Normal file
158
SpaceCadetPinball/TPinballComponent.h
Normal file
@ -0,0 +1,158 @@
|
||||
#pragma once
|
||||
|
||||
struct SpriteData;
|
||||
struct vector2i;
|
||||
struct zmap_header_type;
|
||||
struct gdrv_bitmap8;
|
||||
struct render_sprite;
|
||||
struct component_control;
|
||||
struct vector2;
|
||||
class TPinballTable;
|
||||
|
||||
|
||||
enum class MessageCode
|
||||
{
|
||||
// Private codes <1000, different meaning for each component
|
||||
TFlipperNull = 0,
|
||||
TFlipperExtend = 1,
|
||||
TFlipperRetract = 2,
|
||||
|
||||
TLightTurnOff = 0,
|
||||
TLightTurnOn = 1,
|
||||
TLightGetLightOnFlag = 2,
|
||||
TLightGetFlasherOnFlag = 3,
|
||||
TLightFlasherStart = 4,
|
||||
TLightApplyMultDelay = 5,
|
||||
TLightApplyDelay = 6,
|
||||
TLightFlasherStartTimed = 7,
|
||||
TLightTurnOffTimed = 8,
|
||||
TLightTurnOnTimed = 9,
|
||||
TLightSetOnStateBmpIndex = 11,
|
||||
TLightIncOnStateBmpIndex = 12,
|
||||
TLightDecOnStateBmpIndex = 13,
|
||||
TLightResetTimed = 14,
|
||||
TLightFlasherStartTimedThenStayOn = 15,
|
||||
TLightFlasherStartTimedThenStayOff = 16,
|
||||
TLightToggleValue = 17,
|
||||
TLightResetAndToggleValue = 18,
|
||||
TLightResetAndTurnOn = 19,
|
||||
TLightResetAndTurnOff = 20,
|
||||
TLightToggle = 21,
|
||||
TLightResetAndToggle = 22,
|
||||
TLightSetMessageField = 23,
|
||||
TLightFtTmpOverrideOn = -24,
|
||||
TLightFtTmpOverrideOff = -25,
|
||||
TLightFtResetOverride = -26,
|
||||
|
||||
TLightGroupNull = 0,
|
||||
TLightGroupStepBackward = 24,
|
||||
TLightGroupStepForward = 25,
|
||||
TLightGroupAnimationBackward = 26,
|
||||
TLightGroupAnimationForward = 27,
|
||||
TLightGroupLightShowAnimation = 28,
|
||||
TLightGroupGameOverAnimation = 29,
|
||||
TLightGroupRandomAnimationSaturation = 30,
|
||||
TLightGroupRandomAnimationDesaturation = 31,
|
||||
TLightGroupOffsetAnimationForward = 32,
|
||||
TLightGroupOffsetAnimationBackward = 33,
|
||||
TLightGroupReset = 34,
|
||||
TLightGroupTurnOnAtIndex = 35,
|
||||
TLightGroupTurnOffAtIndex = 36,
|
||||
TLightGroupGetOnCount = 37,
|
||||
TLightGroupGetLightCount = 38,
|
||||
TLightGroupGetMessage2 = 39,
|
||||
TLightGroupGetAnimationFlag = 40,
|
||||
TLightGroupResetAndTurnOn = 41,
|
||||
TLightGroupResetAndTurnOff = 42,
|
||||
TLightGroupRestartNotifyTimer = 43,
|
||||
TLightGroupFlashWhenOn = 44,
|
||||
TLightGroupToggleSplitIndex = 45,
|
||||
TLightGroupStartFlasher = 46,
|
||||
TLightGroupCountdownEnded = 47,
|
||||
|
||||
TBumperSetBmpIndex = 11,
|
||||
TBumperIncBmpIndex = 12,
|
||||
TBumperDecBmpIndex = 13,
|
||||
|
||||
TComponentGroupResetNotifyTimer = 48,
|
||||
|
||||
TPopupTargetDisable = 49,
|
||||
TPopupTargetEnable = 50,
|
||||
|
||||
TBlockerDisable = 51,
|
||||
TBlockerEnable = 52,
|
||||
TBlockerRestartTimeout = 59,
|
||||
|
||||
TGateDisable = 53,
|
||||
TGateEnable = 54,
|
||||
|
||||
TKickoutRestartTimer = 55,
|
||||
|
||||
TSinkUnknown7 = 7,
|
||||
TSinkResetTimer = 56,
|
||||
|
||||
TSoloTargetDisable = 49,
|
||||
TSoloTargetEnable = 50,
|
||||
|
||||
TTimerResetTimer = 59,
|
||||
|
||||
ControlBallCaptured = 57,
|
||||
ControlBallReleased = 58,
|
||||
ControlTimerExpired = 60,
|
||||
ControlNotifyTimerExpired = 61,
|
||||
ControlSpinnerLoopReset = 62,
|
||||
ControlCollision = 63,
|
||||
ControlEnableMultiplier = 64,
|
||||
ControlDisableMultiplier = 65,
|
||||
ControlMissionComplete = 66,
|
||||
ControlMissionStarted = 67,
|
||||
|
||||
// Public codes 1000+, apply to all components
|
||||
LeftFlipperInputPressed = 1000,
|
||||
LeftFlipperInputReleased = 1001,
|
||||
RightFlipperInputPressed = 1002,
|
||||
RightFlipperInputReleased = 1003,
|
||||
PlungerInputPressed = 1004,
|
||||
PlungerInputReleased = 1005,
|
||||
Pause = 1008,
|
||||
Resume = 1009,
|
||||
LooseFocus = 1010,
|
||||
SetTiltLock = 1011,
|
||||
ClearTiltLock = 1012,
|
||||
StartGamePlayer1 = 1013,
|
||||
NewGame = 1014,
|
||||
PlungerFeedBall = 1015,
|
||||
PlungerStartFeedTimer = 1016,
|
||||
PlungerLaunchBall = 1017,
|
||||
PlungerRelaunchBall = 1018,
|
||||
PlayerChanged = 1020,
|
||||
SwitchToNextPlayer = 1021,
|
||||
GameOver = 1022,
|
||||
Reset = 1024,
|
||||
};
|
||||
|
||||
class TPinballComponent
|
||||
{
|
||||
public:
|
||||
TPinballComponent(TPinballTable* table, int groupIndex, bool loadVisuals);
|
||||
virtual ~TPinballComponent();
|
||||
virtual int Message(MessageCode code, float value);
|
||||
virtual void port_draw();
|
||||
int get_scoring(unsigned int index) const;
|
||||
virtual vector2 get_coordinates();
|
||||
void SpriteSet(int index) const;
|
||||
void SpriteSetBall(int index, vector2i pos, float depth) const;
|
||||
|
||||
char UnusedBaseFlag;
|
||||
char ActiveFlag;
|
||||
int MessageField;
|
||||
char* GroupName;
|
||||
component_control* Control;
|
||||
int GroupIndex;
|
||||
render_sprite* RenderSprite;
|
||||
TPinballTable* PinballTable;
|
||||
std::vector<SpriteData>* ListBitmap;
|
||||
private:
|
||||
float VisualPosNormX;
|
||||
float VisualPosNormY;
|
||||
};
|
||||
714
SpaceCadetPinball/TPinballTable.cpp
Normal file
714
SpaceCadetPinball/TPinballTable.cpp
Normal file
@ -0,0 +1,714 @@
|
||||
#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<TTextBox*>(find_component("info_text_box"));
|
||||
pb::MissTextBox = dynamic_cast<TTextBox*>(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<int>(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<float>(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<TPinballTable*>(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<TPinballTable*>(caller);
|
||||
table->LightShowTimer = 0;
|
||||
table->Message(MessageCode::StartGamePlayer1, 0.0);
|
||||
}
|
||||
|
||||
void TPinballTable::replay_timer_callback(int timerId, void* caller)
|
||||
{
|
||||
auto table = static_cast<TPinballTable*>(caller);
|
||||
table->ReplayActiveFlag = 0;
|
||||
table->ReplayTimer = 0;
|
||||
}
|
||||
|
||||
void TPinballTable::tilt_timeout(int timerId, void* caller)
|
||||
{
|
||||
auto table = static_cast<TPinballTable*>(caller);
|
||||
vector2 vec{};
|
||||
|
||||
table->TiltTimeoutTimer = 0;
|
||||
if (table->TiltLockFlag)
|
||||
{
|
||||
for (auto ball : table->BallList)
|
||||
{
|
||||
table->Drain->Collision(ball, &vec, &vec, 0.0, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
104
SpaceCadetPinball/TPinballTable.h
Normal file
104
SpaceCadetPinball/TPinballTable.h
Normal file
@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
|
||||
#include "maths.h"
|
||||
#include "TPinballComponent.h"
|
||||
|
||||
class TBall;
|
||||
struct scoreStruct;
|
||||
class TFlipper;
|
||||
class TPlunger;
|
||||
class TDrain;
|
||||
class TDemo;
|
||||
class TLightGroup;
|
||||
|
||||
struct score_struct_super
|
||||
{
|
||||
scoreStruct* ScoreStruct;
|
||||
int Score;
|
||||
int ScoreE9Part;
|
||||
int JackpotScore;
|
||||
int BallCount;
|
||||
int ExtraBalls;
|
||||
int BallLockedCounter;
|
||||
};
|
||||
|
||||
|
||||
class TPinballTable : public TPinballComponent
|
||||
{
|
||||
public:
|
||||
TPinballTable();
|
||||
~TPinballTable() override;
|
||||
TPinballComponent* find_component(LPCSTR componentName);
|
||||
TPinballComponent* find_component(int groupIndex);
|
||||
int AddScore(int score);
|
||||
void ChangeBallCount(int count);
|
||||
void tilt(float time);
|
||||
void port_draw() override;
|
||||
int Message(MessageCode code, float value) override;
|
||||
TBall* AddBall(vector2 position);
|
||||
int BallCountInRect(const RectF& rect);
|
||||
int BallCountInRect(const vector2& pos, float margin);
|
||||
|
||||
static void EndGame_timeout(int timerId, void* caller);
|
||||
static void LightShow_timeout(int timerId, void* caller);
|
||||
static void replay_timer_callback(int timerId, void* caller);
|
||||
static void tilt_timeout(int timerId, void* caller);
|
||||
|
||||
TFlipper* FlipperL;
|
||||
TFlipper* FlipperR;
|
||||
scoreStruct* CurScoreStruct;
|
||||
scoreStruct* ScoreBallcount;
|
||||
scoreStruct* ScorePlayerNumber1;
|
||||
int CheatsUsed{};
|
||||
int SoundIndex1{};
|
||||
int SoundIndex2{};
|
||||
int SoundIndex3{};
|
||||
int BallInDrainFlag;
|
||||
int CurScore{};
|
||||
int CurScoreE9{};
|
||||
int LightShowTimer;
|
||||
int EndGameTimeoutTimer;
|
||||
int TiltTimeoutTimer;
|
||||
score_struct_super PlayerScores[4]{};
|
||||
int PlayerCount;
|
||||
int CurrentPlayer;
|
||||
TPlunger* Plunger;
|
||||
TDrain* Drain;
|
||||
TDemo* Demo{};
|
||||
int XOffset{};
|
||||
int YOffset{};
|
||||
int Width{};
|
||||
int Height{};
|
||||
std::vector<TPinballComponent*> ComponentList;
|
||||
std::vector<TBall*> BallList;
|
||||
std::vector<TFlipper*> FlipperList;
|
||||
TLightGroup* LightGroup;
|
||||
float GravityDirVectMult{};
|
||||
float GravityAngleX{};
|
||||
float GravityAnglY{};
|
||||
float CollisionCompOffset{};
|
||||
vector2 PlungerPosition{};
|
||||
int ScoreMultiplier{};
|
||||
int ScoreAdded{};
|
||||
int ReflexShotScore{};
|
||||
int BonusScore{};
|
||||
bool BonusScoreFlag{};
|
||||
int JackpotScore{};
|
||||
bool JackpotScoreFlag{};
|
||||
int UnknownP71{};
|
||||
int BallCount{};
|
||||
int MaxBallCount;
|
||||
int ExtraBalls{};
|
||||
int MultiballCount{};
|
||||
int BallLockedCounter{};
|
||||
bool MultiballFlag;
|
||||
int UnknownP78{};
|
||||
int ReplayActiveFlag{};
|
||||
int ReplayTimer;
|
||||
int UnknownP81{};
|
||||
int UnknownP82{};
|
||||
int TiltLockFlag;
|
||||
|
||||
private:
|
||||
static int score_multipliers[5];
|
||||
};
|
||||
199
SpaceCadetPinball/TPlunger.cpp
Normal file
199
SpaceCadetPinball/TPlunger.cpp
Normal file
@ -0,0 +1,199 @@
|
||||
#include "pch.h"
|
||||
#include "TPlunger.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "maths.h"
|
||||
#include "pb.h"
|
||||
#include "render.h"
|
||||
#include "TBall.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TPlunger::TPlunger(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
|
||||
{
|
||||
visualStruct visual{};
|
||||
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
Boost = 0.0;
|
||||
BallFeedTimer_ = 0;
|
||||
PullbackTimer_ = 0;
|
||||
SoundIndexP1 = visual.SoundIndex4;
|
||||
SoundIndexP2 = visual.SoundIndex3;
|
||||
HardHitSoundId = visual.Kicker.HardHitSoundId;
|
||||
Threshold = 1000000000.0;
|
||||
|
||||
// FT:MaxPullback = 50; 3DPB: MaxPullback = 100, PullbackIncrement is floored.
|
||||
if (pb::FullTiltMode)
|
||||
{
|
||||
MaxPullback = 50;
|
||||
PullbackIncrement = MaxPullback / (ListBitmap->size() * 8.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
MaxPullback = 100;
|
||||
PullbackIncrement = std::floor(MaxPullback / (ListBitmap->size() * 8.0f));
|
||||
}
|
||||
|
||||
Elasticity = 0.5f;
|
||||
Smoothness = 0.5f;
|
||||
|
||||
PullbackDelay = 0.025f;
|
||||
float* floatArr = loader::query_float_attribute(groupIndex, 0, 601);
|
||||
table->PlungerPosition = {floatArr[0], floatArr[1]};
|
||||
}
|
||||
|
||||
void TPlunger::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
|
||||
{
|
||||
if (PinballTable->TiltLockFlag || SomeCounter > 0)
|
||||
{
|
||||
auto boost = RandFloat() * MaxPullback * 0.1f + MaxPullback;
|
||||
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, 0, boost);
|
||||
if (SomeCounter)
|
||||
SomeCounter--;
|
||||
Message(MessageCode::PlungerInputReleased, 0.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto boost = RandFloat() * Boost * 0.1f + Boost;
|
||||
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, Threshold, boost);
|
||||
}
|
||||
}
|
||||
|
||||
int TPlunger::Message(MessageCode code, float value)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::PlungerInputPressed:
|
||||
if (!PullbackStartedFlag && (!pb::FullTiltMode || PinballTable->MultiballCount > 0 && !PinballTable->
|
||||
TiltLockFlag))
|
||||
{
|
||||
PullbackStartedFlag = true;
|
||||
Boost = 0.0;
|
||||
Threshold = 1000000000.0;
|
||||
loader::play_sound(HardHitSoundId, this, "TPlunger1");
|
||||
PullbackTimer(0, this);
|
||||
}
|
||||
break;
|
||||
case MessageCode::PlungerFeedBall:
|
||||
{
|
||||
if (PinballTable->BallCountInRect(PinballTable->PlungerPosition, PinballTable->CollisionCompOffset * 1.2f))
|
||||
{
|
||||
timer::set(1.0f, this, BallFeedTimer);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto ball = PinballTable->AddBall(PinballTable->PlungerPosition);
|
||||
assertm(ball, "Failure to create ball in plunger");
|
||||
PinballTable->MultiballCount++;
|
||||
PinballTable->BallInDrainFlag = 0;
|
||||
pb::tilt_no_more();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCode::PlungerStartFeedTimer:
|
||||
timer::set(0.95999998f, this, BallFeedTimer);
|
||||
loader::play_sound(SoundIndexP1, this, "TPlunger2");
|
||||
break;
|
||||
case MessageCode::PlungerLaunchBall:
|
||||
PullbackStartedFlag = true;
|
||||
Boost = MaxPullback;
|
||||
Message(MessageCode::PlungerInputReleased, 0.0f);
|
||||
break;
|
||||
case MessageCode::PlungerRelaunchBall:
|
||||
SomeCounter++;
|
||||
timer::set(value, this, BallFeedTimer);
|
||||
loader::play_sound(SoundIndexP1, this, "TPlunger2_1");
|
||||
PullbackStartedFlag = true;
|
||||
PullbackTimer(0, this);
|
||||
break;
|
||||
case MessageCode::PlayerChanged:
|
||||
PullbackStartedFlag = false;
|
||||
Boost = 0.0f;
|
||||
Threshold = 1000000000.0f;
|
||||
SomeCounter = 0;
|
||||
timer::kill(BallFeedTimer);
|
||||
timer::kill(PullbackTimer);
|
||||
timer::kill(ReleasedTimer);
|
||||
break;
|
||||
case MessageCode::SetTiltLock:
|
||||
SomeCounter = 0;
|
||||
timer::kill(BallFeedTimer);
|
||||
break;
|
||||
case MessageCode::PlungerInputReleased:
|
||||
case MessageCode::Resume:
|
||||
case MessageCode::LooseFocus:
|
||||
if (PullbackStartedFlag && !SomeCounter)
|
||||
{
|
||||
PullbackStartedFlag = false;
|
||||
Threshold = 0.0;
|
||||
if (PullbackTimer_)
|
||||
timer::kill(PullbackTimer_);
|
||||
PullbackTimer_ = 0;
|
||||
loader::play_sound(SoundIndexP2, this, "TPlunger3");
|
||||
SpriteSet(0);
|
||||
timer::set(PullbackDelay, this, ReleasedTimer);
|
||||
}
|
||||
break;
|
||||
case MessageCode::Reset:
|
||||
{
|
||||
PullbackStartedFlag = false;
|
||||
Boost = 0.0f;
|
||||
Threshold = 1000000000.0f;
|
||||
SomeCounter = 0;
|
||||
|
||||
timer::kill(BallFeedTimer);
|
||||
timer::kill(PullbackTimer);
|
||||
timer::kill(ReleasedTimer);
|
||||
|
||||
SpriteSet(0);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
control::handler(code, this);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TPlunger::BallFeedTimer(int timerId, void* caller)
|
||||
{
|
||||
auto plunger = static_cast<TPlunger*>(caller);
|
||||
plunger->Message(MessageCode::PlungerFeedBall, 0.0);
|
||||
}
|
||||
|
||||
void TPlunger::PullbackTimer(int timerId, void* caller)
|
||||
{
|
||||
auto plunger = static_cast<TPlunger*>(caller);
|
||||
plunger->Boost += plunger->PullbackIncrement;
|
||||
if (plunger->Boost <= plunger->MaxPullback)
|
||||
{
|
||||
if (plunger->SomeCounter)
|
||||
{
|
||||
plunger->PullbackTimer_ = timer::set(plunger->PullbackDelay / 4.0f, plunger, PullbackTimer);
|
||||
}
|
||||
else
|
||||
{
|
||||
plunger->PullbackTimer_ = timer::set(plunger->PullbackDelay, plunger, PullbackTimer);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
plunger->PullbackTimer_ = 0;
|
||||
plunger->Boost = plunger->MaxPullback;
|
||||
}
|
||||
|
||||
int index = static_cast<int>(floor(
|
||||
static_cast<float>(plunger->ListBitmap->size() - 1) *
|
||||
(plunger->Boost / plunger->MaxPullback)));
|
||||
plunger->SpriteSet(index);
|
||||
}
|
||||
|
||||
void TPlunger::ReleasedTimer(int timerId, void* caller)
|
||||
{
|
||||
auto plunger = static_cast<TPlunger*>(caller);
|
||||
plunger->Threshold = 1000000000.0;
|
||||
plunger->Boost = 0.0;
|
||||
}
|
||||
27
SpaceCadetPinball/TPlunger.h
Normal file
27
SpaceCadetPinball/TPlunger.h
Normal file
@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
class TPlunger :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TPlunger(TPinballTable* table, int groupIndex);
|
||||
~TPlunger() override = default;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
int Message(MessageCode code, float value) override;
|
||||
|
||||
static void BallFeedTimer(int timerId, void* caller);
|
||||
static void PullbackTimer(int timerId, void* caller);
|
||||
static void ReleasedTimer(int timerId, void* caller);
|
||||
|
||||
int PullbackTimer_;
|
||||
int BallFeedTimer_;
|
||||
float MaxPullback;
|
||||
float PullbackIncrement;
|
||||
float PullbackDelay;
|
||||
int SoundIndexP1;
|
||||
int SoundIndexP2;
|
||||
bool PullbackStartedFlag{};
|
||||
int SomeCounter{};
|
||||
};
|
||||
88
SpaceCadetPinball/TPopupTarget.cpp
Normal file
88
SpaceCadetPinball/TPopupTarget.cpp
Normal file
@ -0,0 +1,88 @@
|
||||
#include "pch.h"
|
||||
#include "TPopupTarget.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "render.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TPopupTarget::TPopupTarget(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
|
||||
{
|
||||
Timer = 0;
|
||||
TimerTime = *loader::query_float_attribute(groupIndex, 0, 407);
|
||||
}
|
||||
|
||||
int TPopupTarget::Message(MessageCode code, float value)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::TPopupTargetDisable:
|
||||
ActiveFlag = 0;
|
||||
SpriteSet(-1);
|
||||
break;
|
||||
case MessageCode::TPopupTargetEnable:
|
||||
Timer = timer::set(TimerTime, this, TimerExpired);
|
||||
break;
|
||||
case MessageCode::PlayerChanged:
|
||||
PlayerMessagefieldBackup[PinballTable->CurrentPlayer] = MessageField;
|
||||
MessageField = PlayerMessagefieldBackup[static_cast<int>(floor(value))];
|
||||
TPopupTarget::Message(MessageField ? MessageCode::TPopupTargetDisable : MessageCode::TPopupTargetEnable, 0.0);
|
||||
break;
|
||||
case MessageCode::Reset:
|
||||
{
|
||||
MessageField = 0;
|
||||
int* playerPtr = PlayerMessagefieldBackup;
|
||||
for (auto index = 0; index < PinballTable->PlayerCount; ++index)
|
||||
{
|
||||
*playerPtr = 0;
|
||||
++playerPtr;
|
||||
}
|
||||
|
||||
if (Timer)
|
||||
timer::kill(Timer);
|
||||
TimerExpired(0, this);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TPopupTarget::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge)
|
||||
{
|
||||
if (PinballTable->TiltLockFlag)
|
||||
{
|
||||
maths::basic_collision(ball, nextPosition, direction, Elasticity, Smoothness, 1000000000.0, 0.0);
|
||||
}
|
||||
else if (maths::basic_collision(
|
||||
ball,
|
||||
nextPosition,
|
||||
direction,
|
||||
Elasticity,
|
||||
Smoothness,
|
||||
Threshold,
|
||||
Boost) > Threshold)
|
||||
{
|
||||
if (HardHitSoundId)
|
||||
loader::play_sound(HardHitSoundId, this, "TPopupTarget1");
|
||||
Message(MessageCode::TPopupTargetDisable, 0.0);
|
||||
control::handler(MessageCode::ControlCollision, this);
|
||||
}
|
||||
}
|
||||
|
||||
void TPopupTarget::TimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto target = static_cast<TPopupTarget*>(caller);
|
||||
target->Timer = 0;
|
||||
target->ActiveFlag = 1;
|
||||
target->SpriteSet(0);
|
||||
if (timerId)
|
||||
{
|
||||
if (target->SoftHitSoundId)
|
||||
loader::play_sound(target->SoftHitSoundId, target, "TPopupTarget2");
|
||||
}
|
||||
}
|
||||
18
SpaceCadetPinball/TPopupTarget.h
Normal file
18
SpaceCadetPinball/TPopupTarget.h
Normal file
@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
class TPopupTarget :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TPopupTarget(TPinballTable* table, int groupIndex);
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
|
||||
static void TimerExpired(int timerId, void* caller);
|
||||
|
||||
int Timer;
|
||||
float TimerTime;
|
||||
int PlayerMessagefieldBackup[4]{};
|
||||
};
|
||||
188
SpaceCadetPinball/TRamp.cpp
Normal file
188
SpaceCadetPinball/TRamp.cpp
Normal file
@ -0,0 +1,188 @@
|
||||
#include "pch.h"
|
||||
#include "TRamp.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "TBall.h"
|
||||
#include "TEdgeSegment.h"
|
||||
#include "TLine.h"
|
||||
#include "TPinballTable.h"
|
||||
#include "TTableLayer.h"
|
||||
|
||||
|
||||
TRamp::TRamp(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false)
|
||||
{
|
||||
visualStruct visual{};
|
||||
vector2 wall1End{}, wall1Start{}, wall2Start{}, wall2End{};
|
||||
|
||||
MessageField = 0;
|
||||
UnusedBaseFlag = 1;
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
CollisionGroup = visual.CollisionGroup;
|
||||
|
||||
BallFieldMult = loader::query_float_attribute(groupIndex, 0, 701, 0.2f);
|
||||
BallZOffsetFlag = static_cast<int>(loader::query_float_attribute(groupIndex, 0, 1305, 0));
|
||||
|
||||
auto floatArr3Plane = loader::query_float_attribute(groupIndex, 0, 1300);
|
||||
RampPlaneCount = static_cast<int>(floor(*floatArr3Plane));
|
||||
RampPlane = reinterpret_cast<ramp_plane_type*>(floatArr3Plane + 1);
|
||||
|
||||
auto wall0Arr = loader::query_float_attribute(groupIndex, 0, 1303);
|
||||
auto wall0CollisionGroup = 1 << static_cast<int>(floor(wall0Arr[0]));
|
||||
auto wall0Pts = reinterpret_cast<wall_point_type*>(wall0Arr + 2);
|
||||
Line1 = new TLine(this, &ActiveFlag, wall0CollisionGroup, wall0Pts->Pt1, wall0Pts->Pt0);
|
||||
Line1->WallValue = nullptr;
|
||||
Line1->place_in_grid(&AABB);
|
||||
EdgeList.push_back(Line1);
|
||||
|
||||
auto wall1Arr = loader::query_float_attribute(groupIndex, 0, 1301);
|
||||
Wall1CollisionGroup = 1 << static_cast<int>(floor(wall1Arr[0]));
|
||||
auto wall1Enabled = static_cast<int>(floor(wall1Arr[1]));
|
||||
Wall1BallOffset = wall1Arr[7];
|
||||
maths::find_closest_edge(
|
||||
RampPlane,
|
||||
RampPlaneCount,
|
||||
reinterpret_cast<wall_point_type*>(wall1Arr + 3),
|
||||
wall1End,
|
||||
wall1Start);
|
||||
|
||||
Line2 = new TLine(this, &ActiveFlag, CollisionGroup, wall1Start, wall1End);
|
||||
Line2->WallValue = nullptr;
|
||||
Line2->place_in_grid(&AABB);
|
||||
EdgeList.push_back(Line2);
|
||||
|
||||
auto wall2Arr = loader::query_float_attribute(groupIndex, 0, 1302);
|
||||
Wall2CollisionGroup = 1 << static_cast<int>(floor(wall2Arr[0]));
|
||||
auto wall2Enabled = static_cast<int>(floor(wall2Arr[1]));
|
||||
Wall2BallOffset = wall2Arr[7];
|
||||
maths::find_closest_edge(
|
||||
RampPlane,
|
||||
RampPlaneCount,
|
||||
reinterpret_cast<wall_point_type*>(wall2Arr + 3),
|
||||
wall2End,
|
||||
wall2Start);
|
||||
|
||||
Line3 = new TLine(this, &ActiveFlag, CollisionGroup, wall2Start, wall2End);
|
||||
Line3->WallValue = nullptr;
|
||||
Line3->place_in_grid(&AABB);
|
||||
EdgeList.push_back(Line3);
|
||||
|
||||
|
||||
auto xMin = 1000000000.0f;
|
||||
auto yMin = 1000000000.0f;
|
||||
auto yMax = -1000000000.0f;
|
||||
auto xMax = -1000000000.0f;
|
||||
for (auto index = 0; index < RampPlaneCount; index++)
|
||||
{
|
||||
auto& plane = RampPlane[index];
|
||||
vector2* pointOrder[4] = { &plane.V1, &plane.V2, &plane.V3, &plane.V1 };
|
||||
|
||||
xMin = std::min(std::min(std::min(plane.V3.X, plane.V1.X), plane.V2.X), xMin);
|
||||
yMin = std::min(std::min(std::min(plane.V3.Y, plane.V1.Y), plane.V2.Y), xMin); // Sic
|
||||
xMax = std::max(std::max(std::max(plane.V3.X, plane.V1.X), plane.V2.X), xMin);
|
||||
yMax = std::max(std::max(std::max(plane.V3.Y, plane.V1.Y), plane.V2.Y), xMin);
|
||||
|
||||
for (auto pt = 0; pt < 3; pt++)
|
||||
{
|
||||
auto& point1 = *pointOrder[pt], point2 = *pointOrder[pt + 1];
|
||||
auto collisionGroup = 0;
|
||||
|
||||
if (point1 == wall1End && point2 == wall1Start)
|
||||
{
|
||||
if (wall1Enabled)
|
||||
collisionGroup = Wall1CollisionGroup;
|
||||
}
|
||||
else if (point1 == wall2End && point2 == wall2Start)
|
||||
{
|
||||
if (wall2Enabled)
|
||||
collisionGroup = Wall2CollisionGroup;
|
||||
}
|
||||
else
|
||||
collisionGroup = visual.CollisionGroup;
|
||||
|
||||
if (collisionGroup)
|
||||
{
|
||||
auto line = new TLine(this, &ActiveFlag, collisionGroup, point1, point2);
|
||||
line->WallValue = &plane;
|
||||
line->place_in_grid(&AABB);
|
||||
EdgeList.push_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
plane.FieldForce.X = cos(plane.GravityAngle2) * sin(plane.GravityAngle1) *
|
||||
PinballTable->GravityDirVectMult;
|
||||
plane.FieldForce.Y = sin(plane.GravityAngle2) * sin(plane.GravityAngle1) *
|
||||
PinballTable->GravityDirVectMult;
|
||||
}
|
||||
|
||||
Field.ActiveFlag = &ActiveFlag;
|
||||
Field.CollisionComp = this;
|
||||
Field.CollisionGroup = visual.CollisionGroup;
|
||||
|
||||
auto x1 = xMax;
|
||||
auto y1 = yMax;
|
||||
auto x0 = xMin;
|
||||
auto y0 = yMin;
|
||||
TTableLayer::edges_insert_square(y0, x0, y1, x1, nullptr, &Field);
|
||||
}
|
||||
|
||||
void TRamp::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
|
||||
{
|
||||
ball->not_again(edge);
|
||||
ball->Position.X = nextPosition->X;
|
||||
ball->Position.Y = nextPosition->Y;
|
||||
ball->RayMaxDistance -= distance;
|
||||
|
||||
auto plane = static_cast<ramp_plane_type*>(edge->WallValue);
|
||||
if (plane)
|
||||
{
|
||||
ball->CollisionFlag = 1;
|
||||
ball->CollisionOffset.X = plane->BallCollisionOffset.X;
|
||||
ball->CollisionOffset.Y = plane->BallCollisionOffset.Y;
|
||||
ball->CollisionOffset.Z = plane->BallCollisionOffset.Z;
|
||||
ball->RampFieldForce.X = plane->FieldForce.X;
|
||||
ball->RampFieldForce.Y = plane->FieldForce.Y;
|
||||
ball->Position.Z = ball->Position.X * ball->CollisionOffset.X + ball->Position.Y * ball->CollisionOffset.Y +
|
||||
ball->Radius + ball->CollisionOffset.Z;
|
||||
ball->CollisionMask = CollisionGroup;
|
||||
return;
|
||||
}
|
||||
|
||||
if (edge == Line1)
|
||||
{
|
||||
if (!PinballTable->TiltLockFlag)
|
||||
{
|
||||
loader::play_sound(SoftHitSoundId, ball, "TRamp");
|
||||
control::handler(MessageCode::ControlCollision, this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ball->CollisionFlag = 0;
|
||||
if (edge == Line2)
|
||||
{
|
||||
ball->CollisionMask = Wall1CollisionGroup;
|
||||
if (BallZOffsetFlag)
|
||||
ball->Position.Z = ball->Radius + Wall1BallOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
ball->CollisionMask = Wall2CollisionGroup;
|
||||
if (BallZOffsetFlag)
|
||||
ball->Position.Z = ball->Radius + Wall2BallOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int TRamp::FieldEffect(TBall* ball, vector2* vecDst)
|
||||
{
|
||||
vecDst->X = ball->RampFieldForce.X - ball->Direction.X * ball->Speed * BallFieldMult;
|
||||
vecDst->Y = ball->RampFieldForce.Y - ball->Direction.Y * ball->Speed * BallFieldMult;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void TRamp::port_draw()
|
||||
{
|
||||
TCollisionComponent::port_draw();
|
||||
}
|
||||
31
SpaceCadetPinball/TRamp.h
Normal file
31
SpaceCadetPinball/TRamp.h
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "TCollisionComponent.h"
|
||||
#include "TEdgeManager.h"
|
||||
|
||||
struct ramp_plane_type;
|
||||
|
||||
class TRamp :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TRamp(TPinballTable* table, int groupIndex);
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
int FieldEffect(TBall* ball, vector2* vecDst) override;
|
||||
void port_draw() override;
|
||||
|
||||
field_effect_type Field{};
|
||||
int CollisionGroup;
|
||||
bool BallZOffsetFlag;
|
||||
int RampPlaneCount;
|
||||
float BallFieldMult;
|
||||
ramp_plane_type* RampPlane;
|
||||
TEdgeSegment* Line2;
|
||||
TEdgeSegment* Line3;
|
||||
TEdgeSegment* Line1;
|
||||
int Wall1CollisionGroup;
|
||||
int Wall2CollisionGroup;
|
||||
float Wall1BallOffset;
|
||||
float Wall2BallOffset;
|
||||
};
|
||||
78
SpaceCadetPinball/TRollover.cpp
Normal file
78
SpaceCadetPinball/TRollover.cpp
Normal file
@ -0,0 +1,78 @@
|
||||
#include "pch.h"
|
||||
#include "TRollover.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "gdrv.h"
|
||||
#include "loader.h"
|
||||
#include "render.h"
|
||||
#include "TBall.h"
|
||||
#include "TEdgeSegment.h"
|
||||
#include "timer.h"
|
||||
#include "TPinballTable.h"
|
||||
|
||||
TRollover::TRollover(TPinballTable* table, int groupIndex, bool createWall) : TCollisionComponent(
|
||||
table, groupIndex, createWall)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
TRollover::TRollover(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, false)
|
||||
{
|
||||
SpriteSet(0);
|
||||
build_walls(groupIndex);
|
||||
}
|
||||
|
||||
|
||||
int TRollover::Message(MessageCode code, float value)
|
||||
{
|
||||
if (code == MessageCode::Reset)
|
||||
{
|
||||
ActiveFlag = 1;
|
||||
RolloverFlag = 0;
|
||||
SpriteSet(0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TRollover::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge)
|
||||
{
|
||||
ball->Position.X = nextPosition->X;
|
||||
ball->Position.Y = nextPosition->Y;
|
||||
ball->RayMaxDistance -= distance;
|
||||
ball->not_again(edge);
|
||||
|
||||
if (!PinballTable->TiltLockFlag)
|
||||
{
|
||||
if (RolloverFlag)
|
||||
{
|
||||
timer::set(0.1f, this, TimerExpired);
|
||||
ActiveFlag = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
loader::play_sound(SoftHitSoundId, ball, "TRollover");
|
||||
control::handler(MessageCode::ControlCollision, this);
|
||||
}
|
||||
RolloverFlag = RolloverFlag == 0;
|
||||
SpriteSet(RolloverFlag ? -1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
void TRollover::build_walls(int groupIndex)
|
||||
{
|
||||
visualStruct visual{};
|
||||
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
float* arr1 = loader::query_float_attribute(groupIndex, 0, 600);
|
||||
TEdgeSegment::install_wall(arr1, this, &ActiveFlag, visual.CollisionGroup, 0.0, 600);
|
||||
float* arr2 = loader::query_float_attribute(groupIndex, 0, 603);
|
||||
TEdgeSegment::install_wall(arr2, this, &RolloverFlag, visual.CollisionGroup, 0.0, 603);
|
||||
}
|
||||
|
||||
void TRollover::TimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto roll = static_cast<TRollover*>(caller);
|
||||
roll->ActiveFlag = 1;
|
||||
}
|
||||
20
SpaceCadetPinball/TRollover.h
Normal file
20
SpaceCadetPinball/TRollover.h
Normal file
@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
class TRollover :
|
||||
public TCollisionComponent
|
||||
{
|
||||
protected:
|
||||
TRollover(TPinballTable* table, int groupIndex, bool createWall);
|
||||
public:
|
||||
TRollover(TPinballTable* table, int groupIndex);
|
||||
~TRollover() override = default;
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
void build_walls(int groupIndex);
|
||||
|
||||
static void TimerExpired(int timerId, void* caller);
|
||||
|
||||
char RolloverFlag{};
|
||||
};
|
||||
91
SpaceCadetPinball/TSink.cpp
Normal file
91
SpaceCadetPinball/TSink.cpp
Normal file
@ -0,0 +1,91 @@
|
||||
#include "pch.h"
|
||||
#include "TSink.h"
|
||||
|
||||
|
||||
#include "control.h"
|
||||
#include "loader.h"
|
||||
#include "render.h"
|
||||
#include "TPinballTable.h"
|
||||
#include "TBall.h"
|
||||
#include "TDrain.h"
|
||||
#include "timer.h"
|
||||
|
||||
TSink::TSink(TPinballTable* table, int groupIndex) : TCollisionComponent(table, groupIndex, true)
|
||||
{
|
||||
visualStruct visual{};
|
||||
|
||||
MessageField = 0;
|
||||
loader::query_visual(groupIndex, 0, &visual);
|
||||
BallThrowDirection = visual.Kicker.ThrowBallDirection;
|
||||
ThrowAngleMult = visual.Kicker.ThrowBallAngleMult;
|
||||
ThrowSpeedMult1 = visual.Kicker.Boost;
|
||||
ThrowSpeedMult2 = visual.Kicker.ThrowBallMult * 0.01f;
|
||||
SoundIndex4 = visual.SoundIndex4;
|
||||
SoundIndex3 = visual.SoundIndex3;
|
||||
auto floatArr = loader::query_float_attribute(groupIndex, 0, 601);
|
||||
BallPosition.X = floatArr[0];
|
||||
BallPosition.Y = floatArr[1];
|
||||
TimerTime = *loader::query_float_attribute(groupIndex, 0, 407);
|
||||
}
|
||||
|
||||
int TSink::Message(MessageCode code, float value)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case MessageCode::TSinkResetTimer:
|
||||
if (value < 0.0f)
|
||||
value = TimerTime;
|
||||
timer::set(value, this, TimerExpired);
|
||||
break;
|
||||
case MessageCode::PlayerChanged:
|
||||
timer::kill(TimerExpired);
|
||||
PlayerMessagefieldBackup[PinballTable->CurrentPlayer] = MessageField;
|
||||
MessageField = PlayerMessagefieldBackup[static_cast<int>(floor(value))];
|
||||
break;
|
||||
case MessageCode::Reset:
|
||||
{
|
||||
timer::kill(TimerExpired);
|
||||
MessageField = 0;
|
||||
for (auto &msgBackup : PlayerMessagefieldBackup)
|
||||
msgBackup = 0;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TSink::Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance, TEdgeSegment* edge)
|
||||
{
|
||||
if (PinballTable->TiltLockFlag)
|
||||
{
|
||||
PinballTable->Drain->Collision(ball, nextPosition, direction, distance, edge);
|
||||
}
|
||||
else
|
||||
{
|
||||
ball->Disable();
|
||||
loader::play_sound(SoundIndex4, ball, "TSink1");
|
||||
control::handler(MessageCode::ControlCollision, this);
|
||||
}
|
||||
}
|
||||
|
||||
void TSink::TimerExpired(int timerId, void* caller)
|
||||
{
|
||||
auto sink = static_cast<TSink*>(caller);
|
||||
auto table = sink->PinballTable;
|
||||
if (table->BallCountInRect(sink->BallPosition, table->CollisionCompOffset * 2.0f))
|
||||
{
|
||||
timer::set(0.5f, sink, TimerExpired);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto ball = table->AddBall(sink->BallPosition);
|
||||
assertm(ball, "Failure to create ball in sink");
|
||||
ball->CollisionDisabledFlag = true;
|
||||
ball->throw_ball(&sink->BallThrowDirection, sink->ThrowAngleMult, sink->ThrowSpeedMult1,
|
||||
sink->ThrowSpeedMult2);
|
||||
if (sink->SoundIndex3)
|
||||
loader::play_sound(sink->SoundIndex3, ball, "TSink2");
|
||||
}
|
||||
}
|
||||
25
SpaceCadetPinball/TSink.h
Normal file
25
SpaceCadetPinball/TSink.h
Normal file
@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include "maths.h"
|
||||
#include "TCollisionComponent.h"
|
||||
|
||||
class TSink :
|
||||
public TCollisionComponent
|
||||
{
|
||||
public:
|
||||
TSink(TPinballTable* table, int groupIndex);
|
||||
int Message(MessageCode code, float value) override;
|
||||
void Collision(TBall* ball, vector2* nextPosition, vector2* direction, float distance,
|
||||
TEdgeSegment* edge) override;
|
||||
|
||||
static void TimerExpired(int timerId, void* caller);
|
||||
|
||||
float TimerTime;
|
||||
vector2 BallPosition{};
|
||||
vector3 BallThrowDirection{};
|
||||
float ThrowAngleMult;
|
||||
float ThrowSpeedMult1;
|
||||
float ThrowSpeedMult2;
|
||||
int SoundIndex4;
|
||||
int SoundIndex3;
|
||||
int PlayerMessagefieldBackup[4]{};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user