diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml new file mode 100644 index 0000000..4062d84 --- /dev/null +++ b/.github/workflows/swift.yml @@ -0,0 +1,34 @@ +# This workflow will build a Swift project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift + +name: Swift + +on: + push: + branches: [ "development" ] + pull_request: + branches: [ "development" ] + +jobs: + build: + + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: swift build -v + - name: Run tests + run: swift test --enable-code-coverage + - name: Generate coverage report + run: | + xcrun llvm-cov export -format="lcov" \ + .build/debug/RolePlayingCorePackageTests.xctest/Contents/MacOS/RolePlayingCorePackageTests \ + -instr-profile .build/debug/codecov/default.profdata > coverage.lcov + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: ./coverage.lcov + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + verbose: true diff --git a/.gitignore b/.gitignore index 47042c2..8e1c0bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,14 @@ .DS_Store xcuserdata +DerivedData +.swiftpm/xcode/package.xcworkspace/ +.swiftpm/xcode/xcuserdata/ + +# Build artifacts +.build/ + +# Other common ignores +*.swp +*.bak +*.orig +*.lock \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 12e88bb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: swift -osx_image: xcode10 -script: -- set -o pipefail -- travis_retry xcodebuild -workspace RolePlayingCore.xcworkspace -scheme "RolePlayingCore" -destination "platform=iOS Simulator,name=iPhone 8" analyze | xcpretty -- travis_retry xcodebuild -workspace RolePlayingCore.xcworkspace -scheme "CharacterGenerator" -destination "platform=iOS Simulator,name=iPhone 8" -enableCodeCoverage NO build-for-testing test | xcpretty -- travis_retry xcodebuild -workspace RolePlayingCore.xcworkspace -scheme "RolePlayingCore" -destination "platform=iOS Simulator,name=iPhone 8" -enableCodeCoverage YES build-for-testing test | xcpretty -after_success: - - bash <(curl -s https://codecov.io/bash) -J 'RolePlayingCore' -cF ios -X xcodeplist diff --git a/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj b/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj deleted file mode 100644 index 6795738..0000000 --- a/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj +++ /dev/null @@ -1,668 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - B621A3B01F0C1C7400E55236 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B621A3AF1F0C1C7400E55236 /* Assets.xcassets */; }; - B621A3B31F0C1C7400E55236 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B621A3B11F0C1C7400E55236 /* LaunchScreen.storyboard */; }; - B621A3BE1F0C1C7500E55236 /* CharacterGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B621A3BD1F0C1C7500E55236 /* CharacterGeneratorTests.swift */; }; - B64F68E02EA6867B006D6C77 /* CharacterGeneratorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64F68DF2EA6867B006D6C77 /* CharacterGeneratorApp.swift */; }; - B64F68E22EA68686006D6C77 /* PlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64F68E12EA68686006D6C77 /* PlayerListView.swift */; }; - B64F68E42EA6868A006D6C77 /* PlayerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64F68E32EA6868A006D6C77 /* PlayerDetailView.swift */; }; - B6688B4F2EABCB11000A83DD /* ExperiencePoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6688B4E2EABCB11000A83DD /* ExperiencePoints.swift */; }; - B6A1AE471F0C441E008ADF08 /* Classes.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE431F0C4383008ADF08 /* Classes.json */; }; - B6A1AE481F0C4421008ADF08 /* Configuration.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE441F0C4383008ADF08 /* Configuration.json */; }; - B6A1AE491F0C4426008ADF08 /* Currencies.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE451F0C4383008ADF08 /* Currencies.json */; }; - B6A1AE4A1F0C4429008ADF08 /* Species.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE461F0C4383008ADF08 /* Species.json */; }; - B6A1AE4C1F0C5ABF008ADF08 /* SpeciesNames.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE4B1F0C5ABF008ADF08 /* SpeciesNames.json */; }; - B6A1AE501F0D6594008ADF08 /* CharacterSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A1AE4F1F0D6594008ADF08 /* CharacterSheet.swift */; }; - B6C87F431F83CD3E007CB209 /* CharacterGeneratorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C87F421F83CD3E007CB209 /* CharacterGeneratorUITests.swift */; }; - B6D1DB5520DD4A8100B402AC /* RolePlayingCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6D1DB5420DD4A8100B402AC /* RolePlayingCore.framework */; }; - B6D1DB5620DD4A8100B402AC /* RolePlayingCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B6D1DB5420DD4A8100B402AC /* RolePlayingCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - B621A3BA1F0C1C7500E55236 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = B621A39B1F0C1C7400E55236 /* Project object */; - proxyType = 1; - remoteGlobalIDString = B621A3A21F0C1C7400E55236; - remoteInfo = CharacterGenerator; - }; - B6C87F451F83CD3E007CB209 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = B621A39B1F0C1C7400E55236 /* Project object */; - proxyType = 1; - remoteGlobalIDString = B621A3A21F0C1C7400E55236; - remoteInfo = CharacterGenerator; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - B6ADA2A820C1D97F00012B7C /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - B6D1DB5620DD4A8100B402AC /* RolePlayingCore.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - B621A3A31F0C1C7400E55236 /* CharacterGenerator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CharacterGenerator.app; sourceTree = BUILT_PRODUCTS_DIR; }; - B621A3AF1F0C1C7400E55236 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - B621A3B21F0C1C7400E55236 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - B621A3B41F0C1C7400E55236 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B621A3B91F0C1C7500E55236 /* CharacterGeneratorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CharacterGeneratorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - B621A3BD1F0C1C7500E55236 /* CharacterGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterGeneratorTests.swift; sourceTree = ""; }; - B621A3BF1F0C1C7500E55236 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B621A3D61F0C1E3500E55236 /* GenericPlayer image license.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "GenericPlayer image license.txt"; sourceTree = ""; }; - B64F68DF2EA6867B006D6C77 /* CharacterGeneratorApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterGeneratorApp.swift; sourceTree = ""; }; - B64F68E12EA68686006D6C77 /* PlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerListView.swift; sourceTree = ""; }; - B64F68E32EA6868A006D6C77 /* PlayerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDetailView.swift; sourceTree = ""; }; - B6688B4E2EABCB11000A83DD /* ExperiencePoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperiencePoints.swift; sourceTree = ""; }; - B6A1AE431F0C4383008ADF08 /* Classes.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Classes.json; sourceTree = ""; }; - B6A1AE441F0C4383008ADF08 /* Configuration.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Configuration.json; sourceTree = ""; }; - B6A1AE451F0C4383008ADF08 /* Currencies.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Currencies.json; sourceTree = ""; }; - B6A1AE461F0C4383008ADF08 /* Species.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Species.json; sourceTree = ""; }; - B6A1AE4B1F0C5ABF008ADF08 /* SpeciesNames.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SpeciesNames.json; sourceTree = ""; }; - B6A1AE4F1F0D6594008ADF08 /* CharacterSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterSheet.swift; sourceTree = ""; }; - B6C87F401F83CD3E007CB209 /* CharacterGeneratorUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CharacterGeneratorUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - B6C87F421F83CD3E007CB209 /* CharacterGeneratorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterGeneratorUITests.swift; sourceTree = ""; }; - B6C87F441F83CD3E007CB209 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B6D1DB5420DD4A8100B402AC /* RolePlayingCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RolePlayingCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - B621A3A01F0C1C7400E55236 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - B6D1DB5520DD4A8100B402AC /* RolePlayingCore.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B621A3B61F0C1C7500E55236 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B6C87F3D1F83CD3E007CB209 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - B621A39A1F0C1C7400E55236 = { - isa = PBXGroup; - children = ( - B621A3A51F0C1C7400E55236 /* CharacterGenerator */, - B621A3BC1F0C1C7500E55236 /* CharacterGeneratorTests */, - B6C87F411F83CD3E007CB209 /* CharacterGeneratorUITests */, - B621A3A41F0C1C7400E55236 /* Products */, - B6D1DB5420DD4A8100B402AC /* RolePlayingCore.framework */, - B6A1AE3F1F0C3702008ADF08 /* Frameworks */, - ); - sourceTree = ""; - }; - B621A3A41F0C1C7400E55236 /* Products */ = { - isa = PBXGroup; - children = ( - B621A3A31F0C1C7400E55236 /* CharacterGenerator.app */, - B621A3B91F0C1C7500E55236 /* CharacterGeneratorTests.xctest */, - B6C87F401F83CD3E007CB209 /* CharacterGeneratorUITests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - B621A3A51F0C1C7400E55236 /* CharacterGenerator */ = { - isa = PBXGroup; - children = ( - B64F68DF2EA6867B006D6C77 /* CharacterGeneratorApp.swift */, - B6A1AE421F0C4383008ADF08 /* Configuration */, - B621A3D71F0C1EE300E55236 /* Player */, - B621A3AF1F0C1C7400E55236 /* Assets.xcassets */, - B621A3D61F0C1E3500E55236 /* GenericPlayer image license.txt */, - B621A3B11F0C1C7400E55236 /* LaunchScreen.storyboard */, - B621A3B41F0C1C7400E55236 /* Info.plist */, - ); - path = CharacterGenerator; - sourceTree = ""; - }; - B621A3BC1F0C1C7500E55236 /* CharacterGeneratorTests */ = { - isa = PBXGroup; - children = ( - B621A3BD1F0C1C7500E55236 /* CharacterGeneratorTests.swift */, - B621A3BF1F0C1C7500E55236 /* Info.plist */, - ); - path = CharacterGeneratorTests; - sourceTree = ""; - }; - B621A3D71F0C1EE300E55236 /* Player */ = { - isa = PBXGroup; - children = ( - B64F68E12EA68686006D6C77 /* PlayerListView.swift */, - B64F68E32EA6868A006D6C77 /* PlayerDetailView.swift */, - B6A1AE4F1F0D6594008ADF08 /* CharacterSheet.swift */, - B6688B4E2EABCB11000A83DD /* ExperiencePoints.swift */, - ); - path = Player; - sourceTree = ""; - }; - B6A1AE3F1F0C3702008ADF08 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; - B6A1AE421F0C4383008ADF08 /* Configuration */ = { - isa = PBXGroup; - children = ( - B6A1AE441F0C4383008ADF08 /* Configuration.json */, - B6A1AE431F0C4383008ADF08 /* Classes.json */, - B6A1AE451F0C4383008ADF08 /* Currencies.json */, - B6A1AE461F0C4383008ADF08 /* Species.json */, - B6A1AE4B1F0C5ABF008ADF08 /* SpeciesNames.json */, - ); - path = Configuration; - sourceTree = ""; - }; - B6C87F411F83CD3E007CB209 /* CharacterGeneratorUITests */ = { - isa = PBXGroup; - children = ( - B6C87F421F83CD3E007CB209 /* CharacterGeneratorUITests.swift */, - B6C87F441F83CD3E007CB209 /* Info.plist */, - ); - path = CharacterGeneratorUITests; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - B621A3A21F0C1C7400E55236 /* CharacterGenerator */ = { - isa = PBXNativeTarget; - buildConfigurationList = B621A3CD1F0C1C7500E55236 /* Build configuration list for PBXNativeTarget "CharacterGenerator" */; - buildPhases = ( - B621A39F1F0C1C7400E55236 /* Sources */, - B621A3A01F0C1C7400E55236 /* Frameworks */, - B621A3A11F0C1C7400E55236 /* Resources */, - B6ADA2A820C1D97F00012B7C /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = CharacterGenerator; - productName = CharacterGenerator; - productReference = B621A3A31F0C1C7400E55236 /* CharacterGenerator.app */; - productType = "com.apple.product-type.application"; - }; - B621A3B81F0C1C7500E55236 /* CharacterGeneratorTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = B621A3D01F0C1C7500E55236 /* Build configuration list for PBXNativeTarget "CharacterGeneratorTests" */; - buildPhases = ( - B621A3B51F0C1C7500E55236 /* Sources */, - B621A3B61F0C1C7500E55236 /* Frameworks */, - B621A3B71F0C1C7500E55236 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - B621A3BB1F0C1C7500E55236 /* PBXTargetDependency */, - ); - name = CharacterGeneratorTests; - productName = CharacterGeneratorTests; - productReference = B621A3B91F0C1C7500E55236 /* CharacterGeneratorTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - B6C87F3F1F83CD3E007CB209 /* CharacterGeneratorUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = B6C87F491F83CD3E007CB209 /* Build configuration list for PBXNativeTarget "CharacterGeneratorUITests" */; - buildPhases = ( - B6C87F3C1F83CD3E007CB209 /* Sources */, - B6C87F3D1F83CD3E007CB209 /* Frameworks */, - B6C87F3E1F83CD3E007CB209 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - B6C87F461F83CD3E007CB209 /* PBXTargetDependency */, - ); - name = CharacterGeneratorUITests; - productName = CharacterGeneratorUITests; - productReference = B6C87F401F83CD3E007CB209 /* CharacterGeneratorUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - B621A39B1F0C1C7400E55236 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0900; - LastUpgradeCheck = 1620; - ORGANIZATIONNAME = "Brian Arnold"; - TargetAttributes = { - B621A3A21F0C1C7400E55236 = { - CreatedOnToolsVersion = 9.0; - LastSwiftMigration = 1020; - }; - B621A3B81F0C1C7500E55236 = { - CreatedOnToolsVersion = 9.0; - LastSwiftMigration = 1020; - TestTargetID = B621A3A21F0C1C7400E55236; - }; - B6C87F3F1F83CD3E007CB209 = { - CreatedOnToolsVersion = 9.0; - LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; - TestTargetID = B621A3A21F0C1C7400E55236; - }; - }; - }; - buildConfigurationList = B621A39E1F0C1C7400E55236 /* Build configuration list for PBXProject "CharacterGenerator" */; - compatibilityVersion = "Xcode 8.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = B621A39A1F0C1C7400E55236; - productRefGroup = B621A3A41F0C1C7400E55236 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - B621A3A21F0C1C7400E55236 /* CharacterGenerator */, - B621A3B81F0C1C7500E55236 /* CharacterGeneratorTests */, - B6C87F3F1F83CD3E007CB209 /* CharacterGeneratorUITests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - B621A3A11F0C1C7400E55236 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B6A1AE481F0C4421008ADF08 /* Configuration.json in Resources */, - B6A1AE4A1F0C4429008ADF08 /* Species.json in Resources */, - B621A3B31F0C1C7400E55236 /* LaunchScreen.storyboard in Resources */, - B6A1AE491F0C4426008ADF08 /* Currencies.json in Resources */, - B621A3B01F0C1C7400E55236 /* Assets.xcassets in Resources */, - B6A1AE471F0C441E008ADF08 /* Classes.json in Resources */, - B6A1AE4C1F0C5ABF008ADF08 /* SpeciesNames.json in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B621A3B71F0C1C7500E55236 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B6C87F3E1F83CD3E007CB209 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - B621A39F1F0C1C7400E55236 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B64F68E42EA6868A006D6C77 /* PlayerDetailView.swift in Sources */, - B6A1AE501F0D6594008ADF08 /* CharacterSheet.swift in Sources */, - B64F68E22EA68686006D6C77 /* PlayerListView.swift in Sources */, - B64F68E02EA6867B006D6C77 /* CharacterGeneratorApp.swift in Sources */, - B6688B4F2EABCB11000A83DD /* ExperiencePoints.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B621A3B51F0C1C7500E55236 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B621A3BE1F0C1C7500E55236 /* CharacterGeneratorTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B6C87F3C1F83CD3E007CB209 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B6C87F431F83CD3E007CB209 /* CharacterGeneratorUITests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - B621A3BB1F0C1C7500E55236 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = B621A3A21F0C1C7400E55236 /* CharacterGenerator */; - targetProxy = B621A3BA1F0C1C7500E55236 /* PBXContainerItemProxy */; - }; - B6C87F461F83CD3E007CB209 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = B621A3A21F0C1C7400E55236 /* CharacterGenerator */; - targetProxy = B6C87F451F83CD3E007CB209 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - B621A3B11F0C1C7400E55236 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - B621A3B21F0C1C7400E55236 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - B621A3CB1F0C1C7500E55236 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 6.0; - }; - name = Debug; - }; - B621A3CC1F0C1C7500E55236 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 6.0; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - B621A3CE1F0C1C7500E55236 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = CharacterGenerator/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGenerator; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - B621A3CF1F0C1C7500E55236 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = CharacterGenerator/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGenerator; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - B621A3D11F0C1C7500E55236 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = CharacterGeneratorTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CharacterGenerator.app/CharacterGenerator"; - }; - name = Debug; - }; - B621A3D21F0C1C7500E55236 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = CharacterGeneratorTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CharacterGenerator.app/CharacterGenerator"; - }; - name = Release; - }; - B6C87F471F83CD3E007CB209 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = CharacterGeneratorUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = CharacterGenerator; - }; - name = Debug; - }; - B6C87F481F83CD3E007CB209 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = CharacterGeneratorUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = CharacterGenerator; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - B621A39E1F0C1C7400E55236 /* Build configuration list for PBXProject "CharacterGenerator" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B621A3CB1F0C1C7500E55236 /* Debug */, - B621A3CC1F0C1C7500E55236 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B621A3CD1F0C1C7500E55236 /* Build configuration list for PBXNativeTarget "CharacterGenerator" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B621A3CE1F0C1C7500E55236 /* Debug */, - B621A3CF1F0C1C7500E55236 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B621A3D01F0C1C7500E55236 /* Build configuration list for PBXNativeTarget "CharacterGeneratorTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B621A3D11F0C1C7500E55236 /* Debug */, - B621A3D21F0C1C7500E55236 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B6C87F491F83CD3E007CB209 /* Build configuration list for PBXNativeTarget "CharacterGeneratorUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B6C87F471F83CD3E007CB209 /* Debug */, - B6C87F481F83CD3E007CB209 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = B621A39B1F0C1C7400E55236 /* Project object */; -} diff --git a/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json b/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d8db8d6..0000000 --- a/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "83.5x83.5", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json b/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json deleted file mode 100644 index da4a164..0000000 --- a/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/Contents.json b/CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/Contents.json deleted file mode 100644 index 442af97..0000000 --- a/CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "sample-894-t-shirt@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/sample-894-t-shirt@2x.png b/CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/sample-894-t-shirt@2x.png deleted file mode 100644 index f2286ff..0000000 Binary files a/CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/sample-894-t-shirt@2x.png and /dev/null differ diff --git a/CharacterGenerator/CharacterGenerator/Base.lproj/LaunchScreen.storyboard b/CharacterGenerator/CharacterGenerator/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 6514e06..0000000 --- a/CharacterGenerator/CharacterGenerator/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Classes.json b/CharacterGenerator/CharacterGenerator/Configuration/Classes.json deleted file mode 100644 index 7e8262c..0000000 --- a/CharacterGenerator/CharacterGenerator/Configuration/Classes.json +++ /dev/null @@ -1,147 +0,0 @@ -{ - "experience points" : [ - 0, - 300, - 900, - 2700, - 6500, - 14000, - 23000, - 34000, - 48000, - 64000, - 85000, - 100000, - 120000, - 140000, - 165000, - 195000, - 225000, - 265000, - 305000, - 355000], - "classes": [ - { - "name": "Cleric", - "plural": "Clerics", - "hit dice": "d8", - "primary ability": ["Wisdom"], - "saving throws": ["Wisdom", "Charisma"], - "starting wealth": "5d4x10", - "armor": ["light", "medium", "shield"], - "weapons": ["simple"], - }, - { - "name": "Fighter", - "plural": "Fighters", - "hit dice": "d10", - "primary ability": ["Strength"], - "alternate primary ability": ["Dexterity"], - "saving throws": ["Strength", "Constitution"], - "starting wealth": "5d4x10", - "armor": ["all"], - "weapons": ["simple", "martial"], - }, - { - "name": "Rogue", - "plural": "Rogues", - "aliases": ["Thief"], - "hit dice": "d8", - "primary ability": ["Dexterity"], - "saving throws": ["Dexterity", "Intelligence"], - "starting wealth": "4d4x10", - "armor": ["light"], - "weapons": ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], - }, - { - "name": "Wizard", - "plural": "Wizards", - "hit dice": "d6", - "primary ability": ["Intelligence"], - "saving throws": ["Intelligence", "Wisdom"], - "starting wealth": "4d4x10", - "armor": [], - "weapons": ["dagger", "dart", "sling", "quarterstaff", "light crossbow"], - }, - { - "name": "Barbarian", - "plural": "Barbarians", - "hit dice": "d12", - "primary ability": ["Strength"], - "saving throws": ["Strength", "Constitution"], - "starting wealth": "2d4x10", - "armor": ["light", "medium", "shield"], - "weapons": ["simple", "martial"], - }, - { - "name": "Bard", - "plural": "Bards", - "hit dice": "d8", - "primary ability": ["Charisma"], - "saving throws": ["Dexterity", "Charisma"], - "starting wealth": "5d4x10", - "armor": ["light"], - "weapons": ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], - }, - { - "name": "Druid", - "plural": "Druids", - "hit dice": "d8", - "primary ability": ["Wisdom"], - "saving throws": ["Intelligence", "Wisdom"], - "starting wealth": "2d4x10", - "armor": ["light", "medium", "shields", "nonmetal"], - "weapons": ["club", "dagger", "dart", "javelin", "mace", "quarterstaff", "scimitar", "sickle", "sling", "spear"], - }, - { - "name": "Monk", - "plural": "Monks", - "hit dice": "d8", - "primary ability": ["Dexterity", "Wisdom"], - "saving throws": ["Strength", "Dexterity"], - "starting wealth": "5d4", - "armor": [], - "weapons": ["simple", "shortsword"], - }, - { - "name": "Paladin", - "plural": "Paladins", - "hit dice": "d10", - "primary ability": ["Strength", "Charisma"], - "saving throws": ["Wisdom", "Charisma"], - "starting wealth": "5d4x10", - "armor": ["all"], - "weapons": ["simple", "martial"], - }, - { - "name": "Ranger", - "plural": "Rangers", - "hit dice": "d10", - "primary ability": ["Dexterity", "Wisdom"], - "saving throws": ["Strength", "Dexterity"], - "starting wealth": "5d4x10", - "armor": ["light", "medium", "shield"], - "weapons": ["simple", "martial"], - }, - { - "name": "Sorcerer", - "plural": "Sorcerers", - "hit dice": "d6", - "primary ability": ["Charisma"], - "saving throws": ["Constitution", "Charisma"], - "starting wealth": "3d4x10", - "armor": [], - "weapons": ["dagger", "dart", "sling", "quarterstaff", "light crossbow"], - }, - { - "name": "Warlock", - "plural": "Warlocks", - "hit dice": "d8", - "primary ability": ["Charisma"], - "saving throws": ["Wisdom", "Charisma"], - "starting wealth": "4d4x10", - "armor": ["light"], - "weapons": ["simple"], - } - ] -} diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Species.json b/CharacterGenerator/CharacterGenerator/Configuration/Species.json deleted file mode 100644 index d8875a9..0000000 --- a/CharacterGenerator/CharacterGenerator/Configuration/Species.json +++ /dev/null @@ -1,220 +0,0 @@ -{ - "species": [ - { - "name": "Dwarf", - "plural": "Dwarves", - "ability scores": { "Constitution": 2 }, - "minimum age": 50, - "lifespan": 350, - "alignment": "Lawful Good", - "base height": 4, - "height modifier": "2d4", - "base weight": 130, - "weight modifier": "2d6", - "speed": 25, - "darkvision": 60, - "skills": ["dwarven resilience"], - "weapons" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], - "tools": ["smith's tools", "brewer's supplies", "mason's tools"], - "history": ["stonecunning"], - "languages": ["Common", "Dwarvish"], - "subspecies": [{ - "name": "Hill Dwarf", - "plural": "Hill Dwarves", - "ability scores": { "Wisdom": 2 }, - "hit point bonus": 1, - "base height": "3'5\"", - "base weight": 115, - }, - { - "name": "Mountain Dwarf", - "plural": "Mountain Dwarves", - "ability scores": { "Strength": 2 }, - "armor": ["light", "medium"], - }] - }, - { - "name": "Elf", - "plural": "Elves", - "ability scores": { "Dexterity": 2 }, - "minimum age": 100, - "lifespan": 750, - "alignment": "Chaotic Good", - "base height": "4'6\"", - "height modifier": "2d10", - "base weight": 100, - "weight modifier": "1d4", - "speed": 30, - "darkvision": 60, - "resilience": ["poison", "poison damage"], - "skills": ["keen senses", "fey ancestry", "trance"], - "languages": ["Common", "Elvish"], - "subspecies": [{ - "name": "High Elf", - "plural": "High Elves", - "ability scores": { "Intelligence": 1 }, - "base weight:": 90, - "weapons": ["longsword", "shortsword", "shortbow", "longbow"], - "extra languages": 1, - "spells": ["wizard cantrip"], - "hit point bonus": 1 - }, - { - "name": "Wood Elf", - "plural": "Wood Elves", - "ability scores": { "Wisdom": 1 }, - "weapons": ["longsword", "shortsword", "shortbow", "longbow"], - "skills": ["fleet of foot", "mask of the wild"] - }, - { - "name": "Dark Elf", - "plural": "Dark Elves", - "aliases": ["Drow"], - "ability scores": { "Charisma": 1 }, - "base height": "4'5\"", - "height modifier": "2d6", - "base weight": 75, - "weight modifier": "1d6", - "darkvision": 120, - "weapons": ["rapier", "shortsword", "hand crossbow"], - "spells": ["dancing lights", "faerie fire:3", "darkness:5"] - }] - }, - { - "name": "Halfling", - "plural": "Halflings", - "ability scores": { "Dexterity": 2 }, - "minimum age": 20, - "lifespan": 150, - "alignment": "Lawful Good", - "base height": "2'7\"", - "base weight": 35, - "height modifier": "2d4", - "speed": 25, - "skills": ["lucky", "brave", "halfling nimbleness"], - "weapons" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], - "tools": ["smith's tools", "brewer's supplies", "mason's tools"], - "history": ["stonecunning"], - "languages": ["Common", "Halfling"], - "subspecies": [{ - "name": "Lightfoot", - "plural": "Lightfoots", - "ability scores": { "Charisma": 1 }, - "skills": ["stealthy"] - }, - { - "name": "Stout", - "plural": "Stouts", - "ability scores": { "Constitution": 1 }, - "skills": ["stout resilience"] - }] - }, - { - "name": "Human", - "plural": "Humans", - "ability scores": { "Strength": 1, "Dexterity": 1, "Constitution": 1, "Intelligence": 1, "Wisdom": 1, "Charisma": 1 }, - "minimum age": 18, - "lifespan": 90, - "base height": "4'8\"", - "height modifier": "2d10", - "base weight": 110, - "weight modifier": "2d4", - "speed": 30, - "languages": ["Common"], - "extra languages": 1 - }, - { - "name": "Dragonborn", - "plural": "Dragonborn", - "ability scores": { "Strength": 2, "Charisma": 1 }, - "minimum age": 15, - "lifespan": 80, - "alignment": "Good", - "base height": "5'6\"", - "height modifier": "2d8", - "base weight": 175, - "weight modifier": "2d6", - "speed": 30, - "skills": ["draconic ancestry", "breath weapon", "damage"], - "languages": ["Common", "Draconic"] - }, - { - "name": "Gnome", - "plural": "Gnomes", - "ability scores": { "Intelligence": 2 }, - "minimum age": 40, - "lifespan": 425, - "alignment": "Good", - "base height": "2'11\"", - "height modifier": "2d4", - "base weight": 35, - "speed": 25, - "darkvision": 60, - "skills": ["gnome cunning"], - "languages": ["Common", "Gnomish"], - "subspecies": [{ - "name": "Forest Gnome", - "plural": "Forest Gnomes", - "ability scores": { "Dexterity": 1 }, - "skills": ["natural illusionist", "speak with small beasts"] - }, - { - "name": "Rock Gnome", - "plural": "Rock Gnomes", - "ability scores": { "Constitution": 1 }, - "skills": ["artificers lore", "tinker"] - }] - }, - { - "name": "Half-Elf", - "plural": "Half-Elves", - "ability scores": { "Strength": 1 }, - "ability score bonuses": 2, - "minimum age": 20, - "lifespan": 180, - "alignment": "Chaotic", - "base height": "4'9\"", - "height modifier": "2d8", - "base weight": 110, - "weight modifier": "2d4", - "speed": 30, - "darkvision": 60, - "skills": ["fey ancestry"], - "languages": ["Common", "Elvish"], - "extra languages": 1, - "skill versatility": 2 - }, - { - "name": "Half-Orc", - "plural": "Half-Orcs", - "ability scores": { "Strength": 2, "Constitution": 1 }, - "minimum age": 14, - "lifespan": 75, - "alignment": "Chaotic", - "base height": "4'10\"", - "height modifier": "2d10", - "base weight": 140, - "weight modifier": "2d6", - "speed": 30, - "darkvision": 60, - "skills": ["menacing", "relentless endurance", "savage attacks"], - "languages": ["Common", "Orcish"] - }, - { - "name": "Tiefling", - "plural": "Tieflings", - "ability scores": { "Intelligence": 1, "Charisma": 2 }, - "minimum age": 18, - "lifespan": 100, - "alignment": "Chaotic", - "base height": "4'9\"", - "height modifier": "2d8", - "base weight": 110, - "weight modifier": "2d4", - "speed": 30, - "darkvision": 60, - "skills": ["hellish resistance", "infernal legacy"], - "languages": ["Common", "Infernal"] - } - ] -} diff --git a/CharacterGenerator/CharacterGenerator/GenericPlayer image license.txt b/CharacterGenerator/CharacterGenerator/GenericPlayer image license.txt deleted file mode 100644 index 8863864..0000000 --- a/CharacterGenerator/CharacterGenerator/GenericPlayer image license.txt +++ /dev/null @@ -1,31 +0,0 @@ -FREE ICONS FROM GLYPHISH -Created by Joseph Wain, 2014 -Web: http://glyphish.com or http://penandthink.com -Twitter: @glyphish or @jpwain - -Using these free icons requires attribution -- see below. It's easy and fun! - -Created by Joseph Wain and downloaded from http://glyphish.com - -This work is licensed under the Creative Commons Attribution 3.0 United States License. To view a copy, visit http://creativecommons.org/licenses/by/3.0/us/ - -You are free to share and to remix it remix under the following conditions: - - * You must attribute the work in the manner specified by the author (SEE BELOW). - - * For any reuse or distribution, you must make clear to others the license terms of this work. - - * The above conditions can be waived if you get permission from the copyright holder - (send me an email to discuss). - -You're free to use these icons for commercial and non-commercial purposes, for yourself, your company and your clients, and to edit, remix and otherwise modify them, as long as clear attribution is provided. You may not sell or redistribute the icons themselves as icons. - -Additionally, you may not use them in a way that encourages downstream distribution -- no templates or skins or theme kits or similar uses, no app-building tools or similar. (The person using the theme or template might not know where the icons came from and thus wouldn't be following the license.) This specifically prohibits the use in any kind of "app builder" tool or platform, especially where the icons are provided as choices to people using the tool to create apps. - -ATTRIBUTION -- A note on your app's website, like "Icons from Glyphish" or similar, plus a link back to glyphish.com, is the preferred form of attribution. For questions about attribution, contact me via the Glyphish website. - -USE WITHOUT ATTRIBUTION -- If attribution is difficult, unworkable or undesireable for your application, get in touch to discuss other options. Or, get the full Glyphish sets, which do not require attribution. - -Enjoy! - --Joseph \ No newline at end of file diff --git a/CharacterGenerator/CharacterGenerator/Info.plist b/CharacterGenerator/CharacterGenerator/Info.plist deleted file mode 100644 index 4decacf..0000000 --- a/CharacterGenerator/CharacterGenerator/Info.plist +++ /dev/null @@ -1,53 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UIStatusBarTintParameters - - UINavigationBar - - Style - UIBarStyleDefault - Translucent - - - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift b/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift deleted file mode 100644 index 41d94e8..0000000 --- a/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// CharacterGeneratorTests.swift -// CharacterGeneratorTests -// -// Created by Brian Arnold on 7/4/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest -@testable import CharacterGenerator - -class CharacterGeneratorTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testCharacterGenerator() { - // TODO: there isn't much logic to test, since we use RolePlayingCore, which has its own tests. - } - -} diff --git a/CharacterGenerator/CharacterGeneratorTests/Info.plist b/CharacterGenerator/CharacterGeneratorTests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/CharacterGenerator/CharacterGeneratorTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift b/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift deleted file mode 100644 index e54248f..0000000 --- a/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// CharacterGeneratorUITests.swift -// CharacterGeneratorUITests -// -// Created by Brian Arnold on 7/4/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -class CharacterGeneratorUITests: XCTestCase { - - override func setUp() { - super.setUp() - - continueAfterFailure = false - - XCUIApplication().launch() - } - - override func tearDown() { - // TODO: reset state once we have more state - - super.tearDown() - } - - func testAddingAndShowingDetail() { - let app = XCUIApplication() - let playersNavigationBar = app.navigationBars["Players"] - let addButton = playersNavigationBar.buttons["Add"] - addButton.tap() - addButton.tap() - addButton.tap() - - let tablesQuery = app.tables - XCTAssertEqual(tablesQuery.cells.count, 3, "Expected 3 items") - tablesQuery.cells.element(boundBy: 1).tap() - app.navigationBars["Character Sheet"].buttons["Players"].tap() - - // TODO: more checks - } - -} diff --git a/CharacterGenerator/CharacterGeneratorUITests/Info.plist b/CharacterGenerator/CharacterGeneratorUITests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/CharacterGenerator/CharacterGeneratorUITests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj new file mode 100644 index 0000000..61242a4 --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj @@ -0,0 +1,655 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + B6E1ADAB2EB3BC5B00000512 /* RolePlayingCore in Frameworks */ = {isa = PBXBuildFile; productRef = B6E1ADAA2EB3BC5B00000512 /* RolePlayingCore */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + B6E1AD8D2EB3BC2D00000512 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B6E1AD752EB3BC2C00000512 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B6E1AD7C2EB3BC2C00000512; + remoteInfo = CharacterGenerator; + }; + B6E1AD972EB3BC2D00000512 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B6E1AD752EB3BC2C00000512 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B6E1AD7C2EB3BC2C00000512; + remoteInfo = CharacterGenerator; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + B6E1AD7D2EB3BC2C00000512 /* CharacterGenerator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CharacterGenerator.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B6E1AD8C2EB3BC2D00000512 /* CharacterGeneratorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CharacterGeneratorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B6E1AD962EB3BC2D00000512 /* CharacterGeneratorUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CharacterGeneratorUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + B6E1AD7F2EB3BC2C00000512 /* CharacterGenerator */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = CharacterGenerator; + sourceTree = ""; + }; + B6E1AD8F2EB3BC2D00000512 /* CharacterGeneratorTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = CharacterGeneratorTests; + sourceTree = ""; + }; + B6E1AD992EB3BC2D00000512 /* CharacterGeneratorUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = CharacterGeneratorUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + B6E1AD7A2EB3BC2C00000512 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B6E1ADAB2EB3BC5B00000512 /* RolePlayingCore in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6E1AD892EB3BC2D00000512 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6E1AD932EB3BC2D00000512 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B6E1AD742EB3BC2C00000512 = { + isa = PBXGroup; + children = ( + B6E1AD7F2EB3BC2C00000512 /* CharacterGenerator */, + B6E1AD8F2EB3BC2D00000512 /* CharacterGeneratorTests */, + B6E1AD992EB3BC2D00000512 /* CharacterGeneratorUITests */, + B6E1AD7E2EB3BC2C00000512 /* Products */, + ); + sourceTree = ""; + }; + B6E1AD7E2EB3BC2C00000512 /* Products */ = { + isa = PBXGroup; + children = ( + B6E1AD7D2EB3BC2C00000512 /* CharacterGenerator.app */, + B6E1AD8C2EB3BC2D00000512 /* CharacterGeneratorTests.xctest */, + B6E1AD962EB3BC2D00000512 /* CharacterGeneratorUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B6E1AD7C2EB3BC2C00000512 /* CharacterGenerator */ = { + isa = PBXNativeTarget; + buildConfigurationList = B6E1ADA02EB3BC2D00000512 /* Build configuration list for PBXNativeTarget "CharacterGenerator" */; + buildPhases = ( + B6E1AD792EB3BC2C00000512 /* Sources */, + B6E1AD7A2EB3BC2C00000512 /* Frameworks */, + B6E1AD7B2EB3BC2C00000512 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + B6E1AD7F2EB3BC2C00000512 /* CharacterGenerator */, + ); + name = CharacterGenerator; + packageProductDependencies = ( + B6E1ADAA2EB3BC5B00000512 /* RolePlayingCore */, + ); + productName = CharacterGenerator; + productReference = B6E1AD7D2EB3BC2C00000512 /* CharacterGenerator.app */; + productType = "com.apple.product-type.application"; + }; + B6E1AD8B2EB3BC2D00000512 /* CharacterGeneratorTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B6E1ADA32EB3BC2D00000512 /* Build configuration list for PBXNativeTarget "CharacterGeneratorTests" */; + buildPhases = ( + B6E1AD882EB3BC2D00000512 /* Sources */, + B6E1AD892EB3BC2D00000512 /* Frameworks */, + B6E1AD8A2EB3BC2D00000512 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + B6E1AD8E2EB3BC2D00000512 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + B6E1AD8F2EB3BC2D00000512 /* CharacterGeneratorTests */, + ); + name = CharacterGeneratorTests; + packageProductDependencies = ( + ); + productName = CharacterGeneratorTests; + productReference = B6E1AD8C2EB3BC2D00000512 /* CharacterGeneratorTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + B6E1AD952EB3BC2D00000512 /* CharacterGeneratorUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B6E1ADA62EB3BC2D00000512 /* Build configuration list for PBXNativeTarget "CharacterGeneratorUITests" */; + buildPhases = ( + B6E1AD922EB3BC2D00000512 /* Sources */, + B6E1AD932EB3BC2D00000512 /* Frameworks */, + B6E1AD942EB3BC2D00000512 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + B6E1AD982EB3BC2D00000512 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + B6E1AD992EB3BC2D00000512 /* CharacterGeneratorUITests */, + ); + name = CharacterGeneratorUITests; + packageProductDependencies = ( + ); + productName = CharacterGeneratorUITests; + productReference = B6E1AD962EB3BC2D00000512 /* CharacterGeneratorUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B6E1AD752EB3BC2C00000512 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2600; + LastUpgradeCheck = 2610; + TargetAttributes = { + B6E1AD7C2EB3BC2C00000512 = { + CreatedOnToolsVersion = 26.0.1; + }; + B6E1AD8B2EB3BC2D00000512 = { + CreatedOnToolsVersion = 26.0.1; + TestTargetID = B6E1AD7C2EB3BC2C00000512; + }; + B6E1AD952EB3BC2D00000512 = { + CreatedOnToolsVersion = 26.0.1; + TestTargetID = B6E1AD7C2EB3BC2C00000512; + }; + }; + }; + buildConfigurationList = B6E1AD782EB3BC2C00000512 /* Build configuration list for PBXProject "CharacterGenerator" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B6E1AD742EB3BC2C00000512; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + B6E1ADA92EB3BC5B00000512 /* XCLocalSwiftPackageReference "../../../../RolePlayingCore" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = B6E1AD7E2EB3BC2C00000512 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B6E1AD7C2EB3BC2C00000512 /* CharacterGenerator */, + B6E1AD8B2EB3BC2D00000512 /* CharacterGeneratorTests */, + B6E1AD952EB3BC2D00000512 /* CharacterGeneratorUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B6E1AD7B2EB3BC2C00000512 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6E1AD8A2EB3BC2D00000512 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6E1AD942EB3BC2D00000512 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B6E1AD792EB3BC2C00000512 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6E1AD882EB3BC2D00000512 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6E1AD922EB3BC2D00000512 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + B6E1AD8E2EB3BC2D00000512 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B6E1AD7C2EB3BC2C00000512 /* CharacterGenerator */; + targetProxy = B6E1AD8D2EB3BC2D00000512 /* PBXContainerItemProxy */; + }; + B6E1AD982EB3BC2D00000512 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B6E1AD7C2EB3BC2C00000512 /* CharacterGenerator */; + targetProxy = B6E1AD972EB3BC2D00000512 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + B6E1AD9E2EB3BC2D00000512 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = VHX6TEH729; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; + }; + name = Debug; + }; + B6E1AD9F2EB3BC2D00000512 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = VHX6TEH729; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 6.0; + }; + name = Release; + }; + B6E1ADA12EB3BC2D00000512 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = VHX6TEH729; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 15.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGenerator; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 26.0; + }; + name = Debug; + }; + B6E1ADA22EB3BC2D00000512 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = VHX6TEH729; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 15.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGenerator; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 26.0; + }; + name = Release; + }; + B6E1ADA42EB3BC2D00000512 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = VHX6TEH729; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CharacterGenerator.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CharacterGenerator"; + XROS_DEPLOYMENT_TARGET = 26.0; + }; + name = Debug; + }; + B6E1ADA52EB3BC2D00000512 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = VHX6TEH729; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CharacterGenerator.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CharacterGenerator"; + XROS_DEPLOYMENT_TARGET = 26.0; + }; + name = Release; + }; + B6E1ADA72EB3BC2D00000512 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = VHX6TEH729; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_TARGET_NAME = CharacterGenerator; + XROS_DEPLOYMENT_TARGET = 26.0; + }; + name = Debug; + }; + B6E1ADA82EB3BC2D00000512 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = VHX6TEH729; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_TARGET_NAME = CharacterGenerator; + XROS_DEPLOYMENT_TARGET = 26.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B6E1AD782EB3BC2C00000512 /* Build configuration list for PBXProject "CharacterGenerator" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B6E1AD9E2EB3BC2D00000512 /* Debug */, + B6E1AD9F2EB3BC2D00000512 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B6E1ADA02EB3BC2D00000512 /* Build configuration list for PBXNativeTarget "CharacterGenerator" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B6E1ADA12EB3BC2D00000512 /* Debug */, + B6E1ADA22EB3BC2D00000512 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B6E1ADA32EB3BC2D00000512 /* Build configuration list for PBXNativeTarget "CharacterGeneratorTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B6E1ADA42EB3BC2D00000512 /* Debug */, + B6E1ADA52EB3BC2D00000512 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B6E1ADA62EB3BC2D00000512 /* Build configuration list for PBXNativeTarget "CharacterGeneratorUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B6E1ADA72EB3BC2D00000512 /* Debug */, + B6E1ADA82EB3BC2D00000512 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + B6E1ADA92EB3BC5B00000512 /* XCLocalSwiftPackageReference "../../../../RolePlayingCore" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../../../RolePlayingCore; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + B6E1ADAA2EB3BC5B00000512 /* RolePlayingCore */ = { + isa = XCSwiftPackageProductDependency; + productName = RolePlayingCore; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = B6E1AD752EB3BC2C00000512 /* Project object */; +} diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..ffdfe15 --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,85 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CharacterGenerator/CharacterGenerator/CharacterGeneratorApp.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/CharacterGeneratorApp.swift similarity index 99% rename from CharacterGenerator/CharacterGenerator/CharacterGeneratorApp.swift rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/CharacterGeneratorApp.swift index 8a35e9e..947e9f2 100644 --- a/CharacterGenerator/CharacterGenerator/CharacterGeneratorApp.swift +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/CharacterGeneratorApp.swift @@ -8,6 +8,7 @@ import SwiftUI import RolePlayingCore +import Combine @main struct CharacterGeneratorApp: App { diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Backgrounds.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Backgrounds.json new file mode 100644 index 0000000..5589999 --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Backgrounds.json @@ -0,0 +1,132 @@ +{ + "backgrounds": [ + { + "name": "Acolyte", + "ability scores": ["Intelligence", "Wisdom", "Charisma"], + "feat": "Magic Initiate (Cleric)", + "skill proficiencies" : ["Insight", "Religion"], + "tool proficiency": "Calligrapher's Supplies", + "equipment": [["Calligrapher's Supplies", "Book (prayers)", "Holy Symbol", "Parchment (10 sheets)", "Robe", "8 GP"], ["50 GP"]] + }, + { + "name": "Artisan", + "ability scores": ["Strength", "Dexterity", "Intelligence"], + "feat": "Crafter", + "skill proficiencies" : ["Investigation", "Persuasion"], + "tool proficiency": "Artisan's Tools", + "equipment": [["Artisan's Tools", "2 Pouches", "Traveler's Clothes", "32 GP"], ["50 GP"]] + }, + { + "name": "Charlatan", + "ability scores": ["Dexterity", "Constitution", "Charisma"], + "feat": "Skilled", + "skill proficiencies" : ["Deception", "Sleight of Hand"], + "tool proficiency": "Forgery Kit", + "equipment": [["Forgery Kit", "Costume", "Fine Clothes", "15 GP"], ["50 GP"]] + }, + { + "name": "Criminal", + "ability scores": ["Dexterity", "Constitution", "Intelligence"], + "feat": "Alert", + "skill proficiencies" : ["Sleight of Hand", "Stealth"], + "tool proficiency": "Thieves' Tools", + "equipment": [["2 Daggers", "Thieves' Tools", "Crowbar", "2 Pouches", "Traveler's Clothes", "16 GP"], ["50 GP"]] + }, + { + "name": "Entertainer", + "ability scores": ["Strength", "Dexterity", "Charisma"], + "feat": "Musician", + "skill proficiencies" : ["Acrobatics", "Performance"], + "tool proficiency": "Musical Instrument", + "equipment": [["Musical Instrument", "2 Costumes", "Mirror", "Perfume", "Traveler's Clothes", "11 GP"], ["50 GP"]] + }, + { + "name": "Farmer", + "ability scores": ["Strength", "Constitution", "Wisdom"], + "feat": "Tough", + "skill proficiencies" : ["Animal Handling", "Nature"], + "tool proficiency": "Carpenter's Tools", + "equipment": [["Sickle", "Carpenter's Tools", "Healer's Kit", "Iron Pot", "Shovel", "Traveler's Clothes", "30 GP"], ["50 GP"]] + }, + { + "name": "Guard", + "ability scores": ["Strength", "Intelligence", "Wisdom"], + "feat": "Alert", + "skill proficiencies" : ["Athletics", "Perception"], + "tool proficiency": "Gaming Set", + "equipment": [["Spear", "Light Crossbow", "20 Bolts", "Gaming Set", "Hooded Lantern", "Manacles", "Quiver", "Traveler's Clothes", "12 GP"], ["50 GP"]] + }, + { + "name": "Guide", + "ability scores": ["Dexterity", "Constitution", "Wisdom"], + "feat": "Magic Initiate (Druid)", + "skill proficiencies" : ["Stealth", "Survival"], + "tool proficiency": "Cartographer's Tools", + "equipment": [["Shortbow", "20 Arrows", "Cartographer's Tools", "Bedroll", "Quiver", "Tent", "Traveler's Clothes", "3 GP"], ["50 GP"]] + }, + { + "name": "Hermit", + "ability scores": ["Constitution", "Wisdom", "Charisma"], + "feat": "Healer", + "skill proficiencies" : ["Medicine", "Religion"], + "tool proficiency": "Herbalism Kit", + "equipment": [["Quarterstaff", "Herbalism Kit", "Bedroll", "Book (philosophy)", "Lamp", "Oil (3 flasks)", "Traveler's Clothes", "16 GP"], ["50 GP"]] + }, + { + "name": "Merchant", + "ability scores": ["Constitution", "Intelligence", "Charisma"], + "feat": "Lucky", + "skill proficiencies" : ["Animal Handling", "Persuasion"], + "tool proficiency": "Navigator's Tools", + "equipment": [["Navigator's Tools", "2 Pouches", "Traveler's Clothes", "22 GP"], ["50 GP"]] + }, + { + "name": "Noble", + "ability scores": ["Strength", "Intelligence", "Charisma"], + "feat": "Skilled", + "skill proficiencies" : ["History", "Persuasion"], + "tool proficiency": "Gaming Set", + "equipment": [["Gaming Set", "Fine Clothes", "Perfume", "29 GP"], ["50 GP"]] + }, + { + "name": "Sage", + "ability scores": ["Constitution", "Intelligence", "Wisdom"], + "feat": "Magic Initiate (Wizard)", + "skill proficiencies" : ["Arcana", "History"], + "tool proficiency": "Calligrapher's Supplies", + "equipment": [["Quarterstaff", "Calligrapher's Supplies", "Book (history)", "Parchment (8 sheets)", "Robe", "8 GP"], ["50 GP"]] + }, + { + "name": "Sailor", + "ability scores": ["Strength", "Dexterity", "Wisdom"], + "feat": "Tavern Brawler", + "skill proficiencies" : ["Acrobatics", "Perception"], + "tool proficiency": "Navigator's Tools", + "equipment": [["Dagger", "Navigator's Tools", "Rope", "Traveler's Clothes", "20 GP"], ["50 GP"]] + }, + { + "name": "Scribe", + "ability scores": ["Dexterity", "Intelligence", "Wisdom"], + "feat": "Skilled", + "skill proficiencies" : ["Investigation", "Perception"], + "tool proficiency": "Calligrapher's Supplies", + "equipment": [["Calligrapher's Supplies", "Fine Clothes", "Lamp", "Oil (3 flasks)", "Parchment (12 sheets)", "23 GP"], ["50 GP"]] + }, + { + "name": "Soldier", + "ability scores": ["Strength", "Dexterity", "Constitution"], + "feat": "Savage Attacker", + "skill proficiencies" : ["Athletics", "Intimidation"], + "tool proficiency": "Gaming Set", + "equipment": [["Spear", "Shortbow", "20 Arrows", "Gaming Set", "Healer's Kit", "Quiver", "Traveler's Clothes", "14 GP"], ["50 GP"]] + }, + { + "name": "Wayfarer", + "ability scores": ["Dexterity", "Wisdom", "Charisma"], + "feat": "Lucky", + "skill proficiencies" : ["Insight", "Stealth"], + "tool proficiency": "Thieves' Tools", + "equipment": [["2 Daggers", "Thieves' Tools", "Gaming Set", "Bedroll", "2 Pouches", "Traveler's Clothes", "16 GP"], ["50 GP"]] + } + ] +} diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Classes.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Classes.json new file mode 100644 index 0000000..46a665e --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Classes.json @@ -0,0 +1,189 @@ +{ + "experience points" : [ + 0, + 300, + 900, + 2700, + 6500, + 14000, + 23000, + 34000, + 48000, + 64000, + 85000, + 100000, + 120000, + 140000, + 165000, + 195000, + 225000, + 265000, + 305000, + 355000], + "classes": [ + { + "name": "Cleric", + "plural": "Clerics", + "hit dice": "d8", + "primary ability": ["Wisdom"], + "saving throws": ["Wisdom", "Charisma"], + "starting skill count": 2, + "skill proficiencies": ["History", "Insight", "Medicine", "Persuasion", "Religion"], + "starting wealth": "5d4x10", + "starting equipment": [["Chain Shirt", "Shield", "Mace", "Holy Symbol", "Priest's Pack", "7 GP"], ["110 GP"]], + "armor training": ["light", "medium", "shield"], + "weapon proficiencies": ["simple"], + }, + { + "name": "Fighter", + "plural": "Fighters", + "hit dice": "d10", + "primary ability": ["Strength"], + "alternate primary ability": ["Dexterity"], + "saving throws": ["Strength", "Constitution"], + "starting skill count": 2, + "skill proficiencies": ["Acrobatics", "Animal Handling", "Athletics", "History", "Insight", "Intimidation", "Persuasion", "Perception", "Survival"], + "starting wealth": "5d4x10", + "starting equipment": [["Chain Mail", "Greatsword", "Flail", "8 Javelins", "Dungeoneer's Pack", "4 GP"], ["Studded Leather Armor", "Scimitar", "Shortsword", "Longbow", "20 Arrows", "Quiver", "Dungeoneer's Pack", "11 GP"], ["155 GP"]], + "armor training": ["light", "medium", "heavy", "shield"], + "weapon proficiencies": ["simple", "martial"], + }, + { + "name": "Rogue", + "plural": "Rogues", + "aliases": ["Thief"], + "hit dice": "d8", + "primary ability": ["Dexterity"], + "saving throws": ["Dexterity", "Intelligence"], + "starting skill count": 4, + "skill proficiencies": ["Acrobatics", "Athletics", "Deception", "Insight", "Intimidation", "Investigation", "Perception", "Persuasion", "Sleight of Hand", "Stealth"], + "tool proficiencies": ["Thieves' Tools"], + "starting wealth": "4d4x10", + "starting equipment": [["Leather Armor", "2 Daggers", "Shortsword", "Shortbow", "20 Arrows", "Quiver", "Thieves' Tools", "Burglar's Pack", "8 GP"], ["100 GP"]], + "armor training": ["light"], + "weapon proficiencies": ["simple", "martial (finesse or light)"], + }, + { + "name": "Wizard", + "plural": "Wizards", + "hit dice": "d6", + "primary ability": ["Intelligence"], + "saving throws": ["Intelligence", "Wisdom"], + "starting skill count": 2, + "skill proficiencies": ["Arcana", "History", "Insight", "Investigation", "Medicine", "Nature", "Religion"], + "starting wealth": "4d4x10", + "starting equipment": [["2 Daggers", "Arcane Focus (quarterstaff)", "Robe", "Spellbook", "Scholar's Pack", "5 GP"], ["55 GP"]], + "armor training": [], + "weapon proficiencies": ["simple"], + }, + { + "name": "Barbarian", + "plural": "Barbarians", + "hit dice": "d12", + "primary ability": ["Strength"], + "saving throws": ["Strength", "Constitution"], + "starting skill count": 2, + "skill proficiencies": ["Animal Handling", "Athletics", "Intimidation", "Nature", "Perception", "Survival"], + "starting wealth": "2d4x10", + "starting equipment": [["Greataxe", "4 Handaxes", "Explorer's Pack", "15 GP"], ["75 GP"]], + "armor training": ["light", "medium", "shield"], + "weapon proficiencies": ["simple", "martial"], + }, + { + "name": "Bard", + "plural": "Bards", + "hit dice": "d8", + "primary ability": ["Charisma"], + "saving throws": ["Dexterity", "Charisma"], + "starting skill count": 3, + "tools": 3, + "tool proficiencies": ["Musical Instrument"], + "starting wealth": "5d4x10", + "starting equipment": [["Leather Armor", "2 Daggers", "Any Tool", "Entertainer's Pack", "19 GP"], ["90 GP"]], + "armor training": ["light"], + "weapon proficiencies": ["simple"], + }, + { + "name": "Druid", + "plural": "Druids", + "hit dice": "d8", + "primary ability": ["Wisdom"], + "saving throws": ["Intelligence", "Wisdom"], + "starting skill count": 2, + "skill proficiencies": ["Arcana", "Animal Handling", "Insight", "Medicine", "Nature", "Perception", "Religion", "Survival"], + "tools": 1, + "tool proficiencies": ["Herbalism Kit"], + "starting wealth": "2d4x10", + "starting equipment": [["Leather Armor", "Shield", "Sickle", "Druidic Focus (Quarterstaff)", "Explorer's Pack", "Herbalism Kit", "9 GP"], ["50 GP"]], + "armor training": ["light", "shields"], + "weapon proficiencies": ["simple"], + }, + { + "name": "Monk", + "plural": "Monks", + "hit dice": "d8", + "primary ability": ["Dexterity", "Wisdom"], + "saving throws": ["Strength", "Dexterity"], + "starting skill count": 2, + "skill proficiencies": ["Acrobatics", "Athletics", "History", "Insight", "Religion", "Stealth"], + "tools": 1, + "tool proficiencies": ["Artisan's Tools", "Musical Instrument"], + "starting wealth": "5d4", + "starting equipment": [["Spear", "5 Daggers", "Any Tool", "Explorer's Pack", "11 GP"], ["50 GP"]], + "armor training": [], + "weapon proficiencies": ["simple", "martial (light)"], + }, + { + "name": "Paladin", + "plural": "Paladins", + "hit dice": "d10", + "primary ability": ["Strength", "Charisma"], + "saving throws": ["Wisdom", "Charisma"], + "starting skill count": 2, + "skill proficiencies": ["Athletics", "Insight", "Intimidation", "Medicine", "Persuasion", "Religion"], + "starting wealth": "5d4x10", + "starting equipment": [["Chain Mail", "Shield", "Longsword", "6 Javelins", "Holy Symbol", "Priest's Pack", "9 GP"], ["150 GP"]], + "armor training": ["light", "medium", "heavy", "shield"], + "weapon proficiencies": ["simple", "martial"], + }, + { + "name": "Ranger", + "plural": "Rangers", + "hit dice": "d10", + "primary ability": ["Dexterity", "Wisdom"], + "saving throws": ["Strength", "Dexterity"], + "starting skill count": 3, + "skill proficiencies": ["Animal Handling", "Athletics", "Insight", "Investigation", "Nature", "Perception", "Stealth", "Survival"], + "starting wealth": "5d4x10", + "starting equipment": [["Studded Leather Armor", "Scimitar", "Shortsword", "Longbow", "20 Arrows", "Quiver", "Druidic Focus (sprig of mistletoe)", "Explorer's Pack", "7 GP"], ["150 GP"]], + "armor training": ["light", "medium", "shield"], + "weapon proficiencies": ["simple", "martial"], + }, + { + "name": "Sorcerer", + "plural": "Sorcerers", + "hit dice": "d6", + "primary ability": ["Charisma"], + "saving throws": ["Constitution", "Charisma"], + "starting skill count": 2, + "skill proficiencies": ["Arcana", "Deception", "Insight", "Intimidation", "Persuasion", "Religion"], + "starting wealth": "3d4x10", + "starting equipment": [["Spear", "2 Daggers", "Arcane Focus (crystal)", "Dungeoneer's Pack", "28 GP"], ["50 GP"]], + "armor training": [], + "weapon proficiencies": ["simple"], + }, + { + "name": "Warlock", + "plural": "Warlocks", + "hit dice": "d8", + "primary ability": ["Charisma"], + "saving throws": ["Wisdom", "Charisma"], + "starting skill count": 2, + "skill proficiencies": ["Arcana", "Deception", "History", "Intimidation", "Investigation", "Nature", "Religion"], + "starting wealth": "4d4x10", + "starting equipment": [["Leather Armor", "Sickle", "2 Daggers", "Arcane Focus (orb)", "Book (occult lore)", "Scholar's Pack", "15 GP"], ["100 GP"]], + "armor training": ["light"], + "weapon proficiencies": ["simple"], + } + ] +} diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json similarity index 67% rename from CharacterGenerator/CharacterGenerator/Configuration/Configuration.json rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json index 68336eb..1252ee6 100644 --- a/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json @@ -1,5 +1,7 @@ { "currencies": ["Currencies"], + "skills": ["Skills"], + "backgrounds": ["Backgrounds"], "classes": ["Classes"], "species": ["Species"], "species names": "SpeciesNames" diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Currencies.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Currencies.json similarity index 100% rename from CharacterGenerator/CharacterGenerator/Configuration/Currencies.json rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Currencies.json diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Skills.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Skills.json new file mode 100644 index 0000000..a1441e2 --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Skills.json @@ -0,0 +1,76 @@ +{ + "skills": [ + { + "name": "Acrobatics", + "ability": "Dexterity" + }, + { + "name": "Animal Handling", + "ability": "Wisdom" + }, + { + "name": "Arcana", + "ability": "Intelligence" + }, + { + "name": "Athletics", + "ability": "Strength" + }, + { + "name": "Deception", + "ability": "Charisma" + }, + { + "name": "History", + "ability": "Intelligence" + }, + { + "name": "Insight", + "ability": "Wisdom" + }, + { + "name": "Intimidation", + "ability": "Charisma" + }, + { + "name": "Investigation", + "ability": "Intelligence" + }, + { + "name": "Medicine", + "ability": "Wisdom" + }, + { + "name": "Nature", + "ability": "Intelligence" + }, + { + "name": "Perception", + "ability": "Wisdom" + }, + { + "name": "Performance", + "ability": "Charisma" + }, + { + "name": "Persuasion", + "ability": "Charisma" + }, + { + "name": "Religion", + "ability": "Intelligence" + }, + { + "name": "Sleight of Hand", + "ability": "Dexterity" + }, + { + "name": "Stealth", + "ability": "Dexterity" + }, + { + "name": "Survival", + "ability": "Wisdom" + } + ] +} diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json new file mode 100644 index 0000000..e717b81 --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json @@ -0,0 +1,138 @@ +{ + "creature types": [ + { "name": "Aberration" }, + { "name": "Beast" }, + { "name": "Celestial" }, + { "name": "Construct" }, + { "name": "Dragon" }, + { "name": "Elemental" }, + { "name": "Fey" }, + { "name": "Fiend" }, + { "name": "Giant" }, + { "name": "Humanoid", "is default": true }, + { "name": "Monstrosity" }, + { "name": "Ooze" }, + { "name": "Plant" }, + { "name": "Undead" } + ], + "species": [ + { + "name": "Aasimar", + "plural": "Aasimar", + "lifespan": 160, + "base sizes": ["4-7 ft", "2-4 ft"], + "speed": 30, + "darkvision": 60 + }, + { + "name": "Dragonborn", + "plural": "Dragonborn", + "lifespan": 80, + "base sizes": ["5-7 ft"], + "speed": 30, + "darkvision": 60, + }, + { + "name": "Dwarf", + "plural": "Dwarves", + "lifespan": 350, + "base sizes": ["4-5 ft"], + "speed": 30, + "darkvision": 120, + "subspecies": [{ + "name": "Hill Dwarf", + "plural": "Hill Dwarves", + }, + { + "name": "Mountain Dwarf", + "plural": "Mountain Dwarves", + }] + }, + { + "name": "Elf", + "plural": "Elves", + "lifespan": 750, + "base sizes": ["5-6 ft"], + "speed": 30, + "darkvision": 60, + "subspecies": [{ + "name": "High Elf", + "plural": "High Elves" + }, + { + "name": "Wood Elf", + "plural": "Wood Elves" + }, + { + "name": "Drow", + "plural": "Drow", + "aliases": ["Dark Elf"], + "darkvision": 120 + }] + }, + { + "name": "Gnome", + "plural": "Gnomes", + "lifespan": 425, + "base sizes": ["3-4 ft"], + "speed": 30, + "darkvision": 60, + "subspecies": [{ + "name": "Forest Gnome", + "plural": "Forest Gnomes", + }, + { + "name": "Rock Gnome", + "plural": "Rock Gnomes", + }] + }, + { + "name": "Goliath", + "plural": "Goliaths", + "lifespan": 80, + "base sizes": ["7-8 ft"], + "speed": 35 + }, + { + "name": "Halfling", + "plural": "Halflings", + "lifespan": 150, + "base sizes": ["2-3 ft"], + "speed": 30, + "subspecies": [{ + "name": "Lightfoot", + "plural": "Lightfoots", + "aliases": ["Tallfellow"] + }, + { + "name": "Stout", + "plural": "Stouts", + "aliases": ["Strongheart"] + }] + + }, + { + "name": "Human", + "plural": "Humans", + "lifespan": 80, + "base sizes": ["4-7 ft", "2-4 ft"], + "speed": 30 + }, + { + "name": "Orc", + "plural": "Orcs", + "lifespan": 80, + "base sizes": ["6-7 ft"], + "speed": 30, + "darkvision": 120 + }, + { + "name": "Tiefling", + "plural": "Tieflings", + "lifespan": 80, + "base sizes": ["4-7 ft", "3-4 ft"], + "speed": 30, + "darkvision": 60 + } + ] +} diff --git a/CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json similarity index 80% rename from CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json index 664fd79..d4816c1 100644 --- a/CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json @@ -1,5 +1,11 @@ { "names": { + "Aasimar": { + "family type": "Family", + "Male": ["Aliban", "Mykiel", "Tadriel", "Valandras", "Diero", "Evendur", "Hunin", "Taman", "Zasheir", "Azrael", "Castiel", "Metatron", "Remiel", "Sachiel"], + "Female": ["Galladia", "Myllandra", "Seraphina", "Arveene", "Nephis", "Reani", "Selise", "Yasha", "Zora", "Jhessail", "Ananiel", "Jophiel", "Sariel", "Zuriel"], + "family": ["Amolune", "Solvaar", "Vaerthine", "Brightsun", "Moontide", "Whisperwind"] + }, "Dwarf": { "family type": "Clan", "Male": ["Adrik", "Alberich", "Baern", "Barendd", "Brottor", "Flint", "Dain", "Darrak", "Delg", "Eberk", "Einkil", "Fargrim", "Flint", "Gardain", "Harbek", "Kildrak", "Morgran", "Orsik", "Oskar", "Rangrim", "Rurik", "Talkinn", "Thoradin", "Thorin", "Tordek", "Traubon", "Travok", "Ulfgar", "Veit", "Vondal"], @@ -33,6 +39,13 @@ "family": ["Clethtinthiallor", "Daardendrian", "Delmirev", "Drachedandion", "Fenkenkabradon", "Kepeshkmolik", "Kerrhylon", "Kimbatuul", "Linxakasendalor", "Myastan", "Nemmonis", "Norixius", "Ophinshtalajiir", "Prexijandilin", "Shestendeliath", "Turnuroth", "Verthisathurgiesh", "Yarjerit"], "child": ["Climber", "Earbender", "Leaper", "Pious", "Shieldbiter", "Zealous"] }, + "Goliath": { + "family type": "Clan", + "Male": ["Aukan", "Eglath", "Gae-Al", "Gauthak", "Ilikan", "Keothi", "Kuori", "Lo-Kag", "Manneo", "Maveith", "Nalla", "Orilo", "Paavu", "Pethani", "Thalai", "Thotham", "Uthal", "Vaunea", "Vimak"], + "Female": ["Aukan", "Eglath", "Gae-Al", "Gauthak", "Ilikan", "Keothi", "Kuori", "Lo-Kag", "Manneo", "Maveith", "Nalla", "Orilo", "Paavu", "Pethani", "Thalai", "Thotham", "Uthal", "Vaunea", "Vimak"], + "family": ["Anakalathai", "Elanithino", "Gathakanathi", "Kalagiano", "Katho-Olavi", "Kolae-Gileana", "Ogolakanu", "Thuliaga", "Thunukalathi", "Vaimei-Laga"], + "nickname": ["Bearkiller", "Dawncaller", "Fearless", "Flintfinder", "Horncarver", "Keeneye", "Lonehunter", "Longleaper", "Rootsmasher", "Skywatcher", "Steadyhand", "Threadtwister", "Twice-Orphaned", "Twistedlimb", "Wordpainter"] + }, "Gnome": { "family type": "Clan", "Male": ["Alston", "Alvyn", "Boddynock", "Brocc", "Burgell", "Dimble", "Eldon", "Erky", "Fonkin", "Frug", "Gerbo", "Gimble", "Glim", "Jebeddo", "Kellen", "Namfoodle", "Orryn", "Roondar", "Seebo", "Sindri", "Warryn", "Wrenn", "Zook"], @@ -40,12 +53,6 @@ "family": ["Beren", "Daergel", "Folkor", "Garrick", "Nackle", "Murning", "Ningel", "Raulnor", "Scheppen", "Timbers", "Turen"], "nickname": ["Aleslosh", "Ashhearth", "Badger", "Cloak", "Doublelock", "Filchbatter", "Fnipper", "Ku", "Nim", "Oneshoe", "Pock", "Sparklegem", "Stumbleduck"] }, - "Half-Elf": { - "aliases": ["Elf", "Human"] - }, - "Half-Orc": { - "aliases": ["Orc", "Human"] - }, "Orc": { "family type": "", "Male": ["Dench", "Feng", "Gell", "Henk", "Holg", "Imsh", "Keth", "Krusk", "Mhurren", "Ront", "Shump", "Thokk"], diff --git a/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift similarity index 84% rename from CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift index 9f14332..419e2d9 100644 --- a/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift @@ -26,33 +26,36 @@ class CharacterSheet { // Mapping between sections/items and key paths to properties. var keys: [[PartialKeyPath]] = [ [\.experiencePoints], - [\.speciesName, \.className], + [\.backgroundName, \.speciesName, \.className], [\.abilities], - [\.initiative, \.speed, \.size], + [\.skills], + [\.initiative, \.speed], [\.armorClass, \.proficiencyBonus, \.passivePerception], [\.maximumHitPoints, \.hitDice], - [\.height, \.weight], + [\.height, \.size], [\.money] ] // Mapping of properties to label keys. var labelKeys: [[String]] = [ ["Experience Points"], - ["Species", "Class", "Subclass"], + ["Background", "Species", "Class", "Subclass"], ["Abilities"], - ["Initiative", "Speed", "Size"], + ["Skills"], + ["Initiative", "Speed"], ["Armor Class", "Proficiency Bonus", "Passive Perception"], ["Hit Points", "Hit Dice"], - ["Height", "Weight"], + ["Height", "Size"], ["Money"] ] // Mapping of properties to view types. var cellIdentifiers: [[String]] = [ ["experiencePoints"], - ["labeledText", "labeledText"], + ["labeledText", "labeledText", "labeledText"], ["abilities"], - ["labeledNumber", "labeledNumber", "labeledText"], + ["labeledText"], + ["labeledNumber", "labeledNumber"], ["labeledNumber", "labeledNumber", "labeledNumber"], ["labeledNumber", "labeledText"], ["labeledText", "labeledText"], @@ -69,6 +72,7 @@ class CharacterSheet { var experiencePoints: String { "\(player.experiencePoints)" } var level: String { "\(player.level)" } + var backgroundName: String { player.backgroundName } var className: String { player.className } var speciesName: String { player.speciesName } var alignment: String { @@ -79,6 +83,9 @@ class CharacterSheet { } } var abilities: AbilityScores { player.abilities } + var skills: String { + player.skillProficiencies.skillNames.joined(separator: ", ") + } var initiative: String { player.initiativeModifier.displayModifier } var armorClass: String { "\(player.armorClass)" } var proficiencyBonus: String { player.proficiencyBonus.displayModifier } @@ -88,7 +95,6 @@ class CharacterSheet { var money: String { "\(player.money)" } var gender: String { player.gender.map(\.rawValue) ?? "Androgynous" } var height: String { player.height.displayString } - var weight: String { player.weight.displayString } var speed: String { let value = player.speed let distance = Measurement(value: Double(value), unit: UnitLength.feet) diff --git a/CharacterGenerator/CharacterGenerator/Player/ExperiencePoints.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/ExperiencePoints.swift similarity index 100% rename from CharacterGenerator/CharacterGenerator/Player/ExperiencePoints.swift rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/ExperiencePoints.swift diff --git a/CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift similarity index 91% rename from CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift index e3f825e..ff5a553 100644 --- a/CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift @@ -24,12 +24,11 @@ struct PlayerDetailView: View { ForEach(0.. some View { - let cellIdentifier = characterSheet.cellIdentifiers[indexPath.section][indexPath.item] - let keys = characterSheet.keys[indexPath.section][indexPath.item] - let label = characterSheet.labelKeys[indexPath.section][indexPath.item] + private func traitView(for section: Int, item: Int) -> some View { + let cellIdentifier = characterSheet.cellIdentifiers[section][item] + let keys = characterSheet.keys[section][item] + let label = characterSheet.labelKeys[section][item] switch cellIdentifier { case "labeledText": @@ -198,7 +197,7 @@ struct AbilityItemView: View { } .padding(8) .frame(maxWidth: .infinity) - .background(Color(.tertiarySystemBackground)) + .background(.background.tertiary) .cornerRadius(8) } } diff --git a/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift similarity index 93% rename from CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift index a2c1147..a90037a 100644 --- a/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift @@ -32,11 +32,11 @@ struct PlayerListView: View { } .navigationTitle("Characters") .toolbar { - ToolbarItem(placement: .navigationBarLeading) { + ToolbarItem(placement: .topBarLeading) { EditButton() } - ToolbarItem(placement: .navigationBarTrailing) { + ToolbarItem(placement: .topBarTrailing) { Button { appState.addNewCharacter() if let newPlayer = appState.players[0] { @@ -56,7 +56,7 @@ struct PlayerRowView: View { var body: some View { HStack { - Image("GenericPlayer") + Image(systemName: "tshirt") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift new file mode 100644 index 0000000..b41ef9a --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift @@ -0,0 +1,16 @@ +// +// CharacterGeneratorTests.swift +// CharacterGeneratorTests +// +// Created by Brian Arnold on 10/30/25. +// + +import Testing + +struct CharacterGeneratorTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift new file mode 100644 index 0000000..26d28d9 --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift @@ -0,0 +1,41 @@ +// +// CharacterGeneratorUITests.swift +// CharacterGeneratorUITests +// +// Created by Brian Arnold on 10/30/25. +// + +import XCTest + +final class CharacterGeneratorUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } +} diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITestsLaunchTests.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITestsLaunchTests.swift new file mode 100644 index 0000000..40c580c --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// CharacterGeneratorUITestsLaunchTests.swift +// CharacterGeneratorUITests +// +// Created by Brian Arnold on 10/30/25. +// + +import XCTest + +final class CharacterGeneratorUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/RolePlayingCore.xcworkspace/contents.xcworkspacedata b/Examples/CharacterGenerator/Examples.xcworkspace/contents.xcworkspacedata similarity index 66% rename from RolePlayingCore.xcworkspace/contents.xcworkspacedata rename to Examples/CharacterGenerator/Examples.xcworkspace/contents.xcworkspacedata index 90c313c..e7f6926 100644 --- a/RolePlayingCore.xcworkspace/contents.xcworkspacedata +++ b/Examples/CharacterGenerator/Examples.xcworkspace/contents.xcworkspacedata @@ -4,7 +4,4 @@ - - diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..c68b2a1 --- /dev/null +++ b/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "RolePlayingCore", + platforms: [ + .iOS(.v18), + .macOS(.v15) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "RolePlayingCore", + targets: ["RolePlayingCore"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "RolePlayingCore", + path: "Sources/RolePlayingCore"), + .testTarget( + name: "RolePlayingCoreTests", + dependencies: ["RolePlayingCore"], + path: "Tests/RolePlayingCoreTests", + resources: [ + .process("TestResources") + ]), + ] +) diff --git a/README.md b/README.md index 2501330..66b6d57 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,21 @@ -# RolePlayingCore [![Build Status](https://travis-ci.org/mrlegowatch/RolePlayingCore.svg?branch=master)](https://travis-ci.org/mrlegowatch/RolePlayingCore) [![codecov.io](https://codecov.io/gh/mrlegowatch/RolePlayingCore/branch/master/graphs/badge.svg)](https://codecov.io/gh/mrlegowatch/RolePlayingCore/branch/master) +# RolePlayingCore ![Build Status](https://github.com/mrlegowatch/RolePlayingCore/workflows/Swift/badge.svg) +![Code Coverage](https://codecov.io/gh/mrlegowatch/RolePlayingCore/branch/development/graph/badge.svg) +![Swift Version](https://img.shields.io/badge/Swift-6.0-orange.svg) +![Platform](https://img.shields.io/badge/platform-iOS%20%7C%20macOS%20%7C%20Linux-lightgrey.svg) +[![License](https://img.shields.io/github/license/mrlegowatch/RolePlayingCore)](LICENSE) This framework provides reusable role playing game core logic in the Swift language. It is a work-in-progress. Capabilities will be provided incrementally over time. -The short-term goal for this project is to provide core logic for implementing a role playing game on macOS, iOS and Linux. The architecture is intended to be flexible enough to support Open Game Content in addition to similar kinds of games, and nimble enough to minimize upstream dependencies. The iOS platform in framework library format is being targeted first because this provides the most restrictive environment for implementing a library intended for reuse. The longer-term goal is to leverage this as a framework or library for implementing role playing games and utilities on the desktop and web. +The short-term goal for this project is to provide core logic for implementing a role playing game on macOS, iOS, and Linux. The architecture is intended to be flexible enough to support Open Game Content in addition to similar kinds of games, and nimble enough to minimize upstream dependencies. -## Requirements - -Xcode 26 or Swift 5 are required. +The library is built as a generic Swift Package, and the Example character generator app uses the iOS SDK with SwiftUI. The longer-term goal is to leverage this as a framework or library for implementing role playing games and utilities on the desktop and web. ## Organization The current organizational groupings include: -* **Common**: common utilities such as height and weight, and a runtime error enum -* **Currency**: currency types, conversion and parsing +* **Common**: common utilities such as height and weight, and a runtime error type +* **Currency**: currency types, conversion, and parsing * **Dice**: dice types and parsing * **Player**: the player, species, classes, and related types diff --git a/RolePlayingCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/RolePlayingCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/RolePlayingCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj b/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj deleted file mode 100644 index c49d70b..0000000 --- a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj +++ /dev/null @@ -1,778 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - B62055F81E19DD23002494AB /* RolePlayingCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B62055EE1E19DD23002494AB /* RolePlayingCore.framework */; }; - B62055FD1E19DD23002494AB /* RolePlayingCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62055FC1E19DD23002494AB /* RolePlayingCoreTests.swift */; }; - B62055FF1E19DD23002494AB /* RolePlayingCore.h in Headers */ = {isa = PBXBuildFile; fileRef = B62055F11E19DD23002494AB /* RolePlayingCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; - B620560A1E19DDC0002494AB /* DiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62056091E19DDC0002494AB /* DiceTests.swift */; }; - B620560E1E19DDD0002494AB /* Dice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B620560B1E19DDD0002494AB /* Dice.swift */; }; - B620560F1E19DDD0002494AB /* DiceParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B620560C1E19DDD0002494AB /* DiceParser.swift */; }; - B621A3981F0C052D00E55236 /* NameGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B621A3971F0C052D00E55236 /* NameGeneratorTests.swift */; }; - B621A3991F0C06A700E55236 /* NameGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B621A3961F0C020000E55236 /* NameGenerator.swift */; }; - B62D89C41F09A3870095D587 /* DiceParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62D89C31F09A3870095D587 /* DiceParserTests.swift */; }; - B64C369621756BC300C4F6BE /* DiceRoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C369521756BC300C4F6BE /* DiceRoll.swift */; }; - B6688B512EACF5B7000A83DD /* Initiative.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6688B502EACF5AE000A83DD /* Initiative.swift */; }; - B66AF8FC1EAE87A800C15F8E /* TestClasses.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8F41EAE87A800C15F8E /* TestClasses.json */; }; - B66AF8FD1EAE87A800C15F8E /* TestConfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8F51EAE87A800C15F8E /* TestConfiguration.json */; }; - B66AF8FE1EAE87A800C15F8E /* TestCurrencies.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8F61EAE87A800C15F8E /* TestCurrencies.json */; }; - B66AF8FF1EAE87A800C15F8E /* TestMoreClasses.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8F71EAE87A800C15F8E /* TestMoreClasses.json */; }; - B66AF9001EAE87A800C15F8E /* TestMoreSpecies.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8F81EAE87A800C15F8E /* TestMoreSpecies.json */; }; - B66AF9011EAE87A800C15F8E /* TestNames.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8F91EAE87A800C15F8E /* TestNames.json */; }; - B66AF9021EAE87A800C15F8E /* TestPlayers.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8FA1EAE87A800C15F8E /* TestPlayers.json */; }; - B66AF9031EAE87A800C15F8E /* TestSpecies.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8FB1EAE87A800C15F8E /* TestSpecies.json */; }; - B66AF9051EAE88C300C15F8E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B66AF9041EAE88C300C15F8E /* Configuration.swift */; }; - B66AF9071EAE88FF00C15F8E /* ConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B66AF9061EAE88FF00C15F8E /* ConfigurationTests.swift */; }; - B66AF9091EAE8B8100C15F8E /* MissingClassPlayers.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF9081EAE8B8000C15F8E /* MissingClassPlayers.json */; }; - B66AF90B1EAE8BF100C15F8E /* MissingSpeciesPlayers.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF90A1EAE8BF100C15F8E /* MissingSpeciesPlayers.json */; }; - B67450E41F118A8B0061FD6F /* SpeciesNamesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67450E31F118A8B0061FD6F /* SpeciesNamesTests.swift */; }; - B681B7161EAAC8E2001DE78B /* SimpleDice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B681B7151EAAC8E2001DE78B /* SimpleDice.swift */; }; - B681B7181EAAC8EE001DE78B /* DiceModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B681B7171EAAC8EE001DE78B /* DiceModifier.swift */; }; - B681B71A1EAAC8FB001DE78B /* CompoundDice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B681B7191EAAC8FB001DE78B /* CompoundDice.swift */; }; - B681B71C1EAAC904001DE78B /* DroppingDice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B681B71B1EAAC904001DE78B /* DroppingDice.swift */; }; - B681B71E1EAACA9B001DE78B /* Die.swift in Sources */ = {isa = PBXBuildFile; fileRef = B681B71D1EAACA9B001DE78B /* Die.swift */; }; - B698515E1F1171EC006A537A /* SpeciesNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = B698515D1F1171EC006A537A /* SpeciesNames.swift */; }; - B69851621F11850A006A537A /* TestSpeciesNames.json in Resources */ = {isa = PBXBuildFile; fileRef = B69851611F11850A006A537A /* TestSpeciesNames.json */; }; - B69F84691E58B8F700A4D2B0 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69F84681E58B8F700A4D2B0 /* Player.swift */; }; - B69F846B1E58D33900A4D2B0 /* PlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69F846A1E58D33900A4D2B0 /* PlayerTests.swift */; }; - B69F846D1E58D66900A4D2B0 /* Players.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69F846C1E58D66900A4D2B0 /* Players.swift */; }; - B69F846F1E59155A00A4D2B0 /* PlayersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69F846E1E59155A00A4D2B0 /* PlayersTests.swift */; }; - B69F84731E591DE400A4D2B0 /* InvalidClassPlayers.json in Resources */ = {isa = PBXBuildFile; fileRef = B69F84721E591DE400A4D2B0 /* InvalidClassPlayers.json */; }; - B69F84751E591E9800A4D2B0 /* InvalidSpeciesPlayers.json in Resources */ = {isa = PBXBuildFile; fileRef = B69F84741E591E9800A4D2B0 /* InvalidSpeciesPlayers.json */; }; - B6A1AE591F0E4C59008ADF08 /* InvalidConfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE581F0E4C59008ADF08 /* InvalidConfiguration.json */; }; - B6A29EB51EFE9B9F00DAB40C /* Currencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A29EB41EFE9B9F00DAB40C /* Currencies.swift */; }; - B6CF53901E51DA1300CADD9F /* ClassTraits.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF538F1E51DA1300CADD9F /* ClassTraits.swift */; }; - B6CF53921E51DEDD00CADD9F /* ClassTraitsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53911E51DEDD00CADD9F /* ClassTraitsTests.swift */; }; - B6CF53CE1E54DF4500CADD9F /* JSONFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53CD1E54DF4500CADD9F /* JSONFile.swift */; }; - B6CF53D01E54E2E200CADD9F /* Classes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53CF1E54E2E200CADD9F /* Classes.swift */; }; - B6CF53D21E54E2EF00CADD9F /* Species.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53D11E54E2EF00CADD9F /* Species.swift */; }; - B6CF53D91E56442800CADD9F /* SpeciesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53D81E56442800CADD9F /* SpeciesTests.swift */; }; - B6CF53DB1E56443A00CADD9F /* ClassesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53DA1E56443A00CADD9F /* ClassesTests.swift */; }; - B6CF53E51E57429D00CADD9F /* JSONFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53E41E57429D00CADD9F /* JSONFileTests.swift */; }; - B6CF53E71E5742C700CADD9F /* JSONFile.json in Resources */ = {isa = PBXBuildFile; fileRef = B6CF53E61E5742C700CADD9F /* JSONFile.json */; }; - B6CF53E91E57460F00CADD9F /* InvalidJSONFile.json in Resources */ = {isa = PBXBuildFile; fileRef = B6CF53E81E57460F00CADD9F /* InvalidJSONFile.json */; }; - B6CF53EB1E574ED400CADD9F /* HalfBakedJSONFile.json in Resources */ = {isa = PBXBuildFile; fileRef = B6CF53EA1E574ED400CADD9F /* HalfBakedJSONFile.json */; }; - B6D2EB1F26D7B7E900F99B35 /* RandomIndexGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D2EB1E26D7B7E900F99B35 /* RandomIndexGenerator.swift */; }; - B6F070361E4F991D00F66918 /* AlignmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F070351E4F991D00F66918 /* AlignmentTests.swift */; }; - B6F070381E4FBD6500F66918 /* SpeciesTraits.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */; }; - B6F0703A1E4FC03700F66918 /* Money.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F070391E4FC03700F66918 /* Money.swift */; }; - B6F0703C1E4FC18500F66918 /* SpeciesTraitsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F0703B1E4FC18500F66918 /* SpeciesTraitsTests.swift */; }; - B6F4AA481F12C9FA000C72D2 /* CharacterGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F4AA471F12C9FA000C72D2 /* CharacterGenerator.swift */; }; - B6F4AA4A1F12CD2A000C72D2 /* CharacterGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F4AA491F12CD2A000C72D2 /* CharacterGeneratorTests.swift */; }; - B6F4AA4C1F12CE17000C72D2 /* TestCharacterGenerator.json in Resources */ = {isa = PBXBuildFile; fileRef = B6F4AA4B1F12CE17000C72D2 /* TestCharacterGenerator.json */; }; - B6FA6CB01E47ACA1004D91B1 /* UnitCurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CAF1E47ACA1004D91B1 /* UnitCurrency.swift */; }; - B6FA6CB41E47ACCC004D91B1 /* ServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CB31E47ACCC004D91B1 /* ServiceError.swift */; }; - B6FA6CB61E47B080004D91B1 /* CurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CB51E47B080004D91B1 /* CurrencyTests.swift */; }; - B6FA6CB91E47B4B1004D91B1 /* ServiceErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CB81E47B4B1004D91B1 /* ServiceErrorTests.swift */; }; - B6FA6CBB1E47B7F6004D91B1 /* Height.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CBA1E47B7F6004D91B1 /* Height.swift */; }; - B6FA6CBD1E47B803004D91B1 /* Weight.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CBC1E47B803004D91B1 /* Weight.swift */; }; - B6FA6CBF1E47C2F9004D91B1 /* HeightTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CBE1E47C2F9004D91B1 /* HeightTests.swift */; }; - B6FA6CC11E47C306004D91B1 /* WeightTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CC01E47C306004D91B1 /* WeightTests.swift */; }; - B6FA6CC51E4A96D1004D91B1 /* Ability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CC41E4A96D1004D91B1 /* Ability.swift */; }; - B6FA6CC91E4BF5F9004D91B1 /* AbilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CC81E4BF5F9004D91B1 /* AbilityTests.swift */; }; - B6FA6CCB1E4E7AEA004D91B1 /* Alignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CCA1E4E7AEA004D91B1 /* Alignment.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - B62055F91E19DD23002494AB /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = B62055E51E19DD23002494AB /* Project object */; - proxyType = 1; - remoteGlobalIDString = B62055ED1E19DD23002494AB; - remoteInfo = RolePlayingCore; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - B62055EE1E19DD23002494AB /* RolePlayingCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RolePlayingCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B62055F11E19DD23002494AB /* RolePlayingCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RolePlayingCore.h; sourceTree = ""; }; - B62055F21E19DD23002494AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B62055F71E19DD23002494AB /* RolePlayingCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RolePlayingCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - B62055FC1E19DD23002494AB /* RolePlayingCoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RolePlayingCoreTests.swift; sourceTree = ""; }; - B62055FE1E19DD23002494AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B62056091E19DDC0002494AB /* DiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiceTests.swift; sourceTree = ""; }; - B620560B1E19DDD0002494AB /* Dice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dice.swift; sourceTree = ""; }; - B620560C1E19DDD0002494AB /* DiceParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiceParser.swift; sourceTree = ""; }; - B621A3961F0C020000E55236 /* NameGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGenerator.swift; sourceTree = ""; }; - B621A3971F0C052D00E55236 /* NameGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGeneratorTests.swift; sourceTree = ""; }; - B62D89C31F09A3870095D587 /* DiceParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiceParserTests.swift; sourceTree = ""; }; - B64C369521756BC300C4F6BE /* DiceRoll.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiceRoll.swift; sourceTree = ""; }; - B6688B502EACF5AE000A83DD /* Initiative.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Initiative.swift; sourceTree = ""; }; - B66AF8F41EAE87A800C15F8E /* TestClasses.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestClasses.json; sourceTree = ""; }; - B66AF8F51EAE87A800C15F8E /* TestConfiguration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestConfiguration.json; sourceTree = ""; }; - B66AF8F61EAE87A800C15F8E /* TestCurrencies.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestCurrencies.json; sourceTree = ""; }; - B66AF8F71EAE87A800C15F8E /* TestMoreClasses.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestMoreClasses.json; sourceTree = ""; }; - B66AF8F81EAE87A800C15F8E /* TestMoreSpecies.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestMoreSpecies.json; sourceTree = ""; }; - B66AF8F91EAE87A800C15F8E /* TestNames.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestNames.json; sourceTree = ""; }; - B66AF8FA1EAE87A800C15F8E /* TestPlayers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestPlayers.json; sourceTree = ""; }; - B66AF8FB1EAE87A800C15F8E /* TestSpecies.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestSpecies.json; sourceTree = ""; }; - B66AF9041EAE88C300C15F8E /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; - B66AF9061EAE88FF00C15F8E /* ConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationTests.swift; sourceTree = ""; }; - B66AF9081EAE8B8000C15F8E /* MissingClassPlayers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = MissingClassPlayers.json; sourceTree = ""; }; - B66AF90A1EAE8BF100C15F8E /* MissingSpeciesPlayers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = MissingSpeciesPlayers.json; sourceTree = ""; }; - B67450E31F118A8B0061FD6F /* SpeciesNamesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeciesNamesTests.swift; sourceTree = ""; }; - B681B7151EAAC8E2001DE78B /* SimpleDice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleDice.swift; sourceTree = ""; }; - B681B7171EAAC8EE001DE78B /* DiceModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiceModifier.swift; sourceTree = ""; }; - B681B7191EAAC8FB001DE78B /* CompoundDice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompoundDice.swift; sourceTree = ""; }; - B681B71B1EAAC904001DE78B /* DroppingDice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DroppingDice.swift; sourceTree = ""; }; - B681B71D1EAACA9B001DE78B /* Die.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Die.swift; sourceTree = ""; }; - B698515D1F1171EC006A537A /* SpeciesNames.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeciesNames.swift; sourceTree = ""; }; - B69851611F11850A006A537A /* TestSpeciesNames.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = TestSpeciesNames.json; sourceTree = ""; }; - B69F84681E58B8F700A4D2B0 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; - B69F846A1E58D33900A4D2B0 /* PlayerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerTests.swift; sourceTree = ""; }; - B69F846C1E58D66900A4D2B0 /* Players.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Players.swift; sourceTree = ""; }; - B69F846E1E59155A00A4D2B0 /* PlayersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayersTests.swift; sourceTree = ""; }; - B69F84721E591DE400A4D2B0 /* InvalidClassPlayers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InvalidClassPlayers.json; sourceTree = ""; }; - B69F84741E591E9800A4D2B0 /* InvalidSpeciesPlayers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InvalidSpeciesPlayers.json; sourceTree = ""; }; - B6A1AE581F0E4C59008ADF08 /* InvalidConfiguration.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = InvalidConfiguration.json; sourceTree = ""; }; - B6A29EB41EFE9B9F00DAB40C /* Currencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Currencies.swift; sourceTree = ""; }; - B6CF538F1E51DA1300CADD9F /* ClassTraits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassTraits.swift; sourceTree = ""; }; - B6CF53911E51DEDD00CADD9F /* ClassTraitsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassTraitsTests.swift; sourceTree = ""; }; - B6CF53CD1E54DF4500CADD9F /* JSONFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFile.swift; sourceTree = ""; }; - B6CF53CF1E54E2E200CADD9F /* Classes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Classes.swift; sourceTree = ""; }; - B6CF53D11E54E2EF00CADD9F /* Species.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Species.swift; sourceTree = ""; }; - B6CF53D81E56442800CADD9F /* SpeciesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeciesTests.swift; sourceTree = ""; }; - B6CF53DA1E56443A00CADD9F /* ClassesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassesTests.swift; sourceTree = ""; }; - B6CF53E41E57429D00CADD9F /* JSONFileTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFileTests.swift; sourceTree = ""; }; - B6CF53E61E5742C700CADD9F /* JSONFile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = JSONFile.json; sourceTree = ""; }; - B6CF53E81E57460F00CADD9F /* InvalidJSONFile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InvalidJSONFile.json; sourceTree = ""; }; - B6CF53EA1E574ED400CADD9F /* HalfBakedJSONFile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HalfBakedJSONFile.json; sourceTree = ""; }; - B6D2EB1E26D7B7E900F99B35 /* RandomIndexGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomIndexGenerator.swift; sourceTree = ""; }; - B6F070351E4F991D00F66918 /* AlignmentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlignmentTests.swift; sourceTree = ""; }; - B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeciesTraits.swift; sourceTree = ""; }; - B6F070391E4FC03700F66918 /* Money.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Money.swift; sourceTree = ""; }; - B6F0703B1E4FC18500F66918 /* SpeciesTraitsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeciesTraitsTests.swift; sourceTree = ""; }; - B6F4AA471F12C9FA000C72D2 /* CharacterGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CharacterGenerator.swift; sourceTree = ""; }; - B6F4AA491F12CD2A000C72D2 /* CharacterGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterGeneratorTests.swift; sourceTree = ""; }; - B6F4AA4B1F12CE17000C72D2 /* TestCharacterGenerator.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = TestCharacterGenerator.json; sourceTree = ""; }; - B6FA6CAF1E47ACA1004D91B1 /* UnitCurrency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitCurrency.swift; sourceTree = ""; }; - B6FA6CB31E47ACCC004D91B1 /* ServiceError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceError.swift; sourceTree = ""; }; - B6FA6CB51E47B080004D91B1 /* CurrencyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyTests.swift; sourceTree = ""; }; - B6FA6CB81E47B4B1004D91B1 /* ServiceErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceErrorTests.swift; sourceTree = ""; }; - B6FA6CBA1E47B7F6004D91B1 /* Height.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Height.swift; sourceTree = ""; }; - B6FA6CBC1E47B803004D91B1 /* Weight.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Weight.swift; sourceTree = ""; }; - B6FA6CBE1E47C2F9004D91B1 /* HeightTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeightTests.swift; sourceTree = ""; }; - B6FA6CC01E47C306004D91B1 /* WeightTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeightTests.swift; sourceTree = ""; }; - B6FA6CC41E4A96D1004D91B1 /* Ability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Ability.swift; sourceTree = ""; }; - B6FA6CC81E4BF5F9004D91B1 /* AbilityTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AbilityTests.swift; sourceTree = ""; }; - B6FA6CCA1E4E7AEA004D91B1 /* Alignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Alignment.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - B62055EA1E19DD23002494AB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B62055F41E19DD23002494AB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - B62055F81E19DD23002494AB /* RolePlayingCore.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - B62055E41E19DD23002494AB = { - isa = PBXGroup; - children = ( - B62055F01E19DD23002494AB /* RolePlayingCore */, - B62055FB1E19DD23002494AB /* RolePlayingCoreTests */, - B62055EF1E19DD23002494AB /* Products */, - ); - sourceTree = ""; - }; - B62055EF1E19DD23002494AB /* Products */ = { - isa = PBXGroup; - children = ( - B62055EE1E19DD23002494AB /* RolePlayingCore.framework */, - B62055F71E19DD23002494AB /* RolePlayingCoreTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - B62055F01E19DD23002494AB /* RolePlayingCore */ = { - isa = PBXGroup; - children = ( - B6F4AA461F12C9E5000C72D2 /* Configuration */, - B6FA6CAD1E47AC8D004D91B1 /* Common */, - B6FA6CAE1E47AC8D004D91B1 /* Currency */, - B62056081E19DD5D002494AB /* Dice */, - B6FA6CC31E4A96C2004D91B1 /* Player */, - B62055F11E19DD23002494AB /* RolePlayingCore.h */, - B62055F21E19DD23002494AB /* Info.plist */, - ); - path = RolePlayingCore; - sourceTree = ""; - }; - B62055FB1E19DD23002494AB /* RolePlayingCoreTests */ = { - isa = PBXGroup; - children = ( - B62055FC1E19DD23002494AB /* RolePlayingCoreTests.swift */, - B6FA6CC81E4BF5F9004D91B1 /* AbilityTests.swift */, - B6F070351E4F991D00F66918 /* AlignmentTests.swift */, - B69F846A1E58D33900A4D2B0 /* PlayerTests.swift */, - B69F846E1E59155A00A4D2B0 /* PlayersTests.swift */, - B6FA6CB51E47B080004D91B1 /* CurrencyTests.swift */, - B62056091E19DDC0002494AB /* DiceTests.swift */, - B62D89C31F09A3870095D587 /* DiceParserTests.swift */, - B6FA6CBE1E47C2F9004D91B1 /* HeightTests.swift */, - B6FA6CC01E47C306004D91B1 /* WeightTests.swift */, - B6CF53911E51DEDD00CADD9F /* ClassTraitsTests.swift */, - B6CF53DA1E56443A00CADD9F /* ClassesTests.swift */, - B6F0703B1E4FC18500F66918 /* SpeciesTraitsTests.swift */, - B6CF53D81E56442800CADD9F /* SpeciesTests.swift */, - B66AF9061EAE88FF00C15F8E /* ConfigurationTests.swift */, - B6CF53E41E57429D00CADD9F /* JSONFileTests.swift */, - B6FA6CB81E47B4B1004D91B1 /* ServiceErrorTests.swift */, - B67450E31F118A8B0061FD6F /* SpeciesNamesTests.swift */, - B621A3971F0C052D00E55236 /* NameGeneratorTests.swift */, - B6F4AA491F12CD2A000C72D2 /* CharacterGeneratorTests.swift */, - B6CF53E61E5742C700CADD9F /* JSONFile.json */, - B6CF53E81E57460F00CADD9F /* InvalidJSONFile.json */, - B6CF53EA1E574ED400CADD9F /* HalfBakedJSONFile.json */, - B66AF9081EAE8B8000C15F8E /* MissingClassPlayers.json */, - B66AF90A1EAE8BF100C15F8E /* MissingSpeciesPlayers.json */, - B69F84721E591DE400A4D2B0 /* InvalidClassPlayers.json */, - B69F84741E591E9800A4D2B0 /* InvalidSpeciesPlayers.json */, - B66AF8F41EAE87A800C15F8E /* TestClasses.json */, - B66AF8F71EAE87A800C15F8E /* TestMoreClasses.json */, - B66AF8F51EAE87A800C15F8E /* TestConfiguration.json */, - B6F4AA4B1F12CE17000C72D2 /* TestCharacterGenerator.json */, - B66AF8F61EAE87A800C15F8E /* TestCurrencies.json */, - B66AF8F91EAE87A800C15F8E /* TestNames.json */, - B69851611F11850A006A537A /* TestSpeciesNames.json */, - B66AF8FA1EAE87A800C15F8E /* TestPlayers.json */, - B66AF8FB1EAE87A800C15F8E /* TestSpecies.json */, - B66AF8F81EAE87A800C15F8E /* TestMoreSpecies.json */, - B6A1AE581F0E4C59008ADF08 /* InvalidConfiguration.json */, - B62055FE1E19DD23002494AB /* Info.plist */, - ); - path = RolePlayingCoreTests; - sourceTree = ""; - }; - B62056081E19DD5D002494AB /* Dice */ = { - isa = PBXGroup; - children = ( - B681B71D1EAACA9B001DE78B /* Die.swift */, - B620560B1E19DDD0002494AB /* Dice.swift */, - B64C369521756BC300C4F6BE /* DiceRoll.swift */, - B681B7151EAAC8E2001DE78B /* SimpleDice.swift */, - B681B7171EAAC8EE001DE78B /* DiceModifier.swift */, - B681B7191EAAC8FB001DE78B /* CompoundDice.swift */, - B681B71B1EAAC904001DE78B /* DroppingDice.swift */, - B620560C1E19DDD0002494AB /* DiceParser.swift */, - ); - path = Dice; - sourceTree = ""; - }; - B6F4AA461F12C9E5000C72D2 /* Configuration */ = { - isa = PBXGroup; - children = ( - B66AF9041EAE88C300C15F8E /* Configuration.swift */, - B6F4AA471F12C9FA000C72D2 /* CharacterGenerator.swift */, - ); - path = Configuration; - sourceTree = ""; - }; - B6FA6CAD1E47AC8D004D91B1 /* Common */ = { - isa = PBXGroup; - children = ( - B6FA6CB31E47ACCC004D91B1 /* ServiceError.swift */, - B6FA6CBA1E47B7F6004D91B1 /* Height.swift */, - B6FA6CBC1E47B803004D91B1 /* Weight.swift */, - B621A3961F0C020000E55236 /* NameGenerator.swift */, - B6D2EB1E26D7B7E900F99B35 /* RandomIndexGenerator.swift */, - B6CF53CD1E54DF4500CADD9F /* JSONFile.swift */, - B698515D1F1171EC006A537A /* SpeciesNames.swift */, - ); - path = Common; - sourceTree = ""; - }; - B6FA6CAE1E47AC8D004D91B1 /* Currency */ = { - isa = PBXGroup; - children = ( - B6FA6CAF1E47ACA1004D91B1 /* UnitCurrency.swift */, - B6A29EB41EFE9B9F00DAB40C /* Currencies.swift */, - B6F070391E4FC03700F66918 /* Money.swift */, - ); - path = Currency; - sourceTree = ""; - }; - B6FA6CC31E4A96C2004D91B1 /* Player */ = { - isa = PBXGroup; - children = ( - B6FA6CC41E4A96D1004D91B1 /* Ability.swift */, - B6FA6CCA1E4E7AEA004D91B1 /* Alignment.swift */, - B6CF538F1E51DA1300CADD9F /* ClassTraits.swift */, - B6CF53CF1E54E2E200CADD9F /* Classes.swift */, - B69F84681E58B8F700A4D2B0 /* Player.swift */, - B6688B502EACF5AE000A83DD /* Initiative.swift */, - B69F846C1E58D66900A4D2B0 /* Players.swift */, - B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */, - B6CF53D11E54E2EF00CADD9F /* Species.swift */, - ); - path = Player; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - B62055EB1E19DD23002494AB /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - B62055FF1E19DD23002494AB /* RolePlayingCore.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - B62055ED1E19DD23002494AB /* RolePlayingCore */ = { - isa = PBXNativeTarget; - buildConfigurationList = B62056021E19DD23002494AB /* Build configuration list for PBXNativeTarget "RolePlayingCore" */; - buildPhases = ( - B62055E91E19DD23002494AB /* Sources */, - B62055EA1E19DD23002494AB /* Frameworks */, - B62055EB1E19DD23002494AB /* Headers */, - B62055EC1E19DD23002494AB /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = RolePlayingCore; - productName = RolePlayingCore; - productReference = B62055EE1E19DD23002494AB /* RolePlayingCore.framework */; - productType = "com.apple.product-type.framework"; - }; - B62055F61E19DD23002494AB /* RolePlayingCoreTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = B62056051E19DD23002494AB /* Build configuration list for PBXNativeTarget "RolePlayingCoreTests" */; - buildPhases = ( - B62055F31E19DD23002494AB /* Sources */, - B62055F41E19DD23002494AB /* Frameworks */, - B62055F51E19DD23002494AB /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - B62055FA1E19DD23002494AB /* PBXTargetDependency */, - ); - name = RolePlayingCoreTests; - productName = RolePlayingCoreTests; - productReference = B62055F71E19DD23002494AB /* RolePlayingCoreTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - B62055E51E19DD23002494AB /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 1620; - ORGANIZATIONNAME = "Brian Arnold"; - TargetAttributes = { - B62055ED1E19DD23002494AB = { - CreatedOnToolsVersion = 8.2.1; - LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; - }; - B62055F61E19DD23002494AB = { - CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = J69E69SP27; - LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; - }; - }; - }; - buildConfigurationList = B62055E81E19DD23002494AB /* Build configuration list for PBXProject "RolePlayingCore" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = B62055E41E19DD23002494AB; - productRefGroup = B62055EF1E19DD23002494AB /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - B62055ED1E19DD23002494AB /* RolePlayingCore */, - B62055F61E19DD23002494AB /* RolePlayingCoreTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - B62055EC1E19DD23002494AB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B62055F51E19DD23002494AB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B69851621F11850A006A537A /* TestSpeciesNames.json in Resources */, - B6A1AE591F0E4C59008ADF08 /* InvalidConfiguration.json in Resources */, - B6CF53E91E57460F00CADD9F /* InvalidJSONFile.json in Resources */, - B66AF9021EAE87A800C15F8E /* TestPlayers.json in Resources */, - B66AF8FE1EAE87A800C15F8E /* TestCurrencies.json in Resources */, - B66AF90B1EAE8BF100C15F8E /* MissingSpeciesPlayers.json in Resources */, - B69F84751E591E9800A4D2B0 /* InvalidSpeciesPlayers.json in Resources */, - B66AF9011EAE87A800C15F8E /* TestNames.json in Resources */, - B66AF8FD1EAE87A800C15F8E /* TestConfiguration.json in Resources */, - B66AF9091EAE8B8100C15F8E /* MissingClassPlayers.json in Resources */, - B69F84731E591DE400A4D2B0 /* InvalidClassPlayers.json in Resources */, - B66AF8FF1EAE87A800C15F8E /* TestMoreClasses.json in Resources */, - B66AF9031EAE87A800C15F8E /* TestSpecies.json in Resources */, - B6F4AA4C1F12CE17000C72D2 /* TestCharacterGenerator.json in Resources */, - B66AF9001EAE87A800C15F8E /* TestMoreSpecies.json in Resources */, - B6CF53EB1E574ED400CADD9F /* HalfBakedJSONFile.json in Resources */, - B66AF8FC1EAE87A800C15F8E /* TestClasses.json in Resources */, - B6CF53E71E5742C700CADD9F /* JSONFile.json in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - B62055E91E19DD23002494AB /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B69F846D1E58D66900A4D2B0 /* Players.swift in Sources */, - B681B71C1EAAC904001DE78B /* DroppingDice.swift in Sources */, - B681B7161EAAC8E2001DE78B /* SimpleDice.swift in Sources */, - B6F4AA481F12C9FA000C72D2 /* CharacterGenerator.swift in Sources */, - B6FA6CB41E47ACCC004D91B1 /* ServiceError.swift in Sources */, - B6D2EB1F26D7B7E900F99B35 /* RandomIndexGenerator.swift in Sources */, - B6F0703A1E4FC03700F66918 /* Money.swift in Sources */, - B6FA6CCB1E4E7AEA004D91B1 /* Alignment.swift in Sources */, - B698515E1F1171EC006A537A /* SpeciesNames.swift in Sources */, - B6A29EB51EFE9B9F00DAB40C /* Currencies.swift in Sources */, - B66AF9051EAE88C300C15F8E /* Configuration.swift in Sources */, - B6CF53CE1E54DF4500CADD9F /* JSONFile.swift in Sources */, - B6CF53D21E54E2EF00CADD9F /* Species.swift in Sources */, - B6FA6CB01E47ACA1004D91B1 /* UnitCurrency.swift in Sources */, - B6CF53901E51DA1300CADD9F /* ClassTraits.swift in Sources */, - B621A3991F0C06A700E55236 /* NameGenerator.swift in Sources */, - B6688B512EACF5B7000A83DD /* Initiative.swift in Sources */, - B681B71E1EAACA9B001DE78B /* Die.swift in Sources */, - B64C369621756BC300C4F6BE /* DiceRoll.swift in Sources */, - B620560E1E19DDD0002494AB /* Dice.swift in Sources */, - B6CF53D01E54E2E200CADD9F /* Classes.swift in Sources */, - B620560F1E19DDD0002494AB /* DiceParser.swift in Sources */, - B6FA6CBB1E47B7F6004D91B1 /* Height.swift in Sources */, - B6F070381E4FBD6500F66918 /* SpeciesTraits.swift in Sources */, - B681B71A1EAAC8FB001DE78B /* CompoundDice.swift in Sources */, - B6FA6CBD1E47B803004D91B1 /* Weight.swift in Sources */, - B6FA6CC51E4A96D1004D91B1 /* Ability.swift in Sources */, - B681B7181EAAC8EE001DE78B /* DiceModifier.swift in Sources */, - B69F84691E58B8F700A4D2B0 /* Player.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B62055F31E19DD23002494AB /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B6CF53E51E57429D00CADD9F /* JSONFileTests.swift in Sources */, - B6CF53D91E56442800CADD9F /* SpeciesTests.swift in Sources */, - B6F0703C1E4FC18500F66918 /* SpeciesTraitsTests.swift in Sources */, - B6F4AA4A1F12CD2A000C72D2 /* CharacterGeneratorTests.swift in Sources */, - B69F846F1E59155A00A4D2B0 /* PlayersTests.swift in Sources */, - B62D89C41F09A3870095D587 /* DiceParserTests.swift in Sources */, - B6CF53DB1E56443A00CADD9F /* ClassesTests.swift in Sources */, - B621A3981F0C052D00E55236 /* NameGeneratorTests.swift in Sources */, - B6CF53921E51DEDD00CADD9F /* ClassTraitsTests.swift in Sources */, - B6FA6CB91E47B4B1004D91B1 /* ServiceErrorTests.swift in Sources */, - B67450E41F118A8B0061FD6F /* SpeciesNamesTests.swift in Sources */, - B6F070361E4F991D00F66918 /* AlignmentTests.swift in Sources */, - B6FA6CC91E4BF5F9004D91B1 /* AbilityTests.swift in Sources */, - B6FA6CB61E47B080004D91B1 /* CurrencyTests.swift in Sources */, - B620560A1E19DDC0002494AB /* DiceTests.swift in Sources */, - B6FA6CBF1E47C2F9004D91B1 /* HeightTests.swift in Sources */, - B62055FD1E19DD23002494AB /* RolePlayingCoreTests.swift in Sources */, - B6FA6CC11E47C306004D91B1 /* WeightTests.swift in Sources */, - B66AF9071EAE88FF00C15F8E /* ConfigurationTests.swift in Sources */, - B69F846B1E58D33900A4D2B0 /* PlayerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - B62055FA1E19DD23002494AB /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = B62055ED1E19DD23002494AB /* RolePlayingCore */; - targetProxy = B62055F91E19DD23002494AB /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - B62056001E19DD23002494AB /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - B62056011E19DD23002494AB /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - B62056031E19DD23002494AB /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; - INFOPLIST_FILE = RolePlayingCore/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.RolePlayingCore; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - B62056041E19DD23002494AB /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; - INFOPLIST_FILE = RolePlayingCore/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.RolePlayingCore; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - B62056061E19DD23002494AB /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = RolePlayingCoreTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.RolePlayingCoreTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - B62056071E19DD23002494AB /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = RolePlayingCoreTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.RolePlayingCoreTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - B62055E81E19DD23002494AB /* Build configuration list for PBXProject "RolePlayingCore" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B62056001E19DD23002494AB /* Debug */, - B62056011E19DD23002494AB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B62056021E19DD23002494AB /* Build configuration list for PBXNativeTarget "RolePlayingCore" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B62056031E19DD23002494AB /* Debug */, - B62056041E19DD23002494AB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B62056051E19DD23002494AB /* Build configuration list for PBXNativeTarget "RolePlayingCoreTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B62056061E19DD23002494AB /* Debug */, - B62056071E19DD23002494AB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = B62055E51E19DD23002494AB /* Project object */; -} diff --git a/RolePlayingCore/RolePlayingCore.xcodeproj/xcshareddata/xcschemes/RolePlayingCore.xcscheme b/RolePlayingCore/RolePlayingCore.xcodeproj/xcshareddata/xcschemes/RolePlayingCore.xcscheme deleted file mode 100644 index 933d100..0000000 --- a/RolePlayingCore/RolePlayingCore.xcodeproj/xcshareddata/xcschemes/RolePlayingCore.xcscheme +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/RolePlayingCore/RolePlayingCore/Common/Height.swift b/RolePlayingCore/RolePlayingCore/Common/Height.swift deleted file mode 100644 index 4ffe5e1..0000000 --- a/RolePlayingCore/RolePlayingCore/Common/Height.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// Height -// RolePlayingCore -// -// Created by Brian Arnold on 2/5/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import Foundation - -/// Height is a measurement of length. -public typealias Height = Measurement - -extension String { - - /// Parses ft or ', in or ", cm, m into a measurement of length. - /// Returns nil if the string could not be parsed. - public var parseHeight: Height? { - var value: Double? - var unit: UnitLength = .feet - - // Try going feet-first. - let feetList = ["'", "ft"] - var feetEndRange: Range? - for key in feetList { - feetEndRange = self.range(of: key) - if let feetEndRange = feetEndRange { - value = Double(self[..(value: inches, unit: .inches).converted(to: .feet).value - value = value != nil ? value! + inchesInFeet : inchesInFeet - break - } - } - - // If neither feet nor inches were specified, try metric. - if value == nil { - let metricMap: [String: UnitLength] = [ - "cm": .centimeters, - "m": .meters] - - for (key, metricUnit) in metricMap { - if let range = self.range(of: key) { - value = Double(self[.. Height { - let height: Height? - - if let double = try? self.decode(Double.self, forKey: key) { - height = Height(value: double, unit: .feet) - } else { - height = try self.decode(String.self, forKey: key).parseHeight - } - - // Throw if we were unsuccessful parsing. - guard height != nil else { - let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Missing string or number for Height value") - throw DecodingError.dataCorrupted(context) - } - - return height! - } - - /// Decodes either a number or a string into a Height, if present. - /// - /// - throws `DecodingError.dataCorrupted` if the height could not be decoded. - func decodeIfPresent(_ type: Height.Type, forKey key: K) throws -> Height? { - let height: Height? - - if let double = try? self.decode(Double.self, forKey: key) { - height = Height(value: double, unit: .feet) - } else if let string = try self.decodeIfPresent(String.self, forKey: key) { - height = string.parseHeight - } else { - height = nil - } - - return height - } -} - -extension Height { - - /// Returns a string representation of the height suitable for display. - public var displayString: String { - let locale = Locale.current - let usesMetric = locale.measurementSystem == .metric - - if usesMetric { - // Use centimeters for metric locales - let cm = self.converted(to: .centimeters) - return cm.formatted(.measurement(width: .abbreviated, usage: .personHeight)) - } else { - // Use feet and inches for imperial locales - let totalInches = self.converted(to: .inches).value - let feet = Int(totalInches / 12) - let inches = Int(totalInches.truncatingRemainder(dividingBy: 12)) - - let feetMeasurement = Measurement(value: Double(feet), unit: UnitLength.feet) - let feetString = feetMeasurement.formatted(.measurement(width: .abbreviated)) - - let inchesMeasurement = Measurement(value: Double(inches), unit: UnitLength.inches) - let inchesString = inchesMeasurement.formatted(.measurement(width: .abbreviated)) - return "\(feetString), \(inchesString)" - } - } -} diff --git a/RolePlayingCore/RolePlayingCore/Common/JSONFile.swift b/RolePlayingCore/RolePlayingCore/Common/JSONFile.swift deleted file mode 100644 index 8e9bbea..0000000 --- a/RolePlayingCore/RolePlayingCore/Common/JSONFile.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// JSONFile.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/15/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - - -extension Bundle { - - /// Loads a specified JSON-formatted UTF-8 file from this bundle's resources, and returns its dictionary. - /// - /// - parameter fileName: The name of the file, without its extension. - /// - /// - throws: `ServiceError.runtimeError` if the file is missing, or can't be parsed into a dictionary. - /// - throws: `NSError` if the file can't be read. - public func loadJSON(_ fileName: String) throws -> Data { - guard let url = self.url(forResource: fileName, withExtension: "json") else { throw RuntimeError("Could not load \(fileName).json from \(self.bundleURL)") } - return try Data(contentsOf: url, options: [.mappedIfSafe]) - } - -} diff --git a/RolePlayingCore/RolePlayingCore/Common/ServiceError.swift b/RolePlayingCore/RolePlayingCore/Common/ServiceError.swift deleted file mode 100644 index ab5335d..0000000 --- a/RolePlayingCore/RolePlayingCore/Common/ServiceError.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ServiceError.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/5/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -/* - TODO: Consider switching to Swift Foundation error types if they become available. - - In the meantime, define "std::exception"-style errors manually from Error protocol. - */ - -public enum ServiceError: Error { - - /// Represents an error detected at runtime. - case runtimeError(String, String) - -} - -/// Returns a ServiceError.runtimeError with the specified message. -/// The source function, file and line are recorded in location. -public func RuntimeError(_ message: String, source: String = #function, file: String = #file, line: Int = #line) -> ServiceError { - return ServiceError.runtimeError(message, "Thrown in \(source) (File: \(file) Line: \(line))") -} - -extension ServiceError: CustomStringConvertible { - - /// Returns a description of the error, including the type, message, - /// source function, file and line. - public var description: String { - switch self { - case .runtimeError(let message, let location): - return "Runtime error: \(message)\n\(location)" - } - } - -} diff --git a/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift b/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift deleted file mode 100644 index 85cc190..0000000 --- a/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// Currencies.swift -// RolePlayingCore -// -// Created by Brian Arnold on 6/24/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -public struct Currencies { - - /// A map of all currently loaded currencies. - internal static var allCurrencies: [String: UnitCurrency] = [:] - - /// Returns the unit currency corresponding to this symbol. Returns nil if no symbol matches. - public static func find(_ symbol: String) -> UnitCurrency? { - return Currencies.allCurrencies[symbol] - } - - public static func add(_ currency: UnitCurrency) { - allCurrencies[currency.symbol] = currency - } - - public static func setDefault(_ newBaseUnit: UnitCurrency) { - // Remove the old base unit from all currencies. - let oldSymbol = UnitCurrency.baseUnitCurrency.symbol - guard oldSymbol != newBaseUnit.symbol else { - return - } - - allCurrencies[oldSymbol] = nil - - UnitCurrency.baseUnitCurrency = newBaseUnit - } - -} - -extension Currencies: Codable { - - /// TODO: Codable and NSCoding haven't yet converged. In the meantime, - /// mirror UnitCurrency, using Codable instead of NSCoding. - private struct Currency: Codable { - let symbol: String - let coefficient: Double - let name: String - let plural: String - let isDefault: Bool - - private enum CodingKeys: String, CodingKey { - case symbol - case coefficient - case name - case plural - case isDefault = "is default" - } - - // For writing - init(_ unitCurrency: UnitCurrency) { - self.symbol = unitCurrency.symbol - self.coefficient = (unitCurrency.converter as! UnitConverterLinear).coefficient - self.name = unitCurrency.name - self.plural = unitCurrency.plural - self.isDefault = unitCurrency == .baseUnit() - } - - // For reading - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - symbol = try container.decode(String.self, forKey: .symbol) - coefficient = try container.decode(Double.self, forKey: .coefficient) - name = try container.decode(String.self, forKey: .name) - plural = try container.decode(String.self, forKey: .plural) - isDefault = try container.decodeIfPresent(Bool.self, forKey: .isDefault) ?? false - } - } - - private enum CodingKeys: String, CodingKey { - case currencies - } - - /// Decodes an array of currencies, setting the default currency if present. - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let currencies = try container.decode([Currency].self, forKey: .currencies) - for currency in currencies { - let converter = UnitConverterLinear(coefficient: currency.coefficient) - let unitCurrency = UnitCurrency(symbol: currency.symbol, converter: converter, name: currency.name, plural: currency.plural) - - Currencies.add(unitCurrency) - - if currency.isDefault { - Currencies.setDefault(unitCurrency) - } - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - var currencies = [Currency]() - for unitCurrency in Currencies.allCurrencies.values { - let currency = Currency(unitCurrency) - currencies.append(currency) - } - - try container.encode(currencies, forKey: .currencies) - } - -} diff --git a/RolePlayingCore/RolePlayingCore/Currency/UnitCurrency.swift b/RolePlayingCore/RolePlayingCore/Currency/UnitCurrency.swift deleted file mode 100644 index 3c51883..0000000 --- a/RolePlayingCore/RolePlayingCore/Currency/UnitCurrency.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// UnitCurrency.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/5/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import Foundation - -/// Units of currency or coinage. -/// -/// Use `Measurement` to hold values of currency. -public final class UnitCurrency : Dimension, @unchecked Sendable { - - /// The singular unit name used when the unitStyle is long. - public internal(set) var name: String! - - /// The plural unit name used when the unitStyle is long. - public internal(set) var plural: String! - - /// The default base unit is a currency called "credit". It may be replaced at runtime. - internal static var baseUnitCurrency = UnitCurrency(symbol: "c", converter: UnitConverterLinear(coefficient: 1.0), name: "credit", plural: "credits") - - public override class func baseUnit() -> UnitCurrency { - return baseUnitCurrency - } - - public init(symbol: String, converter: UnitConverter, name: String, plural: String) { - self.name = name - self.plural = plural - super.init(symbol: symbol, converter: converter) - } - - // TODO: In order to provide the other init method, I was required to implement - // this one as well. However, I don't know how to reconcile NSCoder with Codable. - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - -} diff --git a/RolePlayingCore/RolePlayingCore/Dice/DiceParser.swift b/RolePlayingCore/RolePlayingCore/Dice/DiceParser.swift deleted file mode 100644 index 88a6cdd..0000000 --- a/RolePlayingCore/RolePlayingCore/Dice/DiceParser.swift +++ /dev/null @@ -1,305 +0,0 @@ -// -// DiceParser.swift -// RolePlayingCore -// -// Created by Brian Arnold on 11/23/16. -// Copyright © 2016-2017 Brian Arnold. All rights reserved. -// - -import Foundation - - -// TODO: the initial implementation performed naïve sub-string searches, and was very limited. -// This implementation uses a lightweight tokenizer and parser, and is a lot more robust. -// A smaller implementation might leverage regular expression, but may be harder to maintain. - -/// Types of errors handled by this parser. -internal enum DiceParseError: Error { - case invalidCharacter(String) - case invalidDieSides(Int) - case missingMinus - case missingSimpleDice - case missingDieSides - case missingExpression - case consecutiveNumbers - case consecutiveMathOperators - case consecutiveDiceExpressions -} - -/// Types of tokens supported by this parser. -internal enum Token { - case number(Int) - case mathOperator(String) - case die - case drop(String) - - static let mathOperatorCharacters = CharacterSet(charactersIn: CompoundDice.mathOperators.keys.reduce("", +)) - static let dieCharacters = CharacterSet(charactersIn: "dD") - static let dropCharacters = CharacterSet(charactersIn: DroppingDice.Drop.allCases.map({ $0.rawValue }).reduce("", +)) - static let percentCharacters = CharacterSet(charactersIn: "%") - - init?(from scalar: UnicodeScalar) { - if Token.mathOperatorCharacters.contains(scalar) { - self = .mathOperator(String(scalar)) - } else if Token.dieCharacters.contains(scalar) { - self = .die - } else if Token.dropCharacters.contains(scalar) { - self = .drop(String(scalar)) - } else if Token.percentCharacters.contains(scalar) { - self = .number(100) - } else { - return nil - } - } - - var isDropping: Bool { - // TODO: is this the most compact way to compare an enum? - guard case .drop(_) = self else { return false } - return true - } -} - -/// An internal buffer for parsing numbers from a string. -private struct NumberBuffer { - private var buffer: String = "" - - mutating func append(_ scalar: UnicodeScalar) { - buffer.append(Character(scalar)) - } - - mutating func flush() -> Int? { - guard !buffer.isEmpty else { return nil } - defer { buffer = "" } - return Int(buffer) - } -} - -/// Converts a dice-formatted string into a sequence of tokens. -/// If an unknown character is encountered, an empty array is returned. -internal func tokenize(_ string: String) throws -> [Token] { - var tokens = [Token]() - var numberBuffer = NumberBuffer() - - for scalar in string.unicodeScalars { - // Numbers consume multiple characters - if CharacterSet.decimalDigits.contains(scalar) { - numberBuffer.append(scalar) - } else { - // Flush the current number before parsing the next character - if let value = numberBuffer.flush() { - tokens.append(.number(value)) - } - - // Skip spaces and newlines - guard !CharacterSet.whitespacesAndNewlines.contains(scalar) else { continue } - - if let token = Token(from: scalar) { - tokens.append(token) - } else { - throw DiceParseError.invalidCharacter(String(scalar)) - } - } - } - - if let value = numberBuffer.flush() { - tokens.append(.number(value)) - } - - return tokens -} - -/// Look-ahead at the next token and return whether it's `.drop`. -private func isDropping(_ tokens: [Token], after index: Int) -> Bool { - guard index + 1 < tokens.count else { return false } - let nextToken = tokens[index + 1] - return nextToken.isDropping -} - -/// The internal state of the parser when it processes tokens. -private struct DiceParserState { - var lastNumber: Int? = nil - var lastDice: Dice? = nil - var lastMathOperator: String? = nil - var isParsingDie = false - - /// Parses a number and stores it either in lastDice sides or lastNumber - mutating func parse(number: Int) throws { - if isParsingDie { - guard lastDice == nil else { throw DiceParseError.consecutiveDiceExpressions } - guard let die = Die(rawValue: number) else { throw DiceParseError.invalidDieSides(number) } - let times = lastNumber ?? 1 - lastDice = SimpleDice(die, times: times) - isParsingDie = false - lastNumber = nil - } else { - guard lastNumber == nil else { throw DiceParseError.consecutiveNumbers } - lastNumber = number - } - } - - /// Initiates parsing a die expression; finishes when parsing dice sides as an integer. - mutating func parseDie() throws { - guard !isParsingDie else { throw DiceParseError.consecutiveDiceExpressions } - isParsingDie = true - } - - /// Parses a dropping dice supported by DroppingDice. - /// Must be preceded by a SimpleDice and a '-' math operator. - mutating func parse(drop: String) throws { - guard let simpleDice = lastDice as? SimpleDice else { throw DiceParseError.missingSimpleDice } - guard lastMathOperator == "-" else { throw DiceParseError.missingMinus } - - let diceDrop = DroppingDice.Drop(rawValue: drop)! - lastDice = DroppingDice(simpleDice, drop: diceDrop) - lastMathOperator = nil - } - - /// Parses a math operator supported by CompoundDice. - mutating func parse(math: String) throws { - guard lastMathOperator == nil else { throw DiceParseError.consecutiveMathOperators } - lastMathOperator = math - } - - // Returns a Dice from either the last number (DiceModifier) or lastDice, and resets their state. - mutating func flush() -> Dice? { - let returnDice: Dice? - - if let number = lastNumber { - returnDice = DiceModifier(number) - lastNumber = nil - } else if let dice = lastDice { - returnDice = dice - lastDice = nil - } else { - returnDice = nil - } - - return returnDice - } - - /// Returns combined dice from the current parsed dice passed in as lhs, - /// and the current parse state as rhs. - /// - /// If there is no current parsed dice, the current parse state is returned. - /// If there is no math operator or no rhs, lhs is returned. - mutating func combine(_ lhsDice: Dice?) -> Dice? { - guard let lhsDice = lhsDice else { return flush() } - guard let mathOperator = lastMathOperator, let rhsDice = flush() else { return lhsDice } - - // If we have a left hand side, a math operator and a right hand side, combine them. - let returnDice = CompoundDice(lhs: lhsDice, rhs: rhsDice, mathOperator: mathOperator) - lastMathOperator = nil - - return returnDice - } - - // Checks for invalid or incomplete state at the end of parsing. - func finishParsing() throws { - if isParsingDie { - throw DiceParseError.missingDieSides - } else if lastMathOperator != nil { - throw DiceParseError.missingExpression - } - } -} - -/// Converts an array of tokens into Dice. -internal func parse(_ tokens: [Token]) throws -> Dice? { - var parsedDice: Dice? = nil - - var state = DiceParserState() - for (index, token) in tokens.enumerated() { - switch token { - case .number(let value): - try state.parse(number: value) - case .die: - try state.parseDie() - case .drop(let drop): - try state.parse(drop: drop) - case .mathOperator(let math): - if !isDropping(tokens, after: index) { - parsedDice = state.combine(parsedDice) - } - try state.parse(math: math) - } - } - parsedDice = state.combine(parsedDice) - try state.finishParsing() - - return parsedDice -} - -public extension String { - - /// Creates a Dice instance from a string formatted as: - /// > `[]d[|-]*` - /// - Supported dice sides are 4, 6, 8, 10, 12, 20 and %. - /// - times, modifier and dropping (-L for lowest, -H for highest) are optional. - /// - /// - parameter from: A string, for example: "d8", "2d12+2", "4d6-L", "1", "2d4+3d12-4". - /// - returns: Dice representing the parsed string. Returns `nil` if the string - /// could not be interpreted; for example, if there are extraneous - /// characters, or an unsupported dice such as d7 is specified. - var parseDice: Dice? { - var dice: Dice? = nil - - do { - let tokens = try tokenize(self) - dice = try parse(tokens) - } - catch let error { - print("Error parsing dice: \(error.localizedDescription)") - } - - return dice - } - -} - -// TODO: this works around the fact that we can't explicitly make the Dice protocol Decodable. -// We first decode to String or Int, then create a derived Dice type from those. -// -// Containers can still match to the type (note Dice.protocol in the function declarations). -public extension KeyedDecodingContainer { - - /// Decodes either an integer or a formatted string into a Dice. - /// See `String.parseDice` for supported string formats. - /// - /// - throws `DecodingError.dataCorrupted` if the dice is not present or could not be decoded. - func decode(_ type: Dice.Protocol, forKey key: K) throws -> Dice { - let dice: Dice? - - if let number = try? self.decode(Int.self, forKey: key) { - dice = DiceModifier(number) - } else { - dice = try self.decode(String.self, forKey: key).parseDice - } - - // Throw if we were unsuccessful parsing. - guard dice != nil else { - let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Missing string or number for Dice value") - throw DecodingError.dataCorrupted(context) - } - - return dice! - } - - /// Decodes either an integer or a formatted string into a Dice, if present. - /// See `String.parseDice` for supported string formats. - /// - /// - throws `DecodingError.dataCorrupted` if the dice could not be decoded. - func decodeIfPresent(_ type: Dice.Protocol, forKey key: K) throws -> Dice? { - let dice: Dice? - - if let number = try? self.decode(Int.self, forKey: key) { - dice = DiceModifier(number) - } else if let string = try self.decodeIfPresent(String.self, forKey: key) { - dice = string.parseDice - } else { - dice = nil - } - - return dice - } - -} diff --git a/RolePlayingCore/RolePlayingCore/Info.plist b/RolePlayingCore/RolePlayingCore/Info.plist deleted file mode 100644 index e6f4652..0000000 --- a/RolePlayingCore/RolePlayingCore/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 0.1 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift b/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift deleted file mode 100644 index e3c423a..0000000 --- a/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// Class.swift -// RolePlayingCore -// -// Created by Brian Arnold on 11/12/16. -// Copyright © 2016-2017 Brian Arnold. All rights reserved. -// - -import Foundation - -public struct ClassTraits { - - public var name: String - public var plural: String - public var hitDice: Dice - public var startingWealth: Dice - - public var descriptiveTraits: [String: String] - public var primaryAbility: [Ability] - public var savingThrows: [Ability] - public var experiencePoints: [Int]? - - /// Accesses the experiencePoints array for the specified 1-based level. - public func minExperiencePoints(at level: Int) -> Int { - // Map the level to an index of the array - let index = max(1, level) - 1 - guard let experiencePoints else { return 0 } - guard index < experiencePoints.count else { return experiencePoints.last ?? 0 } - return experiencePoints[index] - } - - /// Accesses the maximum level for this class. - public var maxLevel: Int { - guard let experiencePoints else { return 0 } - return experiencePoints.count - } - - /// Accesses the maximum experience points for the specified 1-based level. - public func maxExperiencePoints(at level: Int) -> Int { - // One less than the minimum for the next level - minExperiencePoints(at: level + 1) - 1 - } - - // TODO: weapons, armor, skills, etc. - - public init(name: String, plural: String, hitDice: Dice, startingWealth: Dice, descriptiveTraits: [String: String] = [:], primaryAbility: [Ability] = [], savingThrows: [Ability] = [], experiencePoints: [Int]? = nil) { - self.name = name - self.plural = plural - self.hitDice = hitDice - self.startingWealth = startingWealth - - self.descriptiveTraits = descriptiveTraits - self.primaryAbility = primaryAbility - self.savingThrows = savingThrows - self.experiencePoints = experiencePoints - } -} - -extension ClassTraits: Codable { - - private enum CodingKeys: String, CodingKey { - case name - case plural - case hitDice = "hit dice" - case startingWealth = "starting wealth" - case descriptiveTraits = "descriptive traits" - case primaryAbility = "primary ability" - case savingThrows = "saving throws" - case experiencePoints = "experience points" - } - - public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - - // Try decoding properties - let name = try values.decode(String.self, forKey: .name) - let plural = try values.decode(String.self, forKey: .plural) - let hitDice = try values.decode(Dice.self, forKey: .hitDice) - let startingWealth = try values.decode(Dice.self, forKey: .startingWealth) - - let descriptiveTraits = try values.decodeIfPresent([String:String].self, forKey: .descriptiveTraits) - let primaryAbility = try values.decodeIfPresent([Ability].self, forKey: .primaryAbility) - let savingThrows = try values.decodeIfPresent([Ability].self, forKey: .savingThrows) - let experiencePoints = try values.decodeIfPresent([Int].self, forKey: .experiencePoints) - - // Safely set properties - self.name = name - self.plural = plural - self.hitDice = hitDice - self.startingWealth = startingWealth - - self.descriptiveTraits = descriptiveTraits ?? [:] - self.primaryAbility = primaryAbility ?? [] - self.savingThrows = savingThrows ?? [] - self.experiencePoints = experiencePoints - } - - public func encode(to encoder: Encoder) throws { - var values = encoder.container(keyedBy: CodingKeys.self) - - try values.encode(name, forKey: .name) - try values.encode(plural, forKey: .plural) - try values.encode("\(hitDice)", forKey: .hitDice) - try values.encode("\(startingWealth)", forKey: .startingWealth) - - try values.encodeIfPresent(descriptiveTraits, forKey: .descriptiveTraits) - try values.encodeIfPresent(primaryAbility, forKey: .primaryAbility) - try values.encodeIfPresent(savingThrows, forKey: .savingThrows) - try values.encodeIfPresent(experiencePoints, forKey: .experiencePoints) - } -} diff --git a/RolePlayingCore/RolePlayingCore/Player/Classes.swift b/RolePlayingCore/RolePlayingCore/Player/Classes.swift deleted file mode 100644 index fc33862..0000000 --- a/RolePlayingCore/RolePlayingCore/Player/Classes.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Classes.swift -// RolePlayingCore -// -// Created by Brian Arnold on 11/13/16. -// Copyright © 2016 Brian Arnold. All rights reserved. -// - -// A set of class traits -public struct Classes: Codable { - - public var classes = [ClassTraits]() - public var experiencePoints: [Int]? - - private enum CodingKeys: String, CodingKey { - case classes - case experiencePoints = "experience points" - } - - public func find(_ className: String?) -> ClassTraits? { - return classes.first(where: { $0.name == className }) - } - - public var count: Int { return classes.count } - - public subscript(index: Int) -> ClassTraits? { - get { - return classes[index] - } - } -} diff --git a/RolePlayingCore/RolePlayingCore/Player/Players.swift b/RolePlayingCore/RolePlayingCore/Player/Players.swift deleted file mode 100644 index 0e9a326..0000000 --- a/RolePlayingCore/RolePlayingCore/Player/Players.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// Players.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/18/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import Foundation - -extension Player { - - // TODO: support KeyedArchiver? - func resolveSpecies(from species: Species) throws { - guard let speciesTraits = species.find(self.speciesName) else { - throw RuntimeError("Could not resolve species name \(self.speciesName)") - } - self.speciesTraits = speciesTraits - } - - // TODO: support KeyedArchiver? - func resolveClass(from classes: Classes) throws { - guard let classTraits = classes.find(self.className) else { - throw RuntimeError("Could not resolve class name \(self.className)") - } - self.classTraits = classTraits - } - -} - -public class Players: Codable { - - public var players = [Player]() - - public func resolve(classes: Classes, species: Species) throws { - for player in players { - try player.resolveSpecies(from: species) - try player.resolveClass(from: classes) - } - } - - // TODO: inherit protocols for these - - public var count: Int { return players.count } - - public subscript(index: Int) -> Player? { - get { - return players[index] - } - } - - public func insert(_ player: Player, at index: Int) { - players.insert(player, at: index) - } - - public func remove(at index: Int) { - players.remove(at: index) - } - -} diff --git a/RolePlayingCore/RolePlayingCore/Player/RacialNames.swift b/RolePlayingCore/RolePlayingCore/Player/RacialNames.swift deleted file mode 100644 index 181af93..0000000 --- a/RolePlayingCore/RolePlayingCore/Player/RacialNames.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// RacialNames.swift -// RolePlayingCore -// -// Created by Brian Arnold on 7/8/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import Foundation - -public struct RacialNames: Codable { - - struct FamilyNames: Codable { - let familyType: String - let maleNames: [String] - let femaleNames: [String] - let familyNames: [String]? - let childNames: [String]? - let ethnicities: [String]? - let nicknames: [String]? - let aliases: [String]? - - private enum CodingKeys: String, CodingKey { - case familyType = "family type" - case maleNames = "Male" - case femaleNames = "Female" - case familyNames = "family" - case childNames = "child" - case ethnicities - case nicknames - case aliases - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let familyType: String - let maleNames: [String] - let femaleNames: [String] - - let aliases = try container.decodeIfPresent([String].self, forKey: .aliases) - if aliases != nil { - familyType = "" - maleNames = [] - femaleNames = [] - } else { - familyType = try container.decode(String.self, forKey: .familyType) - maleNames = try container.decode([String].self, forKey: .maleNames) - femaleNames = try container.decode([String].self, forKey: .femaleNames) - } - let familyNames = try container.decodeIfPresent([String].self, forKey: .familyNames) - let childNames = try container.decodeIfPresent([String].self, forKey: .childNames) - let ethnicities = try container.decodeIfPresent([String].self, forKey: .ethnicities) - let nicknames = try container.decodeIfPresent([String].self, forKey: .nicknames) - - self.familyType = familyType - self.maleNames = maleNames - self.femaleNames = femaleNames - self.familyNames = familyNames - self.childNames = childNames - self.ethnicities = ethnicities - self.nicknames = nicknames - self.aliases = aliases - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(familyType, forKey: .familyType) - try container.encode(maleNames, forKey: .maleNames) - try container.encode(femaleNames, forKey: .femaleNames) - try container.encodeIfPresent(familyNames, forKey: .familyNames) - try container.encodeIfPresent(childNames, forKey: .childNames) - try container.encodeIfPresent(ethnicities, forKey: .ethnicities) - try container.encodeIfPresent(nicknames, forKey: .nicknames) - try container.encodeIfPresent(aliases, forKey: .aliases) - } - } - - let names: [String: FamilyNames] - - - static let randomNumberGenerator: RandomNumberGenerator = DefaultRandomNumberGenerator() - - func resolveRacialNames(_ racialTraits: RacialTraits) -> FamilyNames { - guard names[racialTraits.name] == nil, let parentName = racialTraits.parentName else { return names[racialTraits.name]! } - - return names[parentName]! - } - - func resolveAliasNames(_ familyNames: FamilyNames) -> FamilyNames { - guard let aliases = familyNames.aliases else { return familyNames } - - let index = RacialNames.randomNumberGenerator.random(aliases.count) - let randomName = aliases[index] - return names[randomName]! - } - - func resolveGender(_ gender: Player.Gender?) -> Player.Gender { - guard gender == nil else { return gender! } - return RacialNames.randomNumberGenerator.random(2) == 0 ? .male : .female - } - - public func randomName(racialTraits: RacialTraits, gender: Player.Gender?) -> String { - // Determine race or parent race (for subraces) - var familyNames = resolveRacialNames(racialTraits) - familyNames = resolveAliasNames(familyNames) - - let gender = resolveGender(gender) - let genderNames = gender == .male ? familyNames.maleNames : familyNames.femaleNames - let nameGenerator = NameGenerator(genderNames) - return nameGenerator.makeName() - } - - // TODO: family names, child names, nicknames -} diff --git a/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift b/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift deleted file mode 100644 index 493ada4..0000000 --- a/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift +++ /dev/null @@ -1,277 +0,0 @@ -// -// SpeciesTraits.swift -// RolePlayingCore -// -// Created by Brian Arnold on 11/12/16. -// Copyright © 2016-2017 Brian Arnold. All rights reserved. -// - -import Foundation - -public struct SpeciesTraits { - - public var name: String - public var plural: String - public var aliases: [String] - public var descriptiveTraits: [String: String] - public var abilityScoreIncrease: AbilityScores - public var minimumAge: Int! - public var lifespan: Int! - public var alignment: Alignment? - public var baseHeight: Height! - public var heightModifier: Dice - public var baseWeight: Weight! - public var weightModifier: Dice - public var darkVision: Int! - public var speed: Int! - public var hitPointBonus: Int - - public var parentName: String? - public var subspecies: [SpeciesTraits] = [] - - public enum Size { - case small - case medium - case large - } - - public func size(from height: Height) -> Size { - let heightInFeet = height.converted(to: .feet) - switch heightInFeet.value { - case 0..<4: - return .small - case 4..<7: - return .medium - default: - return .large - } - } - - public var size: Size { size(from: baseHeight) } - - public init(name: String, - plural: String, - aliases: [String] = [], - descriptiveTraits: [String: String] = [:], - abilityScoreIncrease: AbilityScores = AbilityScores(), - minimumAge: Int, - lifespan: Int, - alignment: Alignment? = nil, - baseHeight: Height, - heightModifier: Dice = DiceModifier(0), - baseWeight: Weight, - weightModifier: Dice = DiceModifier(0), - darkVision: Int, - speed: Int, - hitPointBonus: Int = 0) { - self.name = name - self.plural = plural - self.aliases = aliases - self.descriptiveTraits = descriptiveTraits - self.abilityScoreIncrease = abilityScoreIncrease - self.minimumAge = minimumAge - self.lifespan = lifespan - self.alignment = alignment - self.baseHeight = baseHeight - self.heightModifier = heightModifier - self.baseWeight = baseWeight - self.weightModifier = weightModifier - self.darkVision = darkVision - self.speed = speed - self.hitPointBonus = hitPointBonus - } -} - -extension SpeciesTraits: Codable { - - private enum CodingKeys: String, CodingKey { - case name - case plural - case aliases - case descriptiveTraits = "descriptive traits" - case abilityScoreIncrease = "ability scores" - case minimumAge = "minimum age" - case lifespan - case alignment - case baseHeight = "base height" - case heightModifier = "height modifier" - case baseWeight = "base weight" - case weightModifier = "weight modifier" - case darkVision = "darkvision" - case speed - case hitPointBonus = "hit point bonus" - case subspecies - } - - public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - - // Try decoding properties - let name = try values.decode(String.self, forKey: .name) - let plural = try values.decode(String.self, forKey: .plural) - let aliases = try values.decodeIfPresent([String].self, forKey: .aliases) - let descriptiveTraits = try values.decodeIfPresent([String:String].self, forKey: .descriptiveTraits) - let abilityScoreIncrease = try values.decodeIfPresent(AbilityScores.self, forKey: .abilityScoreIncrease) - let minimumAge = try values.decodeIfPresent(Int.self, forKey: .minimumAge) - let lifespan = try values.decodeIfPresent(Int.self, forKey: .lifespan) - let alignment = try values.decodeIfPresent(Alignment.self, forKey: .alignment) - let baseHeight = try values.decodeIfPresent(Height.self, forKey: .baseHeight) - let heightModifier = try values.decodeIfPresent(Dice.self, forKey: .heightModifier) - let baseWeight = try values.decodeIfPresent(Weight.self, forKey: .baseWeight) - let weightModifier = try values.decodeIfPresent(Dice.self, forKey: .weightModifier) - let darkVision = try values.decodeIfPresent(Int.self, forKey: .darkVision) - let speed = try values.decodeIfPresent(Int.self, forKey: .speed) - let hitPointBonus = try values.decodeIfPresent(Int.self, forKey: .hitPointBonus) - - // Safely set properties - self.name = name - self.plural = plural - self.aliases = aliases ?? [] - self.descriptiveTraits = descriptiveTraits ?? [:] - self.abilityScoreIncrease = abilityScoreIncrease ?? AbilityScores() - self.minimumAge = minimumAge - self.lifespan = lifespan - self.alignment = alignment - self.baseHeight = baseHeight - self.heightModifier = heightModifier ?? DiceModifier(0) - self.baseWeight = baseWeight - self.weightModifier = weightModifier ?? DiceModifier(0) - self.darkVision = darkVision - self.speed = speed - self.hitPointBonus = hitPointBonus ?? 0 - - // Decode subspecies - if var subspecies = try? values.nestedUnkeyedContainer(forKey: .subspecies) { - while (!subspecies.isAtEnd) { - var subspeciesTraits = try subspecies.decode(SpeciesTraits.self) - subspeciesTraits.blendTraits(from: self) - self.subspecies.append(subspeciesTraits) - } - } - - } - - /// Inherit parent traits, for each trait that is not already set. - public mutating func blendTraits(from parent: SpeciesTraits) { - // Name, plural, aliases and descriptive traits are unique to each set of species traits. - // The rest may be inherited from the parent. - self.parentName = parent.name - - // Combine ability scores together - self.abilityScoreIncrease += parent.abilityScoreIncrease - - if self.minimumAge == nil { - self.minimumAge = parent.minimumAge - } - if self.lifespan == nil { - self.lifespan = parent.lifespan - } - if self.alignment == nil { - self.alignment = parent.alignment - } - if self.baseHeight == nil { - self.baseHeight = parent.baseHeight - } - if self.heightModifier.sides == 0 { - self.heightModifier = parent.heightModifier - } - if self.baseWeight == nil { - self.baseWeight = parent.baseWeight - } - if self.weightModifier.sides == 0 { - self.weightModifier = parent.weightModifier - } - if self.darkVision == nil { - self.darkVision = parent.darkVision - } - if self.speed == nil { - self.speed = parent.speed - } - if self.hitPointBonus == 0 { - self.hitPointBonus = parent.hitPointBonus - } - } - - public func encode(to encoder: Encoder) throws { - var values = encoder.container(keyedBy: CodingKeys.self) - - try values.encode(name, forKey: .name) - try values.encode(plural, forKey: .plural) - try values.encode(aliases, forKey: .aliases) - try values.encode(descriptiveTraits, forKey: .descriptiveTraits) - try values.encode(abilityScoreIncrease, forKey: .abilityScoreIncrease) - try values.encode(minimumAge, forKey: .minimumAge) - try values.encode(lifespan, forKey: .lifespan) - - // Encode alignment using its enum string values, since SpeciesTraits is a type. - if alignment != nil { - try values.encode("\(alignment!)", forKey: .alignment) - } - - try values.encode("\(baseHeight!)", forKey: .baseHeight) - try values.encode("\(heightModifier)", forKey: .heightModifier) - try values.encode("\(baseWeight!)", forKey: .baseWeight) - try values.encode("\(weightModifier)", forKey: .weightModifier) - try values.encode(darkVision, forKey: .darkVision) - try values.encode(speed, forKey: .speed) - try values.encode(hitPointBonus, forKey: .hitPointBonus) - - var subspeciesContainer = values.nestedUnkeyedContainer(forKey: .subspecies) - for subspeciesTraits in subspecies { - try subspeciesTraits.encode(to: &subspeciesContainer, parent: self) - } - } - - public func encode(to container: inout UnkeyedEncodingContainer, parent: SpeciesTraits) throws { - // Name, plural, aliases and descriptive traits are unique to each set of species traits. - // The rest may be inherited from the parent. - var values = container.nestedContainer(keyedBy: CodingKeys.self) - - try values.encode(name, forKey: .name) - try values.encode(plural, forKey: .plural) - if self.aliases.count > 0 { - try values.encode(aliases, forKey: .aliases) - } - if self.descriptiveTraits.count > 0 { - try values.encode(descriptiveTraits, forKey: .descriptiveTraits) - } - - // Un-combine ability scores - if self.abilityScoreIncrease != parent.abilityScoreIncrease { - let delta = self.abilityScoreIncrease - parent.abilityScoreIncrease - try values.encode(delta, forKey: .abilityScoreIncrease) - } - - if self.minimumAge != parent.minimumAge { - try values.encode(self.minimumAge, forKey: .minimumAge) - } - if self.lifespan != parent.lifespan { - try values.encode(self.lifespan, forKey: .lifespan) - } - if self.alignment != nil && self.alignment != parent.alignment { - try values.encode("\(alignment!)", forKey: .alignment) - } - if self.baseHeight != parent.baseHeight { - try values.encode("\(self.baseHeight!)", forKey: .baseHeight) - } - if self.heightModifier.sides != parent.heightModifier.sides { - try values.encode("\(self.heightModifier)", forKey: .heightModifier) - } - if self.baseWeight != parent.baseWeight { - try values.encode("\(self.baseWeight!)", forKey: .baseWeight) - } - if self.weightModifier.sides != parent.weightModifier.sides { - try values.encode("\(self.weightModifier)", forKey: .weightModifier) - } - if self.darkVision != parent.darkVision { - try values.encode(self.darkVision, forKey: .darkVision) - } - if self.speed != parent.speed { - try values.encode(self.speed, forKey: .speed) - } - if self.hitPointBonus != parent.hitPointBonus { - try values.encode(self.hitPointBonus, forKey: .hitPointBonus) - } - } - -} diff --git a/RolePlayingCore/RolePlayingCore/RolePlayingCore.h b/RolePlayingCore/RolePlayingCore/RolePlayingCore.h deleted file mode 100644 index 108b903..0000000 --- a/RolePlayingCore/RolePlayingCore/RolePlayingCore.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// RolePlayingCore.h -// RolePlayingCore -// -// Created by Brian Arnold on 1/1/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -#import - -//! Project version number for RolePlayingCore. -FOUNDATION_EXPORT double RolePlayingCoreVersionNumber; - -//! Project version string for RolePlayingCore. -FOUNDATION_EXPORT const unsigned char RolePlayingCoreVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/RolePlayingCore/RolePlayingCoreTests/AbilityTests.swift b/RolePlayingCore/RolePlayingCoreTests/AbilityTests.swift deleted file mode 100644 index 22b7124..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/AbilityTests.swift +++ /dev/null @@ -1,330 +0,0 @@ -// -// AbilityTests.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/8/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -@testable import RolePlayingCore - -class AbilityTests: XCTestCase { - - func testStringAbbreviation() { - do { - let string = "Strength" - XCTAssertEqual(string.abbreviated, "STR", "Strength abbreviated") - } - - do { - let string = "a" - XCTAssertEqual(string.abbreviated, "A", "A abbreviated") - } - - do { - let empty = "" - XCTAssertEqual(empty.abbreviated, "", "empty abbreviated") - } - } - - func testIntScoreModifier() { - let expectedScores = [-5, -4, -4, -3, -3, -2, -2, -1, -1, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10] - - for score in 1...30 { - let expectedScore = expectedScores[score - 1] - XCTAssertEqual(score.scoreModifier, expectedScore, "score \(score) modifier") - } - } - - func testAbilityStruct() { - let strength = Ability("Strength") - XCTAssertEqual(strength.name, "Strength", "strength name") - XCTAssertEqual(strength.abbreviated, "STR", "strength name abbreviated") - } - - func testAbilityEquatable() { - let strength = Ability("Strength") - XCTAssertTrue(strength == Ability("Strength"), "strength equatable") - } - - func testAbilityHashable() { - let strength = Ability("Strength") - let strengthClone = Ability("Strength") - let intelligence = Ability("Intelligence") - - var map: [Ability: Int] = [strength: 12, intelligence: 3] - map[strengthClone] = 9 - XCTAssertEqual(map[strength], 9, "strength hashable") - } - - func testAbilityEncodable() { - struct AbilityContainer: Codable { - let ability: Ability - } - let abilityScores = AbilityContainer(ability: Ability("Strength")) - let encoder = JSONEncoder() - do { - let encoded = try encoder.encode(abilityScores) - let dictionary = try JSONSerialization.jsonObject(with: encoded, options: .allowFragments) as? [String: String] - XCTAssertNotNil(dictionary, "serialized strength should be non-nil") - let strength = dictionary?["ability"] - XCTAssertEqual(strength, "Strength", "Round-trip for Ability should be Strength") - } - catch let error { - XCTFail("encoding threw an error: \(error)") - } - } - - func testAbilityDecodable() { - let traits = """ - { - "ability": "Strength" - } - """.data(using: .utf8)! - struct AbilityContainer: Decodable { - let ability: Ability - } - - let decoder = JSONDecoder() - do { - let decoded = try decoder.decode(AbilityContainer.self, from: traits) - XCTAssertEqual(decoded.ability.name, "Strength", "decoded ability name") - } - catch let error { - XCTFail("decoding threw an error: \(error)") - } - } - - func testNonMutableAbilityScores() { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - let abilityScores = AbilityScores([brawn: 9, reflexes: 12, stamina: 15]) - - XCTAssertEqual(abilityScores.count, 3, "ability scores count") - XCTAssertEqual(abilityScores[brawn], 9, "ability scores brawn") - XCTAssertEqual(abilityScores[reflexes], 12, "ability scores reflexes") - XCTAssertEqual(abilityScores[stamina], 15, "ability scores stamina") - - let expectedModifiers = AbilityScores([brawn: -1, reflexes: 1, stamina: 2]) - XCTAssertEqual(abilityScores.modifiers, expectedModifiers, "ability scores modifiers") - } - - func testMutableAbilityScores() { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - var abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - - // Change 2 of the 3 scores - abilityScores[reflexes] = 11 - abilityScores[stamina] = 18 - - // Verify that 2 of the 3 scores changed - XCTAssertEqual(abilityScores[brawn], 8, "ability scores reflexes") - XCTAssertEqual(abilityScores[reflexes], 11, "ability scores reflexes") - XCTAssertEqual(abilityScores[stamina], 18, "ability scores reflexes") - - // Check that modifiers reflect mutated scores - let expectedModifiers = AbilityScores([brawn: -1, reflexes: 0, stamina: 4]) - XCTAssertEqual(abilityScores.modifiers, expectedModifiers, "ability scores modifiers") - - // Verify that a score can't be nil'd out - abilityScores[stamina] = nil - XCTAssertEqual(abilityScores[stamina], 18, "ability scores can't be set to nil") - - let invalidAbility = Ability("Charm") - abilityScores[invalidAbility] = 8 - XCTAssertNil(abilityScores[invalidAbility], "invalid ability should not set a score") - } - - func testAbilityScoresDecodable() { - // Test with implicit [String: Int] as from JSON - do { - let traits = """ - {"Strength": 12, "Intelligence": 8} - """.data(using: .utf8)! - - let decoder = JSONDecoder() - let abilityScores = try? decoder.decode(AbilityScores.self, from: traits) - XCTAssertNotNil(abilityScores, "ability scores should be non-nil") - - let strength = Ability("Strength") - let intelligence = Ability("Intelligence") - - XCTAssertEqual(abilityScores?[strength], 12, "ability scores dictionary strength") - XCTAssertEqual(abilityScores?[intelligence], 8, "ability scores dictionary intelligence") - } - } - - func testAbilityScoresEncodable() { - let abilityScores = AbilityScores([Ability("Brawn"): 12, Ability("Charm"): 3]) - - let encoder = JSONEncoder() - do { - let encoded = try encoder.encode(abilityScores) - let serialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String:Int] - let brawn = serialized?["Brawn"] - let charm = serialized?["Charm"] - XCTAssertEqual(brawn, 12, "encoded brawn") - XCTAssertEqual(charm, 3, "encoded charm") - } - catch let error { - XCTFail("decoding threw an error: \(error)") - } - } - - func testAbilityScoreKey() { - // Housekeeping: code coverage for AbilityKey - do { - let abilityKey = AbilityScores.AbilityKey(stringValue: "Wisdom")! - XCTAssertNil(abilityKey.intValue, "AbilityKey does not use intValue") - } - - do { - let abilityKey = AbilityScores.AbilityKey(intValue: 2) - XCTAssertNil(abilityKey, "AbilityKey does not use intValue") - } - } - - func testAddingAbilityScores() { - // Test adding scores with modifiers using = ... + ... - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - let combinedScores = abilityScores + abilityScores.modifiers - - XCTAssertEqual(combinedScores[brawn], 7, "adding ability scores brawn") - XCTAssertEqual(combinedScores[reflexes], 14, "adding ability scores reflexes") - XCTAssertEqual(combinedScores[stamina], 20, "adding ability scores stamina") - } - - // Test adding one score using += ... - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - var abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - let oneScore = AbilityScores([reflexes: -3]) - abilityScores += oneScore - - XCTAssertEqual(abilityScores[brawn], 8, "adding ability scores brawn") - XCTAssertEqual(abilityScores[reflexes], 10, "adding ability scores reflexes") - XCTAssertEqual(abilityScores[stamina], 17, "adding ability scores stamina") - } - - // Test that adding unrelated scores has no effect - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - - let intelligence = Ability("Intelligence") - let wisdom = Ability("Wisdom") - let unrelatedScores = AbilityScores([intelligence: 14, wisdom: 5]) - let combinedScores = abilityScores + unrelatedScores - - XCTAssertEqual(combinedScores.count, 3, "adding ability scores count") - - XCTAssertEqual(combinedScores[brawn], 8, "adding ability scores brawn") - XCTAssertEqual(combinedScores[reflexes], 13, "adding ability scores reflexes") - XCTAssertEqual(combinedScores[stamina], 17, "adding ability scores stamina") - } - } - - func testSubtractingAbilityScores() { - // Test adding scores with modifiers using = ... + ... - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - let combinedScores = abilityScores - abilityScores.modifiers - - XCTAssertEqual(combinedScores[brawn], 9, "adding ability scores brawn") - XCTAssertEqual(combinedScores[reflexes], 12, "adding ability scores reflexes") - XCTAssertEqual(combinedScores[stamina], 14, "adding ability scores stamina") - } - - // Test adding one score using += ... - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - var abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - let oneScore = AbilityScores([reflexes: -3]) - abilityScores -= oneScore - - XCTAssertEqual(abilityScores[brawn], 8, "adding ability scores brawn") - XCTAssertEqual(abilityScores[reflexes], 16, "adding ability scores reflexes") - XCTAssertEqual(abilityScores[stamina], 17, "adding ability scores stamina") - } - - // Test that adding unrelated scores has no effect - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - - let intelligence = Ability("Intelligence") - let wisdom = Ability("Wisdom") - let unrelatedScores = AbilityScores([intelligence: 14, wisdom: 5]) - let combinedScores = abilityScores - unrelatedScores - - XCTAssertEqual(combinedScores.count, 3, "adding ability scores count") - - XCTAssertEqual(combinedScores[brawn], 8, "adding ability scores brawn") - XCTAssertEqual(combinedScores[reflexes], 13, "adding ability scores reflexes") - XCTAssertEqual(combinedScores[stamina], 17, "adding ability scores stamina") - } - } - - func testDefaultAbilityScores() { - // Test default ability scores - do { - let abilityScores = AbilityScores() - - XCTAssertEqual(abilityScores.count, 6, "default ability scores count") - - // Test names and values - let abilityNames = ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"] - for ability in abilityScores.abilities { - XCTAssertTrue(abilityNames.contains(ability.name), "default ability name") - XCTAssertEqual(abilityScores[ability], 0, "default ability score 0") - } - } - - // Test default ability scores with non-default abilities - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - let abilityScores = AbilityScores(defaults: [brawn, reflexes, stamina]) - XCTAssertEqual(abilityScores.count, 3, "default ability scores count") - - // Test values via keys and values - for ability in abilityScores.abilities { - XCTAssertEqual(abilityScores[ability], 0, "default ability score 0") - } - for value in abilityScores.values { - XCTAssertEqual(value, 0, "default ability score 0") - } - } - } - -} diff --git a/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift b/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift deleted file mode 100644 index db90c75..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift +++ /dev/null @@ -1,407 +0,0 @@ -// -// AlignmentTests.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/11/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import Foundation - -import XCTest - -import RolePlayingCore - -class AlignmentTests: XCTestCase { - - func testEthics() { - // Test the literal values - do { - XCTAssertEqual(Ethics.lawful.rawValue, "Lawful", "lawful string") - XCTAssertEqual(Ethics.neutral.rawValue, "Neutral", "neutral string") - XCTAssertEqual(Ethics.chaotic.rawValue, "Chaotic", "chaotic string") - - XCTAssertEqual(Ethics.lawful.value, 1.0, "lawful value") - XCTAssertEqual(Ethics.neutral.value, 0.0, "neutral value") - XCTAssertEqual(Ethics.chaotic.value, -1.0, "chaotic value") - } - - // Test creation by raw value - do { - let lawful = Ethics(rawValue: "Lawful") - XCTAssertNotNil(lawful, "lawful should be non-nil") - XCTAssertEqual(lawful, Ethics.lawful, "lawful enum") - XCTAssertEqual(lawful?.value, 1.0, "lawful value") - - let neutral = Ethics(rawValue: "Neutral") - XCTAssertNotNil(neutral, "neutral should be non-nil") - XCTAssertEqual(neutral, Ethics.neutral, "neutral enum") - XCTAssertEqual(neutral?.value, 0.0, "neutral value") - - let chaotic = Ethics(rawValue: "Chaotic") - XCTAssertNotNil(chaotic, "chaotic should be non-nil") - XCTAssertEqual(chaotic, Ethics.chaotic, "chaotic enum") - XCTAssertEqual(chaotic?.value, -1.0, "chaotic value") - } - - // Test creation by Double value - do { - let lawful = Ethics(0.8) - XCTAssertNotNil(lawful, "lawful should be non-nil") - XCTAssertEqual(lawful, Ethics.lawful, "lawful enum") - XCTAssertEqual(lawful.value, 1.0, "lawful value") - - let neutral = Ethics(-0.1) - XCTAssertNotNil(neutral, "neutral should be non-nil") - XCTAssertEqual(neutral, Ethics.neutral, "neutral enum") - XCTAssertEqual(neutral.value, 0.0, "neutral value") - - let chaotic = Ethics(-0.34) - XCTAssertNotNil(chaotic, "chaotic should be non-nil") - XCTAssertEqual(chaotic, Ethics.chaotic, "chaotic enum") - XCTAssertEqual(chaotic.value, -1.0, "chaotic value") - } - } - - func testMorals() { - // Test the literal values - do { - XCTAssertEqual(Morals.good.rawValue, "Good", "good string") - XCTAssertEqual(Morals.neutral.rawValue, "Neutral", "neutral string") - XCTAssertEqual(Morals.evil.rawValue, "Evil", "evil string") - - XCTAssertEqual(Morals.good.value, 1.0, "good value") - XCTAssertEqual(Morals.neutral.value, 0.0, "neutral value") - XCTAssertEqual(Morals.evil.value, -1.0, "evil value") - } - - // Test creation by raw value - do { - let good = Morals(rawValue: "Good") - XCTAssertNotNil(good, "good should be non-nil") - XCTAssertEqual(good, Morals.good, "good enum") - XCTAssertEqual(good?.value, 1.0, "good value") - - let neutral = Morals(rawValue: "Neutral") - XCTAssertNotNil(neutral, "neutral should be non-nil") - XCTAssertEqual(neutral, Morals.neutral, "neutral enum") - XCTAssertEqual(neutral?.value, 0.0, "neutral value") - - let evil = Morals(rawValue: "Evil") - XCTAssertNotNil(evil, "evil should be non-nil") - XCTAssertEqual(evil, Morals.evil, "evil enum") - XCTAssertEqual(evil?.value, -1.0, "evil value") - } - - // Test creation by Double value - do { - let good = Morals(0.334) - XCTAssertNotNil(good, "good should be non-nil") - XCTAssertEqual(good, Morals.good, "good enum") - XCTAssertEqual(good.value, 1.0, "good value") - - let neutral = Morals(0.2) - XCTAssertNotNil(neutral, "neutral should be non-nil") - XCTAssertEqual(neutral, Morals.neutral, "neutral enum") - XCTAssertEqual(neutral.value, 0.0, "neutral value") - - let evil = Morals(-0.9) - XCTAssertNotNil(evil, "evil should be non-nil") - XCTAssertEqual(evil, Morals.evil, "evil enum") - XCTAssertEqual(evil.value, -1.0, "evil value") - } - - } - - func testAlignmentType() { - // Test creation by raw value - do { - let neutralGood = Alignment.Kind(.neutral, .good) - XCTAssertEqual(neutralGood.description, "Neutral Good", "description") - - let neutralEvil = Alignment.Kind(.neutral, .evil) - XCTAssertEqual(neutralEvil.description, "Neutral Evil", "description") - - let chaoticNeutral = Alignment.Kind(.chaotic, .neutral) - XCTAssertEqual(chaoticNeutral.description, "Chaotic Neutral", "description") - - XCTAssertEqual(neutralGood, Alignment.Kind(.neutral, .good), "equatable") - XCTAssertNotEqual(neutralGood, neutralEvil, "equatable") - XCTAssertNotEqual(neutralEvil, chaoticNeutral, "equatable") - } - - // Test creation by Double value - do { - let lawfulNeutral = Alignment(ethics: 0.7, morals: 0.0) - XCTAssertEqual(lawfulNeutral.description, "Lawful Neutral", "description") - - let neutral = Alignment(ethics: 0.1, morals: -0.2) - XCTAssertEqual(neutral.description, "Neutral", "description") - - let chaoticGood = Alignment(ethics: -1, morals: 1) - XCTAssertEqual(chaoticGood.description, "Chaotic Good", "description") - } - } - - func testAlignment() { - // Test creation by raw value - do { - let lawfulGood = Alignment(.lawful, .good) - XCTAssertEqual(lawfulGood.description, "Lawful Good", "description") - - let neutralGood = Alignment(.neutral, .good) - XCTAssertEqual(neutralGood.description, "Neutral Good", "description") - - let chaoticNeutral = Alignment(.chaotic, .neutral) - XCTAssertEqual(chaoticNeutral.description, "Chaotic Neutral", "description") - - XCTAssertEqual(lawfulGood, Alignment(.lawful, .good), "equatable") - XCTAssertNotEqual(lawfulGood, neutralGood, "equatable") - XCTAssertNotEqual(neutralGood, chaoticNeutral, "equatable") - } - - // Test creation by Double value - do { - let lawfulNeutral = Alignment(ethics: 0.9, morals: 0.0) - XCTAssertEqual(lawfulNeutral.description, "Lawful Neutral", "description") - - let neutral = Alignment(ethics: 0.1, morals: -0.1) - XCTAssertEqual(neutral.description, "Neutral", "description") - - let chaoticEvil = Alignment(ethics: -1, morals: -1) - XCTAssertEqual(chaoticEvil.description, "Chaotic Evil", "description") - } - - // Test changing alignment - do { - var alignment = Alignment(.neutral, .evil) - XCTAssertEqual(alignment.ethics, 0, "ethics value") - XCTAssertEqual(alignment.morals, -1, "morals value") - XCTAssertEqual(alignment.kind.ethics, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(alignment.kind.morals, Morals.evil, "morals enumeration") - - - alignment.morals += 0.8 - XCTAssertEqual(alignment.morals, -0.2, accuracy: 0.00001, "morals value") - XCTAssertEqual(alignment.kind.ethics, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(alignment.kind.morals, Morals.neutral, "morals enumeration") - - // Try to exceed 1.0 and confirm the ethics value did not change - alignment.ethics += 5.4 - XCTAssertEqual(alignment.ethics, 0, "ethics value") - XCTAssertEqual(alignment.kind.ethics, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(alignment.kind.morals, Morals.neutral, "morals enumeration") - - alignment.morals += 0.8 - XCTAssertEqual(alignment.morals, 0.6, accuracy: 0.00001, "morals value") - XCTAssertEqual(alignment.kind.ethics, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(alignment.kind.morals, Morals.good, "morals enumeration") - - // Try to exceed 1.0 and confirm that morals value did not change - alignment.morals += 0.8 - XCTAssertEqual(alignment.morals, 0.6, accuracy: 0.00001, "morals value") - XCTAssertEqual(alignment.kind.ethics, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(alignment.kind.morals, Morals.good, "morals enumeration") -} - } - - func testAlignmentParsing() { - // Test initializing from valid string - do { - let neutralGood = "Neutral Good".parseAlignment - XCTAssertNotNil(neutralGood, "alignment should be non-nil") - XCTAssertEqual(neutralGood?.0, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(neutralGood?.1, Morals.good, "morals enumeration") - - let neutral = "Neutral".parseAlignment - XCTAssertNotNil(neutral, "alignment should be non-nil") - XCTAssertEqual(neutral?.0, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(neutral?.1, Morals.neutral, "morals enumeration") - } - - // Test initializing from partial valid string - do { - let chaotic = "Chaotic".parseAlignment - XCTAssertNotNil(chaotic, "alignment should be non-nil") - XCTAssertEqual(chaotic?.0, Ethics.chaotic, "ethics enumeration") - XCTAssertEqual(chaotic?.1, Morals.neutral, "morals enumeration") - - let good = "Good".parseAlignment - XCTAssertNotNil(good, "alignment should be non-nil") - XCTAssertEqual(good?.0, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(good?.1, Morals.good, "morals enumeration") - } - - // Test initializing from bad string - do { - let tooManyWords = "Neutral Neutral Neutral".parseAlignment - XCTAssertNil(tooManyWords, "too many words should be nil") - - let mismatchedWords = "Foo Bar".parseAlignment - XCTAssertNil(mismatchedWords, "mismatched words should be nil") - - let wrongWord = "Cat".parseAlignment - XCTAssertNil(wrongWord, "wrong word should be nil") - } - - } - - func testAlignmentDictionaryDecoding() { - let decoder = JSONDecoder() - - // Test initializing from dictionary of doubles - do { - let lawfulNeutralTraits = """ - { - "ethics": 1, - "morals": 0.2 - } - """.data(using: .utf8)! - let lawfulNeutral = try? decoder.decode(Alignment.self, from: lawfulNeutralTraits) - XCTAssertNotNil(lawfulNeutral, "alignment should be non-nil") - XCTAssertEqual(lawfulNeutral?.kind.ethics, Ethics.lawful, "ethics enumeration") - XCTAssertEqual(lawfulNeutral?.kind.morals, Morals.neutral, "morals enumeration") - - } - - // Test initializing from dictionary of strings - do { - let chaoticNeutralTraits = """ - { - "ethics": "Chaotic", - "morals": "Neutral" - } - """.data(using: .utf8)! - let chaoticNeutral = try? decoder.decode(Alignment.self, from: chaoticNeutralTraits) - XCTAssertNotNil(chaoticNeutral, "alignment should be non-nil") - XCTAssertEqual(chaoticNeutral?.kind.ethics, Ethics.chaotic, "ethics enumeration") - XCTAssertEqual(chaoticNeutral?.kind.morals, Morals.neutral, "morals enumeration") - } - - // Test initializing from bad dictionary keys - do { - let badTraitKeys = """ - { - "Howdy": "Lawful", - "Doody": "Evil" - } - """.data(using: .utf8)! - let badTrait = try? decoder.decode(Alignment.self, from: badTraitKeys) - XCTAssertNil(badTrait, "bad trait keys should be nil") - } - - // Test initializing from bad dictionary values - do { - let notStringTraits = """ - { - "ethics": ["Chaotic"], - "morals": ["Neutral"] - } - """.data(using: .utf8)! - let notString = try? decoder.decode(Alignment.self, from: notStringTraits) - XCTAssertNil(notString, "non-string traits should be nil") - - let notValidTraits = """ - { - "ethics": "Choatic", - "morals": "Eliv" - } - """.data(using: .utf8)! - let notValid = try? decoder.decode(Alignment.self, from: notValidTraits) - XCTAssertNil(notValid, "non-valid traits should be nil") - } - } - - func testAlignmentStringDecoding() { - let decoder = JSONDecoder() - - struct AlignmentContainer: Decodable { - let alignment: Alignment - } - - // Test string values - do { - let stringTrait = """ - { - "alignment": "Chaotic Evil" - } - """.data(using: .utf8)! - do { - let decoded = try decoder.decode(AlignmentContainer.self, from: stringTrait) - XCTAssertEqual("\(decoded.alignment)", "Chaotic Evil", "decoded string should be round-trip") - } - catch let error { - XCTFail("Error decoding string trait: \(error)") - } - - let notValidTrait = """ - { - "alignment": "Hello" - } - """.data(using: .utf8)! - do { - let notValid = try decoder.decode(AlignmentContainer.self, from: notValidTrait) - XCTAssertNil(notValid, "non-valid traits should be nil") - } - catch let error { - print("Successfully failed decoding invalid string trait: \(error)") - } - } - } - - func testAlignmentEncoding() { - let encoder = JSONEncoder() - - // Test default encoding - do { - struct AlignmentContainer: Encodable { - let alignment: Alignment - } - let container = AlignmentContainer(alignment: Alignment(.chaotic, .good)) - let encoded = try encoder.encode(container) - let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) - - if let dictionary = deserialized as? [String: Any] { - XCTAssertNotNil(dictionary["alignment"] as? [String: Double], "player traits round trip alignment") - if let alignment = dictionary["alignment"] as? [String: Double] { - XCTAssertEqual(alignment["ethics"], -1, "player traits round trip alignment ethics") - XCTAssertEqual(alignment["morals"], 1, "player traits round trip alignment ethics") - } - } - } - catch let error { - XCTFail("Encoding Alignment failed, error: \(error)") - } - - // Test stringified encoding - do { - struct AlignmentContainer: Encodable { - let alignment: Alignment - - enum CodingKeys: String, CodingKey { - case alignment - } - - // Stringify alignment - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode("\(alignment)", forKey: .alignment) - } - } - - let container = AlignmentContainer(alignment: Alignment(.chaotic, .good)) - let encoded = try! encoder.encode(container) - let deserialized = try? JSONSerialization.jsonObject(with: encoded, options: []) - XCTAssertNotNil(deserialized, "player traits round trip") - - if let dictionary = deserialized as? [String: String] { - if let alignment = dictionary["alignment"] { - XCTAssertEqual(alignment, "Chaotic Good", "player traits round trip alignment ethics") - } - } - } - - - } - -} diff --git a/RolePlayingCore/RolePlayingCoreTests/CharacterGeneratorTests.swift b/RolePlayingCore/RolePlayingCoreTests/CharacterGeneratorTests.swift deleted file mode 100644 index d09fd1c..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/CharacterGeneratorTests.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// CharacterGeneratorTests.swift -// RolePlayingCoreTests -// -// Created by Brian Arnold on 7/9/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -import RolePlayingCore - -class CharacterGeneratorTests: XCTestCase { - - let bundle = Bundle(for: CharacterGeneratorTests.self) - - let sampleSize = 256 - - func testCharacterGenerator() { - do { - let configuration = try Configuration("TestCharacterGenerator", from: bundle) - let characterGenerator = try CharacterGenerator(configuration, from: bundle) - - for _ in 0.. - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/RolePlayingCore/RolePlayingCoreTests/JSONFileTests.swift b/RolePlayingCore/RolePlayingCoreTests/JSONFileTests.swift deleted file mode 100644 index 01efffb..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/JSONFileTests.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// JSONFileTests.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/17/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -import RolePlayingCore - -struct JSONFileData: Codable { - let boolValue: Bool - - struct DictionaryValue: Codable { - let stringValue: String - let doubleValue: Double - let arrayValue: [Int] - } - let dictionaryValue: DictionaryValue -} - -struct AnyFileData: Codable { - // No-op -} - -class JSONFileTests: XCTestCase { - - let decoder = JSONDecoder() - - func testJSON() { - // This test file has all of the basic elements of a JSON file - let bundle = Bundle(for: JSONFileTests.self) - do { - let jsonData = try bundle.loadJSON("JSONFile") - let jsonObject = try decoder.decode(JSONFileData.self, from: jsonData) - - // Test for contents of JSON file - - let bool = jsonObject.boolValue - XCTAssertNotNil(bool, "bool should be non-nil") - XCTAssertEqual(bool, true, "bool should be true") - - let dictionary = jsonObject.dictionaryValue - - let string = dictionary.stringValue - XCTAssertEqual(string, "foo", "string should be \"foo\"") - - let double = dictionary.doubleValue - XCTAssertEqual(double, 2.1, "double should be 2.1") - - let array = dictionary.arrayValue - XCTAssertEqual(array, [2, 3], "array should be [2, 3]") - } - catch let error { - XCTFail("loadJSON should not throw an error: \(error)") - } - } - - func testMissingJSON() { - // This test file is not present in the bundle. - let bundle = Bundle(for: JSONFileTests.self) - do { - let jsonObject = try bundle.loadJSON("MissingJSONFile") - XCTAssertNil(jsonObject, "should not get here") - } - catch let error { - XCTAssertTrue(error is ServiceError, "expected ServiceError.runtimeError, got \(error)") - } - } - - func testInvalidJSON() { - // This test file contains errors in formatting. - let bundle = Bundle(for: JSONFileTests.self) - do { - let jsonData = try bundle.loadJSON("InvalidJSONFile") - let jsonObject = try decoder.decode(AnyFileData.self, from: jsonData) - XCTAssertNil(jsonObject, "should not get here") - } - catch let error { - print("Successfully caught \(error)") - // OK we got here. it's an error NSCocoaErrorDomain Code 3840 "No value for key in object around character 41." - } - } - - func testHalfBakedJSON() { - // This test file lacks a dictionary at the root. - let bundle = Bundle(for: JSONFileTests.self) - do { - let jsonData = try bundle.loadJSON("HalfBakedJSONFile") - let jsonObject = try decoder.decode(AnyFileData.self, from: jsonData) - XCTAssertNil(jsonObject, "should not get here") - } - catch let error { - print("Successfully caught \(error)") - // OK we got here. it's an error NSCocoaErrorDomain Code 3840 "JSON text did not start with an array or object and option to allow fragments not set." - } - } -} diff --git a/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift b/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift deleted file mode 100644 index 1bc8dac..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift +++ /dev/null @@ -1,620 +0,0 @@ -// -// PlayerTests.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/18/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -import RolePlayingCore - -class PlayerTests: XCTestCase { - - var humanTraits: Data! - var human: SpeciesTraits! - var fighterTraits: Data! - var fighter: ClassTraits! - - override func setUp() { - // TODO: Need to initialize UnitCurrency before creating Money instances in Player class. - // Only load once. TODO: this has a side effect on other unit tests: currencies are already loaded. - let bundle = Bundle(for: PlayerTests.self) - let decoder = JSONDecoder() - let data = try! bundle.loadJSON("TestCurrencies") - _ = try! decoder.decode(Currencies.self, from: data) - - self.fighterTraits = """ - { - "name": "Fighter", - "plural": "Fighters", - "hit dice": "d10", - "primary ability": ["Strength"], - "alternate primary ability": ["Dexterity"], - "saving throws": ["Strength", "Constitution"], - "starting wealth": "5d4x10", - "experience points": [0, 300, 900, 2700] - } - """.data(using: .utf8) - self.fighter = try! decoder.decode(ClassTraits.self, from: self.fighterTraits) - - self.humanTraits = """ - { - "name": "Human", - "plural": "Humans", - "ability scores": {"Strength": 1, "Dexterity": 1, "Constitution": 1, "Intelligence": 1, "Wisdom": 1, "Charisma": 1}, - "minimum age": 18, - "lifespan": 90, - "base height": "4'8\\"", - "height modifier": "2d10", - "base weight": 110, - "weight modifier": "2d4", - "speed": 30, - "languages": ["Common"], - "extra languages": 1 - } - """.data(using: .utf8) - self.human = try! decoder.decode(SpeciesTraits.self, from: self.humanTraits) - } - - func testPlayer() { - let decoder = JSONDecoder() - - // Test construction from types - do { - let player = Player("Frodo", speciesTraits: human, classTraits: fighter, gender: .female, alignment: Alignment(.lawful, .neutral)) - XCTAssertEqual(player.name, "Frodo", "player name") - XCTAssertEqual(player.className, "Fighter", "class name") - XCTAssertEqual(player.speciesName, "Human", "species name") - - XCTAssertEqual(player.descriptiveTraits.count, 0, "descriptiveTraits") - - XCTAssertEqual(player.gender, Player.Gender.female, "gender") - XCTAssertEqual(player.alignment, Alignment(.lawful, .neutral), "alignment") - - // Abilities is scores plus species modifiers, so + 1 - for key in player.abilities.abilities { - let score = player.abilities[key]! - XCTAssertTrue((4...19).contains(score), "ability score \(score) for \(key)") - } - - // I do the maths - XCTAssertTrue((4.66666...6.33334).contains(player.height.value), "height \(player.height.value)") - - // 110 + 2...8 * 2...20 - XCTAssertTrue((114...270).contains(player.weight.value), "weight \(player.weight.value)") - - XCTAssertTrue((1...10).contains(player.maximumHitPoints), "maximum hit points") - XCTAssertEqual(player.maximumHitPoints, player.currentHitPoints, "current hit points") - XCTAssertEqual("\(player.classTraits.hitDice)", "d10", "hit dice") - XCTAssertEqual(player.experiencePoints, 0, "experience points") - XCTAssertEqual(player.level, 1, "level") - - XCTAssertTrue((50...200).contains(player.money.value), "money \(player.money.value)") - - XCTAssertEqual(player.proficiencyBonus, 2, "proficiency bonus") - } - - // Test construction from minimum required traits - do { - let playerTraits = """ - { - "name": "Bilbo", - "species": "Human", - "class": "Fighter", - "gender": "Male", - "height": "3'9\\"", - "weight": 120, - "ability scores": {"Dexterity": 13}, - "money": 130, - "maximum hit points": 10 - } - """.data(using: .utf8)! - - do { - let player = try decoder.decode(Player.self, from: playerTraits) - player.speciesTraits = human - player.classTraits = fighter - - XCTAssertEqual(player.name, "Bilbo", "player name") - XCTAssertEqual(player.className, "Fighter", "class name") - XCTAssertEqual(player.speciesName, "Human", "species name") - - XCTAssertEqual(player.gender, Player.Gender.male, "gender") - XCTAssertNil(player.alignment, "alignment") - - XCTAssertEqual(player.height.value, 3.75, "height") - XCTAssertEqual(player.weight.value, 120, "weight") - - XCTAssertEqual(player.maximumHitPoints, 10, "maximum hit points") - XCTAssertEqual(player.maximumHitPoints, player.currentHitPoints, "current hit points") - - XCTAssertEqual(player.experiencePoints, 0, "experience points") - XCTAssertEqual(player.level, 1, "level") - - XCTAssertEqual(player.money.value, 130, "money") - - } - catch let error { - XCTFail("decode player failed, error: \(error)") - } - } - - // Test construction with optional traits - do { - let playerTraits = """ - { - "name": "Bilbo", - "species": "Human", - "class": "Fighter", - "alignment": "Lawful Evil", - "height": "3'9\\"", - "weight": 120, - "ability scores": {"Strength": 12}, - "money": 130, - "maximum hit points": 10, - "experience points": 2300, - "level": 2 - } - """.data(using: .utf8)! - - do { - let player = try decoder.decode(Player.self, from: playerTraits) - player.speciesTraits = human - player.classTraits = fighter - - XCTAssertNil(player.gender, "gender") - XCTAssertEqual(player.alignment, Alignment(.lawful, .evil), "alignment") - - XCTAssertEqual(player.canLevelUp, true, "level up") - XCTAssertEqual("\(player.hitDice)", "2d10", "hit dice") - player.levelUp() - XCTAssertEqual(player.level, 3, "level") - XCTAssertTrue(player.maximumHitPoints > 15, "experience points") - - XCTAssertEqual(player.canLevelUp, false, "level up") - XCTAssertEqual("\(player.hitDice)", "3d10", "hit dice") - - player.levelUp() - XCTAssertEqual(player.level, 3, "level") - } - catch let error { - XCTFail("decode player failed, error: \(error)") - } - } - } - - func testPlayerRoundTrip() { - let playerTraits = """ - { - "name": "Bilbo", - "species": "Human", - "class": "Fighter", - "gender": "Male", - "alignment": "Neutral Good", - "height": "3'9\\"", - "weight": 120, - "ability scores": {"Dexterity": 13}, - "money": 130, - "maximum hit points": 20, - "current hit points": 9, - "level": 2 - } - """.data(using: .utf8)! - - let decoder = JSONDecoder() - let player = try? decoder.decode(Player.self, from: playerTraits) - let encoder = JSONEncoder() - let encodedPlayer = try! encoder.encode(player) - let encoded = try? JSONSerialization.jsonObject(with: encodedPlayer, options: []) - XCTAssertNotNil(encoded, "player traits round trip") - - if let encoded = encoded as? [String: Any] { - XCTAssertEqual(encoded["name"] as? String, "Bilbo", "player traits round trip name") - XCTAssertEqual(encoded["gender"] as? String, "Male", "player traits round trip gender") - XCTAssertNotNil(encoded["alignment"] as? [String: Double], "player traits round trip alignment") - if let alignment = encoded["alignment"] as? [String: Double] { - XCTAssertEqual(alignment["ethics"], 0, "player traits round trip alignment ethics") - XCTAssertEqual(alignment["morals"], 1, "player traits round trip alignment ethics") - } - XCTAssertEqual(encoded["height"] as? String, "3.75 ft", "player traits round trip height") - XCTAssertEqual(encoded["weight"] as? String, "120.0 lb", "player traits round trip weight") - - let abilities = encoded["ability scores"] as? [String: Int] - XCTAssertNotNil(abilities) - print("\(String(describing: abilities))") - XCTAssertEqual(abilities?["Dexterity"], 13, "player traits round trip ability scores") - - XCTAssertEqual(encoded["money"] as? String, "130.0 gp", "player traits round trip money") - XCTAssertEqual(encoded["maximum hit points"] as? Int, 20, "player traits round trip maximum hit points") - XCTAssertEqual(encoded["current hit points"] as? Int, 9, "player traits round trip current hit points") - XCTAssertEqual(encoded["level"] as? Int, 2, "player traits round trip level") - } else { - XCTFail("Failed to deserialize encoded Player into dictionary") - } - } - - func testMissingTraits() { - let decoder = JSONDecoder() - - // Test that each missing trait results in nil - do { - let traits = "{:}".data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) - XCTAssertNil(player) - } - - do { - let traits = """ - { - "name": "Bilbo" - } - """.data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) - XCTAssertNil(player) - } - - - do { - let traits = """ - { - "name": "Bilbo", - "height": "3'9\\"" - } - """.data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) - XCTAssertNil(player) - } - - do { - let traits = """ - { - "name": "Bilbo", - "height": "3'9\\"", - "weight": 120 - } - """.data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) - XCTAssertNil(player) - } - - do { - let traits = """ - { - "name": "Bilbo", - "height": "3'9\\"", - "weight": 120, - "ability scores": {"Dexterity": 13} - } - """.data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) - XCTAssertNil(player) - } - - do { - let traits = """ - { - "name": "Bilbo", - "height": "3'9\\"", - "weight": 120, - "ability scores": {"Dexterity": 13}, - "money": 130] - } - """.data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) - XCTAssertNil(player) - } - } - - func expectedModifier(for abilityScore: Int) -> Int { - let selfMinus10 = abilityScore - 10 - return selfMinus10 < 0 ? Int(floor(Double(selfMinus10) / 2.0)) : selfMinus10 / 2 - } - - func testComputedProperties() { - let player = Player("Gandalf", speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.neutral, .good)) - - // Test speed (from species traits) - XCTAssertEqual(player.speed, 30, "speed should match species speed") - - // Test modifiers - for ability in player.modifiers.abilities { - let abilityScore = player.abilities[ability]! - let expectedModifier = expectedModifier(for: abilityScore) - XCTAssertEqual(player.modifiers[ability], expectedModifier, "modifier calculation") - } - - // Test initiative - XCTAssertEqual(player.initiativeModifier, player.modifiers[.dexterity], "initiative modifier") - XCTAssertEqual(player.initiativeScore, 10 + player.modifiers[.dexterity], "initiative score") - - // Test passive perception - XCTAssertEqual(player.passivePerception, 10 + player.modifiers[.wisdom], "passive perception") - } - - func testProficiencyBonus() { - let player = Player("Aragorn", speciesTraits: human, classTraits: fighter) - - // Level 1-4: +2 - player.level = 1 - XCTAssertEqual(player.proficiencyBonus, 2, "proficiency bonus at level 1") - - player.level = 4 - XCTAssertEqual(player.proficiencyBonus, 2, "proficiency bonus at level 4") - - // Level 5-8: +3 - player.level = 5 - XCTAssertEqual(player.proficiencyBonus, 3, "proficiency bonus at level 5") - - player.level = 8 - XCTAssertEqual(player.proficiencyBonus, 3, "proficiency bonus at level 8") - - // Level 9-12: +4 - player.level = 9 - XCTAssertEqual(player.proficiencyBonus, 4, "proficiency bonus at level 9") - - player.level = 12 - XCTAssertEqual(player.proficiencyBonus, 4, "proficiency bonus at level 12") - - // Level 13-16: +5 - player.level = 13 - XCTAssertEqual(player.proficiencyBonus, 5, "proficiency bonus at level 13") - - player.level = 16 - XCTAssertEqual(player.proficiencyBonus, 5, "proficiency bonus at level 16") - - // Level 17-20: +6 - player.level = 17 - XCTAssertEqual(player.proficiencyBonus, 6, "proficiency bonus at level 17") - - player.level = 20 - XCTAssertEqual(player.proficiencyBonus, 6, "proficiency bonus at level 20") - } - - func testHitDiceAtDifferentLevels() { - let player = Player("Legolas", speciesTraits: human, classTraits: fighter) - - player.level = 1 - XCTAssertEqual("\(player.hitDice)", "d10", "hit dice at level 1") - - player.level = 5 - XCTAssertEqual("\(player.hitDice)", "5d10", "hit dice at level 5") - - player.level = 10 - XCTAssertEqual("\(player.hitDice)", "10d10", "hit dice at level 10") - - player.level = 20 - XCTAssertEqual("\(player.hitDice)", "20d10", "hit dice at level 20") - } - - func testHashableConformance() { - let player1 = Player("Gimli", speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.lawful, .good)) - player1.descriptiveTraits = ["ideal": "Honor", "bond": "My axe"] - - let player2 = Player("Gimli", speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.lawful, .good)) - player2.speciesTraits = human - player2.classTraits = fighter - player2.baseAbilities = player1.baseAbilities - player2.height = player1.height - player2.weight = player1.weight - player2.maximumHitPoints = player1.maximumHitPoints - player2.currentHitPoints = player1.currentHitPoints - player2.experiencePoints = player1.experiencePoints - player2.money = player1.money - player2.descriptiveTraits = ["ideal": "Honor", "bond": "My axe"] - - // Test equality - XCTAssertEqual(player1, player2, "identical players should be equal") - - // Test hash values - var hasher1 = Hasher() - player1.hash(into: &hasher1) - let hash1 = hasher1.finalize() - - var hasher2 = Hasher() - player2.hash(into: &hasher2) - let hash2 = hasher2.finalize() - - XCTAssertEqual(hash1, hash2, "identical players should have same hash") - - // Test that players can be used in Sets - let playerSet: Set = [player1, player2] - XCTAssertEqual(playerSet.count, 1, "set should contain only one unique player") - } - - func testPlayerInequality() { - let player1 = Player("Boromir", speciesTraits: human, classTraits: fighter) - let player2 = Player("Faramir", speciesTraits: human, classTraits: fighter) - - // Different names - XCTAssertNotEqual(player1, player2, "players with different names should not be equal") - - // Different hit points - let player3 = Player("Boromir", speciesTraits: human, classTraits: fighter) - player3.baseAbilities = player1.baseAbilities - player3.height = player1.height - player3.weight = player1.weight - player3.money = player1.money - player3.currentHitPoints = player3.currentHitPoints - 5 - - XCTAssertNotEqual(player1, player3, "players with different current HP should not be equal") - } - - func testGenderCases() { - // Test all gender cases - let female = Player("Diana", speciesTraits: human, classTraits: fighter, gender: .female) - XCTAssertEqual(female.gender, .female, "female gender") - - let male = Player("Arthur", speciesTraits: human, classTraits: fighter, gender: .male) - XCTAssertEqual(male.gender, .male, "male gender") - - let agender = Player("Riley", speciesTraits: human, classTraits: fighter, gender: nil) - XCTAssertNil(agender.gender, "nil gender for androgynous/hermaphroditic") - } - - func testDescriptiveTraits() { - let player = Player("Samwise", speciesTraits: human, classTraits: fighter) - - // Initially empty - XCTAssertEqual(player.descriptiveTraits.count, 0) - - // Add traits - player.descriptiveTraits["ideal"] = "Loyalty" - player.descriptiveTraits["bond"] = "My friends" - player.descriptiveTraits["flaw"] = "Too trusting" - player.descriptiveTraits["background"] = "Gardener" - - XCTAssertEqual(player.descriptiveTraits.count, 4) - XCTAssertEqual(player.descriptiveTraits["ideal"], "Loyalty") - XCTAssertEqual(player.descriptiveTraits["bond"], "My friends") - XCTAssertEqual(player.descriptiveTraits["flaw"], "Too trusting") - XCTAssertEqual(player.descriptiveTraits["background"], "Gardener") - } - - func testAbilityScoresRoll() { - var abilities = AbilityScores() - abilities.roll() - - // Verify all abilities have valid scores (4d6-L should give 3-18) - for ability in abilities.abilities { - let score = abilities[ability]! - XCTAssertTrue((3...18).contains(score), "rolled ability score should be between 3 and 18") - } - - // Verify all six abilities are set - XCTAssertEqual(abilities.abilities.count, 6, "should have 6 abilities") - } - - func testDiceHitDiceExtension() { - // Test the hitDice extension on Dice - let d6 = SimpleDice(.d6) - - let level1HitDice = d6.hitDice(level: 1) - XCTAssertEqual("\(level1HitDice)", "d6") - - let level5HitDice = d6.hitDice(level: 5) - XCTAssertEqual("\(level5HitDice)", "5d6") - - let level10HitDice = d6.hitDice(level: 10) - XCTAssertEqual("\(level10HitDice)", "10d6") - } - - func testSpeciesAndClassTraitsDidSet() { - let player = Player("Test", speciesTraits: human, classTraits: fighter) - - XCTAssertEqual(player.speciesName, "Human") - XCTAssertEqual(player.className, "Fighter") - - // Create a mock second species (we'll reuse human but check the didSet is called) - let mockSpecies = human! - player.speciesTraits = mockSpecies - XCTAssertEqual(player.speciesName, mockSpecies.name) - - // Create a mock second class (we'll reuse fighter but check the didSet is called) - let mockClass = fighter! - player.classTraits = mockClass - XCTAssertEqual(player.className, mockClass.name) - } - - func testPlayerEncodingWithDescriptiveTraits() { - let decoder = JSONDecoder() - let encoder = JSONEncoder() - - let playerTraits = """ - { - "name": "Pippin", - "species": "Human", - "class": "Fighter", - "descriptive traits": { - "ideal": "Adventure", - "bond": "The Shire", - "flaw": "Impulsive" - }, - "height": "4'2\\"", - "weight": 95, - "ability scores": {"Charisma": 14, "Dexterity": 15}, - "money": 100, - "maximum hit points": 12 - } - """.data(using: .utf8)! - - do { - let player = try decoder.decode(Player.self, from: playerTraits) - XCTAssertEqual(player.descriptiveTraits.count, 3) - XCTAssertEqual(player.descriptiveTraits["ideal"], "Adventure") - XCTAssertEqual(player.descriptiveTraits["bond"], "The Shire") - XCTAssertEqual(player.descriptiveTraits["flaw"], "Impulsive") - - // Test encoding - let encoded = try encoder.encode(player) - let decodedDict = try JSONSerialization.jsonObject(with: encoded) as? [String: Any] - let encodedTraits = decodedDict?["descriptive traits"] as? [String: String] - XCTAssertNotNil(encodedTraits) - XCTAssertEqual(encodedTraits?["ideal"], "Adventure") - } catch { - XCTFail("Failed to decode/encode player with descriptive traits: \(error)") - } - } - - func testRollHitPointsClassMethod() { - // Test the static rollHitPoints method - let hitPoints = Player.rollHitPoints(classTraits: fighter, speciesTraits: human) - - // Fighter has d10, so minimum should be 6 (max(10/2+1, roll)), - // but roll could be lower, so minimum is actually 6 + species bonus - // Maximum is 10 + species bonus - XCTAssertTrue((6...10).contains(hitPoints), "hit points should be in valid range") - } - - func testMultipleLevelUps() { - let decoder = JSONDecoder() - - let playerTraits = """ - { - "name": "Merry", - "species": "Human", - "class": "Fighter", - "height": "4'2\\"", - "weight": 95, - "ability scores": {"Strength": 14}, - "money": 100, - "maximum hit points": 12, - "experience points": 0, - "level": 1 - } - """.data(using: .utf8)! - - let player = try! decoder.decode(Player.self, from: playerTraits) - player.speciesTraits = human - player.classTraits = fighter - - let initialHP = player.maximumHitPoints - - // Add enough XP to level up to level 2 - player.experiencePoints = 301 - XCTAssertTrue(player.canLevelUp) - player.levelUp() - XCTAssertEqual(player.level, 2) - XCTAssertTrue(player.maximumHitPoints > initialHP, "HP should increase on level up") - - // Add enough XP to level up to level 3 - player.experiencePoints = 901 - XCTAssertTrue(player.canLevelUp) - player.levelUp() - XCTAssertEqual(player.level, 3) - - // Add enough XP to level up to level 4 - player.experiencePoints = 2701 - XCTAssertTrue(player.canLevelUp) - player.levelUp() - XCTAssertEqual(player.level, 4) - - // Without enough XP, cannot level up - player.experiencePoints = 2701 - XCTAssertFalse(player.canLevelUp) - player.levelUp() // Should do nothing - XCTAssertEqual(player.level, 4) - } - -} diff --git a/RolePlayingCore/RolePlayingCoreTests/PlayersTests.swift b/RolePlayingCore/RolePlayingCoreTests/PlayersTests.swift deleted file mode 100644 index 540d47f..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/PlayersTests.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// PlayersTests.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/18/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -import RolePlayingCore - -class PlayersTests: XCTestCase { - - let bundle = Bundle(for: PlayersTests.self) - let decoder = JSONDecoder() - - var classes: Classes! - var species: Species! - - override func setUp() { - // TODO: Need to initialize UnitCurrency before creating Money instances in Player class. - let currenciesData = try! bundle.loadJSON("TestCurrencies") - _ = try! decoder.decode(Currencies.self, from: currenciesData) - - let classesData = try! bundle.loadJSON("TestClasses") - classes = try! decoder.decode(Classes.self, from: classesData) - - let speciesData = try! bundle.loadJSON("TestSpecies") - species = try! decoder.decode(Species.self, from: speciesData) - } - - func testPlayers() { - - var players: Players! = nil - do { - let playersData = try bundle.loadJSON("TestPlayers") - players = try decoder.decode(Players.self, from: playersData) - try players.resolve(classes: classes, species: species) - } - catch let error { - XCTFail("players.load failed, error \(error)") - } - XCTAssertEqual(players.players.count, 2, "players count") - XCTAssertEqual(players.count, 2, "players count") - - let removedPlayer = players[0]! - players.remove(at: 0) - XCTAssertEqual(players.count, 1, "players count") - - players.insert(removedPlayer, at: 1) - XCTAssertEqual(players.count, 2, "players count") - XCTAssertTrue(players[1]! === removedPlayer, "players count") - } - - func testMissingTraits() { - do { - let playersData = try! bundle.loadJSON("InvalidClassPlayers") - let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(classes: classes, species: species) - XCTFail("players.load should have failed") - } - catch let error { - print("players.load correctly threw an error \(error)") - } - - do { - let playersData = try! bundle.loadJSON("InvalidSpeciesPlayers") - let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(classes: classes, species: species) - XCTFail("players.resolve should have failed") - } - catch let error { - print("players.resolve correctly threw an error \(error)") - } - - do { - let playersData = try! bundle.loadJSON("MissingClassPlayers") - let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(classes: classes, species: species) - XCTFail("players.resolve should have failed") - } - catch let error { - print("players.resolve correctly threw an error \(error)") - } - - do { - let playersData = try! bundle.loadJSON("MissingSpeciesPlayers") - let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(classes: classes, species: species) - - XCTFail("players.resolve should have failed") - } - catch let error { - print("players.resolve correctly threw an error \(error)") - } - } - - -} diff --git a/RolePlayingCore/RolePlayingCoreTests/RacialNamesTests.swift b/RolePlayingCore/RolePlayingCoreTests/RacialNamesTests.swift deleted file mode 100644 index 306b19c..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/RacialNamesTests.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// SpeciesNamesTests.swift -// RolePlayingCoreTests -// -// Created by Brian Arnold on 7/8/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -@testable import RolePlayingCore - -class SpeciesNamesTests: XCTestCase { - - func testRacialNames() { - let bundle = Bundle(for: SpeciesNamesTests.self) - do { - let data = try bundle.loadJSON("TestRacialNames") - let decoder = JSONDecoder() - let speciesNames = try decoder.decode(RacialNames.self, from: data) - - XCTAssertEqual(speciesNames.names.count, 10, "Number of species name families") - - // TODO: find a way to test just the minimum functionality. - // In the meantime, use the test species. - let bundle = Bundle(for: SpeciesNamesTests.self) - let jsonData = try bundle.loadJSON("TestRaces") - let species = try decoder.decode(Species.self, from: jsonData) - let moreJsonData = try bundle.loadJSON("TestMoreRaces") - let moreRaces = try decoder.decode(Species.self, from: moreJsonData) - - let allRaces = Species() - allRaces.species = species.species + moreRaces.species - - // TODO: random names are hard; for now, get code coverage. - do { - _ = speciesNames.randomName(speciesTraits: allRaces.find("Human")!, gender: .female) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Elf")!, gender: .male) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Mountain Dwarf")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Stout")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Half-Elf")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Half-Orc")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Dragonborn")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Tiefling")!, gender: nil) - - } - - let encoder = JSONEncoder() - _ = try encoder.encode(speciesNames) - } - catch let error { - XCTFail("error thrown: \(error)") - } - } -} diff --git a/RolePlayingCore/RolePlayingCoreTests/RolePlayingCoreTests.swift b/RolePlayingCore/RolePlayingCoreTests/RolePlayingCoreTests.swift deleted file mode 100644 index e091ad9..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/RolePlayingCoreTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// RolePlayingCoreTests.swift -// RolePlayingCoreTests -// -// Created by Brian Arnold on 1/1/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -import RolePlayingCore - -class RolePlayingCoreTests: XCTestCase { - - - func testRolePlayingCore() { - // TODO: implement an integration test for the whole library. - } - -} diff --git a/RolePlayingCore/RolePlayingCoreTests/ServiceErrorTests.swift b/RolePlayingCore/RolePlayingCoreTests/ServiceErrorTests.swift deleted file mode 100644 index 75bd7e8..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/ServiceErrorTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// ServiceErrorTests.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/5/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -import RolePlayingCore - -class ServiceErrorTests: XCTestCase { - - func testServiceError() { - do { - throw RuntimeError("Gah!") - } - catch let error { - XCTAssertTrue(error is ServiceError, "should be a service error") - let description = "\(error)" - XCTAssertTrue(description.contains("Runtime error"), "should be a runtime error") - XCTAssertTrue(description.contains("Gah!"), "should contain the message") - XCTAssertTrue(description.contains("testServiceError"), "should have throw function name in it") - XCTAssertTrue(description.contains("ServiceErrorTests"), "should have throw file name in it") - } - - } - -} diff --git a/RolePlayingCore/RolePlayingCoreTests/SpeciesNamesTests.swift b/RolePlayingCore/RolePlayingCoreTests/SpeciesNamesTests.swift deleted file mode 100644 index 195a265..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/SpeciesNamesTests.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// SpeciesNamesTests.swift -// RolePlayingCoreTests -// -// Created by Brian Arnold on 7/8/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -@testable import RolePlayingCore - -class SpeciesNamesTests: XCTestCase { - - func testSpeciesNames() { - let bundle = Bundle(for: SpeciesNamesTests.self) - do { - let data = try bundle.loadJSON("TestSpeciesNames") - let decoder = JSONDecoder() - let speciesNames = try decoder.decode(SpeciesNames.self, from: data) - - XCTAssertEqual(speciesNames.names.count, 10, "Number of species name families") - - // TODO: find a way to test just the minimum functionality. - // In the meantime, use the test species. - let bundle = Bundle(for: SpeciesNamesTests.self) - let jsonData = try bundle.loadJSON("TestSpecies") - let species = try decoder.decode(Species.self, from: jsonData) - let moreJsonData = try bundle.loadJSON("TestMoreSpecies") - let moreSpecies = try decoder.decode(Species.self, from: moreJsonData) - - let allSpecies = Species() - allSpecies.species = species.species + moreSpecies.species - - // TODO: random names are hard; for now, get code coverage. - do { - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Human")!, gender: .female) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Elf")!, gender: .male) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Mountain Dwarf")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Stout")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Half-Elf")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Half-Orc")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Dragonborn")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Tiefling")!, gender: nil) - - } - - let encoder = JSONEncoder() - _ = try encoder.encode(speciesNames) - } - catch let error { - XCTFail("error thrown: \(error)") - } - } -} diff --git a/RolePlayingCore/RolePlayingCoreTests/SpeciesTests.swift b/RolePlayingCore/RolePlayingCoreTests/SpeciesTests.swift deleted file mode 100644 index 7a41677..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/SpeciesTests.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// SpeciesTests.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/16/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -import RolePlayingCore - -class SpeciesTests: XCTestCase { - - let bundle = Bundle(for: SpeciesTests.self) - let decoder = JSONDecoder() - - func testDefaultInit() { - let species = Species() - XCTAssertEqual(species.species.count, 0, "default init") - } - - func testSpecies() { - do { - let jsonData = try bundle.loadJSON("TestSpecies") - let species = try decoder.decode(Species.self, from: jsonData) - - XCTAssertNotNil(species, "Species file failed to load") - - XCTAssertEqual(species.leafSpecies.count, 8, "all species") - XCTAssertEqual(species.count, 11, "all species") - XCTAssertNotNil(species[0], "species by index") - - // Test finding a species by name - XCTAssertNotNil(species.find("Human"), "Fighter should be non-nil") - XCTAssertNil(species.find("Foo"), "Foo should be nil") - XCTAssertNil(species.find(nil), "nil species name should find nil") - } - catch let error { - XCTFail("Species threw an error: \(error)") - } - } - - func testUncommonSpecies() { - // Test throwing constructor - do { - let jsonData = try bundle.loadJSON("TestMoreSpecies") - let species = try decoder.decode(Species.self, from: jsonData) - - XCTAssertNotNil(species, "Species file failed to load") - - // There should be 5 species plus 2 subspecies - XCTAssertEqual(species.species.count, 7, "all species") - - } - catch let error { - XCTFail("Species threw an error: \(error)") - } - } - -} diff --git a/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift b/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift deleted file mode 100644 index 8aa5853..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift +++ /dev/null @@ -1,425 +0,0 @@ -// -// SpeciesTraitsTests.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/11/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -import RolePlayingCore - -class SpeciesTraitsTests: XCTestCase { - - let decoder = JSONDecoder() - - func testSpeciesTraits() { - // Test typical traits - do { - let traits = """ - { - "name": "Human", - "plural": "Humans", - "ability scores": {"Strength": 1, "Dexterity": 1, "Constitution": 1, "Intelligence": 1, "Wisdom": 1, "Charisma": 1}, - "minimum age": 18, - "lifespan": 90, - "base height": "4'8\\"", - "height modifier": "2d10", - "base weight": 110, - "weight modifier": "2d4", - "speed": 30, - "languages": ["Common"], - "extra languages": 1 - } - """.data(using: .utf8)! - var speciesTraits: SpeciesTraits? = nil - do { - speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) - } - catch let error { - XCTFail("Failed to decode species traits, error: \(error)") - } - - XCTAssertNotNil(speciesTraits) - XCTAssertEqual(speciesTraits?.name, "Human", "name") - XCTAssertEqual(speciesTraits?.plural, "Humans", "plural") - XCTAssertEqual(speciesTraits?.abilityScoreIncrease.count, 6, "ability score increase") - if let scores = speciesTraits?.abilityScoreIncrease.values { - for score in scores { - XCTAssertEqual(score, 1, "ability score increase") - } - } - - XCTAssertEqual(speciesTraits?.minimumAge, 18, "minimum age") - XCTAssertEqual(speciesTraits?.lifespan, 90, "lifespan") - XCTAssertEqual(speciesTraits?.baseHeight.value ?? 0, 4.666666, accuracy: 0.000001, "base height") - - let heightModifier = speciesTraits?.heightModifier as? SimpleDice - XCTAssertNotNil(heightModifier, "height modifier") - XCTAssertEqual(heightModifier?.sides, 10, "height modifier") - XCTAssertEqual(heightModifier?.times, 2, "height modifier") - - XCTAssertEqual(speciesTraits?.baseWeight.value ?? 0, 110.0, "base height") - - let weightModifier = speciesTraits?.weightModifier as? SimpleDice - XCTAssertNotNil(weightModifier, "weight modifier") - XCTAssertEqual(weightModifier?.sides, 4, "weight modifier") - XCTAssertEqual(weightModifier?.times, 2, "weight modifier") - - XCTAssertEqual(speciesTraits?.speed, 30, "speed") - - XCTAssertEqual(speciesTraits?.aliases.count, 0, "aliases") - - XCTAssertEqual(speciesTraits?.size, SpeciesTraits.Size.medium, "size") - XCTAssertNil(speciesTraits?.alignment, "alignment") - } - - // Test minimum traits - do { - let traits = """ - { - "name": "Giant Human", - "plural": "Giant Humans", - "minimum age": 18, - "lifespan": 90, - "base height": "7'8\\"", - "height modifier": "2d10", - "base weight": 110, - "speed": 30 - } - """.data(using: .utf8)! - var speciesTraits: SpeciesTraits? = nil - do { - speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) - } - catch let error { - XCTFail("Failed to decode species traits, error: \(error)") - } - XCTAssertNotNil(speciesTraits) - XCTAssertEqual(speciesTraits?.name, "Giant Human", "name") - XCTAssertEqual(speciesTraits?.plural, "Giant Humans", "plural") - XCTAssertEqual(speciesTraits?.abilityScoreIncrease.count, 6, "ability score increase") - if let scores = speciesTraits?.abilityScoreIncrease.values { - for score in scores { - XCTAssertEqual(score, 0, "ability score increase") - } - } - - XCTAssertEqual(speciesTraits?.minimumAge, 18, "minimum age") - XCTAssertEqual(speciesTraits?.lifespan, 90, "lifespan") - XCTAssertEqual(speciesTraits?.baseHeight.value ?? 0, 7.666666, accuracy: 0.000001, "base height") - - let heightModifier = speciesTraits?.heightModifier as? SimpleDice - XCTAssertNotNil(heightModifier, "height modifier") - XCTAssertEqual(heightModifier?.sides, 10, "height modifier") - XCTAssertEqual(heightModifier?.times, 2, "height modifier") - - XCTAssertEqual(speciesTraits?.baseWeight.value ?? 0, 110.0, "base height") - - let weightModifier = speciesTraits?.weightModifier as? DiceModifier - XCTAssertEqual(weightModifier?.modifier, 0, "weight modifier") - - XCTAssertEqual(speciesTraits?.speed, 30, "speed") - - XCTAssertEqual(speciesTraits?.aliases.count, 0, "aliases") - XCTAssertNil(speciesTraits?.alignment, "alignment") - - XCTAssertEqual(speciesTraits?.size, SpeciesTraits.Size.large, "size") - } - - // Test optional traits - do { - let traits = """ - { - "name": "Small Human", - "plural": "Small Humans", - "minimum age": 18, - "lifespan": 90, - "base height": "2'8\\"", - "height modifier": "2d10", - "base weight": 110, - "speed": 30, - "alignment": "Lawful Neutral", - "aliases": ["Big Human"] - } - """.data(using: .utf8)! - - var speciesTraits: SpeciesTraits? = nil - do { - speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) - } - catch let error { - XCTFail("Failed to decode species traits, error: \(error)") - } - XCTAssertNotNil(speciesTraits) - - XCTAssertEqual(speciesTraits?.size, SpeciesTraits.Size.small, "size") - - XCTAssertNotNil(speciesTraits?.alignment, "alignment should be non-nil") - let foundAlignment = speciesTraits?.alignment?.kind ?? Alignment(.neutral, .neutral).kind - XCTAssertEqual(foundAlignment, Alignment(.lawful, .neutral).kind, "alignment kind") - - XCTAssertEqual(speciesTraits?.aliases.count, 1, "aliases count") - } - } - - func testMissingTraits() { - - // Test that each missing trait results in nil - do { - let traits = "{}".data(using: .utf8)! - let speciesTraits = try? decoder.decode(SpeciesTraits.self, from: traits) - XCTAssertNil(speciesTraits) - } - - do { - let traits = """ - { "name": "Giant Human" } - """.data(using: .utf8)! - let speciesTraits = try? decoder.decode(SpeciesTraits.self, from: traits) - XCTAssertNil(speciesTraits) - } - - - do { - let traits = """ - { - "plural": "Giant Humans" - } - """.data(using: .utf8)! - let speciesTraits = try? decoder.decode(SpeciesTraits.self, from: traits) - XCTAssertNil(speciesTraits) - } - } - - func testDecodingSpeciesTraits() { - do { - let traits = """ - { - "name": "Human", - "plural": "Humans", - "minimum age": 18, - "lifespan": 90, - "base height": "4'8\\"", - "height modifier": "2d10", - "base weight": 110, - "speed": 30, - "subspecies": [ - { - "name": "Subhuman", - "plural": "Subhumans", - "minimum age": 15, - "lifespan": 60, - "base height": "2'8\\"", - "height modifier": "2d6", - "base weight": 45, - "speed": 10 - } - ] - } - """.data(using: .utf8)! - let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) - if let subspeciesTraits = speciesTraits.subspecies.first { - - XCTAssertEqual(subspeciesTraits.name, "Subhuman", "name") - XCTAssertEqual(subspeciesTraits.plural, "Subhumans", "plural") - XCTAssertEqual(subspeciesTraits.abilityScoreIncrease.count, 6, "ability score increase") - for score in subspeciesTraits.abilityScoreIncrease.values { - XCTAssertEqual(score, 0, "ability score increase") - } - - XCTAssertEqual(subspeciesTraits.minimumAge, 15, "minimum age") - XCTAssertEqual(subspeciesTraits.lifespan, 60, "lifespan") - XCTAssertEqual(subspeciesTraits.baseHeight.value, 2.666666, accuracy: 0.000001, "base height") - - let heightModifier = subspeciesTraits.heightModifier as? SimpleDice - XCTAssertNotNil(heightModifier, "height modifier") - XCTAssertEqual(heightModifier?.sides, 6, "height modifier") - XCTAssertEqual(heightModifier?.times, 2, "height modifier") - - XCTAssertEqual(subspeciesTraits.baseWeight.value, 45.0, "base height") - - let weightModifier = subspeciesTraits.weightModifier as? SimpleDice - XCTAssertNil(weightModifier, "weight modifier") - - XCTAssertEqual(subspeciesTraits.speed, 10, "speed") - - XCTAssertEqual(subspeciesTraits.aliases.count, 0, "aliases") - - XCTAssertEqual(subspeciesTraits.size, SpeciesTraits.Size.small, "size") - XCTAssertNil(subspeciesTraits.alignment, "alignment") - - XCTAssertEqual(subspeciesTraits.hitPointBonus, 0, "hit point bonus") - - } else { - XCTFail("decode failed for traits with subspecies traits") - } - } - catch let error { - XCTFail("decode failed, error: \(error)") - } - - // Test the other half overrides - do { - let traits = """ - { - "name": "Human", - "plural": "Humans", - "minimum age": 18, - "lifespan": 90, - "base height": "4'8\\"", - "height modifier": "2d10", - "base weight": 110, - "speed": 30, - "subspecies": [ - { - "name": "Folk", - "plural": "Folks", - "aliases": ["Plainfolk"], - "weight modifier": "d8", - "ability scores": {"Strength": 2, "Dexterity": 1, "Constitution": 3, "Intelligence": 2, "Wisdom": 1, "Charisma": 1}, - "alignment": "Neutral", - "darkvision": 20, - "hit point bonus": 2 - } - ] - } - """.data(using: .utf8)! - - let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) - if let subspeciesTraits = speciesTraits.subspecies.first { - - XCTAssertNotNil(subspeciesTraits) - XCTAssertEqual(subspeciesTraits.name, "Folk", "name") - XCTAssertEqual(subspeciesTraits.plural, "Folks", "plural") - XCTAssertEqual(subspeciesTraits.abilityScoreIncrease.count, 6, "ability score increase") - for score in subspeciesTraits.abilityScoreIncrease.values { - XCTAssertNotEqual(score, 0, "ability score increase") - } - - XCTAssertEqual(subspeciesTraits.minimumAge, 18, "minimum age") - XCTAssertEqual(subspeciesTraits.lifespan, 90, "lifespan") - XCTAssertEqual(subspeciesTraits.baseHeight.value, 4.666666, accuracy: 0.000001, "base height") - - let heightModifier = subspeciesTraits.heightModifier as? SimpleDice - XCTAssertNotNil(heightModifier, "height modifier") - XCTAssertEqual(heightModifier?.sides, 10, "height modifier") - XCTAssertEqual(heightModifier?.times, 2, "height modifier") - - XCTAssertEqual(subspeciesTraits.baseWeight.value, 110, "base height") - - let weightModifier = subspeciesTraits.weightModifier as? SimpleDice - XCTAssertNotNil(weightModifier, "weight modifier") - XCTAssertEqual(weightModifier?.sides, 8, "weight modifier") - XCTAssertEqual(weightModifier?.times, 1, "weight modifier") - - XCTAssertEqual(subspeciesTraits.speed, 30, "speed") - - XCTAssertEqual(subspeciesTraits.aliases.count, 1, "aliases") - - XCTAssertEqual(subspeciesTraits.size, SpeciesTraits.Size.medium, "size") - XCTAssertNotNil(subspeciesTraits.alignment, "alignment") - let foundAlignment = subspeciesTraits.alignment?.kind ?? Alignment(.lawful, .good).kind - XCTAssertEqual(foundAlignment, Alignment(.neutral, .neutral).kind, "alignment kind") - - XCTAssertEqual(subspeciesTraits.hitPointBonus, 2, "hit point bonus") - } else { - XCTFail("decode failed for traits with subspecies traits") - } - } - catch let error { - XCTFail("decode failed with error: \(error)") - } - } - - func testEncodingSubspeciesTraits() { - let speciesTraits = SpeciesTraits(name: "Human", plural: "Humans", aliases: [], descriptiveTraits: [:], abilityScoreIncrease: AbilityScores(), minimumAge: 18, lifespan: 90, alignment: Alignment(.lawful, .neutral), baseHeight: "4ft 9 in".parseHeight!, heightModifier: DiceModifier(0), baseWeight: "178 lb".parseWeight!, weightModifier: DiceModifier(0), darkVision: 0, speed: 45, hitPointBonus: 0) - - let encoder = JSONEncoder() - - do { - var copyOfSpeciesTraits = speciesTraits - var subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", minimumAge: 14, lifespan: 45, baseHeight: "3 ft".parseHeight!, baseWeight: "100 lb".parseWeight!, darkVision: 0, speed: 30) - subspeciesTraits.blendTraits(from: copyOfSpeciesTraits) - copyOfSpeciesTraits.subspecies.append(subspeciesTraits) - - let encoded = try encoder.encode(copyOfSpeciesTraits) - let dictionary = try JSONSerialization.jsonObject(with: encoded, options: []) as! [String: Any] - - // Confirm species traits - XCTAssertEqual(dictionary["name"] as? String, "Human", "encoding name") - XCTAssertEqual(dictionary["plural"] as? String, "Humans", "encoding name") - - XCTAssertEqual(dictionary["minimum age"] as? Int, 18, "encoding name") - XCTAssertEqual(dictionary["lifespan"] as? Int, 90, "encoding lifespan") - XCTAssertEqual(dictionary["alignment"] as? String, "Lawful Neutral", "encoding alignment") - XCTAssertEqual(dictionary["base height"]! as! String, "4.75 ft", "encoding base height") - XCTAssertEqual(dictionary["height modifier"] as? String, "0", "encoding height modifier") - XCTAssertEqual(dictionary["base weight"]! as! String, "178.0 lb", "encoding base weight") - XCTAssertEqual(dictionary["weight modifier"] as? String, "0", "encoding weight modifier") - - XCTAssertEqual(dictionary["darkvision"] as? Int, 0, "encoding name") - XCTAssertEqual(dictionary["speed"] as? Int, 45, "encoding name") - XCTAssertEqual(dictionary["hit point bonus"] as? Int, 0, "encoding base height") - - // Confirm subspecies traits - if let subspecies = dictionary["subspecies"] as? [[String: Any]], let firstSubspecies = subspecies.first { - XCTAssertEqual(firstSubspecies["name"] as? String, "Subhuman", "encoding name") - XCTAssertEqual(firstSubspecies["plural"] as? String, "Subhumans", "encoding name") - - XCTAssertEqual(firstSubspecies["minimum age"] as? Int, 14, "encoding name") - XCTAssertEqual(firstSubspecies["lifespan"] as? Int, 45, "encoding lifespan") - XCTAssertNil(firstSubspecies["alignment"], "encoding alignment") - XCTAssertEqual(firstSubspecies["base height"]! as! String, "3.0 ft", "encoding base height") - XCTAssertNil(firstSubspecies["height modifier"], "encoding height modifier") - XCTAssertEqual(firstSubspecies["base weight"]! as! String, "100.0 lb", "encoding base weight") - XCTAssertNil(firstSubspecies["weight modifier"], "encoding weight modifier") - - XCTAssertNil(firstSubspecies["darkvision"], "encoding darkvision") - XCTAssertEqual(firstSubspecies["speed"] as? Int, 30, "encoding speed") - XCTAssertNil(firstSubspecies["hit point bonus"], "encoding hit point bonus") - - } else { - XCTFail("subspecies should be non-nil and contain at least one subspecies") - } - } - catch let error { - XCTFail("decode failed with error: \(error)") - } - - do { - var copyOfSpeciesTraits = speciesTraits - let subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", aliases: ["Minions"], descriptiveTraits: ["background": "Something"], abilityScoreIncrease: AbilityScores([Ability("Strength"): 2]), minimumAge: 14, lifespan: 45, alignment: Alignment(.neutral, .evil), baseHeight: "3 ft".parseHeight!, heightModifier: "d4".parseDice!, baseWeight: "100 lb".parseWeight!, weightModifier: "d6".parseDice!, darkVision: 10, speed: 45, hitPointBonus: 1) - copyOfSpeciesTraits.subspecies.append(subspeciesTraits) - - let encoded = try encoder.encode(copyOfSpeciesTraits) - let dictionary = try JSONSerialization.jsonObject(with: encoded, options: []) as! [String: Any] - - // Confirm subspecies traits - if let subspecies = dictionary["subspecies"] as? [[String: Any]], let firstSubspecies = subspecies.first { - XCTAssertEqual(firstSubspecies["name"] as? String, "Subhuman", "encoding name") - XCTAssertEqual(firstSubspecies["plural"] as? String, "Subhumans", "encoding name") - - XCTAssertEqual(firstSubspecies["minimum age"] as? Int, 14, "encoding name") - XCTAssertEqual(firstSubspecies["lifespan"] as? Int, 45, "encoding lifespan") - XCTAssertEqual(firstSubspecies["alignment"] as? String, "Neutral Evil", "encoding alignment") - XCTAssertEqual(firstSubspecies["base height"]! as! String, "3.0 ft", "encoding base height") - XCTAssertEqual(firstSubspecies["height modifier"] as? String, "d4", "encoding height modifier") - XCTAssertEqual(firstSubspecies["base weight"]! as! String, "100.0 lb", "encoding base weight") - XCTAssertEqual(firstSubspecies["weight modifier"] as? String, "d6", "encoding weight modifier") - - XCTAssertEqual(firstSubspecies["darkvision"] as? Int, 10, "encoding darkvision") - XCTAssertNil(firstSubspecies["speed"], "encoding speed") - XCTAssertEqual(firstSubspecies["hit point bonus"] as? Int, 1, "encoding hit point bonus") - } else { - XCTFail("subspecies should be non-nil and contain at least one subspecies") - } - } - catch let error { - XCTFail("decode failed with error: \(error)") - } - - } - -} diff --git a/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json b/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json deleted file mode 100644 index d47eeb7..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "species": [ - { - "name": "Dragonborn", - "plural": "Dragonborn", - "ability scores": { "Strength": 2, "Charisma": 1 }, - "minimum age": 15, - "lifespan": 80, - "alignment": "Good", - "base height": "5'6\"", - "height modifier": "2d8", - "base weight": 175, - "weight modifier": "2d6", - "speed": 30, - "skills": ["draconic ancestry", "breath weapon", "damage"], - "languages": ["Common", "Draconic"] - }, - { - "name": "Gnome", - "plural": "Gnomes", - "ability scores": { "Intelligence": 2 }, - "minimum age": 40, - "lifespan": 425, - "alignment": "Good", - "base height": "2'11\"", - "height modifier": "2d4", - "base weight": 35, - "speed": 25, - "darkvision": 60, - "skills": ["gnome cunning"], - "languages": ["Common", "Gnomish"], - "subspecies": [{ - "name": "Forest Gnome", - "plural": "Forest Gnomes", - "ability scores": { "Dexterity": 1 }, - "skills": ["natural illusionist", "speak with small beasts"] - }, - { - "name": "Rock Gnome", - "plural": "Rock Gnomes", - "ability scores": { "Constitution": 1 }, - "skills": ["artificers lore", "tinker"] - }] - }, - { - "name": "Half-Elf", - "plural": "Half-Elves", - "ability scores": { "Strength": 1 }, - "ability score bonuses": 2, - "minimum age": 20, - "lifespan": 180, - "alignment": "Chaotic", - "base height": "4'9\"", - "height modifier": "2d8", - "base weight": 110, - "weight modifier": "2d4", - "speed": 30, - "darkvision": 60, - "skills": ["fey ancestry"], - "languages": ["Common", "Elvish"], - "extra languages": 1, - "skill versatility": 2 - }, - { - "name": "Half-Orc", - "plural": "Half-Orcs", - "ability scores": { "Strength": 2, "Constitution": 1 }, - "minimum age": 14, - "lifespan": 75, - "alignment": "Chaotic", - "base height": "4'10\"", - "height modifier": "2d10", - "base weight": 140, - "weight modifier": "2d6", - "speed": 30, - "darkvision": 60, - "skills": ["menacing", "relentless endurance", "savage attacks"], - "languages": ["Common", "Orcish"] - }, - { - "name": "Tiefling", - "plural": "Tieflings", - "ability scores": { "Intelligence": 1, "Charisma": 2 }, - "minimum age": 18, - "lifespan": 100, - "alignment": "Chaotic", - "base height": "4'9\"", - "height modifier": "2d8", - "base weight": 110, - "weight modifier": "2d4", - "speed": 30, - "darkvision": 60, - "skills": ["hellish resistance", "infernal legacy"], - "languages": ["Common", "Infernal"] - } - ] -} diff --git a/RolePlayingCore/RolePlayingCoreTests/TestSpecies.json b/RolePlayingCore/RolePlayingCoreTests/TestSpecies.json deleted file mode 100644 index ce7e696..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/TestSpecies.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "species": [ - { - "name": "Dwarf", - "plural": "Dwarves", - "ability scores": { "Constitution": 2 }, - "minimum age": 50, - "lifespan": 350, - "alignment": "Lawful Good", - "base height": 4, - "height modifier": "2d4", - "base weight": 130, - "weight modifier": "2d6", - "speed": 25, - "darkvision": 60, - "skills": ["dwarven resilience"], - "weapons" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], - "tools": ["smith's tools", "brewer's supplies", "mason's tools"], - "history": ["stonecunning"], - "languages": ["Common", "Dwarvish"], - "subspecies": [{ - "name": "Hill Dwarf", - "plural": "Hill Dwarves", - "ability scores": { "Wisdom": 2 }, - "hit point bonus": 1, - "base height": "3'5\"", - "base weight": 115, - }, - { - "name": "Mountain Dwarf", - "plural": "Mountain Dwarves", - "ability scores": { "Strength": 2 }, - "armor": ["light", "medium"], - }] - }, - { - "name": "Elf", - "plural": "Elves", - "ability scores": { "Dexterity": 2 }, - "minimum age": 100, - "lifespan": 750, - "alignment": "Chaotic Good", - "base height": "4'6\"", - "height modifier": "2d10", - "base weight": 100, - "weight modifier": "1d4", - "speed": 30, - "darkvision": 60, - "resilience": ["poison", "poison damage"], - "skills": ["keen senses", "fey ancestry", "trance"], - "languages": ["Common", "Elvish"], - "subspecies": [{ - "name": "High Elf", - "plural": "High Elves", - "ability scores": { "Intelligence": 1 }, - "base weight:": 90, - "weapons": ["longsword", "shortsword", "shortbow", "longbow"], - "extra languages": 1, - "spells": ["wizard cantrip"], - "hit point bonus": 1 - }, - { - "name": "Wood Elf", - "plural": "Wood Elves", - "ability scores": { "Wisdom": 1 }, - "weapons": ["longsword", "shortsword", "shortbow", "longbow"], - "skills": ["fleet of foot", "mask of the wild"] - }, - { - "name": "Dark Elf", - "plural": "Dark Elves", - "aliases": ["Drow"], - "ability scores": { "Charisma": 1 }, - "base height": "4'5\"", - "height modifier": "2d6", - "base weight": 75, - "weight modifier": "1d6", - "darkvision": 120, - "weapons": ["rapier", "shortsword", "hand crossbow"], - "spells": ["dancing lights", "faerie fire:3", "darkness:5"] - }] - }, - { - "name": "Halfling", - "plural": "Halflings", - "ability scores": { "Dexterity": 2 }, - "minimum age": 20, - "lifespan": 150, - "alignment": "Lawful Good", - "base height": "2'7\"", - "base weight": 35, - "height modifier": "2d4", - "speed": 25, - "skills": ["lucky", "brave", "halfling nimbleness"], - "weapons" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], - "tools": ["smith's tools", "brewer's supplies", "mason's tools"], - "history": ["stonecunning"], - "languages": ["Common", "Halfling"], - "subspecies": [{ - "name": "Lightfoot", - "plural": "Lightfoots", - "ability scores": { "Charisma": 1 }, - "skills": ["stealthy"] - }, - { - "name": "Stout", - "plural": "Stouts", - "ability scores": { "Constitution": 1 }, - "skills": ["stout resilience"] - }] - }, - { - "name": "Human", - "plural": "Humans", - "ability scores": { "Strength": 1, "Dexterity": 1, "Constitution": 1, "Intelligence": 1, "Wisdom": 1, "Charisma": 1 }, - "minimum age": 18, - "lifespan": 90, - "base height": "4'8\"", - "height modifier": "2d10", - "base weight": 110, - "weight modifier": "2d4", - "speed": 30, - "languages": ["Common"], - "extra languages": 1 - } - ] -} diff --git a/RolePlayingCore/RolePlayingCoreTests/WeightTests.swift b/RolePlayingCore/RolePlayingCoreTests/WeightTests.swift deleted file mode 100644 index 49e2109..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/WeightTests.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// UnitWeightTests.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/5/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -import RolePlayingCore - -class UnitWeightTests: XCTestCase { - - let decoder = JSONDecoder() - - func testWeights() { - do { - let howHeavy = "70".parseWeight - XCTAssertNotNil(howHeavy, "weight should be non-nil") - XCTAssertEqual(howHeavy?.value, 70, "weight should be 3.0") - } - - do { - let howHeavy = "3.0".parseWeight - XCTAssertNotNil(howHeavy, "weight should be non-nil") - XCTAssertEqual(howHeavy?.value, 3.0, "weight should be 3.0") - } - - do { - let howHeavy = "45lb".parseWeight - XCTAssertNotNil(howHeavy, "weight should be non-nil") - XCTAssertEqual(howHeavy?.value, 45, "weight should be 45") - } - - do { - let howHeavy = "174 kg".parseWeight - XCTAssertNotNil(howHeavy, "weight should be non-nil") - XCTAssertEqual(howHeavy?.value, 174, "weight should be 174") - } - } - - func testInvalidWeights() { - do { - let howHeavy = "99 hello".parseWeight - XCTAssertNil(howHeavy, "weight should be nil") - } - } - - func testEncodingWeight() { - struct WeightContainer: Encodable { - let weight: Weight - - enum CodingKeys: String, CodingKey { - case weight - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode("\(weight)", forKey: .weight) - } - } - let weightContainer = WeightContainer(weight: Weight(value: 2.9, unit: .kilograms)) - - // Do a round-trip through serialization, then deserialization to confirm that it became a string - do { - let encoder = JSONEncoder() - let encoded = try encoder.encode(weightContainer) - let deserialized = try JSONSerialization.jsonObject(with: encoded, options: .allowFragments) - print("deserialized = \n\(deserialized)") - let container = deserialized as? [String: Any] - let weight = container?["weight"] as? String - XCTAssertEqual(weight, "2.9 kg", "Encoded weight did not become a string") - } - catch let error { - XCTFail("Encoding weight threw an error: \(error)") - } - } - - func testDecodingHeight() { - struct WeightContainer: Decodable { - let weight: Weight - } - - // Test decoding from string height - do { - let traits = """ - { - "weight": "147 lb" - } - """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode(WeightContainer.self, from: traits) - - XCTAssertEqual(decoded.weight.value, 147, "Decoded weight should be 147 lb") - } catch let error { - XCTFail("Decoding weight threw an error: \(error)") - } - } - - - // Test decoding from double height - do { - let traits = """ - { - "weight": 17 - } - """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode(WeightContainer.self, from: traits) - - XCTAssertEqual(decoded.weight.value, 17, "Decoded weight should be 17 lb") - } catch let error { - XCTFail("Decoding weight threw an error: \(error)") - } - } - - // Test failure to decode - // Test decoding from double height - do { - let traits = """ - { - "weight": "abcdefg" - } - """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - _ = try decoder.decode(WeightContainer.self, from: traits) - XCTFail("Decoding weight should have thrown an error") - - } catch let error { - print("Decoding invalid weight successfully threw an error: \(error)") - } - } - - } - - func testDecodingWeightIfPresent() { - struct WeightContainer: Decodable { - let weight: Weight? // The ? will trigger decodeIfPresent in the decoder - } - - // Test decoding from string height - do { - let traits = """ - { - "weight": "220 lb" - } - """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode(WeightContainer.self, from: traits) - - XCTAssertEqual(decoded.weight?.value, 220.0, "Decoded weight should be 220 lb") - } catch let error { - XCTFail("Decoding weight threw an error: \(error)") - } - } - - - // Test decoding from double height - do { - let traits = """ - { - "weight": 6.5 - } - """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode(WeightContainer.self, from: traits) - - XCTAssertEqual(decoded.weight?.value, 6.5, "Decoded weight should be 4 ft 3 in") - } catch let error { - XCTFail("Decoding weight threw an error: \(error)") - } - } - } - -} diff --git a/Sources/RolePlayingCore/Common/Height.swift b/Sources/RolePlayingCore/Common/Height.swift new file mode 100644 index 0000000..b58f21d --- /dev/null +++ b/Sources/RolePlayingCore/Common/Height.swift @@ -0,0 +1,157 @@ +// +// Height +// RolePlayingCore +// +// Created by Brian Arnold on 2/5/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Foundation + +/// A measurement of length. +public typealias Height = Measurement + +extension String { + + /// Parses ft or ', in or ", cm, m into a measurement of length. + /// Returns nil if the string could not be parsed. + public var parseHeight: Height? { + let trimmed = self.trimmingCharacters(in: .whitespaces) + + // Try parsing imperial units (feet and/or inches) + if let height = parseImperialHeight(from: trimmed) { + return height + } + + // Try parsing metric units + if let height = parseMetricHeight(from: trimmed) { + return height + } + + // Try parsing as a plain number (default to feet) + if let value = Double(trimmed) { + return Height(value: value, unit: .feet) + } + + return nil + } + + private func parseImperialHeight(from string: String) -> Height? { + let feetMarkers = ["'", "ft"] + let inchesMarkers = ["\"", "in"] + + // Look for feet marker + var feetValue: Double = 0 + var feetEndIndex = string.startIndex + var foundFeet = false + + for marker in feetMarkers { + if let range = string.range(of: marker) { + let feetString = string[.. Height? { + let metricUnits: [(marker: String, unit: UnitLength)] = [ + ("cm", .centimeters), + ("m", .meters) + ] + + for (marker, unit) in metricUnits { + if let range = string.range(of: marker) { + let valueString = string[.. Height { + let height: Height? + + if let double = try? self.decode(Double.self, forKey: key) { + height = Height(value: double, unit: .feet) + } else { + height = try self.decode(String.self, forKey: key).parseHeight + } + + // Throw if we were unsuccessful parsing. + guard let height else { + let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Missing string or number for Height value") + throw DecodingError.dataCorrupted(context) + } + + return height + } + + /// Decodes either a number or a string into a Height, if present. + /// + /// - throws `DecodingError.dataCorrupted` if the height could not be decoded. + func decodeIfPresent(_ type: Height.Type, forKey key: K) throws -> Height? { + let height: Height? + + if let double = try? self.decode(Double.self, forKey: key) { + height = Height(value: double, unit: .feet) + } else if let string = try self.decodeIfPresent(String.self, forKey: key) { + height = string.parseHeight + } else { + height = nil + } + + return height + } +} + +extension Height { + + /// Returns a string representation of the height suitable for display. + public var displayString: String { + // Use feet and inches for imperial locales + let totalInches = self.converted(to: .inches).value + let feet = Int(totalInches / 12) + let inches = Int(totalInches.truncatingRemainder(dividingBy: 12)) + + let feetMeasurement = Measurement(value: Double(feet), unit: UnitLength.feet) + let feetString = feetMeasurement.formatted(.measurement(width: .abbreviated)) + + let inchesMeasurement = Measurement(value: Double(inches), unit: UnitLength.inches) + let inchesString = inchesMeasurement.formatted(.measurement(width: .abbreviated)) + return "\(feetString), \(inchesString)" + } +} diff --git a/RolePlayingCore/RolePlayingCore/Common/NameGenerator.swift b/Sources/RolePlayingCore/Common/NameGenerator.swift similarity index 99% rename from RolePlayingCore/RolePlayingCore/Common/NameGenerator.swift rename to Sources/RolePlayingCore/Common/NameGenerator.swift index 52a5742..055e9ae 100644 --- a/RolePlayingCore/RolePlayingCore/Common/NameGenerator.swift +++ b/Sources/RolePlayingCore/Common/NameGenerator.swift @@ -11,7 +11,6 @@ /// This implements a Markov-Chain-based name generator. /// See for a description. public struct NameGenerator { - private var nameStarters: [String] private var nameParts: [String: [String]] diff --git a/RolePlayingCore/RolePlayingCore/Common/RandomIndexGenerator.swift b/Sources/RolePlayingCore/Common/RandomIndexGenerator.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Common/RandomIndexGenerator.swift rename to Sources/RolePlayingCore/Common/RandomIndexGenerator.swift diff --git a/RolePlayingCore/RolePlayingCore/Common/SpeciesNames.swift b/Sources/RolePlayingCore/Common/SpeciesNames.swift similarity index 98% rename from RolePlayingCore/RolePlayingCore/Common/SpeciesNames.swift rename to Sources/RolePlayingCore/Common/SpeciesNames.swift index 57ca7f2..6a712a8 100644 --- a/RolePlayingCore/RolePlayingCore/Common/SpeciesNames.swift +++ b/Sources/RolePlayingCore/Common/SpeciesNames.swift @@ -8,6 +8,7 @@ import Foundation +/// Collections of species names, for use in generating random names. public struct SpeciesNames: Codable { struct FamilyNames: Codable { diff --git a/RolePlayingCore/RolePlayingCore/Common/Weight.swift b/Sources/RolePlayingCore/Common/Weight.swift similarity index 69% rename from RolePlayingCore/RolePlayingCore/Common/Weight.swift rename to Sources/RolePlayingCore/Common/Weight.swift index ff7c033..04088a3 100644 --- a/RolePlayingCore/RolePlayingCore/Common/Weight.swift +++ b/Sources/RolePlayingCore/Common/Weight.swift @@ -15,31 +15,37 @@ extension String { /// Parses "lb" or "kg" into a measurement of mass. public var parseWeight: Weight? { - var value: Double? - var unit: UnitMass = .pounds + let trimmed = self.trimmingCharacters(in: .whitespaces) - let weightMap: [String: UnitMass] = [ - "lb": .pounds, - "kg": .kilograms] - - for (key, weightUnit) in weightMap { - if let range = self.range(of: key) { - value = Double(self[.. Weight? { + let weightUnits: [(marker: String, unit: UnitMass)] = [ + ("lb", .pounds), + ("kg", .kilograms) + ] + + for (marker, unit) in weightUnits { + if let range = string.range(of: marker) { + let valueString = string[.. Player { diff --git a/RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift b/Sources/RolePlayingCore/Configuration/Configuration.swift similarity index 69% rename from RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift rename to Sources/RolePlayingCore/Configuration/Configuration.swift index d5a9b6c..cec418e 100644 --- a/RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift +++ b/Sources/RolePlayingCore/Configuration/Configuration.swift @@ -8,12 +8,12 @@ import Foundation -// TODO: this needs work. Nominally it's purpose is to help integrate related classes, -// but because we don't have much in terms of requirements, it's not doing much besides -// wiring up species and classes to players. - +/// Represents a collection of JSON file names that belong to a bundle. +/// Used by the `Configuration`. public struct ConfigurationFiles: Decodable { let currencies: [String] + let skills: [String] + let backgrounds: [String] let species: [String] let classes: [String] let players: [String]? @@ -21,6 +21,8 @@ public struct ConfigurationFiles: Decodable { private enum CodingKeys: String, CodingKey { case currencies + case skills + case backgrounds case species case classes case players @@ -28,13 +30,15 @@ public struct ConfigurationFiles: Decodable { } } -/// This is designed to configure a client from a framework or application bundle. +/// Configure a client's data from a framework or application bundle. public struct Configuration { - let bundle: Bundle public var configurationFiles: ConfigurationFiles + public var currencies = Currencies() + public var backgrounds = Backgrounds() + public var skills = Skills() public var species = Species() public var classes = Classes() public var players = Players() @@ -52,18 +56,31 @@ public struct Configuration { for currenciesFile in configurationFiles.currencies { let jsonData = try bundle.loadJSON(currenciesFile) - _ = try jsonDecoder.decode(Currencies.self, from: jsonData) + let currencies = try jsonDecoder.decode(Currencies.self, from: jsonData) + self.currencies.add(currencies.all) + } + + for skillsFile in configurationFiles.skills { + let jsonData = try bundle.loadJSON(skillsFile) + let skills = try jsonDecoder.decode(Skills.self, from: jsonData) + self.skills.add(skills.all) + } + + for backgroundsFile in configurationFiles.backgrounds { + let jsonData = try bundle.loadJSON(backgroundsFile) + let backgrounds = try jsonDecoder.decode(Backgrounds.self, from: jsonData, configuration: self) + self.backgrounds.add(backgrounds.all) } for speciesFile in configurationFiles.species { let jsonData = try bundle.loadJSON(speciesFile) - let species = try jsonDecoder.decode(Species.self, from: jsonData) + let species = try jsonDecoder.decode(Species.self, from: jsonData, configuration: self) self.species.species += species.species } for classFile in configurationFiles.classes { let jsonData = try bundle.loadJSON(classFile) - let classes = try jsonDecoder.decode(Classes.self, from: jsonData) + let classes = try jsonDecoder.decode(Classes.self, from: jsonData, configuration: self) self.classes.classes += classes.classes // Update the shared classes experience points table, then @@ -79,11 +96,9 @@ public struct Configuration { if let playersFiles = configurationFiles.players { for playersFile in playersFiles { let jsonData = try bundle.loadJSON(playersFile) - let players = try jsonDecoder.decode(Players.self, from: jsonData) - try players.resolve(classes: self.classes, species: self.species) + let players = try jsonDecoder.decode(Players.self, from: jsonData, configuration: self) self.players.players += players.players } } } - } diff --git a/Sources/RolePlayingCore/Configuration/ConfigurationError.swift b/Sources/RolePlayingCore/Configuration/ConfigurationError.swift new file mode 100644 index 0000000..3d1fbc4 --- /dev/null +++ b/Sources/RolePlayingCore/Configuration/ConfigurationError.swift @@ -0,0 +1,56 @@ +// +// ConfigurationError.swift +// RolePlayingCore +// +// Created by Brian Arnold on 10/31/25. +// + +/// Configuration errors that may be detected at runtime. +public enum ConfigurationError: Error { + + /// File is not found. + case missingFile(String, String, String) + + /// File is not JSON format. + case missingJSON(String, String) + + /// Missing a type that belongs to the configuration. + case missingType(String, String, String) +} + +/// Returns a missingFile ConfigurationError with the specified message. +/// The source function, file and line are recorded in location. +public func missingFileError(_ fileName: String, _ bundlePath: String, source: String = #function, file: String = #file, line: Int = #line) -> Error { + return ConfigurationError.missingFile(fileName, bundlePath, "Thrown in \(source) (File: \(file) Line: \(line))") +} + +/// Returns a badJSON ConfigurationError with the specified message. +/// The source function, file and line are recorded in location. +public func missingJSONError(_ name: String, source: String = #function, file: String = #file, line: Int = #line) -> Error { + return ConfigurationError.missingJSON(name, "Thrown in \(source) (File: \(file) Line: \(line))") +} + +/// Returns a missingType ConfigurationError with the specified message. +/// The source function, file and line are recorded in location. +public func missingTypeError(_ kind: String, _ name: String, source: String = #function, file: String = #file, line: Int = #line) -> Error { + return ConfigurationError.missingType(kind, name, "Thrown in \(source) (File: \(file) Line: \(line))") +} + +extension ConfigurationError: CustomStringConvertible { + + private static let prefix = "Configuration error" + + /// Returns a description of the error, including the type, message, + /// source function, file and line. + public var description: String { + switch self { + case .missingFile(let fileName, let bundlePath, let location): + return "\(Self.prefix): Missing file \(fileName) in bundle \(bundlePath)\n\(location)" + case .missingJSON(let name, let location): + return "\(Self.prefix): Missing \(name) in configuration file\n\(location)" + case .missingType(let kind, let name, let location): + return "\(Self.prefix): Could not resolve \(kind) named \(name)\n\(location)" + } + } + +} diff --git a/Sources/RolePlayingCore/Configuration/JSONFile.swift b/Sources/RolePlayingCore/Configuration/JSONFile.swift new file mode 100644 index 0000000..3018d4b --- /dev/null +++ b/Sources/RolePlayingCore/Configuration/JSONFile.swift @@ -0,0 +1,26 @@ +// +// JSONFile.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/15/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Foundation + +extension Bundle { + + /// Loads a specified JSON-formatted UTF-8 file from this bundle's resources, and returns its dictionary. + /// + /// - parameter fileName: The name of the file, without its extension. + /// - parameter source: The name of the calling function. Defaults to `#function`. + /// - parameter file: The name of the file where the error occurred. Defaults to `#file`. + /// - parameter line: The line number where the error occurred. Defaults to `#line`. + /// + /// - throws: `ConfigurationError.missingFile` if the file is missing. + /// - throws: `NSError` if the file can't be read. + public func loadJSON(_ fileName: String, source: String = #function, file: String = #file, line: Int = #line) throws -> Data { + guard let url = self.url(forResource: fileName, withExtension: "json") else { throw missingFileError("\(fileName).json", "\(self.bundleURL)", source: source, file: file, line: line) } + return try Data(contentsOf: url, options: [.mappedIfSafe]) + } +} diff --git a/Sources/RolePlayingCore/Currency/Currencies.swift b/Sources/RolePlayingCore/Currency/Currencies.swift new file mode 100644 index 0000000..08a7921 --- /dev/null +++ b/Sources/RolePlayingCore/Currency/Currencies.swift @@ -0,0 +1,59 @@ +// +// Currencies.swift +// RolePlayingCore +// +// Created by Brian Arnold on 6/24/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Foundation + +/// A collection of currencies. +public struct Currencies: Codable { + + /// A dictionary of currencies indexed by currency symbol. + private var allCurrencies: [String: UnitCurrency] = [:] + + /// A read-only array of currencies. + var all: [UnitCurrency] { Array(allCurrencies.values) } + + /// Returns a currencies instance that can access a currency by symbol, and a base unit currency if specified. + public init(_ currencies: [UnitCurrency] = []) { + add(currencies) + } + + /// Accesses the currency with the specified symbol. + public subscript(symbol: String) -> UnitCurrency? { + return allCurrencies[symbol] + } + + /// Adds the array of currencies to the collection. Updates the default or base unit currency if specified. + mutating func add(_ currencies: [UnitCurrency]) { + allCurrencies = Dictionary(currencies.map { ($0.symbol, $0) }, uniquingKeysWith: { _, last in last }) + + if let baseCurrency = allCurrencies.first(where: { $0.value.isDefault }) { + UnitCurrency.setBaseUnit(baseCurrency.value) + } + } + + // MARK: Codable conformance + + private enum CodingKeys: String, CodingKey { + case currencies + } + + /// Decodes an array of currencies, updating the default or base unit currency if specified. + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let currencies = try container.decode([UnitCurrency].self, forKey: .currencies) + add(currencies) + } + + /// Encodes an array of currencies. + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(all, forKey: .currencies) + } +} diff --git a/RolePlayingCore/RolePlayingCore/Currency/Money.swift b/Sources/RolePlayingCore/Currency/Money.swift similarity index 53% rename from RolePlayingCore/RolePlayingCore/Currency/Money.swift rename to Sources/RolePlayingCore/Currency/Money.swift index 860d457..0261894 100644 --- a/RolePlayingCore/RolePlayingCore/Currency/Money.swift +++ b/Sources/RolePlayingCore/Currency/Money.swift @@ -11,14 +11,42 @@ import Foundation /// A measurement of currency. public typealias Money = Measurement +// NOTE: @retroactive is a compiler-suggested workaround in case Measurement +// adopts CodableWithConfiguration in the future. +extension Money: @retroactive CodableWithConfiguration { + + public init(from decoder: any Decoder, configuration: Currencies) throws { + let container = try decoder.singleValueContainer() + + if let double = try? container.decode(Double.self) { + self = Money(value: double, unit: UnitCurrency.baseUnit()) + } else { + let string = try container.decode(String.self) + if let money = string.parseMoney(configuration) { + self = money + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Failed to decode Money from \"\(string)\"") + } + } + } + + public func encode(to encoder: any Encoder, configuration: Currencies) throws { + var container = encoder.singleValueContainer() + try container.encode(self.description) + } +} + public extension String { /// Parses numbers with currency symbols into money. - var parseMoney: Money? { + /// If there is no currency symbol, the number is associated with the base unit currency. + func parseMoney(_ configuration: Currencies) -> Money? { var value: Double? var unit: UnitCurrency = .baseUnit() - for currency in Currencies.allCurrencies.values { + // Get a thread-safe snapshot of all currencies + let allCurrencies = configuration.all + for currency in allCurrencies { if let range = self.range(of: currency.symbol), range.upperBound == self.endIndex { value = Double(self[.. Money { - let money: Money? - - if let double = try? self.decode(Double.self, forKey: key) { - money = Money(value: double, unit: .baseUnit()) - } else { - money = try self.decode(String.self, forKey: key).parseMoney - } + guard let value else { return nil } - // Throw if we were unsuccessful parsing. - guard money != nil else { - let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Missing string or number for Money value") - throw DecodingError.dataCorrupted(context) - } - - return money! + return Money(value: value, unit: unit) } - - /// Decodes either a number or a string into Money, if present. - /// - /// - throws `DecodingError.dataCorrupted` if the money could not be decoded. - func decodeIfPresent(_ type: Money.Type, forKey key: K) throws -> Money? { - let money: Money? - - if let double = try? self.decode(Double.self, forKey: key) { - money = Money(value: double, unit: .baseUnit()) - } else if let string = try self.decodeIfPresent(String.self, forKey: key) { - money = string.parseMoney - } else { - money = nil - } - - return money - } - } - extension MeasurementFormatter { /// The formatter requires a specialization that knows how to find UnitCurrency's baseUnit() and @@ -95,5 +79,4 @@ extension MeasurementFormatter { let formatString = unitStyle == .short ? "%@%@" : "%@ %@" return String(format: formatString, valueString, unitsString!) } - } diff --git a/Sources/RolePlayingCore/Currency/UnitCurrency.swift b/Sources/RolePlayingCore/Currency/UnitCurrency.swift new file mode 100644 index 0000000..6193a8b --- /dev/null +++ b/Sources/RolePlayingCore/Currency/UnitCurrency.swift @@ -0,0 +1,92 @@ +// +// UnitCurrency.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/5/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Foundation +import Synchronization + +/// Units of currency or coinage. +/// +/// Use `Measurement` to hold values of currency. +public final class UnitCurrency : Dimension, @unchecked Sendable { + + /// The singular unit name used when the unitStyle is long. + public internal(set) var name: String! + + /// The plural unit name used when the unitStyle is long. + public internal(set) var plural: String! + + /// Whether this currency is meant to be the default or base unit currency. + public internal(set) var isDefault: Bool = false + + /// The default or base unit currency. + private static let base: Mutex = Mutex(UnitCurrency(symbol: "Credits", converter: UnitConverterLinear(coefficient: 1.0), name: "Credit", plural: "Credits")) + + /// Sets the base unit currency using a Mutex to manage concurrency. + public static func setBaseUnit(_ unit: UnitCurrency) { + base.withLock { $0 = unit } + } + + /// Returns the base unit currency using a Mutex to manage concurrency. + public override class func baseUnit() -> UnitCurrency { + base.withLock { return $0 } + } + + public init(symbol: String, converter: UnitConverter, name: String, plural: String, isDefault: Bool = false) { + self.name = name + self.plural = plural + self.isDefault = isDefault + super.init(symbol: symbol, converter: converter) + } + + // MARK: NSCoding Support + + /// Required initializer for NSCoding support, because `Dimension` conforms to `NSSecureCoding`. + /// Since this class has a custom initializer, Swift requires all designated initializers from the superclass. + /// + /// This library actually uses `Codable` for serialization (see below). + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } +} + +// MARK: Codable support + +extension UnitCurrency: Codable { + + private enum CodingKeys: String, CodingKey { + case symbol + case coefficient + case name + case plural + case isDefault = "is default" + } + + public convenience init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let symbol = try container.decode(String.self, forKey: .symbol) + let coefficient = try container.decode(Double.self, forKey: .coefficient) + let name = try container.decode(String.self, forKey: .name) + let plural = try container.decode(String.self, forKey: .plural) + let isDefault = try container.decodeIfPresent(Bool.self, forKey: .isDefault) ?? false + + let converter = UnitConverterLinear(coefficient: coefficient) + self.init(symbol: symbol, converter: converter, name: name, plural: plural, isDefault: isDefault) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.symbol, forKey: .symbol) + try container.encode(self.converter.baseUnitValue(fromValue: 1.0), forKey: .coefficient) + try container.encode(self.name, forKey: .name) + try container.encode(self.plural, forKey: .plural) + if self.isDefault { + try container.encode(self.isDefault, forKey: .isDefault) + } + } +} diff --git a/RolePlayingCore/RolePlayingCore/Dice/CompoundDice.swift b/Sources/RolePlayingCore/Dice/CompoundDice.swift similarity index 74% rename from RolePlayingCore/RolePlayingCore/Dice/CompoundDice.swift rename to Sources/RolePlayingCore/Dice/CompoundDice.swift index 3bc3e4c..49df869 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/CompoundDice.swift +++ b/Sources/RolePlayingCore/Dice/CompoundDice.swift @@ -12,10 +12,9 @@ import Foundation /// General-purpose composition of dice rolls. /// /// The two primary use cases for this type are: -/// - combining two rolls, e.g., "2d4+d6", -/// - using a modifier, e.g., "d12+2". +/// - combining two rolls, e.g., "`2d4+d6`", +/// - using a modifier, e.g., "`d12+2`". public struct CompoundDice: Dice { - public let lhs: Dice public let rhs: Dice public let mathOperator: String @@ -40,15 +39,20 @@ public struct CompoundDice: Dice { internal typealias MathOperator = (Int, Int) -> Int /// Mapping of strings to function signatures. - internal static let mathOperators: [String: MathOperator] = ["+": (+), "-": (-), "x": (*), "*": (*), "/": (/)] - - /// Rolls the specified number of times, optionally adding or multiplying a modifier, - /// and returning the result. + internal let mathOperators: [String: MathOperator] = ["+": (+), "-": (-), "x": (*), "*": (*), "/": (/)] + + /// Rolls the dice on both sides and combines them with the math operator, + /// returning the result. public func roll() -> DiceRoll { let lhsRoll = lhs.roll() let rhsRoll = rhs.roll() - let result = CompoundDice.mathOperators[mathOperator]!(lhsRoll.result, rhsRoll.result) + guard let operation = mathOperators[mathOperator] else { + // Fallback if operator is unknown (shouldn't happen if properly constructed) + return DiceRoll(lhsRoll.result, "\(lhsRoll.description) ? \(rhsRoll.description)") + } + + let result = operation(lhsRoll.result, rhsRoll.result) let description = "\(lhsRoll.description) \(mathOperator) \(rhsRoll.description)" return DiceRoll(result, description) @@ -59,5 +63,4 @@ public struct CompoundDice: Dice { /// Returns a description of the left and right hand sides with the math operator. public var description: String { "\(lhs)\(mathOperator)\(rhs)" } - } diff --git a/RolePlayingCore/RolePlayingCore/Dice/Dice.swift b/Sources/RolePlayingCore/Dice/Dice.swift similarity index 98% rename from RolePlayingCore/RolePlayingCore/Dice/Dice.swift rename to Sources/RolePlayingCore/Dice/Dice.swift index f3f3e36..a18c7ad 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/Dice.swift +++ b/Sources/RolePlayingCore/Dice/Dice.swift @@ -16,5 +16,4 @@ public protocol Dice: CustomStringConvertible { /// Returns the number of dice sides. var sides: Int { get } - } diff --git a/RolePlayingCore/RolePlayingCore/Dice/DiceModifier.swift b/Sources/RolePlayingCore/Dice/DiceModifier.swift similarity index 74% rename from RolePlayingCore/RolePlayingCore/Dice/DiceModifier.swift rename to Sources/RolePlayingCore/Dice/DiceModifier.swift index caf3ae0..000b348 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/DiceModifier.swift +++ b/Sources/RolePlayingCore/Dice/DiceModifier.swift @@ -9,7 +9,6 @@ /// A dice modifier is a constant value that can take the place of a Dice instance. public struct DiceModifier: Dice { - public let modifier: Int public init(_ modifier: Int) { @@ -20,8 +19,10 @@ public struct DiceModifier: Dice { return DiceRoll(modifier, "\(modifier)") } + /// Returns the modifier value to conform to the Dice protocol. + /// Note: A modifier doesn't have "sides" in the traditional sense, + /// but this property is required by the protocol. public var sides: Int { modifier } public var description: String { "\(modifier)" } - } diff --git a/Sources/RolePlayingCore/Dice/DiceParser.swift b/Sources/RolePlayingCore/Dice/DiceParser.swift new file mode 100644 index 0000000..509fb7a --- /dev/null +++ b/Sources/RolePlayingCore/Dice/DiceParser.swift @@ -0,0 +1,401 @@ +// +// DiceParser.swift +// RolePlayingCore +// +// Created by Brian Arnold on 11/23/16. +// Copyright © 2016-2017 Brian Arnold. All rights reserved. +// + +import Foundation + +// MARK: - Parse Errors + +/// Types of errors handled by this parser. +enum DiceParseError: Error, LocalizedError { + case invalidCharacter(String) + case invalidDieSides(Int) + case missingMinus + case missingSimpleDice + case missingDieSides + case missingExpression + case consecutiveNumbers + case consecutiveMathOperators + case consecutiveDiceExpressions + + var errorDescription: String? { + switch self { + case .invalidCharacter(let char): + return "Invalid character '\(char)' in dice expression" + case .invalidDieSides(let sides): + return "Invalid die with \(sides) sides" + case .missingMinus: + return "Drop modifier requires '-' operator" + case .missingSimpleDice: + return "Drop modifier can only be applied to simple dice" + case .missingDieSides: + return "Die specification missing number of sides" + case .missingExpression: + return "Math operator missing right-hand expression" + case .consecutiveNumbers: + return "Cannot have consecutive numbers without an operator" + case .consecutiveMathOperators: + return "Cannot have consecutive math operators" + case .consecutiveDiceExpressions: + return "Cannot have consecutive dice expressions without an operator" + } + } +} + +// MARK: - Token + + +/// Types of tokens supported by this parser. +enum Token { + case number(Int) + case mathOperator(String) + case die + case drop(String) + + // MARK: Character Sets + + private static let mathOperatorCharacters = CharacterSet(charactersIn: "+-x*/") + private static let dieCharacters = CharacterSet(charactersIn: "dD") + private static let dropCharacters = CharacterSet( + charactersIn: DroppingDice.Drop.allCases.map(\.rawValue).joined() + ) + private static let percentCharacters = CharacterSet(charactersIn: "%") + + // MARK: Initialization + + init?(from scalar: UnicodeScalar) { + switch scalar { + case _ where Self.mathOperatorCharacters.contains(scalar): + self = .mathOperator(String(scalar)) + case _ where Self.dieCharacters.contains(scalar): + self = .die + case _ where Self.dropCharacters.contains(scalar): + self = .drop(String(scalar)) + case _ where Self.percentCharacters.contains(scalar): + self = .number(100) + default: + return nil + } + } + + // MARK: Properties + + var isDropping: Bool { + if case .drop = self { return true } + return false + } +} + +// MARK: - Number Buffer + +/// An internal buffer for parsing numbers from a string. +private struct NumberBuffer { + private var buffer = "" + + var isEmpty: Bool { buffer.isEmpty } + + mutating func append(_ scalar: UnicodeScalar) { + buffer.append(String(scalar)) + } + + mutating func flush() -> Int? { + guard !buffer.isEmpty else { return nil } + defer { buffer = "" } + return Int(buffer) + } +} + +// MARK: - Tokenizer + +/// Converts a dice-formatted string into a sequence of tokens. +/// +/// - Parameter string: The string to tokenize (e.g., "2d6+3") +/// - Returns: An array of tokens representing the parsed string +/// - Throws: `DiceParseError.invalidCharacter` if an unknown character is encountered +func tokenize(_ string: String) throws -> [Token] { + var tokens: [Token] = [] + var numberBuffer = NumberBuffer() + + for scalar in string.unicodeScalars { + if CharacterSet.decimalDigits.contains(scalar) { + // Numbers consume multiple characters + numberBuffer.append(scalar) + } else { + // Flush any accumulated number before processing the next character + if let value = numberBuffer.flush() { + tokens.append(.number(value)) + } + + // Skip whitespace + guard !CharacterSet.whitespacesAndNewlines.contains(scalar) else { continue } + + // Parse token or throw error for invalid characters + guard let token = Token(from: scalar) else { + throw DiceParseError.invalidCharacter(String(scalar)) + } + + tokens.append(token) + } + } + + // Flush any remaining number + if let value = numberBuffer.flush() { + tokens.append(.number(value)) + } + + return tokens +} + +// MARK: - Parser State + +/// The internal state of the parser when it processes tokens. +private struct DiceParserState { + private(set) var lastNumber: Int? + private(set) var lastDice: Dice? + private(set) var lastMathOperator: String? + private(set) var isParsingDie = false + + // MARK: Parsing Methods + + /// Parses a number and stores it either in `lastDice.sides` or `lastNumber`. + /// + /// - Parameter number: The number value to parse + /// - Throws: `DiceParseError` if consecutive numbers or dice expressions are encountered + mutating func parse(number: Int) throws { + if isParsingDie { + guard lastDice == nil else { + throw DiceParseError.consecutiveDiceExpressions + } + guard let die = Die(rawValue: number) else { + throw DiceParseError.invalidDieSides(number) + } + + let times = lastNumber ?? 1 + lastDice = SimpleDice(die, times: times) + isParsingDie = false + lastNumber = nil + } else { + guard lastNumber == nil else { + throw DiceParseError.consecutiveNumbers + } + lastNumber = number + } + } + + /// Initiates parsing a die expression. + /// The die is completed when parsing dice sides as an integer. + /// + /// - Throws: `DiceParseError.consecutiveDiceExpressions` if already parsing a die + mutating func parseDie() throws { + guard !isParsingDie else { + throw DiceParseError.consecutiveDiceExpressions + } + isParsingDie = true + } + + /// Parses a dropping dice modifier supported by `DroppingDice`. + /// Must be preceded by a `SimpleDice` and a '-' math operator. + /// + /// - Parameter drop: The drop modifier string ("L" or "H") + /// - Throws: `DiceParseError` if preconditions are not met + mutating func parse(drop: String) throws { + guard let simpleDice = lastDice as? SimpleDice else { + throw DiceParseError.missingSimpleDice + } + guard lastMathOperator == "-" else { + throw DiceParseError.missingMinus + } + + let diceDrop = DroppingDice.Drop(rawValue: drop)! + lastDice = DroppingDice(simpleDice, drop: diceDrop) + lastMathOperator = nil + } + + /// Parses a math operator supported by `CompoundDice`. + /// + /// - Parameter math: The math operator string + /// - Throws: `DiceParseError.consecutiveMathOperators` if another operator is pending + mutating func parse(math: String) throws { + guard lastMathOperator == nil else { + throw DiceParseError.consecutiveMathOperators + } + lastMathOperator = math + } + + /// Returns a `Dice` from either the last number (`DiceModifier`) or `lastDice`, + /// and resets their state. + /// + /// - Returns: A `Dice` instance or `nil` if no pending dice exist + mutating func flush() -> Dice? { + if let number = lastNumber { + lastNumber = nil + return DiceModifier(number) + } else if let dice = lastDice { + lastDice = nil + return dice + } + return nil + } + + /// Returns combined dice from the current parsed dice and the current parse state. + /// + /// If there is no current parsed dice, the current parse state is returned. + /// If there is no math operator or no right-hand side, the left-hand side is returned. + /// + /// - Parameter lhsDice: The left-hand side dice to combine with current state + /// - Returns: The combined dice or the original dice if no combination is possible + mutating func combine(_ lhsDice: Dice?) -> Dice? { + guard let lhsDice else { return flush() } + guard let mathOperator = lastMathOperator, let rhsDice = flush() else { + return lhsDice + } + + // Combine left-hand side, math operator, and right-hand side + lastMathOperator = nil + return CompoundDice(lhs: lhsDice, rhs: rhsDice, mathOperator: mathOperator) + } + + /// Checks for invalid or incomplete state at the end of parsing. + /// + /// - Throws: `DiceParseError` if the parser is in an incomplete state + func validate() throws { + if isParsingDie { + throw DiceParseError.missingDieSides + } else if lastMathOperator != nil { + throw DiceParseError.missingExpression + } + } +} + +// MARK: - Parser + +/// Look-ahead at the next token and return whether it's a `.drop` token. +private func isNextTokenDropping(_ tokens: [Token], after index: Int) -> Bool { + guard index + 1 < tokens.count else { return false } + return tokens[index + 1].isDropping +} + +/// Converts an array of tokens into a `Dice` object. +/// +/// - Parameter tokens: The tokens to parse +/// - Returns: A `Dice` instance representing the parsed expression, or `nil` if empty +/// - Throws: `DiceParseError` if the token sequence is invalid +func parse(_ tokens: [Token]) throws -> Dice? { + var parsedDice: Dice? + var state = DiceParserState() + + for (index, token) in tokens.enumerated() { + switch token { + case .number(let value): + try state.parse(number: value) + + case .die: + try state.parseDie() + + case .drop(let drop): + try state.parse(drop: drop) + + case .mathOperator(let math): + // Only combine if the next token isn't a drop modifier + if !isNextTokenDropping(tokens, after: index) { + parsedDice = state.combine(parsedDice) + } + try state.parse(math: math) + } + } + + parsedDice = state.combine(parsedDice) + try state.validate() + + return parsedDice +} + +// MARK: - String Extension + +public extension String { + + /// Creates a `Dice` instance from a dice notation string. + /// + /// Supported format: `[]d[|-]*` + /// + /// Examples: + /// - `"d8"` → Simple 8-sided die + /// - `"2d12+2"` → Two 12-sided dice plus 2 + /// - `"4d6-L"` → Four 6-sided dice, drop lowest + /// - `"1"` → Constant modifier of 1 + /// - `"2d4+3d12-4"` → Compound expression + /// + /// Supported dice sides: 4, 6, 8, 10, 12, 20, and % (100) + /// + /// - Returns: A `Dice` instance, or `nil` if the string cannot be parsed + var parseDice: Dice? { + do { + let tokens = try tokenize(self) + return try parse(tokens) + } catch { + print("Error parsing dice: \(error.localizedDescription)") + return nil + } + } +} + +// MARK: - Decoding Extensions + +public extension KeyedDecodingContainer { + + /// Decodes either an integer or a dice notation string into a `Dice`. + /// + /// - Parameters: + /// - type: The `Dice.Protocol` metatype + /// - key: The coding key for the value + /// - Returns: A decoded `Dice` instance + /// - Throws: `DecodingError.dataCorrupted` if the value cannot be decoded as dice + func decode(_ type: Dice.Protocol, forKey key: K) throws -> Dice { + // Try decoding as an integer first (for constant modifiers) + if let number = try? decode(Int.self, forKey: key) { + return DiceModifier(number) + } + + // Try decoding as a string and parsing as dice notation + if let string = try? decode(String.self, forKey: key), + let dice = string.parseDice { + return dice + } + + // Throw if neither approach succeeded + let context = DecodingError.Context( + codingPath: codingPath, + debugDescription: "Could not decode Dice from string or number" + ) + throw DecodingError.dataCorrupted(context) + } + + /// Decodes either an integer or a dice notation string into a `Dice`, if present. + /// + /// - Parameters: + /// - type: The `Dice.Protocol` metatype + /// - key: The coding key for the value + /// - Returns: A decoded `Dice` instance, or `nil` if the key is not present + /// - Throws: `DecodingError.dataCorrupted` if the value is present but cannot be decoded + func decodeIfPresent(_ type: Dice.Protocol, forKey key: K) throws -> Dice? { + // Return nil if the key doesn't exist + guard contains(key) else { return nil } + + // Try decoding as an integer first + if let number = try? decode(Int.self, forKey: key) { + return DiceModifier(number) + } + + // Try decoding as a string and parsing as dice notation + if let string = try? decode(String.self, forKey: key) { + return string.parseDice + } + + return nil + } +} + diff --git a/RolePlayingCore/RolePlayingCore/Dice/DiceRoll.swift b/Sources/RolePlayingCore/Dice/DiceRoll.swift similarity index 90% rename from RolePlayingCore/RolePlayingCore/Dice/DiceRoll.swift rename to Sources/RolePlayingCore/Dice/DiceRoll.swift index 3ddccde..c65146b 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/DiceRoll.swift +++ b/Sources/RolePlayingCore/Dice/DiceRoll.swift @@ -8,12 +8,12 @@ /// Encapsulates a result with its intermediate values. public struct DiceRoll: CustomStringConvertible { - + /// The result of the roll. public let result: Int /// A string representing the intermediate values of the dice roll. - /// For example, a "3d6" might return "(4+1+5)". + /// For example, a "`3d6`" might return "`(4+1+5)`". public let description: String /// Creates a roll with its accompanying description of intermediate values. @@ -21,6 +21,5 @@ public struct DiceRoll: CustomStringConvertible { self.result = result self.description = description } - } diff --git a/RolePlayingCore/RolePlayingCore/Dice/Die.swift b/Sources/RolePlayingCore/Dice/Die.swift similarity index 81% rename from RolePlayingCore/RolePlayingCore/Dice/Die.swift rename to Sources/RolePlayingCore/Dice/Die.swift index 38d390d..4f99824 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/Die.swift +++ b/Sources/RolePlayingCore/Dice/Die.swift @@ -15,7 +15,8 @@ public enum Die: Int { case d10 = 10 case d12 = 12 case d20 = 20 - case d100 = 100 // AKA "d%" + /// Also known as "`d%`" + case d100 = 100 /// Rolls once and returns a number between 1 and this dice type. public func roll(using generator: inout G) -> Int { @@ -30,19 +31,12 @@ public enum Die: Int { /// Rolls the specified number of times and returns an array of numbers between 1 and this dice type. public func roll(_ times: Int) -> [Int] { - var rolls = [Int](repeating: 0, count: times) // preallocating is much faster than appending - - for index in 0 ..< times { - rolls[index] = roll() - } - return rolls + return (0.. DiceRoll { - let lastRoll: [Int] = dice.roll() - let droppedRoll = (drop == .lowest) ? lastRoll.min() : lastRoll.max() + let lastRoll = dice.rollAll() + + guard let droppedRoll = drop == .lowest ? lastRoll.min() : lastRoll.max() else { + // Edge case: no rolls to drop (shouldn't happen in practice) + return DiceRoll(0, "(0)") + } - let result = lastRoll.reduce(0, +) - droppedRoll! - let description = "(\(dice.rollDescription(lastRoll)) - \(droppedRoll!))" + let result = lastRoll.reduce(0, +) - droppedRoll + let description = "(\(dice.rollDescription(lastRoll)) - \(droppedRoll))" return DiceRoll(result, description) } @@ -55,5 +58,4 @@ public struct DroppingDice: Dice { public var description: String { return "\(dice)-\(drop.rawValue)" } - } diff --git a/RolePlayingCore/RolePlayingCore/Dice/SimpleDice.swift b/Sources/RolePlayingCore/Dice/SimpleDice.swift similarity index 75% rename from RolePlayingCore/RolePlayingCore/Dice/SimpleDice.swift rename to Sources/RolePlayingCore/Dice/SimpleDice.swift index d17b64d..04bd19c 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/SimpleDice.swift +++ b/Sources/RolePlayingCore/Dice/SimpleDice.swift @@ -9,7 +9,6 @@ /// A simple dice has a single die, and an optional number of times to roll. public struct SimpleDice: Dice { - public let die: Die public let times: Int @@ -21,17 +20,15 @@ public struct SimpleDice: Dice { } /// Rolls the specified number of times, returning the array of rolls. - internal func roll() -> [Int] { + internal func rollAll() -> [Int] { return die.roll(times) } /// Rolls the specified number of times, returning the sum of the rolls and a description. public func roll() -> DiceRoll { - let lastRoll: [Int] = roll() - + let lastRoll = rollAll() let result = lastRoll.reduce(0, +) let description = rollDescription(lastRoll) - return DiceRoll(result, description) } @@ -46,21 +43,15 @@ public struct SimpleDice: Dice { /// Returns the last roll as a sequence of added numbers in parenthesis. internal func rollDescription(_ lastRoll: [Int]) -> String { - var resultString: String + guard !lastRoll.isEmpty else { return "0" } - var rolls = lastRoll - let last = rolls.popLast()! - if rolls.count == 0 { - resultString = "\(last)" - } else { - resultString = "(" - for roll in rolls { - resultString += "\(roll) + " - } - resultString += "\(last))" + // Single roll doesn't need parentheses + guard lastRoll.count > 1 else { + return "\(lastRoll[0])" } - return resultString + // Multiple rolls: format as (roll1 + roll2 + ... + rollN) + let rollsString = lastRoll.map(String.init).joined(separator: " + ") + return "(\(rollsString))" } - } diff --git a/RolePlayingCore/RolePlayingCore/Player/Ability.swift b/Sources/RolePlayingCore/Player/Ability.swift similarity index 93% rename from RolePlayingCore/RolePlayingCore/Player/Ability.swift rename to Sources/RolePlayingCore/Player/Ability.swift index acb01cd..e0d8e98 100644 --- a/RolePlayingCore/RolePlayingCore/Player/Ability.swift +++ b/Sources/RolePlayingCore/Player/Ability.swift @@ -6,15 +6,14 @@ // Copyright © 2016-2017 Brian Arnold. All rights reserved. // -public struct Ability { - +/// A named ability. +public struct Ability: Sendable { public let name: String /// Creates an ability name. public init(_ name: String) { self.name = name } - } extension Ability: Equatable { } @@ -28,7 +27,6 @@ extension String { let index = self.index(self.startIndex, offsetBy: min(self.count, 3)) return self[.. BackgroundTraits? { + return allBackgrounds[backgroundName] + } + + /// Returns the number of background traits in the collection. + public var count: Int { allBackgrounds.count } + + /// Accesses a background traits instance by index. + public subscript(index: Int) -> BackgroundTraits? { + guard index >= 0 && index < count else { return nil } + return all[index] + } + + /// Returns a random background traits instance using the specified random index generator. + public func randomElementByIndex(using generator: inout G) -> BackgroundTraits { + return all.randomElementByIndex(using: &generator)! + } + + // MARK: Codable conformance + + private enum CodingKeys: String, CodingKey { + case backgrounds + } + + public init(from decoder: Decoder, configuration: Configuration) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + let backgrounds = try values.decode([BackgroundTraits].self, forKey: .backgrounds, configuration: configuration) + add(backgrounds) + } + + public func encode(to encoder: Encoder, configuration: Configuration) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(all, forKey: .backgrounds, configuration: configuration) + } +} diff --git a/Sources/RolePlayingCore/Player/ClassTraits.swift b/Sources/RolePlayingCore/Player/ClassTraits.swift new file mode 100644 index 0000000..2155215 --- /dev/null +++ b/Sources/RolePlayingCore/Player/ClassTraits.swift @@ -0,0 +1,184 @@ +// +// Class.swift +// RolePlayingCore +// +// Created by Brian Arnold on 11/12/16. +// Copyright © 2016-2017 Brian Arnold. All rights reserved. +// + +import Foundation + +/// Traits representing a class. +public struct ClassTraits { + public var name: String + public var plural: String + public var hitDice: Dice + public var startingWealth: Dice + + public var descriptiveTraits: [String: String] + public var primaryAbility: [Ability] + public var alternatePrimaryAbility: [Ability]? + public var savingThrows: [Ability] + public var experiencePoints: [Int]? + public var startingSkillCount: Int + public var skillProficiencies: [Skill] + public var weaponProficiencies: [String] + public var toolProficiencies: [String] + public var armorTraining: [String] + public var startingEquipment: [[String]] + + /// Accesses the experiencePoints array for the specified 1-based level. + public func minExperiencePoints(at level: Int) -> Int { + // Map the level to an index of the array + let index = max(1, level) - 1 + guard let experiencePoints else { return 0 } + guard index < experiencePoints.count else { return experiencePoints.last ?? 0 } + return experiencePoints[index] + } + + /// Accesses the maximum level for this class. + public var maxLevel: Int { + guard let experiencePoints else { return 0 } + return experiencePoints.count + } + + /// Accesses the maximum experience points for the specified 1-based level. + public func maxExperiencePoints(at level: Int) -> Int { + guard level > 0 else { return 0 } + + // One less than the minimum for the next level + return minExperiencePoints(at: level + 1) - 1 + } + + // TODO: weapons, armor, skills, etc. + + public init(name: String, + plural: String, + hitDice: Dice, + startingWealth: Dice, + descriptiveTraits: [String: String] = [:], + primaryAbility: [Ability] = [], + alternatePrimaryAbility: [Ability]? = nil, + savingThrows: [Ability] = [], + startingSkillCount: Int = 2, + skillProficiencies: [Skill] = [], + weaponProficiencies: [String] = [], + toolProficiencies: [String] = [], + armorTraining: [String] = [], + startingEquipment: [[String]] = [], + experiencePoints: [Int]? = nil) { + self.name = name + self.plural = plural + self.hitDice = hitDice + self.startingWealth = startingWealth + + self.descriptiveTraits = descriptiveTraits + self.primaryAbility = primaryAbility + self.alternatePrimaryAbility = alternatePrimaryAbility + self.savingThrows = savingThrows + self.startingSkillCount = startingSkillCount + self.skillProficiencies = skillProficiencies + self.weaponProficiencies = weaponProficiencies + self.toolProficiencies = toolProficiencies + self.armorTraining = armorTraining + self.startingEquipment = startingEquipment + self.experiencePoints = experiencePoints + } +} + +extension ClassTraits: CodableWithConfiguration { + + private enum CodingKeys: String, CodingKey { + case name + case plural + case hitDice = "hit dice" + case startingWealth = "starting wealth" + case descriptiveTraits = "descriptive traits" + case primaryAbility = "primary ability" + case alternatePrimaryAbility = "alternate primary ability" + case savingThrows = "saving throws" + case startingSkillCount = "starting skill count" + case skillProficiencies = "skill proficiencies" + case weaponProficiencies = "weapon proficiencies" + case toolProficiencies = "tool proficiencies" + case armorTraining = "armor training" + case startingEquipment = "starting equipment" + case experiencePoints = "experience points" + } + + public init(from decoder: Decoder, configuration: Configuration) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + // Try decoding properties + let name = try values.decode(String.self, forKey: .name) + let plural = try values.decode(String.self, forKey: .plural) + let hitDice = try values.decode(Dice.self, forKey: .hitDice) + let startingWealth = try values.decode(Dice.self, forKey: .startingWealth) + + let descriptiveTraits = try values.decodeIfPresent([String:String].self, forKey: .descriptiveTraits) + let primaryAbility = try values.decodeIfPresent([Ability].self, forKey: .primaryAbility) + let alternatePrimaryAbility = try values.decodeIfPresent([Ability].self, forKey: .alternatePrimaryAbility) + let savingThrows = try values.decodeIfPresent([Ability].self, forKey: .savingThrows) + let startingSkillCount: Int? = try values.decodeIfPresent(Int.self, forKey: .startingSkillCount) + + // Decode skill proficiency names and resolve them using configuration + let skillNames = try values.decodeIfPresent([String].self, forKey: .skillProficiencies) ?? [] + let resolvedSkills: [Skill] = try skillNames.skills(from: configuration.skills) + + let weaponProficiencies = try values.decodeIfPresent([String].self, forKey: .weaponProficiencies) + let toolProficiencies = try values.decodeIfPresent([String].self, forKey: .toolProficiencies) + let armorTraining: [String]? = try values.decodeIfPresent([String].self, forKey: .armorTraining) + let startingEquipment: [[String]]? = try values.decodeIfPresent([[String]].self, forKey: .startingEquipment) + + let experiencePoints = try values.decodeIfPresent([Int].self, forKey: .experiencePoints) + + // Safely set properties + self.name = name + self.plural = plural + self.hitDice = hitDice + self.startingWealth = startingWealth + + self.descriptiveTraits = descriptiveTraits ?? [:] + self.primaryAbility = primaryAbility ?? [] + self.alternatePrimaryAbility = alternatePrimaryAbility + self.savingThrows = savingThrows ?? [] + self.startingSkillCount = startingSkillCount ?? 2 + self.skillProficiencies = resolvedSkills + self.weaponProficiencies = weaponProficiencies ?? [] + self.toolProficiencies = toolProficiencies ?? [] + self.armorTraining = armorTraining ?? [] + self.startingEquipment = startingEquipment ?? [] + + self.experiencePoints = experiencePoints + } + + public func encode(to encoder: Encoder, configuration: Configuration) throws { + var values = encoder.container(keyedBy: CodingKeys.self) + + try values.encode(name, forKey: .name) + try values.encode(plural, forKey: .plural) + try values.encode("\(hitDice)", forKey: .hitDice) + try values.encode("\(startingWealth)", forKey: .startingWealth) + + try values.encode(descriptiveTraits, forKey: .descriptiveTraits) + try values.encode(primaryAbility, forKey: .primaryAbility) + try values.encodeIfPresent(alternatePrimaryAbility, forKey: .alternatePrimaryAbility) + try values.encode(savingThrows, forKey: .savingThrows) + try values.encode(startingSkillCount, forKey: .startingSkillCount) + try values.encode(skillProficiencies.skillNames, forKey: .skillProficiencies) + try values.encode(weaponProficiencies, forKey: .weaponProficiencies) + try values.encode(toolProficiencies, forKey: .toolProficiencies) + try values.encode(armorTraining, forKey: .armorTraining) + try values.encode(startingEquipment, forKey: .startingEquipment) + + try values.encodeIfPresent(experiencePoints, forKey: .experiencePoints) + } +} + +extension ClassTraits { + + /// Returns a random array of skill proficiencies, of a count matching startingSkillCount. + public func randomSkillProficiencies() -> [Skill] { + return skillProficiencies.randomSkills(count: startingSkillCount) + } +} diff --git a/Sources/RolePlayingCore/Player/Classes.swift b/Sources/RolePlayingCore/Player/Classes.swift new file mode 100644 index 0000000..7e826fa --- /dev/null +++ b/Sources/RolePlayingCore/Player/Classes.swift @@ -0,0 +1,50 @@ +// +// Classes.swift +// RolePlayingCore +// +// Created by Brian Arnold on 11/13/16. +// Copyright © 2016 Brian Arnold. All rights reserved. +// + +import Foundation + +/// A collection of class traits. +public struct Classes: CodableWithConfiguration { + + public var classes: [ClassTraits] + public var experiencePoints: [Int]? + + public init(_ classes: [ClassTraits] = []) { + self.classes = classes + } + + private enum CodingKeys: String, CodingKey { + case classes + case experiencePoints = "experience points" + } + + public func find(_ className: String?) -> ClassTraits? { + return classes.first(where: { $0.name == className }) + } + + public var count: Int { classes.count } + + public subscript(index: Int) -> ClassTraits? { + guard index >= 0 && index < classes.count else { return nil } + return classes[index] + } + + // MARK: CodableWithConfiguration conformance + + public init(from decoder: Decoder, configuration: Configuration) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + self.classes = try values.decode([ClassTraits].self, forKey: .classes, configuration: configuration) + self.experiencePoints = try values.decodeIfPresent([Int].self, forKey: .experiencePoints) + } + + public func encode(to encoder: Encoder, configuration: Configuration) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(classes, forKey: .classes, configuration: configuration) + try container.encodeIfPresent(experiencePoints, forKey: .experiencePoints) + } +} diff --git a/Sources/RolePlayingCore/Player/CreatureSize.swift b/Sources/RolePlayingCore/Player/CreatureSize.swift new file mode 100644 index 0000000..356c2be --- /dev/null +++ b/Sources/RolePlayingCore/Player/CreatureSize.swift @@ -0,0 +1,89 @@ +// +// CreatureSize.swift +// RolePlayingCore +// +// Created by Brian Arnold on 10/27/25. +// Copyright © 2025 Brian Arnold. All rights reserved. +// + +/// A player character or monster size. +public enum CreatureSize: String { + case tiny + case small + case medium + case large + case huge + case gargantuan + + /// Returns a creature size relative to the specified height. + init(from height: Height) { + let heightInFeet = height.converted(to: .feet) + switch heightInFeet.value { + case 0..<4: + self = .small + case 4..<8: + self = .medium + default: + self = .large + } + } + + /// Integer range in inches + var range: Range { + switch self { + case .tiny: return 12..<24 + case .small: return 24..<48 + case .medium: return 48..<96 + case .large: return 96..<120 + case .huge: return 120..<180 + case .gargantuan: return 180..<240 + } + } + + /// Space required in feet (dimension feet x feet) + var space: Double { + switch self { + case .tiny: return 2.5 + case .small, .medium: return 5.0 + case .large: return 10.0 + case .huge: return 15.0 + case .gargantuan: return 20.0 + } + } + + /// Space required in squares + var squares: Double { + switch self { + case .tiny: return 0.25 + case .small, .medium: return 1.0 + case .large: return 4.0 + case .huge: return 9.0 + case .gargantuan: return 16.0 + } + } +} + +extension Height { + + /// Generates a random height from a named size, or a string range "min-max" appended with "ft" (feet) or "in" (inches). + public static func randomHeight(from rangeString: String) -> Height { + let range: Range + if let namedSize = CreatureSize(rawValue: rangeString) { + range = namedSize.range + } else if rangeString.hasSuffix("ft") { + let minMaxString = rangeString.replacing("ft", with: "").replacing(" ", with: "").components(separatedBy: "-") + let minValue = (Int(minMaxString[0]) ?? 4) * 12 + let maxValue = (Int(minMaxString[1]) ?? 7) * 12 + range = minValue.. Dice { return SimpleDice(Die(rawValue: self.sides)!, times: level) } } -// TODO: Is this a base class for Character? What about NPC? Monster? Should we have a protocol? -public class Player: Codable { +/// The base class for a player character, including its background, species, class, abilities, skills, hit points, and so on. +public class Player: CodableWithConfiguration { + /// The player's name. public var name: String - public var descriptiveTraits: [String: String] // ideals, bonds, flaws, background - - public private(set) var speciesName: String - public private(set) var className: String - - public var speciesTraits: SpeciesTraits! { - didSet { - self.speciesName = speciesTraits.name - } - } - public var classTraits: ClassTraits! { - didSet { - self.className = classTraits.name - } - } + public var backgroundTraits: BackgroundTraits! + public var speciesTraits: SpeciesTraits! + public var classTraits: ClassTraits! + + public var backgroundName: String { backgroundTraits?.name ?? "" } + public var speciesName: String { speciesTraits?.name ?? "" } + public var className: String { classTraits?.name ?? "" } + + public private(set) var skillProficiencies: [Skill] + + /// Descriptive traits, such as ideals, bonds, flaws, a background story, etc. + public var descriptiveTraits: [String: String] + public enum Gender: String, Codable, CaseIterable { case female = "Female" case male = "Male" @@ -60,7 +58,6 @@ public class Player: Codable { public var alignment: Alignment? public var height: Height - public var weight: Weight // TODO: birthdate and age @@ -69,9 +66,19 @@ public class Player: Codable { /// Ability scores public var baseAbilities: AbilityScores - public var abilities: AbilityScores { baseAbilities + speciesTraits.abilityScoreIncrease } - public var modifiers: AbilityScores { abilities.modifiers } + public var backgroundAbilities: [Ability] + public var backgroundAbilityIncrease: AbilityScores { + var scores = AbilityScores() + for ability in backgroundAbilities { + scores[ability]! += 1 + } + return scores + } + // TODO: limit adding backgroundAbilityIncrease to max score of 20 + public var abilities: AbilityScores { baseAbilities + backgroundAbilityIncrease } + public var modifiers: AbilityScores { abilities.modifiers } + /// Hit points, hit dice, experience points, and level public var maximumHitPoints: Int @@ -80,7 +87,7 @@ public class Player: Codable { public var level: Int public var speed: Int { speciesTraits.speed } - public var size: SpeciesTraits.Size { speciesTraits.size(from: height) } + public var size: CreatureSize { CreatureSize(from: height) } public var hitDice: Dice { classTraits.hitDice.hitDice(level: level) } @@ -100,14 +107,16 @@ public class Player: Codable { private enum CodingKeys: String, CodingKey { case name + case backgroundName = "background" case speciesName = "species" case className = "class" case descriptiveTraits = "descriptive traits" case gender case alignment case height - case weight case baseAbilities = "ability scores" + case backgroundAbilities = "background ability scores" + case skillProficiencies = "skill proficiencies" case maximumHitPoints = "maximum hit points" case currentHitPoints = "current hit points" case experiencePoints = "experience points" @@ -115,81 +124,116 @@ public class Player: Codable { case money } - public required init(from decoder: Decoder) throws { + public required init(from decoder: Decoder, configuration: Configuration) throws { let values = try decoder.container(keyedBy: CodingKeys.self) // Try decoding properties let name = try values.decode(String.self, forKey: .name) + let backgroundName = try values.decode(String.self, forKey: .backgroundName) let speciesName = try values.decode(String.self, forKey: .speciesName) let className = try values.decode(String.self, forKey: .className) let descriptiveTraits = try values.decodeIfPresent([String:String].self, forKey: .descriptiveTraits) let gender = try values.decodeIfPresent(Gender.self, forKey: .gender) let alignment = try values.decodeIfPresent(Alignment.self, forKey: .alignment) let height = try values.decode(Height.self, forKey: .height) - let weight = try values.decode(Weight.self, forKey: .weight) let baseAbilities = try values.decode(AbilityScores.self, forKey: .baseAbilities) + let backgroundAbilities = try values.decode([String].self, forKey: .backgroundAbilities) + + // Decode skill proficiency names and resolve them using configuration + let skillNames = try values.decode([String].self, forKey: .skillProficiencies) + var resolvedSkills: [Skill] = [] + for skillName in skillNames { + guard let skill = configuration.skills[skillName] else { + throw missingTypeError("skill", skillName) + } + resolvedSkills.append(skill) + } + let maximumHitPoints = try values.decode(Int.self, forKey: .maximumHitPoints) let currentHitPoints = try values.decodeIfPresent(Int.self, forKey: .currentHitPoints) let experiencePoints = try values.decodeIfPresent(Int.self, forKey: .experiencePoints) let level = try values.decodeIfPresent(Int.self, forKey: .level) - let money = try values.decode(Money.self, forKey: .money) + let money = try values.decode(Money.self, forKey: .money, configuration: configuration.currencies) + + // Resolve backgroundTraits from configuration + guard let backgroundTraits = configuration.backgrounds[backgroundName] else { + throw missingTypeError("background", backgroundName) + } + + // Resolve speciesTraits from configuration + guard let speciesTraits = configuration.species.find(speciesName) else { + throw missingTypeError("species", speciesName) + } + + // Resolve classTraits from configuration + guard let classTraits = configuration.classes.find(className) else { + throw missingTypeError("class", className) + } // Safely set properties self.name = name - self.speciesName = speciesName - self.className = className self.descriptiveTraits = descriptiveTraits ?? [:] self.gender = gender self.alignment = alignment self.height = height - self.weight = weight self.baseAbilities = baseAbilities + self.backgroundAbilities = backgroundAbilities.map { Ability($0) } + self.skillProficiencies = resolvedSkills self.maximumHitPoints = maximumHitPoints self.currentHitPoints = currentHitPoints ?? maximumHitPoints self.experiencePoints = experiencePoints ?? 0 self.level = level ?? 1 self.money = money + self.backgroundTraits = backgroundTraits + self.speciesTraits = speciesTraits + self.classTraits = classTraits } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder, configuration: Configuration) throws { var values = encoder.container(keyedBy: CodingKeys.self) // Try decoding properties try values.encode(name, forKey: .name) + try values.encode(backgroundName, forKey: .backgroundName) try values.encode(speciesName, forKey: .speciesName) try values.encode(className, forKey: .className) try values.encodeIfPresent(descriptiveTraits, forKey: .descriptiveTraits) try values.encodeIfPresent(gender, forKey: .gender) try values.encodeIfPresent(alignment, forKey: .alignment) try values.encode("\(height)", forKey: .height) - try values.encode("\(weight)", forKey: .weight) try values.encode(baseAbilities, forKey: .baseAbilities) + try values.encode(backgroundAbilities.map({ $0.name }), forKey: .backgroundAbilities) + try values.encode(skillProficiencies.skillNames, forKey: .skillProficiencies) try values.encode(maximumHitPoints, forKey: .maximumHitPoints) try values.encodeIfPresent(currentHitPoints, forKey: .currentHitPoints) try values.encodeIfPresent(experiencePoints, forKey: .experiencePoints) try values.encodeIfPresent(level, forKey: .level) - try values.encode("\(money)", forKey: .money) + try values.encode(money, forKey: .money, configuration: configuration.currencies) } // Creates a player character. - public init(_ name: String, speciesTraits: SpeciesTraits, classTraits: ClassTraits, gender: Gender? = nil, alignment: Alignment? = nil) { + public init(_ name: String, backgroundTraits: BackgroundTraits, speciesTraits: SpeciesTraits, classTraits: ClassTraits, gender: Gender? = nil, alignment: Alignment? = nil) { self.name = name self.descriptiveTraits = [:] - self.speciesName = speciesTraits.name - self.className = classTraits.name + self.backgroundTraits = backgroundTraits self.speciesTraits = speciesTraits self.classTraits = classTraits self.gender = gender self.alignment = alignment - let extraHeight = speciesTraits.heightModifier.roll().result - self.height = (speciesTraits.baseHeight + Height(value: Double(extraHeight), unit: .inches)).converted(to: .feet) - - let extraWeight = extraHeight * speciesTraits.weightModifier.roll().result - self.weight = speciesTraits.baseWeight + Weight(value: Double(extraWeight), unit: .pounds) + // TODO: More heavily weight the first base size (primary vs. secondary) + let baseSize = speciesTraits.baseSizes.randomElement()! + self.height = Height.randomHeight(from: baseSize) self.baseAbilities = AbilityScores() self.baseAbilities.roll() + + // TODO: roll for 2 or 3 background abilities, and if 2, add one random ability score twice + self.backgroundAbilities = backgroundTraits.abilityScores.map { Ability($0) } + + var allSkills = classTraits.randomSkillProficiencies() + allSkills.append(backgroundTraits.skillProficiencies) + self.skillProficiencies = allSkills self.maximumHitPoints = Player.rollHitPoints(classTraits: classTraits, speciesTraits: speciesTraits) self.currentHitPoints = self.maximumHitPoints @@ -204,7 +248,7 @@ public class Player: Codable { // MARK: Implementation public class func rollHitPoints(classTraits: ClassTraits, speciesTraits: SpeciesTraits) -> Int { - return max(classTraits.hitDice.sides / 2 + 1, classTraits.hitDice.roll().result) + speciesTraits.hitPointBonus + return max(classTraits.hitDice.sides / 2 + 1, classTraits.hitDice.roll().result) } public func rollHitPoints() -> Int { @@ -222,22 +266,21 @@ public class Player: Codable { maximumHitPoints += rollHitPoints() - // TODO: add more for leveling up + // TODO: add more details for leveling up } - } extension Player: Hashable { public static func == (lhs: Player, rhs: Player) -> Bool { return lhs.name == rhs.name && + lhs.backgroundName == rhs.backgroundName && lhs.speciesName == rhs.speciesName && lhs.className == rhs.className && lhs.descriptiveTraits == rhs.descriptiveTraits && lhs.gender == rhs.gender && lhs.alignment == rhs.alignment && lhs.height == rhs.height && - lhs.weight == rhs.weight && lhs.baseAbilities == rhs.baseAbilities && lhs.maximumHitPoints == rhs.maximumHitPoints && lhs.currentHitPoints == rhs.currentHitPoints && @@ -248,12 +291,12 @@ extension Player: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(name) + hasher.combine(backgroundName) hasher.combine(speciesName) hasher.combine(className) hasher.combine(gender) hasher.combine(alignment) hasher.combine(height) - hasher.combine(weight) hasher.combine(baseAbilities) hasher.combine(maximumHitPoints) hasher.combine(currentHitPoints) @@ -261,5 +304,4 @@ extension Player: Hashable { hasher.combine(level) hasher.combine(money) } - } diff --git a/Sources/RolePlayingCore/Player/Players.swift b/Sources/RolePlayingCore/Player/Players.swift new file mode 100644 index 0000000..f84f544 --- /dev/null +++ b/Sources/RolePlayingCore/Player/Players.swift @@ -0,0 +1,52 @@ +// +// Players.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/18/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Foundation + +/// A collection of player characters. +public class Players: CodableWithConfiguration { + public var players: [Player] + + public init(_ players: [Player] = []) { + self.players = players + } + + // TODO: inherit protocols for these + + public var count: Int { return players.count } + + public subscript(index: Int) -> Player? { + get { + return players[index] + } + } + + public func insert(_ player: Player, at index: Int) { + players.insert(player, at: index) + } + + public func remove(at index: Int) { + players.remove(at: index) + } + + // MARK: Codable conformance + + private enum CodingKeys: String, CodingKey { + case players + } + + public required init(from decoder: Decoder, configuration: Configuration) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + players = try container.decode([Player].self, forKey: .players, configuration: configuration) + } + + public func encode(to encoder: Encoder, configuration: Configuration) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(players, forKey: .players, configuration: configuration) + } +} diff --git a/Sources/RolePlayingCore/Player/Skills.swift b/Sources/RolePlayingCore/Player/Skills.swift new file mode 100644 index 0000000..3c834ef --- /dev/null +++ b/Sources/RolePlayingCore/Player/Skills.swift @@ -0,0 +1,106 @@ +// +// Skill.swift +// RolePlayingCore +// +// Created by Brian Arnold on 10/26/25. +// Copyright © 2025 Brian Arnold. All rights reserved. +// + +/// A skill proficiency associated with an ability. +public struct Skill { + public let name: String + public let ability: Ability +} + +extension Skill: Codable { } + +extension Skill: Hashable { } + +/// A collection of skills. +public struct Skills: Codable { + + /// A dictionary of skills indexed by name. + private var allSkills: [String: Skill] = [:] + + /// An array of skills. + public var all: [Skill] { Array(allSkills.values) } + + /// Returns a skills instance that can access a skill by name. + public init(_ skills: [Skill] = []) { + add(skills) + } + + /// Adds the array of skills to the collection. + mutating func add(_ skills: [Skill]) { + allSkills = Dictionary(skills.map { ($0.name, $0) }, uniquingKeysWith: { _, last in last }) + } + + /// Accesses a skill by name. + public subscript(skillName: String) -> Skill? { + return allSkills[skillName] + } + + /// Returns the number of skills in the collection. + public var count: Int { return allSkills.count } + + /// Accesses a skill by index. + public subscript(index: Int) -> Skill? { + return all[index] + } + + // MARK: Codable conformance + + private enum CodingKeys: String, CodingKey { + case skills + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let skills = try container.decode([Skill].self, forKey: .skills) + add(skills) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(all, forKey: .skills) + } +} + +extension Sequence where Element == String { + + /// Returns an array of skills from this array of skill names, using the specified skills collection. + public func skills(from skills: Skills) throws -> [Skill] { + try self.map { skillName in + guard let skill = skills[skillName] else { + throw missingTypeError("skill", skillName) + } + return skill + } + } +} + +extension Sequence where Element == Skill { + + /// Returns a random array of skills with the specified skill count. + public func randomSkills(count: Int) -> [Element] { + var selected: [Element] = [] + var remaining: [Element] = Array(self) + + for _ in 0.. Bool in - speciesTraits.subspecies.count == 0 - } + return species.filter { $0.subspecies.isEmpty } } /// Returns the species matching the specified name, or nil if not present. - public func find(_ speciesName: String?) -> SpeciesTraits? { - guard speciesName != nil else { return nil } - + public func find(_ speciesName: String) -> SpeciesTraits? { return species.first(where: { $0.name == speciesName }) } - public var count: Int { return species.count } + public var count: Int { species.count } public subscript(index: Int) -> SpeciesTraits? { - get { - return species[index] - } + guard index >= 0 && index < species.count else { return nil } + return species[index] } public func randomElementByIndex(using generator: inout G) -> SpeciesTraits { @@ -44,16 +46,20 @@ public class Species: Codable { enum CodingKeys: String, CodingKey { case species + case creatureTypes = "creature types" } /// Overridden to stitch together subspecies embedded in species. - public required init(from decoder: Decoder) throws { + public required init(from decoder: Decoder, configuration: Configuration) throws { let root = try decoder.container(keyedBy: CodingKeys.self) + let creatureTypes = try root.decodeIfPresent([CreatureType].self, forKey: .creatureTypes) + self.creatureTypes = creatureTypes ?? [] + var leaf = try root.nestedUnkeyedContainer(forKey: .species) var species = [SpeciesTraits]() while (!leaf.isAtEnd) { - let speciesTraits = try leaf.decode(SpeciesTraits.self) + let speciesTraits = try leaf.decode(SpeciesTraits.self, configuration: configuration) species.append(speciesTraits) /// If there are subspecies, append those @@ -65,4 +71,16 @@ public class Species: Codable { self.species = species } + public func encode(to encoder: any Encoder, configuration: Configuration) throws { + var root = encoder.container(keyedBy: CodingKeys.self) + if !creatureTypes.isEmpty { + try root.encode(creatureTypes, forKey: .creatureTypes) + } + + var leaf = root.nestedUnkeyedContainer(forKey: .species) + let rootSpecies = self.species.filter { $0.parentName == nil } + for speciesTraits in rootSpecies { + try leaf.encode(speciesTraits, configuration: configuration) + } + } } diff --git a/Sources/RolePlayingCore/Player/SpeciesTraits.swift b/Sources/RolePlayingCore/Player/SpeciesTraits.swift new file mode 100644 index 0000000..79ac703 --- /dev/null +++ b/Sources/RolePlayingCore/Player/SpeciesTraits.swift @@ -0,0 +1,169 @@ +// +// SpeciesTraits.swift +// RolePlayingCore +// +// Created by Brian Arnold on 11/12/16. +// Copyright © 2016-2017 Brian Arnold. All rights reserved. +// + +import Foundation + +/// Traits representing a species. +public struct SpeciesTraits { + + public var name: String + public var plural: String + public var aliases: [String] + public var creatureType: CreatureType + public var descriptiveTraits: [String: String] + public var lifespan: Int! + + public var baseSizes: [String] + + public var darkVision: Int! + public var speed: Int! + + public var parentName: String? + public var subspecies: [SpeciesTraits] = [] + + public init(name: String, + plural: String, + aliases: [String] = [], + creatureType: CreatureType, + descriptiveTraits: [String: String] = [:], + lifespan: Int, + baseSizes: [String] = ["4-7"], + darkVision: Int, + speed: Int) { + self.name = name + self.plural = plural + self.aliases = aliases + self.creatureType = creatureType + self.descriptiveTraits = descriptiveTraits + self.lifespan = lifespan + self.baseSizes = baseSizes + self.darkVision = darkVision + self.speed = speed + } +} + +extension SpeciesTraits: CodableWithConfiguration { + + private enum CodingKeys: String, CodingKey { + case name + case plural + case aliases + case creatureType = "creature type" + case descriptiveTraits = "descriptive traits" + case lifespan + case baseSizes = "base sizes" + case darkVision = "darkvision" + case speed + case subspecies + } + + public init(from decoder: Decoder, configuration: Configuration) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + // Try decoding properties + let name = try values.decode(String.self, forKey: .name) + let plural = try values.decode(String.self, forKey: .plural) + let aliases = try values.decodeIfPresent([String].self, forKey: .aliases) + let creatureType = try values.decodeIfPresent(String.self, forKey: .creatureType) + let descriptiveTraits = try values.decodeIfPresent([String:String].self, forKey: .descriptiveTraits) + let lifespan = try values.decodeIfPresent(Int.self, forKey: .lifespan) + let baseSizes = try values.decodeIfPresent([String].self, forKey: .baseSizes) + let darkVision = try values.decodeIfPresent(Int.self, forKey: .darkVision) + let speed = try values.decodeIfPresent(Int.self, forKey: .speed) + + // Safely set properties + self.name = name + self.plural = plural + self.aliases = aliases ?? [] + self.creatureType = creatureType != nil ? CreatureType(creatureType!) : configuration.species.defaultCreatureType + self.descriptiveTraits = descriptiveTraits ?? [:] + self.lifespan = lifespan + self.baseSizes = baseSizes ?? ["4-7"] + self.darkVision = darkVision + self.speed = speed + + // Decode subspecies + if var subspecies = try? values.nestedUnkeyedContainer(forKey: .subspecies) { + while (!subspecies.isAtEnd) { + var subspeciesTraits = try subspecies.decode(SpeciesTraits.self, configuration: configuration) + subspeciesTraits.blendTraits(from: self) + self.subspecies.append(subspeciesTraits) + } + } + } + + /// Inherit parent traits, for each trait that is not already set. + public mutating func blendTraits(from parent: SpeciesTraits) { + // Name, plural, aliases and descriptive traits are unique to each set of species traits. + // The rest may be inherited from the parent. + self.parentName = parent.name + self.creatureType = parent.creatureType + + if self.baseSizes.isEmpty { + self.baseSizes = parent.baseSizes + } + + if self.lifespan == nil { + self.lifespan = parent.lifespan + } + + if self.darkVision == nil { + self.darkVision = parent.darkVision + } + if self.speed == nil { + self.speed = parent.speed + } + } + + public func encode(to encoder: Encoder, configuration: Configuration) throws { + var values = encoder.container(keyedBy: CodingKeys.self) + + try values.encode(name, forKey: .name) + try values.encode(plural, forKey: .plural) + try values.encode(aliases, forKey: .aliases) + try values.encode(creatureType.name, forKey: .creatureType) + try values.encode(descriptiveTraits, forKey: .descriptiveTraits) + try values.encode(lifespan, forKey: .lifespan) + try values.encode(baseSizes, forKey: .baseSizes) + try values.encode(darkVision, forKey: .darkVision) + try values.encode(speed, forKey: .speed) + + var subspeciesContainer = values.nestedUnkeyedContainer(forKey: .subspecies) + for subspeciesTraits in subspecies { + try subspeciesTraits.encode(to: &subspeciesContainer, parent: self) + } + } + + public func encode(to container: inout UnkeyedEncodingContainer, parent: SpeciesTraits) throws { + // Name, plural, aliases and descriptive traits are unique to each set of species traits. + // The rest may be inherited from the parent. + var values = container.nestedContainer(keyedBy: CodingKeys.self) + + try values.encode(name, forKey: .name) + try values.encode(plural, forKey: .plural) + try values.encode(creatureType.name, forKey: .creatureType) + if self.aliases.count > 0 { + try values.encode(aliases, forKey: .aliases) + } + if self.descriptiveTraits.count > 0 { + try values.encode(descriptiveTraits, forKey: .descriptiveTraits) + } + if self.lifespan != parent.lifespan { + try values.encode(self.lifespan, forKey: .lifespan) + } + if self.baseSizes != parent.baseSizes { + try values.encode(self.baseSizes, forKey: .baseSizes) + } + if self.darkVision != parent.darkVision { + try values.encode(self.darkVision, forKey: .darkVision) + } + if self.speed != parent.speed { + try values.encode(self.speed, forKey: .speed) + } + } +} diff --git a/Tests/RolePlayingCoreTests/AbilityTests.swift b/Tests/RolePlayingCoreTests/AbilityTests.swift new file mode 100644 index 0000000..7f38b16 --- /dev/null +++ b/Tests/RolePlayingCoreTests/AbilityTests.swift @@ -0,0 +1,311 @@ +// +// AbilityTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/8/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Testing +@testable import RolePlayingCore +import Foundation + +@Suite("Ability Tests") +struct AbilityTests { + + @Test("String abbreviation") + func stringAbbreviation() { + let strength = "Strength" + #expect(strength.abbreviated == "STR", "Strength abbreviated") + + let lettera = "a" + #expect(lettera.abbreviated == "A", "A abbreviated") + + let empty = "" + #expect(empty.abbreviated == "", "empty abbreviated") + } + + @Test("Int score modifier") + func intScoreModifier() { + let expectedScores = [-5, -4, -4, -3, -3, -2, -2, -1, -1, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10] + + for score in 1...30 { + let expectedScore = expectedScores[score - 1] + #expect(score.scoreModifier == expectedScore, "score \(score) modifier") + } + } + + @Test("Ability struct") + func abilityStruct() { + let strength = Ability("Strength") + #expect(strength.name == "Strength", "strength name") + #expect(strength.abbreviated == "STR", "strength name abbreviated") + } + + @Test("Ability equatable") + func abilityEquatable() { + let strength = Ability("Strength") + #expect(strength == Ability("Strength"), "strength equatable") + #expect(strength != Ability("Intelligence"), "intelligence not equal to strength") + } + + @Test("Ability hashable") + func abilityHashable() { + let strength = Ability("Strength") + let strengthClone = Ability("Strength") + let intelligence = Ability("Intelligence") + + var map: [Ability: Int] = [strength: 12, intelligence: 3] + map[strengthClone] = 9 + #expect(map[strength] == 9, "strength hashable") + } + + @Test("Ability encodable") + func abilityEncodable() throws { + struct AbilityContainer: Codable { + let ability: Ability + } + let abilityScores = AbilityContainer(ability: Ability("Strength")) + let encoder = JSONEncoder() + + let encoded = try encoder.encode(abilityScores) + let dictionary = try JSONSerialization.jsonObject(with: encoded, options: .allowFragments) as? [String: String] + let unwrappedDictionary = try #require(dictionary, "serialized strength should be non-nil") + let strength = unwrappedDictionary["ability"] + #expect(strength == "Strength", "Round-trip for Ability should be Strength") + } + + @Test("Ability decodable") + func abilityDecodable() throws { + let traits = """ + { + "ability": "Strength" + } + """.data(using: .utf8)! + struct AbilityContainer: Decodable { + let ability: Ability + } + + let decoder = JSONDecoder() + let decoded = try decoder.decode(AbilityContainer.self, from: traits) + #expect(decoded.ability.name == "Strength", "decoded ability name") + } + + @Test("Non-mutable ability scores") + func nonMutableAbilityScores() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + let abilityScores = AbilityScores([brawn: 9, reflexes: 12, stamina: 15]) + + #expect(abilityScores.count == 3, "ability scores count") + #expect(abilityScores[brawn] == 9, "ability scores brawn") + #expect(abilityScores[reflexes] == 12, "ability scores reflexes") + #expect(abilityScores[stamina] == 15, "ability scores stamina") + + let expectedModifiers = AbilityScores([brawn: -1, reflexes: 1, stamina: 2]) + #expect(abilityScores.modifiers == expectedModifiers, "ability scores modifiers") + } + + @Test("Mutable ability scores") + func mutableAbilityScores() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + var abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + + // Change 2 of the 3 scores + abilityScores[reflexes] = 11 + abilityScores[stamina] = 18 + + // Verify that 2 of the 3 scores changed + #expect(abilityScores[brawn] == 8, "ability scores reflexes") + #expect(abilityScores[reflexes] == 11, "ability scores reflexes") + #expect(abilityScores[stamina] == 18, "ability scores reflexes") + + // Check that modifiers reflect mutated scores + let expectedModifiers = AbilityScores([brawn: -1, reflexes: 0, stamina: 4]) + #expect(abilityScores.modifiers == expectedModifiers, "ability scores modifiers") + + // Verify that a score can't be nil'd out + abilityScores[stamina] = nil + #expect(abilityScores[stamina] == 18, "ability scores can't be set to nil") + + let invalidAbility = Ability("Charm") + abilityScores[invalidAbility] = 8 + #expect(abilityScores[invalidAbility] == nil, "invalid ability should not set a score") + } + + @Test("Ability scores decodable") + func abilityScoresDecodable() throws { + // Test with implicit [String: Int] as from JSON + let traits = """ + {"Strength": 12, "Intelligence": 8} + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let abilityScores = try? decoder.decode(AbilityScores.self, from: traits) + let unwrappedAbilityScores = try #require(abilityScores, "ability scores should be non-nil") + + let strength = Ability("Strength") + let intelligence = Ability("Intelligence") + + #expect(unwrappedAbilityScores[strength] == 12, "ability scores dictionary strength") + #expect(unwrappedAbilityScores[intelligence] == 8, "ability scores dictionary intelligence") + } + + @Test("Ability scores encodable") + func abilityScoresEncodable() throws { + let abilityScores = AbilityScores([Ability("Brawn"): 12, Ability("Charm"): 3]) + + let encoder = JSONEncoder() + let encoded = try encoder.encode(abilityScores) + let serialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String:Int] + let brawn = serialized?["Brawn"] + let charm = serialized?["Charm"] + #expect(brawn == 12, "encoded brawn") + #expect(charm == 3, "encoded charm") + } + + @Test("Ability score key") + func abilityScoreKey() { + // Housekeeping: code coverage for AbilityKey + let wisdomKey = AbilityScores.AbilityKey(stringValue: "Wisdom")! + #expect(wisdomKey.intValue == nil, "AbilityKey does not use intValue") + + let intKey = AbilityScores.AbilityKey(intValue: 2) + #expect(intKey == nil, "AbilityKey does not use intValue") + } + + @Test("Adding modifiers") + func addingModifiers() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + let combinedScores = abilityScores + abilityScores.modifiers + + #expect(combinedScores[brawn] == 7, "adding ability scores brawn") + #expect(combinedScores[reflexes] == 14, "adding ability scores reflexes") + #expect(combinedScores[stamina] == 20, "adding ability scores stamina") + } + + @Test("Adding one score") + func addingOneScore() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + var abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + let oneScore = AbilityScores([reflexes: -3]) + abilityScores += oneScore + + #expect(abilityScores[brawn] == 8, "adding ability scores brawn") + #expect(abilityScores[reflexes] == 10, "adding ability scores reflexes") + #expect(abilityScores[stamina] == 17, "adding ability scores stamina") + } + + @Test("Adding unrelated scores") + func addingUnrelatedScores() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + + let intelligence = Ability("Intelligence") + let wisdom = Ability("Wisdom") + let unrelatedScores = AbilityScores([intelligence: 14, wisdom: 5]) + let combinedScores = abilityScores + unrelatedScores + + #expect(combinedScores.count == 3, "adding ability scores count") + + #expect(combinedScores[brawn] == 8, "adding ability scores brawn") + #expect(combinedScores[reflexes] == 13, "adding ability scores reflexes") + #expect(combinedScores[stamina] == 17, "adding ability scores stamina") + } + + @Test("Subtracting ability scores") + func subtractingAbilityScores() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + let combinedScores = abilityScores - abilityScores.modifiers + + #expect(combinedScores[brawn] == 9, "adding ability scores brawn") + #expect(combinedScores[reflexes] == 12, "adding ability scores reflexes") + #expect(combinedScores[stamina] == 14, "adding ability scores stamina") + } + + @Test("Subtracting one score") + func subtractingOneScore() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + var abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + let oneScore = AbilityScores([reflexes: -3]) + abilityScores -= oneScore + + #expect(abilityScores[brawn] == 8, "adding ability scores brawn") + #expect(abilityScores[reflexes] == 16, "adding ability scores reflexes") + #expect(abilityScores[stamina] == 17, "adding ability scores stamina") + } + + @Test("Subtracting unrelated scores") + func subtractingUnrelatedScores() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + + let intelligence = Ability("Intelligence") + let wisdom = Ability("Wisdom") + let unrelatedScores = AbilityScores([intelligence: 14, wisdom: 5]) + let combinedScores = abilityScores - unrelatedScores + + #expect(combinedScores.count == 3, "adding ability scores count") + + #expect(combinedScores[brawn] == 8, "adding ability scores brawn") + #expect(combinedScores[reflexes] == 13, "adding ability scores reflexes") + #expect(combinedScores[stamina] == 17, "adding ability scores stamina") + } + + @Test("Default ability scores") + func defaultAbilityScores() { + let abilityScores = AbilityScores() + + #expect(abilityScores.count == 6, "default ability scores count") + + // Test names and values + let abilityNames = ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"] + for ability in abilityScores.abilities { + #expect(abilityNames.contains(ability.name), "default ability name") + #expect(abilityScores[ability] == 0, "default ability score 0") + } + } + + @Test("Non-default ability scores") + func nonDefaultAbilityScores() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + let abilityScores = AbilityScores(defaults: [brawn, reflexes, stamina]) + #expect(abilityScores.count == 3, "default ability scores count") + + // Test values via keys and values + for ability in abilityScores.abilities { + #expect(abilityScores[ability] == 0, "default ability score 0") + } + for value in abilityScores.values { + #expect(value == 0, "default ability score 0") + } + } +} diff --git a/Tests/RolePlayingCoreTests/AlignmentTests.swift b/Tests/RolePlayingCoreTests/AlignmentTests.swift new file mode 100644 index 0000000..300548c --- /dev/null +++ b/Tests/RolePlayingCoreTests/AlignmentTests.swift @@ -0,0 +1,307 @@ +// +// AlignmentTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/11/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Testing +import RolePlayingCore +import Foundation + +@Suite("Alignment Tests") +struct AlignmentTests { + + @Test("Ethics by string") + func ethicsByString() throws { + let lawful = try #require(Ethics(rawValue: "Lawful"), "lawful should be non-nil") + #expect(lawful == Ethics.lawful, "lawful enum") + #expect(lawful.value == 1.0, "lawful value") + + let neutral = try #require(Ethics(rawValue: "Neutral"), "neutral should be non-nil") + #expect(neutral == Ethics.neutral, "neutral enum") + #expect(neutral.value == 0.0, "neutral value") + + let chaotic = try #require(Ethics(rawValue: "Chaotic"), "chaotic should be non-nil") + #expect(chaotic == Ethics.chaotic, "chaotic enum") + #expect(chaotic.value == -1.0, "chaotic value") + } + + @Test("Ethics by value") + func ethicsByValue() { + let lawful = Ethics(0.8) + #expect(lawful == Ethics.lawful, "lawful enum") + #expect(lawful.value == 1.0, "lawful value") + + let neutral = Ethics(-0.1) + #expect(neutral == Ethics.neutral, "neutral enum") + #expect(neutral.value == 0.0, "neutral value") + + let chaotic = Ethics(-0.34) + #expect(chaotic == Ethics.chaotic, "chaotic enum") + #expect(chaotic.value == -1.0, "chaotic value") + } + + @Test("Morals by string") + func moralsByString() throws { + let good = try #require(Morals(rawValue: "Good"), "good should be non-nil") + #expect(good == Morals.good, "good enum") + #expect(good.value == 1.0, "good value") + + let neutral = try #require(Morals(rawValue: "Neutral"), "neutral should be non-nil") + #expect(neutral == Morals.neutral, "neutral enum") + #expect(neutral.value == 0.0, "neutral value") + + let evil = try #require(Morals(rawValue: "Evil"), "evil should be non-nil") + #expect(evil == Morals.evil, "evil enum") + #expect(evil.value == -1.0, "evil value") + } + + @Test("Morals by value") + func moralsByValue() { + let good = Morals(0.334) + #expect(good == Morals.good, "good enum") + #expect(good.value == 1.0, "good value") + + let neutral = Morals(0.2) + #expect(neutral == Morals.neutral, "neutral enum") + #expect(neutral.value == 0.0, "neutral value") + + let evil = Morals(-0.9) + #expect(evil == Morals.evil, "evil enum") + #expect(evil.value == -1.0, "evil value") + } + + @Test("Alignment by type") + func alignmentByType() { + let neutralGood = Alignment.Kind(.neutral, .good) + #expect(neutralGood.description == "Neutral Good", "description") + + let neutralEvil = Alignment.Kind(.neutral, .evil) + #expect(neutralEvil.description == "Neutral Evil", "description") + + let chaoticNeutral = Alignment.Kind(.chaotic, .neutral) + #expect(chaoticNeutral.description == "Chaotic Neutral", "description") + + #expect(neutralGood == Alignment.Kind(.neutral, .good), "equatable") + #expect(neutralGood != neutralEvil, "equatable") + #expect(neutralEvil != chaoticNeutral, "equatable") + } + + @Test("Alignment by value") + func alignmentByValue() { + let lawfulNeutral = Alignment(ethics: 0.7, morals: 0.0) + #expect(lawfulNeutral.description == "Lawful Neutral", "description") + + let neutral = Alignment(ethics: 0.1, morals: -0.2) + #expect(neutral.description == "Neutral", "description") + + let chaoticGood = Alignment(ethics: -1, morals: 1) + #expect(chaoticGood.description == "Chaotic Good", "description") + } + + @Test("Changing alignment") + func changingAlignment() { + // Test changing alignment + var alignment = Alignment(.neutral, .evil) + #expect(alignment.ethics == 0, "ethics value") + #expect(alignment.morals == -1, "morals value") + #expect(alignment.kind.ethics == Ethics.neutral, "ethics enumeration") + #expect(alignment.kind.morals == Morals.evil, "morals enumeration") + + + alignment.morals += 0.8 + #expect(abs(alignment.morals - (-0.2)) < 0.00001, "morals value") + #expect(alignment.kind.ethics == Ethics.neutral, "ethics enumeration") + #expect(alignment.kind.morals == Morals.neutral, "morals enumeration") + + // Try to exceed 1.0 and confirm the ethics value did not change + alignment.ethics += 5.4 + #expect(alignment.ethics == 0, "ethics value") + #expect(alignment.kind.ethics == Ethics.neutral, "ethics enumeration") + #expect(alignment.kind.morals == Morals.neutral, "morals enumeration") + + alignment.morals += 0.8 + #expect(abs(alignment.morals - 0.6) < 0.00001, "morals value") + #expect(alignment.kind.ethics == Ethics.neutral, "ethics enumeration") + #expect(alignment.kind.morals == Morals.good, "morals enumeration") + + // Try to exceed 1.0 and confirm that morals value did not change + alignment.morals += 0.8 + #expect(abs(alignment.morals - 0.6) < 0.00001, "morals value") + #expect(alignment.kind.ethics == Ethics.neutral, "ethics enumeration") + #expect(alignment.kind.morals == Morals.good, "morals enumeration") + } + + @Test("Alignment parsing") + func alignmentParsing() throws { + // Test initializing from valid string + let neutralGood = try #require("Neutral Good".parseAlignment, "alignment should be non-nil") + #expect(neutralGood.0 == Ethics.neutral, "ethics enumeration") + #expect(neutralGood.1 == Morals.good, "morals enumeration") + + let neutral = try #require("Neutral".parseAlignment, "alignment should be non-nil") + #expect(neutral.0 == Ethics.neutral, "ethics enumeration") + #expect(neutral.1 == Morals.neutral, "morals enumeration") + + // Test initializing from partial valid string + let chaotic = try #require("Chaotic".parseAlignment, "alignment should be non-nil") + #expect(chaotic.0 == Ethics.chaotic, "ethics enumeration") + #expect(chaotic.1 == Morals.neutral, "morals enumeration") + + let good = try #require("Good".parseAlignment, "alignment should be non-nil") + #expect(good.0 == Ethics.neutral, "ethics enumeration") + #expect(good.1 == Morals.good, "morals enumeration") + + // Test initializing from bad string + let tooManyWords = "Neutral Neutral Neutral".parseAlignment + #expect(tooManyWords == nil, "too many words should be nil") + + let mismatchedWords = "Foo Bar".parseAlignment + #expect(mismatchedWords == nil, "mismatched words should be nil") + + let wrongWord = "Cat".parseAlignment + #expect(wrongWord == nil, "wrong word should be nil") + } + + @Test("Alignment dictionary decoding") + func alignmentDictionaryDecoding() throws { + let decoder = JSONDecoder() + + // Test initializing from dictionary of doubles + do { + let lawfulNeutralTraits = """ + { + "ethics": 1, + "morals": 0.2 + } + """.data(using: .utf8)! + let lawfulNeutral = try? decoder.decode(Alignment.self, from: lawfulNeutralTraits) + let unwrappedLawfulNeutral = try #require(lawfulNeutral, "alignment should be non-nil") + #expect(unwrappedLawfulNeutral.kind.ethics == Ethics.lawful, "ethics enumeration") + #expect(unwrappedLawfulNeutral.kind.morals == Morals.neutral, "morals enumeration") + } + + // Test initializing from dictionary of strings + do { + let chaoticNeutralTraits = """ + { + "ethics": "Chaotic", + "morals": "Neutral" + } + """.data(using: .utf8)! + let chaoticNeutral = try? decoder.decode(Alignment.self, from: chaoticNeutralTraits) + let unwrappedChaoticNeutral = try #require(chaoticNeutral, "alignment should be non-nil") + #expect(unwrappedChaoticNeutral.kind.ethics == Ethics.chaotic, "ethics enumeration") + #expect(unwrappedChaoticNeutral.kind.morals == Morals.neutral, "morals enumeration") + } + + // Test initializing from bad dictionary keys + do { + let badTraitKeys = """ + { + "Howdy": "Lawful", + "Doody": "Evil" + } + """.data(using: .utf8)! + let badTrait = try? decoder.decode(Alignment.self, from: badTraitKeys) + #expect(badTrait == nil, "bad trait keys should be nil") + } + + // Test initializing from bad dictionary values + do { + let notStringTraits = """ + { + "ethics": ["Chaotic"], + "morals": ["Neutral"] + } + """.data(using: .utf8)! + let notString = try? decoder.decode(Alignment.self, from: notStringTraits) + #expect(notString == nil, "non-string traits should be nil") + + let notValidTraits = """ + { + "ethics": "Choatic", + "morals": "Eliv" + } + """.data(using: .utf8)! + let notValid = try? decoder.decode(Alignment.self, from: notValidTraits) + #expect(notValid == nil, "non-valid traits should be nil") + } + } + + @Test("Alignment string decoding") + func alignmentStringDecoding() throws { + let decoder = JSONDecoder() + + struct AlignmentContainer: Decodable { + let alignment: Alignment + } + + // Test string values + let stringTrait = """ + { + "alignment": "Chaotic Evil" + } + """.data(using: .utf8)! + + let decoded = try decoder.decode(AlignmentContainer.self, from: stringTrait) + #expect("\(decoded.alignment)" == "Chaotic Evil", "decoded string should be round-trip") + + let notValidTrait = """ + { + "alignment": "Hello" + } + """.data(using: .utf8)! + + // Expect this to throw an error + #expect(throws: Error.self) { + try decoder.decode(AlignmentContainer.self, from: notValidTrait) + } + } + + @Test("Alignment encoding") + func alignmentEncoding() throws { + let encoder = JSONEncoder() + + struct AlignmentContainer: Encodable { + let alignment: Alignment + } + let container = AlignmentContainer(alignment: Alignment(.chaotic, .good)) + let encoded = try encoder.encode(container) + let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) + + let dictionary = try #require(deserialized as? [String: Any], "deserialized should be a dictionary") + let alignmentDict = try #require(dictionary["alignment"] as? [String: Double], "player traits round trip alignment") + #expect(alignmentDict["ethics"] == -1, "player traits round trip alignment ethics") + #expect(alignmentDict["morals"] == 1, "player traits round trip alignment morals") + } + + @Test("Stringified encoding") + func stringifiedEncoding() throws { + let encoder = JSONEncoder() + + struct AlignmentContainer: Encodable { + let alignment: Alignment + + enum CodingKeys: String, CodingKey { + case alignment + } + + // Stringify alignment + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("\(alignment)", forKey: .alignment) + } + } + + let container = AlignmentContainer(alignment: Alignment(.chaotic, .good)) + let encoded = try encoder.encode(container) + let deserialized = try #require(try? JSONSerialization.jsonObject(with: encoded, options: []), "player traits round trip") + + let dictionary = try #require(deserialized as? [String: String], "should be string dictionary") + let alignment = try #require(dictionary["alignment"], "alignment should exist") + #expect(alignment == "Chaotic Good", "player traits round trip alignment") + } +} diff --git a/Tests/RolePlayingCoreTests/BackgroundsTests.swift b/Tests/RolePlayingCoreTests/BackgroundsTests.swift new file mode 100644 index 0000000..e4ecb94 --- /dev/null +++ b/Tests/RolePlayingCoreTests/BackgroundsTests.swift @@ -0,0 +1,149 @@ +// +// BackgroundsTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 10/27/25. +// Copyright © 2025 Brian Arnold. All rights reserved. +// + +import Testing +import RolePlayingCore +import Foundation + +@Suite("Backgrounds Tests") +struct BackgroundsTests { + + let decoder = JSONDecoder() + let configuration: Configuration + + init() throws { + configuration = try Configuration("TestConfiguration", from: .module) + } + + @Test("Decode background traits") + func backgroundTraitsDecoding() async throws { + // Given: JSON data representing a background + let jsonData = """ + { + "name": "Acolyte", + "ability scores": ["Intelligence", "Wisdom"], + "feat": "Magic Initiate", + "skill proficiencies": ["Insight", "Religion"], + "tool proficiency": "Calligrapher's Supplies", + "equipment": [["Holy Symbol", "Prayer Book", "Vestments", "10 GP"], ["15 GP"]] + } + """.data(using: .utf8)! + + // When: Decoding the JSON data with configuration + let background = try decoder.decode(BackgroundTraits.self, from: jsonData, configuration: configuration) + + // Then: The properties should match the input + #expect(background.name == "Acolyte", "Name should match") + #expect(background.abilityScores == ["Intelligence", "Wisdom"], "Ability scores should match") + #expect(background.feat == "Magic Initiate", "Feat should match") + #expect(background.skillProficiencies.count == 2, "Should have 2 skill proficiencies") + #expect(background.skillProficiencies.skillNames == ["Insight", "Religion"], "Skill names should match") + #expect(background.toolProficiency == "Calligrapher's Supplies", "Tool proficiency should match") + #expect(background.equipment.count == 2, "Should have 2 equipment choices") + #expect(background.equipment[0] == ["Holy Symbol", "Prayer Book", "Vestments", "10 GP"], "First equipment choice should match") + } + + @Test("Encode background traits with round-trip") + func backgroundTraitsEncoding() async throws { + // Given: A BackgroundTraits instance + let jsonData = """ + { + "name": "Criminal", + "ability scores": ["Dexterity", "Intelligence"], + "feat": "Alert", + "skill proficiencies": ["Deception", "Stealth"], + "tool proficiency": "Thieves' Tools", + "equipment": [["Crowbar", "Dark Clothes", "Thieves' Tools", "16 GP"]] + } + """.data(using: .utf8)! + + let background = try decoder.decode(BackgroundTraits.self, from: jsonData, configuration: configuration) + + // When: Encoding the background back to JSON + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let encodedData = try encoder.encode(background, configuration: configuration) + + // Then: The encoded data should be decodable and match the original + let decodedBackground = try decoder.decode(BackgroundTraits.self, from: encodedData, configuration: configuration) + + #expect(decodedBackground.name == background.name, "Name should match after round-trip") + #expect(decodedBackground.abilityScores == background.abilityScores, "Ability scores should match after round-trip") + #expect(decodedBackground.feat == background.feat, "Feat should match after round-trip") + #expect(decodedBackground.skillProficiencies.skillNames == background.skillProficiencies.skillNames, "Skills should match after round-trip") + #expect(decodedBackground.toolProficiency == background.toolProficiency, "Tool proficiency should match after round-trip") + #expect(decodedBackground.equipment == background.equipment, "Equipment should match after round-trip") + } + + @Test("Decode and query backgrounds collection") + func backgroundsCollection() async throws { + // Given: JSON data representing a collection of backgrounds + let jsonData = """ + { + "backgrounds": [ + { + "name": "Acolyte", + "ability scores": ["Intelligence", "Wisdom"], + "feat": "Magic Initiate", + "skill proficiencies": ["Insight", "Religion"], + "tool proficiency": "Calligrapher's Supplies", + "equipment": [["Holy Symbol", "Prayer Book", "Vestments", "10 GP"], ["15 GP"]] + }, + { + "name": "Criminal", + "ability scores": ["Dexterity", "Intelligence"], + "feat": "Alert", + "skill proficiencies": ["Deception", "Stealth"], + "tool proficiency": "Thieves' Tools", + "equipment": [["Crowbar", "Dark Clothes", "Thieves' Tools", "16 GP"]] + }, + { + "name": "Soldier", + "ability scores": ["Strength", "Constitution"], + "feat": "Savage Attacker", + "skill proficiencies": ["Athletics", "Intimidation"], + "tool proficiency": "Gaming Set", + "equipment": [["Spear", "Shortbow", "20 Arrows", "Quiver", "Uniform", "16 GP"]] + } + ] + } + """.data(using: .utf8)! + + // When: Decoding the JSON data into a Backgrounds collection with configuration + let backgrounds = try decoder.decode(Backgrounds.self, from: jsonData, configuration: configuration) + + // Then: The collection should have the correct count + #expect(backgrounds.count == 3, "Should have 3 backgrounds") + + // Then: The find method should locate backgrounds by name + let acolyte = try #require(backgrounds["Acolyte"]) + #expect(acolyte.name == "Acolyte", "Found background should be Acolyte") + #expect(acolyte.feat == "Magic Initiate", "Acolyte feat should match") + + let criminal = try #require(backgrounds["Criminal"]) + #expect(criminal.skillProficiencies.skillNames == ["Deception", "Stealth"], "Criminal skills should match") + + // Then: The find method should return nil for non-existent backgrounds + let nonExistent = backgrounds["Wizard"] + #expect(nonExistent == nil, "Should not find non-existent background") + + // Then: Subscript access should work correctly + _ = try #require(backgrounds[0]) + _ = try #require(backgrounds[2]) + + // Then: Round-trip encoding should preserve data + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + let encodedData = try encoder.encode(backgrounds, configuration: configuration) + let decodedBackgrounds = try decoder.decode(Backgrounds.self, from: encodedData, configuration: configuration) + + #expect(decodedBackgrounds.count == backgrounds.count, "Count should match after round-trip") + let decodedCriminal = try #require(decodedBackgrounds["Criminal"]) + #expect(decodedCriminal.toolProficiency == backgrounds["Criminal"]?.toolProficiency, "Should find Criminal with matching tool proficiency after round-trip") + } +} diff --git a/Tests/RolePlayingCoreTests/CharacterGeneratorTests.swift b/Tests/RolePlayingCoreTests/CharacterGeneratorTests.swift new file mode 100644 index 0000000..8e6673c --- /dev/null +++ b/Tests/RolePlayingCoreTests/CharacterGeneratorTests.swift @@ -0,0 +1,39 @@ +// +// CharacterGeneratorTests.swift +// RolePlayingCoreTests +// +// Created by Brian Arnold on 7/9/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Testing +import RolePlayingCore +import Foundation + +@Suite("Character Generator Tests") +struct CharacterGeneratorTests { + + let bundle = Bundle.module + + let sampleSize = 256 + + @Test("Character generation") + func characterGenerator() async throws { + let configuration = try Configuration("TestCharacterGenerator", from: bundle) + let characterGenerator = try CharacterGenerator(configuration, from: bundle) + + for _ in 0..= 19, "max value") + } + + @Test("Dice times capitalized") + func diceTimesCapitalized() { + let formatString = "2D10" + let formatDice = formatString.parseDice + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + #expect((2...20).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((10.0...12.0).contains(mean), "expected mean around 11.0, got \(mean)") + + // TODO: Because 2d10 produces a bell curve, the actual min/max may be harder to get in a sample + #expect(minValue <= 3, "min value") + #expect(maxValue >= 19, "max value") + } + + @Test("Dice add modifier") + func diceAddModifier() { + let formatString = "1d20+4" + let formatDice = formatString.parseDice + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + #expect((5...24).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") + + #expect(minValue == 5, "min value") + #expect(maxValue == 24, "max value") + } + + @Test("Dice percent") + func dicePercent() { + let formatString = "d%" + let formatDice = formatString.parseDice + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + #expect((1...100).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((45.0...56.0).contains(mean), "expected mean around 50.5, got \(mean)") + + /// With such a big range, we may not hit the absolute min/max for the specified sample size. + #expect(minValue <= 2, "min value") + #expect(maxValue >= 99, "max value") + + // Check that the description has the % + if formatDice != nil { + #expect(formatDice!.description == "d%", "% description") + } + } + + @Test("Multiply with X") + func multiplyWithX() { + let formatString = "2d4x10" + let formatDice = formatString.parseDice + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + #expect((20...80).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((46.0...56.0).contains(mean), "expected mean around 50.0, got \(mean)") + + #expect(minValue == 20, "min value") + #expect(maxValue == 80, "max value") + } + + @Test("Multiply with asterisk") + func multiplyWithAsterisk() { + let formatString = "2d4*10" + let formatDice = formatString.parseDice + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + #expect((20...80).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((46.0...56.0).contains(mean), "expected mean around 50.0, got \(mean)") + + #expect(minValue == 20, "min value") + #expect(maxValue == 80, "max value") + } + + @Test("Divide") + func divide() { + let formatString = "d100/10" + let formatDice = formatString.parseDice + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + #expect((0...10).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((4.0...5.0).contains(mean), "expected mean around 4.5, got \(mean)") + + #expect(minValue >= 0, "min value") + #expect(maxValue <= 10, "max value") + } + + @Test("Dropping lowest") + func droppingLowest() { + let formatString = "4d6-L" + + let formatDice = formatString.parseDice + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + #expect((3...18).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + + let mean = Double(sum)/Double(sampleSize) + #expect((11.0...13.5).contains(mean), "expected mean around 12.25, got \(mean)") + + // TODO: Because 4x-L produces a sharp bell curve, the actual min/max may be harder to get in a sample + #expect(minValue <= 5, "min value") + #expect(maxValue >= 16, "max value") + + #expect(formatDice?.sides == 6, "Dice sides") + if let formatDice = formatDice { + #expect(formatDice.description == "4d6-L", "SimpleDice description") + } + } + + @Test("Complex dice format string") + func complexDiceFormatString() { + let formatString = "2d4+3d12-4" + let formatDice = formatString.parseDice + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + #expect((1...40).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + + let mean = Double(sum)/Double(sampleSize) + #expect((19.0...22.0).contains(mean), "expected mean around 20.5, got \(mean)") + + // TODO: Because this produces a sharp bell curve, the actual min/max may be harder to get in a sample + #expect(minValue <= 7, "min value") + #expect(maxValue >= 34, "max value") + + #expect(formatDice?.sides == 4, "Dice sides") + if formatDice != nil { + #expect(formatDice!.description == "2d4+3d12-4", "SimpleDice description") + } + } + + @Test("Complex dice operator precedence") + func complexDiceOperatorPrecedence() { + let formatString = "2d4+d12-2+5" + let formatDice = formatString.parseDice + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") + + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + #expect((6...23).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + + let mean = Double(sum)/Double(sampleSize) + #expect((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") + + // TODO: Because this produces a bell curve, the actual min/max may be harder to get in a sample + #expect(minValue <= 7, "min value") + #expect(maxValue >= 22, "max value") + + #expect(formatDice?.sides == 4, "Dice sides") + if formatDice != nil { + #expect(formatDice!.description == "2d4+d12-2+5", "SimpleDice description") + } + } + + @Test("Complex dice extra roll dropping with whitespace") + func complexDiceExtraRollDroppingWithWhitespace() { + let formatString = "3d4- L + d12 -\n2 + 5" + let formatDice = formatString.parseDice + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") + + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + #expect((6...23).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + + let mean = Double(sum)/Double(sampleSize) + #expect((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") + + // TODO: Because this produces a bell curve, the actual min/max may be harder to get in a sample + #expect(minValue <= 7, "min value") + #expect(maxValue >= 22, "max value") + + #expect(formatDice?.sides == 4, "Dice sides") + if formatDice != nil { + #expect(formatDice!.description == "3d4-L+d12-2+5", "SimpleDice description") + } + } + + @Test("Constant modifiers") + func constantModifiers() { + let formatString = "1+3" + let formatDice = formatString.parseDice + #expect(formatDice != nil, "Dice from \(formatString) should not be nil") + + if let formatDice = formatDice { + #expect(formatDice.description == "1+3", "format string") + let lastRoll = formatDice.roll() + #expect(lastRoll.description == "1 + 3", "format string") + } + } + + @Test("Invalid dice format strings", arguments: [ + ("d7", "unsupported dice number"), + ("dhello", "unsupported dice number"), + ("2+elephants", "unsupported character tokens"), + ("3d", "missing dice sides"), + ("2-", "missing expression"), + ("2d4H", "dropping missing minus"), + ("2-H", "dropping missing SimpleDice"), + ("3 4", "consecutive numbers"), + ("3++4", "consecutive math operators"), + ("d4d4", "consecutive dice expressions"), + ("dd4", "consecutive dice 'd' characters") + ]) + func invalidDiceFormatStrings(badFormatString: String, reason: String) { + let roll = badFormatString.parseDice + #expect(roll == nil, "'\(badFormatString)' \(reason)") + } +} diff --git a/Tests/RolePlayingCoreTests/DiceTests.swift b/Tests/RolePlayingCoreTests/DiceTests.swift new file mode 100644 index 0000000..89b9e1b --- /dev/null +++ b/Tests/RolePlayingCoreTests/DiceTests.swift @@ -0,0 +1,236 @@ +// +// DiceTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 11/12/16. +// Copyright © 2016-2017 Brian Arnold. All rights reserved. +// + +import Testing +import RolePlayingCore + +/// Use a sample size large enough to hit relatively tight ranges of +/// expected mean, min and max values below. +let sampleSize = 1024 + +/// Consequences of testing with the random number generator are: +/// +/// - Tolerance may be wide enough in some cases that they may not catch all regressions (false positives) +/// - Once in a blue moon, tests may fail just outside of the tolerance (false negatives) +/// +@Suite("Dice Tests") +struct DiceTests { + + @Test("Create die") + func createDie() { + // Test raw value creation matches enums + let d4 = Die(rawValue: 4) + #expect(d4 == Die.d4, "d4") + let d6 = Die(rawValue: 6) + #expect(d6 == Die.d6, "d6") + let d8 = Die(rawValue: 8) + #expect(d8 == Die.d8, "d8") + let d10 = Die(rawValue: 10) + #expect(d10 == Die.d10, "d10") + let d12 = Die(rawValue: 12) + #expect(d12 == Die.d12, "d12") + let d20 = Die(rawValue: 20) + #expect(d20 == Die.d20, "d20") + let d100 = Die(rawValue: 100) + #expect(d100 == Die.d100, "Dice %") + } + + @Test("Create die negative") + func createDieNegative() { + // Negative tests: bad raw values and strings + let badDie = Die(rawValue: 7) + #expect(badDie == nil, "d7 should be nil") + } + + @Test("Roll die") + func rollDie() { + let die: Die = .d4 + + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = die.roll() + #expect((1...4).contains(roll), "rolling d4, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + + } + let mean = Double(sum)/Double(sampleSize) + #expect((2.0...3.0).contains(mean), "expected mean around 2.5, got \(mean)") + + #expect(minValue == 1, "min value") + #expect(maxValue == 4, "max value") + + #expect(Die.d4.description == "d4", "d4 description") + } + + @Test("Dice modifier") + func diceModifier() { + let diceModifier = DiceModifier(7) + let diceRoll = diceModifier.roll() + #expect(diceModifier.modifier == 7, "dice modifier value") + #expect(diceRoll.result == 7, "dice modifier roll") + #expect(diceModifier.sides == 7, "dice modifier sides") + #expect(diceModifier.description == "7", "dice modifier description") + #expect(diceRoll.description == "7", "dice modifier lastRollDescription") + } + + @Test("Simple dice d12") + func simpleDiceD12() { + let simpleDice = SimpleDice(.d12) + + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = simpleDice.roll().result + #expect((1...12).contains(roll), "rolling d12, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((6.0...7.0).contains(mean), "expected mean around 6.5, got \(mean)") + + // TODO: Because 2x produces a bell curve, the actual min/max may be harder to get in a sample + #expect(minValue <= 1, "min value") + #expect(maxValue >= 12, "max value") + + #expect(simpleDice.sides == 12, "SimpleDice sides") + #expect("\(simpleDice.description)" == "d12", "SimpleDice description") + } + + @Test("Simple dice 2d8") + func testSimpleDice2d8() { + let simpleDice = SimpleDice(.d8, times: 2) + + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = simpleDice.roll().result + #expect((2...16).contains(roll), "rolling 2d8, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((7.5...9.5).contains(mean), "expected mean around 8.5, got \(mean)") + + // TODO: Because 2x produces a bell curve, the actual min/max may be harder to get in a sample + #expect(minValue == 2, "min value") + #expect(maxValue == 16, "max value") + + #expect(simpleDice.sides == 8, "SimpleDice sides") + #expect("\(simpleDice.description)" == "2d8", "SimpleDice description") + } + + @Test("Dropping dice lowest") + func droppingDiceLowest() { + let simpleDice = DroppingDice(.d6, times: 4, drop: .lowest) + + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = simpleDice.roll().result + #expect((3...18).contains(roll), "rolling 4d6-L, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((11.0...13.5).contains(mean), "expected mean around 12.25, got \(mean)") + + // TODO: Because 4x-L produces a sharp bell curve, the actual min/max may be harder to get in a sample + #expect(minValue <= 5, "min value") + #expect(maxValue >= 16, "max value") + + #expect(simpleDice.sides == 6, "SimpleDice sides") + #expect("\(simpleDice.description)" == "4d6-L", "SimpleDice description") + + // TODO: verify that it is actually dropping the lowest score. + } + + @Test("Dropping dice highest") + func droppingDiceHighest() { + let simpleDice = DroppingDice(.d4, times: 3, drop: .highest) + + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = simpleDice.roll().result + #expect((2...8).contains(roll), "rolling 3d4-H, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((3.7...4.3).contains(mean), "expected mean around 4, got \(mean)") + + #expect(minValue == 2, "min value") + #expect(maxValue == 8, "max value") + + #expect(simpleDice.sides == 4, "SimpleDice sides") + #expect("\(simpleDice.description)" == "3d4-H", "SimpleDice description") + + // TODO: verify that it is actually dropping the highest score. + } + + @Test("Compound dice with modifier") + func compoundDiceWithModifier() { + let compoundDice = CompoundDice(.d8, times: 2, modifier: 4) + + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = compoundDice.roll().result + #expect((6...20).contains(roll), "rolling 2d8+4, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((12.0...14.0).contains(mean), "expected mean around 13.0, got \(mean)") + + #expect(minValue == 6, "min value") + #expect(maxValue == 20, "max value") + + #expect(compoundDice.sides == 8, "CompoundDice sides") + + #expect("\(compoundDice.description)" == "2d8+4", "CompoundDice description") + } + + @Test("Compound dice with dice") + func compoundDiceWithDice() { + let compoundDice = CompoundDice(lhs: SimpleDice(.d8, times: 2), rhs: SimpleDice(.d4), mathOperator: "+") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = compoundDice.roll().result + #expect((3...20).contains(roll), "rolling 2d8+d4, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((11.0...12.0).contains(mean), "expected mean around 11.5, got \(mean)") + + #expect(minValue <= 4, "min value") + #expect(maxValue >= 19, "max value") + + #expect(compoundDice.sides == 8, "CompoundDice sides") + #expect("\(compoundDice.description)" == "2d8+d4", "CompoundDice description") + } +} + diff --git a/Tests/RolePlayingCoreTests/HeightTests.swift b/Tests/RolePlayingCoreTests/HeightTests.swift new file mode 100644 index 0000000..3ccfa2f --- /dev/null +++ b/Tests/RolePlayingCoreTests/HeightTests.swift @@ -0,0 +1,180 @@ +// +// UnitHeightTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/5/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Testing +import RolePlayingCore +import Foundation + +@Suite("Height Parsing and Serialization Tests") +struct UnitHeightTests { + + @Test("Parse various height formats") + func heights() async throws { + do { + let howTall = "5".parseHeight + #expect(howTall != nil, "height should be non-nil") + #expect(howTall?.value == 5.0, "height should be 5.0") + } + + do { + let howTall = "3.0".parseHeight + #expect(howTall != nil, "height should be non-nil") + #expect(howTall?.value == 3.0, "height should be 3.0") + } + + do { + let howTall = "4 ft 3 in".parseHeight + #expect(howTall != nil, "height should be non-nil") + #expect(howTall?.value == 4.0 + 3.0/12.0, "height should be 4.25") + } + + do { + let howTall = "73in".parseHeight + #expect(howTall != nil, "height should be non-nil") + let howTallValue = howTall?.value ?? 0.0 + #expect(abs(howTallValue - (6.0 + 1.0/12.0)) < 0.0001, "height should be 6.08") + } + + do { + let howTall = "5'4\"".parseHeight + #expect(howTall != nil, "height should be non-nil") + #expect(howTall?.value == 5.0 + 4.0/12.0, "height should be 5.33") + } + + do { + let howTall = "130 cm".parseHeight?.converted(to: .meters) + #expect(howTall != nil, "height should be non-nil") + #expect(howTall?.value == 1.3, "height should be 1.3") + } + + do { + let howTall = "2.1m".parseHeight + #expect(howTall != nil, "height should be non-nil") + #expect(howTall?.value == 2.1, "height should be 2.1") + } + } + + @Test("Parse invalid height strings") + func invalidHeights() async throws { + let howTall = "3 hello".parseHeight + #expect(howTall == nil, "height should be nil") + } + + @Test("Encode height to JSON") + func encodingHeight() async throws { + struct HeightContainer: Encodable { + let height: Height + + enum CodingKeys: String, CodingKey { + case height + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("\(height)", forKey: .height) + } + } + let heightContainer = HeightContainer(height: Height(value: 3.2, unit: .meters)) + + // Do a round-trip through serialization, then deserialization to confirm that it became a string + let encoder = JSONEncoder() + let encoded = try encoder.encode(heightContainer) + let deserialized = try JSONSerialization.jsonObject(with: encoded, options: .allowFragments) + print("deserialized = \n\(deserialized)") + let container = deserialized as? [String: Any] + let height = container?["height"] as? String + #expect(height == "3.2 m", "Encoded height did not become a string") + } + + @Test("Decode height from JSON") + func decodingHeight() async throws { + struct HeightContainer: Decodable { + let height: Height + } + + // Test decoding from string height + do { + let traits = """ + { + "height": "4ft 3in" + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let decoded = try decoder.decode(HeightContainer.self, from: traits) + + #expect(decoded.height.value == 4.25, "Decoded height should be 4 ft 3 in") + } + + // Test decoding from double height + do { + let traits = """ + { + "height": 6.5 + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let decoded = try decoder.decode(HeightContainer.self, from: traits) + + #expect(decoded.height.value == 6.5, "Decoded height should be 6.5") + } + + // Test failure to decode + do { + let traits = """ + { + "height": "abcdefg" + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + #expect(throws: (any Error).self) { + _ = try decoder.decode(HeightContainer.self, from: traits) + } + } + } + + @Test("Decode optional height from JSON") + func decodingHeightIfPresent() async throws { + struct HeightContainer: Decodable { + let height: Height? // The ? will trigger decodeIfPresent in the decoder + } + + // Test decoding from string height + do { + let traits = """ + { + "height": "4ft 3in" + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let decoded = try decoder.decode(HeightContainer.self, from: traits) + + #expect(decoded.height?.value == 4.25, "Decoded height should be 4 ft 3 in") + } + + // Test decoding from double height + do { + let traits = """ + { + "height": 6.5 + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let decoded = try decoder.decode(HeightContainer.self, from: traits) + + #expect(decoded.height?.value == 6.5, "Decoded height should be 6.5") + } + } +} + + + diff --git a/Tests/RolePlayingCoreTests/JSONFileTests.swift b/Tests/RolePlayingCoreTests/JSONFileTests.swift new file mode 100644 index 0000000..5aa1243 --- /dev/null +++ b/Tests/RolePlayingCoreTests/JSONFileTests.swift @@ -0,0 +1,98 @@ +// +// JSONFileTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/17/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Testing +import RolePlayingCore +import Foundation + +struct JSONFileData: Codable { + let boolValue: Bool + + struct DictionaryValue: Codable { + let stringValue: String + let doubleValue: Double + let arrayValue: [Int] + } + let dictionaryValue: DictionaryValue +} + +struct AnyFileData: Codable { + // No-op +} + +@Suite("JSON File Loading Tests") +struct JSONFileTests { + + let decoder = JSONDecoder() + + let testBundle = Bundle.module + + @Test("Load and parse valid JSON file") + func json() async throws { + // This test file has all of the basic elements of a JSON file + let bundle = testBundle + + let jsonData = try bundle.loadJSON("JSONFile") + let jsonObject = try decoder.decode(JSONFileData.self, from: jsonData) + + // Test for contents of JSON file + let bool = jsonObject.boolValue + #expect(bool == true, "bool should be true") + + let dictionary = jsonObject.dictionaryValue + + let string = dictionary.stringValue + #expect(string == "foo", "string should be \"foo\"") + + let double = dictionary.doubleValue + #expect(double == 2.1, "double should be 2.1") + + let array = dictionary.arrayValue + #expect(array == [2, 3], "array should be [2, 3]") + } + + @Test("Attempt to load missing JSON file") + func missingJSON() async throws { + // This test file is not present in the bundle. + let bundle = testBundle + + #expect(throws: (any Error).self) { + _ = try bundle.loadJSON("MissingJSONFile") + } + + // Verify it's specifically a ConfigurationError + do { + _ = try bundle.loadJSON("MissingJSONFile") + Issue.record("Should have thrown an error") + } catch { + #expect(error is ConfigurationError, "expected ConfigurationError, got \(error)") + } + } + + @Test("Attempt to parse invalid JSON file") + func invalidJSON() async throws { + // This test file contains errors in formatting. + let bundle = testBundle + + #expect(throws: (any Error).self) { + let jsonData = try bundle.loadJSON("InvalidJSONFile") + _ = try decoder.decode(AnyFileData.self, from: jsonData) + } + } + + @Test("Attempt to parse half-baked JSON file") + func halfBakedJSON() async throws { + // This test file lacks a dictionary at the root. + let bundle = testBundle + + #expect(throws: (any Error).self) { + let jsonData = try bundle.loadJSON("HalfBakedJSONFile") + _ = try decoder.decode(AnyFileData.self, from: jsonData) + } + } +} diff --git a/RolePlayingCore/RolePlayingCoreTests/NameGeneratorTests.swift b/Tests/RolePlayingCoreTests/NameGeneratorTests.swift similarity index 68% rename from RolePlayingCore/RolePlayingCoreTests/NameGeneratorTests.swift rename to Tests/RolePlayingCoreTests/NameGeneratorTests.swift index 95dbda8..5fd1456 100644 --- a/RolePlayingCore/RolePlayingCoreTests/NameGeneratorTests.swift +++ b/Tests/RolePlayingCoreTests/NameGeneratorTests.swift @@ -6,19 +6,21 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing import RolePlayingCore +import Foundation /// Use a mock random number generator so we can hardcode expected generated names. -class NameGeneratorTests: XCTestCase { +@Suite("Name Generator Tests") +struct NameGeneratorTests { - func testNameGenerator() { - let bundle = Bundle(for: NameGeneratorTests.self) - let data = try! bundle.loadJSON("TestNames") + @Test("Name generator produces expected names with mock generator") + func nameGenerator() throws { + let bundle = Bundle.module + let data = try bundle.loadJSON("TestNames") let decoder = JSONDecoder() - let nameGenerator = try! decoder.decode(NameGenerator.self, from: data) + let nameGenerator = try decoder.decode(NameGenerator.self, from: data) var generator = MockIndexGenerator() let expectedNames = ["Abadh", "Eunach", "Aillach", "Alsearbore", "Aod", "Aodel", "Edan", "Aodvoda", "Argcran", "Art"] @@ -26,7 +28,7 @@ class NameGeneratorTests: XCTestCase { for index in 0..<10 { let name = nameGenerator.makeName(using: &generator) generatedNames.append(name) - XCTAssertEqual(expectedNames[index], name, "expected generated name") + #expect(expectedNames[index] == name, "Expected generated name to match") } print("Generated names: \(generatedNames)") @@ -34,7 +36,8 @@ class NameGeneratorTests: XCTestCase { let _ = nameGenerator.makeName() } - func testRandomNumberGenerator() { + @Test("Random number generator produces sequential indices") + func randomNumberGenerator() { var generator = MockIndexGenerator() let testNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] diff --git a/Tests/RolePlayingCoreTests/PlayerTests.swift b/Tests/RolePlayingCoreTests/PlayerTests.swift new file mode 100644 index 0000000..7898241 --- /dev/null +++ b/Tests/RolePlayingCoreTests/PlayerTests.swift @@ -0,0 +1,638 @@ +// +// PlayerTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/18/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Testing +@testable import RolePlayingCore +import Foundation + +@Suite("Player Tests") +struct PlayerTests { + + let decoder = JSONDecoder() + let configuration: Configuration + let skillTraits: Data + let skills: Skills + let soldierTraits: Data + let soldier: BackgroundTraits + let humanTraits: Data + let human: SpeciesTraits + let fighterTraits: Data + let fighter: ClassTraits + + init() throws { + configuration = try Configuration("TestConfiguration", from: .module) + + self.skillTraits = """ + { + "skills": [ + { + "name": "Acrobatics", + "ability": "Dexterity" + }, + { + "name": "Animal Handling", + "ability": "Wisdom" + }, + { + "name": "Arcana", + "ability": "Intelligence" + }, + { + "name": "Athletics", + "ability": "Strength" + }, + { + "name": "Deception", + "ability": "Charisma" + }, + { + "name": "History", + "ability": "Intelligence" + }, + { + "name": "Insight", + "ability": "Wisdom" + }, + { + "name": "Intimidation", + "ability": "Charisma" + }, + { + "name": "Investigation", + "ability": "Intelligence" + }, + { + "name": "Medicine", + "ability": "Wisdom" + }, + { + "name": "Nature", + "ability": "Intelligence" + }, + { + "name": "Perception", + "ability": "Wisdom" + }, + { + "name": "Performance", + "ability": "Charisma" + }, + { + "name": "Persuasion", + "ability": "Charisma" + }, + { + "name": "Religion", + "ability": "Intelligence" + }, + { + "name": "Sleight of Hand", + "ability": "Dexterity" + }, + { + "name": "Stealth", + "ability": "Dexterity" + }, + { + "name": "Survival", + "ability": "Wisdom" + } + ] + } + """.data(using: .utf8)! + self.skills = try! decoder.decode(Skills.self, from: self.skillTraits) + + self.soldierTraits = """ + { + "name": "Soldier", + "ability scores": ["Strength", "Dexterity", "Constitution"], + "feat": "Savage Attacker", + "skill proficiencies" : ["Athletics", "Intimidation"], + "tool proficiency": "Gaming Set", + "equipment": [["Spear", "Shortbow", "20 Arrows", "Gaming Set", "Healer's Kit", "Quiver", "Traveler's Clothes", "14 GP"], ["50 GP"]] + } + """.data(using: .utf8)! + self.soldier = try! decoder.decode(BackgroundTraits.self, from: self.soldierTraits, configuration: configuration) + + self.fighterTraits = """ + { + "name": "Fighter", + "plural": "Fighters", + "hit dice": "d10", + "primary ability": ["Strength"], + "alternate primary ability": ["Dexterity"], + "saving throws": ["Strength", "Constitution"], + "starting wealth": "5d4x10", + "experience points": [0, 300, 900, 2700] + } + """.data(using: .utf8)! + self.fighter = try! decoder.decode(ClassTraits.self, from: self.fighterTraits, configuration: configuration) + + self.humanTraits = """ + { + "name": "Human", + "plural": "Humans", + "lifespan": 90, + "base height": "4'8\\"", + "height modifier": "2d10", + "base weight": 110, + "weight modifier": "2d4", + "speed": 30, + "languages": ["Common"], + "extra languages": 1 + } + """.data(using: .utf8)! + self.human = try! decoder.decode(SpeciesTraits.self, from: self.humanTraits, configuration: configuration) + } + + @Test("Create player with basic traits") + func player() async throws { + let player = Player("Frodo", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .female, alignment: Alignment(.lawful, .neutral)) + #expect(player.name == "Frodo", "player name") + #expect(player.className == "Fighter", "class name") + #expect(player.speciesName == "Human", "species name") + + #expect(player.descriptiveTraits.count == 0, "descriptiveTraits") + + #expect(player.gender == Player.Gender.female, "gender") + #expect(player.alignment == Alignment(.lawful, .neutral), "alignment") + + // Abilities is scores plus species modifiers, so + 1 + for key in player.abilities.abilities { + let score = player.abilities[key]! + #expect((3...20).contains(score), "ability score \(score) for \(key)") + } + + // I do the maths + #expect((4..<7).contains(player.height.value), "height \(player.height.value)") + + #expect((1...10).contains(player.maximumHitPoints), "maximum hit points") + #expect(player.maximumHitPoints == player.currentHitPoints, "current hit points") + #expect("\(player.classTraits.hitDice)" == "d10", "hit dice") + #expect(player.experiencePoints == 0, "experience points") + #expect(player.level == 1, "level") + + #expect((50...200).contains(player.money.value), "money \(player.money.value)") + + #expect(player.proficiencyBonus == 2, "proficiency bonus") + } + + @Test("Decode player with minimum required traits") + func minimumTraitsPlayer() async throws { + let playerTraits = """ + { + "name": "Bilbo", + "background": "Sailor", + "species": "Human", + "class": "Fighter", + "gender": "Male", + "height": "3'9\\"", + "ability scores": {"Dexterity": 13, "Charisma": 12}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skill proficiencies": ["Athletics"], + "money": 130, + "maximum hit points": 10 + } + """.data(using: .utf8)! + + let player = try decoder.decode(Player.self, from: playerTraits, configuration: configuration) + player.speciesTraits = human + player.classTraits = fighter + + #expect(player.name == "Bilbo", "player name") + #expect(player.className == "Fighter", "class name") + #expect(player.speciesName == "Human", "species name") + + #expect(player.gender == Player.Gender.male, "gender") + #expect(player.alignment == nil, "alignment") + + #expect(player.abilities[.dexterity] == 14, "dexterity") + #expect(player.abilities[.charisma] == 12, "charisma") + + #expect(player.height.value == 3.75, "height") + + #expect(player.maximumHitPoints == 10, "maximum hit points") + #expect(player.maximumHitPoints == player.currentHitPoints, "current hit points") + + #expect(player.experiencePoints == 0, "experience points") + #expect(player.level == 1, "level") + + #expect(player.money.value == 130, "money") + } + + @Test("Decode player with optional traits and level up") + func optionalPlayerTraits() async throws { + let playerTraits = """ + { + "name": "Bilbo", + "background": "Sailor", + "species": "Human", + "class": "Fighter", + "alignment": "Lawful Evil", + "height": "3'9\\"", + "ability scores": {"Strength": 12}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skill proficiencies": ["Athletics"], + "money": 130, + "maximum hit points": 10, + "experience points": 2300, + "level": 2 + } + """.data(using: .utf8)! + + let player = try decoder.decode(Player.self, from: playerTraits, configuration: configuration) + player.speciesTraits = human + player.classTraits = fighter + + #expect(player.gender == nil, "gender") + #expect(player.alignment == Alignment(.lawful, .evil), "alignment") + + #expect(player.canLevelUp == true, "level up") + #expect("\(player.hitDice)" == "2d10", "hit dice") + player.levelUp() + #expect(player.level == 3, "level") + #expect(player.maximumHitPoints > 15, "experience points") + + #expect(player.canLevelUp == false, "level up") + #expect("\(player.hitDice)" == "3d10", "hit dice") + + player.levelUp() + #expect(player.level == 3, "level") + } + + @Test("Encode and decode player round trip") + func playerRoundTrip() async throws { + let playerTraits = """ + { + "name": "Bilbo", + "background": "Sailor", + "species": "Human", + "class": "Fighter", + "gender": "Male", + "alignment": "Neutral Good", + "height": "3'9\\"", + "ability scores": {"Dexterity": 13}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skill proficiencies": ["Athletics"], + "money": 130, + "maximum hit points": 20, + "current hit points": 9, + "level": 2 + } + """.data(using: .utf8)! + + let player = try #require(try? decoder.decode(Player.self, from: playerTraits, configuration: configuration)) + let encoder = JSONEncoder() + let encodedPlayer = try encoder.encode(player, configuration: configuration) + let encoded = try #require(try? JSONSerialization.jsonObject(with: encodedPlayer, options: []) as? [String: Any]) + + #expect(encoded["name"] as? String == "Bilbo", "player traits round trip name") + #expect(encoded["gender"] as? String == "Male", "player traits round trip gender") + + let alignment = try #require(encoded["alignment"] as? [String: Double]) + #expect(alignment["ethics"] == 0, "player traits round trip alignment ethics") + #expect(alignment["morals"] == 1, "player traits round trip alignment morals") + + #expect(encoded["height"] as? String == "3.75 ft", "player traits round trip height") + + let abilities = try #require(encoded["ability scores"] as? [String: Int]) + #expect(abilities["Dexterity"] == 13, "player traits round trip ability scores") + + let backgroundAbilities = try #require(encoded["background ability scores"] as? [String]) + #expect(backgroundAbilities.count == 3, "player traits round trip background ability scores count") + #expect(backgroundAbilities.contains("Strength"), "player traits round trip background ability scores") + #expect(!backgroundAbilities.contains("Charisma"), "player traits round trip background ability scores") + + #expect(encoded["money"] as? String == "130.0 gp", "player traits round trip money") + #expect(encoded["maximum hit points"] as? Int == 20, "player traits round trip maximum hit points") + #expect(encoded["current hit points"] as? Int == 9, "player traits round trip current hit points") + #expect(encoded["level"] as? Int == 2, "player traits round trip level") + } + + @Test("Verify missing required traits cause decode failure", arguments: [ + "{:}", + """ + { + "name": "Bilbo" + } + """, + """ + { + "name": "Bilbo", + "height": "3'9\\"", + } + """, + """ + { + "name": "Bilbo", + "height": "3'9\\"", + "ability scores": {"Dexterity": 13} + } + """, + """ + { + "name": "Bilbo", + "height": "3'9\\"", + "ability scores": {"Dexterity": 13}, + "money": 130] + } + """ + ]) + func missingTraits(json: String) async throws { + let traits = json.data(using: .utf8)! + let player = try? decoder.decode(Player.self, from: traits, configuration: configuration) + #expect(player == nil) + } + + func expectedModifier(for abilityScore: Int) -> Int { + let selfMinus10 = abilityScore - 10 + return selfMinus10 < 0 ? Int((Double(selfMinus10) / 2.0).rounded(.down)) : selfMinus10 / 2 + } + + @Test("Verify computed properties") + func computedProperties() async throws { + let player = Player("Gandalf", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.neutral, .good)) + + // Test speed (from species traits) + #expect(player.speed == 30, "speed should match species speed") + #expect(player.size == .medium, "size should match species size") + + // Test modifiers + for ability in player.modifiers.abilities { + let abilityScore = player.abilities[ability]! + let expectedModifier = expectedModifier(for: abilityScore) + #expect(player.modifiers[ability] == expectedModifier, "modifier calculation") + } + + // Test initiative + #expect(player.initiativeModifier == player.modifiers[.dexterity], "initiative modifier") + #expect(player.initiativeScore == 10 + player.modifiers[.dexterity], "initiative score") + + // Test passive perception + #expect(player.passivePerception == 10 + player.modifiers[.wisdom], "passive perception") + } + + @Test("Verify proficiency bonus at different levels", arguments: [ + (1, 2), (4, 2), + (5, 3), (8, 3), + (9, 4), (12, 4), + (13, 5), (16, 5), + (17, 6), (20, 6) + ]) + func proficiencyBonus(level: Int, expectedBonus: Int) async throws { + let player = Player("Aragorn", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) + player.level = level + #expect(player.proficiencyBonus == expectedBonus, "proficiency bonus at level \(level)") + } + + @Test("Verify hit dice at different levels", arguments: [ + (1, "d10"), + (5, "5d10"), + (10, "10d10"), + (20, "20d10") + ]) + func hitDiceAtDifferentLevels(level: Int, expectedDice: String) async throws { + let player = Player("Legolas", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) + player.level = level + #expect("\(player.hitDice)" == expectedDice, "hit dice at level \(level)") + } + + @Test("Verify Hashable conformance") + func hashableConformance() async throws { + let player1 = Player("Gimli", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.lawful, .good)) + player1.descriptiveTraits = ["ideal": "Honor", "bond": "My axe"] + + let player2 = Player("Gimli", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.lawful, .good)) + player2.speciesTraits = human + player2.classTraits = fighter + player2.baseAbilities = player1.baseAbilities + player2.height = player1.height + player2.maximumHitPoints = player1.maximumHitPoints + player2.currentHitPoints = player1.currentHitPoints + player2.experiencePoints = player1.experiencePoints + player2.money = player1.money + player2.descriptiveTraits = ["ideal": "Honor", "bond": "My axe"] + + // Test equality + #expect(player1 == player2, "identical players should be equal") + + // Test hash values + var hasher1 = Hasher() + player1.hash(into: &hasher1) + let hash1 = hasher1.finalize() + + var hasher2 = Hasher() + player2.hash(into: &hasher2) + let hash2 = hasher2.finalize() + + #expect(hash1 == hash2, "identical players should have same hash") + + // Test that players can be used in Sets + let playerSet: Set = [player1, player2] + #expect(playerSet.count == 1, "set should contain only one unique player") + } + + @Test("Verify player inequality") + func playerInequality() async throws { + let player1 = Player("Boromir", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) + let player2 = Player("Faramir", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) + + // Different names + #expect(player1 != player2, "players with different names should not be equal") + + // Different hit points + let player3 = Player("Boromir", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) + player3.baseAbilities = player1.baseAbilities + player3.height = player1.height + player3.money = player1.money + player3.currentHitPoints = player3.currentHitPoints - 5 + + #expect(player1 != player3, "players with different current HP should not be equal") + } + + @Test("Verify gender cases") + func genderCases() async throws { + // Test all gender cases + let female = Player("Diana", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .female) + #expect(female.gender == .female, "female gender") + + let male = Player("Arthur", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .male) + #expect(male.gender == .male, "male gender") + + let agender = Player("Riley", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: nil) + #expect(agender.gender == nil, "nil gender for androgynous/hermaphroditic") + } + + @Test("Verify descriptive traits") + func descriptiveTraits() async throws { + let player = Player("Samwise", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) + + // Initially empty + #expect(player.descriptiveTraits.count == 0) + + // Add traits + player.descriptiveTraits["ideal"] = "Loyalty" + player.descriptiveTraits["bond"] = "My friends" + player.descriptiveTraits["flaw"] = "Too trusting" + player.descriptiveTraits["background"] = "Gardener" + + #expect(player.descriptiveTraits.count == 4) + #expect(player.descriptiveTraits["ideal"] == "Loyalty") + #expect(player.descriptiveTraits["bond"] == "My friends") + #expect(player.descriptiveTraits["flaw"] == "Too trusting") + #expect(player.descriptiveTraits["background"] == "Gardener") + } + + @Test("Verify ability scores roll") + func abilityScoresRoll() async throws { + var abilities = AbilityScores() + abilities.roll() + + // Verify all abilities have valid scores (4d6-L should give 3-18) + for ability in abilities.abilities { + let score = abilities[ability]! + #expect((3...18).contains(score), "rolled ability score should be between 3 and 18") + } + + // Verify all six abilities are set + #expect(abilities.abilities.count == 6, "should have 6 abilities") + } + + @Test("Verify dice hit dice extension") + func diceHitDiceExtension() async throws { + // Test the hitDice extension on Dice + let d6 = SimpleDice(.d6) + + let level1HitDice = d6.hitDice(level: 1) + #expect("\(level1HitDice)" == "d6") + + let level5HitDice = d6.hitDice(level: 5) + #expect("\(level5HitDice)" == "5d6") + + let level10HitDice = d6.hitDice(level: 10) + #expect("\(level10HitDice)" == "10d6") + } + + @Test("Verify species and class traits didSet") + func speciesAndClassTraitsDidSet() async throws { + let player = Player("Test", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) + + #expect(player.backgroundName == "Soldier") + #expect(player.speciesName == "Human") + #expect(player.className == "Fighter") + + // Create a mock second species (we'll reuse human but check the didSet is called) + let mockSpecies = human + player.speciesTraits = mockSpecies + #expect(player.speciesName == mockSpecies.name) + + // Create a mock second class (we'll reuse fighter but check the didSet is called) + let mockClass = fighter + player.classTraits = mockClass + #expect(player.className == mockClass.name) + } + + @Test("Encode and decode player with descriptive traits") + func playerEncodingWithDescriptiveTraits() async throws { + let encoder = JSONEncoder() + + let playerTraits = """ + { + "name": "Pippin", + "background": "Sailor", + "species": "Human", + "class": "Fighter", + "descriptive traits": { + "ideal": "Adventure", + "bond": "The Shire", + "flaw": "Impulsive" + }, + "height": "4'2\\"", + "ability scores": {"Charisma": 14, "Dexterity": 15}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skill proficiencies": ["Athletics"], + "money": 100, + "maximum hit points": 12 + } + """.data(using: .utf8)! + + let player = try decoder.decode(Player.self, from: playerTraits, configuration: configuration) + #expect(player.descriptiveTraits.count == 3) + #expect(player.descriptiveTraits["ideal"] == "Adventure") + #expect(player.descriptiveTraits["bond"] == "The Shire") + #expect(player.descriptiveTraits["flaw"] == "Impulsive") + + // Test encoding + let encoded = try encoder.encode(player, configuration: configuration) + let decodedDict = try #require(try? JSONSerialization.jsonObject(with: encoded) as? [String: Any]) + let encodedTraits = try #require(decodedDict["descriptive traits"] as? [String: String]) + #expect(encodedTraits["ideal"] == "Adventure") + } + + @Test("Verify rollHitPoints class method") + func rollHitPointsClassMethod() async throws { + // Test the static rollHitPoints method + let hitPoints = Player.rollHitPoints(classTraits: fighter, speciesTraits: human) + + // Fighter has d10, so minimum should be 6 (max(10/2+1, roll)), + // but roll could be lower, so minimum is actually 6 + species bonus + // Maximum is 10 + species bonus + #expect((6...10).contains(hitPoints), "hit points should be in valid range") + } + + @Test("Verify multiple level ups") + func multipleLevelUps() async throws { + let playerTraits = """ + { + "name": "Merry", + "background": "Sailor", + "species": "Human", + "class": "Fighter", + "height": "4'2\\"", + "ability scores": {"Strength": 14}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skill proficiencies": ["Athletics"], + "money": 100, + "maximum hit points": 12, + "experience points": 0, + "level": 1 + } + """.data(using: .utf8)! + + let player = try decoder.decode(Player.self, from: playerTraits, configuration: configuration) + player.speciesTraits = human + player.classTraits = fighter + + let initialHP = player.maximumHitPoints + + // Add enough XP to level up to level 2 + player.experiencePoints = 301 + #expect(player.canLevelUp) + player.levelUp() + #expect(player.level == 2) + #expect(player.maximumHitPoints > initialHP, "HP should increase on level up") + + // Add enough XP to level up to level 3 + player.experiencePoints = 901 + #expect(player.canLevelUp) + player.levelUp() + #expect(player.level == 3) + + // Add enough XP to level up to level 4 + player.experiencePoints = 2701 + #expect(player.canLevelUp) + player.levelUp() + #expect(player.level == 4) + + // Without enough XP, cannot level up + player.experiencePoints = 2701 + #expect(player.canLevelUp == false) + player.levelUp() // Should do nothing + #expect(player.level == 4) + } +} diff --git a/Tests/RolePlayingCoreTests/PlayersTests.swift b/Tests/RolePlayingCoreTests/PlayersTests.swift new file mode 100644 index 0000000..6b8a852 --- /dev/null +++ b/Tests/RolePlayingCoreTests/PlayersTests.swift @@ -0,0 +1,62 @@ +// +// PlayersTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/18/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Testing +import RolePlayingCore +import Foundation + +@Suite("Players Tests") +struct PlayersTests { + + let bundle = Bundle.module + let decoder = JSONDecoder() + let configuration: Configuration + + init() throws { + configuration = try Configuration("TestConfiguration", from: .module) + } + + @Test("Load and manipulate players collection") + func players() async throws { + let playersData = try bundle.loadJSON("TestPlayers") + let players = try decoder.decode(Players.self, from: playersData, configuration: configuration) + + #expect(players.players.count == 2, "players count") + #expect(players.count == 2, "players count") + + let removedPlayer = try #require(players[0]) + players.remove(at: 0) + #expect(players.count == 1, "players count") + + players.insert(removedPlayer, at: 1) + #expect(players.count == 2, "players count") + #expect(players[1]! === removedPlayer, "players count") + } + + @Test("Verify missing or invalid traits cause resolution failure", arguments: [ + "InvalidClassPlayers", + "InvalidSpeciesPlayers", + "MissingClassPlayers", + "MissingSpeciesPlayers" + ]) + func missingTraits(jsonFile: String) async throws { + let playersData = try bundle.loadJSON(jsonFile) + + // Attempt to decode, expecting an error to be thrown during decoding + // since all trait resolution now happens during the decoding phase + do { + _ = try decoder.decode(Players.self, from: playersData, configuration: configuration) + + // If we reach here, no error was thrown - the test should fail + Issue.record("Expected an error to be thrown for \(jsonFile), but none was thrown") + } catch { + // Success - an error was thrown as expected + // Optionally, you could verify the specific error type or message here + } + } +} diff --git a/Tests/RolePlayingCoreTests/SpeciesNamesTests.swift b/Tests/RolePlayingCoreTests/SpeciesNamesTests.swift new file mode 100644 index 0000000..481d3a2 --- /dev/null +++ b/Tests/RolePlayingCoreTests/SpeciesNamesTests.swift @@ -0,0 +1,63 @@ +// +// SpeciesNamesTests.swift +// RolePlayingCoreTests +// +// Created by Brian Arnold on 7/8/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Testing +@testable import RolePlayingCore +import Foundation + +@Suite("Species Names") +struct SpeciesNamesTests { + + let configuration: Configuration + + init() throws { + configuration = try Configuration("TestConfiguration", from: .module) + } + + @Test("Loading and generating species names") + func speciesNames() async throws { + let bundle = Bundle.module + let data = try bundle.loadJSON("TestSpeciesNames") + let decoder = JSONDecoder() + let speciesNames = try decoder.decode(SpeciesNames.self, from: data) + + #expect(speciesNames.names.count == 8, "Number of species name families") + + // TODO: find a way to test just the minimum functionality. + // In the meantime, use the test species. + let jsonData = try bundle.loadJSON("TestSpecies") + let species = try decoder.decode(Species.self, from: jsonData, configuration: configuration) + let moreJsonData = try bundle.loadJSON("TestMoreSpecies") + let moreSpecies = try decoder.decode(Species.self, from: moreJsonData, configuration: configuration) + + let allSpecies = Species() + allSpecies.species = species.species + moreSpecies.species + + // TODO: random names are hard; for now, get code coverage. + let human = try #require(allSpecies.find("Human")) + _ = speciesNames.randomName(speciesTraits: human, gender: .female) + + let elf = try #require(allSpecies.find("Elf")) + _ = speciesNames.randomName(speciesTraits: elf, gender: .male) + + let mountainDwarf = try #require(allSpecies.find("Mountain Dwarf")) + _ = speciesNames.randomName(speciesTraits: mountainDwarf, gender: nil) + + let stout = try #require(allSpecies.find("Stout")) + _ = speciesNames.randomName(speciesTraits: stout, gender: nil) + + let dragonborn = try #require(allSpecies.find("Dragonborn")) + _ = speciesNames.randomName(speciesTraits: dragonborn, gender: nil) + + let tiefling = try #require(allSpecies.find("Tiefling")) + _ = speciesNames.randomName(speciesTraits: tiefling, gender: nil) + + let encoder = JSONEncoder() + _ = try encoder.encode(speciesNames) + } +} diff --git a/Tests/RolePlayingCoreTests/SpeciesTests.swift b/Tests/RolePlayingCoreTests/SpeciesTests.swift new file mode 100644 index 0000000..04b77e0 --- /dev/null +++ b/Tests/RolePlayingCoreTests/SpeciesTests.swift @@ -0,0 +1,52 @@ +// +// SpeciesTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/16/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Testing +import RolePlayingCore +import Foundation + +@Suite("Species Tests") +struct SpeciesTests { + + let bundle = Bundle.module + let decoder = JSONDecoder() + let configuration: Configuration + + init() throws { + configuration = try Configuration("TestConfiguration", from: .module) + } + + @Test("Default initialization creates empty species") + func defaultInit() async throws { + let species = Species() + #expect(species.species.count == 0, "default init") + } + + @Test("Load and parse species from JSON file") + func species() async throws { + let jsonData = try bundle.loadJSON("TestSpecies") + let species = try decoder.decode(Species.self, from: jsonData, configuration: configuration) + + #expect(species.leafSpecies.count == 8, "all species") + #expect(species.count == 11, "all species") + #expect(species[0] != nil, "species by index") + + // Test finding a species by name + #expect(species.find("Human") != nil, "Fighter should be non-nil") + #expect(species.find("Foo") == nil, "Foo should be nil") + } + + @Test("Load uncommon species from JSON file") + func uncommonSpecies() async throws { + let jsonData = try bundle.loadJSON("TestMoreSpecies") + let species = try decoder.decode(Species.self, from: jsonData, configuration: configuration) + + // There should be 5 species plus 2 subspecies + #expect(species.species.count == 5, "all species") + } +} diff --git a/Tests/RolePlayingCoreTests/SpeciesTraitsTests.swift b/Tests/RolePlayingCoreTests/SpeciesTraitsTests.swift new file mode 100644 index 0000000..f7bcc3e --- /dev/null +++ b/Tests/RolePlayingCoreTests/SpeciesTraitsTests.swift @@ -0,0 +1,212 @@ +// +// SpeciesTraitsTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/11/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Testing +import RolePlayingCore +import Foundation + +@Suite("Species Traits Tests") +struct SpeciesTraitsTests { + + let decoder = JSONDecoder() + let configuration: Configuration + + init() throws { + configuration = try Configuration("TestConfiguration", from: .module) + } + + @Test("Decode basic species traits") + func speciesTraits() async throws { + let traits = """ + { + "name": "Human", + "plural": "Humans", + "lifespan": 90, + "speed": 30, + "languages": ["Common"], + "extra languages": 1 + } + """.data(using: .utf8)! + + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits, configuration: configuration) + + #expect(speciesTraits.name == "Human", "name") + #expect(speciesTraits.plural == "Humans", "plural") + #expect(speciesTraits.aliases.count == 0, "aliases") + #expect(speciesTraits.lifespan == 90, "lifespan") + #expect(speciesTraits.speed == 30, "speed") + } + + @Test("Decode minimum required traits") + func minimumTraits() async throws { + let traits = """ + { + "name": "Giant Human", + "plural": "Giant Humans", + "lifespan": 90, + "speed": 30 + } + """.data(using: .utf8)! + + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits, configuration: configuration) + + #expect(speciesTraits.name == "Giant Human", "name") + #expect(speciesTraits.plural == "Giant Humans", "plural") + #expect(speciesTraits.lifespan == 90, "lifespan") + #expect(speciesTraits.speed == 30, "speed") + #expect(speciesTraits.aliases.count == 0, "aliases") + } + + @Test("Decode optional traits like aliases") + func optionalTraits() async throws { + let traits = """ + { + "name": "Small Human", + "plural": "Small Humans", + "lifespan": 90, + "speed": 30, + "aliases": ["Big Human"] + } + """.data(using: .utf8)! + + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits, configuration: configuration) + + #expect(speciesTraits.aliases.count == 1, "aliases count") + } + + @Test("Verify missing required traits cause decode failure", arguments: [ + "{}", + """ + { "name": "Giant Human" } + """, + """ + { + "plural": "Giant Humans" + } + """ + ]) + func missingTraits(json: String) async throws { + let traits = json.data(using: .utf8)! + + #expect(throws: (any Error).self) { + _ = try decoder.decode(SpeciesTraits.self, from: traits, configuration: configuration) + } + } + + @Test("Decode species with subspecies") + func decodingSpeciesTraits() async throws { + let traits = """ + { + "name": "Human", + "plural": "Humans", + "lifespan": 90, + "speed": 30, + "subspecies": [ + { + "name": "Subhuman", + "plural": "Subhumans", + "lifespan": 60, + "speed": 10 + } + ] + } + """.data(using: .utf8)! + + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits, configuration: configuration) + let subspeciesTraits = try #require(speciesTraits.subspecies.first) + + #expect(subspeciesTraits.name == "Subhuman", "name") + #expect(subspeciesTraits.plural == "Subhumans", "plural") + #expect(subspeciesTraits.lifespan == 60, "lifespan") + #expect(subspeciesTraits.speed == 10, "speed") + #expect(subspeciesTraits.aliases.count == 0, "aliases") + } + + @Test("Subspecies inherit parent traits") + func subspeciesOverrides() async throws { + let traits = """ + { + "name": "Human", + "plural": "Humans", + "lifespan": 90, + "speed": 30, + "subspecies": [ + { + "name": "Folk", + "plural": "Folks", + "aliases": ["Plainfolk"], + "darkvision": 20 + } + ] + } + """.data(using: .utf8)! + + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits, configuration: configuration) + let subspeciesTraits = try #require(speciesTraits.subspecies.first) + + #expect(subspeciesTraits.name == "Folk", "name") + #expect(subspeciesTraits.plural == "Folks", "plural") + #expect(subspeciesTraits.lifespan == 90, "lifespan") + #expect(subspeciesTraits.speed == 30, "speed") + #expect(subspeciesTraits.aliases.count == 1, "aliases") + #expect(subspeciesTraits.baseSizes == speciesTraits.baseSizes, "size") + } + + @Test("Encode subspecies traits with blending") + func encodingSubspeciesTraits() async throws { + let speciesTraits = SpeciesTraits(name: "Human", plural: "Humans", aliases: [], creatureType: configuration.species.defaultCreatureType, descriptiveTraits: [:], lifespan: 90, darkVision: 0, speed: 45) + + let encoder = JSONEncoder() + + // Test 1: Subspecies with blended traits + do { + var copyOfSpeciesTraits = speciesTraits + var subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", creatureType: configuration.species.defaultCreatureType, lifespan: 45, darkVision: 0, speed: 30) + subspeciesTraits.blendTraits(from: copyOfSpeciesTraits) + copyOfSpeciesTraits.subspecies.append(subspeciesTraits) + + let encoded = try encoder.encode(copyOfSpeciesTraits, configuration: configuration) + let dictionary = try JSONSerialization.jsonObject(with: encoded, options: []) as! [String: Any] + + // Confirm species traits + #expect(dictionary["name"] as? String == "Human", "encoding name") + #expect(dictionary["plural"] as? String == "Humans", "encoding plural") + #expect(dictionary["lifespan"] as? Int == 90, "encoding lifespan") + #expect(dictionary["darkvision"] as? Int == 0, "encoding darkvision") + #expect(dictionary["speed"] as? Int == 45, "encoding speed") + + // Confirm subspecies traits + let subspecies = try #require(dictionary["subspecies"] as? [[String: Any]]) + let firstSubspecies = try #require(subspecies.first) + #expect(firstSubspecies["name"] as? String == "Subhuman", "encoding name") + #expect(firstSubspecies["plural"] as? String == "Subhumans", "encoding plural") + #expect(firstSubspecies["lifespan"] as? Int == 45, "encoding lifespan") + #expect(firstSubspecies["darkvision"] == nil, "encoding darkvision should be nil") + #expect(firstSubspecies["speed"] as? Int == 30, "encoding speed") + } + + // Test 2: Subspecies with different overrides + do { + var copyOfSpeciesTraits = speciesTraits + let subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", aliases: ["Minions"], creatureType: configuration.species.defaultCreatureType, descriptiveTraits: ["background": "Something"], lifespan: 45, darkVision: 10, speed: 45) + copyOfSpeciesTraits.subspecies.append(subspeciesTraits) + + let encoded = try encoder.encode(copyOfSpeciesTraits, configuration: configuration) + let dictionary = try JSONSerialization.jsonObject(with: encoded, options: []) as! [String: Any] + + // Confirm subspecies traits + let subspecies = try #require(dictionary["subspecies"] as? [[String: Any]]) + let firstSubspecies = try #require(subspecies.first) + #expect(firstSubspecies["name"] as? String == "Subhuman", "encoding name") + #expect(firstSubspecies["plural"] as? String == "Subhumans", "encoding plural") + #expect(firstSubspecies["lifespan"] as? Int == 45, "encoding lifespan") + #expect(firstSubspecies["darkvision"] as? Int == 10, "encoding darkvision") + #expect(firstSubspecies["speed"] == nil, "encoding speed should be nil") + } + } +} diff --git a/Tests/RolePlayingCoreTests/TestBundleClass.swift b/Tests/RolePlayingCoreTests/TestBundleClass.swift new file mode 100644 index 0000000..db119d7 --- /dev/null +++ b/Tests/RolePlayingCoreTests/TestBundleClass.swift @@ -0,0 +1,11 @@ +// +// TestBundleClass.swift +// RolePlayingCore +// +// Created by Brian Arnold on 10/29/25. +// Copyright © 2025 Brian Arnold. All rights reserved. +// + +import Foundation + +class TestBundleClass { } diff --git a/RolePlayingCore/RolePlayingCoreTests/HalfBakedJSONFile.json b/Tests/RolePlayingCoreTests/TestResources/HalfBakedJSONFile.json similarity index 100% rename from RolePlayingCore/RolePlayingCoreTests/HalfBakedJSONFile.json rename to Tests/RolePlayingCoreTests/TestResources/HalfBakedJSONFile.json diff --git a/RolePlayingCore/RolePlayingCoreTests/InvalidClassPlayers.json b/Tests/RolePlayingCoreTests/TestResources/InvalidClassPlayers.json similarity index 92% rename from RolePlayingCore/RolePlayingCoreTests/InvalidClassPlayers.json rename to Tests/RolePlayingCoreTests/TestResources/InvalidClassPlayers.json index 5cc545a..5bff776 100644 --- a/RolePlayingCore/RolePlayingCoreTests/InvalidClassPlayers.json +++ b/Tests/RolePlayingCoreTests/TestResources/InvalidClassPlayers.json @@ -6,7 +6,6 @@ "class": "Software Engineer", "species": "High Elf", "height": "3'9\"", - "weight": 120, "ability scores": {"Dexterity": 13}, "money": 130, "maximum hit points": 10 diff --git a/RolePlayingCore/RolePlayingCoreTests/InvalidConfiguration.json b/Tests/RolePlayingCoreTests/TestResources/InvalidConfiguration.json similarity index 100% rename from RolePlayingCore/RolePlayingCoreTests/InvalidConfiguration.json rename to Tests/RolePlayingCoreTests/TestResources/InvalidConfiguration.json diff --git a/RolePlayingCore/RolePlayingCoreTests/InvalidJSONFile.json b/Tests/RolePlayingCoreTests/TestResources/InvalidJSONFile.json similarity index 100% rename from RolePlayingCore/RolePlayingCoreTests/InvalidJSONFile.json rename to Tests/RolePlayingCoreTests/TestResources/InvalidJSONFile.json diff --git a/RolePlayingCore/RolePlayingCoreTests/InvalidSpeciesPlayers.json b/Tests/RolePlayingCoreTests/TestResources/InvalidSpeciesPlayers.json similarity index 94% rename from RolePlayingCore/RolePlayingCoreTests/InvalidSpeciesPlayers.json rename to Tests/RolePlayingCoreTests/TestResources/InvalidSpeciesPlayers.json index 8303c2a..897cdd3 100644 --- a/RolePlayingCore/RolePlayingCoreTests/InvalidSpeciesPlayers.json +++ b/Tests/RolePlayingCoreTests/TestResources/InvalidSpeciesPlayers.json @@ -7,7 +7,6 @@ "species": "Frog", "alignment": "Lawful Evil", "height": "3'9\"", - "weight": 120, "ability scores": {"Strength": 12}, "money": 130, "maximum hit points": 10, diff --git a/RolePlayingCore/RolePlayingCoreTests/JSONFile.json b/Tests/RolePlayingCoreTests/TestResources/JSONFile.json similarity index 100% rename from RolePlayingCore/RolePlayingCoreTests/JSONFile.json rename to Tests/RolePlayingCoreTests/TestResources/JSONFile.json diff --git a/RolePlayingCore/RolePlayingCoreTests/MissingClassPlayers.json b/Tests/RolePlayingCoreTests/TestResources/MissingClassPlayers.json similarity index 91% rename from RolePlayingCore/RolePlayingCoreTests/MissingClassPlayers.json rename to Tests/RolePlayingCoreTests/TestResources/MissingClassPlayers.json index 7fddeeb..97a0881 100644 --- a/RolePlayingCore/RolePlayingCoreTests/MissingClassPlayers.json +++ b/Tests/RolePlayingCoreTests/TestResources/MissingClassPlayers.json @@ -5,7 +5,6 @@ "gender": "Male", "species": "High Elf", "height": "3'9\"", - "weight": 120, "ability scores": {"Dexterity": 13}, "money": 130, "maximum hit points": 10 diff --git a/RolePlayingCore/RolePlayingCoreTests/MissingSpeciesPlayers.json b/Tests/RolePlayingCoreTests/TestResources/MissingSpeciesPlayers.json similarity index 91% rename from RolePlayingCore/RolePlayingCoreTests/MissingSpeciesPlayers.json rename to Tests/RolePlayingCoreTests/TestResources/MissingSpeciesPlayers.json index 4a975f0..3f77de7 100644 --- a/RolePlayingCore/RolePlayingCoreTests/MissingSpeciesPlayers.json +++ b/Tests/RolePlayingCoreTests/TestResources/MissingSpeciesPlayers.json @@ -5,7 +5,6 @@ "gender": "Male", "class": "Fighter", "height": "3'9\"", - "weight": 120, "ability scores": {"Dexterity": 13}, "money": 130, "maximum hit points": 10 diff --git a/Tests/RolePlayingCoreTests/TestResources/TestBackgrounds.json b/Tests/RolePlayingCoreTests/TestResources/TestBackgrounds.json new file mode 100644 index 0000000..e076e7e --- /dev/null +++ b/Tests/RolePlayingCoreTests/TestResources/TestBackgrounds.json @@ -0,0 +1,37 @@ +{ + "backgrounds": [ + { + "name": "Acolyte", + "ability scores": ["Intelligence", "Wisdom", "Charisma"], + "feat": "Magic Initiate (Cleric)", + "skill proficiencies" : ["Insight", "Religion"], + "tool proficiency": "Calligrapher's Supplies", + "equipment": [["Calligrapher's Supplies", "Book (prayers)", "Holy Symbol", "Parchment (10 sheets)", "Robe", "8 GP"], ["50 GP"]] + }, + { + "name": "Farmer", + "ability scores": ["Strength", "Constitution", "Wisdom"], + "feat": "Tough", + "skill proficiencies" : ["Animal Handling", "Nature"], + "tool proficiency": "Carpenter's Tools", + "equipment": [["Sickle", "Carpenter's Tools", "Healer's Kit", "Iron Pot", "Shovel", "Traveler's Clothes", "30 GP"], ["50 GP"]] + }, + { + "name": "Guide", + "ability scores": ["Dexterity", "Constitution", "Wisdom"], + "feat": "Magic Initiate (Druid)", + "skill proficiencies" : ["Stealth", "Survival"], + "tool proficiency": "Cartographer's Tools", + "equipment": [["Shortbow", "20 Arrows", "Cartographer's Tools", "Bedroll", "Quiver", "Tent", "Traveler's Clothes", "3 GP"], ["50 GP"]] + }, + { + "name": "Sailor", + "ability scores": ["Strength", "Dexterity", "Wisdom"], + "feat": "Tavern Brawler", + "skill proficiencies" : ["Acrobatics", "Perception"], + "tool proficiency": "Navigator's Tools", + "equipment": [["Dagger", "Navigator's Tools", "Rope", "Traveler's Clothes", "20 GP"], ["50 GP"]] + } + ] +} + diff --git a/RolePlayingCore/RolePlayingCoreTests/TestCharacterGenerator.json b/Tests/RolePlayingCoreTests/TestResources/TestCharacterGenerator.json similarity index 75% rename from RolePlayingCore/RolePlayingCoreTests/TestCharacterGenerator.json rename to Tests/RolePlayingCoreTests/TestResources/TestCharacterGenerator.json index e833c7d..9d84902 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestCharacterGenerator.json +++ b/Tests/RolePlayingCoreTests/TestResources/TestCharacterGenerator.json @@ -1,5 +1,7 @@ { "currencies": ["TestCurrencies"], + "skills": ["TestSkills"], + "backgrounds": ["TestBackgrounds"], "classes": ["TestClasses", "TestMoreClasses"], "species": ["TestSpecies", "TestMoreSpecies"], "players": ["TestPlayers"], diff --git a/RolePlayingCore/RolePlayingCoreTests/TestClasses.json b/Tests/RolePlayingCoreTests/TestResources/TestClasses.json similarity index 79% rename from RolePlayingCore/RolePlayingCoreTests/TestClasses.json rename to Tests/RolePlayingCoreTests/TestResources/TestClasses.json index 864c9f2..ff859b5 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestClasses.json +++ b/Tests/RolePlayingCoreTests/TestResources/TestClasses.json @@ -28,8 +28,8 @@ "primary ability": ["Wisdom"], "saving throws": ["Wisdom", "Charisma"], "starting wealth": "5d4x10", - "armor": ["light", "medium", "shield"], - "weapons": ["simple"], + "armor training": ["light", "medium", "shield"], + "weapon proficiencies": ["simple"], }, { "name": "Fighter", @@ -39,8 +39,8 @@ "alternate primary ability": ["Dexterity"], "saving throws": ["Strength", "Constitution"], "starting wealth": "5d4x10", - "armor": ["all"], - "weapons": ["simple", "martial"], + "armor training": ["all"], + "weapon proficiencies": ["simple", "martial"], }, { "name": "Rogue", @@ -50,8 +50,8 @@ "primary ability": ["Dexterity"], "saving throws": ["Dexterity", "Intelligence"], "starting wealth": "4d4x10", - "armor": ["light"], - "weapons": ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], + "armor training": ["light"], + "weapon proficiencies": ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], }, { "name": "Wizard", @@ -60,8 +60,8 @@ "primary ability": ["Intelligence"], "saving throws": ["Intelligence", "Wisdom"], "starting wealth": "4d4x10", - "armor": [], - "weapons": ["dagger", "dart", "sling", "quarterstaff", "light crossbow"], + "armor training": [], + "weapon proficiencies": ["dagger", "dart", "sling", "quarterstaff", "light crossbow"], } ] } diff --git a/Tests/RolePlayingCoreTests/TestResources/TestClassesConfiguration.json b/Tests/RolePlayingCoreTests/TestResources/TestClassesConfiguration.json new file mode 100644 index 0000000..09157d3 --- /dev/null +++ b/Tests/RolePlayingCoreTests/TestResources/TestClassesConfiguration.json @@ -0,0 +1,8 @@ +{ + "currencies": ["TestCurrencies"], + "skills": ["TestSkills"], + "backgrounds": [], + "classes": [], + "species": [], + "players": [] +} diff --git a/RolePlayingCore/RolePlayingCoreTests/TestConfiguration.json b/Tests/RolePlayingCoreTests/TestResources/TestConfiguration.json similarity index 71% rename from RolePlayingCore/RolePlayingCoreTests/TestConfiguration.json rename to Tests/RolePlayingCoreTests/TestResources/TestConfiguration.json index b770154..ffddca6 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestConfiguration.json +++ b/Tests/RolePlayingCoreTests/TestResources/TestConfiguration.json @@ -1,5 +1,7 @@ { "currencies": ["TestCurrencies"], + "skills": ["TestSkills"], + "backgrounds": ["TestBackgrounds"], "classes": ["TestClasses", "TestMoreClasses"], "species": ["TestSpecies", "TestMoreSpecies"], "players": ["TestPlayers"] diff --git a/RolePlayingCore/RolePlayingCoreTests/TestCurrencies.json b/Tests/RolePlayingCoreTests/TestResources/TestCurrencies.json similarity index 100% rename from RolePlayingCore/RolePlayingCoreTests/TestCurrencies.json rename to Tests/RolePlayingCoreTests/TestResources/TestCurrencies.json diff --git a/RolePlayingCore/RolePlayingCoreTests/TestMoreClasses.json b/Tests/RolePlayingCoreTests/TestResources/TestMoreClasses.json similarity index 69% rename from RolePlayingCore/RolePlayingCoreTests/TestMoreClasses.json rename to Tests/RolePlayingCoreTests/TestResources/TestMoreClasses.json index 95f2acd..ba7da1f 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestMoreClasses.json +++ b/Tests/RolePlayingCoreTests/TestResources/TestMoreClasses.json @@ -7,8 +7,8 @@ "primary ability": ["Strength"], "saving throws": ["Strength", "Constitution"], "starting wealth": "2d4x10", - "armor": ["light", "medium", "shield"], - "weapons": ["simple", "martial"], + "armor training": ["light", "medium", "shield"], + "weapon proficiencies": ["simple", "martial"], }, { "name": "Bard", @@ -17,8 +17,8 @@ "primary ability": ["Charisma"], "saving throws": ["Dexterity", "Charisma"], "starting wealth": "5d4x10", - "armor": ["light"], - "weapons": ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], + "armor training": ["light"], + "weapon proficiencies": ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], }, { "name": "Druid", @@ -27,8 +27,8 @@ "primary ability": ["Wisdom"], "saving throws": ["Intelligence", "Wisdom"], "starting wealth": "2d4x10", - "armor": ["light", "medium", "shields", "nonmetal"], - "weapons": ["club", "dagger", "dart", "javelin", "mace", "quarterstaff", "scimitar", "sickle", "sling", "spear"], + "armor training": ["light", "medium", "shields", "nonmetal"], + "weapon proficiencies": ["club", "dagger", "dart", "javelin", "mace", "quarterstaff", "scimitar", "sickle", "sling", "spear"], }, { "name": "Monk", @@ -37,8 +37,8 @@ "primary ability": ["Dexterity", "Wisdom"], "saving throws": ["Strength", "Dexterity"], "starting wealth": "5d4", - "armor": [], - "weapons": ["simple", "shortsword"], + "armor training": [], + "weapon proficiencies": ["simple", "shortsword"], }, { "name": "Paladin", @@ -47,8 +47,8 @@ "primary ability": ["Strength", "Charisma"], "saving throws": ["Wisdom", "Charisma"], "starting wealth": "5d4x10", - "armor": ["all"], - "weapons": ["simple", "martial"], + "armor training": ["all"], + "weapon proficiencies": ["simple", "martial"], }, { "name": "Ranger", @@ -57,8 +57,8 @@ "primary ability": ["Dexterity", "Wisdom"], "saving throws": ["Strength", "Dexterity"], "starting wealth": "5d4x10", - "armor": ["light", "medium", "shield"], - "weapons": ["simple", "martial"], + "armor training": ["light", "medium", "shield"], + "weapon proficiencies": ["simple", "martial"], }, { "name": "Sorcerer", @@ -67,8 +67,8 @@ "primary ability": ["Charisma"], "saving throws": ["Constitution", "Charisma"], "starting wealth": "3d4x10", - "armor": [], - "weapons": ["dagger", "dart", "sling", "quarterstaff", "light crossbow"], + "armor training": [], + "weapon proficiencies": ["dagger", "dart", "sling", "quarterstaff", "light crossbow"], }, { "name": "Warlock", @@ -77,8 +77,8 @@ "primary ability": ["Charisma"], "saving throws": ["Wisdom", "Charisma"], "starting wealth": "4d4x10", - "armor": ["light"], - "weapons": ["simple"], + "armor training": ["light"], + "weapon proficiencies": ["simple"], } ] } diff --git a/Tests/RolePlayingCoreTests/TestResources/TestMoreSpecies.json b/Tests/RolePlayingCoreTests/TestResources/TestMoreSpecies.json new file mode 100644 index 0000000..161afa0 --- /dev/null +++ b/Tests/RolePlayingCoreTests/TestResources/TestMoreSpecies.json @@ -0,0 +1,41 @@ +{ + "species": [ + { + "name": "Dragonborn", + "plural": "Dragonborn", + "minimum age": 15, + "lifespan": 80, + "speed": 30, + "skill proficiencies": ["draconic ancestry", "breath weapon", "damage"], + "languages": ["Common", "Draconic"] + }, + { + "name": "Gnome", + "plural": "Gnomes", + "lifespan": 425, + "speed": 25, + "darkvision": 60, + "skill proficiencies": ["gnome cunning"], + "languages": ["Common", "Gnomish"], + "subspecies": [{ + "name": "Forest Gnome", + "plural": "Forest Gnomes", + "skill proficiencies": ["natural illusionist", "speak with small beasts"] + }, + { + "name": "Rock Gnome", + "plural": "Rock Gnomes", + "skill proficiencies": ["artificers lore", "tinker"] + }] + }, + { + "name": "Tiefling", + "plural": "Tieflings", + "lifespan": 100, + "speed": 30, + "darkvision": 60, + "skill proficiencies": ["hellish resistance", "infernal legacy"], + "languages": ["Common", "Infernal"] + } + ] +} diff --git a/RolePlayingCore/RolePlayingCoreTests/TestNames.json b/Tests/RolePlayingCoreTests/TestResources/TestNames.json similarity index 100% rename from RolePlayingCore/RolePlayingCoreTests/TestNames.json rename to Tests/RolePlayingCoreTests/TestResources/TestNames.json diff --git a/RolePlayingCore/RolePlayingCoreTests/TestPlayers.json b/Tests/RolePlayingCoreTests/TestResources/TestPlayers.json similarity index 69% rename from RolePlayingCore/RolePlayingCoreTests/TestPlayers.json rename to Tests/RolePlayingCoreTests/TestResources/TestPlayers.json index e84d205..0d364b3 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestPlayers.json +++ b/Tests/RolePlayingCoreTests/TestResources/TestPlayers.json @@ -3,23 +3,27 @@ { "name": "Frodo", "gender": "Male", + "background": "Sailor", "class": "Cleric", "species": "High Elf", "height": "3'9\"", - "weight": 120, "ability scores": {"Dexterity": 13}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skill proficiencies": ["Athletics"], "money": 130, "maximum hit points": 10 }, { "name": "Bilbo", "gender": "Female", + "background": "Sailor", "class": "Fighter", "species": "Human", "alignment": "Lawful Evil", "height": "3'9\"", - "weight": 120, "ability scores": {"Strength": 12}, + "background ability scores": ["Strength", "Dexterity", "Dexterity"], + "skill proficiencies": ["Athletics"], "money": 130, "maximum hit points": 10, "experience points": 2300, diff --git a/Tests/RolePlayingCoreTests/TestResources/TestSkills.json b/Tests/RolePlayingCoreTests/TestResources/TestSkills.json new file mode 100644 index 0000000..e4e3714 --- /dev/null +++ b/Tests/RolePlayingCoreTests/TestResources/TestSkills.json @@ -0,0 +1,77 @@ +{ + "skills": [ + { + "name": "Acrobatics", + "ability": "Dexterity" + }, + { + "name": "Animal Handling", + "ability": "Wisdom" + }, + { + "name": "Arcana", + "ability": "Intelligence" + }, + { + "name": "Athletics", + "ability": "Strength" + }, + { + "name": "Deception", + "ability": "Charisma" + }, + { + "name": "History", + "ability": "Intelligence" + }, + { + "name": "Insight", + "ability": "Wisdom" + }, + { + "name": "Intimidation", + "ability": "Charisma" + }, + { + "name": "Investigation", + "ability": "Intelligence" + }, + { + "name": "Medicine", + "ability": "Wisdom" + }, + { + "name": "Nature", + "ability": "Intelligence" + }, + { + "name": "Perception", + "ability": "Wisdom" + }, + { + "name": "Performance", + "ability": "Charisma" + }, + { + "name": "Persuasion", + "ability": "Charisma" + }, + { + "name": "Religion", + "ability": "Intelligence" + }, + { + "name": "Sleight of Hand", + "ability": "Dexterity" + }, + { + "name": "Stealth", + "ability": "Dexterity" + }, + { + "name": "Survival", + "ability": "Wisdom" + } + ] +} + diff --git a/Tests/RolePlayingCoreTests/TestResources/TestSpecies.json b/Tests/RolePlayingCoreTests/TestResources/TestSpecies.json new file mode 100644 index 0000000..8aff22b --- /dev/null +++ b/Tests/RolePlayingCoreTests/TestResources/TestSpecies.json @@ -0,0 +1,83 @@ +{ + "creature types": [ + { "name": "Aberration" }, + { "name": "Beast" }, + { "name": "Celestial" }, + { "name": "Construct" }, + { "name": "Dragon" }, + { "name": "Elemental" }, + { "name": "Fey" }, + { "name": "Fiend" }, + { "name": "Giant" }, + { "name": "Humanoid", "is default": true }, + { "name": "Monstrosity" }, + { "name": "Ooze" }, + { "name": "Plant" }, + { "name": "Undead" } + ], + "species": [ + { + "name": "Dwarf", + "plural": "Dwarves", + "lifespan": 350, + "base sizes": ["4-5 ft"], + "speed": 30, + "darkvision": 120, + "subspecies": [{ + "name": "Hill Dwarf", + "plural": "Hill Dwarves", + }, + { + "name": "Mountain Dwarf", + "plural": "Mountain Dwarves", + }] + }, + { + "name": "Elf", + "plural": "Elves", + "lifespan": 750, + "base sizes": ["5-6 ft"], + "speed": 30, + "darkvision": 60, + "subspecies": [{ + "name": "High Elf", + "plural": "High Elves" + }, + { + "name": "Wood Elf", + "plural": "Wood Elves" + }, + { + "name": "Drow", + "plural": "Drow", + "aliases": ["Dark Elf"], + "darkvision": 120 + }] + }, + { + "name": "Halfling", + "plural": "Halflings", + "lifespan": 150, + "base sizes": ["2-3 ft"], + "speed": 30, + "subspecies": [{ + "name": "Lightfoot", + "plural": "Lightfoots", + "aliases": ["Tallfellow"] + }, + { + "name": "Stout", + "plural": "Stouts", + "aliases": ["Strongheart"] + }] + + }, + { + "name": "Human", + "plural": "Humans", + "lifespan": 80, + "base sizes": ["4-7 ft", "2-4 ft"], + "speed": 30 + } + ] +} diff --git a/RolePlayingCore/RolePlayingCoreTests/TestSpeciesNames.json b/Tests/RolePlayingCoreTests/TestResources/TestSpeciesNames.json similarity index 97% rename from RolePlayingCore/RolePlayingCoreTests/TestSpeciesNames.json rename to Tests/RolePlayingCoreTests/TestResources/TestSpeciesNames.json index 201f5b0..45e4d6b 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestSpeciesNames.json +++ b/Tests/RolePlayingCoreTests/TestResources/TestSpeciesNames.json @@ -40,12 +40,6 @@ "family": ["Beren", "Daergel", "Folkor", "Garrick", "Nackle", "Murning", "Ningel", "Raulnor", "Scheppen", "Timbers", "Turen"], "nickname": ["Aleslosh", "Ashhearth", "Badger", "Cloak", "Doublelock", "Filchbatter", "Fnipper", "Ku", "Nim", "Oneshoe", "Pock", "Sparklegem", "Stumbleduck"] }, - "Half-Elf": { - "aliases": ["Elf", "Human"] - }, - "Half-Orc": { - "aliases": ["Orc", "Human"] - }, "Orc": { "family type": "", "Male": ["Dench", "Feng", "Gell", "Henk", "Holg", "Imsh", "Keth", "Krusk", "Mhurren", "Ront", "Shump", "Thokk"], diff --git a/Tests/RolePlayingCoreTests/WeightTests.swift b/Tests/RolePlayingCoreTests/WeightTests.swift new file mode 100644 index 0000000..88fa411 --- /dev/null +++ b/Tests/RolePlayingCoreTests/WeightTests.swift @@ -0,0 +1,153 @@ +// +// UnitWeightTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/5/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Testing +import RolePlayingCore +import Foundation + +@Suite("Weight Parsing and Serialization Tests") +struct UnitWeightTests { + + @Test("Parse various weight formats") + func weights() async throws { + do { + let howHeavy = "70".parseWeight + #expect(howHeavy != nil, "weight should be non-nil") + #expect(howHeavy?.value == 70, "weight should be 70") + } + + do { + let howHeavy = "3.0".parseWeight + #expect(howHeavy != nil, "weight should be non-nil") + #expect(howHeavy?.value == 3.0, "weight should be 3.0") + } + + do { + let howHeavy = "45lb".parseWeight + #expect(howHeavy != nil, "weight should be non-nil") + #expect(howHeavy?.value == 45, "weight should be 45") + } + + do { + let howHeavy = "174 kg".parseWeight + #expect(howHeavy != nil, "weight should be non-nil") + #expect(howHeavy?.value == 174, "weight should be 174") + } + } + + @Test("Parse invalid weight strings") + func invalidWeights() async throws { + let howHeavy = "99 hello".parseWeight + #expect(howHeavy == nil, "weight should be nil") + } + + @Test("Encode weight to JSON") + func encodingWeight() async throws { + struct WeightContainer: Encodable { + let weight: Weight + + enum CodingKeys: String, CodingKey { + case weight + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("\(weight)", forKey: .weight) + } + } + let weightContainer = WeightContainer(weight: Weight(value: 2.9, unit: .kilograms)) + + // Do a round-trip through serialization, then deserialization to confirm that it became a string + let encoder = JSONEncoder() + let encoded = try encoder.encode(weightContainer) + let deserialized = try JSONSerialization.jsonObject(with: encoded, options: .allowFragments) + print("deserialized = \n\(deserialized)") + let container = deserialized as? [String: Any] + let weight = container?["weight"] as? String + #expect(weight == "2.9 kg", "Encoded weight did not become a string") + } + + @Test("Decode weight from JSON") + func decodingWeight() async throws { + struct WeightContainer: Decodable { + let weight: Weight + } + + do { + let traits = """ + { + "weight": "147 lb" + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let decoded = try decoder.decode(WeightContainer.self, from: traits) + + #expect(decoded.weight.value == 147, "Decoded weight should be 147 lb") + } + + do { + let traits = """ + { + "weight": 17 + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let decoded = try decoder.decode(WeightContainer.self, from: traits) + + #expect(decoded.weight.value == 17, "Decoded weight should be 17 lb") + } + + do { + let traits = """ + { + "weight": "abcdefg" + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + #expect(throws: (any Error).self) { + _ = try decoder.decode(WeightContainer.self, from: traits) + } + } + } + + @Test("Decode optional weight from JSON") + func decodingWeightIfPresent() async throws { + struct WeightContainer: Decodable { + let weight: Weight? // The ? will trigger decodeIfPresent in the decoder + } + + do { + let traits = """ + { + "weight": "220 lb" + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let decoded = try decoder.decode(WeightContainer.self, from: traits) + + #expect(decoded.weight?.value == 220.0, "Decoded weight should be 220 lb") + } + + do { + let traits = """ + { + "weight": 6.5 + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let decoded = try decoder.decode(WeightContainer.self, from: traits) + + #expect(decoded.weight?.value == 6.5, "Decoded weight should be 6.5") + } + } +} diff --git a/codecov.yml b/codecov.yml index 7dc263a..87fb0be 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,3 +1,3 @@ coverage: ignore: - - RolePlayingCore/RolePlayingCoreTests/* + - Tests/RolePlayingCoreTests/*