diff --git a/.github/workflows/mac-universal.yml b/.github/workflows/mac-universal.yml new file mode 100644 index 000000000..874e2ccdf --- /dev/null +++ b/.github/workflows/mac-universal.yml @@ -0,0 +1,73 @@ +name: Mac universal build +on: + push: + branches: + - universal + release: + types: [published] +jobs: + build: + runs-on: macos-14 + env: + CONFIGURATION: ${{ matrix.configuration }} + BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }} + PROD_MACOS_CERTIFICATE: '${{ secrets.PROD_MACOS_CERTIFICATE }}' + PROD_MACOS_CERTIFICATE_PWD: '${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}' + PROD_MACOS_CERTIFICATE_NAME: '${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}' + PROD_MACOS_CI_KEYCHAIN_PWD: '${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}' + PROD_MACOS_NOTARIZATION_APPLE_ID: '${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}' + PROD_MACOS_NOTARIZATION_TEAM_ID: '${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}' + PROD_MACOS_NOTARIZATION_PWD: '${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}' + BUILD_OS: macos + strategy: + fail-fast: false + matrix: + configuration: + - Release + - Debug + steps: + - uses: actions/checkout@v4 + + # Download macos-intel + - uses: robinraju/release-downloader@v1 + id: download + with: + latest: true + fileName: 'cboe-macos-intel-${{ matrix.configuration }}.tar' + extract: true + out-file-path: 'cboe-macos-intel-${{ matrix.configuration }}' + + # Download macos-silicon + - uses: robinraju/release-downloader@v1 + with: + latest: true + fileName: 'cboe-macos-silicon-${{ matrix.configuration }}.tar' + extract: true + out-file-path: 'cboe-macos-silicon-${{ matrix.configuration }}' + + - run: .github/workflows/scripts/mac/make-universal.sh + + # Skipping this for now because of issue nqnstudios#13 + - name: Codesign and notarize + run: 'SIGN="no" NOTARIZE="no" .github/workflows/scripts/mac/sign-apps.sh' + + - name: 'Tar files' + run: 'tar -cvf cboe-macos-universal-${{matrix.configuration}}.tar "build/Blades of Exile"' + + # Upload everything as artifact + - uses: actions/upload-artifact@v4 + with: + name: mac-universal-dependencies-${{matrix.configuration}} + path: cboe-macos-universal-${{matrix.configuration}}.tar + + # upload a release + - name: Github release + uses: softprops/action-gh-release@v2 + with: + files: cboe-macos-universal-${{ matrix.configuration }}.tar + tag_name: ${{ steps.download.outputs.tag_name }} + + - name: 'Itch.io release' + run: './.github/workflows/scripts/butler_push.sh' + shell: bash + if: ${{ matrix.configuration == 'Release' }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..b66803497 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,131 @@ +on: + push: + branches: + - 'itch-edition' + tags: + - "v*.*.*" + pull_request: + branches: + - itch-edition +jobs: + release: + env: + ARCH: ${{ matrix.os.flag }} + MACOSX_DEPLOYMENT_TARGET: 10.15 + BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }} + PROD_MACOS_CERTIFICATE: '${{ secrets.PROD_MACOS_CERTIFICATE }}' + PROD_MACOS_CERTIFICATE_PWD: '${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}' + PROD_MACOS_CERTIFICATE_NAME: '${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}' + PROD_MACOS_CI_KEYCHAIN_PWD: '${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}' + PROD_MACOS_NOTARIZATION_APPLE_ID: '${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}' + PROD_MACOS_NOTARIZATION_TEAM_ID: '${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}' + PROD_MACOS_NOTARIZATION_PWD: '${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}' + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + DEBUG_FLAG: ${{ matrix.configuration == 'Debug' && 'true' || 'false' }} + BUILD_OS: ${{ matrix.os.name }} + strategy: + fail-fast: false + matrix: + os: + - name: macos + suffix: '-intel' + flag: x86_64 + deps: macos-universal + version: 13 + scons-script: './.github/workflows/scripts/mac/scons-build.sh' + - name: macos + flag: arm64 + suffix: '-silicon' + deps: macos-universal + version: 14 + scons-script: './.github/workflows/scripts/mac/scons-build.sh' +# - name: ubuntu +# suffix: '' +# version: 22.04 +# scons-script: scons + - name: windows + suffix: '' + version: 2022 + scons-script: './.github/workflows/scripts/win/scons-build.bat' + configuration: + - Release + - Debug + runs-on: '${{ matrix.os.name }}-${{ matrix.os.version }}' + steps: + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v7 + with: + script: "core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');" + - name: checkout + uses: actions/checkout@v4 + with: + submodules: true + - name: Download dependency build + uses: robinraju/release-downloader@v1 + with: + repository: NQNStudios/cboe-dependencies + latest: true + fileName: 'dependencies-${{ matrix.os.deps || matrix.os.name }}-${{ matrix.configuration }}.tar' + extract: true + out-file-path: 'deps' + - name: Windows build dependencies + run: 'vcpkg install libxml2 && pip install scons' + if: ${{ matrix.os.name == 'windows' }} + - name: Mac build dependencies + run: brew install scons + if: ${{ matrix.os.name == 'macos' }} + - name: Linux build dependencies + run: sudo apt-get update && sudo apt-get install scons libxml2-utils libgl-dev libopenal-dev + if: ${{ matrix.os.name == 'ubuntu' }} + - name: Install TGUI + run: 'sudo ./.github/workflows/scripts/linux/install-tgui.sh' + if: ${{ matrix.os.name == 'ubuntu' }} + - name: Build + run: '${{ matrix.os.scons-script }} test=false debug=$DEBUG_FLAG' + shell: bash + + - name: Download fix-rpaths.py script + run: git clone https://gist.github.com/NQNStudios/7145bcf6621891f5176c8caa165d6b93 + if: ${{ matrix.os.name == 'macos' }} + - name: Fix rpaths game + run: 'python 7145bcf6621891f5176c8caa165d6b93/fix-rpaths.py "build/Blades of Exile/Blades of Exile.app"' + if: ${{ matrix.os.name == 'macos' }} + - name: Fix rpaths scenario editor + run: 'python 7145bcf6621891f5176c8caa165d6b93/fix-rpaths.py "build/Blades of Exile/BoE Scenario Editor.app"' + if: ${{ matrix.os.name == 'macos' }} + - name: Fix rpaths character editor + run: 'python 7145bcf6621891f5176c8caa165d6b93/fix-rpaths.py "build/Blades of Exile/BoE Character Editor.app"' + if: ${{ matrix.os.name == 'macos' }} + + - run: cp .itch.toml "build/Blades of Exile/" + shell: bash + + - name: 'Tar unsigned files' + run: 'tar -cvf cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }}-unsigned.tar "Blades of Exile"' + working-directory: '${{ github.workspace }}/build' + - name: upload pre-signing artifact + uses: actions/upload-artifact@v4 + with: + name: cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }}-unsigned + path: '${{ github.workspace }}/build/cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }}-unsigned.tar' + # Skipping this for now because of issue nqnstudios#13 + - name: Codesign and notarize + run: 'SIGN="no" NOTARIZE="no" ./.github/workflows/scripts/mac/sign-apps.sh' + if: ${{ matrix.os.name == 'macos' }} + - name: 'Tar files' + run: 'tar -cvf cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }}.tar "Blades of Exile"' + working-directory: '${{ github.workspace }}/build' + - name: 'Upload Artifact' + uses: actions/upload-artifact@v4 + with: + name: cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }} + path: '${{ github.workspace }}/build/cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }}.tar' + - name: Github release + uses: softprops/action-gh-release@v2 + with: + files: '${{ github.workspace }}/build/cboe-${{ matrix.os.name }}${{ matrix.os.suffix }}-${{ matrix.configuration }}.tar' + if: ${{ startsWith(github.ref, 'refs/tags/') }} + - name: 'Itch.io release' + run: './.github/workflows/scripts/butler_push.sh' + shell: bash + if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.configuration == 'Release' && matrix.os.name != 'macos' }} diff --git a/.github/workflows/scripts/butler_push.sh b/.github/workflows/scripts/butler_push.sh new file mode 100755 index 000000000..5012a04f6 --- /dev/null +++ b/.github/workflows/scripts/butler_push.sh @@ -0,0 +1,28 @@ +#! /bin/bash + +butler_channel="" +butler_exe="" +release_dir="" +if [ "$BUILD_OS" = "ubuntu" ]; then + butler_channel=linux-amd64 + butler_exe=butler + release_dir="linux" +elif [ "$BUILD_OS" = "windows" ]; then + butler_channel=windows-amd64 + butler_exe=butler.exe + release_dir="windows" +elif [ "$BUILD_OS" = "macos" ]; then + butler_channel=darwin-amd64 + butler_exe=butler + release_dir="macos" +fi + +# -L follows redirects +# -O specifies output name +curl -L -o butler.zip https://broth.itch.ovh/butler/${butler_channel}/LATEST/archive/default +unzip butler.zip +# GNU unzip tends to not set the executable bit even though it's set in the .zip +chmod +x ${butler_exe} +# just a sanity check run (and also helpful in case you're sharing CI logs) +./${butler_exe} -V +./${butler_exe} push "build/Blades of Exile/" nqn/blades-of-exile:${release_dir} diff --git a/.github/workflows/scripts/linux/install-tgui.sh b/.github/workflows/scripts/linux/install-tgui.sh index 876bcdccb..9b32464df 100755 --- a/.github/workflows/scripts/linux/install-tgui.sh +++ b/.github/workflows/scripts/linux/install-tgui.sh @@ -3,7 +3,7 @@ git clone --depth 1 -b 0.9 https://github.com/texus/TGUI.git cd TGUI export CLICOLOR_FORCE=1 -cmake -D TGUI_CXX_STANDARD=14 . +SFML_DIR=../deps/lib/cmake/SFML cmake -D TGUI_CXX_STANDARD=14 -D SFML_DIR=../deps/lib/cmake/SFML . make cmake --install . cd .. # Probably not needed but... diff --git a/.github/workflows/scripts/mac/make-universal.sh b/.github/workflows/scripts/mac/make-universal.sh new file mode 100755 index 000000000..0675d9680 --- /dev/null +++ b/.github/workflows/scripts/mac/make-universal.sh @@ -0,0 +1,27 @@ +#! /bin/bash + +if [ -z "$CONFIGURATION" ]; then + CONFIGURATION=Release +fi + +INTEL=cboe-macos-intel-$CONFIGURATION +SILICON=cboe-macos-silicon-$CONFIGURATION + +combine() { + mkdir -p "build/Blades of Exile/$1.app/Contents/MacOS" + lipo -create "$SILICON/Blades of Exile/$1.app/Contents/MacOS/$1" "$INTEL/Blades of Exile/$1.app/Contents/MacOS/$1" -output "build/Blades of Exile/$1.app/Contents/MacOS/$1" + cp -r "$SILICON/Blades of Exile/$1.app/Contents/Frameworks" "build/Blades of Exile/$1.app/Contents/" + cp -r "$SILICON/Blades of Exile/$1.app/Contents/Resources" "build/Blades of Exile/$1.app/Contents/" + cp "$SILICON/Blades of Exile/$1.app/Contents/Info.plist" "build/Blades of Exile/$1.app/Contents/" + cp "$SILICON/Blades of Exile/$1.app/Contents/PkgInfo" "build/Blades of Exile/$1.app/Contents/" +} + +combine "Blades of Exile" +combine "BoE Scenario Editor" +combine "BoE Character Editor" + +cp -r "$SILICON/Blades of Exile/Blades of Exile Base" "build/Blades of Exile/" +cp -r "$SILICON/Blades of Exile/Blades of Exile Scenarios" "build/Blades of Exile/" +cp -r "$SILICON/Blades of Exile/data" "build/Blades of Exile/" +cp -r "$SILICON/Blades of Exile/docs" "build/Blades of Exile/" +cp "$SILICON/Blades of Exile/.itch.toml" "build/Blades of Exile/" \ No newline at end of file diff --git a/.github/workflows/scripts/mac/scons-build.sh b/.github/workflows/scripts/mac/scons-build.sh index 4cdf6d328..a130f4780 100755 --- a/.github/workflows/scripts/mac/scons-build.sh +++ b/.github/workflows/scripts/mac/scons-build.sh @@ -4,4 +4,4 @@ export CC="$(brew --prefix llvm)/bin/clang" export CXX="$(brew --prefix llvm)/bin/clang++" export SDKROOT="$(xcrun --show-sdk-path)" -scons CXXFLAGS="-I/usr/local/opt/zlib/include" LINKFLAGS="-L/usr/local/opt/zlib/lib" $@ +scons CXXFLAGS="-I/usr/local/opt/zlib/include -arch $ARCH" LINKFLAGS="-L/usr/local/opt/zlib/lib" $@ diff --git a/.github/workflows/scripts/mac/sign-apps.sh b/.github/workflows/scripts/mac/sign-apps.sh new file mode 100755 index 000000000..40ae904be --- /dev/null +++ b/.github/workflows/scripts/mac/sign-apps.sh @@ -0,0 +1,79 @@ +#! /bin/bash + +# CODE-SIGNING STEP +# Original Source: https://federicoterzi.com/blog/automatic-code-signing-and-notarization-for-macos-apps-using-github-actions/ +# Modified by NQNStudios + +# Turn our base64-encoded certificate back to a regular .p12 file + +echo $PROD_MACOS_CERTIFICATE | base64 --decode > certificate.p12 + +# We need to create a new keychain, otherwise using the certificate will prompt +# with a UI dialog asking for the certificate password, which we can't +# use in a headless CI environment + +security create-keychain -p "$PROD_MACOS_CI_KEYCHAIN_PWD" build.keychain +security default-keychain -s build.keychain +security unlock-keychain -p "$PROD_MACOS_CI_KEYCHAIN_PWD" build.keychain +security import certificate.p12 -k build.keychain -P "$PROD_MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign +security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$PROD_MACOS_CI_KEYCHAIN_PWD" build.keychain + +sign() { + if [ "$SIGN" = "no" ]; + then + return + fi + APP_PATH="build/Blades of Exile/$1.app" + + # We finally codesign our app bundle, specifying the Hardened runtime option + + /usr/bin/codesign --force -s "$PROD_MACOS_CERTIFICATE_NAME" --options runtime "$APP_PATH"/Contents/Frameworks/*.dylib -v + (cd "$APP_PATH" && frameworks=Contents/Frameworks/*.framework/Versions/A/* && \ + for framework in $frameworks; do + if [ -f "$framework" ]; then + /usr/bin/codesign --force -s "$PROD_MACOS_CERTIFICATE_NAME" --options runtime "$framework" -v + fi + done) + /usr/bin/codesign --force -s "$PROD_MACOS_CERTIFICATE_NAME" --options runtime "$APP_PATH"/Contents/Frameworks/*.framework/Versions/A/Resources/Info.plist -v + /usr/bin/codesign --force -s "$PROD_MACOS_CERTIFICATE_NAME" --options runtime "$APP_PATH/Contents/Info.plist" -v + + /usr/bin/codesign --force -s "$PROD_MACOS_CERTIFICATE_NAME" --options runtime "$APP_PATH" -v + + # NOTARIZATION STEP + if [ "$NOTARIZE" = "no" ]; + then + return + fi + + # (same source) + + # Store the notarization credentials so that we can prevent a UI password dialog + # from blocking the CI + + echo "Create keychain profile" + xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD" + + # We can't notarize an app bundle directly, but we need to compress it as an archive. + # Therefore, we create a zip file containing our app bundle, so that we can send it to the + # notarization service + + echo "Creating temp notarization archive" + ditto -c -k --keepParent "$APP_PATH" "notarization.zip" + + # Here we send the notarization request to the Apple's Notarization service, waiting for the result. + # This typically takes a few seconds inside a CI environment, but it might take more depending on the App + # characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if + # you're curious + + echo "Notarize app" + xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait + + # Finally, we need to "attach the staple" to our executable, which will allow our app to be + # validated by macOS even when an internet connection is not available. + echo "Attach staple" + xcrun stapler staple "$APP_PATH" || exit 1 +} + +sign "Blades of Exile" +sign "BoE Scenario Editor" +sign "BoE Character Editor" \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 656289c2b..f8fa759e9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -11,3 +11,6 @@ [submodule "deps/fix-rpaths"] path = deps/fix-rpaths url = https://gist.github.com/NQNStudios/7145bcf6621891f5176c8caa165d6b93 +[submodule "rsrc/scenarios/custom"] + path = rsrc/scenarios/custom + url = https://github.com/NQNStudios/cboe-scenarios diff --git a/.itch.toml b/.itch.toml new file mode 100644 index 000000000..2a0393fd1 --- /dev/null +++ b/.itch.toml @@ -0,0 +1,9 @@ +[[actions]] +name = "Play" +path = "Blades of Exile.exe" +platform = "windows" + +[[actions]] +name = "Play" +path = "Blades of Exile.app" +platform = "osx" \ No newline at end of file diff --git a/README.md b/README.md index 629580c1c..e9c500905 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ correct place to search. For example, if you installed Boost via Homebrew, you m add something like the following to the Project Build Settings, under Linking->Other Linking Flags: - -lboost_filesystem -lboost_system -L/usr/local/Cellar/boost/1.57.0/lib + -lboost_filesystem -lboost_system -lboost_locale -L/usr/local/Cellar/boost/1.57.0/lib Again with Homebrew, you may also need to add the following to Apple LLVM Custom Compiler Flags -> Other C++ Flags: diff --git a/SConstruct b/SConstruct index 19a9fce5f..4a2fcc12d 100644 --- a/SConstruct +++ b/SConstruct @@ -232,13 +232,15 @@ elif platform == "win32": vcpkg_other_libs = list(filter(path.exists, vcpkg_other_libs)) vcpkg_other_bins = [path.join(d.get_abspath(), 'bin') for d in vcpkg_other_packages] vcpkg_other_bins = list(filter(path.exists, vcpkg_other_bins)) - - include_paths=[path.join(vcpkg_installed, 'include')] + vcpkg_other_includes + project_includes = [] + for (root, dirs, files) in os.walk('src'): + project_includes.append(path.join(os.getcwd(), root)) + include_paths=project_includes env.Append( - LINKFLAGS=['/SUBSYSTEM:WINDOWS','/ENTRY:mainCRTStartup',f'/MACHINE:X{arch_short}'], + LINKFLAGS=['/SUBSYSTEM:WINDOWS','/ENTRY:mainCRTStartup',f'/MACHINE:X{arch_short}', '/VERBOSE', '/NODEFAULTLIB:libboost_filesystem-vc142-mt-x64-1_85.lib'], CXXFLAGS=['/EHsc','/MD','/FIglobal.hpp'], CPPPATH=include_paths, - LIBPATH=[path.join(vcpkg_installed, 'lib')] + vcpkg_other_libs + vcpkg_other_bins, + LIBPATH=[], LIBS=Split(""" kernel32 user32 @@ -259,7 +261,10 @@ elif platform == "win32": def build_app_package(env, source, build_dir, info): env.Install(build_dir, source) elif platform == "posix": - env.Append(CXXFLAGS=["-std=c++14","-include","global.hpp"]) + env.Append( + CXXFLAGS=["-std=c++14","-include","global.hpp"], + #LINKFLAGS=["-rpath-link", "deps/lib/", "-rpath", "./"] + ) def build_app_package(env, source, build_dir, info): env.Install(build_dir, source) @@ -392,6 +397,7 @@ if not env.GetOption('clean'): check_header('boost/spirit/include/classic.hpp', 'Boost.Spirit.Classic') check_lib('boost_system', 'Boost.System', suffixes, boost_versions) check_lib('boost_filesystem', 'Boost.Filesystem', suffixes, boost_versions) + check_lib('boost_locale', 'Boost.Locale', suffixes, boost_versions) sfml_suffixes = ['-d'] check_lib('sfml-system', 'SFML-system', sfml_suffixes) check_lib('sfml-window', 'SFML-window', sfml_suffixes) @@ -415,6 +421,7 @@ if not env.GetOption('clean'): if platform == 'posix': def check_tgui(conf, second_attempt=False): if conf.CheckLib('libtgui', language='C++'): + bundled_libs.append('tgui') return conf else: if second_attempt: @@ -496,7 +503,31 @@ Export("data_dir") SConscript(["rsrc/SConscript", "doc/SConscript"]) # Bundle required frameworks and libraries - +def handle_bundled_libs(extension, prefix=''): + target_dirs = ["#build/Blades of Exile", "#build/test"] + for lib in bundled_libs: + for lpath in env['LIBPATH']: + def check_path(src_file): + _dir = os.path.dirname(src_file) + print(f'checking {_dir} for {prefix}{lib}') + try: + print(os.listdir(_dir)) + except: + pass + if path.exists(src_file) and src_file != "/usr/lib/x86_64-linux-gnu/libz.so": + for targ in target_dirs: + for so in os.listdir(_dir): + if os.path.basename(src_file) in so: + print(f'found {path.join(_dir, so)}') + env.Install(targ, path.join(_dir, so)) + return True + return False + if check_path(path.join(lpath, prefix + lib + extension)): + break + elif check_path(path.join(lpath.replace('lib', 'bin'), prefix + lib + extension)): + break + elif check_path(path.join(lpath, 'x86_64-linux-gnu', prefix + lib + extension)): + break if platform == "darwin": app_targets = [] if 'game' in targets: @@ -538,20 +569,7 @@ elif platform == "win32": brotlidec brotlicommon """) - target_dirs = ["#build/Blades of Exile", "#build/test"] - for lib in bundled_libs: - for lpath in env['LIBPATH']: - src_file = path.join(lpath, lib + ".dll") - if path.exists(src_file): - for targ in target_dirs: - env.Install(targ, src_file) - break - elif 'lib' in lpath: - src_file = path.join(lpath.replace('lib', 'bin'), lib + ".dll") - if path.exists(src_file): - for targ in target_dirs: - env.Install(targ, src_file) - break + handle_bundled_libs(".dll") # Extra: Microsoft redistributable libraries installer if 'msvc' in env["TOOLS"]: if path.exists("deps/VCRedistInstall.exe"): @@ -559,11 +577,41 @@ elif platform == "win32": else: print("WARNING: Cannot find installer for the MSVC redistributable libraries for your version of Visual Studio.") print("Please download it from Microsoft's website and place it at:") - print(" deps/VCRedistInstall.exe") + print(" deps/VCRedistInstall.exe") # Create it so its lack doesn't cause makensis to break # (Because the installer is an optional component.) os.makedirs("build/Blades of Exile", exist_ok=True) open("build/Blades of Exile/VCRedistInstall.exe", 'w').close() +elif platform == "posix": + targets = [ + "Blades of Exile", + "BoE Character Editor", + "BoE Scenario Editor", + ] + def patchelf(): + to_patch = targets + [so for so in os.listdir("build/Blades of Exile") if '.so' in so] + for targ in to_patch: + subprocess.call(['patchelf', '--set-rpath', '.', targ], cwd='build/Blades of Exile') + atexit.register(patchelf) + bundled_libs += Split(""" + GL + X11 + stdc++ + Xrandr + Xcursor + udev + openal + vorbisenc + vorbisfile + vorbis + ogg + FLAC + freetype + GLdispatch + GLX + xcb + """) + handle_bundled_libs(".so", "lib") if env["package"]: if platform == "darwin": diff --git a/pkg/credits/Funding.txt b/pkg/credits/Funding.txt index aca5479d8..a5ef8089d 100644 --- a/pkg/credits/Funding.txt +++ b/pkg/credits/Funding.txt @@ -10,6 +10,7 @@ Confirmed: - Bret Rodabaugh - Dylan Nugent - Evan Mulrooney +- Glen Chudley - Jake Harrelson - Jared Forcinito - Jeff Potter @@ -25,6 +26,7 @@ Confirmed: - K L - Laura Nelson - Mariann Krizsan +- Maryanne Wachter - Mike Lapinsky - Nathan Rickey - Nick Chaimov diff --git a/proj/vs2013/Common/Common.vcxproj b/proj/vs2013/Common/Common.vcxproj index 2307fbff6..b9be0f8fd 100644 --- a/proj/vs2013/Common/Common.vcxproj +++ b/proj/vs2013/Common/Common.vcxproj @@ -192,6 +192,7 @@ + @@ -287,6 +288,7 @@ + @@ -502,6 +504,7 @@ + @@ -564,4 +567,4 @@ - + \ No newline at end of file diff --git a/proj/vs2013/Common/Common.vcxproj.filters b/proj/vs2013/Common/Common.vcxproj.filters index c435cdb1b..c32810921 100644 --- a/proj/vs2013/Common/Common.vcxproj.filters +++ b/proj/vs2013/Common/Common.vcxproj.filters @@ -831,6 +831,9 @@ Scenario + + DialogXML\Dialogs + @@ -1093,8 +1096,11 @@ Tools + + DialogXML\Dialogs + - + \ No newline at end of file diff --git a/proj/vs2013/vcpkg.json b/proj/vs2013/vcpkg.json index 7dc72c2e7..1b2b04d06 100644 --- a/proj/vs2013/vcpkg.json +++ b/proj/vs2013/vcpkg.json @@ -14,6 +14,7 @@ "boost-chrono", "boost-math", "boost-spirit", - "boost-process" + "boost-process", + "boost-locale" ] } \ No newline at end of file diff --git a/proj/vs2017/Common/Common.vcxproj b/proj/vs2017/Common/Common.vcxproj index d2c0b4f87..3341613c0 100644 --- a/proj/vs2017/Common/Common.vcxproj +++ b/proj/vs2017/Common/Common.vcxproj @@ -344,6 +344,7 @@ + @@ -441,6 +442,7 @@ + @@ -561,6 +563,7 @@ + @@ -617,4 +620,4 @@ - + \ No newline at end of file diff --git a/proj/vs2017/Common/Common.vcxproj.filters b/proj/vs2017/Common/Common.vcxproj.filters index 30cb6bea4..c55922d16 100644 --- a/proj/vs2017/Common/Common.vcxproj.filters +++ b/proj/vs2017/Common/Common.vcxproj.filters @@ -824,6 +824,9 @@ Scenario + + DialogXML\Dialogs + @@ -1079,5 +1082,8 @@ Tools + + DialogXML\Dialogs + - + \ No newline at end of file diff --git a/proj/vs2017/vcpkg.json b/proj/vs2017/vcpkg.json index 7dc72c2e7..1b2b04d06 100644 --- a/proj/vs2017/vcpkg.json +++ b/proj/vs2017/vcpkg.json @@ -14,6 +14,7 @@ "boost-chrono", "boost-math", "boost-spirit", - "boost-process" + "boost-process", + "boost-locale" ] } \ No newline at end of file diff --git a/proj/xc12/BoE.xcodeproj/project.pbxproj b/proj/xc12/BoE.xcodeproj/project.pbxproj index 3642036e6..11ed9d86b 100755 --- a/proj/xc12/BoE.xcodeproj/project.pbxproj +++ b/proj/xc12/BoE.xcodeproj/project.pbxproj @@ -66,6 +66,8 @@ 413AAF672D38A4A5002E9BF1 /* living.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 914698FB1A7362D900F20F5E /* living.cpp */; }; 413FE08F2CECFAFF000D97DC /* winutil.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 413FE08E2CECFAFF000D97DC /* winutil.cpp */; }; 415EEEB02D5534A500B47408 /* prefs.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 415EEEAF2D5534A500B47408 /* prefs.cpp */; }; + 419889802E4541D10080B0FE /* btnpanel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4198897F2E4541D10080B0FE /* btnpanel.cpp */; }; + 419889812E4541D10080B0FE /* btnpanel.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 4198897E2E4541D10080B0FE /* btnpanel.hpp */; }; 41E550542DEB8C2A00A7DF52 /* scen.undo.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 41E550532DEB8C2A00A7DF52 /* scen.undo.cpp */; }; 91034D211B225E4A008F01C1 /* scen.appleevents.mm in Sources */ = {isa = PBXBuildFile; fileRef = 91034D201B225E49008F01C1 /* scen.appleevents.mm */; }; 911A14031B8FAFC600900FD9 /* town_read.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91C2A6EC1B8FA91400346948 /* town_read.cpp */; }; @@ -638,6 +640,8 @@ 41342CE92DFB872400E66BEB /* quest.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = quest.cpp; sourceTree = ""; }; 413FE08E2CECFAFF000D97DC /* winutil.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = winutil.cpp; sourceTree = ""; }; 415EEEAF2D5534A500B47408 /* prefs.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = prefs.cpp; sourceTree = ""; }; + 4198897E2E4541D10080B0FE /* btnpanel.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = btnpanel.hpp; sourceTree = ""; }; + 4198897F2E4541D10080B0FE /* btnpanel.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = btnpanel.cpp; sourceTree = ""; }; 41E550522DEB8C1400A7DF52 /* scen.undo.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = scen.undo.hpp; sourceTree = ""; }; 41E550532DEB8C2A00A7DF52 /* scen.undo.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = scen.undo.cpp; sourceTree = ""; }; 91034D201B225E49008F01C1 /* scen.appleevents.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = scen.appleevents.mm; sourceTree = ""; }; @@ -1155,6 +1159,8 @@ 91E128F41BC2077700C8BE1D /* pictchoice.hpp */, 91E128F51BC2077700C8BE1D /* strchoice.hpp */, 91E128F61BC2077700C8BE1D /* strdlog.hpp */, + 4198897E2E4541D10080B0FE /* btnpanel.hpp */, + 4198897F2E4541D10080B0FE /* btnpanel.cpp */, ); path = dialogs; sourceTree = ""; @@ -1751,6 +1757,7 @@ 9149924C25913E3F00B5BE97 /* container.hpp in Headers */, 9149924E25913E3F00B5BE97 /* led.hpp in Headers */, 9170C5102D717F24009B6E7C /* scen.locpicker.hpp in Headers */, + 419889812E4541D10080B0FE /* btnpanel.hpp in Headers */, 9143044B2970EDC1003A3967 /* keymods.hpp in Headers */, 9149925025913E3F00B5BE97 /* ledgroup.hpp in Headers */, 915473CE2C800AB000EB1C94 /* enchant.hpp in Headers */, @@ -2202,6 +2209,7 @@ 91E128F11BC2076B00C8BE1D /* strdlog.cpp in Sources */, 91CE248A1EA12866005BDCE4 /* utility.cpp in Sources */, 91CE248C1EA12A96005BDCE4 /* render_text.cpp in Sources */, + 419889802E4541D10080B0FE /* btnpanel.cpp in Sources */, 91CE248E1EA12AA3005BDCE4 /* render_shapes.cpp in Sources */, 91A2480E2969CFD200B8D90F /* res_dialog.cpp in Sources */, 91CE24921EA12ABD005BDCE4 /* gfxsheets.cpp in Sources */, @@ -2498,7 +2506,7 @@ "-Wno-quoted-include-in-framework-header", "-Wno-shorten-64-to-32", "-Wno-comma", - "-Werror=implicit-fallthrough", + "-Wimplicit-fallthrough", ); }; name = Debug; @@ -2612,7 +2620,7 @@ "-Wno-quoted-include-in-framework-header", "-Wno-shorten-64-to-32", "-Wno-comma", - "-Werror=implicit-fallthrough", + "-Wimplicit-fallthrough", ); }; name = Release; diff --git a/rsrc/SConscript b/rsrc/SConscript index e86015816..c01b3a7c2 100644 --- a/rsrc/SConscript +++ b/rsrc/SConscript @@ -57,6 +57,7 @@ scen_env.BuildScenario('#build/rsrc/scenarios/stealth.boes', 'scenarios/stealth/ scen_env.BuildScenario('#build/rsrc/scenarios/zakhazi.boes', 'scenarios/zakhazi/header.exs') env.Install(path.join(install_dir, "Blades of Exile Scenarios"), Glob("#build/rsrc/scenarios/*.boes")) +env.Install(path.join(install_dir, "Blades of Exile Scenarios"), "scenarios/custom") env.Install(path.join(install_dir, "Blades of Exile Base"), Glob("#build/rsrc/bases/*.boes")) # Validate dialogs diff --git a/rsrc/dialogs/about-boe.xml b/rsrc/dialogs/about-boe.xml index f290fba02..b703a0de1 100644 --- a/rsrc/dialogs/about-boe.xml +++ b/rsrc/dialogs/about-boe.xml @@ -18,11 +18,11 @@ ORIGINAL GAME:





- OPEN SOURCE CREDITS:











































































+ OPEN SOURCE CREDITS:













































































SCENARIO FIXES AND UPDATES:

- +
Concept, Design, Programming:
Graphics:
@@ -34,11 +34,11 @@ Graphics:






Consulting:



Testing and Troubleshooting:

- Funding:













































+ Funding:

















































Bandit Busywork:
- +
Jeff Vogel
Andrew Hunter
@@ -84,6 +84,7 @@ Bret Rodabaugh
Dylan Nugent
Evan Mulrooney
+ Glen Chudley
Jake Harrelson
Jared Forcinito
Jeff Potter
@@ -99,6 +100,7 @@ K L
Laura Nelson
Mariann Krizsan
+ Maryanne Wachter
Mike Lapinsky
Nathan Rickey
Nick Chaimov
diff --git a/rsrc/dialogs/adventure-notes.xml b/rsrc/dialogs/adventure-notes.xml index 16a0800a4..2dea6c55a 100644 --- a/rsrc/dialogs/adventure-notes.xml +++ b/rsrc/dialogs/adventure-notes.xml @@ -4,16 +4,22 @@ - - + + + diff --git a/rsrc/dialogs/get-items.xml b/rsrc/dialogs/get-items.xml index 53c053514..c5c26d2c1 100644 --- a/rsrc/dialogs/get-items.xml +++ b/rsrc/dialogs/get-items.xml @@ -4,6 +4,8 @@ + Get all gold diff --git a/rsrc/dialogs/pick-scenario.xml b/rsrc/dialogs/pick-scenario.xml index ead9e7e95..d5ecbc9e5 100644 --- a/rsrc/dialogs/pick-scenario.xml +++ b/rsrc/dialogs/pick-scenario.xml @@ -75,6 +75,7 @@ + - - Shift to this town's entrance in this outdoor section? + + Shift to this town's entrance in outdoor section {sec} at {loc} ({loc_str})? - diff --git a/rsrc/dialogs/tiny-button-panel.xml b/rsrc/dialogs/tiny-button-panel.xml new file mode 100644 index 000000000..246eed583 --- /dev/null +++ b/rsrc/dialogs/tiny-button-panel.xml @@ -0,0 +1,16 @@ + + + + + Select: + + + + diff --git a/rsrc/dialogs/welcome.xml b/rsrc/dialogs/welcome.xml index 4413f472a..dd6aab382 100644 --- a/rsrc/dialogs/welcome.xml +++ b/rsrc/dialogs/welcome.xml @@ -9,10 +9,17 @@ Spiderweb Software, created in 1997 and later released as Free Open-Source Software.
- - http://www.spidweb.com/blades/opensource.html + + You're playing an Itch Edition release of OpenBoE. The Itch Edition + was funded by many generous supporters through IndieGoGo, and is + currently funded by donations on Patreon. Click below to follow the project + and see the rewards for becoming a supporter! - + + http://patreon.com/blades_itch_edition + + + Three exciting original scenarios await you, filled with many hours of puzzles, fighting and adventure! In addition to these, hundreds more written by diff --git a/rsrc/graphics/startanim.png b/rsrc/graphics/startanim.png index e4443d9a8..247de61eb 100644 Binary files a/rsrc/graphics/startanim.png and b/rsrc/graphics/startanim.png differ diff --git a/rsrc/scenarios/custom b/rsrc/scenarios/custom new file mode 160000 index 000000000..1f33aa6bd --- /dev/null +++ b/rsrc/scenarios/custom @@ -0,0 +1 @@ +Subproject commit 1f33aa6bd49486cadcb3aa521d86fe8694179851 diff --git a/rsrc/strings/specials-text-town.txt b/rsrc/strings/specials-text-town.txt index db809e25a..b016130ca 100644 --- a/rsrc/strings/specials-text-town.txt +++ b/rsrc/strings/specials-text-town.txt @@ -353,7 +353,7 @@ Number of town to place party in Skip dialog and always change level? Trigger Limitations Special to Call in New Town -A dialog comes up text you supply (saying, perhaps, they've found a stairway). The dialog buttons are Climb and Leave. If the Leave button is pressed, the Jump To special is used, and the party is not allowed to enter the space. If the Climb button is pushed, the party is moved to another town. +A dialog comes up with text you supply (saying, perhaps, they've found a stairway). The dialog buttons are Climb and Leave. If the Leave button is pressed, the Jump To special is used, and the party is not allowed to enter the space. If the Climb button is pushed, the party is moved to another town. -------------------- Relocate Outdoors Unused diff --git a/src/dialogxml/dialogs/3choice.cpp b/src/dialogxml/dialogs/3choice.cpp index fe45e1dfd..7ca629502 100644 --- a/src/dialogxml/dialogs/3choice.cpp +++ b/src/dialogxml/dialogs/3choice.cpp @@ -155,6 +155,13 @@ void cThreeChoice::init_buttons(cBasicButtonType btn1, cBasicButtonType btn2, cB me->add(btn, cur_btn_rect, sout.str()); cur_btn_rect.right = cur_btn_rect.left - 4; } + // Add a record button and hide it + cButton* record = new cButton(*me); + record->setText("Record"); + record->attachClickHandler(std::bind(&cThreeChoice::onRecord, this, _2)); + cur_btn_rect = {buttons_top,10,buttons_top + 23,buttons_right}; + me->add(record, cur_btn_rect, "record"); + record->hide(); } void cThreeChoice::init_pict(pic_num_t pic){ @@ -174,7 +181,7 @@ std::string cThreeChoice::show(){ return "**ERROR**"; // shouldn't be reached } -short custom_choice_dialog(std::array& strs,short pic_num,ePicType pic_type,std::array& buttons,bool anim_pict,short anim_loops, int anim_fps, cDialog* parent) { +short custom_choice_dialog(std::array& strs,short pic_num,ePicType pic_type,std::array& buttons,bool anim_pict,short anim_loops, int anim_fps, cUniverse* univ, cDialog* parent) { set_cursor(sword_curs); std::vector vec(strs.begin(), strs.end()); @@ -185,6 +192,28 @@ short custom_choice_dialog(std::array& strs,short pic_num,ePicTy if(anim_pict) setup_dialog_pict_anim(*(customDialog.operator->()), "pict", anim_loops, anim_fps); + if(univ != nullptr){ + customDialog.setRecordHandler([vec, univ](cDialog& dlg) -> void { + std::string combined; + for(std::string msg : vec){ + if(!msg.empty()){ + if(!combined.empty()){ + combined += " ||"; + } + combined += msg; + } + } + // It's not necessarily a NOTE_SCEN and location shouldn't be empty, + // but these things aren't actually shown to the party in the encounter notes dialog. + if(univ->party.record(NOTE_SCEN, combined, "")){ + give_help(58,0,dlg); + // TODO ASB is only in game source, but this function is in common source so the scenario editor can preview dialogs :( + /* + add_string_to_buf("Added to encounter notes."); + */ + } + }); + } std::string item_hit = customDialog.show(); for(int i = 0; i < 3; i++) { @@ -220,5 +249,23 @@ short once_dialog(cUniverse& univ, cSpecial& spec, eSpecCtxType cur_type, cDialo showError("Dialog box ended up with no buttons."); return -1; } - return custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c, parent); + return custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c, &univ, parent); +} + +cThreeChoice& cThreeChoice::setRecordHandler(record_callback_t rec){ + if(rec == nullptr){ + hasRecord = false; + dlg["record"].hide(); + }else{ + hasRecord = true; + rec_f = rec; + dlg["record"].show(); + } + return *this; +} + +bool cThreeChoice::onRecord(std::string id){ + if(hasRecord) rec_f(dlg); + else dlg[id].hide(); + return hasRecord; } \ No newline at end of file diff --git a/src/dialogxml/dialogs/3choice.hpp b/src/dialogxml/dialogs/3choice.hpp index 281222be0..612b8c7dd 100644 --- a/src/dialogxml/dialogs/3choice.hpp +++ b/src/dialogxml/dialogs/3choice.hpp @@ -37,6 +37,9 @@ namespace {cBasicButtonType null_btn = boost::none;} extern bbtt basic_buttons[71]; #endif +/// The signature of a record handler for cThreeChoice. +typedef std::function record_callback_t; + /// A choice dialog with several strings and up to three buttons. /// This is the class used for dialogs generated by special nodes. /// It generates the dialog dynamically from the given input. @@ -48,6 +51,9 @@ class cThreeChoice : public cChoiceDlog { void init_buttons(cBasicButtonType btn1, cBasicButtonType btn2, cBasicButtonType btn3); void init_pict(pic_num_t pic); const ePicType type; + record_callback_t rec_f; + bool hasRecord; + bool onRecord(std::string id); public: /// Create a dialog with just one button. /// @param strings A list of the strings to place in the dialog. @@ -70,12 +76,21 @@ class cThreeChoice : public cChoiceDlog { /// @param t The type of the icon. /// @param parent Optionally, a parent dialog. cThreeChoice(std::vector& strings, std::array& buttons, pic_num_t pic, ePicType t, cDialog* parent = nullptr); + /// Set a record handler. + /// @param rec The handler. + /// @return This object, for method-call chaining. + /// @note Only one record handler can be set at a time. To remove it, set it to null. + /// @note The presence of the Record button is determined entirely by the presence of a record handler. + /// + /// A record handler should take one parameter, which is a reference to the dialog. + /// (That's the cDialog, not the cThreeChoice.) It should return void. + cThreeChoice& setRecordHandler(record_callback_t rec); /// @copydoc cChoiceDlog::show() /// @note The unique key in this case is the label specified in the button specification. std::string show(); }; -short custom_choice_dialog(std::array& strs,short pic_num,ePicType pic_type,std::array& buttons, bool anim_pict = false, short anim_loops = -1, int anim_fps = -1, cDialog* parent = nullptr); +short custom_choice_dialog(std::array& strs,short pic_num,ePicType pic_type,std::array& buttons, bool anim_pict = false, short anim_loops = -1, int anim_fps = -1, cUniverse* univ = nullptr, cDialog* parent = nullptr); short once_dialog(cUniverse& univ, cSpecial& spec, eSpecCtxType cur_type, cDialog* parent = nullptr); #endif diff --git a/src/dialogxml/dialogs/btnpanel.cpp b/src/dialogxml/dialogs/btnpanel.cpp new file mode 100644 index 000000000..086398cb0 --- /dev/null +++ b/src/dialogxml/dialogs/btnpanel.cpp @@ -0,0 +1,111 @@ +#include "btnpanel.hpp" + +#include +#include + +#include +#include + +#include "dialogxml/widgets/field.hpp" +#include "dialogxml/dialogs/strdlog.hpp" +#include "fileio/resmgr/res_dialog.hpp" +#include "sounds.hpp" +#include "gfx/render_shapes.hpp" +#include "tools/cursors.hpp" + +cButtonPanel::cButtonPanel(cDialog* parent) + : dlg(*ResMgr::dialogs.get("tiny-button-panel"), parent) +{} + +cButtonPanel::cButtonPanel(const std::vector& strs, std::vector> click_handlers, std::string title, std::string ok_str, cDialog* parent) + : cButtonPanel(parent) +{ + setTitle(title); + if(!ok_str.empty()){ + dlg["done"].setText(ok_str); + } + strings = strs; + this->click_handlers = click_handlers; + attachHandlers(); +} + +void cButtonPanel::attachHandlers() { + using namespace std::placeholders; + dlg["left"].attachClickHandler(std::bind(&cButtonPanel::onLeft,this)); + dlg["right"].attachClickHandler(std::bind(&cButtonPanel::onRight,this)); + dlg["done"].attachClickHandler(std::bind(&cButtonPanel::onOkay,this,_1)); + dlg["cancel"].attachClickHandler(std::bind(&cButtonPanel::onCancel,this,_1)); + if(strings.size() <= per_page) { + dlg["left"].hide(); + dlg["right"].hide(); + } + // Attach click handler to the tiny buttons + for(int i = 0; i < per_page; ++i){ + short string_idx = page * per_page + i; + std::ostringstream sout; + sout << "button" << i + 1; + dlg[sout.str()].attachClickHandler([i, this](cDialog&,std::string,eKeyMod) -> bool { + click_handlers[i](*this); + return true; + }); + } +} + +cDialog* cButtonPanel::operator->() { + return &dlg; +} + +bool cButtonPanel::show() { + page = 0; + dlg.run(std::bind(&cButtonPanel::fillPage, this)); + return dlg.getResult(); +} + +void cButtonPanel::fillPage(){ + for(unsigned int i = 0; i < per_page; i++){ + short string_idx = page * per_page + i; + std::ostringstream sout; + sout << "button" << i + 1; + if(string_idx < strings.size()){ + dlg[sout.str()].setText(strings[string_idx]); + dlg[sout.str()].recalcRect(); + dlg[sout.str()].show(); + }else{ + dlg[sout.str()].hide(); + } + } +} + +bool cButtonPanel::onLeft(){ + if(page == 0) page = lastPage(); + else page--; + fillPage(); + return true; +} + +bool cButtonPanel::onRight(){ + if(page == lastPage()) page = 0; + else page++; + fillPage(); + return true; +} + +bool cButtonPanel::onCancel(cDialog& me){ + dlg.setResult(false); + me.toast(false); + return true; +} + +bool cButtonPanel::onOkay(cDialog& me){ + dlg.setResult(true); + me.toast(true); + return true; +} + +void cButtonPanel::setTitle(const std::string &title) { + if(!title.empty()) dlg["title"].setText(title); +} + +size_t cButtonPanel::lastPage() const { + return (strings.size() - 1) / per_page; +} \ No newline at end of file diff --git a/src/dialogxml/dialogs/btnpanel.hpp b/src/dialogxml/dialogs/btnpanel.hpp new file mode 100644 index 000000000..903172967 --- /dev/null +++ b/src/dialogxml/dialogs/btnpanel.hpp @@ -0,0 +1,47 @@ +#ifndef DIALOG_BTNPANEL_H +#define DIALOG_BTNPANEL_H + +#include +#include +#include +#include +#include "dialog.hpp" +#include "dialogxml/widgets/ledgroup.hpp" + +/// A dialog that presents a list of labeled tiny buttons, plus an OK and Cancel button. +/// The list may span several pages. +class cButtonPanel { + const size_t per_page = 5; + cDialog dlg; + bool onLeft(); + bool onRight(); + bool onCancel(cDialog& me); + bool onOkay(cDialog& me); + void attachHandlers(); + void fillPage(); + size_t lastPage() const; + std::vector strings; + std::vector> click_handlers; + size_t page, cur; + cButtonPanel(cDialog* parent); +public: + /// Initializes a dialog from a list of strings. + /// @param strs A list of all strings in the dialog. + /// @param click_handlers A list of click handlers for the strings + /// @param title The title to show in the dialog. + /// @param parent Optionally, a parent dialog. + explicit cButtonPanel(const std::vector& strs, std::vector> click_handlers, std::string title, std::string ok_str = "", cDialog* parent = nullptr); + /// Reference the cDialog powering this choice dialog, perhaps to customize details of it. + /// @return A pointer to the dialog. + cDialog* operator->(); + /// Show the dialog. + /// @return True if OK was pressed, false if Cancel pressed + bool show(); + /// Set the dialog's title. + /// @param title The new title. + void setTitle(const std::string& title); + /// Get the list of strings. + std::vector getStrings() const { return strings; } +}; + +#endif diff --git a/src/dialogxml/dialogs/choicedlog.hpp b/src/dialogxml/dialogs/choicedlog.hpp index 318cca912..37e60c424 100644 --- a/src/dialogxml/dialogs/choicedlog.hpp +++ b/src/dialogxml/dialogs/choicedlog.hpp @@ -17,8 +17,8 @@ /// This class loads a definition from a file, so there can be any amount of other stuff in the dialog, /// and the buttons could be arranged in any fashion you want. class cChoiceDlog { - cDialog dlg; protected: + cDialog dlg; /// The click handler for the dialog's buttons. /// @param me A reference to the current dialog. /// @param id The unique key of the clicked control. diff --git a/src/fileio/fileio_party.cpp b/src/fileio/fileio_party.cpp index f22a83fd8..716702146 100644 --- a/src/fileio/fileio_party.cpp +++ b/src/fileio/fileio_party.cpp @@ -284,6 +284,7 @@ bool load_party_v1(fs::path file_to_load, cUniverse& real_univ, bool town_restor univ.file = path; }else{ univ.party.scen_name = ""; + store_party.scen_name[0] = '\0'; } univ.party.import_legacy(store_party, univ); diff --git a/src/fileio/fileio_scen.cpp b/src/fileio/fileio_scen.cpp index 030c8e3e7..06078ff04 100644 --- a/src/fileio/fileio_scen.cpp +++ b/src/fileio/fileio_scen.cpp @@ -9,10 +9,13 @@ #include "fileio.hpp" #include +#include #include #include #include #include +#include "tools/replay.hpp" +#include "dialogxml/dialogs/strchoice.hpp" #include "dialogxml/dialogs/strdlog.hpp" @@ -27,6 +30,7 @@ #include "replay.hpp" #include "porting.hpp" +#include "fileio/mac_roman.hpp" #include "fileio/resmgr/res_image.hpp" #include "fileio/resmgr/res_sound.hpp" @@ -41,10 +45,11 @@ extern std::string last_load_file; void load_spec_graphics_v1(fs::path scen_file); void load_spec_graphics_v2(int num_sheets); +std::string decode_temp_str(std::string temp_str, std::string encoding); // Load old scenarios (town talk is handled by the town loading function) static bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario load_type); -static bool load_outdoors_v1(fs::path scen_file, location which_out,cOutdoors& the_out, legacy::scenario_data_type& scenario); -static bool load_town_v1(fs::path scen_file,short which_town,cTown& the_town,legacy::scenario_data_type& scenario,std::vector& shops); +static bool load_outdoors_v1(fs::path scen_file, location which_out,cOutdoors& the_out, legacy::scenario_data_type& scenario, std::string encoding); +static bool load_town_v1(fs::path scen_file,short which_town,cTown& the_town,legacy::scenario_data_type& scenario,std::vector& shops, std::string encoding); // Load new scenarios static bool load_scenario_v2(fs::path file_to_load, cScenario& scenario, eLoadScenario load_type); // Some of these are non-static so that the test cases can access them. @@ -69,8 +74,8 @@ static std::string get_file_error() { // Debug builds run from working directory "build/Blades of Exile" // should helpfully let you enter replay test scenarios. -// Support for multiple scenario directories will also help with how I plan -// to handle scenario packaging/distribution for my fork's release. +// The Itch Edition finds scenarios in the Itch client's install path, +// so scenarios can be distributed as Itch games. // - Nat std::vector all_scen_dirs() { std::vector scen_dirs = { scenDir }; @@ -79,6 +84,9 @@ std::vector all_scen_dirs() { scen_dirs.push_back(scen_dir); } + // Experimental: ship with the full scenario archive + scen_dirs.push_back(progDir/"Blades of Exile Scenarios/custom"); + #ifdef DEBUG fs::path replay_scenarios_dir = boost::filesystem::current_path(); replay_scenarios_dir = replay_scenarios_dir/".."/".."/"test"/"replays"/"scenarios"; @@ -87,6 +95,30 @@ std::vector all_scen_dirs() { } #endif + // Itch Edition: Search Itch client apps. + // But not recursively, because the player could have tons of games installed + // and that would search ALL of the installed files. + // In fact, try to disqualify folders ASAP by only allowing .boes, .txt, + // and Itch meta files + // (designers might want to ship a README.txt) + fs::path itch_apps_path = scenDir/".."/".."/"itch"/"apps"; + if(fs::is_directory(itch_apps_path)){ + for(fs::directory_iterator app_iter(itch_apps_path); app_iter != fs::directory_iterator(); app_iter++){ + fs::path app = *app_iter; + if(!fs::is_directory(app)) continue; + for(fs::directory_iterator file_iter(app); file_iter != fs::directory_iterator(); file_iter++){ + fs::path file = *file_iter; + if(file.extension() == ".boes"){ + scen_dirs.push_back(*app_iter); + break; + }else if(file.extension() == ".itch"){ + }else if(file.extension() != ".txt"){ + break; + } + } + } + } + return scen_dirs; } @@ -266,37 +298,96 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario return false; } port_item_list(&item_data); - scenario.import_legacy(temp_scenario); - scenario.import_legacy(item_data); + scenario.import_legacy(temp_scenario, load_type == eLoadScenario::ONLY_HEADER); + if(load_type == eLoadScenario::FULL){ + scenario.ter_types[23].fly_over = false; + scenario.import_legacy(item_data); + } // TODO: Consider skipping the fread and assignment when len is 0 scenario.special_items.resize(50); scenario.journal_strs.resize(50); scenario.spec_strs.resize(100); + + static std::vector encodings_to_try = {"Latin1", "Windows-1252", "MacRoman"}; + fs::path meta = file_to_load.parent_path() / "meta.xml"; + using namespace ticpp; + ticpp::Document meta_doc; + if(!fs::exists(meta)){ + ticpp::Element root_element("meta"); + meta_doc.InsertEndChild(root_element); + }else{ + meta_doc.LoadFile(meta.string()); + } + + auto info = info_from_action(*meta_doc.FirstChildElement()); + std::string encoding = ""; + for(short i = 0; i < 270; i++) { len = (long) (temp_scenario.scen_str_len[i]); fread(temp_str, len, 1, file_id); temp_str[len] = 0; - if(i == 0) scenario.scen_name = temp_str; + + std::string decoded; + std::vector options; + if(info.find("encoding") != info.end() && !info["encoding"].empty()){ + encoding = info["encoding"]; + decoded = decode_temp_str(temp_str, encoding); + }else{ + bool different = false; + for(std::string encoding : encodings_to_try){ + std::string enc = decode_temp_str(temp_str, encoding); + if(!options.empty() && enc != options.back()) different = true; + options.push_back(enc); + } + if(different){ + LOG_VALUE(file_to_load); + for(std::string enc : options){ + LOG_VALUE(enc); + } + int which = -1; + // Comment this out if you're not messing with the metadata: + // which = cStringChoice(options, "Which is best?").show(-1); + if(which != -1){ + encoding = info["encoding"] = encodings_to_try[which]; + decoded = options[which]; + } + else{ + decoded = options[0]; // temp! + } + }else{ + decoded = options.back(); + } + } + + if(i == 0) scenario.scen_name = decoded; else if(i == 1 || i == 2) - scenario.teaser_text[i-1] = temp_str; + scenario.teaser_text[i-1] = decoded; else if(i == 3) - scenario.contact_info[1] = temp_str; + scenario.contact_info[1] = decoded; else if(i >= 4 && i < 10) - scenario.intro_strs[i-4] = temp_str; + scenario.intro_strs[i-4] = decoded; else if(i >= 10 && i < 60) - scenario.journal_strs[i-10] = temp_str; + scenario.journal_strs[i-10] = decoded; else if(i >= 60 && i < 160) { - if(i % 2 == 0) scenario.special_items[(i-60)/2].name = temp_str; - else scenario.special_items[(i-60)/2].descr = temp_str; + if(i % 2 == 0) scenario.special_items[(i-60)/2].name = decoded; + else scenario.special_items[(i-60)/2].descr = decoded; } else if(i >= 260) continue; // These were never ever used, for some reason. - else scenario.spec_strs[i-160] = temp_str; + else scenario.spec_strs[i-160] = decoded; } - + Element new_root("meta"); + ticpp::Document new_doc; + for(auto& p : info){ + Element next_child(p.first); + Text child_text(p.second); + next_child.InsertEndChild(child_text); + new_root.InsertEndChild(next_child); + } + new_doc.InsertEndChild(new_root); + new_doc.SaveFile(meta.string()); + fclose(file_id); - scenario.ter_types[23].fly_over = false; - scenario.scen_file = file_to_load; if(load_type == eLoadScenario::ONLY_HEADER) return true; load_spec_graphics_v1(scenario.scen_file); @@ -306,7 +397,7 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario for(int x = 0; x < temp_scenario.out_width; x++) { for(int y = 0; y < temp_scenario.out_height; y++) { scenario.outdoors[x][y] = new cOutdoors(scenario); - load_outdoors_v1(scenario.scen_file, loc(x,y), *scenario.outdoors[x][y], temp_scenario); + load_outdoors_v1(scenario.scen_file, loc(x,y), *scenario.outdoors[x][y], temp_scenario, encoding); } } @@ -321,7 +412,7 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario case 1: scenario.towns[i] = new cTown(scenario, AREA_MEDIUM); break; case 2: scenario.towns[i] = new cTown(scenario, AREA_SMALL); break; } - load_town_v1(scenario.scen_file, i, *scenario.towns[i], temp_scenario, shops); + load_town_v1(scenario.scen_file, i, *scenario.towns[i], temp_scenario, shops, encoding); } // Enable character creation in starting town scenario.towns[scenario.which_town_start]->has_tavern = true; @@ -2442,7 +2533,7 @@ static long get_town_offset(short which_town, legacy::scenario_data_type& scenar return len_to_jump; } -bool load_town_v1(fs::path scen_file, short which_town, cTown& the_town, legacy::scenario_data_type& scenario, std::vector& shops) { +bool load_town_v1(fs::path scen_file, short which_town, cTown& the_town, legacy::scenario_data_type& scenario, std::vector& shops, std::string encoding) { long len,len_to_jump = 0; char temp_str[256]; legacy::town_record_type store_town; @@ -2506,8 +2597,11 @@ bool load_town_v1(fs::path scen_file, short which_town, cTown& the_town, legacy: len = (long) (store_town.strlens[i]); fread(temp_str, len, 1, file_id); temp_str[len] = 0; - // Trim whitespace off of strings, which are fixed-width in legacy scenarios std::string temp_str_trimmed = temp_str; + if(!encoding.empty()){ + temp_str_trimmed = decode_temp_str(temp_str, encoding); + } + // Trim whitespace off of strings, which are fixed-width in legacy scenarios boost::algorithm::trim_right(temp_str_trimmed); // Whitespace in front of a string would be weird, but possibly intentional if(i == 0) the_town.name = temp_str_trimmed; else if(i >= 1 && i < 17) @@ -2533,20 +2627,26 @@ bool load_town_v1(fs::path scen_file, short which_town, cTown& the_town, legacy: len = (long) (store_talk.strlens[i]); fread(temp_str, len, 1, file_id); temp_str[len] = 0; + std::string temp_str_trimmed = temp_str; + if(!encoding.empty()){ + temp_str_trimmed = decode_temp_str(temp_str, encoding); + } + // Trim whitespace off of strings, which are fixed-width in legacy scenarios + boost::algorithm::trim_right(temp_str_trimmed); // Whitespace in front of a string would be weird, but possibly intentional if(i >= 0 && i < 10) - the_town.talking.people[i].title = temp_str; + the_town.talking.people[i].title = temp_str_trimmed; else if(i >= 10 && i < 20) - the_town.talking.people[i-10].look = temp_str; + the_town.talking.people[i-10].look = temp_str_trimmed; else if(i >= 20 && i < 30) - the_town.talking.people[i-20].name = temp_str; + the_town.talking.people[i-20].name = temp_str_trimmed; else if(i >= 30 && i < 40) - the_town.talking.people[i-30].job = temp_str; + the_town.talking.people[i-30].job = temp_str_trimmed; else if(i >= 160) - the_town.talking.people[i-160].dunno = temp_str; + the_town.talking.people[i-160].dunno = temp_str_trimmed; else { if(i % 2 == 0) - the_town.talking.talk_nodes[(i-40)/2].str1 = temp_str; - else the_town.talking.talk_nodes[(i-40)/2].str2 = temp_str; + the_town.talking.talk_nodes[(i-40)/2].str1 = temp_str_trimmed; + else the_town.talking.talk_nodes[(i-40)/2].str2 = temp_str_trimmed; } } @@ -2583,7 +2683,7 @@ static long get_outdoors_offset(location& which_out, legacy::scenario_data_type& } //mode -> 0 - primary load 1 - add to top 2 - right 3 - bottom 4 - left -bool load_outdoors_v1(fs::path scen_file, location which_out,cOutdoors& the_out, legacy::scenario_data_type& scenario){ +bool load_outdoors_v1(fs::path scen_file, location which_out,cOutdoors& the_out, legacy::scenario_data_type& scenario, std::string encoding){ long len,len_to_jump; char temp_str[256]; legacy::outdoor_record_type store_out; @@ -2619,13 +2719,19 @@ bool load_outdoors_v1(fs::path scen_file, location which_out,cOutdoors& the_out, len = (long) (store_out.strlens[i]); fread(temp_str, len, 1, file_id); temp_str[len] = 0; - if(i == 0) the_out.name = temp_str; - else if(i == 9) the_out.comment = temp_str; - else if(i < 9) the_out.area_desc[i-1].descr = temp_str; + std::string temp_str_trimmed = temp_str; + if(!encoding.empty()){ + temp_str_trimmed = decode_temp_str(temp_str, encoding); + } + // Trim whitespace off of strings, which are fixed-width in legacy scenarios + boost::algorithm::trim_right(temp_str_trimmed); // Whitespace in front of a string would be weird, but possibly intentional + if(i == 0) the_out.name = temp_str_trimmed; + else if(i == 9) the_out.comment = temp_str_trimmed; + else if(i < 9) the_out.area_desc[i-1].descr = temp_str_trimmed; else if(i >= 10 && i < 100) - the_out.spec_strs[i-10] = temp_str; + the_out.spec_strs[i-10] = temp_str_trimmed; else if(i >= 100 && i < 108) - the_out.sign_locs[i-100].text = temp_str; + the_out.sign_locs[i-100].text = temp_str_trimmed; } if(fclose(file_id) != 0) { @@ -2696,3 +2802,15 @@ void load_spec_graphics_v2(int num_sheets) { spec_scen_g.sheets[num_sheets] = &ResMgr::graphics.get(name); } } + +std::string decode_temp_str(std::string temp_str, std::string encoding) { + if(encoding == "MacRoman"){ + std::basic_string utf16; + std::basic_string utf8; + for(char byte : temp_str){ + utf16.push_back(gMORToUnicode[(unsigned char) byte]); + } + return boost::locale::conv::utf_to_utf(utf16); + } + return boost::locale::conv::to_utf(temp_str, encoding); +} \ No newline at end of file diff --git a/src/fileio/mac_roman.hpp b/src/fileio/mac_roman.hpp new file mode 100644 index 000000000..54b34921a --- /dev/null +++ b/src/fileio/mac_roman.hpp @@ -0,0 +1,278 @@ +// Source: https://github.com/fadden/nulib2/blob/master/nufxlib/Charset.c + +/* + * NuFX archive manipulation library + * Copyright (C) 2014 by Andy McFadden, All Rights Reserved. + * This is free software; you can redistribute it and/or modify it under the + * terms of the BSD License, see the file COPYING-LIB. + * + * Miscellaneous NufxLib utility functions. + */ + + /* + * Convert Mac OS Roman to Unicode. Mapping comes from: + * + * http://www.unicode.org/Public/MAPPINGS/VENDORS/APPLE/ROMAN.TXT + * + * We use the "Control Pictures" block for the control characters + * (0x00-0x1f, 0x7f --> 0x2400-0x241f, 0x2421). This is a bit nicer + * than embedding control characters in filenames. + */ +static const uint16_t gMORToUnicode[256] = { + /*0x00*/ 0x2400, // [control] NULL + /*0x01*/ 0x2401, // [control] START OF HEADING + /*0x02*/ 0x2402, // [control] START OF TEXT + /*0x03*/ 0x2403, // [control] END OF TEXT + /*0x04*/ 0x2404, // [control] END OF TRANSMISSION + /*0x05*/ 0x2405, // [control] ENQUIRY + /*0x06*/ 0x2406, // [control] ACKNOWLEDGE + /*0x07*/ 0x2407, // [control] BELL + /*0x08*/ 0x2408, // [control] BACKSPACE + /*0x09*/ 0x2409, // [control] HORIZONTAL TABULATION + /*0x0a*/ 0x240a, // [control] LINE FEED + /*0x0b*/ 0x240b, // [control] VERTICAL TABULATION + /*0x0c*/ 0x240c, // [control] FORM FEED + /*0x0d*/ 0x240d, // [control] CARRIAGE RETURN + /*0x0e*/ 0x240e, // [control] SHIFT OUT + /*0x0f*/ 0x240f, // [control] SHIFT IN + /*0x10*/ 0x2410, // [control] DATA LINK ESCAPE + /*0x11*/ 0x2411, // [control] DEVICE CONTROL ONE + /*0x12*/ 0x2412, // [control] DEVICE CONTROL TWO + /*0x13*/ 0x2413, // [control] DEVICE CONTROL THREE + /*0x14*/ 0x2414, // [control] DEVICE CONTROL FOUR + /*0x15*/ 0x2415, // [control] NEGATIVE ACKNOWLEDGE + /*0x16*/ 0x2416, // [control] SYNCHRONOUS IDLE + /*0x17*/ 0x2417, // [control] END OF TRANSMISSION BLOCK + /*0x18*/ 0x2418, // [control] CANCEL + /*0x19*/ 0x2419, // [control] END OF MEDIUM + /*0x1a*/ 0x241a, // [control] SUBSTITUTE + /*0x1b*/ 0x241b, // [control] ESCAPE + /*0x1c*/ 0x241c, // [control] FILE SEPARATOR + /*0x1d*/ 0x241d, // [control] GROUP SEPARATOR + /*0x1e*/ 0x241e, // [control] RECORD SEPARATOR + /*0x1f*/ 0x241f, // [control] UNIT SEPARATOR + /*0x20*/ 0x0020, // SPACE + /*0x21*/ 0x0021, // EXCLAMATION MARK + /*0x22*/ 0x0022, // QUOTATION MARK + /*0x23*/ 0x0023, // NUMBER SIGN + /*0x24*/ 0x0024, // DOLLAR SIGN + /*0x25*/ 0x0025, // PERCENT SIGN + /*0x26*/ 0x0026, // AMPERSAND + /*0x27*/ 0x0027, // APOSTROPHE + /*0x28*/ 0x0028, // LEFT PARENTHESIS + /*0x29*/ 0x0029, // RIGHT PARENTHESIS + /*0x2A*/ 0x002A, // ASTERISK + /*0x2B*/ 0x002B, // PLUS SIGN + /*0x2C*/ 0x002C, // COMMA + /*0x2D*/ 0x002D, // HYPHEN-MINUS + /*0x2E*/ 0x002E, // FULL STOP + /*0x2F*/ 0x002F, // SOLIDUS + /*0x30*/ 0x0030, // DIGIT ZERO + /*0x31*/ 0x0031, // DIGIT ONE + /*0x32*/ 0x0032, // DIGIT TWO + /*0x33*/ 0x0033, // DIGIT THREE + /*0x34*/ 0x0034, // DIGIT FOUR + /*0x35*/ 0x0035, // DIGIT FIVE + /*0x36*/ 0x0036, // DIGIT SIX + /*0x37*/ 0x0037, // DIGIT SEVEN + /*0x38*/ 0x0038, // DIGIT EIGHT + /*0x39*/ 0x0039, // DIGIT NINE + /*0x3A*/ 0x003A, // COLON + /*0x3B*/ 0x003B, // SEMICOLON + /*0x3C*/ 0x003C, // LESS-THAN SIGN + /*0x3D*/ 0x003D, // EQUALS SIGN + /*0x3E*/ 0x003E, // GREATER-THAN SIGN + /*0x3F*/ 0x003F, // QUESTION MARK + /*0x40*/ 0x0040, // COMMERCIAL AT + /*0x41*/ 0x0041, // LATIN CAPITAL LETTER A + /*0x42*/ 0x0042, // LATIN CAPITAL LETTER B + /*0x43*/ 0x0043, // LATIN CAPITAL LETTER C + /*0x44*/ 0x0044, // LATIN CAPITAL LETTER D + /*0x45*/ 0x0045, // LATIN CAPITAL LETTER E + /*0x46*/ 0x0046, // LATIN CAPITAL LETTER F + /*0x47*/ 0x0047, // LATIN CAPITAL LETTER G + /*0x48*/ 0x0048, // LATIN CAPITAL LETTER H + /*0x49*/ 0x0049, // LATIN CAPITAL LETTER I + /*0x4A*/ 0x004A, // LATIN CAPITAL LETTER J + /*0x4B*/ 0x004B, // LATIN CAPITAL LETTER K + /*0x4C*/ 0x004C, // LATIN CAPITAL LETTER L + /*0x4D*/ 0x004D, // LATIN CAPITAL LETTER M + /*0x4E*/ 0x004E, // LATIN CAPITAL LETTER N + /*0x4F*/ 0x004F, // LATIN CAPITAL LETTER O + /*0x50*/ 0x0050, // LATIN CAPITAL LETTER P + /*0x51*/ 0x0051, // LATIN CAPITAL LETTER Q + /*0x52*/ 0x0052, // LATIN CAPITAL LETTER R + /*0x53*/ 0x0053, // LATIN CAPITAL LETTER S + /*0x54*/ 0x0054, // LATIN CAPITAL LETTER T + /*0x55*/ 0x0055, // LATIN CAPITAL LETTER U + /*0x56*/ 0x0056, // LATIN CAPITAL LETTER V + /*0x57*/ 0x0057, // LATIN CAPITAL LETTER W + /*0x58*/ 0x0058, // LATIN CAPITAL LETTER X + /*0x59*/ 0x0059, // LATIN CAPITAL LETTER Y + /*0x5A*/ 0x005A, // LATIN CAPITAL LETTER Z + /*0x5B*/ 0x005B, // LEFT SQUARE BRACKET + /*0x5C*/ 0x005C, // REVERSE SOLIDUS + /*0x5D*/ 0x005D, // RIGHT SQUARE BRACKET + /*0x5E*/ 0x005E, // CIRCUMFLEX ACCENT + /*0x5F*/ 0x005F, // LOW LINE + /*0x60*/ 0x0060, // GRAVE ACCENT + /*0x61*/ 0x0061, // LATIN SMALL LETTER A + /*0x62*/ 0x0062, // LATIN SMALL LETTER B + /*0x63*/ 0x0063, // LATIN SMALL LETTER C + /*0x64*/ 0x0064, // LATIN SMALL LETTER D + /*0x65*/ 0x0065, // LATIN SMALL LETTER E + /*0x66*/ 0x0066, // LATIN SMALL LETTER F + /*0x67*/ 0x0067, // LATIN SMALL LETTER G + /*0x68*/ 0x0068, // LATIN SMALL LETTER H + /*0x69*/ 0x0069, // LATIN SMALL LETTER I + /*0x6A*/ 0x006A, // LATIN SMALL LETTER J + /*0x6B*/ 0x006B, // LATIN SMALL LETTER K + /*0x6C*/ 0x006C, // LATIN SMALL LETTER L + /*0x6D*/ 0x006D, // LATIN SMALL LETTER M + /*0x6E*/ 0x006E, // LATIN SMALL LETTER N + /*0x6F*/ 0x006F, // LATIN SMALL LETTER O + /*0x70*/ 0x0070, // LATIN SMALL LETTER P + /*0x71*/ 0x0071, // LATIN SMALL LETTER Q + /*0x72*/ 0x0072, // LATIN SMALL LETTER R + /*0x73*/ 0x0073, // LATIN SMALL LETTER S + /*0x74*/ 0x0074, // LATIN SMALL LETTER T + /*0x75*/ 0x0075, // LATIN SMALL LETTER U + /*0x76*/ 0x0076, // LATIN SMALL LETTER V + /*0x77*/ 0x0077, // LATIN SMALL LETTER W + /*0x78*/ 0x0078, // LATIN SMALL LETTER X + /*0x79*/ 0x0079, // LATIN SMALL LETTER Y + /*0x7A*/ 0x007A, // LATIN SMALL LETTER Z + /*0x7B*/ 0x007B, // LEFT CURLY BRACKET + /*0x7C*/ 0x007C, // VERTICAL LINE + /*0x7D*/ 0x007D, // RIGHT CURLY BRACKET + /*0x7E*/ 0x007E, // TILDE + /*0x7f*/ 0x2421, // [control] DELETE + /*0x80*/ 0x00C4, // LATIN CAPITAL LETTER A WITH DIAERESIS + /*0x81*/ 0x00C5, // LATIN CAPITAL LETTER A WITH RING ABOVE + /*0x82*/ 0x00C7, // LATIN CAPITAL LETTER C WITH CEDILLA + /*0x83*/ 0x00C9, // LATIN CAPITAL LETTER E WITH ACUTE + /*0x84*/ 0x00D1, // LATIN CAPITAL LETTER N WITH TILDE + /*0x85*/ 0x00D6, // LATIN CAPITAL LETTER O WITH DIAERESIS + /*0x86*/ 0x00DC, // LATIN CAPITAL LETTER U WITH DIAERESIS + /*0x87*/ 0x00E1, // LATIN SMALL LETTER A WITH ACUTE + /*0x88*/ 0x00E0, // LATIN SMALL LETTER A WITH GRAVE + /*0x89*/ 0x00E2, // LATIN SMALL LETTER A WITH CIRCUMFLEX + /*0x8A*/ 0x00E4, // LATIN SMALL LETTER A WITH DIAERESIS + /*0x8B*/ 0x00E3, // LATIN SMALL LETTER A WITH TILDE + /*0x8C*/ 0x00E5, // LATIN SMALL LETTER A WITH RING ABOVE + /*0x8D*/ 0x00E7, // LATIN SMALL LETTER C WITH CEDILLA + /*0x8E*/ 0x00E9, // LATIN SMALL LETTER E WITH ACUTE + /*0x8F*/ 0x00E8, // LATIN SMALL LETTER E WITH GRAVE + /*0x90*/ 0x00EA, // LATIN SMALL LETTER E WITH CIRCUMFLEX + /*0x91*/ 0x00EB, // LATIN SMALL LETTER E WITH DIAERESIS + /*0x92*/ 0x00ED, // LATIN SMALL LETTER I WITH ACUTE + /*0x93*/ 0x00EC, // LATIN SMALL LETTER I WITH GRAVE + /*0x94*/ 0x00EE, // LATIN SMALL LETTER I WITH CIRCUMFLEX + /*0x95*/ 0x00EF, // LATIN SMALL LETTER I WITH DIAERESIS + /*0x96*/ 0x00F1, // LATIN SMALL LETTER N WITH TILDE + /*0x97*/ 0x00F3, // LATIN SMALL LETTER O WITH ACUTE + /*0x98*/ 0x00F2, // LATIN SMALL LETTER O WITH GRAVE + /*0x99*/ 0x00F4, // LATIN SMALL LETTER O WITH CIRCUMFLEX + /*0x9A*/ 0x00F6, // LATIN SMALL LETTER O WITH DIAERESIS + /*0x9B*/ 0x00F5, // LATIN SMALL LETTER O WITH TILDE + /*0x9C*/ 0x00FA, // LATIN SMALL LETTER U WITH ACUTE + /*0x9D*/ 0x00F9, // LATIN SMALL LETTER U WITH GRAVE + /*0x9E*/ 0x00FB, // LATIN SMALL LETTER U WITH CIRCUMFLEX + /*0x9F*/ 0x00FC, // LATIN SMALL LETTER U WITH DIAERESIS + /*0xA0*/ 0x2020, // DAGGER + /*0xA1*/ 0x00B0, // DEGREE SIGN + /*0xA2*/ 0x00A2, // CENT SIGN + /*0xA3*/ 0x00A3, // POUND SIGN + /*0xA4*/ 0x00A7, // SECTION SIGN + /*0xA5*/ 0x2022, // BULLET + /*0xA6*/ 0x00B6, // PILCROW SIGN + /*0xA7*/ 0x00DF, // LATIN SMALL LETTER SHARP S + /*0xA8*/ 0x00AE, // REGISTERED SIGN + /*0xA9*/ 0x00A9, // COPYRIGHT SIGN + /*0xAA*/ 0x2122, // TRADE MARK SIGN + /*0xAB*/ 0x00B4, // ACUTE ACCENT + /*0xAC*/ 0x00A8, // DIAERESIS + /*0xAD*/ 0x2260, // NOT EQUAL TO + /*0xAE*/ 0x00C6, // LATIN CAPITAL LETTER AE + /*0xAF*/ 0x00D8, // LATIN CAPITAL LETTER O WITH STROKE + /*0xB0*/ 0x221E, // INFINITY + /*0xB1*/ 0x00B1, // PLUS-MINUS SIGN + /*0xB2*/ 0x2264, // LESS-THAN OR EQUAL TO + /*0xB3*/ 0x2265, // GREATER-THAN OR EQUAL TO + /*0xB4*/ 0x00A5, // YEN SIGN + /*0xB5*/ 0x00B5, // MICRO SIGN + /*0xB6*/ 0x2202, // PARTIAL DIFFERENTIAL + /*0xB7*/ 0x2211, // N-ARY SUMMATION + /*0xB8*/ 0x220F, // N-ARY PRODUCT + /*0xB9*/ 0x03C0, // GREEK SMALL LETTER PI + /*0xBA*/ 0x222B, // INTEGRAL + /*0xBB*/ 0x00AA, // FEMININE ORDINAL INDICATOR + /*0xBC*/ 0x00BA, // MASCULINE ORDINAL INDICATOR + /*0xBD*/ 0x03A9, // GREEK CAPITAL LETTER OMEGA + /*0xBE*/ 0x00E6, // LATIN SMALL LETTER AE + /*0xBF*/ 0x00F8, // LATIN SMALL LETTER O WITH STROKE + /*0xC0*/ 0x00BF, // INVERTED QUESTION MARK + /*0xC1*/ 0x00A1, // INVERTED EXCLAMATION MARK + /*0xC2*/ 0x00AC, // NOT SIGN + /*0xC3*/ 0x221A, // SQUARE ROOT + /*0xC4*/ 0x0192, // LATIN SMALL LETTER F WITH HOOK + /*0xC5*/ 0x2248, // ALMOST EQUAL TO + /*0xC6*/ 0x2206, // INCREMENT + /*0xC7*/ 0x00AB, // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + /*0xC8*/ 0x00BB, // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + /*0xC9*/ 0x2026, // HORIZONTAL ELLIPSIS + /*0xCA*/ 0x00A0, // NO-BREAK SPACE + /*0xCB*/ 0x00C0, // LATIN CAPITAL LETTER A WITH GRAVE + /*0xCC*/ 0x00C3, // LATIN CAPITAL LETTER A WITH TILDE + /*0xCD*/ 0x00D5, // LATIN CAPITAL LETTER O WITH TILDE + /*0xCE*/ 0x0152, // LATIN CAPITAL LIGATURE OE + /*0xCF*/ 0x0153, // LATIN SMALL LIGATURE OE + /*0xD0*/ 0x2013, // EN DASH + /*0xD1*/ 0x2014, // EM DASH + /*0xD2*/ 0x201C, // LEFT DOUBLE QUOTATION MARK + /*0xD3*/ 0x201D, // RIGHT DOUBLE QUOTATION MARK + /*0xD4*/ 0x2018, // LEFT SINGLE QUOTATION MARK + /*0xD5*/ 0x2019, // RIGHT SINGLE QUOTATION MARK + /*0xD6*/ 0x00F7, // DIVISION SIGN + /*0xD7*/ 0x25CA, // LOZENGE + /*0xD8*/ 0x00FF, // LATIN SMALL LETTER Y WITH DIAERESIS + /*0xD9*/ 0x0178, // LATIN CAPITAL LETTER Y WITH DIAERESIS + /*0xDA*/ 0x2044, // FRACTION SLASH + /*0xDB*/ 0x00A4, // CURRENCY SIGN (was EURO SIGN) + /*0xDC*/ 0x2039, // SINGLE LEFT-POINTING ANGLE QUOTATION MARK + /*0xDD*/ 0x203A, // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + /*0xDE*/ 0xFB01, // LATIN SMALL LIGATURE FI + /*0xDF*/ 0xFB02, // LATIN SMALL LIGATURE FL + /*0xE0*/ 0x2021, // DOUBLE DAGGER + /*0xE1*/ 0x00B7, // MIDDLE DOT + /*0xE2*/ 0x201A, // SINGLE LOW-9 QUOTATION MARK + /*0xE3*/ 0x201E, // DOUBLE LOW-9 QUOTATION MARK + /*0xE4*/ 0x2030, // PER MILLE SIGN + /*0xE5*/ 0x00C2, // LATIN CAPITAL LETTER A WITH CIRCUMFLEX + /*0xE6*/ 0x00CA, // LATIN CAPITAL LETTER E WITH CIRCUMFLEX + /*0xE7*/ 0x00C1, // LATIN CAPITAL LETTER A WITH ACUTE + /*0xE8*/ 0x00CB, // LATIN CAPITAL LETTER E WITH DIAERESIS + /*0xE9*/ 0x00C8, // LATIN CAPITAL LETTER E WITH GRAVE + /*0xEA*/ 0x00CD, // LATIN CAPITAL LETTER I WITH ACUTE + /*0xEB*/ 0x00CE, // LATIN CAPITAL LETTER I WITH CIRCUMFLEX + /*0xEC*/ 0x00CF, // LATIN CAPITAL LETTER I WITH DIAERESIS + /*0xED*/ 0x00CC, // LATIN CAPITAL LETTER I WITH GRAVE + /*0xEE*/ 0x00D3, // LATIN CAPITAL LETTER O WITH ACUTE + /*0xEF*/ 0x00D4, // LATIN CAPITAL LETTER O WITH CIRCUMFLEX + /*0xF0*/ 0xF8FF, // Apple logo + /*0xF1*/ 0x00D2, // LATIN CAPITAL LETTER O WITH GRAVE + /*0xF2*/ 0x00DA, // LATIN CAPITAL LETTER U WITH ACUTE + /*0xF3*/ 0x00DB, // LATIN CAPITAL LETTER U WITH CIRCUMFLEX + /*0xF4*/ 0x00D9, // LATIN CAPITAL LETTER U WITH GRAVE + /*0xF5*/ 0x0131, // LATIN SMALL LETTER DOTLESS I + /*0xF6*/ 0x02C6, // MODIFIER LETTER CIRCUMFLEX ACCENT + /*0xF7*/ 0x02DC, // SMALL TILDE + /*0xF8*/ 0x00AF, // MACRON + /*0xF9*/ 0x02D8, // BREVE + /*0xFA*/ 0x02D9, // DOT ABOVE + /*0xFB*/ 0x02DA, // RING ABOVE + /*0xFC*/ 0x00B8, // CEDILLA + /*0xFD*/ 0x02DD, // DOUBLE ACUTE ACCENT + /*0xFE*/ 0x02DB, // OGONEK + /*0xFF*/ 0x02C7 // CARON +}; \ No newline at end of file diff --git a/src/game/boe.actions.cpp b/src/game/boe.actions.cpp index 77368b150..4f0a3bad8 100644 --- a/src/game/boe.actions.cpp +++ b/src/game/boe.actions.cpp @@ -692,7 +692,6 @@ void handle_look(location destination, bool right_button, eKeyMod mods, bool& ne if(adj_town_look(destination)) need_redraw = true; // TODO: This would be the place to call OUT_LOOK special - // TODO: Do we really need an is_sign function? if(is_sign(ter_looked_at)) { print_buf(); need_reprint = false; @@ -4273,13 +4272,6 @@ short count_walls(location loc) { // TODO: Generalize this function return answer; } -bool is_sign(ter_num_t ter) { - - if(univ.scenario.ter_types[ter].special == eTerSpec::IS_A_SIGN) - return true; - return false; -} - bool check_for_interrupt(std::string confirm_dialog){ using kb = sf::Keyboard; bool interrupt = false; diff --git a/src/game/boe.actions.hpp b/src/game/boe.actions.hpp index b722c90dc..bf8b26bef 100644 --- a/src/game/boe.actions.hpp +++ b/src/game/boe.actions.hpp @@ -54,7 +54,6 @@ short nearest_monster(); void setup_outdoors(location where); short get_outdoor_num(); short count_walls(location loc); -bool is_sign(ter_num_t ter); bool check_for_interrupt(std::string confirm_dialog = "confirm-interrupt-special"); void handle_startup_button_click(eStartButton btn, eKeyMod mods); diff --git a/src/game/boe.dlgutil.cpp b/src/game/boe.dlgutil.cpp index 67368eceb..acd06706f 100644 --- a/src/game/boe.dlgutil.cpp +++ b/src/game/boe.dlgutil.cpp @@ -28,6 +28,7 @@ #include "utility.hpp" #include "mathutil.hpp" #include "dialogxml/dialogs/strdlog.hpp" +#include "dialogxml/dialogs/btnpanel.hpp" #include "dialogxml/dialogs/choicedlog.hpp" #include "gfx/render_shapes.hpp" #include "tools/winutil.hpp" @@ -60,8 +61,12 @@ extern std::shared_ptr text_sbar,item_sbar,shop_sbar; extern std::shared_ptr done_btn, help_btn; extern bool map_visible; extern cUniverse univ; +extern cCustomGraphics spec_scen_g; extern std::map skill_max; extern void give_help_and_record(short help1, short help2, bool help_forced = false); +extern void post_load(); +extern void start_new_game(bool force = false); +extern void do_load(); short sign_mode,person_graphic,store_person_graphic,store_sign_mode; long num_talk_entries; @@ -659,15 +664,16 @@ std::vector preset_words = { "Ask About...", }; +std::vector preset_word_locs = { + {4, 366}, {70, 366}, {136, 366}, + {4, 389}, {70, 389}, {121, 389}, + {210, 389}, {190, 366}, + {4, 343} +}; + static void reset_talk_words() { // first initialise talk_words here talk_words.clear(); - static const std::vector preset_word_locs = { - {4, 366}, {70, 366}, {136, 366}, - {4, 389}, {70, 389}, {121, 389}, - {210, 389}, {190, 366}, - {4, 343} - }; TextStyle style; style.font = FONT_DUNGEON; style.pointSize = TALK_WORD_SIZE; @@ -1786,8 +1792,16 @@ class cChooseScenario { for(auto& hdr : scen_headers){ // I just checked, and the scenario editor will let you name your scenario "" or " "! std::string name = name_alphabetical(hdr.name); - if(!name.empty()) - me[name.substr(0, 1)].show(); + if(!name.empty()){ + // Starts with a letter: + if(me.hasControl(name.substr(0, 1))){ + me[name.substr(0, 1)].show(); + } + // Starts with a digit: + else if(name[0] >= '0' && name[0] <= '9'){ + me["#"].show(); + } + } } } @@ -1804,6 +1818,7 @@ class cChooseScenario { bool doSelectScenario(int which) { int page = dynamic_cast(me["list"]).getPage(); + scen_header_type scen; if(page == 0) { scen_header_type prefab; switch(which) { @@ -1816,13 +1831,80 @@ class cChooseScenario { prefab.prog_make_ver[0] = 2; prefab.prog_make_ver[1] = 0; prefab.prog_make_ver[2] = 0; - me.setResult(prefab); + scen = prefab; } else { int scen_hit = which + (page - 1) * 3; if(scen_hit >= scen_headers.size()) return false; - me.setResult(scen_headers[scen_hit]); + scen = scen_headers[scen_hit]; } - me.toast(true); + std::vector choices; + std::vector> handlers; + // If no party is loaded, offer to load default or create new + if(!party_in_memory){ + choices.push_back("Create new party"); + handlers.push_back([](cButtonPanel& dlg) -> void { + start_new_game(); + if(party_in_memory){ + dlg->getControl("done").show(); + } + }); + choices.push_back("Load a party"); + handlers.push_back([](cButtonPanel& dlg) -> void { + do_load(); + if(party_in_memory){ + dlg->getControl("done").show(); + } + }); + } + + // Show text files, Offer to load prefab party + std::vector files = extra_files(locate_scenario(scen.file)); + for(fs::path file : files){ + std::string ext = file.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), tolower); + if(ext == ".sav"){ + choices.push_back("Load premade party: " + file.filename().string()); + handlers.push_back([file](cButtonPanel& dlg) -> void { + if(replaying){ + // The next action encodes loading the prefab party, + // which is redundant here + pop_next_action(); + } + if(!load_party(file, univ, spec_scen_g)) { + std::cout << "Failed to load save file: " << file << std::endl; + }else{ + finish_load_party(); + if(overall_mode != MODE_STARTUP) + post_load(); + dlg->getControl("done").show(); + } + }); + }else{ + choices.push_back("Open file: " + file.filename().string()); + handlers.push_back([file](cButtonPanel&) -> void { + launchURL("file://" + file.string()); + }); + } + } + + if(!choices.empty()){ + cButtonPanel panel(choices, handlers, scen.name, "Launch", &me); + if(!party_in_memory){ + panel->getControl("done").hide(); + } + dynamic_cast(panel->getControl("pic")).setPict(scen.intro_pic,PIC_SCEN); + if(panel.show()){ + // Launch pressed. + me.setResult(scen); + me.toast(true); + }; + } + // No extra files. Just launch + else{ + me.setResult(scen); + me.toast(true); + } + return true; } @@ -1870,6 +1952,7 @@ class cChooseScenario { stk.setPage(0); return true; }); + // Letter buttons scroll to an alphabetical position: for(int i = 0; i < 26; ++i){ std::string letter(1, (char)('a' + i)); me[letter].attachClickHandler([this](cDialog& me, std::string letter, eKeyMod) -> bool { @@ -1883,6 +1966,12 @@ class cChooseScenario { return true; }); } + // Number button scrolls to scenarios that start with symbols or digits: + me["#"].attachClickHandler([this](cDialog& me, std::string letter, eKeyMod) -> bool { + auto& stk = dynamic_cast(me["list"]); + stk.setPage(1); + return true; + }); put_scen_info(); diff --git a/src/game/boe.fileio.cpp b/src/game/boe.fileio.cpp index 4e0e9d28b..536f46713 100644 --- a/src/game/boe.fileio.cpp +++ b/src/game/boe.fileio.cpp @@ -340,6 +340,8 @@ std::string name_alphabetical(std::string a) { // The scenario editor will let you prepend whitespace to a scenario name :( boost::algorithm::trim_left(a); std::transform(a.begin(), a.end(), a.begin(), tolower); + // Some party makers start with the name of the corresponding scenario in quotes + if(a.substr(0,1) == "\"") a.erase(a.begin(), a.begin() + 1); if(a.substr(0,2) == "a ") a.erase(a.begin(), a.begin() + 2); else if(a.substr(0,4) == "the ") a.erase(a.begin(), a.begin() + 4); return a; @@ -358,11 +360,9 @@ std::vector build_scen_headers() { std::string scen_file; while(std::getline(in, scen_file)){ scen_header_type scen_head; - fs::path full_path; - for(fs::path scenDir : all_scen_dirs()){ - full_path = scenDir / scen_file; - if (fs::exists(full_path)) - break; + fs::path full_path = locate_scenario(scen_file, true); + if(full_path.empty()){ + LOG("Scenario missing! " + scen_file); } if(load_scenario_header(full_path, scen_head)){ scen_headers.push_back(scen_head); @@ -378,6 +378,12 @@ std::vector build_scen_headers() { } } }else{ + scen_header_type scen_head; + + // Include Bandit Busywork in custom section: + if(load_scenario_header(progDir / "Blades of Exile Scenarios/busywork.boes", scen_head)) + scen_headers.push_back(scen_head); + for(fs::path scenDir : all_scen_dirs()){ std::cout << scenDir << std::endl; fs::recursive_directory_iterator iter(scenDir); @@ -392,7 +398,6 @@ std::vector build_scen_headers() { continue; } - scen_header_type scen_head; if(load_scenario_header(iter->path(), scen_head)) scen_headers.push_back(scen_head); } @@ -544,4 +549,28 @@ void try_auto_save(std::string reason) { ASB("Autosave: Save not completed"); } print_buf(); +} + +std::vector extra_extensions = {".sav", ".txt", ".rtf", ".htm", ".html"}; + +std::vector extra_files(fs::path scen_file) { + std::vector files; + + std::string scen_extension = scen_file.extension().string(); + std::transform(scen_extension.begin(), scen_extension.end(), scen_extension.begin(), tolower); + if(scen_extension != ".exs") return files; + + fs::path directory = scen_file.parent_path(); + + fs::recursive_directory_iterator file_iter(directory); + for(; file_iter != fs::recursive_directory_iterator(); file_iter++) { + fs::path file = *file_iter; + std::string extension = file.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), tolower); + if(std::find(extra_extensions.begin(), extra_extensions.end(), extension) != extra_extensions.end()){ + files.push_back(file); + } + } + + return files; } \ No newline at end of file diff --git a/src/game/boe.fileio.hpp b/src/game/boe.fileio.hpp index 4300a3d20..4016147b8 100644 --- a/src/game/boe.fileio.hpp +++ b/src/game/boe.fileio.hpp @@ -37,4 +37,7 @@ void try_auto_save(std::string reason); // Turn lower-case and strip articles from the front of a scenario title, for alphabetization std::string name_alphabetical(std::string scenario_name); +// Find extra files packaged with legacy scenarios (such as readme, prefab party): +std::vector extra_files(fs::path scen_file); + #endif diff --git a/src/game/boe.graphics.cpp b/src/game/boe.graphics.cpp index 7d76df26b..6c7e90526 100644 --- a/src/game/boe.graphics.cpp +++ b/src/game/boe.graphics.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "boe.global.hpp" @@ -1186,17 +1187,21 @@ void place_trim(short q,short r,location where,ter_num_t ter_type) { } static void init_trim_mask(std::unique_ptr& mask, rectangle src_rect) { - sf::RenderTexture render; + static sf::RenderTexture render; + static bool init = false; + if(!init){ + render.create(28, 36); + init = true; + } rectangle dest_rect; dest_rect.top = src_rect.top % 36; dest_rect.bottom = (src_rect.bottom - 1) % 36 + 1; dest_rect.left = src_rect.left % 28; dest_rect.right = (src_rect.right - 1) % 28 + 1; - std::tie(dest_rect.top, dest_rect.bottom) = std::make_tuple(36 - dest_rect.top, 36 - dest_rect.bottom); render.create(28, 36); render.clear(sf::Color::White); rect_draw_some_item(*ResMgr::graphics.get("trim"), src_rect, render, dest_rect); - render.display(); + // render.display(); // Using it as a mask, we don't need to flip mask.reset(new sf::Texture); mask->create(28, 36); mask->update(render.getTexture().copyToImage()); @@ -1246,6 +1251,13 @@ void draw_trim(short q,short r,short which_trim,ter_num_t ground_ter) { } sf::Color test_color = {0,0,0}, store_color; + location targ; + targ.x = q; + targ.y = r; + if(supressing_some_spaces && (targ != ok_space[0]) && (targ != ok_space[1]) && + (targ != ok_space[2]) && (targ != ok_space[3])) + return; + unsigned short pic = univ.scenario.ter_types[ground_ter].picture; if(pic < 960){ int which_sheet = pic / 50; @@ -1368,67 +1380,6 @@ void place_road(short q,short r,location where,bool here) { to_rect.offset(13 + q * 28,13 + r * 36); rect_draw_some_item (roads_gworld, road_rects[0], terrain_screen_gworld(), to_rect); } - }else{ - // TODO: I suspect this branch is now irrelevant. - ter_num_t ter = coord_to_ter(where.x, where.y); - ter_num_t ref = coord_to_ter(where.x,where.y); - bool horz = false, vert = false; - eTrimType trim = eTrimType::NONE, vertTrim = eTrimType::NONE; - if(ref < univ.scenario.ter_types.size()) { - trim = univ.scenario.ter_types[ref].trim_type; - } - if(ter < univ.scenario.ter_types.size()) { - vertTrim = univ.scenario.ter_types[ter].trim_type; - } - if(where.y > 0) - ter = coord_to_ter(where.x,where.y - 1); - if((where.y == 0) || connect_roads(ter)) - vert = can_build_roads_on(ref); - else if((vertTrim == eTrimType::S && trim == eTrimType::N) || (vertTrim == eTrimType::N && trim == eTrimType::S)) - vert = can_build_roads_on(ref); - - if(((is_out()) && (where.x < 96)) || (!(is_out()) && (where.x < univ.town->max_dim - 1))) - ter = coord_to_ter(where.x + 1,where.y); - eTrimType horzTrim = univ.scenario.ter_types[ter].trim_type; - if(((is_out()) && (where.x == 96)) || (!(is_out()) && (where.x == univ.town->max_dim - 1)) - || connect_roads(ter)) - horz = can_build_roads_on(ref); - else if((horzTrim == eTrimType::W && trim == eTrimType::E) || (horzTrim == eTrimType::E && trim == eTrimType::W)) - horz = can_build_roads_on(ref); - - if(vert){ - if(((is_out()) && (where.y < 96)) || (!(is_out()) && (where.y < univ.town->max_dim - 1))) - ter = coord_to_ter(where.x,where.y + 1); - eTrimType vertTrim = univ.scenario.ter_types[ter].trim_type; - if(((is_out()) && (where.y == 96)) || (!(is_out()) && (where.y == univ.town->max_dim - 1)) - || connect_roads(ter)) - vert = can_build_roads_on(ref); - else if((vertTrim == eTrimType::S && trim == eTrimType::N) || (vertTrim == eTrimType::N && trim == eTrimType::S)) - vert = can_build_roads_on(ref); - else vert = false; - } - - if(horz){ - if(where.x > 0) - ter = coord_to_ter(where.x - 1,where.y); - eTrimType horzTrim = univ.scenario.ter_types[ter].trim_type; - if((where.x == 0) || connect_roads(ter)) - horz = can_build_roads_on(ref); - else if((horzTrim == eTrimType::W && trim == eTrimType::E) || (horzTrim == eTrimType::E && trim == eTrimType::W)) - horz = can_build_roads_on(ref); - else horz = false; - } - - if(horz){ - to_rect = road_dest_rects[5]; - to_rect.offset(13 + q * 28,13 + r * 36); - rect_draw_some_item (roads_gworld, road_rects[2], terrain_screen_gworld(), to_rect); - } - if(vert){ - to_rect = road_dest_rects[4]; - to_rect.offset(13 + q * 28,13 + r * 36); - rect_draw_some_item (roads_gworld, road_rects[3], terrain_screen_gworld(), to_rect); - } } } @@ -1685,6 +1636,65 @@ void draw_targeting_line() { extern bool targeting_line_visible; if(targeting_line_visible && ((overall_mode == MODE_SPELL_TARGET) || (overall_mode == MODE_FIRING) || (overall_mode == MODE_THROWING) || (overall_mode == MODE_FANCY_TARGET) || ((overall_mode == MODE_TOWN_TARGET) && (current_pat[4][4] != 0)))) { + + // Highlight targetable squares including ally/enemy attitude + std::set big_monst_there; + for(short q = 0; q < 9; q++) { + for(short r = 0; r < 9; r++) { + which_space = center; + which_space.x += q - 4; + which_space.y += r - 4; + + if(big_monst_there.count(which_space)) continue; + sf::Color frame_color(0, 0, 0, 0); + int x_width = 1; + int y_width = 1; + short frame_q = q; + short frame_r = r; + if((can_see_light(from_loc,which_space,sight_obscurity) < 5) + && (dist(from_loc,which_space) <= current_spell_range)){ + frame_color = sf::Color(255, 255, 255, 127); + + iLiving* targ = univ.target_there(which_space, TARG_PC); + if(targ != nullptr){ + frame_color = sf::Color(0, 255, 0, 127); + } + targ = univ.target_there(which_space, TARG_MONST); + if(targ != nullptr){ + cCreature* monst = dynamic_cast(targ); + if(!monst->invisible){ + frame_color = targ->is_friendly() ? sf::Color(0, 0, 255, 127) : sf::Color(255, 0, 0, 127); + // Frame needs to be positioned at the monster's top left corner + frame_q -= (which_space.x - monst->cur_loc.x); + frame_r -= (which_space.y - monst->cur_loc.y); + x_width = monst->x_width; + y_width = monst->y_width; + for(int i = 0; i < x_width; ++i){ + for(int j = 0; j < y_width; ++j){ + big_monst_there.insert(loc(monst->cur_loc.x + i, monst->cur_loc.y + j)); + } + } + } + } + + const int ter_screen_left = 19 + 13; // 13 is the white frame + const int ter_screen_top = 7 + 13; // 13 is the white frame + + target_rect.left = ter_screen_left + 28 * frame_q; + target_rect.right = target_rect.left + 28 * x_width; + target_rect.top = ter_screen_top + 36 * frame_r; + target_rect.bottom = target_rect.top + 36 * y_width; + + // Clip the target rect to the window bounds + target_rect.left = max(ter_screen_left, target_rect.left); + target_rect.top = max(ter_screen_top, target_rect.top); + target_rect.right = min(ter_screen_left + 28 * 9, target_rect.right); + target_rect.bottom = min(ter_screen_top + 36 * 9, target_rect.bottom); + + frame_rect(mainPtr(), target_rect, frame_color); + } + } + } if(mouse_to_terrain_coords(which_space, false)) { int xBound = (short) (from_loc.x - center.x + 4); @@ -1712,7 +1722,8 @@ void draw_targeting_line() { target_rect.right = target_rect.left + 28; target_rect.top = 13 + 36 * j + 7; target_rect.bottom = target_rect.top + 36; - frame_rect(mainPtr(), target_rect, sf::Color::White); + static sf::Color target_color(255, 0, 0, 255); + frame_rect(mainPtr(), target_rect, target_color); target_rect.inset(-5,-5); // Now place number of shots left, if drawing center of target diff --git a/src/game/boe.graphutil.cpp b/src/game/boe.graphutil.cpp index 1b432dc95..b6e1d7cae 100644 --- a/src/game/boe.graphutil.cpp +++ b/src/game/boe.graphutil.cpp @@ -134,7 +134,7 @@ void draw_monsters() { for(short k = 0; k < width * height; k++) { std::shared_ptr src_gw; graf_pos_ref(src_gw, source_rect) = spec_scen_g.find_graphic(picture_wanted % 1000 + - ((enc.direction < 4) ? 0 : (width * height)) + k); + ((enc.direction >= 4) ? 0 : (width * height)) + k); to_rect = monst_rects[(width - 1) * 2 + height - 1][k]; to_rect.offset(13 + 28 * where_draw.x,13 + 36 * where_draw.y); rect_draw_some_item(*src_gw, source_rect, terrain_screen_gworld(),to_rect, sf::BlendAlpha); @@ -142,7 +142,7 @@ void draw_monsters() { } if(picture_wanted < 1000) { for(short k = 0; k < width * height; k++) { - source_rect = get_monster_template_rect(picture_wanted,(enc.direction < 4) ? 0 : 1,k); + source_rect = get_monster_template_rect(picture_wanted,(enc.direction >= 4) ? 0 : 1,k); to_rect = monst_rects[(width - 1) * 2 + height - 1][k]; to_rect.offset(13 + 28 * where_draw.x,13 + 36 * where_draw.y); int which_sheet = m_pic_index[picture_wanted].i / 20; @@ -192,13 +192,29 @@ void draw_monsters() { Draw_Some_Item(*src_gw, source_rect, terrain_screen_gworld(), store_loc, 1, 0); } else { pic_num_t this_monst = monst.picture_num; - int pic_mode = (monst.direction) < 4 ? 0 : 1; + int pic_mode = (monst.direction) >= 4 ? 0 : 1; pic_mode += (combat_posing_monster == i + 100) ? 10 : 0; source_rect = get_monster_template_rect(this_monst, pic_mode, k); int which_sheet = (m_pic_index[this_monst].i+k) / 20; sf::Texture& monst_gworld = *ResMgr::graphics.get("monst" + std::to_string(1 + which_sheet)); Draw_Some_Item(monst_gworld, source_rect, terrain_screen_gworld(), store_loc, 1, 0); } + + extern bool targeting_line_visible; + if(is_combat() && !targeting_line_visible){ + // Frame the monster + rectangle target_rect; + target_rect.left = 13 + 28 * where_draw.x; + target_rect.right = target_rect.left + 28 * monst.x_width; + target_rect.top = 13 + 36 * where_draw.y; + target_rect.bottom = target_rect.top + 36 * monst.y_width; + // clip the frame at the window boundaries + target_rect.top = max(13, target_rect.top); + target_rect.left = max(13, target_rect.left); + target_rect.bottom = min(target_rect.bottom, terrain_screen_gworld().getSize().y - 13); + target_rect.right = min(target_rect.right, terrain_screen_gworld().getSize().x - 13); + frame_rect(terrain_screen_gworld(), target_rect, monst.is_friendly() ? sf::Color(0, 0, 255, 127) : sf::Color(255, 0, 0, 127)); + } } } } @@ -448,7 +464,7 @@ void draw_party_symbol(location center) { return; if(!univ.party.is_alive()) return; - if((is_town()) && (univ.party.town_loc.x > 70)) + if((is_town()) && (univ.party.town_loc.x > (univ.scenario.is_legacy ? 70 : LOC_UNUSED))) return; if(is_town() && center != univ.party.town_loc) { diff --git a/src/game/boe.infodlg.cpp b/src/game/boe.infodlg.cpp index 059bd0cdd..008bf7402 100644 --- a/src/game/boe.infodlg.cpp +++ b/src/game/boe.infodlg.cpp @@ -512,10 +512,14 @@ static bool adventure_notes_event_filter(cDialog& me, std::string item_hit, eKey std::string n = boost::lexical_cast(i + 1); if(univ.party.special_notes.size() > store_page_on * 3+i) { me["str" + n].setText(univ.party.special_notes[store_page_on * 3+i].the_str); + me["str" + n].recalcRect(); + me["pane" + n].recalcRect(); me["del" + n].show(); } else { me["str" + n].setText(""); + me["str" + n].recalcRect(); + me["pane" + n].recalcRect(); me["del" + n].hide(); } } @@ -544,6 +548,8 @@ void adventure_notes() { std::string n = boost::lexical_cast(i + 1); if(univ.party.special_notes.size() > i) { encNotes["str" + n].setText(univ.party.special_notes[i].the_str); + encNotes["str" + n].recalcRect(); + encNotes["pane" + n].recalcRect(); encNotes["del" + n].show(); } else encNotes["del" + n].hide(); @@ -707,12 +713,12 @@ void cStringRecorder::operator()(cDialog& me) { play_sound(0); std::string str1, str2; univ.get_strs(str1, str2, spec_type, label1, label2); + // Combine str1 and str2 in one journal entry: + if(!str2.empty()) + str1 += " ||" + str2; if(univ.party.record(note_type, str1, location)){ give_help(58,0,me); ASB("Added to encounter notes."); } - - if(!str2.empty()) - univ.party.record(note_type, str2, location); } diff --git a/src/game/boe.items.cpp b/src/game/boe.items.cpp index 4e9cc41c5..dfe59ea54 100644 --- a/src/game/boe.items.cpp +++ b/src/game/boe.items.cpp @@ -41,7 +41,7 @@ extern short d_rect_index[80]; extern bool map_visible; extern cUniverse univ; -extern void draw_map(bool need_refresh); +extern void draw_map(bool need_refresh, std::string tooltip_text = ""); short selected; @@ -395,6 +395,16 @@ static void put_item_graphics(cDialog& me, size_t& first_item_shown, short& curr me["down"].hide(); else me["down"].show(); + me["gold"].hide(); + me["gold-label"].hide(); + for(cItem* item : item_array){ + if(item->variety == eItemType::GOLD && !item->property){ + me["gold"].show(); + me["gold-label"].show(); + break; + } + } + for(short i = 0; i < ITEMS_IN_WINDOW; i++) { std::ostringstream sout; sout << "item" << i + 1; @@ -460,7 +470,22 @@ static bool display_item_event_filter(cDialog& me, std::string id, size_t& first first_item_shown += ITEMS_IN_WINDOW; put_item_graphics(me, first_item_shown, current_getting_pc, item_array); } - } else if(id.substr(0,2) == "pc") { + } else if(id == "gold"){ + // Get all gold + for(int i = item_array.size() - 1; i >= 0; --i){ + item = *item_array[i]; + if(item.variety != eItemType::GOLD || item.property) continue; + if(item.item_level > 3000) + item.item_level = 3000; + set_item_flag(&item); + give_gold(item.item_level,false); + *item_array[i] = cItem(); + item_array.erase(item_array.begin() + i); + } + play_sound(39); // formerly force_play_sound + put_item_graphics(me, first_item_shown, current_getting_pc, item_array); + } + else if(id.substr(0,2) == "pc") { current_getting_pc = id[2] - '1'; put_item_graphics(me, first_item_shown, current_getting_pc, item_array); } else { @@ -561,7 +586,7 @@ bool show_get_items(std::string titleText, std::vector& itemRefs, short cDialog itemDialog(*ResMgr::dialogs.get("get-items")); auto handler = std::bind(display_item_event_filter, _1, _2, std::ref(first_item), std::ref(pc_getting), std::ref(itemRefs), overload); - itemDialog.attachClickHandlers(handler, {"done", "up", "down"}); + itemDialog.attachClickHandlers(handler, {"done", "up", "down", "gold"}); itemDialog.attachClickHandlers(handler, {"pc1", "pc2", "pc3", "pc4", "pc5", "pc6"}); itemDialog.setResult(false); cTextMsg& title = dynamic_cast(itemDialog["title"]); @@ -658,17 +683,21 @@ short get_num_of_items(short max_num) { return minmax(0,max_num,numPanel.getResult()); } +// extern declaration doesn't work here?? +const short view_max_dim = 40; +const short map_window_width = 296; +const short map_window_height = 307; void init_mini_map() { double map_scale = get_ui_scale_map(); if (map_scale < 0.1) map_scale = 1.0; if (mini_map().isOpen()) mini_map().close(); - mini_map().create(sf::VideoMode(map_scale*296,map_scale*277), "Map", sf::Style::Titlebar | sf::Style::Close); + mini_map().create(sf::VideoMode(map_scale*map_window_width,map_scale*map_window_height), "Map", sf::Style::Titlebar | sf::Style::Close); // TODO why is 52,62 the default position, anyway? int map_x = get_int_pref("MapWindowX", 52); int map_y = get_int_pref("MapWindowY", 62); mini_map().setPosition(sf::Vector2i(map_x,map_y)); sf::View view; - view.reset(sf::FloatRect(0, 0, map_scale*296,map_scale*277)); + view.reset(sf::FloatRect(0, 0, map_scale*map_window_width,map_scale*map_window_height)); view.setViewport(sf::FloatRect(0, 0, map_scale, map_scale)); mini_map().setView(view); mini_map().setVisible(false); @@ -677,7 +706,7 @@ void init_mini_map() { makeFrontWindow(mainPtr(), mini_map()); // Create and initialize map gworld - if(!map_gworld().create(384, 384)) { + if(!map_gworld().create(view_max_dim * 6, view_max_dim * 6)) { play_sound(2); throw std::string("Failed to initialized automap!"); } else { diff --git a/src/game/boe.locutils.cpp b/src/game/boe.locutils.cpp index 259a4d3fd..a27cdfbe0 100644 --- a/src/game/boe.locutils.cpp +++ b/src/game/boe.locutils.cpp @@ -138,7 +138,9 @@ bool loc_off_world(location p1) { } bool loc_off_act_area(location p1) { - if((p1.x > univ.town->in_town_rect.left) && (p1.x < univ.town->in_town_rect.right) && + if(is_out() && univ.out->is_on_map(p1)) + return false; + else if((p1.x > univ.town->in_town_rect.left) && (p1.x < univ.town->in_town_rect.right) && (p1.y > univ.town->in_town_rect.top) && (p1.y < univ.town->in_town_rect.bottom)) return false; return true; @@ -258,14 +260,14 @@ void update_explored(const location dest) { // All purpose function to check is spot is free for travel into. -bool is_blocked(location to_check) { +bool is_blocked(location to_check, bool count_party) { if(is_out()) { if(!univ.out.is_on_map(to_check.x, to_check.y)) return true; if(impassable(univ.out[to_check.x][to_check.y])) { return true; } - if(to_check == univ.party.out_loc) + if(count_party && to_check == univ.party.out_loc) return true; for(short i = 0; i < univ.party.out_c.size(); i++) if((univ.party.out_c[i].exists)) @@ -292,12 +294,14 @@ bool is_blocked(location to_check) { // Note: The purpose of the above check is to avoid portals. // Party there? - if(is_town() && to_check == univ.party.town_loc) - return true; - if(is_combat()) { - for(short i = 0; i < 6; i++) { - if(univ.party[i].main_status == eMainStatus::ALIVE && to_check == univ.party[i].combat_pos) { - return true; + if(count_party){ + if(is_town() && to_check == univ.party.town_loc) + return true; + if(is_combat()) { + for(short i = 0; i < 6; i++) { + if(univ.party[i].main_status == eMainStatus::ALIVE && to_check == univ.party[i].combat_pos) { + return true; + } } } } @@ -599,3 +603,19 @@ void alter_space(short i,short j,ter_num_t ter) { univ.town->set_up_lights(); } } + +bool vehicle_is_here(const cVehicle& vehicle) { + if(!vehicle.exists) return false; + if(is_out()){ + location party_sector = univ.party.outdoor_corner; + party_sector.x += univ.party.i_w_c.x; + party_sector.y += univ.party.i_w_c.y; + return vehicle.which_town == 200 && vehicle.sector == party_sector; + }else{ + return vehicle.which_town == univ.party.town_num; + } +} + +bool is_sign(ter_num_t ter) { + return univ.scenario.ter_types[ter].special == eTerSpec::IS_A_SIGN; +} diff --git a/src/game/boe.locutils.hpp b/src/game/boe.locutils.hpp index ecf6e0c5d..6778042a2 100644 --- a/src/game/boe.locutils.hpp +++ b/src/game/boe.locutils.hpp @@ -1,6 +1,7 @@ #include #include "location.hpp" +#include "scenario/vehicle.hpp" bool is_explored(short i,short j); void make_explored(short i,short j); @@ -23,7 +24,7 @@ short combat_obscurity(short x,short y); ter_num_t coord_to_ter(short x,short y); bool is_container(location loc); void update_explored(location dest); -bool is_blocked(location to_check); +bool is_blocked(location to_check, bool count_party = true); bool monst_can_be_there(location loc,short m_num); bool monst_adjacent(location loc,short m_num); bool monst_can_see(short m_num,location l); @@ -35,6 +36,7 @@ bool outd_is_special(location to_check); bool impassable(ter_num_t terrain_to_check); short get_blockage(ter_num_t terrain_type); short light_radius(); +bool is_sign(ter_num_t ter); bool pt_in_light(location from_where,location to_where) ;// Assumes, of course, in town or combat bool combat_pt_in_light(location to_where); bool party_sees_a_monst(); // Returns true is a hostile monster is in sight. @@ -43,3 +45,5 @@ location push_loc(location from_where,location to_where); bool spot_impassable(short i,short j); void swap_ter(short i,short j,ter_num_t ter1,ter_num_t ter2); void alter_space(short i,short j,ter_num_t ter); +// Whether a vehicle is in the current town/outdoor section shown on the map +bool vehicle_is_here(const cVehicle& vehicle); \ No newline at end of file diff --git a/src/game/boe.main.cpp b/src/game/boe.main.cpp index 9eb4aa3f8..fa3057efe 100644 --- a/src/game/boe.main.cpp +++ b/src/game/boe.main.cpp @@ -92,6 +92,8 @@ boost::optional scen_arg_out_sec, scen_arg_loc; extern std::string last_load_file; std::string help_text_rsrc = "help"; +std::string last_tooltip_text = ""; + /* // Example feature flags: { @@ -1444,6 +1446,75 @@ void handle_one_minimap_event(const sf::Event& event) { map_window_lost_focus = true; }else if(event.type == sf::Event::MouseMoved){ check_window_moved(mini_map(), last_map_x, last_map_y, "MapWindow"); + + // Check if something interesting is hovered + location tile = minimap_view_rect().topLeft(); + int x = event.mouseMove.x; + int y = event.mouseMove.y; + tile.x += (x - (47 * get_ui_scale_map())) / get_ui_scale_map() / 6; + tile.y += (y - (29 * get_ui_scale_map())) / get_ui_scale_map() / 6; + + std::string tooltip_text = ""; + bool is_on_map; + if(is_out()) is_on_map = tile.x >= 0 && tile.y >= 0 && tile.x < 48 && tile.y < 48; + else is_on_map = univ.town->is_on_map(tile); + location real_tile = tile; + if(is_out()) real_tile = local_to_global(tile); + if(is_on_map && is_explored(real_tile.x, real_tile.y)){ + // Area rectangle hovered + const std::vector& area_desc = is_out() ? univ.out->area_desc : univ.town->area_desc; + for(info_rect_t area : area_desc){ + if(area.empty()) continue; + if(area.contains(tile)){ + tooltip_text += area.descr + " |"; + } + } + + // Sign hovered + const std::vector& sign_locs = is_out() ? univ.out->sign_locs : univ.town->sign_locs; + for(sign_loc_t sign : sign_locs){ + if(sign == tile){ + // make sure the terrain is a sign + ter_num_t ter = is_out() ? univ.out[real_tile] : univ.town->terrain(real_tile.x, real_tile.y); + if(is_sign(ter)) + tooltip_text += "Sign: " + sign.text + " |"; + } + } + // Town entrance hovered + if(is_out()){ + const std::vector& city_locs = univ.out->city_locs; + for(spec_loc_t city : city_locs){ + if(city == tile){ + // don't tooltip hidden towns + ter_num_t ter = is_out() ? univ.out[real_tile] : univ.town->terrain(real_tile.x, real_tile.y); + if(univ.scenario.ter_types[ter].special == eTerSpec::TOWN_ENTRANCE) + tooltip_text += univ.scenario.towns[city.spec]->name + " |"; + } + } + } + // Vehicle hovered + for(auto& boat : univ.party.boats) { + if(!vehicle_is_here(boat)) continue; + if(boat.loc == tile){ + tooltip_text += (boat.property ? "Boat (Not Yours)" : "Your Boat"); + tooltip_text += " |"; + } + } + for(auto& horse : univ.party.horses) { + if(!vehicle_is_here(horse)) continue; + if(horse.loc == tile){ + tooltip_text += (horse.property ? "Horses (Not Yours)" : "Your Horses"); + tooltip_text += " |"; + } + } + // Party hovered + if(tile == (is_out() ? univ.party.loc_in_sec : univ.party.town_loc)){ + tooltip_text += "Your Party"; + } + } + if(tooltip_text != last_tooltip_text) + draw_map(false, tooltip_text); + last_tooltip_text = tooltip_text; }else if(event.type == sf::Event::KeyPressed){ switch(event.key.code){ case sf::Keyboard::Escape: @@ -1491,7 +1562,7 @@ void update_everything() { void redraw_everything() { redraw_screen(REFRESH_ALL); - if(map_visible) draw_map(false); + if(map_visible) draw_map(false, last_tooltip_text); } void Mouse_Pressed(const sf::Event& event, cFramerateLimiter& fps_limiter) { diff --git a/src/game/boe.monster.cpp b/src/game/boe.monster.cpp index 52320bd4a..ef0289eb3 100644 --- a/src/game/boe.monster.cpp +++ b/src/game/boe.monster.cpp @@ -340,7 +340,7 @@ bool monst_hate_spot(short which_m,location *good_loc) { hate_spot = true; } if(hate_spot) { - prospect = find_clear_spot(loc,1); + prospect = find_clear_spot(loc,1,univ.town.monst[which_m].x_width,univ.town.monst[which_m].y_width); if(prospect.x > 0) { *good_loc = prospect; return true; @@ -707,10 +707,12 @@ bool try_move(short i,location start,short x,short y) { } bool combat_move_monster(short which,location destination) { - if(!monst_can_be_there(destination,which)) + if(!monst_can_be_there(destination,which)){ return false; - else if(!monst_check_special_terrain(destination,2,which)) + } + else if(!monst_check_special_terrain(destination,2,which)){ return false; + } else { univ.town.monst[which].direction = set_direction(univ.town.monst[which].cur_loc, destination); univ.town.monst[which].cur_loc = destination; @@ -727,12 +729,26 @@ bool combat_move_monster(short which,location destination) { // Looks at all spaces within 2, looking for a spot which is clear of nastiness and beings // returns {0,0} if none found // TODO: NO WAIT IT DOESN'T LOOK AT ALL SPACES!!! -// TODO: THIS MAKES NO ADJUSTMENTS FOR BIG MONSTERS!!! //mode; // 0 - normal 1 - prefer adjacent space -location find_clear_spot(location from_where,short mode) { +location find_clear_spot(location from_where,short mode,short x_width,short y_width) { location loc,store_loc; short num_tries = 0,r1; - + auto is_clear = [from_where](location loc) -> bool { + return !loc_off_act_area(loc) && !is_blocked(loc) + && can_see_light(from_where,loc,combat_obscurity) == 0 + && (!is_combat() || univ.target_there(loc,TARG_PC) == nullptr) + && (!(is_town()) || (loc != univ.party.town_loc)) + && (!univ.town.is_summon_safe(loc.x, loc.y)); + }; + auto all_is_clear = [from_where, x_width, y_width, is_clear](location loc) -> bool { + for(int x = 0; x < x_width; ++x){ + for(int y = 0; y < y_width; ++y){ + if(!is_clear({loc.x + x, loc.y + y})) + return false; + } + } + return true; + }; while(num_tries < 75) { num_tries++; loc = from_where; @@ -740,11 +756,7 @@ location find_clear_spot(location from_where,short mode) { loc.x = loc.x + r1; r1 = get_ran(1,-2,2); loc.y = loc.y + r1; - if(!loc_off_act_area(loc) && !is_blocked(loc) - && can_see_light(from_where,loc,combat_obscurity) == 0 - && (!is_combat() || univ.target_there(loc,TARG_PC) == nullptr) - && (!(is_town()) || (loc != univ.party.town_loc)) - && (!univ.town.is_summon_safe(loc.x, loc.y))) { + if(all_is_clear(loc)) { if((mode == 0) || ((mode == 1) && (adjacent(from_where,loc)))) return loc; else store_loc = loc; @@ -809,58 +821,65 @@ void monst_inflict_fields(short which_monst) { which_m = &univ.town.monst[which_monst]; bool have_radiate = which_m->abil[eMonstAbil::RADIATE].active; eFieldType which_radiate = which_m->abil[eMonstAbil::RADIATE].radiate.type; + // Judgment call: big monsters shouldn't only get damaged once per damage type if they're on + // multiple of the same field. (Except webs.) + bool quickfire = false; + bool blade_wall = false; + bool force_wall = false; + bool sleep_cloud = false; + bool ice_wall = false; + bool stink_cloud = false; + bool fire_wall = false; for(short i = 0; i < univ.town.monst[which_monst].x_width; i++) for(short j = 0; j < univ.town.monst[which_monst].y_width; j++) if(univ.town.monst[which_monst].is_alive()) { where_check.x = univ.town.monst[which_monst].cur_loc.x + i; where_check.y = univ.town.monst[which_monst].cur_loc.y + j; - // TODO: If the goal is to damage the monster by any fields it's on, why all the break statements? - if(univ.town.is_quickfire(where_check.x,where_check.y)) { + if(!quickfire && univ.town.is_quickfire(where_check.x,where_check.y)) { + quickfire = true; r1 = get_ran(2,1,8); damage_monst(*which_m,7,r1,eDamageType::FIRE); - break; } - if(univ.town.is_blade_wall(where_check.x,where_check.y)) { + if(!blade_wall && univ.town.is_blade_wall(where_check.x,where_check.y)) { + blade_wall = true; r1 = get_ran(6,1,8); - if(have_radiate && which_radiate != eFieldType::WALL_BLADES) + if(!have_radiate || which_radiate != eFieldType::WALL_BLADES) damage_monst(*which_m,7,r1,eDamageType::WEAPON); - break; } - if(univ.town.is_force_wall(where_check.x,where_check.y)) { + if(!force_wall && univ.town.is_force_wall(where_check.x,where_check.y)) { + force_wall = true; r1 = get_ran(3,1,6); - if(have_radiate && which_radiate != eFieldType::WALL_FORCE) + if(!have_radiate || which_radiate != eFieldType::WALL_FORCE) damage_monst(*which_m,7,r1,eDamageType::MAGIC); - break; } - if(univ.town.is_sleep_cloud(where_check.x,where_check.y)) { - if(have_radiate && which_radiate != eFieldType::CLOUD_SLEEP) + if(!sleep_cloud && univ.town.is_sleep_cloud(where_check.x,where_check.y)) { + sleep_cloud = true; + if(!have_radiate || which_radiate != eFieldType::CLOUD_SLEEP) which_m->sleep(eStatus::ASLEEP,3,0); - break; } - if(univ.town.is_ice_wall(where_check.x,where_check.y)) { + if(!ice_wall && univ.town.is_ice_wall(where_check.x,where_check.y)) { + ice_wall = true; r1 = get_ran(3,1,6); - if(have_radiate && which_radiate != eFieldType::WALL_ICE) + if(!have_radiate || which_radiate != eFieldType::WALL_ICE) damage_monst(*which_m,7,r1,eDamageType::COLD); - break; } - if(univ.town.is_scloud(where_check.x,where_check.y)) { + if(!stink_cloud && univ.town.is_scloud(where_check.x,where_check.y)) { + stink_cloud = true; r1 = get_ran(1,2,3); - if(have_radiate && which_radiate != eFieldType::CLOUD_STINK) + if(!have_radiate || which_radiate != eFieldType::CLOUD_STINK) which_m->curse(r1); - break; } if(univ.town.is_web(where_check.x,where_check.y) && which_m->m_type != eRace::BUG) { which_m->spell_note(19); r1 = get_ran(1,2,3); which_m->web(r1); univ.town.set_web(where_check.x,where_check.y,false); - break; } - if(univ.town.is_fire_wall(where_check.x,where_check.y)) { + if(!fire_wall && univ.town.is_fire_wall(where_check.x,where_check.y)) { + fire_wall = true; r1 = get_ran(2,1,6); - if(have_radiate && which_radiate != eFieldType::WALL_FIRE) + if(!have_radiate || which_radiate != eFieldType::WALL_FIRE) damage_monst(*which_m,7,r1,eDamageType::FIRE); - break; } if(univ.town.is_force_cage(where_check.x,where_check.y)) process_force_cage(where_check, univ.get_target_i(*which_m)); @@ -1192,7 +1211,7 @@ void activate_monsters(short code,short /*attitude*/) { if(code == 0) return; for(short i = 0; i < univ.town->creatures.size(); i++) - if(univ.town->creatures[i].spec_enc_code == code) { + if(univ.town->creatures[i].number > 0 && univ.town->creatures[i].spec_enc_code == code) { cTownperson& monst = univ.town->creatures[i]; univ.town.monst.assign(i, monst, univ.scenario.scen_monsters[monst.number], univ.party.easy_mode, univ.difficulty_adjust()); univ.town.monst[i].spec_enc_code = 0; diff --git a/src/game/boe.monster.hpp b/src/game/boe.monster.hpp index be3fb0656..8b68f8a2f 100644 --- a/src/game/boe.monster.hpp +++ b/src/game/boe.monster.hpp @@ -24,7 +24,7 @@ bool seek_party(short i,location l1,location l2); bool flee_party(short i,location l1,location l2); bool try_move(short i,location start,short x,short y); bool combat_move_monster(short which,location destination); -location find_clear_spot(location from_where,short mode); +location find_clear_spot(location from_where,short mode,short x_width = 1, short y_width = 1); location random_shift(location start); bool outdoor_move_monster(short num,location dest); bool town_move_monster(short num,location dest); diff --git a/src/game/boe.newgraph.cpp b/src/game/boe.newgraph.cpp index b978b3a02..01d0fd07f 100644 --- a/src/game/boe.newgraph.cpp +++ b/src/game/boe.newgraph.cpp @@ -34,6 +34,7 @@ #include "tools/enum_map.hpp" #include "replay.hpp" #include +#include short monsters_faces[190] = { 0,1,2,3,4,5,6,7,8,9, @@ -126,10 +127,7 @@ terrain_screen_rects_t terrain_screen_rects() { to.offset(current_terrain_ul); rectangle in_frame = to; - in_frame.top += 11; - in_frame.left += 11; - in_frame.bottom -= 11; - in_frame.right -= 11; + in_frame.inset(14, 14); return {from, to, in_frame}; } @@ -359,10 +357,20 @@ void do_missile_anim(short num_steps,location missile_origin,short sound_num) { return; } - // Eliminate missiles traveling 0 distance + bool can_see_origin = party_can_see(missile_origin) < 6; + bool missile_flying = false; for(short i = 0; i < 30; i++) { - if((store_missiles[i].missile_type >= 0) && (missile_origin == store_missiles[i].dest)) - store_missiles[i].missile_type = -1; + if((store_missiles[i].missile_type >= 0)){ + // Eliminate missiles traveling 0 distance + if(missile_origin == store_missiles[i].dest){ + store_missiles[i].missile_type = -1; + } + // Eliminate missiles whose whole arc is out of view + if(!can_see_origin && party_can_see(store_missiles[i].dest) == 6){ + store_missiles[i].missile_type = -1; + missile_flying = true; + } + } } std::vector missile_targets; @@ -375,7 +383,11 @@ void do_missile_anim(short num_steps,location missile_origin,short sound_num) { missile_targets.push_back(store_missiles[i].dest); } - if(missile_targets.empty()) return; + if(missile_targets.empty()){ + if(missile_flying) + play_sound(-1 * sound_num); + return; + } if(missile_targets.size() == 1){ tracking_missile = missile_indices[0]; @@ -386,9 +398,14 @@ void do_missile_anim(short num_steps,location missile_origin,short sound_num) { tracking_missile = missile_indices[closest_point_idx(missile_targets, camera_dest)]; } - // Start the camera as close as possible to containing the origin and the camera destination - // on the same screen - center = between_anchor_points(missile_origin, camera_dest); + if(can_see_origin){ + // Start the camera as close as possible to containing the origin and the camera destination + // on the same screen + center = between_anchor_points(missile_origin, camera_dest); + }else{ + // Can't see the origin, so showing the whole flight path when we don't have to is kind of a spoiler. + center = camera_dest; + } // make terrain_template contain current terrain all nicely draw_terrain(1); @@ -575,7 +592,7 @@ void do_explosion_anim(short /*sound_num*/,short special_draw, short snd) { draw_terrain(1); if(special_draw != 2) { auto ter_rects = terrain_screen_rects(); - rect_draw_some_item(terrain_screen_gworld().getTexture(),ter_rects.from,mainPtr(),ter_rects.to); + rect_draw_some_item(terrain_screen_gworld().getTexture(),ter_rects.from,mainPtr(),ter_rects.to, ter_rects.in_frame); } TextStyle style; @@ -994,6 +1011,7 @@ void place_talk_str(std::string str_to_place,std::string str_to_place2,short col // First determine the offsets of clickable words. // The added spaces ensure that end-of-word boundaries are found std::string str = str_to_place + " |" + str_to_place2 + " "; + std::vector hilites; std::vector nodes; int wordStart = 0, wordEnd = 0; @@ -1013,6 +1031,16 @@ void place_talk_str(std::string str_to_place,std::string str_to_place2,short col } } + // If the text will overflow onto the preset talk words, shrink it + extern std::vector preset_word_locs; + auto break_info = calculate_line_wrapping(word_place_rect, str, style); + short lines = break_info.size(); + short height = lines * (style.lineHeight+1); + if(height >= word_place_rect.height()){ + short overflow = height - preset_word_locs.back().y; + style.lineHeight -= ceil(overflow / (float) lines); + } + std::vector word_rects = draw_string_hilite(talk_gworld(), word_place_rect, str, style, hilites, color ? CUSTOM_WORD_ON : CUSTOM_WORD_OFF); if(!talk_end_forced) { diff --git a/src/game/boe.party.cpp b/src/game/boe.party.cpp index 95262d51b..da23ecdfc 100644 --- a/src/game/boe.party.cpp +++ b/src/game/boe.party.cpp @@ -1384,13 +1384,17 @@ void cast_town_spell(location where) { case eSpell::UNLOCK: // TODO: Is the unlock spell supposed to have a max range? if(univ.scenario.ter_types[ter].special == eTerSpec::UNLOCKABLE){ - if(univ.scenario.ter_types[ter].flag2 == 10) + short success_chance = 0; + short total_modifier = 0; + short min_fail_roll = (135 - combat_percent[min(19,level)]); + if(univ.scenario.ter_types[ter].flag2 == 10){ r1 = 10000; - else{ - r1 = get_ran(1,1,100) - 5 * adj + 5 * univ.town->difficulty; - r1 += univ.scenario.ter_types[ter].flag2 * 7; + }else{ + total_modifier = -5 * adj + 5 * univ.town.door_diff_adjust() + univ.scenario.ter_types[ter].flag2 * 7; + r1 = get_ran(1,1,100) + total_modifier; + success_chance = minmax(min_fail_roll - 1 - total_modifier, 0, 100); } - if(r1 < (135 - combat_percent[min(19,level)])) { + if(r1 < min_fail_roll) { add_string_to_buf(" Door unlocked."); play_sound(9); univ.town->terrain(where.x,where.y) = univ.scenario.ter_types[ter].flag1; @@ -1398,7 +1402,7 @@ void cast_town_spell(location where) { } else { play_sound(41); - add_string_to_buf(" Didn't work."); + add_string_to_buf(" Didn't work. (" + std::to_string(success_chance) + "\% chance)"); } }else add_string_to_buf(" Wrong terrain type."); @@ -1406,10 +1410,13 @@ void cast_town_spell(location where) { case eSpell::DISPEL_BARRIER: if((univ.town.is_fire_barr(where.x,where.y)) || (univ.town.is_force_barr(where.x,where.y))) { - r1 = get_ran(1,1,100) - 5 * adj + 5 * (univ.town->difficulty / 10) + 25 * univ.town->strong_barriers; + short total_modifier = -5 * adj + 5 * (univ.town->difficulty / 10) + 25 * univ.town->strong_barriers; if(univ.town.is_fire_barr(where.x,where.y)) - r1 -= 8; - if(r1 < (120 - combat_percent[min(19,level)])) { + total_modifier -= 8; + short min_fail_roll = (120 - combat_percent[min(19,level)]); + short success_chance = minmax(min_fail_roll - 1 - total_modifier, 0, 100); + r1 = get_ran(1,1,100) + total_modifier; + if(r1 < min_fail_roll) { add_string_to_buf(" Barrier broken."); univ.town.set_fire_barr(where.x,where.y,false); univ.town.set_force_barr(where.x,where.y,false); @@ -1420,8 +1427,9 @@ void cast_town_spell(location where) { else { store = get_ran(1,0,1); (void) store; // TODO: Why does it even do this? + play_sound(41); - add_string_to_buf(" Didn't work."); + add_string_to_buf(" Didn't work. (" + std::to_string(success_chance) + "\% chance)"); } } else if(univ.town.is_force_cage(where.x,where.y)) { add_string_to_buf(" Cage broken."); diff --git a/src/game/boe.specials.cpp b/src/game/boe.specials.cpp index 97036a6ed..4d50f64f8 100644 --- a/src/game/boe.specials.cpp +++ b/src/game/boe.specials.cpp @@ -1986,8 +1986,8 @@ void special_increase_age(long length, bool queue) { draw_terrain(0); } -void queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc) { - if(spec < 0) return; +bool queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc) { + if(spec < 0) return false; pending_special_type queued_special; queued_special.spec = spec; queued_special.where = spec_loc; @@ -1999,6 +1999,7 @@ void queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, loca run_special(queued_special, nullptr, nullptr, nullptr); else special_queue.push(queued_special); + return true; } void run_special(pending_special_type spec, short* a, short* b, bool* redraw) { @@ -2008,6 +2009,8 @@ void run_special(pending_special_type spec, short* a, short* b, bool* redraw) { univ.party.age = std::max(univ.party.age, store_time); } +extern bool need_enter_town_autosave; + // This is the big painful one, the main special engine entry point // which_mode - says when it was called // which_type - where the special is stored (town, out, scenario) @@ -2165,6 +2168,10 @@ void run_special(eSpecCtx which_mode, eSpecCtxType which_type, spec_num_t start_ erase_out_specials(); else erase_town_specials(); special_in_progress = false; + if(which_mode == eSpecCtx::ENTER_TOWN && need_enter_town_autosave){ + try_auto_save("EnterTown"); + need_enter_town_autosave = false; + } // TODO: Should find a way to do this that doesn't risk stack overflow if(ctx.next_spec == -1 && !special_queue.empty()) { @@ -2637,7 +2644,7 @@ void oneshot_spec(const runtime_state& ctx) { univ.get_strs(strs, ctx.cur_spec_type, spec.m1); // Leave / Take buttons[0] = 9; buttons[1] = 19; - dlg_res = custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c); + dlg_res = custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c, &univ); if(dlg_res == 1) {set_sd = false; ctx.next_spec = -1;} else { store_i = univ.scenario.get_stored_item(spec.ex1a); @@ -2677,7 +2684,7 @@ void oneshot_spec(const runtime_state& ctx) { if((spec.m1 >= 0) || (spec.m2 >= 0)) { univ.get_strs(strs[0],strs[1], ctx.cur_spec_type, spec.m1, spec.m2); buttons[0] = 3; buttons[1] = 2; - dlg_res = custom_choice_dialog(strs,spec.pic,ePicType(spec.pictype),buttons, true, spec.ex1c, spec.ex2c); + dlg_res = custom_choice_dialog(strs,spec.pic,ePicType(spec.pictype),buttons, true, spec.ex1c, spec.ex2c, &univ); // TODO: Make custom_choice_dialog return string? } else dlg_res = cChoiceDlog("basic-trap",{"yes","no"}).show() == "no"; @@ -4006,7 +4013,7 @@ void townmode_spec(const runtime_state& ctx) { else { univ.get_strs(strs,ctx.cur_spec_type, spec.m1); buttons[0] = 9; buttons[1] = 35; - if(custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c) == 1) + if(custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c, &univ) == 1) ctx.next_spec = -1; else { int x = univ.party.get_ptr(10), y = univ.party.get_ptr(11); @@ -4037,7 +4044,7 @@ void townmode_spec(const runtime_state& ctx) { else { univ.get_strs(strs, ctx.cur_spec_type,spec.m1); buttons[0] = 9; buttons[1] = 8; - if(custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c) == 1) { + if(custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c, &univ) == 1) { ctx.next_spec = -1; if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; @@ -4069,7 +4076,7 @@ void townmode_spec(const runtime_state& ctx) { else { univ.get_strs(strs,ctx.cur_spec_type, spec.m1); buttons[0] = 20; buttons[1] = 24; - int i = spec.ex2b == 1 ? 2 : custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons); + int i = spec.ex2b == 1 ? 2 : custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, false, -1, -1, &univ); *ctx.ret_a = 1; if(i == 1) { ctx.next_spec = -1; diff --git a/src/game/boe.specials.hpp b/src/game/boe.specials.hpp index 32523b9f3..203f35d37 100644 --- a/src/game/boe.specials.hpp +++ b/src/game/boe.specials.hpp @@ -20,7 +20,7 @@ void teleport_party(short x,short y,short mode); bool run_stone_circle(short which); void change_level(short town_num,short x,short y); void push_things(); -void queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc); +bool queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc); void run_special(eSpecCtx which_mode, eSpecCtxType which_type, spec_num_t start_spec, location spec_loc, short* a = nullptr, short* b = nullptr, bool* redraw = nullptr); void run_special(pending_special_type spec, short* a, short* b, bool* redraw); diff --git a/src/game/boe.startup.cpp b/src/game/boe.startup.cpp index f46c80f92..f1ad1f0f0 100644 --- a/src/game/boe.startup.cpp +++ b/src/game/boe.startup.cpp @@ -91,9 +91,6 @@ void handle_startup_button_click(eStartButton btn, eKeyMod mods) { force_party = true; start_new_game(true); - } else { - cChoiceDlog("need-party").show(); - break; } } diff --git a/src/game/boe.text.cpp b/src/game/boe.text.cpp index 2aa7a3ac8..f579ba7b1 100644 --- a/src/game/boe.text.cpp +++ b/src/game/boe.text.cpp @@ -1129,7 +1129,7 @@ void print_buf () { line_style.applyTo(text, get_ui_scale()); // A spacing factor of 1.0 within multiline messages doesn't actually line up with other single buffer lines text.setLineSpacing(0.85); - text.setString(message); + text.setString(sf::String::fromUtf8(message.begin(), message.end())); text.setPosition(moveTo); draw_scale_aware_text(text_area_gworld(), text); } @@ -1261,19 +1261,13 @@ std::string get_location(cUniverse* specific_univ) { std::string loc_str = ""; location loc = outdoors ? global_to_local(specific_univ->party.out_loc) : specific_univ->party.town_loc; - if(outdoors) { - loc_str = specific_univ->out->name; - for(short i = 0; i < specific_univ->out->area_desc.size(); i++) - if(loc.in(specific_univ->out->area_desc[i])) { - loc_str = specific_univ->out->area_desc[i].descr; - } - } - if(town){ - loc_str = specific_univ->town->name; - for(short i = 0; i < specific_univ->town->area_desc.size(); i++) - if(loc.in(specific_univ->town->area_desc[i])) { - loc_str = specific_univ->town->area_desc[i].descr; - } + const std::vector& area_desc = (outdoors ? specific_univ->out->area_desc : specific_univ->town->area_desc); + + loc_str = outdoors ? specific_univ->out->name : specific_univ->town->name; + for(const info_rect_t& area : area_desc){ + if(!area.empty() && loc.in(area)) { + loc_str = area.descr; + } } return loc_str; } diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index 7f46d89ad..95e7e5435 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "boe.global.hpp" @@ -62,12 +63,15 @@ location town_force_loc; bool shop_button_active[12]; rectangle map_title_rect = {3,50,15,300}; rectangle map_bar_rect = {15,50,27,300}; +rectangle map_tooltip_rect = {270,50,307,300}; void force_town_enter(short which_town,location where_start) { town_force = which_town; town_force_loc = where_start; } +bool need_enter_town_autosave = false; + //short entry_dir; // if 9, go to forced void start_town_mode(short which_town, short entry_dir, bool debug_enter) { short town_number; @@ -333,8 +337,9 @@ void start_town_mode(short which_town, short entry_dir, bool debug_enter) { if(no_thrash.count(&monst) == 0) monst.active = eCreatureStatus::DEAD; } + bool specials_queued = false; if(!debug_enter) - handle_town_specials(town_number, (short) town_toast,(entry_dir < 9) ? univ.town->start_locs[entry_dir] : town_force_loc); + specials_queued = handle_town_specials(town_number, (short) town_toast,(entry_dir < 9) ? univ.town->start_locs[entry_dir] : town_force_loc); // Flush excess doomguards and viscous goos for(short i = 0; i < univ.town.monst.size(); i++) @@ -491,24 +496,6 @@ void start_town_mode(short which_town, short entry_dir, bool debug_enter) { monst.targ_loc.y = 0; } - // check horses - for(short i = 0; i < univ.party.boats.size(); i++) { - if(univ.scenario.boats[i].which_town >= 0 && univ.scenario.boats[i].loc.x >= 0) { - if(!univ.party.boats[i].exists) { - univ.party.boats[i] = univ.scenario.boats[i]; - univ.party.boats[i].exists = true; - } - } - } - for(short i = 0; i < univ.party.horses.size(); i++) { - if(univ.scenario.horses[i].which_town >= 0 && univ.scenario.horses[i].loc.x >= 0) { - if(!univ.party.horses[i].exists) { - univ.party.horses[i] = univ.scenario.horses[i]; - univ.party.horses[i].exists = true; - } - } - } - clear_map(); reset_item_max(); town_force = 200; @@ -516,7 +503,9 @@ void start_town_mode(short which_town, short entry_dir, bool debug_enter) { // ... except it actually doesn't, because the town enter special is only queued, not run immediately. draw_terrain(1); - try_auto_save("EnterTown"); + // If special nodes still need to be called, we can't do the autosave yet. + if(specials_queued) need_enter_town_autosave = true; + else try_auto_save("EnterTown"); } @@ -643,8 +632,8 @@ location end_town_mode(bool switching_level,location destination, bool debug_lea return to_return; } -void handle_town_specials(short /*town_number*/, bool town_dead,location /*start_loc*/) { - queue_special(eSpecCtx::ENTER_TOWN, eSpecCtxType::TOWN, town_dead ? univ.town->spec_on_entry_if_dead : univ.town->spec_on_entry, univ.party.town_loc); +bool handle_town_specials(short /*town_number*/, bool town_dead,location /*start_loc*/) { + return queue_special(eSpecCtx::ENTER_TOWN, eSpecCtxType::TOWN, town_dead ? univ.town->spec_on_entry_if_dead : univ.town->spec_on_entry, univ.party.town_loc); } void handle_leave_town_specials(short /*town_number*/, short which_spec,location /*start_loc*/) { @@ -1155,23 +1144,30 @@ void pick_lock(location where,short pc_num) { return; } + // Roll to determine if the pick breaks r1 = get_ran(1,1,100) + which_item->abil_strength * 7; - if(r1 < 75) will_break = true; - r1 = get_ran(1,1,100) - 5 * univ.party[pc_num].stat_adj(eSkill::DEXTERITY) + univ.town->difficulty * 7 + // Roll to determine success or fail + short total_modifier = -5 * univ.party[pc_num].stat_adj(eSkill::DEXTERITY) + univ.town.door_diff_adjust() * 7 - 5 * univ.party[pc_num].skill(eSkill::LOCKPICKING) - which_item->abil_strength * 7; - // Nimble? if(univ.party[pc_num].traits[eTrait::NIMBLE]) - r1 -= 8; - + total_modifier -= 8; if(univ.party[pc_num].has_abil_equip(eItemAbil::THIEVING)) - r1 = r1 - 12; + total_modifier -= 12; + + r1 = get_ran(1,1,100) + total_modifier; + unlock_adjust = univ.scenario.ter_types[terrain].flag2; - if((unlock_adjust >= 5) || (r1 > (unlock_adjust * 15 + 30))) { - add_string_to_buf(" Didn't work."); + short success_chance = 0; + short max_success_roll = (unlock_adjust * 15 + 30); + if(unlock_adjust < 5){ + success_chance = minmax(max_success_roll - total_modifier, 0, 100); + } + if((unlock_adjust >= 5) || (r1 > max_success_roll)) { + add_string_to_buf(" Didn't work. (" + std::to_string(success_chance) + "\% chance)"); if(will_break) { add_string_to_buf(" Pick breaks."); univ.party[pc_num].remove_charge(which_item.slot); @@ -1193,16 +1189,23 @@ void bash_door(location where,short pc_num) { short r1,unlock_adjust; terrain = univ.town->terrain(where.x,where.y); - r1 = get_ran(1,1,100) - 15 * univ.party[pc_num].stat_adj(eSkill::STRENGTH) + univ.town->difficulty * 4; - + if(univ.scenario.ter_types[terrain].special != eTerSpec::UNLOCKABLE) { add_string_to_buf(" Wrong terrain type."); return; } + short total_modifier = -15 * univ.party[pc_num].stat_adj(eSkill::STRENGTH) + univ.town.door_diff_adjust() * 4; unlock_adjust = univ.scenario.ter_types[terrain].flag2; - if(unlock_adjust >= 5 || r1 > (unlock_adjust * 15 + 40) || univ.scenario.ter_types[terrain].flag3 != 1) { - add_string_to_buf(" Didn't work."); + short max_success_roll = (unlock_adjust * 15 + 40); + + r1 = get_ran(1,1,100) + total_modifier; + bool success_chance = 0; + if(unlock_adjust < 5 && univ.scenario.ter_types[terrain].flag3 == 1){ + success_chance = minmax(max_success_roll - total_modifier, 0, 100); + } + if(unlock_adjust >= 5 || r1 > max_success_roll || univ.scenario.ter_types[terrain].flag3 != 1) { + add_string_to_buf(" Didn't work. (" + std::to_string(success_chance) + "\% chance)"); damage_pc(univ.party[pc_num],get_ran(1,1,4),eDamageType::SPECIAL,eRace::UNKNOWN); } else { @@ -1301,75 +1304,57 @@ void clear_map() { draw_map(true); } -void draw_map(bool need_refresh) { +const short view_max_dim = 40; +rectangle minimap_view_rect() { + rectangle view_rect; + short total_size, party_loc_x, party_loc_y; + + if((is_out()) || ((is_combat()) && (which_combat_type == 0)) || + ((overall_mode == MODE_TALKING) && (store_pre_talk_mode == MODE_OUTDOORS)) || + ((overall_mode == MODE_SHOPPING) && (store_pre_shop_mode == MODE_OUTDOORS))) { + total_size = 48; + party_loc_x = univ.party.loc_in_sec.x; + party_loc_y = univ.party.loc_in_sec.y; + } + else { + total_size = univ.town->max_dim; + party_loc_x = univ.party.town_loc.x; + party_loc_y = univ.party.town_loc.y; + } + if(total_size <= view_max_dim){ + view_rect = {0, 0, total_size, total_size}; + }else{ + short scroll_max = total_size - view_max_dim; + view_rect.left = minmax(0, scroll_max, party_loc_x - view_max_dim / 2); + view_rect.top = minmax(0, scroll_max, party_loc_y - view_max_dim / 2); + view_rect.right = view_rect.left + view_max_dim; + view_rect.bottom = view_rect.top + view_max_dim; + } + return view_rect; +} + +void draw_map(bool need_refresh, std::string tooltip_text) { if(!map_visible) return; pic_num_t pic; - rectangle the_rect,map_world_rect = {0,0,384,384}; + rectangle the_rect; + + rectangle map_world_rect(map_gworld()); location where; location kludge; rectangle draw_rect,orig_draw_rect = {0,0,6,6},ter_temp_from,base_source_rect = {0,0,12,12}; rectangle dlogpicrect = {6,6,42,42}; bool draw_pcs = true,out_mode; - rectangle view_rect= {0,0,48,48},tiny_rect = {0,0,32,32}, - redraw_rect = {0,0,48,48},big_rect = {0,0,64,64}; // Rectangle visible in view screen - rectangle area_to_draw_from,area_to_draw_on = {29,47,269,287}; + rectangle area_to_draw_on = {29,47,269,287}; ter_num_t what_ter; bool expl; - short total_size = 48; // if full redraw, use this to figure out everything rectangle custom_from; town_map_adj.x = 0; town_map_adj.y = 0; - // view rect is rect that is visible, redraw rect is area to redraw now - // area_to_draw_from is final draw from rect - // area_to_draw_on is final draw to rect - // extern short store_pre_shop_mode,store_pre_talk_mode; - if((is_out()) || ((is_combat()) && (which_combat_type == 0)) || - ((overall_mode == MODE_TALKING) && (store_pre_talk_mode == MODE_OUTDOORS)) || - ((overall_mode == MODE_SHOPPING) && (store_pre_shop_mode == MODE_OUTDOORS))) { - view_rect.left = minmax(0,8,univ.party.loc_in_sec.x - 20); - view_rect.right = view_rect.left + 40; - view_rect.top = minmax(0,8,univ.party.loc_in_sec.y - 20); - view_rect.bottom = view_rect.top + 40; - redraw_rect = view_rect; - } - else { - total_size = univ.town->max_dim; - switch(total_size) { - case 64: - view_rect.left = minmax(0,24,univ.party.town_loc.x - 20); - view_rect.right = view_rect.left + 40; - view_rect.top = minmax(0,24,univ.party.town_loc.y - 20); - view_rect.bottom = view_rect.top + 40; - redraw_rect = big_rect; - break; - case 48: - view_rect.left = minmax(0,8,univ.party.town_loc.x - 20); - view_rect.right = view_rect.left + 40; - view_rect.top = minmax(0,8,univ.party.town_loc.y - 20); - view_rect.bottom = view_rect.top + 40; - redraw_rect = view_rect; - break; - case 32: - view_rect = tiny_rect; - redraw_rect = view_rect; - break; - } - } - if((is_out()) || ((is_combat()) && (which_combat_type == 0)) || - ((overall_mode == MODE_TALKING) && (store_pre_talk_mode == MODE_OUTDOORS)) || - ((overall_mode == MODE_SHOPPING) && (store_pre_shop_mode == MODE_OUTDOORS)) || - is_town() || is_combat()) { - area_to_draw_from = view_rect; - area_to_draw_from.width() = 40; - area_to_draw_from.height() = 40; - area_to_draw_from.left *= 6; - area_to_draw_from.right *= 6; - area_to_draw_from.top *= 6; - area_to_draw_from.bottom *= 6; - } + // We use view_rect as a camera to show only as much of the map as can fit in the window + rectangle view_rect = minimap_view_rect(); if(is_combat()) draw_pcs = false; @@ -1394,7 +1379,7 @@ void draw_map(bool need_refresh) { // Now, if shopping or talking, just don't touch anything. if((overall_mode == MODE_SHOPPING) || (overall_mode == MODE_TALKING)) - redraw_rect.right = -1; + view_rect.right = -1; // Otherwise, clear to black first: else fill_rect(map_gworld(), map_world_rect, sf::Color::Black); @@ -1406,12 +1391,15 @@ void draw_map(bool need_refresh) { out_mode = true; else out_mode = false; - // TODO: It could be possible to draw the entire map here and then only refresh if a spot actually changes terrain type + // While iterating the map, calculate the discovered extent of rooms/areas + const std::vector& area_desc = is_out() ? univ.out->area_desc : univ.town->area_desc; + std::vector known_area_rects(area_desc.size(), {std::numeric_limits::max(),std::numeric_limits::max(),std::numeric_limits::min(),std::numeric_limits::min()}); + sf::Texture& small_ter_gworld = *ResMgr::graphics.get("termap"); - for(where.x = redraw_rect.left; where.x < redraw_rect.right; where.x++) - for(where.y = redraw_rect.top; where.y < redraw_rect.bottom; where.y++) { + for(where.x = view_rect.left; where.x < view_rect.right; where.x++) + for(where.y = view_rect.top; where.y < view_rect.bottom; where.y++) { draw_rect = orig_draw_rect; - draw_rect.offset(6 * where.x, 6 * where.y); + draw_rect.offset(6 * (where.x - view_rect.left), 6 * (where.y - view_rect.top)); if(out_mode) what_ter = univ.out[where.x + 48 * univ.party.i_w_c.x][where.y + 48 * univ.party.i_w_c.y]; @@ -1423,6 +1411,7 @@ void draw_map(bool need_refresh) { expl = univ.out.out_e[where.x + 48 * univ.party.i_w_c.x][where.y + 48 * univ.party.i_w_c.y]; else expl = is_explored(where.x,where.y); + // Draw tile if(expl != 0) { pic = univ.scenario.ter_types[what_ter].map_pic; bool drawLargeIcon = false; @@ -1469,11 +1458,36 @@ void draw_map(bool need_refresh) { if(is_out() ? univ.out->roads[where.x][where.y] : univ.town.is_road(where.x,where.y)) { draw_rect.inset(1,1); - rect_draw_some_item(*ResMgr::graphics.get("trim"),{8,112,12,116},map_gworld(),draw_rect); + rect_draw_some_item(*ResMgr::graphics.get("fields"),{80,28,84,32},map_gworld(),draw_rect); + } + + for(int i = 0; i < area_desc.size(); ++i){ + location real_where = where; + if(is_out()) real_where = local_to_global(where); + info_rect_t area = area_desc[i]; + rectangle& known_bounds = known_area_rects[i]; + // tile is in an area rectangle. see if it extends the party's known bounds of the area + if(!area.empty() && area.contains(where) && !is_blocked(real_where, false)){ + if(where.x < known_bounds.left) known_bounds.left = where.x; + if(where.y < known_bounds.top) known_bounds.top = where.y; + if(where.x + 1 > known_bounds.right) known_bounds.right = where.x + 1; + if(where.y + 1 > known_bounds.bottom) known_bounds.bottom = where.y + 1; + } } } } + // Draw known extent of area rectangles + for(const rectangle& known_bounds : known_area_rects){ + if(!known_bounds.empty()){ + draw_rect.top = 6 * (known_bounds.top - view_rect.top); + draw_rect.left = 6 * (known_bounds.left - view_rect.left); + draw_rect.bottom = 6 * (known_bounds.bottom - view_rect.top); + draw_rect.right = 6 * (known_bounds.right - view_rect.left); + frame_rect(map_gworld(), draw_rect, Colours::RED); + } + } + map_gworld().display(); // this stops flickering if the display time is too long glFlush(); @@ -1501,41 +1515,71 @@ void draw_map(bool need_refresh) { win_draw_string(mini_map(), map_bar_rect,"(Hit Escape to close.)",eTextMode::WRAP,style); if(canMap) { - rect_draw_some_item(map_gworld().getTexture(),area_to_draw_from,mini_map(),area_to_draw_on); + rect_draw_some_item(map_gworld().getTexture(),the_rect,mini_map(),area_to_draw_on); + auto mark_loc = [view_rect, &draw_rect, area_to_draw_on](location where, sf::Color inner, sf::Color outer, eTerSpec if_spec = eTerSpec::NONE) -> void { + location real_where = where; + if(is_out()) real_where = local_to_global(where); + if((is_explored(real_where.x,real_where.y)) && + ((where.x >= view_rect.left) && (where.x < view_rect.right) + && where.y >= view_rect.top && where.y < view_rect.bottom)){ + // if if_spec is specified, make sure the terrain on the spot has the given special + if(if_spec != eTerSpec::NONE){ + ter_num_t ter = is_out() ? univ.out[real_where] : univ.town->terrain(real_where.x, real_where.y); + if(univ.scenario.ter_types[ter].special != if_spec) return; + } + + draw_rect.left = area_to_draw_on.left + 6 * (where.x - view_rect.left); + draw_rect.top = area_to_draw_on.top + 6 * (where.y - view_rect.top); + draw_rect.right = draw_rect.left + 6; + draw_rect.bottom = draw_rect.top + 6; + + fill_rect(mini_map(), draw_rect, inner); + frame_circle(mini_map(), draw_rect, outer); + } + }; + // Now place PCs and monsters if(draw_pcs) { if((is_town()) && (univ.party.status[ePartyStatus::DETECT_LIFE] > 0)) for(short i = 0; i < univ.town.monst.size(); i++) if(univ.town.monst[i].is_alive()) { where = univ.town.monst[i].cur_loc; - if((is_explored(where.x,where.y)) && - ((where.x >= view_rect.left) && (where.x < view_rect.right) - && where.y >= view_rect.top && where.y < view_rect.bottom)){ - - draw_rect.left = area_to_draw_on.left + 6 * (where.x - view_rect.left); - draw_rect.top = area_to_draw_on.top + 6 * (where.y - view_rect.top); - draw_rect.right = draw_rect.left + 6; - draw_rect.bottom = draw_rect.top + 6; - - fill_rect(mini_map(), draw_rect, Colours::GREEN); - frame_circle(mini_map(), draw_rect, Colours::BLUE); - } + mark_loc(where, Colours::GREEN, Colours::BLUE); } if((overall_mode != MODE_SHOPPING) && (overall_mode != MODE_TALKING)) { where = (is_town()) ? univ.party.town_loc : global_to_local(univ.party.out_loc); - draw_rect.left = area_to_draw_on.left + 6 * (where.x - view_rect.left); - draw_rect.top = area_to_draw_on.top + 6 * (where.y - view_rect.top); - draw_rect.right = draw_rect.left + 6; - draw_rect.bottom = draw_rect.top + 6; - fill_rect(mini_map(), draw_rect, Colours::RED); - frame_circle(mini_map(), draw_rect, sf::Color::Black); - + mark_loc(where, Colours::RED, Colours::BLACK); } } + + // Draw signs + const std::vector& sign_locs = is_out() ? univ.out->sign_locs : univ.town->sign_locs; + for(sign_loc_t sign : sign_locs){ + mark_loc(sign, Colours::EMPTY, Colours::YELLOW, eTerSpec::IS_A_SIGN); + } + // Draw town entrances + if(is_out()){ + const std::vector& city_locs = univ.out->city_locs; + // TODO don't draw hidden ones + for(spec_loc_t city : city_locs){ + mark_loc(city, Colours::EMPTY, Colours::GREEN, eTerSpec::TOWN_ENTRANCE); + } + } + // Draw vehicles + for(auto& boat : univ.party.boats) { + if(!vehicle_is_here(boat)) continue; + mark_loc(boat.loc, Colours::MAROON, Colours::BLACK); + } + for(auto& horse : univ.party.horses) { + if(!vehicle_is_here(horse)) continue; + mark_loc(horse.loc, Colours::MAROON, Colours::BLACK); + } } + win_draw_string(mini_map(), map_tooltip_rect,tooltip_text,eTextMode::WRAP,style); + mini_map().setActive(false); mini_map().display(); diff --git a/src/game/boe.town.hpp b/src/game/boe.town.hpp index 9467ad2c7..437edde39 100644 --- a/src/game/boe.town.hpp +++ b/src/game/boe.town.hpp @@ -6,7 +6,7 @@ void force_town_enter(short which_town,location where_start); void start_town_mode(short which_town, short entry_dir, bool debug_enter = false); location end_town_mode(bool switching_level,location destination,bool debug_leave=false); // returns new party location void handle_leave_town_specials(short town_number, short which_spec,location start_loc) ; -void handle_town_specials(short town_number, bool town_dead,location start_loc) ; +bool handle_town_specials(short town_number, bool town_dead,location start_loc) ; bool abil_exists(eItemAbil abil); void start_town_combat(eDirection direction); @@ -28,7 +28,8 @@ void erase_completed_specials(cArea& sector, std::function clear void erase_out_specials(); bool does_location_have_special(cOutdoors& sector, location loc, eTerSpec type); void clear_map(); -void draw_map(bool need_refresh); +rectangle minimap_view_rect(); +void draw_map(bool need_refresh, std::string tooltip_text = ""); bool is_door(location destination); void display_map(); void check_done(); diff --git a/src/gfx/render_shapes.hpp b/src/gfx/render_shapes.hpp index d56610aed..623935354 100644 --- a/src/gfx/render_shapes.hpp +++ b/src/gfx/render_shapes.hpp @@ -58,6 +58,8 @@ namespace Colours { const sf::Color YELLOW { 0xff, 0xff, 0x31}; const sf::Color ORANGE { 0xff, 0x80, 0x00}; const sf::Color LIGHT_BLUE { 0xad, 0xd8, 0xe6 }; // Spell points on dark background + // On Windows, TRANSPARENT is macro-defined as something...? + const sf::Color EMPTY { 0x00, 0x00, 0x00, 0x00 }; // Text colours for shopping / talking // TODO: The Windows version appears to use completely different colours? const sf::Color SHADOW { 0x00, 0x00, 0x68}; // formerly c[3] QD colour = {0,0,26623} (shop/character name shadow, shop subtitle) diff --git a/src/gfx/render_text.cpp b/src/gfx/render_text.cpp index 869ac0704..c374d30ff 100644 --- a/src/gfx/render_text.cpp +++ b/src/gfx/render_text.cpp @@ -117,7 +117,7 @@ break_info_t calculate_line_wrapping(rectangle dest_rect, std::string str, TextS short str_len = str.length(); unsigned short last_line_break = 0,last_word_break = 0; - str_to_draw.setString(str); + str_to_draw.setString(sf::String::fromUtf8(str.begin(), str.end())); // Even if the text is only one line, break_info is required for calculating word boundaries. // So we can't skip the rest of this. @@ -241,10 +241,6 @@ std::string truncate_with_ellipsis(std::string str, const TextStyle& style, int return str; } -std::map substitutions = { - {"–", "--"} -}; - static void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,std::string str,text_params_t& options) { if(str.empty()) return; // Nothing to do! short line_height = options.style.lineHeight; @@ -256,10 +252,7 @@ static void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,st // TODO: Why the heck are we drawing a whole line higher than requested!? adjust_y -= str_to_draw.getLocalBounds().height; - for(auto it : substitutions){ - boost::replace_all(str, it.first, it.second); - } - str_to_draw.setString(str); + str_to_draw.setString(sf::String::fromUtf8(str.begin(), str.end())); short total_width = str_to_draw.getLocalBounds().width; options.style.applyTo(str_to_draw, get_ui_scale()); @@ -333,7 +326,7 @@ static void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,st } for(auto& snippet : options.snippets) { - str_to_draw.setString(snippet.text); + str_to_draw.setString(sf::String::fromUtf8(snippet.text.begin(), snippet.text.end())); str_to_draw.setPosition(snippet.at); if(snippet.hilited) { rectangle bounds = str_to_draw.getGlobalBounds(); @@ -405,7 +398,7 @@ size_t string_length(std::string str, const TextStyle& style, short* height){ sf::Text text; style.applyTo(text); - text.setString(str); + text.setString(sf::String::fromUtf8(str.begin(), str.end())); total_width = text.getLocalBounds().width; if(strings_to_cache.count(str)){ location measurement; diff --git a/src/scenario/area.hpp b/src/scenario/area.hpp index 8308235d1..7d308087b 100644 --- a/src/scenario/area.hpp +++ b/src/scenario/area.hpp @@ -54,6 +54,17 @@ class cArea { bool is_on_map(location loc) const { return loc.x < max_dim && loc.y < max_dim && loc.x >= 0 && loc.y >= 0; } + + std::string loc_str(location where) { + std::string str = name; + for(info_rect_t rect : area_desc){ + if(!rect.empty() && rect.contains(where)){ + str += ": " + rect.descr; + break; + } + } + return str; + } }; #endif diff --git a/src/scenario/scenario.cpp b/src/scenario/scenario.cpp index 0337292ac..5f0df0578 100644 --- a/src/scenario/scenario.cpp +++ b/src/scenario/scenario.cpp @@ -231,7 +231,7 @@ cScenario::cItemStorage::cItemStorage() : ter_type(-1), property(0) { item_odds[i] = 0; } -void cScenario::import_legacy(legacy::scenario_data_type& old){ +void cScenario::import_legacy(legacy::scenario_data_type& old, bool header_only){ is_legacy = true; // TODO eventually the absence of feature flags here will replace is_legacy altogether feature_flags = {}; @@ -267,6 +267,9 @@ void cScenario::import_legacy(legacy::scenario_data_type& old){ rating = eContentRating(old.rating); // TODO: Is this used anywhere? uses_custom_graphics = old.uses_custom_graphics; + + if(header_only) return; + boats.resize(30); horses.resize(30); for(short i = 0; i < 30; i++) { diff --git a/src/scenario/scenario.hpp b/src/scenario/scenario.hpp index b61ca6ef7..3b7944784 100644 --- a/src/scenario/scenario.hpp +++ b/src/scenario/scenario.hpp @@ -148,7 +148,7 @@ class cScenario { towns.back()->init_start(); } - void import_legacy(legacy::scenario_data_type& old); + void import_legacy(legacy::scenario_data_type& old, bool header_only = false); void import_legacy(legacy::scen_item_data_type& old); void writeTo(cTagFile& file) const; void readFrom(const cTagFile& file); diff --git a/src/scenario/special.cpp b/src/scenario/special.cpp index 05bb0d5af..631ae258d 100644 --- a/src/scenario/special.cpp +++ b/src/scenario/special.cpp @@ -21,6 +21,7 @@ #include "skills_traits.hpp" #include "damage.hpp" #include "fields.hpp" +#include "scenario.hpp" bool cTimer::is_valid() const { if(time < 0) return false; @@ -529,6 +530,21 @@ void cSpecial::import_legacy(legacy::special_node_type& old){ } } +// In the editor node list, be as helpful as possible about what the specific node instance does +std::string cSpecial::editor_hint(const cScenario& scenario) const { + std::string hint = (*type).name(); + + switch(type){ + case eSpecType::TOWN_STAIR: + case eSpecType::TOWN_GENERIC_STAIR: + hint += " to "; + if(ex2a < scenario.towns.size()) hint += scenario.towns[ex2a]->loc_str(loc(ex1a, ex1b)); + else hint += "INVALID TOWN"; + default: break; + } + return hint; +} + static eSpecCat getNodeCategory(eSpecType node) { for(int i = 0; i <= int(eSpecCat::OUTDOOR); i++) { eSpecCat cat = eSpecCat(i); diff --git a/src/scenario/special.hpp b/src/scenario/special.hpp index 34683ed14..6f9427a09 100644 --- a/src/scenario/special.hpp +++ b/src/scenario/special.hpp @@ -16,6 +16,7 @@ #include "dialogxml/widgets/pictypes.hpp" namespace legacy { struct special_node_type; }; +class cScenario; static const short SDF_COMPLETE = 250; @@ -108,6 +109,7 @@ class cSpecial { return true; } bool operator!=(const cSpecial& other) const { return !(*this == other); } + std::string editor_hint(const cScenario& scenario) const; }; enum class eSpecCtxType { diff --git a/src/scenario/vehicle.cpp b/src/scenario/vehicle.cpp index 7a2d0c3b5..5716bf3e8 100644 --- a/src/scenario/vehicle.cpp +++ b/src/scenario/vehicle.cpp @@ -25,7 +25,7 @@ cVehicle::cVehicle() : void cVehicle::import_legacy(legacy::horse_record_type& old){ which_town = old.which_town; - exists = old.exists; + exists = which_town >= 0 && which_town <= 200; property = old.property; if(which_town < 200) { loc.x = old.horse_loc.x; @@ -40,7 +40,7 @@ void cVehicle::import_legacy(legacy::horse_record_type& old){ void cVehicle::import_legacy(legacy::boat_record_type& old){ which_town = old.which_town; - exists = old.exists; + exists = which_town >= 0 && which_town <= 200; property = old.property; if(which_town < 200) { loc.x = old.boat_loc.x; diff --git a/src/scenedit/scen.actions.cpp b/src/scenedit/scen.actions.cpp index 91e96bd71..e4e6ae80d 100644 --- a/src/scenedit/scen.actions.cpp +++ b/src/scenedit/scen.actions.cpp @@ -2335,7 +2335,10 @@ void handle_editor_screen_shift(int dx, int dy) { if(town_entrances.size() == 1){ town_entrance_t only_entrance = town_entrances[0]; cChoiceDlog shift_prompt("shift-town-entrance", {"yes", "no"}); - shift_prompt->getControl("out-sec").setText(boost::lexical_cast(only_entrance.out_sec)); + cControl& text = shift_prompt->getControl("prompt"); + text.replaceText("{sec}", boost::lexical_cast(only_entrance.out_sec)); + text.replaceText("{loc}", boost::lexical_cast(only_entrance.loc)); + text.replaceText("{loc_str}", scenario.outdoors[only_entrance.out_sec.x][only_entrance.out_sec.y]->loc_str(only_entrance.loc)); if(shift_prompt.show() == "yes"){ set_current_out(only_entrance.out_sec, true); @@ -2350,9 +2353,9 @@ void handle_editor_screen_shift(int dx, int dy) { std::vector entrance_strings; for(town_entrance_t entrance : town_entrances){ std::ostringstream sstr; - sstr << "Entrance in section " << entrance.out_sec << " at " << entrance.loc; + sstr << "Entrance in section " << entrance.out_sec << " at " << entrance.loc + << " (" <loc_str(entrance.loc) << ")"; entrance_strings.push_back(sstr.str()); - } cStringChoice dlog(entrance_strings, "Shift to one of this town's entrances in the outdoors?"); size_t choice = dlog.show(-1); @@ -2888,6 +2891,7 @@ void place_edit_special(location loc) { bool is_new = false; if(i == specials.size()){ specials.emplace_back(-1,-1,-1); + get_current_area()->specials.emplace_back(); is_new = true; } if(specials[i].spec < 0) { diff --git a/src/scenedit/scen.core.cpp b/src/scenedit/scen.core.cpp index 641b9c662..c01d01f1c 100644 --- a/src/scenedit/scen.core.cpp +++ b/src/scenedit/scen.core.cpp @@ -3344,6 +3344,8 @@ bool build_scenario() { scenario.feature_flags = { {"scenario-meta-format", "V2"}, {"resurrection-balm", "required"}, + // Town difficulty, due to a bug, used to not be added to door difficulty + {"door-town-difficulty", "fixed"} }; fs::path basePath = progDir/"Blades of Exile Base"/(grass ? "bladbase.boes" : "cavebase.boes"); diff --git a/src/scenedit/scen.graphics.cpp b/src/scenedit/scen.graphics.cpp index 40f5bad0c..f0d1a1a59 100644 --- a/src/scenedit/scen.graphics.cpp +++ b/src/scenedit/scen.graphics.cpp @@ -535,15 +535,15 @@ static void apply_mode_buttons() { std::ostringstream strb; switch(mode) { case 0: - strb << i << " - " << (*scenario.scen_specials[i].type).name(); + strb << i << " - " << scenario.scen_specials[i].editor_hint(scenario); set_rb(i,RB_SCEN_SPEC, i, strb.str()); break; case 1: - strb << i << " - " << (*current_terrain->specials[i].type).name(); + strb << i << " - " << current_terrain->specials[i].editor_hint(scenario); set_rb(i,RB_OUT_SPEC, i, strb.str()); break; case 2: - strb << i << " - " << (*town->specials[i].type).name(); + strb << i << " - " << town->specials[i].editor_hint(scenario); set_rb(i,RB_TOWN_SPEC, i, strb.str()); break; } diff --git a/src/universe/party.cpp b/src/universe/party.cpp index ba85429b5..7e3c23a44 100644 --- a/src/universe/party.cpp +++ b/src/universe/party.cpp @@ -274,14 +274,19 @@ void cParty::import_legacy(legacy::stored_items_list_type& old,short which_list) } void cParty::import_legacy(legacy::setup_save_type& old){ - for(int n = 0; n < 4; n++) + for(int n = 0; n < 4; n++){ + setup[n].resize(64, 64); for(int i = 0; i < 64; i++) for(int j = 0; j < 64; j++) setup[n][i][j] = old.setup[n][i][j]; + } } void cParty::cConvers::import_legacy(legacy::talk_save_type old, const cScenario& scenario){ - who_said = scenario.towns[old.personality / 10]->talking.people[old.personality % 10].title; + size_t town = old.personality / 10; + size_t npc = old.personality % 10; + if(town >= scenario.towns.size()) return; + who_said = scenario.towns[town]->talking.people[npc].title; in_town = scenario.towns[old.town_num]->name; int strnums[2] = {old.str1, old.str2}; std::string* strs[2] = {&the_str1, &the_str2}; diff --git a/src/universe/pc.cpp b/src/universe/pc.cpp index 3bd86faa7..5587976fa 100644 --- a/src/universe/pc.cpp +++ b/src/universe/pc.cpp @@ -846,7 +846,7 @@ const cInvenSlot cPlayer::has_class_equip(unsigned int item_class) const { cInvenSlot cPlayer::has_class(unsigned int item_class, bool require_charges) { return find_item_matching([item_class, require_charges](int, const cItem& item) { - return item.special_class == item_class && (!require_charges || item.charges > 0); + return item.special_class == item_class && (!require_charges || item.charges > 0 || item.max_charges == 0); }); } diff --git a/src/universe/universe.cpp b/src/universe/universe.cpp index 9ba324289..327df733f 100644 --- a/src/universe/universe.cpp +++ b/src/universe/universe.cpp @@ -116,6 +116,10 @@ const cTown& cCurTown::operator * () const { return *record(); } +int cCurTown::door_diff_adjust() { + return univ.scenario.has_feature_flag("door-town-difficulty") ? arena->difficulty : 0; +} + void cCurTown::place_preset_fields() { // Initialize barriers, etc. Note non-sfx gets forgotten if this is a town recently visited. fields.resize(record()->max_dim, record()->max_dim); diff --git a/src/universe/universe.hpp b/src/universe/universe.hpp index a40246766..17e9f8af7 100644 --- a/src/universe/universe.hpp +++ b/src/universe/universe.hpp @@ -52,6 +52,7 @@ class cCurTown { void import_legacy(unsigned char(& old_sfx)[64][64], unsigned char(& old_misc_i)[64][64]); void import_legacy(legacy::big_tr_type& old); + int door_diff_adjust(); cTown* operator -> (); cTown& operator * (); const cTown* operator -> () const;