├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── release-drafter.yml └── workflows │ ├── android-test.yml │ └── release-drafter.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── detekt-config.yml ├── google-services.json ├── objectbox-models │ ├── default.json │ └── default.json.bak ├── proguard-rules.pro ├── src │ ├── androidTest │ │ └── java │ │ │ └── me │ │ │ └── kerooker │ │ │ └── rpgnpcgenerator │ │ │ └── view │ │ │ ├── my │ │ │ └── npc │ │ │ │ └── individual │ │ │ │ └── IndividualNpcFragmentTest.kt │ │ │ └── random │ │ │ └── npc │ │ │ └── RandomNpcFragmentTest.kt │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── ic_launcher-web.png │ │ ├── java │ │ │ └── me │ │ │ │ └── kerooker │ │ │ │ └── rpgnpcgenerator │ │ │ │ ├── RpgNpcGeneratorApplication.kt │ │ │ │ ├── legacy │ │ │ │ └── repository │ │ │ │ │ ├── LegacyNpcImporter.kt │ │ │ │ │ └── LegacyNpcRepository.kt │ │ │ │ ├── repository │ │ │ │ └── model │ │ │ │ │ ├── persistence │ │ │ │ │ ├── RepositoryModule.kt │ │ │ │ │ ├── admob │ │ │ │ │ │ └── AdmobRepository.kt │ │ │ │ │ └── npc │ │ │ │ │ │ ├── NpcEntity.kt │ │ │ │ │ │ └── NpcRepository.kt │ │ │ │ │ └── random │ │ │ │ │ └── npc │ │ │ │ │ ├── EnumGenerators.kt │ │ │ │ │ ├── FileGenerators.kt │ │ │ │ │ ├── GeneratedNpc.kt │ │ │ │ │ ├── NpcGenerators.kt │ │ │ │ │ ├── RandomNpcKoinModule.kt │ │ │ │ │ └── TemporaryRandomNpcRepository.kt │ │ │ │ ├── view │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── my │ │ │ │ │ └── npc │ │ │ │ │ │ ├── MyNpcsFragment.kt │ │ │ │ │ │ └── individual │ │ │ │ │ │ └── IndividualNpcFragment.kt │ │ │ │ ├── random │ │ │ │ │ └── npc │ │ │ │ │ │ ├── RandomNpcElementListview.kt │ │ │ │ │ │ ├── RandomNpcElementView.kt │ │ │ │ │ │ ├── RandomNpcFragment.kt │ │ │ │ │ │ └── RandomNpcListView.kt │ │ │ │ ├── settings │ │ │ │ │ └── SettingsFragment.kt │ │ │ │ ├── text │ │ │ │ │ └── ClearFocusEditText.kt │ │ │ │ └── util │ │ │ │ │ └── AnimateRotation.kt │ │ │ │ └── viewmodel │ │ │ │ ├── ViewModelsModule.kt │ │ │ │ ├── admob │ │ │ │ ├── AdmobViewModel.kt │ │ │ │ └── MyBuildConfig.kt │ │ │ │ ├── my │ │ │ │ └── npc │ │ │ │ │ ├── MyNpcsViewModel.kt │ │ │ │ │ └── individual │ │ │ │ │ └── IndividualNpcViewModel.kt │ │ │ │ ├── random │ │ │ │ └── npc │ │ │ │ │ └── RandomNpcViewModel.kt │ │ │ │ └── settings │ │ │ │ └── SettingsViewModel.kt │ │ └── res │ │ │ ├── anim │ │ │ └── rotate_animation.xml │ │ │ ├── color │ │ │ └── mynpcs_individual_edit_text_color.xml │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ ├── dice_192_192.png │ │ │ ├── github_32px.png │ │ │ ├── ic_add_circle_primary_color_24dp.xml │ │ │ ├── ic_bug_report_black_24dp.xml │ │ │ ├── ic_cancel_24dp.xml │ │ │ ├── ic_edit_24dp.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_library_book.xml │ │ │ ├── ic_remove_circle_red_24dp.xml │ │ │ ├── ic_save_24dp.xml │ │ │ ├── ic_settings_24dp.xml │ │ │ ├── ic_star_black_24dp.xml │ │ │ ├── ic_twenty_sided_dice.xml │ │ │ ├── portrait_placeholder.xml │ │ │ └── simple_black_border.xml │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ ├── mynpcs_fragment.xml │ │ │ ├── mynpcs_individual_fragment.xml │ │ │ ├── mynpcs_individual_list.xml │ │ │ ├── mynpcs_individual_list_element.xml │ │ │ ├── randomnpc_element_list_view.xml │ │ │ ├── randomnpc_element_list_view_item.xml │ │ │ ├── randomnpc_element_view.xml │ │ │ └── randomnpc_fragment.xml │ │ │ ├── menu │ │ │ ├── main_bottom_nav.xml │ │ │ ├── mynpcs_individual_editmode_item_menu.xml │ │ │ ├── mynpcs_individual_viewmode_item_menu.xml │ │ │ ├── mynpcs_list_item_menu.xml │ │ │ └── randomnpc_fragment_menu.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── navigation │ │ │ └── nav_graph.xml │ │ │ ├── raw-pt │ │ │ ├── npc_child_professions.txt │ │ │ ├── npc_motivations.txt │ │ │ ├── npc_nicknames.txt │ │ │ ├── npc_personality_trait.txt │ │ │ └── npc_professions.txt │ │ │ ├── raw │ │ │ ├── npc_child_professions.txt │ │ │ ├── npc_motivations.txt │ │ │ ├── npc_names.txt │ │ │ ├── npc_nicknames.txt │ │ │ ├── npc_personality_trait.txt │ │ │ └── npc_professions.txt │ │ │ ├── values-pt │ │ │ ├── individual_npc.xml │ │ │ ├── mainactivity.xml │ │ │ ├── mynpcs.xml │ │ │ ├── npc_enums.xml │ │ │ ├── random_npc.xml │ │ │ ├── settings.xml │ │ │ └── strings.xml │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── individual_npc.xml │ │ │ ├── mainactivity.xml │ │ │ ├── mynpcs.xml │ │ │ ├── npc_enums.xml │ │ │ ├── random_npc.xml │ │ │ ├── settings.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── preferences.xml │ └── test │ │ └── java │ │ ├── io │ │ └── kotest │ │ │ ├── android │ │ │ ├── InstantLivedataListener.kt │ │ │ └── SharedPreferences.kt │ │ │ └── provided │ │ │ └── ProjectConfig.kt │ │ └── me │ │ └── kerooker │ │ └── rpgnpcgenerator │ │ ├── legacy │ │ └── repository │ │ │ └── LegacyNpcRepositoryTest.kt │ │ ├── repository │ │ └── model │ │ │ └── random │ │ │ └── npc │ │ │ ├── EnumGeneratorsTests.kt │ │ │ ├── FileGeneratorsTest.kt │ │ │ └── NpcGeneratorsTests.kt │ │ └── viewmodel │ │ ├── admob │ │ └── AdmobViewmodelTest.kt │ │ └── my │ │ └── npc │ │ └── MyNpcsViewModelTest.kt └── version.properties ├── build.gradle ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ └── Versions.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── renovate.json └── settings.gradle /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$NEXT_PATCH_VERSION 🌈' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | categories: 4 | - title: '🚀 Features' 5 | labels: 6 | - 'feature' 7 | - 'enhancement' 8 | - title: '🐛 Bug Fixes' 9 | labels: 10 | - 'fix' 11 | - 'bugfix' 12 | - 'bug' 13 | - title: '🧰 Maintenance' 14 | label: 'chore' 15 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 16 | template: | 17 | ## Changes 18 | 19 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/android-test.yml: -------------------------------------------------------------------------------- 1 | name: "Android Test" 2 | on: 3 | push: 4 | 5 | jobs: 6 | test: 7 | runs-on: macOS-latest 8 | steps: 9 | - name: checkout 10 | uses: actions/checkout@v2 11 | 12 | - name: run tests 13 | uses: reactivecircus/android-emulator-runner@v2 14 | with: 15 | api-level: 28 16 | emulator-options: "" 17 | script: "./gradlew connectedCheck test detekt" 18 | 19 | - name: upload failure 20 | uses: actions/upload-artifact@v1 21 | if: failure() 22 | with: 23 | name: build-artifact 24 | path: app/build -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v5 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /captures 12 | .externalNativeBuild 13 | .cxx 14 | local/ 15 | .idea/ 16 | build/ 17 | buildSrc/build/ 18 | app/build 19 | local/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RPG NPC Generator 2 | RPG NPC Generator is an Android application to help Dungeon Masters create non-playable-character for their campaigns. 3 | 4 | It aims to be easy yet powerful to help DMs with their creativity and enhance their tabletop experience 5 | 6 | Get it on Google Play 7 | 8 | ## The software 9 | 10 | The app is written 100% in Kotlin and is tested with KotlinTest. We aim to use Android's best practices, but that's not necessarily true for all the components. 11 | 12 | ## Contributing 13 | 14 | Feel free to create an issue or open a PR for whatever you feel like. We're open to all sorts of discussions and improvements! 15 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.so -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "913246948127", 4 | "firebase_url": "https://rpg-npc-generator-53a2d.firebaseio.com", 5 | "project_id": "rpg-npc-generator-53a2d", 6 | "storage_bucket": "rpg-npc-generator-53a2d.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:913246948127:android:6c82fa5d0df401978d3e2a", 12 | "android_client_info": { 13 | "package_name": "me.kerooker.rpgcharactergenerator" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "913246948127-rtggc8lj0c6ih28n3c0609pvvvg03i0f.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyBDjoQQqNnzfFodTQe1aNlYm8ucp2jsT5Y" 25 | } 26 | ], 27 | "services": { 28 | "appinvite_service": { 29 | "other_platform_oauth_client": [ 30 | { 31 | "client_id": "913246948127-rtggc8lj0c6ih28n3c0609pvvvg03i0f.apps.googleusercontent.com", 32 | "client_type": 3 33 | } 34 | ] 35 | } 36 | }, 37 | "admob_app_id": "ca-app-pub-3225017918525788~7747841753" 38 | }, 39 | { 40 | "client_info": { 41 | "mobilesdk_app_id": "1:913246948127:android:d54780527c872b278d3e2a", 42 | "android_client_info": { 43 | "package_name": "me.kerooker.rpgcharactergenerator.debug" 44 | } 45 | }, 46 | "oauth_client": [ 47 | { 48 | "client_id": "913246948127-rtggc8lj0c6ih28n3c0609pvvvg03i0f.apps.googleusercontent.com", 49 | "client_type": 3 50 | } 51 | ], 52 | "api_key": [ 53 | { 54 | "current_key": "AIzaSyBDjoQQqNnzfFodTQe1aNlYm8ucp2jsT5Y" 55 | } 56 | ], 57 | "services": { 58 | "appinvite_service": { 59 | "other_platform_oauth_client": [ 60 | { 61 | "client_id": "913246948127-rtggc8lj0c6ih28n3c0609pvvvg03i0f.apps.googleusercontent.com", 62 | "client_type": 3 63 | } 64 | ] 65 | } 66 | } 67 | }, 68 | { 69 | "client_info": { 70 | "mobilesdk_app_id": "1:913246948127:android:81f8e60bc85ec34d8d3e2a", 71 | "android_client_info": { 72 | "package_name": "me.kerooker.rpgcharactergeneratorpro" 73 | } 74 | }, 75 | "oauth_client": [ 76 | { 77 | "client_id": "913246948127-rtggc8lj0c6ih28n3c0609pvvvg03i0f.apps.googleusercontent.com", 78 | "client_type": 3 79 | } 80 | ], 81 | "api_key": [ 82 | { 83 | "current_key": "AIzaSyBDjoQQqNnzfFodTQe1aNlYm8ucp2jsT5Y" 84 | } 85 | ], 86 | "services": { 87 | "appinvite_service": { 88 | "other_platform_oauth_client": [ 89 | { 90 | "client_id": "913246948127-rtggc8lj0c6ih28n3c0609pvvvg03i0f.apps.googleusercontent.com", 91 | "client_type": 3 92 | } 93 | ] 94 | } 95 | } 96 | }, 97 | { 98 | "client_info": { 99 | "mobilesdk_app_id": "1:913246948127:android:9d46242e8a8644718d3e2a", 100 | "android_client_info": { 101 | "package_name": "me.kerooker.rpgcharactergeneratorpro.debug" 102 | } 103 | }, 104 | "oauth_client": [ 105 | { 106 | "client_id": "913246948127-rtggc8lj0c6ih28n3c0609pvvvg03i0f.apps.googleusercontent.com", 107 | "client_type": 3 108 | } 109 | ], 110 | "api_key": [ 111 | { 112 | "current_key": "AIzaSyBDjoQQqNnzfFodTQe1aNlYm8ucp2jsT5Y" 113 | } 114 | ], 115 | "services": { 116 | "appinvite_service": { 117 | "other_platform_oauth_client": [ 118 | { 119 | "client_id": "913246948127-rtggc8lj0c6ih28n3c0609pvvvg03i0f.apps.googleusercontent.com", 120 | "client_type": 3 121 | } 122 | ] 123 | } 124 | } 125 | } 126 | ], 127 | "configuration_version": "1" 128 | } -------------------------------------------------------------------------------- /app/objectbox-models/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", 3 | "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", 4 | "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", 5 | "entities": [ 6 | { 7 | "id": "1:965012069954660192", 8 | "lastPropertyId": "16:5464541095149212888", 9 | "name": "NpcEntity", 10 | "properties": [ 11 | { 12 | "id": "1:5505077283636440623", 13 | "name": "id", 14 | "type": 6, 15 | "flags": 1 16 | }, 17 | { 18 | "id": "2:8142384949617207154", 19 | "name": "age", 20 | "type": 9 21 | }, 22 | { 23 | "id": "5:4753897017570320626", 24 | "name": "nickname", 25 | "type": 9 26 | }, 27 | { 28 | "id": "6:777274660507681508", 29 | "name": "gender", 30 | "type": 9 31 | }, 32 | { 33 | "id": "7:2894282806420349046", 34 | "name": "sexuality", 35 | "type": 9 36 | }, 37 | { 38 | "id": "8:4107047496141214768", 39 | "name": "race", 40 | "type": 9 41 | }, 42 | { 43 | "id": "9:6625730653070537440", 44 | "name": "profession", 45 | "type": 9 46 | }, 47 | { 48 | "id": "10:636510404345708822", 49 | "name": "motivation", 50 | "type": 9 51 | }, 52 | { 53 | "id": "11:8995285898410800121", 54 | "name": "alignment", 55 | "type": 9 56 | }, 57 | { 58 | "id": "12:3792645898268560037", 59 | "name": "personalityTraits", 60 | "type": 9 61 | }, 62 | { 63 | "id": "13:6023405270920972356", 64 | "name": "languages", 65 | "type": 9 66 | }, 67 | { 68 | "id": "14:4260823185367431858", 69 | "name": "fullName", 70 | "type": 9 71 | }, 72 | { 73 | "id": "15:3627157795465900228", 74 | "name": "imagePath", 75 | "type": 9 76 | }, 77 | { 78 | "id": "16:5464541095149212888", 79 | "name": "notes", 80 | "type": 9 81 | } 82 | ], 83 | "relations": [] 84 | } 85 | ], 86 | "lastEntityId": "1:965012069954660192", 87 | "lastIndexId": "0:0", 88 | "lastRelationId": "0:0", 89 | "lastSequenceId": "0:0", 90 | "modelVersion": 5, 91 | "modelVersionParserMinimum": 5, 92 | "retiredEntityUids": [], 93 | "retiredIndexUids": [], 94 | "retiredPropertyUids": [ 95 | 5589089258033801881, 96 | 2272150580062402924 97 | ], 98 | "retiredRelationUids": [], 99 | "version": 1 100 | } -------------------------------------------------------------------------------- /app/objectbox-models/default.json.bak: -------------------------------------------------------------------------------- 1 | { 2 | "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", 3 | "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", 4 | "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", 5 | "entities": [ 6 | { 7 | "id": "1:965012069954660192", 8 | "lastPropertyId": "15:3627157795465900228", 9 | "name": "NpcEntity", 10 | "properties": [ 11 | { 12 | "id": "1:5505077283636440623", 13 | "name": "id", 14 | "type": 6, 15 | "flags": 1 16 | }, 17 | { 18 | "id": "2:8142384949617207154", 19 | "name": "age", 20 | "type": 9 21 | }, 22 | { 23 | "id": "5:4753897017570320626", 24 | "name": "nickname", 25 | "type": 9 26 | }, 27 | { 28 | "id": "6:777274660507681508", 29 | "name": "gender", 30 | "type": 9 31 | }, 32 | { 33 | "id": "7:2894282806420349046", 34 | "name": "sexuality", 35 | "type": 9 36 | }, 37 | { 38 | "id": "8:4107047496141214768", 39 | "name": "race", 40 | "type": 9 41 | }, 42 | { 43 | "id": "9:6625730653070537440", 44 | "name": "profession", 45 | "type": 9 46 | }, 47 | { 48 | "id": "10:636510404345708822", 49 | "name": "motivation", 50 | "type": 9 51 | }, 52 | { 53 | "id": "11:8995285898410800121", 54 | "name": "alignment", 55 | "type": 9 56 | }, 57 | { 58 | "id": "12:3792645898268560037", 59 | "name": "personalityTraits", 60 | "type": 9 61 | }, 62 | { 63 | "id": "13:6023405270920972356", 64 | "name": "languages", 65 | "type": 9 66 | }, 67 | { 68 | "id": "14:4260823185367431858", 69 | "name": "fullName", 70 | "type": 9 71 | }, 72 | { 73 | "id": "15:3627157795465900228", 74 | "name": "imagePath", 75 | "type": 9 76 | } 77 | ], 78 | "relations": [] 79 | } 80 | ], 81 | "lastEntityId": "1:965012069954660192", 82 | "lastIndexId": "0:0", 83 | "lastRelationId": "0:0", 84 | "lastSequenceId": "0:0", 85 | "modelVersion": 5, 86 | "modelVersionParserMinimum": 5, 87 | "retiredEntityUids": [], 88 | "retiredIndexUids": [], 89 | "retiredPropertyUids": [ 90 | 5589089258033801881, 91 | 2272150580062402924 92 | ], 93 | "retiredRelationUids": [], 94 | "version": 1 95 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keepattributes *Annotation*, InnerClasses 24 | -dontnote kotlinx.serialization.SerializationKt 25 | -keep,includedescriptorclasses class me.kerooker.rpgnpcgenerator.**$$serializer { *; } # <-- change package name to your app's 26 | -keepclassmembers class me.kerooker.rpgnpcgenerator.** { # <-- change package name to your app's 27 | *** Companion; 28 | } 29 | -keepclasseswithmembers class me.kerooker.rpgnpcgenerator.** { # <-- change package name to your app's 30 | kotlinx.serialization.KSerializer serializer(...); 31 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/me/kerooker/rpgnpcgenerator/view/my/npc/individual/IndividualNpcFragmentTest.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.view.my.npc.individual 2 | 3 | import android.view.MenuItem 4 | import androidx.fragment.app.testing.launchFragmentInContainer 5 | import androidx.test.espresso.Espresso.onView 6 | import androidx.test.espresso.action.ViewActions.replaceText 7 | import androidx.test.espresso.assertion.ViewAssertions.matches 8 | import androidx.test.espresso.matcher.ViewMatchers.withText 9 | import io.kotlintest.IsolationMode 10 | import io.kotlintest.TestCase 11 | import io.kotlintest.TestResult 12 | import io.kotlintest.specs.ShouldSpec 13 | import io.mockk.every 14 | import io.mockk.mockk 15 | import kotlinx.coroutines.delay 16 | import me.kerooker.rpgnpcgenerator.R 17 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcEntity 18 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcRepository 19 | import org.koin.core.KoinComponent 20 | import org.koin.core.inject 21 | 22 | class IndividualNpcFragmentTest : ShouldSpec(), KoinComponent { 23 | 24 | private val npcRepository by inject() 25 | 26 | private var npcId: Long = -1 27 | 28 | init { 29 | should("Reset NPC fields when Edit Mode is cancelled") { 30 | val fragment = 31 | launchFragmentInContainer(IndividualNpcFragmentArgs(npcId).toBundle(), R.style.MyAppTheme) 32 | 33 | val item = mockk() 34 | every { item.itemId }.returnsMany(R.id.individual_npc_edit, R.id.individual_npc_cancel) 35 | 36 | 37 | fragment.onFragment { it.onOptionsItemSelected(item) } 38 | onView(withText(dummyNpc.fullName)).perform(replaceText("New NPC Name")) 39 | 40 | fragment.onFragment { it.onOptionsItemSelected(item) } 41 | 42 | delay(100) 43 | onView(withText(dummyNpc.fullName)).check(matches(withText(dummyNpc.fullName))) 44 | } 45 | 46 | should("Not change fields when edit mode is saved") { 47 | val fragment = 48 | launchFragmentInContainer(IndividualNpcFragmentArgs(npcId).toBundle(), R.style.MyAppTheme) 49 | 50 | val item = mockk() 51 | every { item.itemId }.returnsMany(R.id.individual_npc_edit, R.id.individual_npc_save) 52 | 53 | 54 | fragment.onFragment { it.onOptionsItemSelected(item) } 55 | onView(withText(dummyNpc.fullName)).perform(replaceText("New NPC Name")) 56 | 57 | fragment.onFragment { it.onOptionsItemSelected(item) } 58 | 59 | delay(100) 60 | onView(withText("New NPC Name")).check(matches(withText("New NPC Name"))) 61 | } 62 | } 63 | 64 | override fun beforeTest(testCase: TestCase) { 65 | npcRepository.all().value?.forEach { 66 | npcRepository.delete(it) 67 | } 68 | npcId = npcRepository.put(dummyNpc) 69 | } 70 | 71 | override fun afterTest(testCase: TestCase, result: TestResult) { 72 | npcRepository.all().value?.forEach { 73 | npcRepository.delete(it) 74 | } 75 | } 76 | 77 | override fun isolationMode() = IsolationMode.InstancePerTest 78 | } 79 | 80 | private val dummyNpc = NpcEntity( 81 | "FullName", 82 | "Nickname", 83 | "Gender", 84 | "Sexuality", 85 | "Race", 86 | "Age", 87 | "Profession", 88 | "Motiation", 89 | "Alignment", 90 | emptyList(), 91 | emptyList(), 92 | imagePath = null 93 | ) -------------------------------------------------------------------------------- /app/src/androidTest/java/me/kerooker/rpgnpcgenerator/view/random/npc/RandomNpcFragmentTest.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.view.random.npc 2 | 3 | import androidx.test.core.app.launchActivity 4 | import io.kotlintest.IsolationMode 5 | import io.kotlintest.shouldBe 6 | import io.kotlintest.shouldNotBe 7 | import io.kotlintest.specs.FunSpec 8 | import kotlinx.android.synthetic.main.randomnpc_fragment.random_npc_fullname 9 | import me.kerooker.rpgnpcgenerator.view.MainActivity 10 | 11 | class RandomNpcFragmentTest : FunSpec() { 12 | 13 | 14 | init { 15 | test("Should not change currently generated values when fragment is created again") { 16 | var previousText = "" 17 | var afterText = "" 18 | val scenario = launchActivity() 19 | 20 | scenario.onActivity { 21 | previousText = it.random_npc_fullname.text 22 | } 23 | 24 | scenario.recreate() 25 | 26 | scenario.onActivity { 27 | afterText = it.random_npc_fullname.text 28 | } 29 | 30 | previousText shouldBe afterText 31 | previousText shouldNotBe "" 32 | } 33 | } 34 | 35 | override fun isolationMode() = IsolationMode.InstancePerTest 36 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 30 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/RpgNpcGeneratorApplication.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator 2 | 3 | import android.app.Application 4 | import android.provider.Settings 5 | import com.google.android.gms.ads.MobileAds 6 | import me.kerooker.rpgnpcgenerator.legacy.repository.LegacyNpcImporter 7 | import me.kerooker.rpgnpcgenerator.legacy.repository.LegacyNpcRepository 8 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.persistenceModule 9 | import me.kerooker.rpgnpcgenerator.repository.model.random.npc.npcGeneratorsModule 10 | import me.kerooker.rpgnpcgenerator.repository.model.random.npc.randomNpcModule 11 | import me.kerooker.rpgnpcgenerator.viewmodel.viewModelsModule 12 | import org.koin.android.ext.android.inject 13 | import org.koin.android.ext.koin.androidContext 14 | import org.koin.core.context.startKoin 15 | import org.koin.dsl.module 16 | 17 | var isFirebaseDevice = false 18 | private set 19 | 20 | class RpgNpcGeneratorApplication : Application() { 21 | 22 | private val legacyNpcImporter by inject() 23 | 24 | override fun onCreate() { 25 | super.onCreate() 26 | 27 | startKoinModules() 28 | legacyNpcImporter.importAll() 29 | initializeAds() 30 | checkFirebaseDevice() 31 | } 32 | 33 | private fun startKoinModules() { 34 | startKoin { 35 | androidContext(this@RpgNpcGeneratorApplication) 36 | modules(listOf(mainModule, 37 | randomNpcModule, persistenceModule, viewModelsModule, npcGeneratorsModule)) 38 | } 39 | } 40 | 41 | private fun initializeAds() { 42 | MobileAds.initialize(this) 43 | } 44 | 45 | private fun checkFirebaseDevice() { 46 | isFirebaseDevice = Settings.System.getString(contentResolver, "firebase.test.lab") == "true" 47 | } 48 | } 49 | 50 | val mainModule = module { 51 | single { LegacyNpcRepository() } 52 | single { LegacyNpcImporter(get(), get(), get()) } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/legacy/repository/LegacyNpcImporter.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.legacy.repository 2 | 3 | import android.content.Context 4 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcEntity 5 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcRepository 6 | 7 | class LegacyNpcImporter( 8 | private val legacyNpcRepository: LegacyNpcRepository, 9 | private val npcRepository: NpcRepository, 10 | private val context: Context 11 | ) { 12 | 13 | fun importAll() { 14 | val npcs = legacyNpcRepository.loadLegacyNpcs() 15 | 16 | if(npcs.isEmpty()) return 17 | 18 | npcs.forEach { 19 | val newNpc = it.toNpcEntity() 20 | npcRepository.put(newNpc) 21 | } 22 | 23 | context.getSharedPreferences(SAVED_NPCS_SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE).edit().clear().apply() 24 | } 25 | 26 | private fun LegacyNpc.toNpcEntity() = 27 | NpcEntity( 28 | this.name, 29 | "Nickname", 30 | this.gender, 31 | this.sexuality, 32 | this.race, 33 | this.age, 34 | this.profession, 35 | this.motivation, 36 | this.alignment, 37 | this.personalityTraits, 38 | this.languages, 39 | null 40 | ) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/legacy/repository/LegacyNpcRepository.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.legacy.repository 2 | 3 | import android.content.Context 4 | import kotlinx.serialization.json.Json 5 | import kotlinx.serialization.json.JsonArray 6 | import kotlinx.serialization.json.JsonConfiguration 7 | import kotlinx.serialization.json.JsonObject 8 | import org.koin.core.KoinComponent 9 | import org.koin.core.inject 10 | 11 | const val SAVED_NPCS_SHARED_PREFERENCES_NAME = "saved_npcs" 12 | 13 | @Suppress("UNCHECKED_CAST") 14 | class LegacyNpcRepository : KoinComponent { 15 | 16 | private val json = Json(JsonConfiguration.Stable) 17 | private val context by inject() 18 | 19 | fun loadLegacyNpcs(): List { 20 | val npcJsons = getNpcJsons() 21 | 22 | return npcJsons.mapNotNull { 23 | try { 24 | val information = it.getInformationArray() 25 | LegacyNpc( 26 | tryOrEmpty { information.name }, 27 | tryOrEmpty { information.age }, 28 | tryOrEmpty { information.race }, 29 | tryOrEmptyN { information.subRace }, 30 | tryOrEmpty { information.gender }, 31 | tryOrEmpty { information.alignment }, 32 | tryOrEmpty { information.sexuality }, 33 | tryOrEmpty { information.profession }, 34 | tryOrEmpty { information.motivation }, 35 | tryOrEmptyL { information.personalityTraits }, 36 | tryOrEmpty { information.fear }, 37 | tryOrEmptyL { information.languages } 38 | ) 39 | } catch (_: Exception) { 40 | null 41 | } 42 | } 43 | } 44 | 45 | private fun getNpcJsons(): List { 46 | val jsonString = (getSavedNpcsSharedPreferences().all.values as Collection) 47 | 48 | return jsonString.map { 49 | json.parseJson(it.replaceQuote()) as JsonObject 50 | } 51 | } 52 | 53 | private fun getSavedNpcsSharedPreferences() = 54 | context.getSharedPreferences(SAVED_NPCS_SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) 55 | 56 | private fun String.replaceQuote(): String = replace(""", "\"") 57 | 58 | private fun JsonObject.getInformationArray() = getArray("information") 59 | 60 | private val JsonArray.name: String 61 | get() = stringOfType("Name", "name") 62 | 63 | private val JsonArray.age: String 64 | get() = stringOfType("Age", "age").withUnderscoresReplaced().toLowerCase().capitalizeWords() 65 | 66 | private val JsonArray.race: String 67 | get() = stringOfType("Race", "race") 68 | 69 | private val JsonArray.subRace: String? 70 | get() = stringOfType("Race", "subrace") 71 | 72 | private val JsonArray.gender: String 73 | get() = stringOfType("Gender", "gender").toLowerCase().capitalizeWords() 74 | 75 | private val JsonArray.alignment: String 76 | get() = stringOfType("Alignment", "align").withUnderscoresReplaced().toLowerCase().capitalizeWords() 77 | 78 | private val JsonArray.sexuality: String 79 | get() = stringOfType("Sexuality", "sexuality").toLowerCase().capitalizeWords() 80 | 81 | private val JsonArray.profession: String 82 | get() = stringOfType("Profession", "profession") 83 | 84 | private val JsonArray.motivation: String 85 | get() = stringOfType("Motivation", "motivation") 86 | 87 | private val JsonArray.personalityTraits: List 88 | get() = stringListOfType("PersonalityTraits", "traits") 89 | 90 | private val JsonArray.fear: String 91 | get() = stringOfType("Phobia", "phobia") 92 | 93 | private val JsonArray.languages: List 94 | get() = stringListOfType("Language", "spoken").map { it.toLowerCase().capitalizeWords() } 95 | 96 | private fun JsonArray.stringListOfType(type: String, string: String) = 97 | firstOfType(type).jsonObject.node("data")[string]!!.jsonArray.map { it.primitive.content } 98 | 99 | private fun JsonArray.stringOfType(type: String, string: String) = 100 | firstOfType(type).jsonObject.node("data")[string]!!.primitive.content 101 | 102 | private fun JsonArray.firstOfType(type: String) = 103 | first { it.jsonObject.getPrimitive("type").content == "me.kerooker.characterinformation.$type" } 104 | 105 | private fun JsonObject.node(key: String) = get(key) as JsonObject 106 | 107 | private fun String.withUnderscoresReplaced() = replace("_", " ") 108 | 109 | private fun String.capitalizeWords() = split(" ").joinToString(separator = " ") { it.capitalize() } 110 | 111 | private fun tryOrEmpty(block: () -> String) = try { 112 | block() 113 | } catch (_: Exception) { 114 | "" 115 | } 116 | 117 | private fun tryOrEmptyN(block: () -> String?) = try { 118 | block() 119 | } catch (_: Exception) { 120 | null 121 | } 122 | 123 | private fun tryOrEmptyL(block: () -> List) = try { 124 | block() 125 | } catch (_: Exception) { 126 | emptyList() 127 | } 128 | } 129 | 130 | data class LegacyNpc( 131 | val name: String, 132 | val age: String, 133 | val race: String, 134 | val subRace: String?, 135 | val gender: String, 136 | val alignment: String, 137 | val sexuality: String, 138 | val profession: String, 139 | val motivation: String, 140 | val personalityTraits: List, 141 | val fear: String, 142 | val languages: List 143 | ) 144 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/repository/model/persistence/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.repository.model.persistence 2 | 3 | import android.content.Context 4 | import io.objectbox.BoxStore 5 | import io.objectbox.android.AndroidObjectBrowser 6 | import me.kerooker.rpgnpcgenerator.BuildConfig 7 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.admob.AdmobRepository 8 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.MyObjectBox 9 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcEntity 10 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcRepository 11 | import org.koin.core.scope.Scope 12 | import org.koin.dsl.module 13 | 14 | val persistenceModule = module { 15 | single(createdAtStart = true) { createObjectBox() } 16 | single { NpcRepository(get().boxFor(NpcEntity::class.java)) } 17 | single { AdmobRepository(get()) } 18 | } 19 | 20 | private fun Scope.createObjectBox(): BoxStore { 21 | val store = MyObjectBox.builder().androidContext(get()).build() 22 | 23 | if(BuildConfig.DEBUG) { 24 | AndroidObjectBrowser(store).start(get()) 25 | } 26 | return store 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/repository/model/persistence/admob/AdmobRepository.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.repository.model.persistence.admob 2 | 3 | import android.content.Context 4 | import com.tencent.mmkv.MMKV 5 | import java.util.Date 6 | 7 | class AdmobRepository( 8 | context: Context 9 | ) { 10 | 11 | init { 12 | MMKV.initialize(context) 13 | } 14 | 15 | private val kv = MMKV.defaultMMKV() 16 | 17 | private val now = Date().time 18 | var stopSuggestingRemovalUntil: Long 19 | get() = kv.getLong("stopSuggestingRemovalUntil", now + FIVE_SECONDS_MILLIS) 20 | set(value) { 21 | kv.putLong("stopSuggestingRemovalUntil", value) 22 | } 23 | 24 | var stopAdsUntil: Long 25 | get() = kv.getLong("stopAdsUntil", Date().time) 26 | set(value) { 27 | kv.putLong("stopAdsUntil", value) 28 | } 29 | } 30 | 31 | private const val FIVE_SECONDS_MILLIS = 5_000 32 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/repository/model/persistence/npc/NpcEntity.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.repository.model.persistence.npc 2 | 3 | import io.objectbox.annotation.Convert 4 | import io.objectbox.annotation.Entity 5 | import io.objectbox.annotation.Id 6 | import io.objectbox.converter.PropertyConverter 7 | import kotlinx.serialization.ImplicitReflectionSerializer 8 | import kotlinx.serialization.json.Json 9 | import kotlinx.serialization.json.JsonConfiguration 10 | import kotlinx.serialization.list 11 | import kotlinx.serialization.serializer 12 | import kotlinx.serialization.stringify 13 | 14 | @Entity data class NpcEntity( 15 | val fullName: String, 16 | 17 | val nickname: String, 18 | 19 | val gender: String, 20 | 21 | val sexuality: String, 22 | 23 | val race: String, 24 | 25 | val age: String, 26 | 27 | val profession: String, 28 | 29 | val motivation: String, 30 | 31 | val alignment: String, 32 | 33 | @Convert(converter = StringListConverter::class, dbType = String::class) 34 | val personalityTraits: List, 35 | 36 | @Convert(converter = StringListConverter::class, dbType = String::class) 37 | val languages: List, 38 | 39 | val imagePath: String?, 40 | 41 | val notes: String = "", 42 | 43 | @Id var id: Long = 0 44 | ) 45 | 46 | private class StringListConverter : PropertyConverter, String> { 47 | 48 | private val json = Json(JsonConfiguration.Stable) 49 | 50 | @UseExperimental(ImplicitReflectionSerializer::class) 51 | override fun convertToDatabaseValue(entityProperty: List): String { 52 | return json.stringify(entityProperty) 53 | } 54 | 55 | @UseExperimental(ImplicitReflectionSerializer::class) 56 | override fun convertToEntityProperty(databaseValue: String): List { 57 | return json.parse(String.serializer().list, databaseValue) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/repository/model/persistence/npc/NpcRepository.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.repository.model.persistence.npc 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import io.objectbox.Box 6 | import io.objectbox.android.ObjectBoxLiveData 7 | import io.objectbox.query.Query 8 | import io.objectbox.reactive.DataObserver 9 | import io.objectbox.reactive.DataSubscription 10 | 11 | 12 | class NpcRepository( 13 | private val box: Box 14 | ) { 15 | 16 | fun all(): LiveData> { 17 | return ObjectBoxLiveData(box.query().build()) 18 | } 19 | 20 | fun put(npcEntity: NpcEntity): Long { 21 | return box.put(npcEntity) 22 | } 23 | 24 | fun get(id: Long): MutableLiveData { 25 | return ObjectBoxSingleLiveData( 26 | box.query().equal( 27 | NpcEntity_.id, 28 | id 29 | ).build() 30 | ) 31 | } 32 | 33 | fun delete(npcEntity: NpcEntity) { 34 | box.remove(npcEntity) 35 | } 36 | } 37 | 38 | class ObjectBoxSingleLiveData(private val query: Query) : MutableLiveData() { 39 | private var subscription: DataSubscription? = null 40 | private val listener: DataObserver> = DataObserver { data -> postValue(data.single()) } 41 | 42 | override fun onActive() { 43 | if (subscription == null) { 44 | subscription = query.subscribe().observer(listener) 45 | } 46 | } 47 | 48 | override fun onInactive() { 49 | if (!hasObservers()) { 50 | subscription!!.cancel() 51 | subscription = null 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/repository/model/random/npc/EnumGenerators.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("MagicNumber") 2 | 3 | package me.kerooker.rpgnpcgenerator.repository.model.random.npc 4 | 5 | import androidx.annotation.StringRes 6 | import me.kerooker.rpgnpcgenerator.R 7 | import kotlin.random.Random 8 | 9 | interface RandomDistributed { 10 | val distribution: Double 11 | } 12 | 13 | interface NamedResource { 14 | val nameResource: Int 15 | } 16 | 17 | 18 | enum class Age( 19 | @StringRes override val nameResource: Int, 20 | override val distribution: Double 21 | ) : RandomDistributed, 22 | NamedResource { 23 | 24 | Child(R.string.age_child, 5.0), 25 | Teenager(R.string.age_teenager, 10.0), 26 | YoungAdult(R.string.age_young_adult, 35.0), 27 | Adult(R.string.age_adult, 35.0), 28 | Old(R.string.age_old, 10.0), 29 | VeryOld(R.string.age_very_old, 5.0); 30 | 31 | companion object { 32 | fun distributedRandom() = values().toList().distributedRandom() 33 | fun random() = values().random() 34 | } 35 | } 36 | 37 | enum class Alignment( 38 | @StringRes override val nameResource: Int, 39 | override val distribution: Double 40 | ) : RandomDistributed, 41 | NamedResource { 42 | 43 | LawfulGood(R.string.alignment_lawful_good, 2.0), 44 | LawfulNeutral(R.string.alignment_lawful_neutral, 10.0), 45 | LawfulEvil(R.string.alignment_lawful_evil, 2.0), 46 | NeutralGood(R.string.alignment_neutral_good, 10.0), 47 | Neutral(R.string.alignment_neutral, 52.0), 48 | NeutralEvil(R.string.alignment_neutral_evil, 10.0), 49 | ChaoticGood(R.string.alignment_chaotic_good, 2.0), 50 | ChaoticNeutral(R.string.alignment_chaotic_neutral, 10.0), 51 | ChaoticEvil(R.string.alignment_chaotic_evil, 2.0); 52 | 53 | companion object { 54 | fun distributedRandom() = values().toList().distributedRandom() 55 | fun random() = values().random() 56 | } 57 | 58 | } 59 | 60 | enum class Gender( 61 | @StringRes override val nameResource: Int, 62 | override val distribution: Double 63 | ) : RandomDistributed, 64 | NamedResource { 65 | 66 | Male(R.string.gender_male, 50.0), 67 | Female(R.string.gender_female, 50.0); 68 | 69 | companion object { 70 | fun distributedRandom() = values().toList().distributedRandom() 71 | fun random() = values().random() 72 | } 73 | } 74 | 75 | interface Language : NamedResource { 76 | 77 | companion object { 78 | fun values(): List = CommonLanguage.values().toList() + ExoticLanguage.values().toList() 79 | fun random() = 80 | (CommonLanguage.values().toList() + ExoticLanguage.values()).random() as Language 81 | } 82 | } 83 | 84 | enum class CommonLanguage( 85 | @StringRes override val nameResource: Int 86 | ) : Language { 87 | Common(R.string.language_common), 88 | Dwarvish(R.string.language_dwarvish), 89 | Elvish(R.string.language_elvish), 90 | Giant(R.string.language_giant), 91 | Gnomish(R.string.language_gnomish), 92 | Goblin(R.string.language_goblin), 93 | Halfling(R.string.language_halfling), 94 | Orc(R.string.language_orc); 95 | 96 | companion object { 97 | fun random(excluding: List) = values().filter { it !in excluding }.random() 98 | } 99 | } 100 | 101 | enum class ExoticLanguage( 102 | @StringRes override val nameResource: Int 103 | ) : Language { 104 | Celestial(R.string.language_celestial), 105 | Abyssal(R.string.language_abyssal), 106 | Infernal(R.string.language_infernal), 107 | Druidic(R.string.language_druidic), 108 | Draconic(R.string.language_draconic); 109 | 110 | companion object { 111 | fun random(excluding: List) = values().filter { it !in excluding }.random() 112 | } 113 | } 114 | 115 | enum class Race( 116 | @StringRes override val nameResource: Int, 117 | override val distribution: Double, 118 | val racialLanguage: Language? 119 | ) : RandomDistributed, 120 | NamedResource { 121 | 122 | HillDwarf(R.string.race_hill_dwarf, 8.75, CommonLanguage.Dwarvish), 123 | MountainDwarf(R.string.race_mountain_dwarf, 8.75, CommonLanguage.Dwarvish), 124 | 125 | HighElf(R.string.race_high_elf, 5.84, CommonLanguage.Elvish), 126 | WoodElf(R.string.race_wood_elf, 5.83, CommonLanguage.Elvish), 127 | Drow(R.string.race_drow, 5.83, CommonLanguage.Elvish), 128 | 129 | StoutHalfling(R.string.race_stout_halfling, 8.75, CommonLanguage.Halfling), 130 | LightfootHalfling(R.string.race_lightfoot_halfling, 8.75, CommonLanguage.Halfling), 131 | 132 | Human(R.string.race_human, 17.5, null), 133 | 134 | Dragonborn(R.string.race_dragonborn, 6.0, ExoticLanguage.Draconic), 135 | 136 | ForestGnome(R.string.race_forest_gnome, 3.0, CommonLanguage.Gnomish), 137 | RockGnome(R.string.race_rock_gnome, 3.0, CommonLanguage.Gnomish), 138 | 139 | HalfElf(R.string.race_half_elf, 6.0, CommonLanguage.Elvish), 140 | 141 | HalfOrc(R.string.race_half_orc, 6.0, CommonLanguage.Orc), 142 | 143 | Tiefling(R.string.race_tiefling, 6.0, ExoticLanguage.Infernal); 144 | 145 | companion object { 146 | fun distributedRandom() = values().toList().distributedRandom() 147 | fun random() = values().random() 148 | } 149 | } 150 | 151 | enum class Sexuality( 152 | @StringRes override val nameResource: Int, 153 | override val distribution: Double 154 | ) : RandomDistributed, 155 | NamedResource { 156 | 157 | Homosexual(R.string.sexuality_homosexual, 2.0), 158 | Bisexual(R.string.sexuality_bisexual, 2.0), 159 | Asexual(R.string.sexuality_asexual, 1.0), 160 | Heterosexual(R.string.sexuality_heterosexual, 95.0); 161 | 162 | companion object { 163 | fun distributedRandom() = values().toList().distributedRandom() 164 | fun random() = values().random() 165 | } 166 | } 167 | 168 | 169 | // Code inspired from https://github.com/thomasnield/kotlin-statistics/blob/master/src/main/kotlin/org/nield/kotlinstatistics/Random.kt#L111 170 | fun List.distributedRandom(): T { 171 | val probabilities = associateWith { it.distribution } 172 | 173 | val sum = probabilities.values.sum() 174 | 175 | val rangedDistribution = probabilities.run { 176 | 177 | var binStart = 0.0 178 | 179 | asSequence().sortedBy { it.value } 180 | .map { 181 | it.key to OpenDoubleRange( 182 | binStart, 183 | it.value + binStart 184 | ) 185 | } 186 | .onEach { binStart = it.second.endExclusive } 187 | .toMap() 188 | } 189 | 190 | return Random.nextDouble(0.0, sum).let { 191 | rangedDistribution.asIterable().first { rng -> it in rng.value }.key 192 | } 193 | } 194 | 195 | private data class OpenDoubleRange(val start: Double, val endExclusive: Double) { 196 | operator fun contains(it: Double): Boolean { 197 | return it >= start && it < endExclusive 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/repository/model/random/npc/FileGenerators.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.repository.model.random.npc 2 | 3 | import android.content.Context 4 | import androidx.annotation.RawRes 5 | import me.kerooker.rpgnpcgenerator.R 6 | 7 | abstract class FileGenerator( 8 | @RawRes fileResource: Int, 9 | context: Context 10 | ) { 11 | 12 | private val fileLines by lazy { 13 | linesFromRaw( 14 | fileResource, 15 | context 16 | ) 17 | } 18 | 19 | fun random(): String = fileLines.random() 20 | } 21 | 22 | class NameGenerator(context: Context) : FileGenerator(R.raw.npc_names, context) 23 | 24 | class NicknameGenerator(context: Context) : FileGenerator(R.raw.npc_nicknames, context) 25 | 26 | class CommonProfessionGenerator(context: Context) : FileGenerator(R.raw.npc_professions, context) 27 | 28 | class ChildProfessionGenerator(context: Context) : FileGenerator(R.raw.npc_child_professions, context) 29 | 30 | class MotivationGenerator(context: Context) : FileGenerator(R.raw.npc_motivations, context) 31 | 32 | class PersonalityTraitGenerator(context: Context) : FileGenerator(R.raw.npc_personality_trait, context) 33 | 34 | 35 | fun linesFromRaw(@RawRes rawResource: Int, context: Context) = 36 | context.resources.openRawResource(rawResource).bufferedReader().readLines() 37 | 38 | 39 | class ProfessionGenerator( 40 | private val childProfessionGenerator: ChildProfessionGenerator, 41 | private val commonProfessionGenerator: CommonProfessionGenerator 42 | ) { 43 | fun random(age: Age): String { 44 | return when (age) { 45 | Age.Child -> childProfessionGenerator.random() 46 | else -> commonProfessionGenerator.random() 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/repository/model/random/npc/GeneratedNpc.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.repository.model.random.npc 2 | 3 | data class GeneratedNpc( 4 | val name: String, 5 | val nickname: String, 6 | val gender: Gender, 7 | val sexuality: Sexuality, 8 | val race: Race, 9 | val age: Age, 10 | val profession: String, 11 | val motivation: String, 12 | val alignment: Alignment, 13 | val personalityTraits: List, 14 | val languages: List 15 | ) 16 | 17 | data class GeneratedNpcData( 18 | var name: String, 19 | var nickname: String, 20 | var gender: String, 21 | var sexuality: String, 22 | var race: String, 23 | var age: String, 24 | var profession: String, 25 | var motivation: String, 26 | var alignment: String, 27 | val personalityTraits: MutableList, 28 | val languages: MutableList 29 | ) 30 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/repository/model/random/npc/NpcGenerators.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.repository.model.random.npc 2 | 3 | import org.koin.core.KoinComponent 4 | import org.koin.dsl.module 5 | import kotlin.random.Random 6 | 7 | val npcGeneratorsModule = module { 8 | 9 | } 10 | 11 | @Suppress("TooManyFunctions") 12 | class NpcDataGenerator( 13 | private val nameGenerator: NameGenerator, 14 | private val nicknameGenerator: NicknameGenerator, 15 | private val professionGenerator: ProfessionGenerator, 16 | private val motivationGenerator: MotivationGenerator, 17 | private val personalityTraitGenerator: PersonalityTraitGenerator 18 | ) { 19 | fun generateFullName() = nameGenerator.random() + " " + nameGenerator.random() 20 | 21 | fun generateNickname() = nicknameGenerator.random() 22 | 23 | fun generateGender() = Gender.random() 24 | 25 | fun generateDistributedGender() = Gender.distributedRandom() 26 | 27 | fun generateSexuality() = Sexuality.random() 28 | 29 | fun generateDistributedSexuality() = Sexuality.distributedRandom() 30 | 31 | fun generateRace() = Race.random() 32 | 33 | fun generateDistributedRace() = Race.distributedRandom() 34 | 35 | fun generateAge() = Age.random() 36 | 37 | fun generateDistributedAge() = Age.distributedRandom() 38 | 39 | fun generateProfession(age: Age) = professionGenerator.random(age) 40 | 41 | fun generateMotivation() = motivationGenerator.random() 42 | 43 | fun generateAlignment() = Alignment.random() 44 | 45 | fun generateDistributedAlignment() = Alignment.distributedRandom() 46 | 47 | fun generatePersonalityTrait() = personalityTraitGenerator.random() 48 | 49 | fun generateRandomLanguage() = Language.random() 50 | 51 | fun generateCommonLanguage(excluding: List) = CommonLanguage.random(excluding) 52 | 53 | fun generateExoticLanguage(excluding: List) = ExoticLanguage.random(excluding) 54 | } 55 | 56 | class CompleteNpcGenerator( 57 | private val npcDataGenerator: NpcDataGenerator 58 | ) : KoinComponent { 59 | 60 | fun generate(): GeneratedNpc { 61 | val name = generateFullName() 62 | val nickname = generateNickname() 63 | val gender = generateGender() 64 | val sexuality = generateSexuality() 65 | val race = generateRace() 66 | val age = generateAge() 67 | val profession = generateProfession(age) 68 | val motivation = generateMotivation() 69 | val alignment = generateAlignment() 70 | val personalityTraits = generatePersonalityTraits() 71 | val languages = generateLanguages(race.racialLanguage) 72 | 73 | return GeneratedNpc( 74 | name, 75 | nickname, 76 | gender, 77 | sexuality, 78 | race, 79 | age, 80 | profession, 81 | motivation, 82 | alignment, 83 | personalityTraits, 84 | languages 85 | ) 86 | } 87 | 88 | private fun generateFullName() = npcDataGenerator.generateFullName() 89 | 90 | private fun generateNickname() = npcDataGenerator.generateNickname() 91 | 92 | private fun generateGender() = npcDataGenerator.generateDistributedGender() 93 | 94 | private fun generateSexuality() = npcDataGenerator.generateDistributedSexuality() 95 | 96 | private fun generateRace() = npcDataGenerator.generateDistributedRace() 97 | 98 | private fun generateAge() = npcDataGenerator.generateDistributedAge() 99 | 100 | private fun generateProfession(age: Age) = npcDataGenerator.generateProfession(age) 101 | 102 | private fun generateMotivation() = npcDataGenerator.generateMotivation() 103 | 104 | private fun generateAlignment() = npcDataGenerator.generateDistributedAlignment() 105 | 106 | private fun generatePersonalityTraits(): MutableList { 107 | val list = MutableList(2) { npcDataGenerator.generatePersonalityTrait() } 108 | repeat(MaxAmountOfExtraPersonalityTraits) { 109 | if(hitsChance(ChanceOfExtraPersonalityTraits)) 110 | list += npcDataGenerator.generatePersonalityTrait() 111 | } 112 | return list 113 | } 114 | 115 | private fun generateLanguages(racialLanguage: Language?): MutableList { 116 | return mutableListOf() 117 | .tryToAddCommon() 118 | .tryToAddRacial(racialLanguage) 119 | .tryToAddExtraCommonLanguage(racialLanguage) 120 | .tryToAddExtraExoticLanguage(racialLanguage) 121 | 122 | } 123 | 124 | private fun MutableList.tryToAddCommon() = apply { 125 | if (hitsChance(ChanceOfSpeakingCommon)) 126 | this += CommonLanguage.Common 127 | } 128 | 129 | private fun MutableList.tryToAddRacial(racialLanguage: Language?) = apply { 130 | if(hitsChance(ChanceOfSpeakingRacial) && racialLanguage != null) 131 | this += racialLanguage 132 | } 133 | 134 | private fun MutableList.tryToAddExtraCommonLanguage(racialLanguage: Language?) = apply { 135 | if(hitsChance(ChanceOfExtraCommonLanguage)) 136 | this += npcDataGenerator.generateCommonLanguage(this + listOfNotNull(racialLanguage)) 137 | } 138 | 139 | private fun MutableList.tryToAddExtraExoticLanguage(racialLanguage: Language?) = apply { 140 | if(hitsChance(ChanceOfExtraExoticLanguage)) 141 | this += npcDataGenerator.generateExoticLanguage(this + listOfNotNull(racialLanguage)) 142 | } 143 | 144 | private fun hitsChance(chance: Double) = Random.nextDouble() <= chance 145 | 146 | 147 | companion object { 148 | private const val ChanceOfSpeakingCommon = 0.995 149 | private const val ChanceOfSpeakingRacial = 0.995 150 | private const val ChanceOfExtraCommonLanguage = 0.25 151 | private const val ChanceOfExtraExoticLanguage = 0.05 152 | private const val MaxAmountOfExtraPersonalityTraits = 3 153 | private const val ChanceOfExtraPersonalityTraits = 0.25 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/repository/model/random/npc/RandomNpcKoinModule.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.repository.model.random.npc 2 | 3 | import org.koin.dsl.module 4 | 5 | val randomNpcModule = module { 6 | single { NameGenerator(get()) } 7 | single { NicknameGenerator(get()) } 8 | single { CommonProfessionGenerator(get()) } 9 | single { ChildProfessionGenerator(get()) } 10 | single { ProfessionGenerator(get(), get()) } 11 | single { MotivationGenerator(get()) } 12 | single { PersonalityTraitGenerator(get()) } 13 | 14 | single { NpcDataGenerator(get(), get(), get(), get(), get()) } 15 | single { CompleteNpcGenerator(get()) } 16 | 17 | single { TemporaryRandomNpcRepository() } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/repository/model/random/npc/TemporaryRandomNpcRepository.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.repository.model.random.npc 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | 6 | @Suppress("TooManyFunctions") 7 | class TemporaryRandomNpcRepository { 8 | 9 | private val _generatedNpcData = MutableLiveData() 10 | 11 | val generatedNpcData: LiveData 12 | get() = _generatedNpcData 13 | 14 | private val current 15 | get() = generatedNpcData.value ?: throw IllegalStateException("Repository was not started before usage") 16 | 17 | fun setNpc(npc: GeneratedNpcData) { 18 | _generatedNpcData.value = npc 19 | } 20 | 21 | fun setFullName(name: String) = updating { it.name = name } 22 | 23 | fun setNickname(nickname: String) = updating { it.nickname = nickname } 24 | 25 | fun setRace(race: String) = updating { it.race = race } 26 | 27 | fun setGender(gender: String) = updating { it.gender = gender } 28 | 29 | fun setAge(age: String) = updating { it.age = age } 30 | 31 | fun setProfession(profession: String) = updating { it.profession = profession } 32 | 33 | fun setSexuality(sexuality: String) = updating { it.sexuality = sexuality } 34 | 35 | fun setAlignment(alignment: String) = updating { it.alignment = alignment } 36 | 37 | fun setMotivation(motivation: String) = updating { it.motivation = motivation } 38 | 39 | fun setLanguage(index: Int, language: String) = updating { 40 | if (index in it.languages.indices) { 41 | it.languages[index] = language 42 | } else { 43 | it.languages.add(language) 44 | } 45 | } 46 | 47 | fun removeLanguage(index: Int) = updating { it.languages.removeAt(index) } 48 | 49 | fun setPersonality(index: Int, personality: String) = updating { 50 | if(index in it.personalityTraits.indices) { 51 | it.personalityTraits[index] = personality 52 | } else { 53 | it.personalityTraits.add(personality) 54 | } 55 | } 56 | 57 | fun removePersonality(index: Int) = updating { it.personalityTraits.removeAt(index) } 58 | 59 | private inline fun updating(block: (GeneratedNpcData) -> Unit) { 60 | block(current) 61 | _generatedNpcData.value = _generatedNpcData.value 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/view/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.view 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.view.ViewGroup.LayoutParams 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.navigation.findNavController 9 | import androidx.navigation.ui.setupWithNavController 10 | import com.google.android.gms.ads.AdRequest 11 | import com.google.android.gms.ads.AdSize 12 | import com.google.android.gms.ads.AdView 13 | import com.google.android.gms.ads.rewarded.RewardItem 14 | import com.google.android.gms.ads.rewarded.RewardedAd 15 | import com.google.android.gms.ads.rewarded.RewardedAdCallback 16 | import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback 17 | import kotlinx.android.synthetic.main.activity_main.bottom_ad_container 18 | import kotlinx.android.synthetic.main.activity_main.bottom_navigation_view 19 | import kotlinx.android.synthetic.main.activity_main.toolbar 20 | import me.kerooker.rpgnpcgenerator.R 21 | import me.kerooker.rpgnpcgenerator.databinding.ActivityMainBinding 22 | import me.kerooker.rpgnpcgenerator.viewmodel.admob.AdmobViewModel 23 | import org.koin.android.ext.android.inject 24 | import splitties.alertdialog.alertDialog 25 | import splitties.alertdialog.messageResource 26 | import splitties.alertdialog.negativeButton 27 | import splitties.alertdialog.neutralButton 28 | import splitties.alertdialog.onShow 29 | import splitties.alertdialog.positiveButton 30 | import splitties.alertdialog.titleResource 31 | 32 | class MainActivity : AppCompatActivity() { 33 | 34 | private val admobViewModel by inject() 35 | private lateinit var rewardedAd: RewardedAd 36 | 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | super.onCreate(savedInstanceState) 39 | val binding = ActivityMainBinding.inflate(layoutInflater).apply { 40 | lifecycleOwner = this@MainActivity 41 | shouldShowAd = admobViewModel.shouldShowAd 42 | } 43 | 44 | setContentView(binding.root) 45 | val navController = findNavController(R.id.nav_host_fragment) 46 | bottom_navigation_view.setupWithNavController(navController) 47 | 48 | setSupportActionBar(toolbar) 49 | } 50 | 51 | override fun onResume() { 52 | super.onResume() 53 | setupBottomAd() 54 | createRewardedAd() 55 | } 56 | 57 | private fun setupBottomAd() { 58 | if(admobViewModel.shouldShowAd.value == false) return 59 | val view = AdView(this) 60 | view.adSize = AdSize.SMART_BANNER 61 | view.adUnitId = admobViewModel.bannerAdId 62 | 63 | bottom_ad_container.removeAllViews() 64 | bottom_ad_container.addView(view, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) 65 | 66 | view.loadAd(AdRequest.Builder().build()) 67 | } 68 | 69 | 70 | private fun createRewardedAd() { 71 | rewardedAd = RewardedAd(this, admobViewModel.rewardedAdId).apply { 72 | loadAd(AdRequest.Builder().build(), object : RewardedAdLoadCallback() { 73 | 74 | override fun onRewardedAdLoaded() { 75 | setupDismissAdsDialog() 76 | } 77 | }) 78 | } 79 | } 80 | 81 | private fun setupDismissAdsDialog() { 82 | if(!admobViewModel.shouldSuggestRemovingAds()) return 83 | suggestRemovingAds() 84 | admobViewModel.suggestedRemovingAds() 85 | } 86 | 87 | private fun suggestRemovingAds() { 88 | alertDialog { 89 | titleResource = R.string.disable_ads_title 90 | messageResource = R.string.disable_ads_message 91 | 92 | positiveButton(R.string.go_pro) { goPro() } 93 | negativeButton(R.string.watch_ad_remove_ad) { watchAd() } 94 | neutralButton(R.string.not_now) { it.dismiss() } 95 | }.onShow { 96 | positiveButton.setBackgroundColor(context.resources.getColor(android.R.color.transparent)) 97 | negativeButton.setBackgroundColor(context.resources.getColor(android.R.color.transparent)) 98 | neutralButton.setBackgroundColor(context.resources.getColor(android.R.color.transparent)) 99 | 100 | positiveButton.setTextColor(context.resources.getColor(R.color.colorPrimary)) 101 | negativeButton.setTextColor(context.resources.getColor(R.color.colorAccent)) 102 | neutralButton.setTextColor(context.resources.getColor(R.color.colorPrimary)) 103 | }.show() 104 | } 105 | 106 | private fun watchAd() { 107 | rewardedAd.show(this, object : RewardedAdCallback() { 108 | override fun onUserEarnedReward(item: RewardItem) { 109 | admobViewModel.watchedRewardedAd() 110 | } 111 | }) 112 | } 113 | 114 | private fun goPro() { 115 | startActivity( 116 | Intent( 117 | Intent.ACTION_VIEW, 118 | Uri.parse("https://play.google.com/store/apps/details?id=me.kerooker.rpgcharactergeneratorpro") 119 | ) 120 | ) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/view/my/npc/MyNpcsFragment.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.view.my.npc 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView.ScaleType 8 | import androidx.core.view.updateLayoutParams 9 | import androidx.fragment.app.Fragment 10 | import androidx.navigation.fragment.findNavController 11 | import androidx.recyclerview.widget.DiffUtil 12 | import androidx.recyclerview.widget.DiffUtil.calculateDiff 13 | import androidx.recyclerview.widget.RecyclerView 14 | import androidx.recyclerview.widget.RecyclerView.LayoutParams 15 | import androidx.recyclerview.widget.RecyclerView.ViewHolder 16 | import coil.api.load 17 | import com.lucasurbas.listitemview.ListItemView 18 | import jp.wasabeef.recyclerview.animators.LandingAnimator 19 | import kotlinx.android.synthetic.main.mynpcs_fragment.my_npcs_recycler 20 | import me.kerooker.rpgnpcgenerator.R 21 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcEntity 22 | import me.kerooker.rpgnpcgenerator.view.my.npc.MyNpcsAdapter.MyNpcViewHolder 23 | import me.kerooker.rpgnpcgenerator.viewmodel.my.npc.MyNpcsViewModel 24 | import org.koin.android.viewmodel.ext.android.viewModel 25 | import java.io.File 26 | 27 | class MyNpcsFragment : Fragment() { 28 | 29 | private val myNpcsViewModel by viewModel() 30 | 31 | override fun onCreateView( 32 | inflater: LayoutInflater, 33 | container: ViewGroup?, 34 | savedInstanceState: Bundle? 35 | ): View = inflater.inflate(R.layout.mynpcs_fragment, container, false) 36 | 37 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 38 | super.onViewCreated(view, savedInstanceState) 39 | 40 | configureNpcList() 41 | } 42 | 43 | private fun configureNpcList() { 44 | my_npcs_recycler.apply { 45 | adapter = MyNpcsAdapter(emptyList(), { onNpcClick(it) }, { myNpcsViewModel.deleteNpc(it) } ) 46 | itemAnimator = LandingAnimator() 47 | } 48 | 49 | myNpcsViewModel.npcsToDisplay.observe({ this.lifecycle }) { 50 | val adapter = (my_npcs_recycler.adapter as MyNpcsAdapter) 51 | adapter.updateNpcList(it) 52 | } 53 | } 54 | 55 | private fun onNpcClick(npc: NpcEntity) { 56 | val action = MyNpcsFragmentDirections.actionMyNpcsFragmentToIndividualNpcFragment(npc.id) 57 | findNavController().navigate(action) 58 | } 59 | 60 | } 61 | 62 | private class MyNpcsAdapter( 63 | private var npcsToDisplay: List, 64 | val onNpcClick: (NpcEntity) -> Unit, 65 | val onNpcDelete: (NpcEntity) -> Unit 66 | ) : RecyclerView.Adapter() { 67 | 68 | fun updateNpcList(npcList: List) { 69 | val listDiff = calculateDiff(NpcListDiffUtilCallback(npcsToDisplay, npcList)) 70 | npcsToDisplay = npcList 71 | listDiff.dispatchUpdatesTo(this) 72 | } 73 | 74 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyNpcViewHolder { 75 | val view = ListItemView(parent.context).apply { 76 | displayMode = ListItemView.MODE_AVATAR 77 | avatarView.setImageResource(R.drawable.portrait_placeholder) 78 | setMultiline(true) 79 | inflateMenu(R.menu.mynpcs_list_item_menu) 80 | layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) 81 | } 82 | return MyNpcViewHolder(view) 83 | } 84 | 85 | override fun getItemCount() = npcsToDisplay.size 86 | 87 | override fun onBindViewHolder(holder: MyNpcViewHolder, position: Int) { 88 | holder.setDisplayedNpc(npcsToDisplay[position]) 89 | holder.setOnClickListener { onNpcClick(it) } 90 | holder.setOnDeleteListener { onNpcDelete(it) } 91 | } 92 | 93 | class MyNpcViewHolder(val view: ListItemView) : ViewHolder(view) { 94 | 95 | private lateinit var npc: NpcEntity 96 | 97 | @Suppress("MagicNumber") 98 | fun setDisplayedNpc(entity: NpcEntity) { 99 | npc = entity 100 | view.title = "${entity.fullName}, ${entity.nickname}" 101 | view.subtitle = "${entity.gender} ${entity.race}\n${entity.profession}" 102 | view.avatarView.scaleType = ScaleType.FIT_CENTER 103 | view.avatarView.updateLayoutParams { height = (height * 1.33).toInt() } 104 | if(entity.imagePath != null) { 105 | view.avatarView.load(File(entity.imagePath)) 106 | } 107 | } 108 | 109 | fun setOnClickListener(block: (NpcEntity) -> Unit) { 110 | view.setOnClickListener { block(npc) } 111 | } 112 | 113 | fun setOnDeleteListener(block: (NpcEntity) -> Unit) { 114 | view.setOnMenuItemClickListener { 115 | if(it.itemId == R.id.delete) { block(npc) } 116 | } 117 | } 118 | } 119 | } 120 | 121 | private class NpcListDiffUtilCallback( 122 | private val oldNpcs: List, 123 | private val newNpcs: List 124 | ) : DiffUtil.Callback() { 125 | 126 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = 127 | oldNpcs[oldItemPosition].id == newNpcs[newItemPosition].id 128 | 129 | override fun getOldListSize() = oldNpcs.size 130 | 131 | override fun getNewListSize() = newNpcs.size 132 | 133 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = 134 | oldNpcs[oldItemPosition] == newNpcs[newItemPosition] 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/view/random/npc/RandomNpcElementListview.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.view.random.npc 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.util.Log 6 | import androidx.cardview.widget.CardView 7 | import kotlinx.android.synthetic.main.randomnpc_element_list_view_item.view.* 8 | import kotlinx.android.synthetic.main.randomnpc_element_view.view.random_field_dice 9 | 10 | class RandomNpcElementListview(context: Context, attrs: AttributeSet) : CardView(context, attrs) { 11 | 12 | var onDeleteClick = { } 13 | 14 | var onRandomClick = { } 15 | 16 | var onManualInput = object : ManualInputListener { 17 | override fun onManualInput(text: String) { } 18 | } 19 | 20 | override fun onFinishInflate() { 21 | super.onFinishInflate() 22 | prepareEventListeners() 23 | } 24 | 25 | private fun prepareEventListeners() { 26 | random_npc_minus.setOnClickListener { onDeleteClick() } 27 | random_npc_list_item_element.onRandomClick = { onRandomClick() } 28 | random_npc_list_item_element.onManualInput = object : ManualInputListener { 29 | override fun onManualInput(text: String) { 30 | onManualInput.onManualInput(text) 31 | } 32 | } 33 | } 34 | 35 | fun setText(text: String) { random_npc_list_item_element.text = text } 36 | 37 | fun setHint(hint: String) { random_npc_list_item_element.setHint(hint) } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/view/random/npc/RandomNpcElementView.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.view.random.npc 2 | 3 | import android.content.Context 4 | import android.text.Editable 5 | import android.text.InputType.TYPE_CLASS_TEXT 6 | import android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES 7 | import android.util.AttributeSet 8 | import android.view.View 9 | import android.view.inputmethod.EditorInfo 10 | import android.widget.EditText 11 | import android.widget.LinearLayout 12 | import androidx.core.widget.doAfterTextChanged 13 | import kotlinx.android.synthetic.main.randomnpc_element_view.view.random_field_dice 14 | import kotlinx.android.synthetic.main.randomnpc_element_view.view.random_field_text 15 | import kotlinx.android.synthetic.main.randomnpc_element_view.view.random_field_text_layout 16 | import me.kerooker.rpgnpcgenerator.R 17 | import me.kerooker.rpgnpcgenerator.view.util.animateRotation 18 | 19 | 20 | interface ManualInputListener { 21 | fun onManualInput(text: String) 22 | } 23 | 24 | class RandomNpcElementView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { 25 | 26 | var onRandomClick: () -> Unit = { } 27 | 28 | var onManualInput: ManualInputListener = object: ManualInputListener { 29 | override fun onManualInput(text: String) { } 30 | } 31 | 32 | var text 33 | get() = random_field_text.text.toString() 34 | set(value) { 35 | if(text == value) return 36 | random_field_text.setText(value) 37 | } 38 | 39 | init { 40 | View.inflate(context, R.layout.randomnpc_element_view, this) 41 | 42 | val attributes = context.obtainStyledAttributes(attrs, R.styleable.RandomNpcElementView) 43 | random_field_text_layout.hint = attributes.getString(R.styleable.RandomNpcElementView_hint) 44 | random_field_text_layout.editText?.setText(text) 45 | attributes.recycle() 46 | 47 | prepareDiceClick() 48 | prepareTextListener() 49 | 50 | random_field_text.prepareWordWrap() 51 | } 52 | 53 | 54 | private fun prepareDiceClick() { 55 | random_field_dice.setOnClickListener { 56 | it.animateRotation { 57 | onRandomClick() 58 | } 59 | } 60 | } 61 | 62 | private fun prepareTextListener() { 63 | random_field_text.doAfterTextChanged { text: Editable? -> 64 | onManualInput.onManualInput(text?.toString() ?: "") 65 | } 66 | } 67 | 68 | @Suppress("MagicNumber") 69 | private fun EditText.prepareWordWrap() { 70 | inputType = TYPE_CLASS_TEXT or TYPE_TEXT_FLAG_CAP_SENTENCES 71 | setSingleLine(true) 72 | maxLines = 100 73 | setHorizontallyScrolling(false) 74 | imeOptions = EditorInfo.IME_ACTION_DONE 75 | } 76 | 77 | fun setHint(hint: String) { 78 | random_field_text_layout.hint = hint 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/view/random/npc/RandomNpcFragment.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.view.random.npc 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Bitmap.Config 5 | import android.graphics.Canvas 6 | import android.os.Bundle 7 | import android.view.LayoutInflater 8 | import android.view.Menu 9 | import android.view.MenuInflater 10 | import android.view.MenuItem 11 | import android.view.View 12 | import android.view.View.OnLayoutChangeListener 13 | import android.view.ViewGroup 14 | import android.view.ViewPropertyAnimator 15 | import android.widget.ImageView 16 | import androidx.fragment.app.Fragment 17 | import com.google.android.material.bottomnavigation.BottomNavigationItemView 18 | import kotlinx.android.synthetic.main.activity_main.bottom_navigation_view 19 | import kotlinx.android.synthetic.main.activity_main.toolbar 20 | import kotlinx.android.synthetic.main.randomnpc_fragment.screenshot_view 21 | import me.kerooker.rpgnpcgenerator.R 22 | import me.kerooker.rpgnpcgenerator.databinding.RandomnpcFragmentBinding 23 | import me.kerooker.rpgnpcgenerator.view.util.animateRotation 24 | import me.kerooker.rpgnpcgenerator.viewmodel.random.npc.RandomNpcViewModel 25 | import org.koin.android.viewmodel.ext.android.viewModel 26 | 27 | class RandomNpcFragment : Fragment() { 28 | 29 | 30 | private val randomNpcViewModel by viewModel() 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | setHasOptionsMenu(true) 35 | } 36 | 37 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 38 | savedInstanceState: Bundle?): View? { 39 | 40 | val binding = RandomnpcFragmentBinding.inflate(inflater, container, false) 41 | binding.lifecycleOwner = this 42 | binding.npc = randomNpcViewModel.data 43 | binding.randomNpcViewModel = randomNpcViewModel 44 | 45 | return binding.root 46 | } 47 | 48 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 49 | super.onViewCreated(view, savedInstanceState) 50 | prepareOnRandomizeAllClick() 51 | } 52 | 53 | private fun prepareOnRandomizeAllClick() { 54 | val toolbar = activity!!.toolbar 55 | 56 | // This must be done through a LayoutChangeListener because it's the only way to get a reference to the toolbar 57 | // https://stackoverflow.com/questions/30787373/android-how-to-make-transition-animations-on-toolbars-menu-icons 58 | toolbar.addOnLayoutChangeListener(object : OnLayoutChangeListener { 59 | override fun onLayoutChange(v: View?, l: Int, t: Int, r: Int, b: Int, ol: Int, ot: Int, or: Int, ob: Int) { 60 | val item = toolbar.findViewById(R.id.randomize_all) ?: return 61 | toolbar.removeOnLayoutChangeListener(this) 62 | item.setOnClickListener { 63 | it.animateRotation { 64 | onRandomizeAllMenuClick() 65 | } 66 | } 67 | } 68 | }) 69 | } 70 | 71 | 72 | 73 | 74 | private fun onRandomizeAllMenuClick() { 75 | randomNpcViewModel.randomizeAll() 76 | } 77 | 78 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 79 | inflater.inflate(R.menu.randomnpc_fragment_menu, menu) 80 | } 81 | 82 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 83 | when (item.itemId) { 84 | // This won't work because the animation of randomizeAll steals the onClick. The selection must happen there 85 | // R.id.randomize_all -> onRandomizeAllMenuClick() 86 | R.id.save -> onSaveMenuClick() 87 | } 88 | return true 89 | } 90 | 91 | private fun onSaveMenuClick() { 92 | ScreenshotAnimator().animate() 93 | randomNpcViewModel.saveCurrentNpc() 94 | } 95 | 96 | private inner class ScreenshotAnimator { 97 | 98 | private val myNpcsFragmentView = 99 | requireActivity().bottom_navigation_view.findViewById(R.id.myNpcsFragment) 100 | 101 | fun animate() { 102 | if(isAnimating) return 103 | isAnimating = true 104 | val screenshot = takeScreenshot() 105 | screenshot_view.animateToBottom(screenshot) 106 | } 107 | 108 | private fun takeScreenshot(): Bitmap { 109 | val bitmap = Bitmap.createBitmap(requireView().width, requireView().height, Config.ARGB_8888) 110 | val canvas = Canvas(bitmap) 111 | requireView().draw(canvas) 112 | return bitmap 113 | } 114 | 115 | private fun ImageView.animateToBottom(bitmap: Bitmap) { 116 | setImageBitmap(bitmap) 117 | visibility = View.VISIBLE 118 | 119 | shrinkingMoveTo(myNpcsFragmentView) 120 | } 121 | 122 | @Suppress("MagicNumber") 123 | private fun ImageView.shrinkingMoveTo(menuView: BottomNavigationItemView) { 124 | val (initialX, initialY) = x to y 125 | val (myX, myY) = getLocationInWindow() 126 | val (menuX, menuY) = menuView.getLocationInWindow() 127 | 128 | menuView.animateIconRotation() 129 | 130 | animate() 131 | .animateScaleFade() 132 | .animateMovement(myX, myY, menuX, menuY) 133 | .withEndAction { this.reset(initialX, initialY) } 134 | .setDuration(500L) 135 | .start() 136 | } 137 | 138 | private fun View.getLocationInWindow(): Pair { 139 | val arr = IntArray(2) 140 | getLocationInWindow(arr) 141 | 142 | return arr[0] to arr[1] 143 | } 144 | 145 | private fun BottomNavigationItemView.animateIconRotation() = findViewById(R.id.icon).animateRotation() 146 | 147 | @Suppress("MagicNumber") 148 | private fun ViewPropertyAnimator.animateScaleFade() = apply { 149 | alpha(0.05f) 150 | scaleX(0.05f) 151 | scaleY(0.05f) 152 | } 153 | 154 | private fun ViewPropertyAnimator.animateMovement(myX: Int, myY: Int, menuX: Int, menuY: Int) = apply { 155 | x((menuX - myX).toFloat()) 156 | y((menuY - myY).toFloat()) 157 | } 158 | 159 | @Suppress("MagicNumber") 160 | private fun ImageView.reset(initialX: Float, initialY: Float) { 161 | x = initialX 162 | y = initialY 163 | alpha = 1f 164 | scaleX = 1f 165 | scaleY = 1f 166 | isAnimating = false 167 | visibility = View.GONE 168 | setImageBitmap(null) 169 | } 170 | } 171 | 172 | companion object { 173 | private var isAnimating = false 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/view/random/npc/RandomNpcListView.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.view.random.npc 2 | 3 | import android.content.Context 4 | import android.graphics.Rect 5 | import android.util.AttributeSet 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.widget.LinearLayout 9 | import androidx.constraintlayout.widget.ConstraintLayout 10 | import kotlinx.android.synthetic.main.randomnpc_element_list_view.view.add_item_button 11 | import kotlinx.android.synthetic.main.randomnpc_element_list_view.view.add_item_text 12 | import kotlinx.android.synthetic.main.randomnpc_element_list_view.view.list 13 | import me.kerooker.rpgnpcgenerator.R 14 | 15 | interface OnPositionedRandomizeClick { 16 | fun onRandomClick(index: Int) 17 | } 18 | 19 | interface OnPositionedDeleteClick { 20 | fun onDeleteClick(index: Int) 21 | } 22 | 23 | interface IndexedManualInputListener { 24 | fun onManualInput(index: Int, text: String) 25 | } 26 | 27 | 28 | class RandomNpcListView( 29 | context: Context, 30 | attrs: AttributeSet 31 | ) : ConstraintLayout(context, attrs) { 32 | 33 | private val listView by lazy { list } 34 | private val adapter by lazy { RandomNpcListAdapter(listView) } 35 | 36 | init { 37 | View.inflate(context, R.layout.randomnpc_element_list_view, this) 38 | 39 | 40 | val attributes = context.obtainStyledAttributes(attrs, R.styleable.RandomNpcListView) 41 | adapter.hint = attributes.getString(R.styleable.RandomNpcListView_list_hint)!! 42 | attributes.recycle() 43 | 44 | add_item_button.setOnClickListener { addItem() } 45 | add_item_text.setOnClickListener { addItem() } 46 | } 47 | 48 | private fun addItem() { 49 | val nextIndex = listView.childCount 50 | adapter.onPositionedRandomizeClick.onRandomClick(nextIndex) 51 | scrollToAddButton() 52 | } 53 | 54 | @Suppress("MagicNumber") 55 | private fun scrollToAddButton() { 56 | postDelayed( 57 | { 58 | val addButton = add_item_button 59 | val rect = Rect(0, 0, addButton.width, addButton.height) 60 | addButton.requestRectangleOnScreen(rect, false) 61 | }, 100 62 | ) 63 | } 64 | 65 | fun setElements(elements: List) { 66 | adapter.elements = elements 67 | } 68 | 69 | fun setOnPositionedRandomizeClick(onPositionedRandomizeClick: OnPositionedRandomizeClick) { 70 | adapter.onPositionedRandomizeClick = onPositionedRandomizeClick 71 | } 72 | 73 | fun setOnPositionedDeleteClick(onPositionedDeleteClick: OnPositionedDeleteClick) { 74 | adapter.onPositionedDeleteClick = onPositionedDeleteClick 75 | } 76 | 77 | fun setOnIndexedManualInputListener(indexedManualInputListener: IndexedManualInputListener) { 78 | adapter.indexedManualInputListener = indexedManualInputListener 79 | } 80 | 81 | } 82 | 83 | class RandomNpcListAdapter( 84 | private val listView: LinearLayout 85 | ) { 86 | 87 | lateinit var onPositionedRandomizeClick: OnPositionedRandomizeClick 88 | lateinit var onPositionedDeleteClick: OnPositionedDeleteClick 89 | lateinit var indexedManualInputListener: IndexedManualInputListener 90 | 91 | var hint: String = "" 92 | 93 | var elements: List = emptyList() 94 | set(value) { 95 | field = value 96 | updateElements() 97 | } 98 | 99 | private fun updateElements() { 100 | elements.forEachIndexed { index, s -> 101 | updateOrCreate(index, s) 102 | } 103 | removeRemainingViews() 104 | } 105 | 106 | private fun updateOrCreate(index: Int, string: String) { 107 | val currentPosition = listView.getChildAt(index) 108 | if(currentPosition == null) create(index, string) else update(index, string) 109 | } 110 | 111 | private fun create(index: Int, string: String) { 112 | val view = createView(string) 113 | listView.addView(view, index) 114 | } 115 | 116 | private fun createView(string: String): View { 117 | val inflater = LayoutInflater.from(listView.context) 118 | val view = 119 | inflater.inflate(R.layout.randomnpc_element_list_view_item, listView, false) as RandomNpcElementListview 120 | view.prepareTexts(string) 121 | view.prepareListeners() 122 | return view 123 | } 124 | 125 | private fun update(index: Int, text: String) { 126 | val view = listView.getChildAt(index) as RandomNpcElementListview 127 | view.prepareTexts(text) 128 | } 129 | 130 | private fun RandomNpcElementListview.prepareTexts(text: String) { 131 | setText(text) 132 | setHint(hint) 133 | } 134 | 135 | private fun RandomNpcElementListview.prepareListeners() { 136 | onRandomClick = { 137 | val index = listView.indexOfChild(this) 138 | onPositionedRandomizeClick.onRandomClick(index) 139 | } 140 | 141 | onDeleteClick = { 142 | val index = listView.indexOfChild(this) 143 | listView.removeViewAt(index) 144 | onPositionedDeleteClick.onDeleteClick(index) 145 | } 146 | 147 | onManualInput = object : ManualInputListener { 148 | override fun onManualInput(text: String) { 149 | val index = listView.indexOfChild(this@prepareListeners) 150 | indexedManualInputListener.onManualInput(index, text) 151 | } 152 | } 153 | } 154 | 155 | private fun removeRemainingViews() { 156 | val elementsSize = elements.size 157 | val childrenSize = listView.childCount 158 | 159 | val difference = childrenSize - elementsSize 160 | 161 | if(difference <= 0) return 162 | 163 | repeat(difference) { 164 | val lastIndex = listView.childCount - 1 165 | listView.removeViewAt(lastIndex) 166 | } 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/view/settings/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.view.settings 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import androidx.preference.Preference 7 | import androidx.preference.PreferenceFragmentCompat 8 | import com.google.android.gms.oss.licenses.OssLicensesMenuActivity 9 | import me.kerooker.rpgnpcgenerator.R 10 | import me.kerooker.rpgnpcgenerator.viewmodel.settings.SettingsViewModel 11 | import org.koin.android.viewmodel.ext.android.viewModel 12 | 13 | class SettingsFragment : PreferenceFragmentCompat() { 14 | 15 | private val settingsViewModel by viewModel() 16 | 17 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 18 | setPreferencesFromResource(R.xml.preferences, rootKey) 19 | 20 | setupPreferences() 21 | } 22 | 23 | private fun setupPreferences() { 24 | setupRepositoryTouch() 25 | setupOpenSourceLibrariesTouch() 26 | setupReportBugTouch() 27 | setupGoPro() 28 | } 29 | 30 | private fun setupRepositoryTouch() { 31 | findPreference("project_repository")!!.setOnPreferenceClickListener { 32 | openUrl("https://github.com/Kerooker/rpg-npc-generator") 33 | true 34 | } 35 | } 36 | 37 | private fun setupOpenSourceLibrariesTouch() { 38 | findPreference("open_source_libs")!!.setOnPreferenceClickListener { 39 | startActivity(Intent(requireContext(), OssLicensesMenuActivity::class.java)) 40 | true 41 | } 42 | } 43 | 44 | private fun setupReportBugTouch() { 45 | findPreference("bug_report")!!.setOnPreferenceClickListener { 46 | openUrl("https://github.com/Kerooker/rpg-npc-generator/issues") 47 | true 48 | } 49 | } 50 | 51 | private fun setupGoPro() { 52 | val preference = findPreference("go_pro")!! 53 | if(settingsViewModel.isPro()) { 54 | preference.isEnabled = false 55 | } 56 | preference.setOnPreferenceClickListener { 57 | openUrl("https://play.google.com/store/apps/details?id=me.kerooker.rpgcharactergeneratorpro") 58 | true 59 | } 60 | } 61 | 62 | private fun openUrl(url: String) { 63 | startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/view/text/ClearFocusEditText.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.view.text 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.KeyEvent 6 | import com.google.android.material.textfield.TextInputEditText 7 | 8 | class ClearFocusEditText: TextInputEditText { 9 | constructor(context: Context) : super(context) 10 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 11 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) 12 | 13 | override fun onKeyPreIme(keyCode: Int, event: KeyEvent?): Boolean { 14 | if(keyCode == KeyEvent.KEYCODE_BACK) { 15 | clearFocus() 16 | } 17 | 18 | return super.onKeyPreIme(keyCode, event) 19 | } 20 | 21 | /** 22 | * Necessary to remove underline on typos but keeping text suggestions 23 | * https://stackoverflow.com/a/36479831/4257162 24 | */ 25 | override fun isSuggestionsEnabled() = false 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/view/util/AnimateRotation.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.view.util 2 | 3 | import android.view.View 4 | import android.view.animation.Animation 5 | import android.view.animation.Animation.AnimationListener 6 | import android.view.animation.AnimationUtils 7 | import me.kerooker.rpgnpcgenerator.R 8 | 9 | fun View.animateRotation(onAnimationEnd: () -> Unit = { }) { 10 | val animation = AnimationUtils.loadAnimation(context, R.anim.rotate_animation) 11 | animation.setAnimationListener(object: AnimationListener { 12 | override fun onAnimationRepeat(animation: Animation?) { } 13 | 14 | override fun onAnimationEnd(animation: Animation?) { 15 | onAnimationEnd() 16 | } 17 | 18 | override fun onAnimationStart(animation: Animation?) { } 19 | 20 | }) 21 | startAnimation(animation) 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/viewmodel/ViewModelsModule.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.viewmodel 2 | 3 | import me.kerooker.rpgnpcgenerator.viewmodel.admob.AdmobViewModel 4 | import me.kerooker.rpgnpcgenerator.viewmodel.my.npc.MyNpcsViewModel 5 | import me.kerooker.rpgnpcgenerator.viewmodel.my.npc.individual.IndividualNpcViewModel 6 | import me.kerooker.rpgnpcgenerator.viewmodel.random.npc.RandomNpcViewModel 7 | import me.kerooker.rpgnpcgenerator.viewmodel.settings.SettingsViewModel 8 | import org.koin.android.viewmodel.dsl.viewModel 9 | import org.koin.dsl.module 10 | 11 | val viewModelsModule = module { 12 | viewModel { RandomNpcViewModel(get(), get(), get(), get(), get()) } 13 | viewModel { MyNpcsViewModel(get()) } 14 | viewModel { (npcId: Long) -> IndividualNpcViewModel(npcId, get()) } 15 | viewModel { AdmobViewModel(get()) } 16 | viewModel { SettingsViewModel() } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/viewmodel/admob/AdmobViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.viewmodel.admob 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import me.kerooker.rpgnpcgenerator.isFirebaseDevice 7 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.admob.AdmobRepository 8 | import java.util.Date 9 | 10 | @Suppress("ReturnCount") 11 | class AdmobViewModel( 12 | private val admobRepository: AdmobRepository 13 | ) : ViewModel() { 14 | 15 | val bannerAdId = 16 | if("debug" in MyBuildConfig.APPLICATION_ID) "ca-app-pub-3940256099942544/6300978111" 17 | else "ca-app-pub-4066886200642192/4591525789" 18 | 19 | val rewardedAdId = 20 | if("debug" in MyBuildConfig.APPLICATION_ID) "ca-app-pub-3940256099942544/5224354917" 21 | else "ca-app-pub-4066886200642192/4016810716" 22 | 23 | private val _shouldShowAd by lazy { MutableLiveData(calculateShouldShowAd()) } 24 | val shouldShowAd: LiveData by lazy { _shouldShowAd } 25 | 26 | private fun calculateShouldShowAd(): Boolean { 27 | if(isFirebaseDevice) return false 28 | if ("pro" in MyBuildConfig.APPLICATION_ID) return false 29 | if(admobRepository.stopAdsUntil > Date().time) return false 30 | return true 31 | } 32 | 33 | fun shouldSuggestRemovingAds(): Boolean { 34 | if(isFirebaseDevice) return false 35 | if(!calculateShouldShowAd()) return false 36 | if(admobRepository.stopSuggestingRemovalUntil > Date().time) return false 37 | return true 38 | } 39 | 40 | fun suggestedRemovingAds() { 41 | val now = Date() 42 | admobRepository.stopSuggestingRemovalUntil = now.time + TEN_MINUTES_MILLIS 43 | } 44 | 45 | fun watchedRewardedAd() { 46 | val now = Date() 47 | admobRepository.stopSuggestingRemovalUntil = now.time + ONE_DAY_MILLIS 48 | admobRepository.stopAdsUntil = now.time + ONE_DAY_MILLIS 49 | _shouldShowAd.value = calculateShouldShowAd() 50 | } 51 | 52 | } 53 | 54 | private const val ONE_DAY_MILLIS = 24 * 60 * 60 * 1000 55 | private const val TEN_MINUTES_MILLIS = 10 * 60 * 1000 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/viewmodel/admob/MyBuildConfig.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.viewmodel.admob 2 | 3 | import me.kerooker.rpgnpcgenerator.BuildConfig 4 | 5 | @Suppress("MayBeConstant") 6 | object MyBuildConfig { 7 | val APPLICATION_ID = BuildConfig.APPLICATION_ID 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/viewmodel/my/npc/MyNpcsViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.viewmodel.my.npc 2 | 3 | 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.ViewModel 6 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcEntity 7 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcRepository 8 | 9 | class MyNpcsViewModel( 10 | private val npcRepository: NpcRepository 11 | ) : ViewModel() { 12 | 13 | val npcsToDisplay: LiveData> = npcRepository.all() 14 | 15 | fun deleteNpc(npc: NpcEntity) { 16 | npcRepository.delete(npc) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/viewmodel/my/npc/individual/IndividualNpcViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.viewmodel.my.npc.individual 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcEntity 7 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcRepository 8 | 9 | class IndividualNpcViewModel( 10 | npcId: Long, 11 | private val npcRepository: NpcRepository 12 | ) : ViewModel() { 13 | 14 | private val _npc: MutableLiveData = npcRepository.get(npcId) 15 | val npc: LiveData = _npc 16 | 17 | private val _editState = MutableLiveData(EditState.VIEW) 18 | val editState: LiveData = _editState 19 | 20 | fun enableEdit() { 21 | _editState.value = EditState.EDIT 22 | } 23 | 24 | fun saveEdit(npcEntity: NpcEntity) { 25 | _editState.value = EditState.VIEW 26 | val current = npc.value!! 27 | val new = npcEntity.copy(id = current.id) 28 | npcRepository.put(new) 29 | 30 | } 31 | 32 | fun cancelEdit() { 33 | _editState.value = EditState.VIEW 34 | _npc.value = _npc.value 35 | } 36 | } 37 | 38 | enum class EditState { 39 | VIEW, EDIT 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/viewmodel/random/npc/RandomNpcViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.viewmodel.random.npc 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.launch 9 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcEntity 10 | import me.kerooker.rpgnpcgenerator.repository.model.persistence.npc.NpcRepository 11 | import me.kerooker.rpgnpcgenerator.repository.model.random.npc.Age 12 | import me.kerooker.rpgnpcgenerator.repository.model.random.npc.CompleteNpcGenerator 13 | import me.kerooker.rpgnpcgenerator.repository.model.random.npc.GeneratedNpc 14 | import me.kerooker.rpgnpcgenerator.repository.model.random.npc.GeneratedNpcData 15 | import me.kerooker.rpgnpcgenerator.repository.model.random.npc.Language 16 | import me.kerooker.rpgnpcgenerator.repository.model.random.npc.NpcDataGenerator 17 | import me.kerooker.rpgnpcgenerator.repository.model.random.npc.TemporaryRandomNpcRepository 18 | 19 | @Suppress("TooManyFunctions") 20 | class RandomNpcViewModel( 21 | private val context: Context, 22 | private val completeNpcGenerator: CompleteNpcGenerator, 23 | private val npcDataGenerator: NpcDataGenerator, 24 | private val temporaryRandomNpcRepository: TemporaryRandomNpcRepository, 25 | private val npcRepository: NpcRepository 26 | ) : ViewModel() { 27 | 28 | 29 | val data: LiveData by lazy { 30 | if(temporaryRandomNpcRepository.generatedNpcData.value == null) { 31 | temporaryRandomNpcRepository.setNpc(completeNpcGenerator.generate().toNpcData()) 32 | } 33 | temporaryRandomNpcRepository.generatedNpcData 34 | } 35 | 36 | fun randomizeName() = setName(npcDataGenerator.generateFullName()) 37 | 38 | fun setName(name: String) = temporaryRandomNpcRepository.setFullName(name) 39 | 40 | fun randomizeNickname() { 41 | val nickname = npcDataGenerator.generateNickname() 42 | setNickname(nickname) 43 | } 44 | 45 | fun setNickname(nickname: String) = temporaryRandomNpcRepository.setNickname(nickname) 46 | 47 | 48 | fun randomizeRace() { 49 | val race = npcDataGenerator.generateRace() 50 | temporaryRandomNpcRepository.setRace(context.getString(race.nameResource)) 51 | } 52 | 53 | fun setRace(race: String) = temporaryRandomNpcRepository.setRace(race) 54 | 55 | fun randomizeAge() { 56 | val age = npcDataGenerator.generateAge() 57 | if(age.isNotSameGroupAsCurrentAge()) { 58 | randomizeProfession() // It's possible that the generated age has an incompatible profession 59 | } 60 | setAge(context.getString(age.nameResource)) 61 | } 62 | 63 | private fun Age.isNotSameGroupAsCurrentAge(): Boolean { 64 | return when { 65 | currentAgeIsChild() && this != Age.Child -> true 66 | !currentAgeIsChild() && this == Age.Child -> true 67 | else -> false 68 | } 69 | } 70 | 71 | fun setAge(age: String) = temporaryRandomNpcRepository.setAge(age) 72 | 73 | fun randomizeGender() { 74 | val gender = npcDataGenerator.generateGender() 75 | setGender(context.getString(gender.nameResource)) 76 | } 77 | 78 | fun setGender(gender: String) = temporaryRandomNpcRepository.setGender(gender) 79 | 80 | fun randomizeProfession() { 81 | val profession = if(currentAgeIsChild()) { 82 | npcDataGenerator.generateProfession(Age.Child) 83 | } else { 84 | npcDataGenerator.generateProfession(Age.Adult) 85 | } 86 | setProfession(profession) 87 | } 88 | 89 | private fun currentAgeIsChild(): Boolean { 90 | val childResource = context.getString(Age.Child.nameResource) 91 | return data.value?.age.equals(childResource, true) 92 | } 93 | 94 | fun setProfession(profession: String) = temporaryRandomNpcRepository.setProfession(profession) 95 | 96 | fun randomizeSexuality() { 97 | val sexuality = npcDataGenerator.generateSexuality() 98 | setSexuality(context.getString(sexuality.nameResource)) 99 | } 100 | 101 | fun setSexuality(sexuality: String) = temporaryRandomNpcRepository.setSexuality(sexuality) 102 | 103 | fun randomizeAlignment() { 104 | val alignment = npcDataGenerator.generateAlignment() 105 | setAlignment(context.getString(alignment.nameResource)) 106 | } 107 | 108 | fun setAlignment(alignment: String) = temporaryRandomNpcRepository.setAlignment(alignment) 109 | 110 | fun randomizeMotivation() { 111 | val motivation = npcDataGenerator.generateMotivation() 112 | setMotivation(motivation) 113 | } 114 | 115 | fun setMotivation(motivation: String) = temporaryRandomNpcRepository.setMotivation(motivation) 116 | 117 | fun randomizeLanguage(index: Int) { 118 | val languages = Language.values().map { context.getString(it.nameResource) } 119 | val newRandomLanguages = languages.filter { it !in (data.value?.languages ?: emptyList()) } 120 | 121 | val randomLanguage = if (newRandomLanguages.isEmpty()) languages.first() else newRandomLanguages.random() 122 | 123 | setLanguage(index, randomLanguage) 124 | } 125 | 126 | fun setLanguage(index: Int, language: String) { 127 | temporaryRandomNpcRepository.setLanguage(index, language) 128 | } 129 | 130 | fun removeLanguage(index: Int) = temporaryRandomNpcRepository.removeLanguage(index) 131 | 132 | fun randomizePersonality(index: Int) { 133 | val personality = npcDataGenerator.generatePersonalityTrait() 134 | setPersonality(index, personality) 135 | } 136 | 137 | fun setPersonality(index: Int, personality: String) = 138 | temporaryRandomNpcRepository.setPersonality(index, personality) 139 | 140 | fun removePersonality(index: Int) = temporaryRandomNpcRepository.removePersonality(index) 141 | 142 | fun randomizeAll() { 143 | val randomNpc = completeNpcGenerator.generate() 144 | temporaryRandomNpcRepository.setNpc(randomNpc.toNpcData()) 145 | } 146 | 147 | private fun GeneratedNpc.toNpcData(): GeneratedNpcData { 148 | return GeneratedNpcData( 149 | name, 150 | nickname, 151 | context.getString(gender.nameResource), 152 | context.getString(sexuality.nameResource), 153 | context.getString(race.nameResource), 154 | context.getString(age.nameResource), 155 | profession, 156 | motivation, 157 | context.getString(alignment.nameResource), 158 | personalityTraits.toMutableList(), 159 | languages.map { context.getString(it.nameResource) }.toMutableList() 160 | ) 161 | } 162 | 163 | fun saveCurrentNpc() { 164 | val npcToPersist = data.value!!.toEntity() 165 | viewModelScope.launch(Dispatchers.IO) { 166 | npcRepository.put(npcToPersist) 167 | } 168 | } 169 | 170 | private fun GeneratedNpcData.toEntity() = 171 | NpcEntity( 172 | this.name, 173 | this.nickname, 174 | this.gender, 175 | this.sexuality, 176 | this.race, 177 | this.age, 178 | this.profession, 179 | this.motivation, 180 | this.alignment, 181 | this.personalityTraits, 182 | this.languages, 183 | imagePath = null 184 | ) 185 | } 186 | -------------------------------------------------------------------------------- /app/src/main/java/me/kerooker/rpgnpcgenerator/viewmodel/settings/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package me.kerooker.rpgnpcgenerator.viewmodel.settings 2 | 3 | import androidx.lifecycle.ViewModel 4 | import me.kerooker.rpgnpcgenerator.viewmodel.admob.MyBuildConfig 5 | 6 | class SettingsViewModel : ViewModel() { 7 | 8 | fun isPro() = "pro" in MyBuildConfig.APPLICATION_ID 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/res/anim/rotate_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/color/mynpcs_individual_edit_text_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dice_192_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/drawable/dice_192_192.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/github_32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/drawable/github_32px.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_circle_primary_color_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bug_report_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cancel_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_library_book.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_remove_circle_red_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_save_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/portrait_placeholder.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/simple_black_border.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 20 | 21 | 30 | 31 | 32 | 37 | 38 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/mynpcs_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/mynpcs_individual_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | 21 | 22 | 29 | 30 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/mynpcs_individual_list_element.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 16 | 17 | 26 | 27 | 28 | 29 | 30 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 52 | 53 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/layout/randomnpc_element_list_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 19 | 20 | 27 | 28 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/randomnpc_element_list_view_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 19 | 20 | 21 | 22 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/randomnpc_element_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 21 | 22 | 23 | 24 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main_bottom_nav.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 16 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/menu/mynpcs_individual_editmode_item_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/menu/mynpcs_individual_viewmode_item_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/menu/mynpcs_list_item_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/menu/randomnpc_fragment_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoColman/rpg-npc-generator/b79be046d1dfe67d6aa3b26cda9999852d1a79c7/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 18 | 25 | 26 | 31 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/raw-pt/npc_child_professions.txt: -------------------------------------------------------------------------------- 1 | Adestrador de Animais 2 | Agricultor 3 | Apanhador de Ratos 4 | Aprendiz do Xamã 5 | Assistente de Alfaiate 6 | Assistente de Açougueiro 7 | Assistente de Cozinheiro 8 | Assistente de Fabricante de Chapéus 9 | Assistente de Fazendeiro de Frango 10 | Assistente de Ferreiro 11 | Assistente de Jardineiro 12 | Assistente de Padeiro 13 | Assistente de Pastor 14 | Assistente de Pescador 15 | Assistente de Pescador (Água doce) 16 | Assistente de Pescador (Água Salgada) 17 | Assistente de Tecelão 18 | Assistente do Bardo 19 | Assistente do Bibliotecário 20 | Assistente do Fabricante de Cordas 21 | Assistente do Fabricante de Tapetes 22 | Assistente do Fabricante de Tortas 23 | Assistente do Fazedor de Cestas 24 | Assistente do Fazedor de Flautas 25 | Assistente do Fazedor de Redes 26 | Assistente do Flautista 27 | Assistente do Herbalista 28 | Assistente do Ladrão 29 | Assistente do Monge 30 | Atendente 31 | Ator 32 | Batedor de Carteiras 33 | Bobo da Corte 34 | Catador de Minhoca 35 | Catador Esterco 36 | Empacotador 37 | Engraxate 38 | Escravo 39 | Fazendeiro 40 | Fazendeiro de Porco 41 | Fruticultor 42 | Idiota da Vila 43 | Malabarista 44 | Mendigo 45 | Mensageiro 46 | Mineiro 47 | Mágico de Rua 48 | Músico 49 | O Filho de Nobre 50 | Palhaço 51 | Pintor 52 | Pintor de Vidro 53 | Servo 54 | Tocador de Sino 55 | Transportador de Água 56 | Treinador de Cachorro 57 | Vagabundo 58 | Varredor de Rua 59 | Vendedor de Livros 60 | Vendedor de Água 61 | Órfão -------------------------------------------------------------------------------- /app/src/main/res/raw-pt/npc_professions.txt: -------------------------------------------------------------------------------- 1 | Abatedor de Animais 2 | Acadêmico 3 | Acendedor de Lâmpadas 4 | Advogado 5 | Agiota 6 | Agricultor 7 | Agricultor de Ervas 8 | Ajudante de Cozinha 9 | Alambrador 10 | Alfaiate 11 | Alquimista 12 | Apanhador de Ratos 13 | Apicultor 14 | Apostador 15 | Arauto 16 | Arcebispo 17 | Aristocrata 18 | Armeiro 19 | Arqueiro 20 | Arquiteto 21 | Arrowsmith 22 | Artesão 23 | Artista 24 | Artista de Rua 25 | Assaltante 26 | Assassino 27 | Assentador de Tijolos 28 | Astrólogo 29 | Atendente 30 | Ator 31 | Açogueiro de Carne 32 | Açougueiro 33 | Açougueiro de Frango 34 | Bandido 35 | Banqueiro 36 | Barbeiro 37 | Bardo 38 | Barman 39 | Barqueiro 40 | Batedor de Carteiras 41 | Baterista 42 | Beguina 43 | Besteiro 44 | Bibliotecário 45 | Bispo 46 | Bispo Metropolitano 47 | Bobo da Corte 48 | Bombeiro 49 | Boticário 50 | Cafetão 51 | Calígrafo 52 | Camponês 53 | Cantor 54 | Capitão 55 | Capitão da Guarda 56 | Capitão do Mar 57 | Carcereiro 58 | Carpinteiro 59 | Carrasco 60 | Carreteiro de Esterco 61 | Carteiro 62 | Cartógrafo 63 | Cavaleiro 64 | Caçador 65 | Caçador de Pássaros 66 | Caçador Ilegal 67 | Cervejeiro 68 | Chapeleiro 69 | Charlatão 70 | Chaveiro 71 | Cirurgião 72 | Coletor de Esterco 73 | Coletor de Impostos 74 | Colhedor de Algas 75 | Colportor 76 | Comerciante 77 | Comerciante de Feno 78 | Comerciante de Madeira 79 | Comerciante de Pimentas 80 | Comerciante de Tecidos 81 | Comerciante de Vinhos 82 | Comerciante de Água 83 | Comerciante de Óleo 84 | Comerciante de Ópio 85 | Comissário de Bordo 86 | Compositor 87 | Construtor 88 | Construtor Naval 89 | Contador 90 | Contador de Histórias 91 | Contrabandista 92 | Convocador (Lei) 93 | Copeiro 94 | Copista 95 | Cortador de Gelo 96 | Cortador de Pedra 97 | Cortador de Penas 98 | Cortador de Turfa 99 | Cortesã 100 | Costureira 101 | Coureiro 102 | Coveiro 103 | Cozinheiro 104 | Cozinheiro de Campo 105 | Criador de Cães 106 | Criador de Frango 107 | Criador de Fungos 108 | Crítico Gastronômico 109 | Cuidador de Cães de Guarda 110 | Cultista 111 | Cultivador de Minhocas 112 | Curandeiro 113 | dançarina 114 | dentista 115 | detetive 116 | Diplomata 117 | Diretor da Escola 118 | Diretor da Prisão 119 | dono de Pousada 120 | dono de Taverna 121 | doutor 122 | Empacotador 123 | Encanador 124 | Encantador de Cavalos 125 | Encarregado da Caça 126 | Enfermeira 127 | Engenheiro de Cerco 128 | Engraxate 129 | Entalhador 130 | Entalhador de Pedra 131 | Eremita 132 | Escavador 133 | Escoteiro 134 | Escravo 135 | Escritor 136 | Escriturário 137 | Escrivão 138 | Escudeiro 139 | Escultor 140 | Escultor de Cristais 141 | Espião 142 | Estivador 143 | Estudioso 144 | Fabricante de Alaúde 145 | Fabricante de Arcos 146 | Fabricante de Armaduras 147 | Fabricante de Armas 148 | Fabricante de Bolsa 149 | Fabricante de Botões 150 | Fabricante de Cerveja 151 | Fabricante de Cestas 152 | Fabricante de Chapéus 153 | Fabricante de Cola 154 | Fabricante de Cordas 155 | Fabricante de Correio 156 | Fabricante de Correntes 157 | Fabricante de Drogas 158 | Fabricante de Espadas 159 | Fabricante de Facas 160 | Fabricante de Feltro 161 | Fabricante de Gabinetes 162 | Fabricante de Lanternas 163 | Fabricante de Lâmpadas 164 | Fabricante de Mapas 165 | Fabricante de Moedas 166 | Fabricante de Móveis 167 | Fabricante de Papel 168 | Fabricante de Pergaminho 169 | Fabricante de Queijos 170 | Fabricante de Redes 171 | Fabricante de Relógios 172 | Fabricante de Sabão 173 | Fabricante de Sapatos 174 | Fabricante de Sinos 175 | Fabricante de Tapetes 176 | Fabricante de Tonéis 177 | Fabricante de Torta 178 | Fabricante de Vassouras 179 | Fabricante de Velas 180 | Fabricante de Velas Marítimas 181 | Fabricante de Vestidos 182 | Fabricante de Óleo 183 | Fabricantes de Flechas 184 | Falcoeiro 185 | Falso Profeta 186 | Faz Tudo 187 | Fazendeiro 188 | Fazendeiro de Flores 189 | Fazendeiro de Gongo 190 | Fazendeiro de Porcos 191 | Fazendeiro de Vegetais 192 | Ferrador 193 | Ferreiro 194 | Filho de Nobre 195 | Filósofo 196 | Flagelante 197 | Flautista 198 | Fofoqueiro 199 | Foragido 200 | Fornecedor de Navios 201 | Fruticultor 202 | Fundidor 203 | Funileiro 204 | Furtador 205 | Físico 206 | Gravador 207 | Guarda 208 | Guarda da Caravana 209 | Guarda da Cidade 210 | Guarda Florestal 211 | Guarda Costas 212 | Guardador de Porcos 213 | Guia 214 | Herborista 215 | Idiota da Vila 216 | Illustrador 217 | Iluminador 218 | Jardineiro 219 | Joalheiro 220 | Ladrão 221 | Ladrão de Tapetes 222 | Ladrão de Túmulos 223 | Lanceiro 224 | Lapidador de Gemas 225 | Lapidário 226 | Leiteira 227 | Lenhador 228 | Limpa Chaminés 229 | Limpador de Calha 230 | Limpador de Cavalos 231 | Limpador de Estábulos 232 | Limpador de Tecidos 233 | Malabarista 234 | Manicure 235 | Marceneiro 236 | Marinheiro 237 | Marionetista 238 | Matemático 239 | Mendigo 240 | Menestrel 241 | Mensageiro 242 | Mercador 243 | Mercenário 244 | Mestre da Guilda 245 | Mestre de Cães 246 | Miliciano 247 | Mineiro 248 | Moleiro 249 | Monge 250 | Mordomo 251 | Mágico de Rua 252 | Médico da Peste 253 | Músico 254 | Músico Profissional 255 | Navegador 256 | Negociante de Peles 257 | Negociante de Roupas Velhas 258 | Nobre 259 | Oficial de Justiça 260 | Oleiro 261 | Operador de Fornalha 262 | Ourives 263 | Ouriço 264 | Padeiro 265 | Palhaço 266 | Parteira 267 | Pastor 268 | Pastor de Ovelhas 269 | Pedreiro 270 | Peleiro 271 | Penhorista 272 | Peregrino 273 | Pescador 274 | Pescador (Água doce) 275 | Pescador (Água Salgada) 276 | Pescador de Camarões 277 | Pintor 278 | Pintor de Vidros 279 | Piqueiro 280 | Pirata 281 | Poeta 282 | Portador de Incenso 283 | Pregoeiro da Cidade 284 | Procurador 285 | Produtor de Leite 286 | Produtor de Sal 287 | Professor 288 | Prostituta 289 | Provador da Comida Real 290 | Queijeiro 291 | Rapaz da Estrebaria 292 | Raspador de Ostras 293 | Sacerdote 294 | Sanguessuga 295 | Sapateiro 296 | Saqueador 297 | Segurança 298 | Seleiro 299 | Senhor da Terra 300 | Sequestrador 301 | Seringueiro 302 | Servo 303 | Sinecura 304 | Soldado 305 | Soldado de Lança 306 | Tanoeiro 307 | Tapeceiro 308 | Taxidermista 309 | Tecelão 310 | Teólogo 311 | Tintureiro 312 | Tocador de Sinos 313 | Torturador 314 | Tosquiador de Ovelhas 315 | Traficante de Escravos 316 | Transportador de Água 317 | Treinador de Animais 318 | Treinador de Cavalos 319 | Treinador de Cães 320 | Trovador 321 | Vadio 322 | Vagabundo 323 | Vaginarius 324 | Vagão 325 | Varredor de Rua 326 | Veleiro 327 | Vendedor Ambulante 328 | Vendedor de Arcos 329 | Vendedor de Cerveja 330 | Vendedor de Drogas 331 | Vendedor de Frutas 332 | Vendedor de Indulgências 333 | Vendedor de Livros 334 | Vendedor de Torta 335 | Vendedor de Vidros 336 | Verdureiro 337 | Veterinário 338 | Viagrista 339 | Vidente 340 | Vidreiro 341 | Vigia 342 | Vigia Noturno 343 | Violinista 344 | Viticultor 345 | Xamã 346 | Órfão -------------------------------------------------------------------------------- /app/src/main/res/raw/npc_child_professions.txt: -------------------------------------------------------------------------------- 1 | Actor 2 | Animal Trainer 3 | Armorsmith's Assistant 4 | Arrowsmith's Assistant 5 | Attendent 6 | Bagger 7 | Baker's Assistant 8 | Bandit's Assistant 9 | Bard's Assistant 10 | Basketmaker's Assistant 11 | Beggar 12 | Bellringer 13 | Blacksmith's Assistant 14 | Book seller 15 | Chicken Farmer's Assistant 16 | Clown 17 | Cook's Assistant 18 | Crop farmer 19 | Currier 20 | Dog trainer 21 | Drummer 22 | Dung carter 23 | Dung collector 24 | Farmer's Assistant 25 | Fisherman's Assistant 26 | Fisherman's Assistant (freshwater) 27 | Fisherman's Assistant (saltwater) 28 | Fruit farmer 29 | Gardener's Assistant 30 | Glasspainter 31 | Hat maker's Assistant 32 | Herb farmer's Assistant 33 | Herbalist's Assistant 34 | Jester 35 | Juggler 36 | Librarian's Assistant 37 | Lutemaker's Assistant 38 | Meat butcher's Assistant 39 | Miner 40 | Monk's Assistant 41 | Musician 42 | Netmaker's Assistant 43 | Nobleman's son 44 | Orphan 45 | Pickpocket 46 | Pie maker's Assistant 47 | Pig Farmer 48 | Rat catcher 49 | Ropemaker's Assistant 50 | Rugmaker's Assistant 51 | Saltboiler 52 | Servant 53 | Shaman's Apprentice 54 | Sheepshearer's Assistant 55 | Shepherd's Assistant 56 | Shoe shiner 57 | Slave 58 | Street magician 59 | Street sweeper 60 | Swordsmith's Assistant 61 | Tailor's Assistant 62 | Tanner's Assistant 63 | Vagabond 64 | Vegetable Farmer 65 | Village idiot 66 | Water carrier 67 | Waterseller 68 | Weaponsmith's Assistant 69 | Worm farmer -------------------------------------------------------------------------------- /app/src/main/res/raw/npc_motivations.txt: -------------------------------------------------------------------------------- 1 | Abandon an ideology 2 | Answer a call to adventure 3 | Apologize 4 | Appease a god 5 | Appease the gods 6 | Avenge a family member 7 | Avenge a friend 8 | Avoid a person 9 | Avoid failure 10 | Avoid responsibilities 11 | Be a better person 12 | Be a hero 13 | Be a master in their field 14 | Be able to eat 15 | Be accepted by society 16 | Be admired 17 | Be amused 18 | Be better than their rival 19 | Be forgiven 20 | Be in control 21 | Be left alone 22 | Be loved 23 | Be redeemed 24 | Be remembered 25 | Be respected 26 | Be self-sufficient 27 | Be somebody else 28 | Be strong 29 | Be the best they can be 30 | Become a leader 31 | Become anonymous 32 | Become famous 33 | Become godly 34 | Become powerful 35 | Become the strongest 36 | Belong somewhere 37 | Better themselves 38 | Bored and seeking high adventure 39 | Bored and seeking tales of high adventure 40 | Break a habit 41 | Break an addiction 42 | Build their own home 43 | Cause mayhem 44 | Change a law 45 | Change the future 46 | Change the past 47 | Clear a family member's name 48 | Clear a friend's name 49 | Clear their name 50 | Complete a collection 51 | Conquer their fear 52 | Consume everything 53 | Create a safe world 54 | Create a utopia 55 | Create a work of art 56 | Cure a strange disease 57 | Destroy corruption 58 | Destroy evil 59 | Discover a new planet 60 | Do nothing 61 | Do the impossible 62 | Do the right thing 63 | Earn the title of designated champion of their overlord 64 | Eliminate evil 65 | End a war 66 | End suffering of all 67 | End the conflict 68 | End the suffering of a family member 69 | End the suffering of a friend 70 | Entertain others 71 | Escape a bad situation 72 | Escape death 73 | Escape from their current life 74 | Escape their destiny 75 | Establish their own country 76 | Expand the territories of their overlord 77 | Experience a new culture 78 | Experience something new 79 | Explore the oceans 80 | Explore the unexplored 81 | Feel like they're worth something 82 | Fight for their homeland 83 | Find a cure 84 | Find a dream job 85 | Find a job 86 | Find a legendary creature 87 | Find a lost friend 88 | Find a lost lover 89 | Find a more interesting life 90 | Find a new creative outlet 91 | Find a new home 92 | Find a new passion 93 | Find a purpose 94 | Find a purpose in life 95 | Find a thrilling life 96 | Find beauty 97 | Find excitement 98 | Find inspiration 99 | Find love 100 | Find out a secret 101 | Find out the fate of a family member 102 | Find out the fate of a friend 103 | Find out their true identity 104 | Find peace within 105 | Find romance 106 | Find their muse 107 | Find true love 108 | Fix a mistake 109 | Follow orders 110 | Forget their past 111 | Forgive somebody 112 | Free the animals 113 | Fulfill a destiny 114 | Gain the approval of somebody 115 | Gain what somebody else has 116 | Get away from their past 117 | Get rich 118 | Go on an adventure 119 | Have a passionate relationship 120 | Have fun 121 | Have justice done 122 | Have more and more 123 | Have their work recognized 124 | Have what others have 125 | Have what they can never have 126 | Have what they can't have 127 | Inflict pain and suffering wherever they go 128 | Lead a rebellion 129 | Lift a curse 130 | Live 131 | Live a quiet life 132 | Live dangerously 133 | Live forever 134 | Live in peace 135 | Make a difference 136 | Make a sacrifice for the greater good 137 | Make a scientific breakthrough 138 | Make friends 139 | Make people smile 140 | Make sure justice prevails 141 | More power 142 | Never be hurt again 143 | No longer be afraid 144 | No longer be bored 145 | Overcome a death sentence 146 | Overcome a disability 147 | Overcome mockery from the past 148 | Overcome stress 149 | Overthrow the government 150 | Protect a family member 151 | Protect a friend 152 | Protect nature 153 | Protect the innocent 154 | Protect the peace 155 | Protect the planet 156 | Protect their business 157 | Protect their family 158 | Protect their home 159 | Protect their honor 160 | Prove a theory 161 | Prove them wrong 162 | Prove themselves worthy of the family name 163 | Reach perfection 164 | Reach the promised lands 165 | Reconcile with a person 166 | Redeem somebody 167 | Regain their honor 168 | Remain hidden 169 | Repay a debt 170 | Repay a life debt 171 | Resolve their guilt 172 | Restart the world 173 | Restore their family's fortune as they are known to be destitute 174 | Restore their family’s fortune as they are secretly destitute 175 | Restore their family’s good name after a public scandal involving a public schism within the family 176 | Restore their family’s good name after a public scandal involving a suspicious death in the family 177 | Restore their family’s good name after a public scandal involving an ill omen 178 | Restore their family’s good name after a public scandal involving betraying their overlord 179 | Restore their family’s good name after a public scandal involving lost heirloom 180 | Restore their family’s good name after a public scandal involving rumours of infernal dealings 181 | Restore their name after a public scandal involving a divorce 182 | Restore their name after a public scandal involving a public criminal trial 183 | Restore their name after a public scandal involving an act of cowardice 184 | Restore their name after a public scandal involving an affair 185 | Restore their name after a public scandal involving failing to show for a duel 186 | Restore their name after a public scandal involving losing a duel 187 | Restore their name after a public scandal involving losing their fortune to gambling 188 | Restore their name after a public scandal involving spending their fortune in bawdy houses 189 | Retrieve a family heirloom and return it to their manor 190 | Retrieve a lost item 191 | Retrieve a stolen item 192 | Retrieve their lover from death 193 | Retrieve their lover from exile 194 | Retrieve their lover from imprisonment by their own parents 195 | Retrieve their lover from slavers 196 | Retrieve their lover from the arms of another 197 | Retrieve their lover from the lair of a monster 198 | Reunite with a family member 199 | Reunite with a lost friend 200 | Revenge 201 | Rid the world of evil 202 | Rule the city 203 | Run for the borders 204 | Satisfy their curiosity 205 | Save a deity 206 | See others suffer 207 | See the gods pay for their crimes 208 | See the world 209 | Seeking riches for their own sake 210 | Seeking riches to buy land for themselves 211 | Seeking their long-lost twin who was captured by slavers as a child 212 | Seeking their long-lost twin who was lost at sea but rumoured to have been spotted this past month at port 213 | Seeking their long-lost twin who was taken by witches at birth as part of a deal with their parents 214 | Seeking their long-lost twin who was wandered into the misty woods as a child 215 | Seeking to make a name for themselves 216 | Slaying a bloodthirsty demon 217 | Slaying a celestial being traveling the world in disguise 218 | Slaying a fell beast 219 | Slaying a legendary dragon 220 | Slaying a powerful necromancer 221 | Slaying a secretive witch 222 | Slaying a terrible giant 223 | Slaying a trickster devil 224 | Slaying an aberration from beyond 225 | Slaying Orcs in their hordes 226 | Solve a mystery 227 | Solve an ancient mystery 228 | Spread chaos 229 | Spread joy 230 | Spread their ideology 231 | Stand out from the crowd 232 | Start a business 233 | Start a family 234 | Start a new world 235 | Stop a criminal 236 | Take a new direction in life 237 | Thwart destiny 238 | To fit in 239 | Travel back into the past 240 | Travel in space 241 | Travel into the future 242 | Uncover a secret plot 243 | Unique foods and wines from all over the land 244 | Vengeance against the man or woman who killed their lover 245 | Vengeance against the man or woman who killed their mother and/or father 246 | Vengeance against the man or woman who killed their older sibling 247 | Vengeance against the man or woman who killed their son or daughter 248 | Vengeance against the man or woman who killed their Twin 249 | Vengeance against the man or woman who killed their younger sibling 250 | Win a competition 251 | Win a game 252 | Write a book -------------------------------------------------------------------------------- /app/src/main/res/raw/npc_professions.txt: -------------------------------------------------------------------------------- 1 | Academic 2 | Accountant 3 | Actor 4 | Alchemist 5 | Animal Trainer 6 | Apothecary 7 | Archbishop 8 | Archer 9 | Architect 10 | Aristocrat 11 | Armorer 12 | Armorsmith 13 | Arrowsmith 14 | Artisan 15 | Artist 16 | Assassin 17 | Astrologer 18 | Atilliator 19 | Attendent 20 | Bagger 21 | Bailiff 22 | Baker 23 | Bandit 24 | Banker 25 | Barbier 26 | Bard 27 | Bartender 28 | Basketmaker 29 | Beachcomber 30 | Beekeeper 31 | Beer seller 32 | Beerbrewer 33 | Beggar 34 | Beguine 35 | Bellfounder 36 | Bellmaker 37 | Bellringer 38 | Besom maker 39 | Bishop 40 | Blacksmith 41 | Bloodletter 42 | Boatman 43 | Bodger 44 | Bodyguard 45 | Bonecarver 46 | Book seller 47 | Bouncer 48 | Bowman 49 | Bowyer 50 | Brewer 51 | Bricklayer 52 | Buffoon 53 | Builder 54 | Burglar 55 | Busker 56 | Butcher 57 | Butler 58 | Buttonmaker 59 | Cabinetmaker 60 | Calligrapher 61 | Camp cook 62 | Camp follower 63 | Candlestick maker 64 | Captain 65 | Captain of the guard 66 | Caravan guard 67 | Carpenter 68 | Cartographer 69 | Carver 70 | Chainmaker 71 | Chalk cutter 72 | Chapman 73 | Charcoal burner 74 | Charlatan 75 | Cheesemaker 76 | Cheesemonger 77 | Chicken butcher 78 | Chicken Farmer 79 | Chimney sweep 80 | City guardsman 81 | Clark 82 | Clerk 83 | Clockmaker 84 | Clothier 85 | Clown 86 | Cobbler 87 | Coiner 88 | Colporteur 89 | Composer 90 | Cook 91 | Cooper 92 | Coppersmith 93 | Copyist 94 | Cordwainer 95 | Courtesan 96 | Crop farmer 97 | Crossbowman 98 | Crystal carver 99 | Cultist 100 | Cupbearer 101 | Curate 102 | Currier 103 | Cutpurse 104 | Dairy farmer 105 | Dancer 106 | Dentist 107 | Detective 108 | Diplomat 109 | Ditch digger 110 | Doctor 111 | Dog breeder 112 | Dog trainer 113 | Dresser 114 | Drug dealer 115 | Drug farmer 116 | Drummer 117 | Dung carter 118 | Dung collector 119 | Dyer 120 | Engraver 121 | Executioner 122 | Faith healer 123 | Falconer 124 | False prophet 125 | Farmer 126 | Farrier 127 | Feltmaker 128 | Fence 129 | Fewtrer 130 | Fiddler 131 | Fireman 132 | Fisherman 133 | Fisherman (freshwater) 134 | Fisherman (saltwater) 135 | Flagellant 136 | Fletcher 137 | Flower farmer 138 | Food critic 139 | Forester 140 | Fortune teller 141 | Fowler 142 | Fruit farmer 143 | Fruiterer 144 | Fuller 145 | Fungus Farmer 146 | Furniture maker 147 | Furrier 148 | Gambler 149 | Gamekeeper 150 | Gardener 151 | Gardner 152 | Gemcutter 153 | Glass seller 154 | Glassblower 155 | Glasspainter 156 | Gluemaker 157 | Gong farmer 158 | Gossip 159 | Grave digger 160 | Grave robber 161 | Gravedigger 162 | Greengrocer 163 | Grifter 164 | Guardsman 165 | Guide 166 | Guild master 167 | Gutter cleaner 168 | Haberdasher 169 | Handyman 170 | Hat maker 171 | Hatmaker 172 | Hatter 173 | Hawker 174 | Hay merchant 175 | Hayward 176 | Herald 177 | Herb farmer 178 | Herbalist 179 | Herder 180 | Hermit 181 | Hetheleder 182 | Horse trainer 183 | Horse whisperer 184 | Horseleech 185 | Hunter 186 | Huntsman 187 | Icecutter 188 | Illuminator 189 | Illustrator 190 | Incense bearer 191 | Innkeeper 192 | Jailer 193 | Jester 194 | Jeweler 195 | Joiner 196 | Juggler 197 | Knacker 198 | Knapper 199 | Knifeman 200 | Knifesmith 201 | Knight 202 | Lamp lighter 203 | Lampwright 204 | Lancier 205 | Landlord 206 | Lanternmaker 207 | Lapidary 208 | Lawyer 209 | Leatherworker 210 | Leech 211 | Librarian 212 | Lighterman 213 | Linkboy 214 | Locksmith 215 | Lookout 216 | Lutemaker 217 | Lutenist 218 | Mailmaker 219 | Mapmaker 220 | Mariner 221 | Mason 222 | Master of hounds 223 | Mathematician 224 | Meat butcher 225 | Mercenary 226 | Messenger 227 | Metropolitan bishop 228 | Midwife 229 | Militia 230 | Milkmaid 231 | Miller 232 | Miner 233 | Minstrel 234 | Mirrorer 235 | Moneylender 236 | Monk 237 | Mucker 238 | Musician 239 | Nailmaker 240 | Navigator 241 | Netmaker 242 | Night watchman 243 | Noble 244 | Nobleman's son 245 | Nurse 246 | Oil merchant 247 | Oilmaker 248 | Old-clothes dealer 249 | Opium merchant 250 | Orphan 251 | Ostler 252 | Outlaw 253 | Oyster raker 254 | Painter 255 | Papermaker 256 | Parchment maker 257 | Parchmenter 258 | Pardoner 259 | Pawnbroker 260 | Peasant 261 | Peat cutter 262 | Philosopher 263 | Physician 264 | Pickler 265 | Pickpocket 266 | Pie maker 267 | Pie seller 268 | Pig Farmer 269 | Pikeman 270 | Pilgrim 271 | Pimp 272 | Pirate 273 | Plague doctor 274 | Plumber 275 | Plumer 276 | Poacher 277 | Poet 278 | Postman 279 | Potter 280 | Priest 281 | Prison warden 282 | Professional musician 283 | Professor 284 | Prostitute 285 | Puppeteer 286 | Purse maker 287 | Quarryman 288 | Quill cutter 289 | Rat catcher 290 | Ropemaker 291 | Royal food taster 292 | Rugmaker 293 | Rugweaver 294 | Saddler 295 | Sail maker 296 | Sailmaker 297 | Sailor 298 | Saltboiler 299 | Salter 300 | Scholar 301 | Schoolmaster 302 | Scout 303 | Scribe 304 | Scribe 305 | Scrimshaw 306 | Scullion 307 | Sculptor 308 | Sea captain 309 | Seamstress 310 | Seaweed harvester 311 | Servant 312 | Sewerhand 313 | Shaman 314 | Sheepshearer 315 | Shepherd 316 | Shingler 317 | Ship provisioner 318 | Shipwright 319 | Shoe shiner 320 | Shoemaker 321 | Shrimper 322 | Shrubber 323 | Siege engineer 324 | Siever 325 | Silversmith 326 | Sinecure 327 | Singer 328 | Skinner 329 | Slave 330 | Slaver 331 | Smelter 332 | Smith 333 | Smuggler 334 | Soaper 335 | Soldier 336 | Solicitor 337 | Spearman 338 | Spice merchant 339 | Spy 340 | Squire 341 | Stablehand 342 | Stevedore 343 | Steward 344 | Stonecarver 345 | Stonecutter 346 | Stonemason 347 | Stoner 348 | Storyteller 349 | Street magician 350 | Street sweeper 351 | Stringer 352 | Summoner (law) 353 | Surgeon 354 | Swineherd 355 | Swordsmith 356 | Tailor 357 | Tanner 358 | Tapestrymaker 359 | Taverner 360 | Tax collector 361 | Taxidermist 362 | Teacher 363 | Thatcher 364 | Theologian 365 | Thief 366 | Thug 367 | Tinker 368 | Torturer 369 | Town crier 370 | Trader 371 | Trapper 372 | Troubadour 373 | Urchin 374 | Vagabond 375 | Vaginarius 376 | Vagrant 377 | Vegetable Farmer 378 | Veterinarian 379 | Village idiot 380 | Vintner 381 | Wagoner 382 | Wainwright 383 | Water carrier 384 | Waterman 385 | Waterseller 386 | Weaponsmith 387 | Weaver 388 | Window tapper 389 | Wine seller 390 | Wood seller 391 | Woodcarver 392 | Woodcutter 393 | Worm farmer 394 | Writer 395 | Zealot -------------------------------------------------------------------------------- /app/src/main/res/values-pt/individual_npc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Raça 4 | Idade 5 | Gênero 6 | Profissão 7 | Sexualidade 8 | Tendência 9 | Motivação 10 | Idiomas 11 | Idioma 12 | Traços de Personalidade 13 | Traço de Personalidade 14 | Adicionar Item 15 | 16 | 17 | 18 | Editar 19 | Salvar 20 | Cancelar 21 | Anotações 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt/mainactivity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Remover anúncios 4 | Para remover anúncios escolha uma opção abaixo: 5 | Parar por um dia (assistir anúncio) 6 | Comprar PRO 7 | Agora não 8 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt/mynpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Deletar 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt/npc_enums.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Criança 6 | Adolescente 7 | Jovem Adulto 8 | Adulto 9 | Velho 10 | Muito Velho 11 | 12 | 13 | Leal e Bom 14 | Leal e Neutro 15 | Leal e Mau 16 | Neutro e Bom 17 | Neutro 18 | Neutro e Mau 19 | Caótico e Bom 20 | Caótico e Neutro 21 | Caótico e Mau 22 | 23 | 24 | Masculino 25 | Feminino 26 | 27 | 28 | Homossexual 29 | Bissexual 30 | Assexual 31 | Heterossexual 32 | 33 | 34 | Comum 35 | Élfico 36 | Gnômico 37 | Goblin 38 | Halfling 39 | Orc 40 | Celestial 41 | Abissal 42 | Infernal 43 | Silvestre 44 | Dracônico 45 | Anão 46 | Gigante 47 | 48 | 49 | Anão da Colina 50 | Anão da Montanha 51 | Alto Elfo 52 | Elfo da Floresta 53 | Elfo Negro (Drow) 54 | Halfling Robusto 55 | Halfing Pés-Leves 56 | Humano 57 | Draconato 58 | Gnomo da Floresta 59 | Gnomo das Rochas 60 | Meio-Elfo 61 | Meio-Orc 62 | Tiefling 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt/random_npc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nome Completo 5 | Apelido 6 | Raça 7 | Idade 8 | Gênero 9 | Profissão 10 | Sexualidade 11 | Tendência 12 | Motivação 13 | Idiomas 14 | Idioma 15 | Traços de Personalidade 16 | Traço de Personalidade 17 | Adicionar Item 18 | 19 | Tudo Aleatório 20 | Salvar 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sobre 5 | Bibliotecas Open Source 6 | Lista de todas as bibliotecas open source que foram utilizadas neste aplicativo. 7 | 8 | Repositório do projeto 9 | Este projeto é 100% open source, e seu código fonte está inteiramente disponível. Estamos abertos à contribuições e dispostos a melhorar! 10 | 11 | Denunciar um erro 12 | Achou um comportamento estranho? Algo parece fora do lugar? Por favor, denuncie o erro e nos ajude a consertar! 13 | 14 | Compre PRO 15 | Habilitando PRO 16 | Habilitando o PRO retira todos os anúncios e ajuda o desenvolvimenteo do aplicativo. Não há nenhuma outra funcionalidade exclusivamente premium. 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | @string/app_name_pt 3 | 4 | Aleatório 5 | NPCs 6 | Ajustes 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #24A5F1 4 | #0374B6 5 | #D81B60 6 | #ffffffff 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/individual_npc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Race 4 | Age 5 | Gender 6 | Profession 7 | Sexuality 8 | Alignment 9 | Motivation 10 | Languages 11 | Language 12 | Personality Traits 13 | Personality 14 | Add Item 15 | 16 | 17 | 18 | Edit 19 | Save 20 | Cancel 21 | Notes 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/mainactivity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Remove ads 4 | To remove advertisements, choose an option below: 5 | Stop for a day (watch ad) 6 | Go PRO 7 | Not now 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/mynpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Delete 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/npc_enums.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Child 6 | Teenager 7 | Young Adult 8 | Adult 9 | Old 10 | Very Old 11 | 12 | 13 | Lawful Good 14 | Lawful Neutral 15 | Lawful Evil 16 | Neutral Good 17 | Neutral 18 | Neutral Evil 19 | Chaotic Good 20 | Chaotic Neutral 21 | Chaotic Evil 22 | 23 | 24 | Male 25 | Female 26 | 27 | 28 | Homosexual 29 | Bisexual 30 | Asexual 31 | Heterosexual 32 | 33 | 34 | Common 35 | Elvish 36 | Gnomish 37 | Goblin 38 | Halfling 39 | Orc 40 | Celestial 41 | Abyssal 42 | Infernal 43 | Druidic 44 | Draconic 45 | Dwarvish 46 | Giant 47 | 48 | 49 | Hill Dwarf 50 | Mountain Dwarf 51 | High Elf 52 | Wood Elf 53 | Drow 54 | Stout Halfling 55 | Lightfoot Halfling 56 | Human 57 | Dragonborn 58 | Forest Gnome 59 | Rock Gnome 60 | Half-Elf 61 | Half-Orc 62 | Tiefling 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/values/random_npc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Full Name 5 | Nickname 6 | Race 7 | Age 8 | Gender 9 | Profession 10 | Sexuality 11 | Alignment 12 | Motivation 13 | Languages 14 | Language 15 | Personality Traits 16 | Personality Trait 17 | Add Item 18 | 19 | Randomize All 20 | Save 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/values/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | About 5 | Open Source Libraries 6 | List of all the open source libraries that were used in this app 7 | 8 | Project Repository 9 | This project is 100% open source, and it\'s source code is available in its entirety. We are open to contributions and eager to improve! 10 | 11 | Report a bug 12 | Found a strange behavior? Something isn\'t working as it should? Please let us know but submitting a bug report. 13 | 14 | Go PRO 15 | Enabling PRO 16 | Enabling PRO removes advertisements and supports the app development. There is no pro-exclusive features 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | @string/app_name_en 3 | 4 | Randomize 5 | NPCs 6 | Settings 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 16 |