├── .gitignore ├── DEVELOPERS.md ├── README.md ├── auto-updater.js ├── build ├── background.png ├── icon.icns ├── icon.ico └── icon.png ├── css ├── common.css ├── main-window.css └── welcome-window.css ├── data ├── values-test.txt └── values.txt ├── images ├── add-character-plus.svg ├── favorite-selected.svg ├── favorite-unselected.svg ├── icons.ai └── menu-indicator.svg ├── js ├── battle-pairer.js ├── battle-timekeeper.js ├── database-init.js ├── prefs.js ├── utils.js └── views │ ├── background-view.js │ ├── character-comparison-base-view.js │ ├── character-comparison-conflict-view.js │ ├── character-comparison-value-difference-view.js │ ├── character-comparison-view.js │ ├── character-favorites-view.js │ ├── character-selector-multiple.js │ ├── character-selector-single.js │ ├── character-trainer-view.js │ ├── characters-view.js │ ├── common-ground-view.js │ ├── favorite-button.js │ ├── internal-conflict-view.js │ ├── main-base-view.js │ ├── main-view-selector.js │ ├── main-window.js │ ├── value-list-view.js │ └── welcome-window.js ├── loading-status.html ├── main-window.html ├── main.js ├── migrations ├── 001-setup.js └── 002-favorites.js ├── package-lock.json ├── package.json ├── update.html └── welcome-window.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # VSCode directory 61 | .vscode 62 | 63 | # Database file. 64 | characterizer.sqlite 65 | 66 | # built app 67 | dist/ 68 | 69 | yarn.lock 70 | -------------------------------------------------------------------------------- /DEVELOPERS.md: -------------------------------------------------------------------------------- 1 | ### Building for Linux and Windows with Docker for Mac 2 | 3 | Install [Docker for Mac](https://www.docker.com/docker-mac), then: 4 | 5 | ``` 6 | git clone git@github.com:wonderunit/characterizer.git characterizer 7 | docker run --rm -ti -v ${PWD}:/project -v ${PWD##*/}-node-modules:/project/node_modules -v ~/.electron:/root/.electron electronuserland/electron-builder:wine 8 | cd characterizer 9 | npm install 10 | npm prune 11 | npm run dist:win 12 | ``` 13 | 14 | 15 | ## Testing Auto-Update 16 | 17 | - `latest.yml` (for Windows) and `latest-mac.yml` (for Mac) must be published in the GitHub Release along with the other release files. 18 | 19 | To test auto update, create a file called `dev-app-update.yml` in the root source folder with contents like: 20 | 21 | ``` 22 | owner: wonderunit 23 | repo: storyboarder 24 | provider: github 25 | ``` 26 | 27 | ... then decrement the current version in `package.json`. You will be notified that the app is out-of-date (although in dev mode, when unsigned, Squirrel.Mac will throw `Error: Could not get code signature for running application`) 28 | 29 | ## Publishing 30 | 31 | Be sure to have a local `GH_TOKEN` environment variable. For the value: 32 | 33 | Go to: https://github.com/settings/tokens/new 34 | Token description: Storyboarder Publishing 35 | Select scopes: [x] public_repo 36 | 37 | Create a Draft in GitHub Releases as the target for publishing 38 | 39 | Then, publish: 40 | 41 | GH_TOKEN={...} npm run dist:mac -- --publish onTagOrDraft 42 | GH_TOKEN={...} npm run dist:win -- --publish onTagOrDraft 43 | GH_TOKEN={...} npm run dist:linux -- --publish onTagOrDraft -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # characterizer 2 | The best app to train character values and generate conflicts, dilemmas, and common ground between characters. 3 | -------------------------------------------------------------------------------- /auto-updater.js: -------------------------------------------------------------------------------- 1 | // REFERENCE 2 | // https://github.com/iffy/electron-updater-example/blob/master/main.js 3 | // https://github.com/wulkano/kap/blob/b326a5a398affb3652650ddc70d3a95724e755db/app/src/main/auto-updater.js 4 | 5 | const { BrowserWindow, dialog, app } = electron = require('electron') 6 | 7 | const autoUpdater = require('electron-updater').autoUpdater 8 | 9 | const init = () => { 10 | // autoUpdater.on('checking-for-update', () => { 11 | // dialog.showMessageBox(null, { message: 'Checking for update...' }) 12 | // }) 13 | autoUpdater.on('update-available', (ev, info) => { 14 | dialog.showMessageBox( 15 | null, 16 | { 17 | type: 'question', 18 | message: `An update is available to version ${ev.version}. Update now? There will be a short delay while we download the update and install it for you.`, 19 | buttons: ['Later', 'Download and Install Now'] 20 | }, 21 | index => { 22 | if (index) { 23 | // On Windows, this causes an error. Skipping for now. 24 | // BrowserWindow.getAllWindows().forEach(w => w.close()) 25 | 26 | let win 27 | win = new BrowserWindow({ 28 | width: 600, 29 | height: 720, 30 | show: false, 31 | center: true, 32 | resizable: false, 33 | backgroundColor: '#E5E5E5', 34 | webPreferences: { 35 | devTools: true 36 | } 37 | }) 38 | win.on('closed', () => { 39 | win = null 40 | }) 41 | win.loadURL(`file://${__dirname}/update.html`) 42 | win.once('ready-to-show', () => { 43 | win.webContents.send('release-notes', ev.releaseNotes) 44 | win.show() 45 | }) 46 | 47 | autoUpdater.on('download-progress', (progressObj) => { 48 | win.webContents.send('progress', progressObj) 49 | }) 50 | 51 | autoUpdater.on('update-downloaded', (ev, info) => { 52 | dialog.showMessageBox(null, { message: 'Update downloaded; will install in 5 seconds' }) 53 | // Wait 5 seconds, then quit and install 54 | // In your application, you don't need to wait 5 seconds. 55 | // You could call autoUpdater.quitAndInstall(); immediately 56 | setTimeout(function() { 57 | autoUpdater.quitAndInstall() 58 | }, 5000) 59 | }) 60 | 61 | // fail gracelessly if we can't update properly 62 | autoUpdater.on('error', (ev, err) => { 63 | dialog.showMessageBox(null, { message: 'Update failed. Quitting.' }) 64 | win.close() 65 | app.quit() 66 | }) 67 | 68 | // Download and Install 69 | autoUpdater.downloadUpdate() 70 | } 71 | } 72 | ) 73 | }) 74 | // autoUpdater.on('update-not-available', (ev, info) => { 75 | // dialog.showMessageBox(null, { message: 'Update not available.' }) 76 | // }) 77 | autoUpdater.on('error', (ev, err) => { 78 | console.error('Error in auto-updater.') 79 | // dialog.showMessageBox(null, { message: 'Error in auto-updater.' }) 80 | }) 81 | 82 | autoUpdater.autoDownload = false 83 | autoUpdater.checkForUpdates() 84 | } 85 | 86 | exports.init = init 87 | -------------------------------------------------------------------------------- /build/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderunit/characterizer/3c2be13aa09534f68f5598e250e14fbe0e4b5448/build/background.png -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderunit/characterizer/3c2be13aa09534f68f5598e250e14fbe0e4b5448/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderunit/characterizer/3c2be13aa09534f68f5598e250e14fbe0e4b5448/build/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderunit/characterizer/3c2be13aa09534f68f5598e250e14fbe0e4b5448/build/icon.png -------------------------------------------------------------------------------- /css/common.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | 5 | .button { 6 | border-width: 1px; 7 | margin: 10px; 8 | padding: 20px; 9 | border-style: solid; 10 | border-color: #CCC; 11 | cursor: pointer; 12 | } 13 | 14 | .button-selected { 15 | background-color: #CCC; 16 | } 17 | 18 | .button:hover { 19 | background-color: #EEE; 20 | } 21 | 22 | .hidden { 23 | visibility: hidden; 24 | } 25 | 26 | .flex-wrap { 27 | display: flex; 28 | flex-wrap: wrap; 29 | } 30 | 31 | .align-items-center { 32 | align-items: center; 33 | } 34 | 35 | .text-align-right { 36 | text-align: right; 37 | } 38 | 39 | .text-align-center { 40 | text-align: center; 41 | } -------------------------------------------------------------------------------- /css/main-window.css: -------------------------------------------------------------------------------- 1 | /** Reset **/ 2 | h2 { 3 | -webkit-margin-before: 0; 4 | -webkit-margin-after: 0; 5 | } 6 | 7 | body { 8 | padding: 0; 9 | margin: 0; 10 | } 11 | 12 | /** Top Level Components */ 13 | 14 | #message { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | } 19 | 20 | #error { 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | color: red; 25 | } 26 | 27 | #navigation { 28 | max-width: 20%; 29 | flex-grow: 1; 30 | height: 100vh; 31 | overflow-y: scroll; 32 | border-right: solid; 33 | border-right-width: 1px; 34 | border-right-color: #CCC; 35 | background: #ffffff; 36 | } 37 | 38 | #container { 39 | display: flex; 40 | flex-direction: row; 41 | height: 100vh; 42 | overflow: hidden; 43 | } 44 | 45 | #content-container { 46 | max-width: 80%; 47 | flex-grow: 4; 48 | padding-top: 2vh; 49 | height: 100vh; 50 | overflow: scroll; 51 | color: white; 52 | } 53 | 54 | /** Background **/ 55 | 56 | .background-view { 57 | width: 100vw; 58 | height: 100vh; 59 | position: fixed; 60 | z-index: -1; 61 | transition: all; 62 | background: linear-gradient(135deg, #52edff, #0d057e) 63 | } 64 | 65 | .background-darkblue { 66 | opacity: 1; 67 | width: 100vw; 68 | height: 100vh; 69 | position: fixed; 70 | transition: all; 71 | transition-duration: 0.5s; 72 | background: linear-gradient(135deg, #0d057e, #52edff) 73 | } 74 | 75 | .hidden-background { 76 | opacity: 0; 77 | } 78 | 79 | /** Navigation */ 80 | 81 | .main-view-selector-option { 82 | border-bottom-width: 1px; 83 | border-color: #CCC; 84 | border-bottom-style: solid; 85 | padding: 20px; 86 | position: relative; 87 | cursor: pointer; 88 | transition-property: all; 89 | transition-duration: 0.25s; 90 | } 91 | 92 | .main-view-selector-option:hover { 93 | background-color: #EEE; 94 | } 95 | 96 | .main-view-selector-selected { 97 | background-color: #CCC; 98 | } 99 | 100 | /************************/ 101 | /** Main Content Views **/ 102 | /************************/ 103 | 104 | /** Base View **/ 105 | .main-content-view { 106 | margin-left: 2vw; 107 | margin-right: 2vw; 108 | height: 98vh; 109 | position: relative; 110 | } 111 | 112 | /** Characters **/ 113 | 114 | #characters-container { 115 | 116 | } 117 | 118 | #character-input-add-button { 119 | cursor: pointer; 120 | text-align: center; 121 | } 122 | 123 | #input-add-character-name { 124 | width: 75px; 125 | margin-bottom: 10px; 126 | } 127 | 128 | .manage-character-container div { 129 | margin-bottom: 10px; 130 | } 131 | 132 | .manage-character-container { 133 | display: flex; 134 | flex-direction: column; 135 | padding: 25px; 136 | border-style: solid; 137 | margin: 10px; 138 | border-width: 1px; 139 | } 140 | 141 | .manage-character-button { 142 | padding: 10px; 143 | background-color: #AAA; 144 | cursor: pointer; 145 | } 146 | 147 | .manage-characters-add-button-image-container { 148 | position: absolute; 149 | width: 100%; 150 | height: 100%; 151 | display: flex; 152 | flex-direction: column; 153 | justify-content: center; 154 | align-items: center; 155 | margin-left: -25px; 156 | margin-top: -25px; 157 | padding: 0; 158 | } 159 | .manage-characters-add-button-image { 160 | width: 35px; 161 | height: 35px; 162 | } 163 | .manage-characters-add-container { 164 | display: flex; 165 | flex-direction: column; 166 | padding: 25px; 167 | border-style: solid; 168 | margin: 10px; 169 | border-width: 1px; 170 | cursor: pointer; 171 | position: relative; 172 | } 173 | 174 | 175 | /** Value Training View */ 176 | #character-trainer-container { 177 | height: 88vh; 178 | } 179 | 180 | #character-trainer-view-header { 181 | display: flex; 182 | flex-direction: row; 183 | align-items: center; 184 | justify-content: center; 185 | margin-top: 10vh; 186 | font-weight: normal; 187 | } 188 | 189 | .character-trainer-view-character-selector { 190 | font-size: 1.5em; 191 | font-weight: normal; 192 | text-align: right; 193 | border: none; 194 | outline: 0; 195 | color: white; 196 | -webkit-appearance: none; 197 | background: url(../images/menu-indicator.svg) no-repeat; 198 | background-size: 15px; 199 | background-position: 100%; 200 | padding-right: 25px; 201 | direction: rtl; 202 | } 203 | 204 | #character-trainer-view-header-text { 205 | margin-left: 10px; 206 | } 207 | 208 | #character-trainer-view-battle-container { 209 | outline: none; 210 | display: flex; 211 | flex-direction: row; 212 | max-width: 100%; 213 | margin-top: 10vh; 214 | justify-content: space-around; 215 | } 216 | 217 | .battle-view-timer-container { 218 | width: 100%; 219 | display: flex; 220 | } 221 | 222 | #battle-view-button-container { 223 | display: flex; 224 | margin-top: 45px; 225 | justify-content: space-around; 226 | } 227 | 228 | #character-trainer-view-center-container { 229 | display: flex; 230 | flex-direction: column; 231 | justify-content: space-around; 232 | } 233 | 234 | .battle-view-choice-button { 235 | margin: 10px; 236 | background-color: rgba(255, 255, 255, 0.2); 237 | padding: 20px; 238 | cursor: pointer; 239 | width: 24vw; 240 | height: 16vh; 241 | text-align: center; 242 | display: flex; 243 | align-items: center; 244 | justify-content: center; 245 | font-size: 2em; 246 | } 247 | 248 | #character-trainer-view-center-text { 249 | font-size: 1.5em; 250 | } 251 | 252 | .countdown-progress-bar { 253 | height: 20px; 254 | background-color: #CCC; 255 | } 256 | 257 | .battle-view-button { 258 | border-width: 1px; 259 | border-color: black; 260 | border-radius: 8px; 261 | cursor: pointer; 262 | text-align: center; 263 | } 264 | 265 | #value-list-footer { 266 | display: flex; 267 | flex-direction: row; 268 | width: 100%; 269 | justify-content: space-around; 270 | bottom: 4vh; 271 | position: absolute; 272 | } 273 | 274 | #value-list-session { 275 | display: flex; 276 | justify-content: space-around; 277 | } 278 | 279 | .battle-type-view { 280 | text-align: center; 281 | } 282 | 283 | #session-time-view { 284 | margin-left: .4em; 285 | } 286 | 287 | /** Value List */ 288 | 289 | #values-view { 290 | padding-bottom: 5vh; 291 | } 292 | 293 | .value-list-container { 294 | border-width: 1px; 295 | border-color: black; 296 | margin: 10px 0px; 297 | background-color: #AAA; 298 | padding: 20px; 299 | position: relative; 300 | display: flex; 301 | flex-direction: row; 302 | } 303 | 304 | .favorite-button-container-value-list-view { 305 | cursor: pointer; 306 | z-index: 999; 307 | padding-right: 10px; 308 | max-height: 100%; 309 | width: 1em; 310 | height: 1em; 311 | } 312 | 313 | .value-list-label { 314 | position: relative; 315 | z-index: 999999; 316 | } 317 | 318 | .value-list-progress { 319 | position: absolute; 320 | top: 0; 321 | left: 0; 322 | width: 25px; 323 | background-color: #70cc70; 324 | height: 100%; 325 | float: left; 326 | z-index: 1; 327 | } 328 | 329 | /** Character Comparison View */ 330 | 331 | #character-comparison-view { 332 | width: 100%; 333 | display: flex; 334 | flex-direction: row; 335 | position: relative; 336 | } 337 | 338 | .comparison-character-view-container { 339 | margin-right: 10px; 340 | } 341 | 342 | /** Character Favorites **/ 343 | 344 | #favorite-pairs-holder { 345 | margin: 20px 0px 20px 0px; 346 | } 347 | 348 | #favorite-values-holder { 349 | margin: 20px 0px 20px 0px; 350 | } 351 | 352 | /** Value Difference **/ 353 | 354 | #value-difference-view { 355 | margin: 10px 0px 10px 0px; 356 | border-bottom: solid; 357 | border-bottom-width: 1px; 358 | padding-bottom: 1em; 359 | width: 100%; 360 | } 361 | 362 | .value-difference-view-value { 363 | font-size: 1.5em; 364 | font-weight: bold; 365 | text-align: center; 366 | } 367 | 368 | .value-difference-comparison-view { 369 | align-items: center; 370 | } 371 | 372 | .value-difference-view-score-container { 373 | font-size: 0.6em; 374 | align-items: center; 375 | display: flex; 376 | flex-direction: column; 377 | margin-top: 1em; 378 | } 379 | 380 | /** Conflict Comparison **/ 381 | 382 | 383 | /** Common Ground **/ 384 | 385 | .common-ground-view-container { 386 | display: flex; 387 | flex-direction: column; 388 | justify-content: center; 389 | align-items: center; 390 | padding: 20px 0px; 391 | border-bottom-style: solid; 392 | border-bottom-width: 1px; 393 | width: 75vw; 394 | } 395 | 396 | .common-ground-view-name { 397 | font-size: 1.5em; 398 | font-weight: bold; 399 | } 400 | 401 | .common-ground-view-scores { 402 | margin-top: 10px; 403 | font-size: 0.6em; 404 | } 405 | 406 | /** Character Selector **/ 407 | .character-selector-button { 408 | margin: 10px 10px 0px 0px; 409 | border-width: 1px; 410 | padding: 20px; 411 | border-style: solid; 412 | border-color: #CCC; 413 | cursor: pointer; 414 | } 415 | 416 | /** Generics */ 417 | 418 | .comparison-view-selector { 419 | margin-top: 5vh; 420 | } 421 | 422 | .favorite-button-container { 423 | width: 20px; 424 | height: 20px; 425 | cursor: pointer; 426 | z-index: 999; 427 | } 428 | 429 | 430 | .character-selector-button-selected { 431 | background-color: #CCC; 432 | } 433 | 434 | .character-selector-button:hover { 435 | background-color: #EEE; 436 | } 437 | 438 | .favorites-filter-button { 439 | padding: 20px 0px; 440 | cursor: pointer; 441 | } 442 | 443 | .comparison-view-conflicts-header-container { 444 | display: flex; 445 | align-items: center; 446 | justify-content: space-around; 447 | } 448 | 449 | .comparison-view-conflict-container { 450 | display: flex; 451 | align-items: center; 452 | border-bottom-style: solid; 453 | padding: 10px 0px; 454 | border-bottom-width: thin; 455 | } 456 | 457 | .comparison-view-value-view { 458 | flex: 4; 459 | font-size: 1.5em; 460 | font-weight: bold; 461 | } 462 | 463 | .conflict-container-center { 464 | flex: 1; 465 | align-items: center; 466 | display: flex; 467 | flex-direction: column; 468 | } 469 | 470 | .comparison-view { 471 | width: 100%; 472 | display: flex; 473 | flex-direction: column; 474 | position: relative; 475 | margin: 5vh 0vh; 476 | } -------------------------------------------------------------------------------- /css/welcome-window.css: -------------------------------------------------------------------------------- 1 | .button { 2 | cursor: pointer; 3 | } 4 | 5 | #button-container { 6 | display: flex; 7 | flex-direction: row; 8 | width: 100%; 9 | align-items: center; 10 | } 11 | 12 | .recent-documents-header { 13 | font-size: 15px; 14 | opacity: 0.5; 15 | color: #3A3A3A; 16 | } 17 | 18 | .recent-documents-list { 19 | border-bottom-style: solid; 20 | border-bottom-color: #CCC; 21 | border-bottom-width: 1px; 22 | } 23 | 24 | .recent-document { 25 | max-width: 300px; 26 | } -------------------------------------------------------------------------------- /data/values-test.txt: -------------------------------------------------------------------------------- 1 | Abundance 2 | Balance 3 | Challenge 4 | Democracy 5 | Eating 6 | Fame 7 | Generosity 8 | Harmony 9 | Innovation 10 | Justice 11 | Kindness 12 | Love 13 | Making Love 14 | Nature 15 | Optimism 16 | Passion 17 | Quality 18 | Recognition 19 | Success 20 | Tradition 21 | Understanding 22 | Variety 23 | Vitality 24 | Wealth 25 | You -------------------------------------------------------------------------------- /data/values.txt: -------------------------------------------------------------------------------- 1 | A good story 2 | A lost love 3 | A mystery 4 | A special hobby 5 | A special thing 6 | A wonderful journey 7 | Abundance 8 | Accepting help 9 | Accomplishment 10 | Accountability 11 | Accuracy 12 | Achievement 13 | Adventure 14 | Agency 15 | An easy journey 16 | An easy love 17 | An easy score 18 | Analyzing 19 | Animals 20 | Appreciation of objects 21 | Approval 22 | Approval of a close friend 23 | Approval of family 24 | Approval of friends 25 | Approval of parents 26 | Approval of partner 27 | Approval of sibling 28 | Approval of spouse 29 | Art 30 | Authenticity 31 | Authority 32 | Autonomy 33 | Balance 34 | Beauty 35 | Being able to go anywhere 36 | Being alone 37 | Being at parties 38 | Being at workshops 39 | Being in a community 40 | Being in a social place 41 | Being in an audience 42 | Being in an intimate space of togetherness 43 | Being in associations 44 | Being in school 45 | Being on time 46 | Being right 47 | Being someone who matters 48 | Being with family 49 | Belief 50 | Belonging to neighborhoods 51 | Benevolence 52 | Boating 53 | Boldness 54 | Breaking the rules for the right reason 55 | Building 56 | Challenge 57 | Change 58 | Citizenship 59 | Clarity 60 | Cleanliness, orderliness 61 | Collaboration 62 | Collecting 63 | Comfort 64 | Commitment 65 | Committing oneself 66 | Common sense 67 | Communication 68 | Community 69 | Compassion 70 | Competence 71 | Competency 72 | Competing 73 | Competition 74 | Composing 75 | Concern for others 76 | Confidence 77 | Conformity 78 | Connection 79 | Conservation 80 | Consistency 81 | Contribution 82 | Control 83 | Conventional success 84 | Cooking 85 | Cooperation 86 | Coordination 87 | Courage 88 | Creativity 89 | Credibility 90 | Curiosity 91 | Dancing 92 | Day Dreaming 93 | Decisiveness 94 | Defending yourself 95 | Democracy 96 | Designing 97 | Determination 98 | Developing awareness 99 | Discipline 100 | Discovery 101 | Dissenting 102 | Diversity 103 | Drawing 104 | Driving 105 | Ease 106 | Eating 107 | Education 108 | Efficiency 109 | Empathy 110 | Employment 111 | Environment 112 | Equality 113 | Excellence 114 | Exercise 115 | Exploration 116 | Expressing Emotions 117 | Expressing Opinions 118 | Fairness 119 | Faith 120 | Faithfulness 121 | Fame 122 | Family 123 | Fashion 124 | Fight for what you believe 125 | Fitting in 126 | Flair 127 | Flexibility 128 | Forgiveness 129 | Freedom 130 | Friendship 131 | Frugality 132 | Fulfillment 133 | Full transparency 134 | Fun 135 | Generosity 136 | Genuineness 137 | Getting to know oneself 138 | Good will 139 | Goodness 140 | Gratitude 141 | Great Content 142 | Great design 143 | Growing 144 | Growth 145 | Guaranteed now 146 | Happiness 147 | Happiness of a close friend 148 | Happiness of family 149 | Happiness of friends 150 | Happiness of parents 151 | Happiness of partner 152 | Happiness of sibling 153 | Happiness of spouse 154 | Hard won moment of joy 155 | Hard work 156 | Harmony 157 | Having Abilities 158 | Having Customs 159 | Having Duties 160 | Having Education 161 | Having Equal Rights 162 | Having Family 163 | Having Food 164 | Having Friendships 165 | Having Fun 166 | Having Games 167 | Having Literature 168 | Having Norms 169 | Having Parties 170 | Having Peace of mind 171 | Having Policies 172 | Having Relationship to nature 173 | Having Religion 174 | Having Responsibilities 175 | Having Rights 176 | Having Shelter 177 | Having Skills 178 | Having Social Security 179 | Having Teachers 180 | Having Techniques 181 | Having Values 182 | Having Work 183 | Having a place to belong to 184 | Having a place to live 185 | Having a space for expression 186 | Healing 187 | Hedonism 188 | Helping 189 | High risk path 190 | High risk shortcut 191 | Holistic Living 192 | Honesty 193 | Honor 194 | Humor 195 | Imagination 196 | Improvement 197 | Independence 198 | Individuality 199 | Infamy 200 | Influence 201 | Initiative 202 | Inner Harmony 203 | Inner peace 204 | Innovation 205 | Integrity 206 | Intelligence 207 | Intensity 208 | Internal satisfaction 209 | Interpreting 210 | Intimacy 211 | Intuition 212 | Inventiveness 213 | Investigating 214 | Investing 215 | Joy 216 | Justice 217 | Keeping the family together 218 | Kindness 219 | Knowledge 220 | Leadership 221 | Learning 222 | Love 223 | Low risk guaranteed arrival 224 | Loyalty 225 | Lust 226 | Magic 227 | Making Love 228 | Making an impact 229 | Making new friends 230 | Making peace 231 | Making real connections 232 | Manipulation 233 | Me 234 | Meaning 235 | Meaningful Work 236 | Meaningful relationships 237 | Mediating 238 | Mental health 239 | Merit 240 | Moderation 241 | Modesty 242 | Money 243 | Music 244 | Nature 245 | Nurturing 246 | Obedience 247 | Obligation to family 248 | Open-mindedness 249 | Openness 250 | Optimism 251 | Passion 252 | Patriotism 253 | Peace 254 | Peace, Non-violence 255 | Perfection 256 | Perseverance 257 | Persistence 258 | Personal Growth 259 | Personal desire 260 | Personal health 261 | Pets 262 | Planning 263 | Pleasure 264 | Poise 265 | Popularity 266 | Possession of objects 267 | Potential future 268 | Power 269 | Practicality 270 | Preservation 271 | Pride 272 | Privacy 273 | Problem solving 274 | Professionalism 275 | Progress 276 | Prosperity 277 | Punctuality 278 | Purpose 279 | Quality 280 | Quantity 281 | Random act of kindness 282 | Reading 283 | Reciprocity 284 | Recognition 285 | Regularity 286 | Relaxation 287 | Relaxing 288 | Reliability 289 | Religion 290 | Remembering 291 | Reputation 292 | Resourcefulness 293 | Respect 294 | Respect for others 295 | Responsibility 296 | Responsiveness 297 | Resting 298 | Results 299 | Rich solitude 300 | Romance 301 | Rule of Law 302 | Sacrifice 303 | Safety 304 | Safety of a close friend 305 | Safety of family 306 | Safety of friends 307 | Safety of parents 308 | Safety of partner 309 | Safety of sibling 310 | Safety of spouse 311 | Satisfying others 312 | Security 313 | Self-Respect 314 | Self-awareness 315 | Self-confidence 316 | Self-direction 317 | Self-esteem 318 | Self-expression 319 | Self-improvement 320 | Self-love 321 | Self-mastery 322 | Self-reliance 323 | Self-trust 324 | Sense of belonging 325 | Sense of history 326 | Sensuality 327 | Service 328 | Sex 329 | Sexual Intimacy 330 | Sharing 331 | Shopping 332 | Short term dreams 333 | Simplicity 334 | Sincerity 335 | Singing 336 | Skill 337 | Sleeping 338 | Social Status 339 | Social acceptance 340 | Solitude 341 | Solving Puzzles 342 | Speed 343 | Spirituality 344 | Spontaneity 345 | Sports 346 | Stability 347 | Standardization 348 | Status 349 | Stimulation 350 | Straightforwardness 351 | Strength 352 | Studying 353 | Success 354 | Superficial social fun 355 | Swimming 356 | Sympathy 357 | Systemization 358 | Taking care of 359 | Taking risks 360 | Teamwork 361 | The beaten path 362 | The memory of: Family 363 | The memory of: a special friend 364 | The memory of: a special thing 365 | The memory of: history 366 | The promise of low effort high reward 367 | The wonders of the unknown 368 | Timeliness 369 | Tolerance 370 | Tradition 371 | Tranquility 372 | Trust 373 | Trustworthiness 374 | Truth 375 | Understanding 376 | Unity 377 | Value of the team 378 | Variety 379 | Vitality 380 | Wealth 381 | Winning 382 | Wisdom 383 | Working 384 | Writing 385 | You -------------------------------------------------------------------------------- /images/add-character-plus.svg: -------------------------------------------------------------------------------- 1 | add-character-plus -------------------------------------------------------------------------------- /images/favorite-selected.svg: -------------------------------------------------------------------------------- 1 | favorite-selected -------------------------------------------------------------------------------- /images/favorite-unselected.svg: -------------------------------------------------------------------------------- 1 | favorite-unselected -------------------------------------------------------------------------------- /images/icons.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderunit/characterizer/3c2be13aa09534f68f5598e250e14fbe0e4b5448/images/icons.ai -------------------------------------------------------------------------------- /images/menu-indicator.svg: -------------------------------------------------------------------------------- 1 | menu-indicator -------------------------------------------------------------------------------- /js/battle-pairer.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter 2 | const {remote} = require('electron') 3 | const knex = remote.getGlobal('knex') 4 | const MIN_BATTTLE_COUNT = 2 5 | const TOP_PERCENT_CUTOFF = .1 6 | const TOP_RANK_CUTOFF = 20 7 | 8 | module.exports = class BattlePairer extends EventEmitter { 9 | constructor(properties) { 10 | super() 11 | if(!properties.characterID) throw new Error("Missing characterID") 12 | this.characterID = properties.characterID 13 | 14 | if(!properties.choices) throw new Error("Missing choices") 15 | this.choices = properties.choices 16 | 17 | if(!properties.values) throw new Error("Missing values") 18 | this.values = properties.values 19 | 20 | if(!properties.battlePairs) throw new Error("Missing battlePairs") 21 | this.previousBattlePairs = properties.battlePairs 22 | 23 | if(!properties.favorites) throw new Error("Missing favorites") 24 | this.favorites = properties.favorites 25 | 26 | this.battleModes = [ 27 | { 28 | "function": this.getTopPercentileBattle.bind(this), 29 | "weight": 40 30 | }, 31 | { 32 | "function": this.getTopPercentileAndRandomBattle.bind(this), 33 | "weight": 10 34 | }, 35 | { 36 | "function": this.getTopRankBattle.bind(this), 37 | "weight": 40 38 | }, 39 | { 40 | "function": this.getRandomBattle.bind(this), 41 | "weight": 10 42 | }, 43 | { 44 | "function": this.getFavoritesAndTopPercentileBattle.bind(this), 45 | "weight": 40 46 | }, 47 | { 48 | "function": this.getFavoritesAndRandomBattle.bind(this), 49 | "weight": 40 50 | }, 51 | { 52 | "function": this.getFavoritesBattle.bind(this), 53 | "weight": 40 54 | } 55 | ] 56 | 57 | this.valuesMap = properties.valuesMap 58 | 59 | this.valuesPercentRank = [] 60 | 61 | // keeps the values separated by battle count. 62 | this.battleChoiceTiers = [] 63 | for(var i = 0; i { 70 | let self = this 71 | for(let i=0; i 1) { 86 | if(this.isDuplicate(result)) { 87 | console.log(`Found duplicate!`) 88 | return this.getBattle(++rechooseCount) 89 | } 90 | console.log(`getFillDataBattle`) 91 | this.emit('battle-type-try', "Fill Data Battle") 92 | } else { 93 | let battleMode = this.chooseRandomBattleMode() 94 | result = battleMode() 95 | } 96 | if(!result) { 97 | console.log(`Didn't find valid candidates for battle mode.`) 98 | return this.getBattle(rechooseCount) 99 | } 100 | // TODO: check for the case where all matches have played out, and message it. 101 | // For now, let's just let it try to re-choose 102 | if(this.isDuplicate(result) && rechooseCount < 4) { 103 | console.log(`Found duplicate!`) 104 | return this.getBattle(++rechooseCount) 105 | } 106 | 107 | let randomizedResult = [] 108 | let firstIndex = Math.floor(Math.random()*2) 109 | if(firstIndex === 0) { 110 | randomizedResult = [result[0], result[1]] 111 | } else { 112 | randomizedResult = [result[1], result[0]] 113 | } 114 | return randomizedResult 115 | } 116 | 117 | isDuplicate(battlePair) { 118 | let result = false 119 | if(!battlePair || battlePair.length < 2) { 120 | throw new Error("checkDuplicate expects an array of length >= 2") 121 | } 122 | var self = this 123 | var checkForPair = (contender1, contender2) => { 124 | if(!contender1 || !contender2) { 125 | throw new Error("missing contender") 126 | } 127 | if(self.previousBattlePairs.hasOwnProperty(contender1.id)) { 128 | let contender1Pairs = self.previousBattlePairs[contender1.id] 129 | if(contender1Pairs.hasOwnProperty(contender2.id) && contender1Pairs[contender2.id] > 0) { 130 | return true 131 | } 132 | } 133 | return false 134 | } 135 | 136 | result = checkForPair(battlePair[0], battlePair[1]) || checkForPair(battlePair[1], battlePair[0]) 137 | return result 138 | } 139 | 140 | onBattleOutcome(battleOutcome) { 141 | let i = 0; 142 | let isWinnerFound = false 143 | let isLoserFound = false 144 | let self = this 145 | function moveToNextBucket(battleChoiceTier, i, j) { 146 | let target = battleChoiceTier.splice(j, 1)[0] 147 | if(i+1 { 7 | return new Promise((fulfill, reject)=>{ 8 | knex('Values').where({name: value}).select('id') 9 | .then(result => { 10 | // check for existing value 11 | if(!result || result.length === 0) { 12 | knex('Values').insert({name: value, uuid: generateUUID()}) 13 | .then(value => { 14 | // console.log(`Added Value: ${JSON.stringify(value)}`) 15 | fulfill() 16 | }) 17 | } else { 18 | fulfill() 19 | } 20 | }) 21 | .catch(reject) 22 | }) 23 | }) 24 | return Promise.all(writes) 25 | } 26 | 27 | function generateUUID() { 28 | return Math.floor(Math.random() * Math.pow(2, 32)).toString(32) 29 | } 30 | 31 | module.exports = { 32 | seedDB 33 | } 34 | -------------------------------------------------------------------------------- /js/prefs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { app } = require('electron') 4 | const os = require("os"); 5 | 6 | const pkg = require('../package.json') 7 | const util = require('./utils.js') // for Object.equals 8 | 9 | // TODO pref specifics shouldnt be in this module. 10 | const prefFile = path.join(app.getPath('userData'), 'characterizer-preferences.json') 11 | 12 | const defaultPrefs = { 13 | version: pkg.version, 14 | skipTimerLength: 10000 15 | } 16 | 17 | let prefs 18 | 19 | const load = () => { 20 | try { 21 | // load existing prefs 22 | // console.log("READING FROM DISK") 23 | prefs = JSON.parse(fs.readFileSync(prefFile)) 24 | } catch (e) { 25 | prefs = defaultPrefs 26 | try { 27 | savePrefs(prefs) 28 | } catch (e) { 29 | //console.log(e) 30 | } 31 | } 32 | } 33 | 34 | const savePrefs = (newPref) => { 35 | // console.log('SAVEPREFS') 36 | if (!newPref) return 37 | if (Object.equals(newPref,prefs)) { 38 | // console.log("IM THE SAME!!!!") 39 | } else { 40 | prefs = newPref 41 | // console.log("SAVING TO DISK") 42 | fs.writeFileSync(prefFile, JSON.stringify(newPref, null, 2)) 43 | } 44 | } 45 | 46 | const set = (keyPath, value, sync) => { 47 | // console.log('SETTING') 48 | const keys = keyPath.split(/\./) 49 | let obj = prefs 50 | while (keys.length > 1) { 51 | const key = keys.shift() 52 | if (!Object.prototype.hasOwnProperty.call(obj, key)) { 53 | obj[key] = {} 54 | } 55 | obj = obj[key] 56 | } 57 | let keyProp = keys.shift() 58 | let prevValue = obj[keyProp] 59 | if (Object.equals(prevValue,value)) { 60 | console.log("IM THE SAME!!!!") 61 | } else { 62 | obj[keyProp] = value 63 | console.log("SAVING TO DISK") 64 | console.log(prefs) 65 | if (sync) { 66 | fs.writeFileSync(prefFile, JSON.stringify(prefs, null, 2)) 67 | } else { 68 | fs.writeFile(prefFile, JSON.stringify(prefs, null, 2), (err) => { 69 | console.log("SAVED ASYNC") 70 | }) 71 | } 72 | } 73 | } 74 | 75 | const getPrefs = (from) => { 76 | // console.log("GETTING PREFS!!!", from) 77 | return prefs 78 | } 79 | 80 | const migrate = () => { 81 | prefs = Object.assign({}, defaultPrefs, prefs) 82 | prefs.version = defaultPrefs.version 83 | } 84 | 85 | const init = () => { 86 | //console.log("I AM INIT") 87 | load() 88 | if (prefs.version !== defaultPrefs.version) { 89 | migrate() 90 | savePrefs(prefs) 91 | } 92 | } 93 | 94 | init() 95 | 96 | module.exports = { 97 | savePrefs, 98 | getPrefs, 99 | set 100 | } 101 | -------------------------------------------------------------------------------- /js/utils.js: -------------------------------------------------------------------------------- 1 | Object.equals = function( x, y ) { 2 | if ( x === y ) return true; 3 | // if both x and y are null or undefined and exactly the same 4 | 5 | if ( ! ( x instanceof Object ) || ! ( y instanceof Object ) ) return false; 6 | // if they are not strictly equal, they both need to be Objects 7 | 8 | if ( x.constructor !== y.constructor ) return false; 9 | // they must have the exact same prototype chain, the closest we can do is 10 | // test there constructor. 11 | 12 | for ( var p in x ) { 13 | if ( ! x.hasOwnProperty( p ) ) continue; 14 | // other properties were tested using x.constructor === y.constructor 15 | 16 | if ( ! y.hasOwnProperty( p ) ) return false; 17 | // allows to compare x[ p ] and y[ p ] when set to undefined 18 | 19 | if ( x[ p ] === y[ p ] ) continue; 20 | // if they have the same strict value or identity then they are equal 21 | 22 | if ( typeof( x[ p ] ) !== "object" ) return false; 23 | // Numbers, Strings, Functions, Booleans must be strictly equal 24 | 25 | if ( ! Object.equals( x[ p ], y[ p ] ) ) return false; 26 | // Objects and Arrays must be tested recursively 27 | } 28 | 29 | for ( p in y ) { 30 | if ( y.hasOwnProperty( p ) && ! x.hasOwnProperty( p ) ) return false; 31 | // allows x[ p ] to be set to undefined 32 | } 33 | return true; 34 | } 35 | 36 | function convertMS(ms) { 37 | var d, h, m, s 38 | s = Math.floor(ms / 1000) 39 | m = Math.floor(s / 60) 40 | s = s % 60 41 | h = Math.floor(m / 60) 42 | m = m % 60 43 | d = Math.floor(h / 24) 44 | h = h % 24 45 | return { d: d, h: h, m: m, s: s } 46 | } 47 | 48 | function getFriendlyMS(ms) { 49 | let converted = convertMS(ms) 50 | converted.h = converted.h < 10 ? '0'+converted.h : converted.h 51 | converted.m = converted.m < 10 ? '0'+converted.m : converted.m 52 | converted.s = converted.s < 10 ? '0'+converted.s : converted.s 53 | return converted 54 | } 55 | 56 | function semiRandomShuffle(array, factor) { 57 | let f=factor 58 | return array.map((e,i)=>{return [e,i+Math.random()*(f*2-f)]}).sort((a,b)=>{return a[1]-b[1]}).map(e=>{return e[0]}) 59 | } 60 | 61 | // via https://stackoverflow.com/questions/5723154/truncate-a-string-in-the-middle-with-javascript 62 | // https://stackoverflow.com/questions/831552/ellipsis-in-the-middle-of-a-text-mac-style/36470401#36470401 63 | const truncateMiddle = (string, maxLength = 30, separator = '…') => { 64 | if (!string) return string 65 | if (maxLength < 1) return string 66 | if (string.length <= maxLength) return string 67 | if (maxLength == 1) return string.substring(0, 1) + separator 68 | 69 | var midpoint = Math.ceil(string.length / 2) 70 | var toremove = string.length - maxLength 71 | var lstrip = Math.ceil(toremove / 2) 72 | var rstrip = toremove - lstrip 73 | 74 | return string.substring(0, midpoint - lstrip) + 75 | separator + 76 | string.substring(midpoint + rstrip) 77 | } 78 | 79 | function checkObjectPath(keys, object) { 80 | let curObject = object 81 | for(let key of keys) { 82 | if(!curObject[key]) { 83 | return false 84 | } 85 | curObject = curObject[key] 86 | } 87 | return true 88 | } 89 | 90 | module.exports = { 91 | convertMS, 92 | getFriendlyMS, 93 | semiRandomShuffle, 94 | checkObjectPath, 95 | truncateMiddle 96 | } -------------------------------------------------------------------------------- /js/views/background-view.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter 2 | 3 | module.exports = class MainBaseView extends EventEmitter { 4 | constructor(properties) { 5 | super() 6 | this.isLight = true 7 | 8 | this.root = document.createElement("div") 9 | this.root.setAttribute("id", "background-view") 10 | this.root.classList.add("background-view") 11 | 12 | this.darkBackground = document.createElement("div") 13 | this.darkBackground.classList.add("background-darkblue") 14 | this.root.appendChild(this.darkBackground) 15 | } 16 | 17 | getView() { 18 | return this.root 19 | } 20 | 21 | nextBackground() { 22 | this.isLight = !this.isLight 23 | if(this.isLight) { 24 | this.darkBackground.classList.remove("hidden-background") 25 | } else { 26 | this.darkBackground.classList.add("hidden-background") 27 | } 28 | 29 | } 30 | 31 | updateView() { 32 | 33 | } 34 | 35 | viewWillDisappear() { 36 | 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /js/views/character-comparison-base-view.js: -------------------------------------------------------------------------------- 1 | const MainBaseView = require('./main-base-view.js') 2 | const CharacterView = require('./character-selector-multiple.js') 3 | 4 | module.exports = class CharacterComparisonBaseView extends MainBaseView { 5 | constructor(properties) { 6 | super(properties) 7 | this.selectionsAllowedCount = 2 // the allowed number of selections 8 | this.selectedCharacters = this.getSelectedCharacters() 9 | this.valuesViewType = "table" 10 | 11 | this.characterView = new CharacterView(properties) 12 | this.root.appendChild(this.characterView.getView()) 13 | 14 | this.charactersMap = {} 15 | this.getCharacters() 16 | .then(inCharacters => { 17 | this.characters = inCharacters 18 | this.characterView.on('select-character', data => { 19 | this.onSelectCharacter(data.characterID) 20 | }) 21 | this.characterView.on('add-character', data => { 22 | this.emit('add-character', data) 23 | }) 24 | this.characterView.updateView() 25 | for(let character of inCharacters) { 26 | this.charactersMap[character.id] = character 27 | if(this.selectedCharacters.length<2 && this.selectedCharacters.indexOf(character) < 0) { 28 | this.onSelectCharacter(character.id) 29 | } 30 | } 31 | }) 32 | .catch(console.error) 33 | } 34 | 35 | onSelectCharacter(characterID) { 36 | let isExisting = false 37 | for(let i = 0; i this.selectionsAllowedCount) { 56 | this.selectedCharacters.splice(0, this.selectedCharacters.length - this.selectionsAllowedCount) 57 | } 58 | 59 | this.emit('selected-characters', this.selectedCharacters) 60 | 61 | this.characterView.updateView() 62 | this.updateView() 63 | } 64 | 65 | getSelectedCharacterValues() { 66 | let characterValuePromises = [] 67 | for(let aCharacter of this.selectedCharacters) { 68 | characterValuePromises.push(this.getCharacterValues(aCharacter.id)) 69 | } 70 | 71 | return Promise.all(characterValuePromises) 72 | } 73 | 74 | updateView() { 75 | 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /js/views/character-comparison-conflict-view.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils.js') 2 | const CharacterComparisonBaseView = require('./character-comparison-base-view.js') 3 | const FavoriteButton = require('./favorite-button.js') 4 | const { semiRandomShuffle } = require('../utils.js') 5 | const NUM_COMPARISON_ITEMS = 30 6 | const RANDOM_SHUFFLE_FACTOR = 4 7 | 8 | module.exports = class CharacterComparisonConflictView extends CharacterComparisonBaseView { 9 | constructor(properties) { 10 | super(properties) 11 | this.viewType = "all" 12 | 13 | this.viewTypeSelector = document.createElement("select") 14 | this.viewTypeSelector.classList.add("comparison-view-selector") 15 | for(let type of ["all", "favorites, paired", "favorited pairs"]) { 16 | let option = document.createElement("option") 17 | option.setAttribute("value", type) 18 | option.innerHTML = type 19 | this.viewTypeSelector.appendChild(option) 20 | } 21 | this.viewTypeSelector.addEventListener('change', (event)=>{ 22 | this.viewType = event.target.value 23 | this.updateView() 24 | }) 25 | this.root.appendChild(this.viewTypeSelector) 26 | 27 | this.comparisonView = document.createElement("div") 28 | this.comparisonView.classList.add("comparison-view") 29 | this.root.appendChild(this.comparisonView) 30 | this.updateView() 31 | } 32 | 33 | updateView() { 34 | this.comparisonView.innerHTML = `` 35 | let headersContainer = document.createElement("div") 36 | headersContainer.classList.add("comparison-view-conflicts-header-container") 37 | this.comparisonView.appendChild(headersContainer) 38 | 39 | for(let i = 0; i 0) { 41 | let vsView = document.createElement("div") 42 | vsView.classList.add("text-align-center") 43 | vsView.innerHTML = `vs` 44 | headersContainer.appendChild(vsView) 45 | } 46 | let characterName = document.createElement("h2") 47 | characterName.classList.add("comparison-view-conflicts-header") 48 | characterName.innerHTML = this.selectedCharacters[i].name 49 | headersContainer.appendChild(characterName) 50 | } 51 | 52 | let conflictContainer 53 | switch(this.viewType) { 54 | case "favorites, paired": 55 | conflictContainer = this.getFavoriteValuesView() 56 | break 57 | case "favorited pairs": 58 | conflictContainer = this.getFavoritePairedValuesView() 59 | break 60 | case "all": 61 | default: 62 | conflictContainer = this.getValuesView() 63 | break 64 | } 65 | this.comparisonView.appendChild(conflictContainer) 66 | } 67 | 68 | getValuesView() { 69 | let result = document.createElement("div") 70 | 71 | this.getSelectedCharacterValues() 72 | .then(inCharacterValueResults => { 73 | if(inCharacterValueResults.length < 2) { 74 | return 75 | } 76 | 77 | let characterValueResults = [] 78 | for(let i = 0; i < inCharacterValueResults.length; i++) { 79 | let values = inCharacterValueResults[i].slice(0, NUM_COMPARISON_ITEMS) 80 | characterValueResults.push(semiRandomShuffle(values, RANDOM_SHUFFLE_FACTOR)) 81 | } 82 | 83 | for(let valueIndex = 0; valueIndex < NUM_COMPARISON_ITEMS; valueIndex++) { 84 | 85 | var self = this 86 | let favButton = new FavoriteButton() 87 | 88 | // we use this in the add favorite event. 89 | let favoriteData = {} 90 | // we use this to check if the favorite pair exists. 91 | let favoritesPaths = [] 92 | 93 | let character1Values = characterValueResults[0] 94 | let value1 = character1Values[valueIndex] 95 | let character2Values = characterValueResults[1] 96 | let value2 = character2Values[valueIndex] 97 | 98 | let conflictContainer = this.getComparisonView(value1, value2) 99 | 100 | let isFavorite = false 101 | if(favoritesPaths.length > 1) { 102 | isFavorite = utils.checkObjectPath(favoritesPaths[0].concat(favoritesPaths[1]), this.valueComparisonFavorites) 103 | || utils.checkObjectPath(favoritesPaths[1].concat(favoritesPaths[0]), this.valueComparisonFavorites) 104 | } 105 | if(isFavorite) { 106 | favButton.setChecked(true) 107 | favButton.setEnabled(false) 108 | } else { 109 | var self = this 110 | favButton.setHandler(function(event) { 111 | self.emit('add-comparison-favorite', favoriteData) 112 | favButton.setChecked(true) 113 | favButton.setEnabled(false) 114 | }) 115 | favButton.setChecked(false) 116 | favButton.setEnabled(true) 117 | } 118 | 119 | result.appendChild(conflictContainer) 120 | } 121 | }) 122 | .catch(console.error) 123 | 124 | return result 125 | } 126 | 127 | getFavoriteValuesView() { 128 | let result = document.createElement("div") 129 | let character1ID = this.selectedCharacters[0].id 130 | let character2ID = this.selectedCharacters[1].id 131 | let favoritePairs = [] 132 | Promise.all([ 133 | this.getCharacterValueFavorites(character1ID), 134 | this.getCharacterValueFavorites(character2ID), 135 | this.getCharacterValuesMap(character1ID), 136 | this.getCharacterValuesMap(character2ID) 137 | ]).then(results => { 138 | let characterValueFavorites1 = results[0].values 139 | let characterValueFavorites2 = results[1].values 140 | let characterValueMap1 = results[2] 141 | let characterValueMap2 = results[3] 142 | 143 | let characterValues1 = [] 144 | for(let characterValueFavorite of characterValueFavorites1) { 145 | characterValues1.push(characterValueMap1[characterValueFavorite]) 146 | } 147 | characterValues1.sort((a, b) => { 148 | return b.score - a.score 149 | }) 150 | 151 | let characterValues2 = [] 152 | for(let characterValueFavorite of characterValueFavorites2) { 153 | characterValues2.push(characterValueMap1[characterValueFavorite]) 154 | } 155 | characterValues2.sort((a, b) => { 156 | return b.score - a.score 157 | }) 158 | 159 | let favoritePairLength = Math.min(characterValues1.length, characterValues2.length) 160 | let favoritePairs = [] 161 | for(let i = 0; i { 183 | let characterValueMap1 = results[0] 184 | let characterValueMap2 = results[1] 185 | 186 | let favoritePairs = [] 187 | if(this.characterComparisonFavorites[character1ID] 188 | && this.characterComparisonFavorites[character1ID][character2ID]) { 189 | 190 | favoritePairs = this.characterComparisonFavorites[character1ID][character2ID] 191 | } 192 | 193 | for(let j = 0; j{ 239 | let name = this.valuesMap[value.valueID].name 240 | let nameView = document.createElement("div") 241 | nameView.classList.add("comparison-view-value-view") 242 | nameView.innerHTML = name 243 | return nameView 244 | } 245 | 246 | let nameView1 = getValueView(value1) 247 | nameView1.classList.add("text-align-right") 248 | conflictContainer.appendChild(nameView1) 249 | 250 | let centerContainer = document.createElement("div") 251 | centerContainer.classList.add('conflict-container-center') 252 | conflictContainer.appendChild(centerContainer) 253 | 254 | centerContainer.appendChild(favButton.getView()) 255 | let vsView = document.createElement("div") 256 | vsView.innerHTML = `vs` 257 | centerContainer.appendChild(vsView) 258 | 259 | let nameView2 = getValueView(value2) 260 | conflictContainer.appendChild(nameView2) 261 | 262 | return conflictContainer 263 | } 264 | 265 | 266 | } 267 | 268 | -------------------------------------------------------------------------------- /js/views/character-comparison-value-difference-view.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils.js') 2 | const CharacterComparisonBaseView = require('./character-comparison-base-view.js') 3 | const FavoriteButton = require('./favorite-button.js') 4 | 5 | module.exports = class CharacterComparisonValueDifferencesView extends CharacterComparisonBaseView { 6 | constructor(properties) { 7 | super(properties) 8 | 9 | this.comparisonView = document.createElement("div") 10 | this.comparisonView.classList.add("comparison-view") 11 | this.comparisonView.classList.add("align-items-center") 12 | this.root.appendChild(this.comparisonView) 13 | this.updateView() 14 | } 15 | 16 | updateView() { 17 | this.comparisonView.innerHTML = `` 18 | 19 | if(this.selectedCharacters.length < 2) { 20 | return 21 | } 22 | this.getSelectedCharacterValues() 23 | .then(charactersValues => { 24 | 25 | let valuesGrouped = [] 26 | for(let curCharacterValue of charactersValues[0]) { 27 | let groupID = curCharacterValue.valueID 28 | let group = { 29 | groupID: groupID 30 | } 31 | let groupValues = [curCharacterValue] 32 | 33 | let max = curCharacterValue.score, min = curCharacterValue.score, diff = 0 34 | for(let i = 1; i max) { 38 | max = characterValue.score 39 | } 40 | if(characterValue.score < min) { 41 | min = characterValue.score 42 | } 43 | group.diff = max - min 44 | groupValues.push(characterValue) 45 | break 46 | } 47 | 48 | } 49 | } 50 | group.values = groupValues 51 | valuesGrouped.push(group) 52 | } 53 | 54 | valuesGrouped.sort((a, b) => { 55 | return b.diff - a.diff 56 | }) 57 | 58 | for(let valueGrouped of valuesGrouped) { 59 | let view = document.createElement('div') 60 | view.setAttribute("id", "value-difference-view") 61 | 62 | let valueView = document.createElement('div') 63 | valueView.classList.add("value-difference-view-value") 64 | valueView.innerHTML = `${this.valuesMap[valueGrouped.groupID].name}` 65 | view.appendChild(valueView) 66 | 67 | let scoreContainer = document.createElement("div") 68 | scoreContainer.classList.add("value-difference-view-score-container") 69 | view.appendChild(scoreContainer) 70 | 71 | var self = this 72 | let favButton = new FavoriteButton() 73 | scoreContainer.appendChild(favButton.getView()) 74 | 75 | let scoreView = document.createElement('div') 76 | scoreView.innerHTML = `Difference: ${valueGrouped.diff}` 77 | scoreContainer.appendChild(scoreView) 78 | 79 | let index = 0 80 | let favoriteData = {} 81 | let favoritesPaths = [] 82 | for(let characterValue of valueGrouped.values) { 83 | let characterScoreView = document.createElement('div') 84 | characterScoreView.innerHTML = `${this.selectedCharacters[index].name} score: ${characterValue.score}` 85 | scoreContainer.appendChild(characterScoreView) 86 | favoriteData[`character${index+1}ID`] = characterValue.characterID 87 | favoriteData[`value${index+1}ID`] = characterValue.valueID 88 | favoritesPaths.push([characterValue.characterID, characterValue.valueID]) 89 | index++ 90 | } 91 | 92 | let isFavorite = false 93 | if(favoritesPaths.length > 1) { 94 | isFavorite = utils.checkObjectPath(favoritesPaths[0].concat(favoritesPaths[1]), this.valueComparisonFavorites) 95 | || utils.checkObjectPath(favoritesPaths[1].concat(favoritesPaths[0]), this.valueComparisonFavorites) 96 | } 97 | 98 | if(isFavorite) { 99 | favButton.setChecked(true) 100 | favButton.setEnabled(false) 101 | } else { 102 | var self = this 103 | favButton.setHandler(function(event) { 104 | self.emit('add-comparison-favorite', favoriteData) 105 | favButton.setChecked(true) 106 | favButton.setEnabled(false) 107 | }) 108 | favButton.setChecked(false) 109 | favButton.setEnabled(true) 110 | } 111 | 112 | this.comparisonView.appendChild(view) 113 | } 114 | }) 115 | .catch(console.error) 116 | } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /js/views/character-comparison-view.js: -------------------------------------------------------------------------------- 1 | const CharacterComparisonBaseView = require('./character-comparison-base-view.js') 2 | const CharacterView = require('./character-selector-multiple.js') 3 | const FavoriteButton = require('./favorite-button.js') 4 | 5 | module.exports = class CharacterComparisonView extends CharacterComparisonBaseView { 6 | constructor(properties) { 7 | super(properties) 8 | this.valuesViewType = "average" 9 | 10 | this.isFiltering = false 11 | this.favoritesFilter = document.createElement("div") 12 | this.favoritesFilter.classList.add("favorites-filter-button") 13 | this.favoritesFilter.innerHTML = `Show Favorites` 14 | this.favoritesFilter.addEventListener("click", (event) => { 15 | this.isFiltering = !this.isFiltering 16 | if(this.isFiltering) { 17 | this.favoritesFilter.innerHTML = `Show All` 18 | } else { 19 | this.favoritesFilter.innerHTML = `Show Favorites` 20 | } 21 | this.updateView() 22 | }) 23 | this.root.appendChild(this.favoritesFilter) 24 | 25 | this.comparisonView = document.createElement("div") 26 | this.comparisonView.setAttribute("id", "character-comparison-view") 27 | this.root.appendChild(this.comparisonView) 28 | 29 | this.updateView() 30 | } 31 | 32 | updateView() { 33 | this.getSelectedCharacterValues() 34 | .then(characterValueResults => { 35 | if(!characterValueResults || !characterValueResults.length) { 36 | return 37 | } 38 | 39 | this.comparisonView.innerHTML = `` 40 | let comparisonView = document.createElement("div") 41 | comparisonView.setAttribute("id", "character-comparison-view") 42 | 43 | let targetWidth = 100/this.characters.length 44 | let index = 0 45 | 46 | for(let characterValueResult of characterValueResults) { 47 | let character = this.selectedCharacters[index] 48 | let containerDiv = document.createElement("div") 49 | containerDiv.classList.add("comparison-character-view-container") 50 | let nameContainer = document.createElement("h2") 51 | nameContainer.innerHTML = character.name 52 | containerDiv.appendChild(nameContainer) 53 | let curValuesView = this.getScoresView(character.id, characterValueResults[index]) 54 | containerDiv.appendChild(curValuesView) 55 | comparisonView.appendChild(containerDiv) 56 | index++ 57 | } 58 | 59 | this.root.replaceChild(comparisonView, this.comparisonView) 60 | this.comparisonView = comparisonView 61 | }) 62 | 63 | } 64 | 65 | getScoresView(characterID, values) { 66 | if(!values) { 67 | throw new Error("missing values") 68 | } 69 | let result = document.createElement('div') 70 | result.setAttribute("id", "values-view") 71 | 72 | for(let value of values) { 73 | this.getCharacterValueFavorites(characterID).then(characterValueFavorites => { 74 | let valueView = document.createElement('div') 75 | valueView.setAttribute("class", "value-list-container") 76 | 77 | 78 | let favoriteValues = characterValueFavorites.values 79 | let isFavorite = favoriteValues.indexOf(value.valueID) >= 0 80 | let favButtonProperties = { 81 | checked: isFavorite, 82 | enabled: !isFavorite, 83 | className: "favorite-button-container-value-list-view" 84 | } 85 | 86 | var self = this 87 | let favButton = new FavoriteButton(favButtonProperties) 88 | favButton.setHandler(function(event) { 89 | self.emit('add-character-value-favorite', {valueID: value.valueID, characterID: characterID}) 90 | favButton.setChecked(true) 91 | favButton.setEnabled(false) 92 | }) 93 | valueView.appendChild(favButton.getView()) 94 | 95 | let progressView = document.createElement('div') 96 | progressView.setAttribute("class", "value-list-progress") 97 | progressView.setAttribute("style", `width: ${value.score*100}%`) 98 | valueView.appendChild(progressView) 99 | let nameView = document.createElement('div') 100 | nameView.setAttribute("class", "value-list-label") 101 | nameView.innerHTML = `${this.valuesMap[value.valueID.toString()].name} | ${value.score} | Wins: ${value.wins}, Losses: ${value.losses} | Battles: ${value.battleCount}` 102 | valueView.appendChild(nameView) 103 | 104 | if(this.isFiltering && isFavorite) { 105 | result.appendChild(valueView) 106 | } else if(!this.isFiltering) { 107 | result.appendChild(valueView) 108 | } 109 | 110 | }) 111 | 112 | } 113 | 114 | return result 115 | } 116 | 117 | 118 | } -------------------------------------------------------------------------------- /js/views/character-favorites-view.js: -------------------------------------------------------------------------------- 1 | const MainBaseView = require('./main-base-view.js') 2 | 3 | module.exports = class CharacterFavoritesView extends MainBaseView { 4 | constructor(properties) { 5 | super(properties) 6 | 7 | this.root.setAttribute("id", "value-list-container") 8 | 9 | this.characterSelector = document.createElement("select") 10 | this.characterSelector.setAttribute("id", "character-selector") 11 | this.characterSelector.addEventListener('change', (event)=>{ 12 | this.currentCharacterID = parseInt(event.target.value) 13 | this.onSelectCharacter(this.currentCharacterID) 14 | }) 15 | this.root.appendChild(this.characterSelector) 16 | 17 | this.valuesViewContainer = document.createElement("div") 18 | this.valuesViewContainer.setAttribute("id", "values-view-container") 19 | this.root.appendChild(this.valuesViewContainer) 20 | 21 | var selectedCharacters = this.getSelectedCharacters() 22 | if(selectedCharacters && selectedCharacters.length > 0) { 23 | this.currentCharacterID = selectedCharacters[0].id 24 | } 25 | 26 | this.characters = [] 27 | this.getCharacters() 28 | .then(inCharacters => { 29 | this.characters = inCharacters 30 | 31 | if(!this.currentCharacterID && this.characters && this.characters.length) { 32 | this.currentCharacterID = this.characters[0].id 33 | } 34 | 35 | this.characterSelector.innerHTML = `` 36 | for(let character of this.characters) { 37 | let option = document.createElement("option") 38 | option.setAttribute("value", character.id) 39 | if(character.id == this.currentCharacterID) { 40 | option.setAttribute("selected", true) 41 | } 42 | option.innerHTML = character.name 43 | this.characterSelector.appendChild(option) 44 | } 45 | 46 | this.onSelectCharacter(this.currentCharacterID) 47 | }) 48 | .catch(console.error) 49 | } 50 | 51 | updateView() { 52 | 53 | } 54 | 55 | onSelectCharacter(characterID) { 56 | this.currentCharacterID = characterID 57 | let character 58 | for(let aCharacter of this.characters) { 59 | if(aCharacter.id === this.currentCharacterID) { 60 | character = aCharacter 61 | break 62 | } 63 | } 64 | 65 | if(!character) { 66 | return 67 | } 68 | 69 | this.valuesViewContainer.innerHTML = `` 70 | 71 | this.getCharacterValueFavorites(characterID).then(characterBattleFavorites => { 72 | if(!characterBattleFavorites) { 73 | return 74 | } 75 | this.valuesViewContainer.innerHTML = `` 76 | 77 | let values = characterBattleFavorites.values 78 | let favoriteValuesHolder = document.createElement("div") 79 | favoriteValuesHolder.setAttribute("id", "favorite-values-holder") 80 | let valueNames = [] 81 | for(let valueID of values) { 82 | let value = this.valuesMap[valueID] 83 | if(value && value.name) { 84 | valueNames.push(value.name) 85 | } 86 | } 87 | valueNames = valueNames.sort() 88 | for(let valueName of valueNames) { 89 | let view = document.createElement("div") 90 | view.innerHTML = `${valueName}` 91 | favoriteValuesHolder.appendChild(view) 92 | } 93 | this.valuesViewContainer.appendChild(favoriteValuesHolder) 94 | 95 | let pairs = characterBattleFavorites.pairs 96 | let pairsHolder = document.createElement("div") 97 | pairsHolder.setAttribute("id", "favorite-pairs-holder") 98 | for(var value1ID in pairs) { 99 | let value1 = this.valuesMap[value1ID] 100 | for(var value2ID in pairs[value1ID]) { 101 | let value2 = this.valuesMap[value2ID] 102 | let view = document.createElement("div") 103 | view.innerHTML = `${value1.name} vs ${value2.name}` 104 | pairsHolder.appendChild(view) 105 | } 106 | } 107 | this.valuesViewContainer.appendChild(pairsHolder) 108 | }) 109 | 110 | this.emit('selected-characters', [character]) 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /js/views/character-selector-multiple.js: -------------------------------------------------------------------------------- 1 | const MainBaseView = require('./main-base-view.js') 2 | 3 | module.exports = class CharacterView extends MainBaseView { 4 | constructor(properties) { 5 | super(properties) 6 | 7 | this.root = document.createElement('div') 8 | this.root.setAttribute("id", "characters-container") 9 | 10 | this.characterList = document.createElement("div") 11 | this.characterList.setAttribute("id", "character-list") 12 | this.characterList.classList.add("flex-wrap") 13 | this.root.appendChild(this.characterList) 14 | 15 | this.characters = [] 16 | this.getCharacters().then(inValues => { 17 | this.characters = inValues 18 | this.updateView() 19 | }) 20 | } 21 | 22 | addCharacterView(characterName, characterID, isSelected) { 23 | let characterView = document.createElement('div') 24 | characterView.classList.add("character-selector-button") 25 | if(isSelected) { 26 | characterView.classList.add("character-selector-button-selected") 27 | } 28 | characterView.setAttribute("data-id", characterID || 1) 29 | characterView.innerHTML = characterName 30 | characterView.addEventListener('click', this.onCharacterClick.bind(this)); 31 | this.characterList.appendChild(characterView) 32 | } 33 | 34 | onCharacterClick(event) { 35 | this.emit('select-character', {characterID: parseInt(event.target.dataset.id)}) 36 | } 37 | 38 | getView() { 39 | return this.root 40 | } 41 | 42 | updateView() { 43 | let selectedCharacters = this.getSelectedCharacters() 44 | this.characterList.innerHTML = '' 45 | for(let character of this.characters) { 46 | let isSelected = false 47 | for(let selectedCharacter of selectedCharacters) { 48 | if(selectedCharacter.id === character.id) { 49 | isSelected = true 50 | break 51 | } 52 | } 53 | this.addCharacterView(character.name, character.id, isSelected) 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /js/views/character-selector-single.js: -------------------------------------------------------------------------------- 1 | const MainBaseView = require('./main-base-view.js') 2 | 3 | module.exports = class CharacterSelectorSingle extends MainBaseView { 4 | constructor(properties) { 5 | super(properties) 6 | this.characters = [] 7 | this.root = document.createElement("select") 8 | this.root.setAttribute("id", "character-selector") 9 | this.root.addEventListener('change', (event)=>{ 10 | this.emit('change', event) 11 | }) 12 | } 13 | 14 | init() { 15 | this.getCharacters() 16 | .then(inCharacters => { 17 | this.root.innerHTML = `` 18 | if(!inCharacters || !inCharacters.length) { 19 | return 20 | } 21 | 22 | this.character = inCharacters[0] 23 | this.characters = inCharacters 24 | for(let character of inCharacters) { 25 | this.addCharacterView(character.name, character.id, this.character.id === character.id) 26 | } 27 | }) 28 | .catch(console.error) 29 | } 30 | 31 | addCharacterView(characterName, characterID, isSelected) { 32 | let option = document.createElement("option") 33 | option.setAttribute("value", characterID) 34 | option.innerHTML = characterName 35 | if(isSelected) { 36 | option.setAttribute("selected", true) 37 | } 38 | this.root.appendChild(option) 39 | } 40 | 41 | getView() { 42 | return this.root 43 | } 44 | 45 | updateView() { 46 | this.root = `` 47 | let selectedCharacters = this.getSelectedCharacters() 48 | this.characterList.innerHTML = '' 49 | let isSelected = false 50 | for(let character of this.characters) { 51 | if(!isSelected) { 52 | for(let selectedCharacter of selectedCharacters) { 53 | if(selectedCharacter.id === character.id) { 54 | isSelected = true 55 | break 56 | } 57 | } 58 | } 59 | this.addCharacterView(character.name, character.id, isSelected) 60 | } 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /js/views/character-trainer-view.js: -------------------------------------------------------------------------------- 1 | const MainBaseView = require('./main-base-view.js') 2 | const FavoriteButton = require('./favorite-button.js') 3 | const BattleTimeKeeper = require('../battle-timekeeper.js') 4 | const { getFriendlyMS } = require('../utils.js') 5 | const prefsModule = require('electron').remote.require('./js/prefs') 6 | const EXPIRE_TIME = prefsModule.getPrefs()['skipTimerLength'] || 10*1000 7 | 8 | module.exports = class CharacterTrainerView extends MainBaseView { 9 | constructor(properties) { 10 | super(properties) 11 | this.showTimer = true 12 | 13 | this.root.setAttribute("id", "character-trainer-container") 14 | this.root.addEventListener('keypress', this.onKeyPress.bind(this)) 15 | 16 | this.header = document.createElement("h2") 17 | this.header.setAttribute("id", "character-trainer-view-header") 18 | this.root.appendChild(this.header) 19 | 20 | this.characterSelector = document.createElement("select") 21 | this.characterSelector.classList.add("character-trainer-view-character-selector") 22 | this.characterSelector.addEventListener('change', (event)=>{ 23 | this.currentCharacterID = parseInt(event.target.value) 24 | this.onSelectCharacter(this.currentCharacterID) 25 | }) 26 | this.header.appendChild(this.characterSelector) 27 | let chooses = document.createElement("div") 28 | chooses.setAttribute("id", "character-trainer-view-header-text") 29 | chooses.innerHTML = `faces a difficult choice...` 30 | this.header.appendChild(chooses) 31 | 32 | let selectedCharacters = this.getSelectedCharacters() 33 | if(selectedCharacters.length > 0) { 34 | this.character = selectedCharacters[0] 35 | } 36 | 37 | this.characters = [] 38 | this.getCharacters() 39 | .then(inCharacters => { 40 | this.characterSelector.innerHTML = `` 41 | if(!inCharacters || !inCharacters.length) { 42 | return 43 | } 44 | 45 | if(!this.character) { 46 | this.character = inCharacters[0] 47 | } 48 | this.characters = inCharacters 49 | for(let character of this.characters) { 50 | let option = document.createElement("option") 51 | option.setAttribute("value", character.id) 52 | option.innerHTML = character.name 53 | if(this.character.id === character.id) { 54 | option.setAttribute("selected", true) 55 | } 56 | this.characterSelector.appendChild(option) 57 | } 58 | 59 | this.onSelectCharacter(this.character.id) 60 | }) 61 | .catch(console.error) 62 | 63 | this.battleView = document.createElement("div") 64 | this.battleView.setAttribute("id", "character-trainer-view-battle-container") 65 | this.battleView.setAttribute("tabindex", 1) 66 | this.root.appendChild(this.battleView) 67 | 68 | this.buttonContainer = document.createElement("div") 69 | this.buttonContainer.setAttribute("id", "battle-view-button-container") 70 | this.root.appendChild(this.buttonContainer) 71 | 72 | this.skipButton = document.createElement("div") 73 | this.skipButton.setAttribute("class", "battle-view-button") 74 | this.skipButton.addEventListener('click', this.onSkip.bind(this)) 75 | this.skipButton.innerHTML = `Skip` 76 | this.buttonContainer.appendChild(this.skipButton) 77 | 78 | this.footerView = document.createElement("div") 79 | this.footerView.setAttribute("id", "value-list-footer") 80 | this.footerView.classList.add("footer") 81 | this.root.appendChild(this.footerView) 82 | 83 | this.battleCountView = document.createElement("div") 84 | this.battleCountView.setAttribute("id", "value-list-battle-count") 85 | this.footerView.appendChild(this.battleCountView) 86 | 87 | this.sessionView = document.createElement("div") 88 | this.sessionView.setAttribute("id", "value-list-session") 89 | this.sessionCountView = document.createElement("div") 90 | this.sessionView.appendChild(this.sessionCountView) 91 | this.sessionTimeView = document.createElement("div") 92 | this.sessionTimeView.setAttribute("id", "session-time-view") 93 | this.sessionView.appendChild(this.sessionTimeView) 94 | this.footerView.appendChild(this.sessionView) 95 | 96 | let battleTypeView = document.createElement("div") 97 | this.battleTypeView = battleTypeView 98 | this.battleTypeView.classList.add("battle-type-view") 99 | this.footerView.appendChild(this.battleTypeView) 100 | 101 | this.showTimerSwitch = document.createElement("div") 102 | this.showTimerSwitch.setAttribute("id", "battle-view-show-timer") 103 | this.showTimerSwitch.setAttribute("class", "battle-view-button") 104 | this.showTimerSwitch.innerHTML = "Hide Timer" 105 | this.showTimerSwitch.addEventListener("click", this.toggleTimerView.bind(this)) 106 | this.footerView.appendChild(this.showTimerSwitch) 107 | 108 | this.characterSessionStartTime = Date.now() 109 | } 110 | 111 | setupBattleView() { 112 | this.getBattlePairer(this.currentCharacterID).then(battlePairer =>{ 113 | this.battleView.innerHTML = `` 114 | 115 | this.battlePairer = battlePairer 116 | 117 | this.choiceContainer1 = document.createElement("div") 118 | this.choiceContainer1.classList.add("battle-view-choice-button") 119 | this.choiceContainer1.addEventListener('click', this.onChoiceClick.bind(this)) 120 | this.battleView.appendChild(this.choiceContainer1) 121 | 122 | this.centerContainer = document.createElement("div") 123 | this.centerContainer.setAttribute("id", "character-trainer-view-center-container") 124 | 125 | this.favoriteButton = new FavoriteButton({handler: this.onFavorite.bind(this)}) 126 | this.centerContainer.appendChild(this.favoriteButton.getView()) 127 | 128 | let centerText = document.createElement("div") 129 | centerText.setAttribute("id", "character-trainer-view-center-text") 130 | centerText.innerHTML = `or` 131 | this.centerContainer.appendChild(centerText) 132 | 133 | this.timerContainer = document.createElement("div") 134 | this.timerContainer.setAttribute("id", "battle-view-timer-container") 135 | this.timerContainer.classList.add("battle-view-timer-container") 136 | this.countdownTimer = document.createElement("div") 137 | this.countdownTimer.classList.add("countdown-progress-bar") 138 | this.timerContainer.appendChild(this.countdownTimer) 139 | this.centerContainer.appendChild(this.timerContainer) 140 | 141 | this.battleView.appendChild(this.centerContainer) 142 | 143 | this.choiceContainer2 = document.createElement("div") 144 | this.choiceContainer2.classList.add("battle-view-choice-button") 145 | this.choiceContainer2.addEventListener('click', this.onChoiceClick.bind(this)) 146 | this.battleView.appendChild(this.choiceContainer2) 147 | 148 | this.battlePaiererTypeHandler = (battleType)=>{ 149 | this.battleTypeView.innerHTML = battleType 150 | } 151 | this.battlePairer.on("battle-type-try", this.battlePaiererTypeHandler) 152 | 153 | this.setupBattle() 154 | }) 155 | } 156 | 157 | onSelectCharacter(characterID) { 158 | this.clearSessionTimer() 159 | this.characterSessionStartTime = Date.now() 160 | this.curBattleTimeKeeper = new BattleTimeKeeper() 161 | this.currentCharacterID = characterID 162 | for(let aCharacter of this.characters) { 163 | if(aCharacter.id === this.currentCharacterID) { 164 | this.character = aCharacter 165 | break 166 | } 167 | } 168 | this.setupBattleView() 169 | this.updateView() 170 | 171 | this.emit('selected-characters', [this.character]) 172 | } 173 | 174 | getView() { 175 | return this.root 176 | } 177 | 178 | updateView() { 179 | if(this.timerID) { 180 | clearInterval(this.timerID) 181 | this.timerID = null 182 | } 183 | 184 | this.battleCountView.innerHTML = `${this.character.name} // ${this.getCharacterBattleCount(this.character.id)} Questions` 185 | 186 | let session = this.getCharacterSession(this.character.id) 187 | if(this.characterSessionStartTime && this.showTimer) { 188 | this.startSessionTimerView() 189 | this.sessionCountView.innerHTML = `${session.battleCount} Questions in ` 190 | } else { 191 | this.sessionTimeView.innerHTML = `` 192 | this.sessionCountView.innerHTML = `${session.battleCount} Questions` 193 | } 194 | } 195 | 196 | startSessionTimerView() { 197 | this.clearSessionTimer() 198 | this.timerID = setInterval( () => { 199 | this.updateTimerView() 200 | }, 500) 201 | this.updateTimerView() 202 | } 203 | 204 | updateTimerView() { 205 | let now = Date.now() 206 | let elapsed = getFriendlyMS(now - this.characterSessionStartTime) 207 | this.sessionTimeView.innerHTML = `${elapsed.h ? elapsed.h+':' : ''}${elapsed.m}:${elapsed.s}` 208 | } 209 | 210 | clearSessionTimer() { 211 | if(this.timerID) { 212 | clearInterval(this.timerID) 213 | this.timerID = null 214 | this.sessionTimeView.innerHTML = `` 215 | } 216 | } 217 | 218 | viewWillDisappear() { 219 | this.clearBattleTimer() 220 | this.battlePairer.removeListener("battle-type-try", this.battlePaiererTypeHandler) 221 | 222 | if(this.timerID) { 223 | clearInterval(this.timerID) 224 | this.timerID = null 225 | } 226 | } 227 | 228 | setupBattle() { 229 | this.clearBattleTimer() 230 | this.favoriteButton.innerHTML = `Favorite` 231 | 232 | this.choiceContainer1.innerHTML = "" 233 | this.choiceContainer2.innerHTML = "" 234 | let battleData = this.battlePairer.getBattle() 235 | 236 | this.choiceDataOne = battleData[0] 237 | this.choiceContainer1.setAttribute("data-id", this.choiceDataOne.id) 238 | this.choiceContainer1.innerHTML = this.choiceDataOne.name 239 | 240 | this.choiceDataTwo = battleData[1] 241 | this.choiceContainer2.setAttribute("data-id", this.choiceDataTwo.id) 242 | this.choiceContainer2.innerHTML = this.choiceDataTwo.name 243 | 244 | if(this.showTimer) { 245 | this.startBattleTimerView() 246 | } 247 | 248 | this.emit('battle-start', battleData) 249 | this.curBattleTimeKeeper.onBattleStart() 250 | } 251 | 252 | onChoiceClick(event) { 253 | let winnerID = parseInt(event.target.dataset.id) 254 | this.handleChoice(winnerID) 255 | } 256 | 257 | handleChoice(winnerID) { 258 | let winner, loser 259 | let battleOutcome = { 260 | characterID: this.character.id 261 | } 262 | if(winnerID === this.choiceDataOne.id) { 263 | battleOutcome.winner = this.choiceDataOne.id 264 | battleOutcome.loser = this.choiceDataTwo.id 265 | } else { 266 | battleOutcome.winner = this.choiceDataTwo.id 267 | battleOutcome.loser = this.choiceDataOne.id 268 | } 269 | this.emit('battle-update', battleOutcome) 270 | this.setupBattle() 271 | this.curBattleTimeKeeper.onBattleOutcome(battleOutcome) 272 | this.updateView() 273 | } 274 | 275 | onKeyPress(event) { 276 | if(event.key == "1") { 277 | this.handleChoice(this.choiceDataOne.id) 278 | } 279 | if(event.key == "2") { 280 | this.handleChoice(this.choiceDataTwo.id) 281 | } 282 | } 283 | 284 | onSkip(event) { 285 | this.emit('battle-skip') 286 | this.setupBattle() 287 | } 288 | 289 | onFavorite(event) { 290 | this.emit('battle-favorite', {value1: this.choiceDataOne, value2: this.choiceDataTwo, character: this.character}) 291 | this.setupBattle() 292 | } 293 | 294 | clearBattleTimer() { 295 | if(this.battleTimerID) { 296 | clearInterval(this.battleTimerID) 297 | this.battleTimerID = null 298 | } 299 | } 300 | 301 | startBattleTimerView() { 302 | this.battleStartTime = Date.now() 303 | this.battleTimerID = setInterval(() => { 304 | let now = Date.now() 305 | let elapsed = now - this.battleStartTime 306 | if(elapsed > (EXPIRE_TIME)) { 307 | this.onSkip() 308 | } 309 | this.countdownTimer.setAttribute("style", `width: ${((EXPIRE_TIME-elapsed)/EXPIRE_TIME)*100}%`) 310 | }, 1000/120) 311 | } 312 | 313 | toggleTimerView(event) { 314 | 315 | let session = this.getCharacterSession(this.character.id) 316 | this.showTimer = !this.showTimer 317 | if(this.showTimer) { 318 | this.showTimerSwitch.innerHTML = "Hide Timer" 319 | this.timerContainer.classList.remove("hidden") 320 | this.clearBattleTimer() 321 | this.startBattleTimerView() 322 | this.emit("show-timer") 323 | this.startSessionTimerView() 324 | this.sessionCountView.innerHTML = `${session.battleCount} Questions in ` 325 | } else { 326 | this.showTimerSwitch.innerHTML = "Show Timer" 327 | this.timerContainer.classList.add("hidden") 328 | this.clearBattleTimer() 329 | this.emit("hide-timer") 330 | this.clearSessionTimer() 331 | this.sessionCountView.innerHTML = `${session.battleCount} Questions` 332 | } 333 | } 334 | } -------------------------------------------------------------------------------- /js/views/characters-view.js: -------------------------------------------------------------------------------- 1 | const MainBaseView = require('./main-base-view.js') 2 | 3 | module.exports = class CharactersView extends MainBaseView { 4 | constructor(properties) { 5 | super(properties) 6 | this.root.setAttribute("id", "characters-container") 7 | 8 | this.characterList = document.createElement("div") 9 | this.characterList.setAttribute("id", "character-list") 10 | this.characterList.classList.add("flex-wrap") 11 | this.root.appendChild(this.characterList) 12 | 13 | this.characters = [] 14 | this.getCharacters().then(inValues => { 15 | this.characters = inValues 16 | this.updateView() 17 | }) 18 | } 19 | 20 | addCharacterView(character) { 21 | let self = this 22 | let characterName = character.name 23 | let characterID = character.id 24 | 25 | let characterView = document.createElement('div') 26 | characterView.classList.add("manage-character-container") 27 | characterView.setAttribute("data-id", characterID || 1) 28 | 29 | let nameView = document.createElement("div") 30 | nameView.innerHTML = characterName 31 | characterView.appendChild(nameView) 32 | 33 | let battleCountView = document.createElement("div") 34 | battleCountView.innerHTML = `${this.getCharacterBattleCount(characterID)} Battles` 35 | characterView.appendChild(battleCountView) 36 | 37 | let trainButton = document.createElement("div") 38 | trainButton.classList.add("manage-character-button") 39 | trainButton.innerHTML = "Train" 40 | trainButton.addEventListener("click", function(event) { 41 | self.emit("train", [character]) 42 | }) 43 | characterView.appendChild(trainButton) 44 | 45 | let viewValuesButton = document.createElement("div") 46 | viewValuesButton.classList.add("manage-character-button") 47 | viewValuesButton.innerHTML = "Values" 48 | viewValuesButton.addEventListener("click", function(event) { 49 | self.emit("viewValues", [character]) 50 | }) 51 | characterView.appendChild(viewValuesButton) 52 | 53 | characterView.addEventListener('click', this.onCharacterClick.bind(this)); 54 | this.characterList.appendChild(characterView) 55 | } 56 | 57 | addInputCharacterView() { 58 | this.addCharacterViewContainer = document.createElement('div') 59 | this.addCharacterViewContainer.classList.add("manage-characters-add-container") 60 | this.addCharacterViewContainer.dataset.mode = "button" 61 | 62 | this.characterInput = document.createElement("input") 63 | this.characterInput.setAttribute("id", "input-add-character-name") 64 | this.characterInput.setAttribute("type", "text") 65 | this.characterInput.classList.add("hidden") 66 | this.characterInput.addEventListener('keydown', (event)=>{ 67 | if(event.keyCode === 13) { 68 | this.addCharacterFronInput() 69 | } 70 | }) 71 | this.addCharacterViewContainer.appendChild(this.characterInput) 72 | this.characterInputAddButton = document.createElement("div") 73 | this.characterInputAddButton.setAttribute("id", "character-input-add-button") 74 | this.characterInputAddButton.classList.add("hidden") 75 | this.characterInputAddButton.innerHTML = `Add` 76 | this.characterInputAddButton.addEventListener('click', this.addCharacterFronInput.bind(this)) 77 | this.addCharacterViewContainer.appendChild(this.characterInputAddButton) 78 | 79 | // plus biew 80 | let plusContainer = document.createElement("div") 81 | plusContainer.classList.add("manage-characters-add-button-image-container") 82 | this.addCharacterViewContainer.appendChild(plusContainer) 83 | 84 | let plus = document.createElement("img") 85 | plus.setAttribute("src", "images/add-character-plus.svg") 86 | plus.classList.add("manage-characters-add-button-image") 87 | plusContainer.appendChild(plus) 88 | 89 | this.addCharacterViewContainer.addEventListener('click', (event)=>{ 90 | if(this.addCharacterViewContainer.dataset.mode === "button") { 91 | this.characterInput.classList.remove("hidden") 92 | this.characterInput.focus() 93 | this.characterInputAddButton.classList.remove("hidden") 94 | plus.classList.add("hidden") 95 | this.addCharacterViewContainer.dataset.mode = "input" 96 | } else { 97 | this.characterInput.classList.add("hidden") 98 | this.characterInputAddButton.classList.add("hidden") 99 | plus.classList.remove("hidden") 100 | this.addCharacterViewContainer.dataset.mode = "button" 101 | } 102 | 103 | }); 104 | 105 | this.characterList.appendChild(this.addCharacterViewContainer) 106 | } 107 | 108 | addCharacterFronInput(event) { 109 | this.characterList.removeChild(this.addCharacterViewContainer) 110 | let newName = this.characterInput.value 111 | this.addCharacterView({name: newName}, 13371337) 112 | if(newName) { 113 | this.emit('add-character', {name: newName}) 114 | } 115 | this.addInputCharacterView() 116 | } 117 | 118 | onCharacterClick(event) { 119 | this.emit('select-character', {characterID: parseInt(event.target.dataset.id)}) 120 | } 121 | 122 | getView() { 123 | return this.root 124 | } 125 | 126 | updateView() { 127 | let selectedCharacters = this.getSelectedCharacters() 128 | this.characterList.innerHTML = '' 129 | for(let character of this.characters) { 130 | this.addCharacterView(character) 131 | } 132 | 133 | this.addInputCharacterView() 134 | } 135 | } -------------------------------------------------------------------------------- /js/views/common-ground-view.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils.js') 2 | const CharacterComparisonBaseView = require('./character-comparison-base-view.js') 3 | const COMMONNESS_THRESHOLD = .10 4 | 5 | module.exports = class CommonGroundView extends CharacterComparisonBaseView { 6 | constructor(properties) { 7 | super(properties) 8 | this.viewType = "all" 9 | 10 | this.viewTypeSelector = document.createElement("select") 11 | this.viewTypeSelector.classList.add("comparison-view-selector") 12 | for(let type of ["all", "favorites"]) { 13 | let option = document.createElement("option") 14 | option.setAttribute("value", type) 15 | option.innerHTML = type 16 | this.viewTypeSelector.appendChild(option) 17 | } 18 | this.viewTypeSelector.addEventListener('change', (event)=>{ 19 | this.viewType = event.target.value 20 | this.updateView() 21 | }) 22 | this.root.appendChild(this.viewTypeSelector) 23 | 24 | this.comparisonView = document.createElement("div") 25 | this.comparisonView.classList.add("comparison-view") 26 | this.root.appendChild(this.comparisonView) 27 | this.updateView() 28 | } 29 | 30 | updateView() { 31 | this.comparisonView.innerHTML = `` 32 | let headersContainer = document.createElement("div") 33 | headersContainer.classList.add("comparison-view-conflicts-header-container") 34 | this.comparisonView.appendChild(headersContainer) 35 | 36 | for(let i = 0; i 0) { 38 | let vsView = document.createElement("div") 39 | vsView.innerHTML = `and` 40 | headersContainer.appendChild(vsView) 41 | } 42 | let characterName = document.createElement("h2") 43 | characterName.classList.add("comparison-view-conflicts-header") 44 | characterName.innerHTML = this.selectedCharacters[i].name 45 | headersContainer.appendChild(characterName) 46 | } 47 | 48 | if(this.selectedCharacters.length < 2) { 49 | let selectView = document.createElement("div") 50 | selectView.innerHTML = `Please select two characters to show their common ground.` 51 | this.comparisonView.appendChild(selectView) 52 | return 53 | } 54 | 55 | let valuesContainer 56 | switch(this.viewType) { 57 | case "favorites": 58 | valuesContainer = this.getFavoriteValuesView() 59 | break 60 | case "all": 61 | default: 62 | valuesContainer = this.getAllValuesView() 63 | break 64 | } 65 | 66 | this.comparisonView.appendChild(valuesContainer) 67 | } 68 | 69 | getAllValuesView() { 70 | let valuesContainer = document.createElement("div") 71 | 72 | let character1ID = this.selectedCharacters[0].id 73 | let character2ID = this.selectedCharacters[1].id 74 | let favoritePairs = [] 75 | Promise.all([ 76 | this.getCharacterValuesMap(character1ID), 77 | this.getCharacterValuesMap(character2ID) 78 | ]) 79 | .then(results => { 80 | let characterValueMap1 = results[0] 81 | let characterValueMap2 = results[1] 82 | 83 | let commonValues = [] 84 | for(let valueID in characterValueMap1) { 85 | let character1Value = characterValueMap1[valueID] 86 | let character2Value = characterValueMap2[valueID] 87 | 88 | if(Math.abs(character1Value.score - character2Value.score) < COMMONNESS_THRESHOLD 89 | && character1Value.score > 0 && character2Value.score > 0) { 90 | 91 | commonValues.push([character1Value, character2Value]) 92 | } 93 | } 94 | 95 | commonValues.sort((a, b) => { 96 | let value1 = a[0] 97 | let value2 = b[0] 98 | return value2.score - value1.score 99 | }) 100 | 101 | for(let commonValue of commonValues) { 102 | let character1Value = commonValue[0] 103 | let character2Value = commonValue[1] 104 | 105 | let valueContainer = document.createElement("div") 106 | valueContainer.classList.add("common-ground-view-container") 107 | 108 | let name = document.createElement("div") 109 | name.classList.add("common-ground-view-name") 110 | name.innerHTML = this.valuesMap[character1Value.valueID].name 111 | valueContainer.appendChild(name) 112 | 113 | let scoresContainer = document.createElement("div") 114 | scoresContainer.classList.add("common-ground-view-scores") 115 | valueContainer.appendChild(scoresContainer) 116 | 117 | let character1Container = document.createElement("div") 118 | character1Container.classList.add("common-ground-view-character-container") 119 | character1Container.innerHTML = `${this.selectedCharacters[0].name}: ${character1Value.score}` 120 | scoresContainer.appendChild(character1Container) 121 | 122 | let character2Container = document.createElement("div") 123 | character2Container.classList.add("common-ground-view-character-container") 124 | character2Container.innerHTML = `${this.selectedCharacters[1].name}: ${character2Value.score}` 125 | scoresContainer.appendChild(character2Container) 126 | 127 | valuesContainer.appendChild(valueContainer) 128 | } 129 | }) 130 | 131 | return valuesContainer 132 | } 133 | 134 | getFavoriteValuesView() { 135 | let valuesContainer = document.createElement("div") 136 | 137 | let character1ID = this.selectedCharacters[0].id 138 | let character2ID = this.selectedCharacters[1].id 139 | let favoritePairs = [] 140 | Promise.all([ 141 | this.getCharacterValueFavorites(character1ID), 142 | this.getCharacterValueFavorites(character2ID), 143 | this.getCharacterValuesMap(character1ID), 144 | this.getCharacterValuesMap(character2ID) 145 | ]).then(results => { 146 | let characterValueFavorites1 = results[0].values 147 | let characterValueFavorites2 = results[1].values 148 | let characterValueMap1 = results[2] 149 | let characterValueMap2 = results[3] 150 | 151 | let commonValues = [] 152 | 153 | // combine favorites, and make unique 154 | let s = new Set(characterValueFavorites1.concat(characterValueFavorites2)) 155 | let it = s.values() 156 | let favoriteValueIDs = Array.from(it) 157 | 158 | for(let valueID of favoriteValueIDs) { 159 | let character1Value = characterValueMap1[valueID] 160 | let character2Value = characterValueMap2[valueID] 161 | 162 | if(character1Value && character2Value 163 | && Math.abs(character1Value.score - character2Value.score) < COMMONNESS_THRESHOLD 164 | && character1Value.score > 0 && character2Value.score > 0) { 165 | 166 | commonValues.push([character1Value, character2Value]) 167 | } 168 | } 169 | 170 | commonValues.sort((a, b) => { 171 | let value1 = a[0] 172 | let value2 = b[0] 173 | return value2.score - value1.score 174 | }) 175 | 176 | for(let commonValue of commonValues) { 177 | let character1Value = commonValue[0] 178 | let character2Value = commonValue[1] 179 | 180 | let valueContainer = document.createElement("div") 181 | valueContainer.classList.add("common-ground-view-container") 182 | 183 | let name = document.createElement("div") 184 | name.classList.add("common-ground-view-name") 185 | name.innerHTML = this.valuesMap[character1Value.valueID].name 186 | valueContainer.appendChild(name) 187 | 188 | let scoresContainer = document.createElement("div") 189 | scoresContainer.classList.add("common-ground-view-scores") 190 | valueContainer.appendChild(scoresContainer) 191 | 192 | let character1Container = document.createElement("div") 193 | character1Container.classList.add("common-ground-view-character-container") 194 | character1Container.innerHTML = `${this.selectedCharacters[0].name}: ${character1Value.score}` 195 | scoresContainer.appendChild(character1Container) 196 | 197 | let character2Container = document.createElement("div") 198 | character2Container.classList.add("common-ground-view-character-container") 199 | character2Container.innerHTML = `${this.selectedCharacters[1].name}: ${character2Value.score}` 200 | scoresContainer.appendChild(character2Container) 201 | 202 | valuesContainer.appendChild(valueContainer) 203 | } 204 | }) 205 | 206 | return valuesContainer 207 | } 208 | } -------------------------------------------------------------------------------- /js/views/favorite-button.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter 2 | 3 | const ON_ICON_PATH = "images/favorite-selected.svg" 4 | const OFF_ICON_PATH = "images/favorite-unselected.svg" 5 | 6 | module.exports = class MainBaseView extends EventEmitter { 7 | constructor(properties={}) { 8 | super() 9 | this.handleClick = this.handleClick.bind(this) 10 | 11 | this.root = document.createElement("div") 12 | let className = properties.className ? properties.className : "favorite-button-container" 13 | this.root.classList.add(className) 14 | 15 | this.icon = document.createElement("img") 16 | this.icon.setAttribute("src", OFF_ICON_PATH) 17 | this.root.appendChild(this.icon) 18 | this.handler = properties.handler 19 | this.setChecked(properties.hasOwnProperty("checked") ? properties.checked : false) 20 | this.setEnabled(properties.hasOwnProperty("enabled") ? properties.enabled : true) 21 | } 22 | 23 | setEnabled(enabled) { 24 | this.enabled = enabled 25 | if(!this.enabled) { 26 | this.root.removeEventListener("click", this.handleClick) 27 | } else { 28 | this.root.addEventListener("click", this.handleClick) 29 | } 30 | } 31 | 32 | handleClick(event) { 33 | if(this.handler) { 34 | this.handler() 35 | } 36 | } 37 | 38 | setHandler(handler) { 39 | this.handler = handler 40 | } 41 | 42 | setChecked(checked) { 43 | this.checked = checked 44 | this.updateView() 45 | } 46 | 47 | getView() { 48 | return this.root 49 | } 50 | 51 | updateView() { 52 | if(this.checked) { 53 | this.icon.setAttribute("src", ON_ICON_PATH) 54 | } else { 55 | this.icon.setAttribute("src", OFF_ICON_PATH) 56 | } 57 | } 58 | 59 | viewWillDisappear() { 60 | this.root.removeEventListener(this.handler) 61 | } 62 | } -------------------------------------------------------------------------------- /js/views/internal-conflict-view.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils.js') 2 | const MainBaseView = require('./main-base-view.js') 3 | const FavoriteButton = require('./favorite-button.js') 4 | const { semiRandomShuffle } = require('../utils.js') 5 | const NUM_COMPARISON_ITEMS = 60 6 | const RANDOM_SHUFFLE_FACTOR = 4 7 | 8 | module.exports = class InternalConflictView extends MainBaseView { 9 | constructor(properties) { 10 | super(properties) 11 | 12 | this.root.setAttribute("id", "value-list-container") 13 | 14 | this.characterSelector = document.createElement("select") 15 | this.characterSelector.setAttribute("id", "character-selector") 16 | this.characterSelector.addEventListener('change', (event)=>{ 17 | this.currentCharacterID = parseInt(event.target.value) 18 | this.onSelectCharacter(this.currentCharacterID) 19 | }) 20 | this.root.appendChild(this.characterSelector) 21 | 22 | this.isFiltering = false 23 | this.favoritesFilter = document.createElement("div") 24 | this.favoritesFilter.classList.add("favorites-filter-button") 25 | this.favoritesFilter.innerHTML = `Show Favorites` 26 | this.favoritesFilter.addEventListener("click", (event) => { 27 | this.isFiltering = !this.isFiltering 28 | if(this.isFiltering) { 29 | this.favoritesFilter.innerHTML = `Show All` 30 | } else { 31 | this.favoritesFilter.innerHTML = `Show Favorites` 32 | } 33 | this.updateView() 34 | }) 35 | this.root.appendChild(this.favoritesFilter) 36 | 37 | this.valuesViewContainer = document.createElement("div") 38 | this.valuesViewContainer.setAttribute("id", "values-view-container") 39 | 40 | this.comparisonView = document.createElement("div") 41 | this.comparisonView.classList.add("comparison-view") 42 | this.valuesViewContainer.appendChild(this.comparisonView) 43 | this.root.appendChild(this.valuesViewContainer) 44 | 45 | this.characters = [] 46 | this.getCharacters() 47 | .then(inCharacters => { 48 | this.characters = inCharacters 49 | this.characterSelector.innerHTML = `` 50 | for(let character of this.characters) { 51 | let option = document.createElement("option") 52 | option.setAttribute("value", character.id) 53 | option.innerHTML = character.name 54 | this.characterSelector.appendChild(option) 55 | } 56 | if(this.characters && this.characters.length) { 57 | this.onSelectCharacter(this.characters[0].id) 58 | } 59 | }) 60 | .catch(console.error) 61 | } 62 | 63 | updateView() { 64 | if(!this.character) { 65 | return 66 | } 67 | this.comparisonView.innerHTML = `` 68 | 69 | if(this.isFiltering) { 70 | this.getFavoriteValuesView() 71 | } else { 72 | this.getAllValuesView() 73 | } 74 | } 75 | 76 | getComparisonView(value1, value2) { 77 | let conflictContainer = document.createElement("div") 78 | conflictContainer.classList.add("comparison-view-conflict-container") 79 | 80 | let favButton = new FavoriteButton() 81 | 82 | let favoriteData = {} 83 | favoriteData[`character1ID`] = value1.characterID 84 | favoriteData[`value1ID`] = value1.valueID 85 | favoriteData[`character2ID`] = value2.characterID 86 | favoriteData[`value2ID`] = value2.valueID 87 | let isFavorite = utils.checkObjectPath([value1.characterID, value1.valueID, value2.characterID, value2.valueID], this.valueComparisonFavorites) 88 | || utils.checkObjectPath([value2.characterID, value2.valueID, value1.characterID, value1.valueID], this.valueComparisonFavorites) 89 | 90 | if(isFavorite) { 91 | favButton.setChecked(true) 92 | favButton.setEnabled(false) 93 | } else { 94 | var self = this 95 | favButton.setHandler(function(event) { 96 | self.emit('add-comparison-favorite', favoriteData) 97 | favButton.setChecked(true) 98 | favButton.setEnabled(false) 99 | }) 100 | favButton.setChecked(false) 101 | favButton.setEnabled(true) 102 | } 103 | 104 | let getValueView = (value)=>{ 105 | let name = this.valuesMap[value.valueID].name 106 | let nameView = document.createElement("div") 107 | nameView.classList.add("comparison-view-value-view") 108 | nameView.innerHTML = name 109 | return nameView 110 | } 111 | 112 | let nameView1 = getValueView(value1) 113 | nameView1.classList.add("text-align-right") 114 | conflictContainer.appendChild(nameView1) 115 | 116 | let centerContainer = document.createElement("div") 117 | centerContainer.classList.add('conflict-container-center') 118 | conflictContainer.appendChild(centerContainer) 119 | 120 | centerContainer.appendChild(favButton.getView()) 121 | let vsView = document.createElement("div") 122 | vsView.innerHTML = `vs` 123 | centerContainer.appendChild(vsView) 124 | 125 | let nameView2 = getValueView(value2) 126 | conflictContainer.appendChild(nameView2) 127 | this.comparisonView.appendChild(conflictContainer) 128 | } 129 | 130 | getFavoriteValuesView() { 131 | 132 | let values = [] 133 | 134 | if(this.valueComparisonFavorites[this.currentCharacterID]) { 135 | this.getCharacterValuesMap(this.currentCharacterID) 136 | .then(characterValueMap => { 137 | for(let key in this.valueComparisonFavorites[this.currentCharacterID]) { 138 | if(this.valueComparisonFavorites[this.currentCharacterID][key][this.currentCharacterID]) { 139 | let key2 140 | for(let value2ID in this.valueComparisonFavorites[this.currentCharacterID][key][this.currentCharacterID]) { 141 | key2 = value2ID 142 | break 143 | } 144 | let value1 = characterValueMap[key] 145 | let value2 = characterValueMap[key2] 146 | this.getComparisonView(value1, value2) 147 | } 148 | } 149 | }) 150 | .catch(console.error) 151 | } 152 | 153 | let getValueView = (value)=>{ 154 | let name = this.valuesMap[value.valueID].name 155 | let nameView = document.createElement("div") 156 | nameView.innerHTML = name 157 | return nameView 158 | } 159 | } 160 | 161 | getAllValuesView() { 162 | this.getCharacterValues(this.currentCharacterID).then(inCharacterValues => { 163 | let valuesCopy = inCharacterValues.slice(0, NUM_COMPARISON_ITEMS) 164 | let values = semiRandomShuffle(valuesCopy, RANDOM_SHUFFLE_FACTOR) 165 | 166 | for(let i = 0; i { 103 | // convert these to normal json objects (instead of knex objects) 104 | values = JSON.parse(JSON.stringify(values)) 105 | let end = Date.now() 106 | valuesLib = values 107 | for(let value of valuesLib) { 108 | valuesMap[value.id.toString()] = value 109 | } 110 | remote.valuesMap = valuesMap 111 | }) 112 | 113 | 114 | var backgroundView = new BackgroundView() 115 | container.insertBefore(backgroundView.getView(), container.firstChild) 116 | 117 | var mainViewSelector = new MainViewSelector({type: curViewType, mainViews: mainViews}) 118 | document.getElementById("navigation").appendChild(mainViewSelector.getView()) 119 | mainViewSelector.on('select-view', viewType => { 120 | console.log(viewType) 121 | curViewType = viewType 122 | onSelectView() 123 | }) 124 | 125 | // Initialize character data. 126 | ipcRenderer.send('log', {type: 'progress', message: `Initializing Project`}) 127 | displayMessage(`Initializing Project`) 128 | loadValueBattleFavorites() 129 | .then(()=>{ 130 | return loadCharactersValueFavorites() 131 | }) 132 | .then(()=> { 133 | return loadValueComparisonFavorites() 134 | }) 135 | .then(()=>{ 136 | return getCharacters() 137 | }) 138 | .then(characters => { 139 | 140 | var finishSetup = () => { 141 | displayMessage(``) 142 | ipcRenderer.send('workspace-ready') 143 | onSelectView() 144 | } 145 | if(characters && characters.length) { 146 | // do the character data loads in series so we 147 | // can update the loading screen on a per character basis. 148 | // from: https://stackoverflow.com/a/36672042/1535171 149 | var sequence = Promise.resolve() 150 | characters.forEach(function(character) { 151 | sequence = sequence.then(function() { 152 | displayMessage(`Initializing ${character.name}`) 153 | ipcRenderer.send('log', {type: 'progress', message: `Initializing ${character.name}`}) 154 | return getBattlePairer(character.id) 155 | }).then(()=>{}); 156 | }) 157 | sequence.then(()=>{ 158 | finishSetup() 159 | }) 160 | } else { 161 | finishSetup() 162 | } 163 | 164 | }) 165 | .catch(error => { 166 | ipcRenderer.send('log', {type: 'progress', message: `Error: ${error}`}) 167 | }) 168 | 169 | /** 170 | * Switch 171 | */ 172 | function onSelectView() { 173 | if(currentContentView) { 174 | currentContentView.viewWillDisappear() 175 | } 176 | var ContentView = getContentView() 177 | currentContentView = new ContentView(viewProperties) 178 | document.getElementById("content-container").innerHTML = `` 179 | document.getElementById("content-container").appendChild(currentContentView.getView()) 180 | currentContentView.on('battle-update', battleOutcome => { 181 | handleBattleUpdate(battleOutcome) 182 | }) 183 | currentContentView.on('battle-favorite', battleData => { 184 | updateCharacterBattleFavorites(battleData) 185 | }) 186 | currentContentView.on('add-character', data => { 187 | addCharacter(data) 188 | }) 189 | currentContentView.on('selected-characters', data => { 190 | selectedCharacters = data 191 | }) 192 | currentContentView.on('add-comparison-favorite', data => { 193 | addValueComparisonFavorite(data) 194 | }) 195 | currentContentView.on('add-character-value-favorite', data => { 196 | addCharacterValueFavorite(data) 197 | }) 198 | currentContentView.on('train', data => { 199 | curViewType = "characterTrainer" 200 | mainViewSelector.setSelectedViewType(curViewType) 201 | selectedCharacters = data 202 | onSelectView() 203 | }) 204 | currentContentView.on('viewValues', data => { 205 | curViewType = "valueList" 206 | mainViewSelector.setSelectedViewType(curViewType) 207 | selectedCharacters = data 208 | onSelectView() 209 | }) 210 | 211 | backgroundView.nextBackground() 212 | } 213 | 214 | /** 215 | * @return The class for the currently selected main view. 216 | */ 217 | function getContentView() { 218 | switch(curViewType) { 219 | case "characterComparison": 220 | return CharacterComparisonView 221 | case "valueList": 222 | return ValueListView 223 | case "characterFavorites": 224 | return CharacterFavoritesView 225 | case "characterTrainer": 226 | return CharacterTrainerView 227 | case "characterConflictComparison": 228 | return CharacterComparisonConflictView 229 | case "internalConflict": 230 | return InternalConflictView 231 | case "valueDifference": 232 | return CharacterComparisonValueDifferenceView 233 | case "commonGround": 234 | return CommonGroundView 235 | case "manageCharacters": 236 | default: 237 | return CharactersView 238 | } 239 | } 240 | 241 | function updateView() { 242 | currentContentView.updateView() 243 | } 244 | 245 | function addCharacter(record) { 246 | knex('Characters').returning('id').insert(record) 247 | .then((result) => { 248 | let newID = result && result[0] 249 | record.id = newID 250 | characters.push(record) 251 | 252 | let newCharacterValueInserts = valuesLib.map((value)=>{ 253 | return knex('CharacterValues').insert({characterID: newID, valueID: value.id, score: 0.0, wins: 0, losses: 0, battleCount: 0}) 254 | }) 255 | Promise.all(newCharacterValueInserts) 256 | .then(()=>{ 257 | updateView() 258 | if(currentContentView && typeof currentContentView.onSelectCharacter === "function") { 259 | currentContentView.onSelectCharacter(newID) 260 | } 261 | }) 262 | .catch(error => { 263 | displayError(`Error adding character value: ${error}`) 264 | knex.raw(`delete from Characters where "id" = ?`, [newID]) 265 | .then(()=>{}) 266 | .catch(console.error) 267 | }) 268 | }) 269 | .catch(error => { 270 | displayError(`Error adding character value: ${error}`) 271 | }) 272 | } 273 | 274 | /** 275 | * 276 | * @param {Object} battleOutcome 277 | */ 278 | function handleBattleUpdate(battleOutcome) { 279 | let curCharacterValues = characterValues[battleOutcome.characterID] 280 | let isWinnerUpdated, isLoserUpdated 281 | let tailPromises = [] 282 | for(let curCharacterValue of curCharacterValues) { 283 | if(curCharacterValue.valueID == battleOutcome.winner) { 284 | curCharacterValue.wins += 1 285 | curCharacterValue.battleCount = curCharacterValue.wins + curCharacterValue.losses 286 | curCharacterValue.score = curCharacterValue.wins / (curCharacterValue.wins + curCharacterValue.losses) 287 | tailPromises.push(knex('CharacterValues').where({id: curCharacterValue.id}).update(curCharacterValue)) 288 | isWinnerUpdated = true 289 | } else if(curCharacterValue.valueID == battleOutcome.loser) { 290 | curCharacterValue.losses += 1 291 | curCharacterValue.battleCount = curCharacterValue.wins + curCharacterValue.losses 292 | curCharacterValue.score = curCharacterValue.wins / (curCharacterValue.wins + curCharacterValue.losses) 293 | tailPromises.push(knex('CharacterValues').where({id: curCharacterValue.id}).update(curCharacterValue)) 294 | isLoserUpdated = true 295 | } 296 | if(isWinnerUpdated && isLoserUpdated) { 297 | break 298 | } 299 | } 300 | curCharacterValues.sort((a, b) => { 301 | if(a.score === b.score) { 302 | return b.battleCount - a.battleCount 303 | } 304 | return b.score - a.score 305 | }) 306 | 307 | updateBattlePairs(battleOutcome) 308 | battlePairers[battleOutcome.characterID].onBattleOutcome(battleOutcome) 309 | 310 | let characterSession = getCharacterSession(battleOutcome.characterID) 311 | characterSessions[battleOutcome.characterID].battleCount += 1 312 | if(!characterSessions[battleOutcome.characterID].battleStart) { 313 | characterSessions[battleOutcome.characterID].battleStart = Date.now() 314 | } 315 | 316 | tailPromises.push(knex('ValuesBattleOutcomes').insert(battleOutcome)) 317 | setImmediate(()=>{ 318 | Promise.all(tailPromises) 319 | .then(()=> {}) 320 | .catch(console.error) 321 | }) 322 | 323 | backgroundView.nextBackground() 324 | } 325 | 326 | /** 327 | * 328 | * @param {String} characterID 329 | */ 330 | function getCharacterValues(characterID) { 331 | characterID = characterID.toString() 332 | if(characterValues[characterID]) { 333 | return Promise.resolve(characterValues[characterID]) 334 | } else { 335 | return new Promise((resolve, reject) => { 336 | knex.raw(`select * from CharacterValues where "characterID" = ?`, [characterID]) 337 | .then(queryResult => { 338 | queryResult = JSON.parse(JSON.stringify(queryResult)) 339 | let result = queryResult.sort((a, b)=>{ 340 | if(a.score === b.score) { 341 | return b.battleCount - a.battleCount 342 | } 343 | return b.score - a.score 344 | }) 345 | resolve(result) 346 | characterValues[characterID] = result 347 | }) 348 | .catch(console.error) 349 | }) 350 | } 351 | } 352 | 353 | /** 354 | * 355 | * @param {String} characterID 356 | */ 357 | function getCharacterValuesMap(characterID) { 358 | characterID = characterID.toString() 359 | if(characterValuesMap[characterID]) { 360 | return Promise.resolve(characterValuesMap[characterID]) 361 | } else { 362 | return new Promise((fulfill, reject) => { 363 | getCharacterValues(characterID) 364 | .then(values => { 365 | characterValuesMap[characterID] = {} 366 | for(let value of values) { 367 | characterValuesMap[characterID][value.valueID] = value 368 | } 369 | fulfill(characterValuesMap[characterID]) 370 | }) 371 | .catch(console.error) 372 | }) 373 | } 374 | } 375 | 376 | /** 377 | * @return array of characters 378 | */ 379 | function getCharacters() { 380 | if(characters) { 381 | return Promise.resolve(characters) 382 | } else { 383 | return new Promise((resolve, reject)=>{ 384 | knex.select().table('Characters') 385 | .then(result => { 386 | characters = JSON.parse(JSON.stringify(result)) 387 | characters = result 388 | resolve(characters) 389 | }) 390 | .catch(reject) 391 | }) 392 | } 393 | } 394 | 395 | /** 396 | * 397 | * @param {Number} characterID 398 | */ 399 | function getBattlePairer(characterID) { 400 | if(battlePairers[characterID]) { 401 | return Promise.resolve(battlePairers[characterID]) 402 | } else { 403 | return new Promise((fulfill, reject) =>{ 404 | getCharacterValues(characterID).then(characterValues => { 405 | getCharacterBattlePairs(characterID).then(characterBattlePairs => { 406 | getCharacterValueFavorites(characterID).then(favorites =>{ 407 | let properties = { 408 | choices: valuesLib, 409 | characterID: characterID, 410 | values:characterValues, 411 | valuesMap: valuesMap, 412 | favorites: favorites, 413 | battlePairs: characterBattlePairs 414 | } 415 | let battlePairer = new BattlePairer(properties) 416 | battlePairer.init() 417 | .then(()=>{ 418 | battlePairers[characterID] = battlePairer 419 | fulfill(battlePairers[characterID]) 420 | }) 421 | .catch(console.error) 422 | }) 423 | }) 424 | }) 425 | }) 426 | } 427 | 428 | return battlePairers[characterID] 429 | } 430 | 431 | 432 | /** 433 | * 434 | * @param {Number} characterID 435 | * @return {Object} has the form objects of the form { winnerID: { loserID: count }, ... } 436 | */ 437 | function getCharacterBattlePairs(characterID) { 438 | if(characterBattlePairs[characterID]) { 439 | return Promise.resolve(characterBattlePairs[characterID]) 440 | } else { 441 | return new Promise((fulfill, reject) => { 442 | knex.raw(`select * from ValuesBattleOutcomes where "characterID" = ?`, [characterID]) 443 | .then((result) => { 444 | if(!result || result.length < 1 || !characterBattlePairs[characterID]) { 445 | characterBattlePairs[characterID] = {} 446 | } 447 | for(let battleOutcome of result) { 448 | updateBattlePairs(battleOutcome) 449 | } 450 | fulfill(characterBattlePairs[characterID]) 451 | }) 452 | .catch(console.error) 453 | }) 454 | } 455 | } 456 | 457 | /** 458 | * 459 | * @param {Object} battleOutcome 460 | */ 461 | function updateBattlePairs(battleOutcome) { 462 | // update the pair count. 463 | if(!characterBattlePairs[battleOutcome.characterID]) { 464 | characterBattlePairs[battleOutcome.characterID] = {} 465 | } 466 | let battlePairs = characterBattlePairs[battleOutcome.characterID] 467 | if(!battlePairs.hasOwnProperty(battleOutcome.winner)) { 468 | battlePairs[battleOutcome.winner] = {} 469 | } 470 | let winnerPairs = battlePairs[battleOutcome.winner] 471 | if(!winnerPairs.hasOwnProperty(battleOutcome.loser)) { 472 | winnerPairs[battleOutcome.loser] = 0 473 | } 474 | winnerPairs[battleOutcome.loser] += 1 475 | 476 | // update the total count. 477 | if(!characterBattleCounts[battleOutcome.characterID]) { 478 | characterBattleCounts[battleOutcome.characterID] = 0 479 | } 480 | characterBattleCounts[battleOutcome.characterID] += 1 481 | } 482 | 483 | /** 484 | * 485 | * @param {Number} characterID 486 | * @return {Object} objects have the form { value1ID: { value2ID: true, value2ID: true }, value1ID: {value2ID: true } } 487 | */ 488 | function getCharacterValueFavorites(characterID) { 489 | if(characterValueFavorites[characterID]) { 490 | return Promise.resolve(characterValueFavorites[characterID]) 491 | } else { 492 | characterValueFavorites[characterID] = { 493 | pairs: {}, 494 | values: [] 495 | } 496 | return Promise.resolve(characterValueFavorites[characterID]) 497 | } 498 | } 499 | 500 | /** 501 | * Updates both the local cache and database of character battle favorites. 502 | * Note that this is a toggle. If the pair exists, it is deleted. 503 | * 504 | * @param {Object} battleData has the shape { character: character, value1: value, value2: value } 505 | */ 506 | function updateCharacterBattleFavorites(battleData) { 507 | let record = { 508 | value1ID: battleData.value1.id, 509 | value2ID: battleData.value2.id, 510 | characterID: battleData.character.id 511 | } 512 | if(!characterValueFavorites[record.characterID]) { 513 | characterValueFavorites[record.characterID] = { 514 | pairs: {}, 515 | values: [] 516 | } 517 | } 518 | 519 | let battlePairs = characterValueFavorites[record.characterID].pairs 520 | let favoriteValues = characterValueFavorites[record.characterID].values 521 | let value1Pairs 522 | let value2Pairs 523 | if(battlePairs.hasOwnProperty(record.value1ID)) { 524 | value1Pairs = battlePairs[record.value1ID] 525 | } 526 | if(battlePairs.hasOwnProperty(record.value2ID)) { 527 | value2Pairs = battlePairs[record.value2ID] 528 | } 529 | 530 | function removeFromFavoriteValues(valueID) { 531 | let index = favoriteValues.indexOf(valueID) 532 | if(index >= 0) { 533 | favoriteValues.splice(index, 1) 534 | } 535 | } 536 | 537 | let replacements = [record.characterID, record.value1ID, record.value2ID] 538 | if(value1Pairs && value1Pairs.hasOwnProperty(record.value2ID)) { 539 | delete value1Pairs[record.value2ID] 540 | removeFromFavoriteValues(record.value1ID) 541 | removeFromFavoriteValues(record.value2ID) 542 | knex.raw(`delete from ValuesBattleFavorites where "characterID" = ? and "value1ID" = ? and "value2ID" = ?`, replacements) 543 | .then(()=>{}) 544 | .catch(console.error) 545 | } else if(value2Pairs && value2Pairs.hasOwnProperty(record.value1ID)) { 546 | delete value2Pairs[record.value1ID] 547 | knex.raw(`delete from ValuesBattleFavorites where "characterID" = ? and "value2ID" = ? and "value1ID" = ?`, replacements) 548 | .then(()=>{}) 549 | .catch(console.error) 550 | removeFromFavoriteValues(record.value1ID) 551 | removeFromFavoriteValues(record.value2ID) 552 | } else { 553 | if(!value1Pairs) { 554 | battlePairs[record.value1ID] = {} 555 | } 556 | battlePairs[record.value1ID][record.value2ID] = true 557 | 558 | if(favoriteValues.indexOf(record.value1ID) < 0) { 559 | favoriteValues.push(record.value1ID) 560 | } 561 | if(favoriteValues.indexOf(record.value2ID) < 0) { 562 | favoriteValues.push(record.value2ID) 563 | } 564 | 565 | knex('ValuesBattleFavorites').insert(record) 566 | .then(()=>{}) 567 | .catch(console.error) 568 | } 569 | } 570 | 571 | /** 572 | * 573 | * @param {Number} characterID 574 | * @return {Number} Number of battles the character has been trained on. 575 | */ 576 | function getCharacterBattleCount(characterID) { 577 | if(!characterBattleCounts[characterID]) { 578 | characterBattleCounts[characterID] = 0 579 | } 580 | return characterBattleCounts[characterID] 581 | } 582 | 583 | /** 584 | * 585 | * @param {Number} characterID 586 | * @return {Object} Object of the shape { battleCount: count, battleStart: timestamp } 587 | */ 588 | function getCharacterSession(characterID) { 589 | if(!characterSessions[characterID]) { 590 | characterSessions[characterID] = { 591 | battleCount: 0, 592 | battleStart: 0 593 | } 594 | } 595 | return characterSessions[characterID] 596 | } 597 | 598 | function getSelectedCharacters() { 599 | return selectedCharacters 600 | } 601 | 602 | function cacheValueBattleFavorite(record) { 603 | if(!characterValueFavorites[record.characterID]) { 604 | characterValueFavorites[record.characterID] = { 605 | pairs: {}, 606 | values: [] 607 | } 608 | } 609 | 610 | var favoritePairs = characterValueFavorites[record.characterID].pairs 611 | var favoriteValues = characterValueFavorites[record.characterID].values 612 | if(!favoritePairs[record.value1ID]) { 613 | favoritePairs[record.value1ID] = {} 614 | } 615 | favoritePairs[record.value1ID][record.value2ID] = true 616 | 617 | if(favoriteValues.indexOf(record.value1ID) < 0) { 618 | favoriteValues.push(record.value1ID) 619 | } 620 | if(favoriteValues.indexOf(record.value2ID) < 0) { 621 | favoriteValues.push(record.value2ID) 622 | } 623 | } 624 | 625 | function loadValueBattleFavorites() { 626 | return new Promise((fulfill, reject) => { 627 | knex.raw(`select * from ValuesBattleFavorites`) 628 | .then((records) => { 629 | if(!records) { 630 | fulfill([]) 631 | } 632 | for(let record of records) { 633 | cacheValueBattleFavorite(record) 634 | } 635 | fulfill(records) 636 | }) 637 | .catch(reject) 638 | }) 639 | } 640 | 641 | function loadValueComparisonFavorites() { 642 | return new Promise((fulfill, reject) => { 643 | knex.raw(`select * from ValueComparisonFavorites`) 644 | .then(records => { 645 | if(!records) { 646 | return fulfill([]) 647 | } 648 | for(let record of records) { 649 | cacheValueComparisonFavorite(record) 650 | } 651 | fulfill(records) 652 | }) 653 | .catch(reject) 654 | }) 655 | } 656 | 657 | function cacheValueComparisonFavorite(record) { 658 | if(!valueComparisonFavorites[record.character1ID]) { 659 | valueComparisonFavorites[record.character1ID] = {} 660 | } 661 | if(!valueComparisonFavorites[record.character1ID][record.value1ID]) { 662 | valueComparisonFavorites[record.character1ID][record.value1ID] = {} 663 | } 664 | if(!valueComparisonFavorites[record.character1ID][record.value1ID][record.character2ID]) { 665 | valueComparisonFavorites[record.character1ID][record.value1ID][record.character2ID] = {} 666 | } 667 | if(!valueComparisonFavorites[record.character1ID][record.value1ID][record.character2ID][record.value2ID]) { 668 | valueComparisonFavorites[record.character1ID][record.value1ID][record.character2ID][record.value2ID] = {} 669 | } 670 | 671 | if(!characterComparisonFavorites[record.character1ID]) { 672 | characterComparisonFavorites[record.character1ID] = {} 673 | } 674 | if(!characterComparisonFavorites[record.character1ID][record.character2ID]) { 675 | characterComparisonFavorites[record.character1ID][record.character2ID] = [] 676 | } 677 | 678 | let comparisonRecord = {} 679 | comparisonRecord[record.character1ID] = record.value1ID 680 | comparisonRecord[record.character2ID] = record.value2ID 681 | characterComparisonFavorites[record.character1ID][record.character2ID].push(comparisonRecord) 682 | 683 | getCharacterValueFavorites(record.character1ID) 684 | .then((characterValueFavorites) => { 685 | var favoriteValues = characterValueFavorites.values 686 | if(favoriteValues.indexOf(record.value1ID) < 0) { 687 | favoriteValues.push(record.value1ID) 688 | } 689 | 690 | if(record.character1ID === record.character2ID) { 691 | var favoritePairs = characterValueFavorites.pairs 692 | if(!favoritePairs[record.value1ID]) { 693 | favoritePairs[record.value1ID] = {} 694 | } 695 | favoritePairs[record.value1ID][record.value2ID] = true 696 | } 697 | }) 698 | 699 | getCharacterValueFavorites(record.character2ID) 700 | .then((characterValueFavorites) => { 701 | var favoriteValues = characterValueFavorites.values 702 | if(favoriteValues.indexOf(record.value2ID) < 0) { 703 | favoriteValues.push(record.value2ID) 704 | } 705 | }) 706 | } 707 | 708 | function addValueComparisonFavorite(data) { 709 | cacheValueComparisonFavorite(data) 710 | knex('ValueComparisonFavorites').returning('id').insert(data) 711 | .then((result) => { 712 | console.log(`added: ${JSON.stringify(data)}`) 713 | }) 714 | .catch(console.error) 715 | } 716 | 717 | function cacheCharacterValueFavorite(record) { 718 | getCharacterValueFavorites(record.characterID) 719 | .then((characterValueFavorites) => { 720 | var favoriteValues = characterValueFavorites.values 721 | if(favoriteValues.indexOf(record.valueID) < 0) { 722 | favoriteValues.push(record.valueID) 723 | } 724 | }) 725 | } 726 | 727 | function addCharacterValueFavorite(data) { 728 | cacheCharacterValueFavorite(data) 729 | knex('CharacterValueFavorites').returning('id').insert(data) 730 | .then((result) => { 731 | console.log(`added: ${JSON.stringify(data)}`) 732 | }) 733 | .catch(console.error) 734 | } 735 | 736 | function loadCharactersValueFavorites() { 737 | return new Promise((fulfill, reject) => { 738 | knex.raw(`select * from CharacterValueFavorites`) 739 | .then(records => { 740 | for(let record of records) { 741 | cacheCharacterValueFavorite(record) 742 | } 743 | fulfill(records) 744 | }) 745 | .catch(reject) 746 | }) 747 | } 748 | 749 | function displayMessage(message) { 750 | let messageDiv = document.getElementById("message") 751 | messageDiv.innerHTML = message 752 | } 753 | 754 | function displayError(message) { 755 | let messageDiv = document.getElementById("error") 756 | messageDiv.innerHTML = message 757 | } -------------------------------------------------------------------------------- /js/views/value-list-view.js: -------------------------------------------------------------------------------- 1 | const MainBaseView = require('./main-base-view.js') 2 | const FavoriteButton = require('./favorite-button.js') 3 | 4 | module.exports = class ValueListView extends MainBaseView { 5 | constructor(properties) { 6 | super(properties) 7 | 8 | this.root.setAttribute("id", "value-list-container") 9 | 10 | this.characterSelector = document.createElement("select") 11 | this.characterSelector.setAttribute("id", "character-selector") 12 | this.characterSelector.addEventListener('change', (event)=>{ 13 | this.currentCharacterID = parseInt(event.target.value) 14 | this.onSelectCharacter(this.currentCharacterID) 15 | }) 16 | this.root.appendChild(this.characterSelector) 17 | 18 | this.isFiltering = false 19 | this.favoritesFilter = document.createElement("div") 20 | this.favoritesFilter.classList.add("favorites-filter-button") 21 | this.favoritesFilter.innerHTML = `Show Favorites` 22 | this.favoritesFilter.addEventListener("click", (event) => { 23 | this.isFiltering = !this.isFiltering 24 | if(this.isFiltering) { 25 | this.favoritesFilter.innerHTML = `Show All` 26 | } else { 27 | this.favoritesFilter.innerHTML = `Show Favorites` 28 | } 29 | this.updateView() 30 | }) 31 | this.root.appendChild(this.favoritesFilter) 32 | 33 | this.valuesViewContainer = document.createElement("div") 34 | this.valuesViewContainer.setAttribute("id", "values-view-container") 35 | this.root.appendChild(this.valuesViewContainer) 36 | 37 | this.characters = [] 38 | this.getCharacters() 39 | .then(inCharacters => { 40 | this.characters = inCharacters 41 | 42 | let selectedCharacter 43 | let selectedCharacters = this.getSelectedCharacters() 44 | if(selectedCharacters && selectedCharacters.length) { 45 | selectedCharacter = selectedCharacters[0] 46 | } else if(this.characters && this.characters.length) { 47 | selectedCharacter = this.characters[0] 48 | } 49 | 50 | this.characterSelector.innerHTML = `` 51 | for(let character of this.characters) { 52 | 53 | let option = document.createElement("option") 54 | option.setAttribute("value", character.id) 55 | option.innerHTML = character.name 56 | this.characterSelector.appendChild(option) 57 | if(selectedCharacter && selectedCharacter.id === character.id) { 58 | option.setAttribute("selected", true) 59 | } 60 | } 61 | this.onSelectCharacter(selectedCharacter.id) 62 | }) 63 | .catch(console.error) 64 | } 65 | 66 | updateView() { 67 | if(!this.currentCharacterID) { 68 | return 69 | } 70 | 71 | this.valuesViewContainer.innerHTML = `` 72 | 73 | this.getCharacterValues(this.currentCharacterID) 74 | .then(characterValues => { 75 | this.valuesView = this.getValuesView(characterValues) 76 | this.valuesViewContainer.appendChild(this.valuesView) 77 | }) 78 | .catch(console.error) 79 | } 80 | 81 | onSelectCharacter(characterID) { 82 | this.currentCharacterID = characterID 83 | this.updateView() 84 | } 85 | 86 | getValuesView(values) { 87 | let result = document.createElement('div') 88 | result.setAttribute("id", "values-view") 89 | 90 | for(let value of values) { 91 | this.getCharacterValueFavorites(this.currentCharacterID) 92 | .then(characterValueFavorites => { 93 | let favoriteValues = characterValueFavorites.values 94 | let isFavorite = favoriteValues.indexOf(value.valueID) >= 0 95 | 96 | let valueView = document.createElement('div') 97 | valueView.classList.add("value-list-container") 98 | 99 | let favButtonProperties = { 100 | checked: isFavorite, 101 | enabled: !isFavorite, 102 | className: "favorite-button-container-value-list-view" 103 | } 104 | 105 | var self = this 106 | let favButton = new FavoriteButton(favButtonProperties) 107 | favButton.setHandler(function(event) { 108 | self.emit('add-character-value-favorite', {valueID: value.valueID, characterID: self.currentCharacterID}) 109 | favButton.setChecked(true) 110 | favButton.setEnabled(false) 111 | }) 112 | valueView.appendChild(favButton.getView()) 113 | 114 | let progressView = document.createElement('div') 115 | progressView.setAttribute("class", "value-list-progress") 116 | progressView.setAttribute("style", `width: ${value.score*100}%`) 117 | valueView.appendChild(progressView) 118 | let nameView = document.createElement('div') 119 | nameView.setAttribute("class", "value-list-label") 120 | nameView.innerHTML = `${this.valuesMap[value.valueID.toString()].name} | ${value.score} | Wins: ${value.wins}, Losses: ${value.losses} | Battles: ${value.battleCount}` 121 | valueView.appendChild(nameView) 122 | if(this.isFiltering && isFavorite) { 123 | result.appendChild(valueView) 124 | } else if(!this.isFiltering) { 125 | result.appendChild(valueView) 126 | } 127 | }) 128 | .catch(console.error) 129 | } 130 | return result 131 | } 132 | 133 | } -------------------------------------------------------------------------------- /js/views/welcome-window.js: -------------------------------------------------------------------------------- 1 | const {ipcRenderer, shell, remote} = require('electron') 2 | const knex = remote.getGlobal('knex') 3 | const prefsModule = require('electron').remote.require('./js/prefs.js') 4 | const fs = require('fs') 5 | 6 | let container = document.getElementById("container") 7 | let recentContainer = document.createElement("div") 8 | recentContainer.setAttribute("id", "recent-container") 9 | let recentHeader = document.createElement("h2") 10 | recentHeader.classList.add("recent-documents-header") 11 | recentHeader.innerHTML = `Recent Documents` 12 | recentContainer.appendChild(recentHeader) 13 | container.appendChild(recentContainer) 14 | 15 | let recentDocumentsList = document.createElement("div") 16 | recentDocumentsList.classList.add("flex-wrap") 17 | recentDocumentsList.classList.add("recent-documents-list") 18 | recentContainer.appendChild(recentDocumentsList) 19 | 20 | function updateRecentDocuments() { 21 | recentDocumentsList.innerHTML = `` 22 | let recentDocuments = prefsModule.getPrefs('welcome')['recentDocuments'] 23 | if (recentDocuments && recentDocuments.length>0) { 24 | for (var recentDocument of recentDocuments) { 25 | try { 26 | fs.accessSync(recentDocument.filename, fs.R_OK) 27 | } catch (e) { 28 | // It isn't accessible 29 | continue 30 | } 31 | let recent = document.createElement("div") 32 | recent.classList.add("recent-document") 33 | recent.classList.add("button") 34 | recent.innerHTML = recentDocument.title 35 | recent.setAttribute("data-filename", recentDocument.filename) 36 | recent.addEventListener("click", (event)=>{ 37 | event.target.setAttribute("disabled", true) 38 | ipcRenderer.send("open-project", event.target.dataset.filename) 39 | }) 40 | recentDocumentsList.appendChild(recent) 41 | } 42 | } 43 | } 44 | 45 | let buttonContainer = document.createElement("div") 46 | buttonContainer.setAttribute("id", "button-container") 47 | container.appendChild(buttonContainer) 48 | 49 | let newButton = document.createElement("div") 50 | newButton.innerHTML = "New Project" 51 | newButton.classList.add("button") 52 | newButton.addEventListener('click', event => { 53 | ipcRenderer.send('new-project') 54 | }) 55 | buttonContainer.appendChild(newButton) 56 | 57 | let openButton = document.createElement("div") 58 | openButton.innerText = "Open Project" 59 | openButton.classList.add("button") 60 | openButton.addEventListener('click', event => { 61 | ipcRenderer.send('browse-for-project') 62 | }) 63 | buttonContainer.appendChild(openButton) 64 | 65 | ipcRenderer.on('update-recent-documents', (event, args)=>{ 66 | updateRecentDocuments() 67 | }) 68 | 69 | updateRecentDocuments() 70 | 71 | -------------------------------------------------------------------------------- /loading-status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 45 | 46 | 47 | 48 |
49 |
Loading
50 |
Initializing …
51 |
52 | 53 | 54 | 73 | 74 | -------------------------------------------------------------------------------- /main-window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Characterizer 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 | 17 |
18 |
19 |
20 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const {app, ipcMain, BrowserWindow, dialog} = require('electron') 2 | const path = require('path') 3 | const url = require('url') 4 | const fs = require('fs') 5 | const userDataPath = app.getPath('userData') 6 | const valuesSeedDataPath = path.join(__dirname, "data", "values.txt") 7 | const {seedDB} = require('./js/database-init.js') 8 | const prefModule = require('./js/prefs.js') 9 | const utils = require('./js/utils.js') 10 | const trash = require('trash') 11 | const autoUpdater = require('./auto-updater') 12 | const isDev = require('electron-is-dev') 13 | 14 | let welcomeWindow 15 | let loadingStatusWindow 16 | let mainWindow 17 | 18 | function createWelcomeWindow () { 19 | welcomeWindow = new BrowserWindow({width: 800, height: 600}) 20 | welcomeWindow.loadURL(url.format({ 21 | pathname: path.join(__dirname, 'welcome-window.html'), 22 | title: "Characterizer", 23 | protocol: 'file:', 24 | slashes: true 25 | })) 26 | 27 | welcomeWindow.on('closed', () => { 28 | mainWindow = null 29 | }) 30 | 31 | if (!isDev) { 32 | autoUpdater.init() 33 | } 34 | } 35 | 36 | app.on('ready', createWelcomeWindow) 37 | 38 | app.on('window-all-closed', () => { 39 | if (process.platform !== 'darwin') { 40 | app.quit() 41 | } 42 | }) 43 | 44 | ipcMain.on('open-project', (e, arg)=>{ 45 | showMainWindow(arg) 46 | addToRecentDocs(arg) 47 | }) 48 | 49 | ipcMain.on('browse-for-project', (e, arg)=> { 50 | browseForProject() 51 | }) 52 | 53 | function browseForProject() { 54 | let properties = { 55 | title:"Open Script", 56 | filters:[ 57 | { 58 | name: 'Database File', 59 | extensions: ['sqlite'] 60 | } 61 | ] 62 | } 63 | dialog.showOpenDialog(properties, (filenames)=>{ 64 | if (filenames) { 65 | showMainWindow(filenames[0]) 66 | addToRecentDocs(filenames[0]) 67 | } 68 | }) 69 | } 70 | 71 | ipcMain.on('new-project', (e, arg)=> { 72 | createNewProject() 73 | }) 74 | 75 | ipcMain.on('log', (event, opt) => { 76 | !loadingStatusWindow.isDestroyed() && loadingStatusWindow.webContents.send('log', opt) 77 | }) 78 | 79 | ipcMain.on('workspace-ready', event => { 80 | mainWindow && mainWindow.show() 81 | !loadingStatusWindow.isDestroyed() && loadingStatusWindow.hide() 82 | }) 83 | 84 | 85 | function createNewProject() { 86 | let properties = { 87 | title: "New Project", 88 | buttonLabel: "Create", 89 | } 90 | dialog.showSaveDialog(properties, filename => { 91 | if(!filename) { 92 | return 93 | } 94 | function openWindow() { 95 | fs.mkdirSync(filename) 96 | let projectName = path.basename(filename) 97 | let dbFileName = path.join(filename, projectName + '.sqlite') 98 | showMainWindow(dbFileName) 99 | addToRecentDocs(dbFileName) 100 | } 101 | 102 | if(fs.existsSync(filename)) { 103 | if(fs.lstatSync(filename).isDirectory()) { 104 | console.log('\ttrash existing folder', filename) 105 | trash(filename) 106 | .then(openWindow) 107 | .catch(console.error) 108 | } else { 109 | dialog.showMessageBox(null, { 110 | message: "Could not overwrite file " + path.basename(filename) + ". Only folders can be overwritten." 111 | }) 112 | return 113 | } 114 | } else { 115 | openWindow() 116 | } 117 | }) 118 | } 119 | 120 | function showMainWindow(dbFile) { 121 | let basename = path.basename(dbFile) 122 | if(!loadingStatusWindow) { 123 | loadingStatusWindow = new BrowserWindow({ 124 | width: 450, 125 | height: 150, 126 | backgroundColor: '#333333', 127 | show: false, 128 | frame: false, 129 | resizable: false 130 | }) 131 | } 132 | 133 | loadingStatusWindow.loadURL(`file://${__dirname}/loading-status.html?name=${basename}`) 134 | loadingStatusWindow.once('ready-to-show', () => { 135 | loadingStatusWindow.show() 136 | }) 137 | 138 | try { 139 | var knex = require('knex')({ 140 | client: 'sqlite3', 141 | connection: { 142 | filename: dbFile 143 | } 144 | }) 145 | global.knex = knex 146 | 147 | let migrationsPath = path.join(__dirname, `migrations`) 148 | 149 | knex.migrate.latest({directory: migrationsPath}) 150 | .then(()=> { 151 | return seedDB(knex, {valuesSeedDataPath}) 152 | }) 153 | .then(()=>{ 154 | if(mainWindow) { 155 | mainWindow.close() 156 | } 157 | mainWindow = new BrowserWindow({ 158 | width: 800, 159 | height: 600, 160 | show: false, 161 | title: basename, 162 | }) 163 | mainWindow.loadURL(url.format({ 164 | pathname: path.join(__dirname, 'main-window.html'), 165 | protocol: 'file:', 166 | slashes: true 167 | })) 168 | mainWindow.on('closed', () => { 169 | mainWindow = null 170 | }) 171 | }) 172 | .catch(error => { 173 | // The loading status window doesn't receive the message unless it's delayed a little 174 | setTimeout(()=>{ 175 | loadingStatusWindow.webContents.send('log', { type: "progress", message: error.toString()}) 176 | }, 500) 177 | }) 178 | } catch(error) { 179 | setTimeout(()=>{ 180 | loadingStatusWindow.webContents.send('log', { type: "progress", message: error.toString()}) 181 | }, 500) 182 | } 183 | 184 | } 185 | 186 | let addToRecentDocs = (filename, metadata={}) => { 187 | let prefs = prefModule.getPrefs('add to recent') 188 | 189 | let recentDocuments 190 | if (!prefs.recentDocuments) { 191 | recentDocuments = [] 192 | } else { 193 | recentDocuments = JSON.parse(JSON.stringify(prefs.recentDocuments)) 194 | } 195 | 196 | let currPos = 0 197 | 198 | for (var document of recentDocuments) { 199 | if (document.filename == filename) { 200 | recentDocuments.splice(currPos, 1) 201 | break 202 | } 203 | currPos++ 204 | } 205 | 206 | let recentDocument = metadata 207 | 208 | if (!recentDocument.title) { 209 | let title = filename.split(path.sep) 210 | title = title[title.length-1] 211 | title = title.split('.') 212 | title.splice(-1,1) 213 | title = title.join('.') 214 | recentDocument.title = title 215 | } 216 | 217 | recentDocument.filename = filename 218 | recentDocument.time = Date.now() 219 | recentDocuments.unshift(recentDocument) 220 | // save 221 | prefModule.set('recentDocuments', recentDocuments) 222 | 223 | if(welcomeWindow) { 224 | welcomeWindow.webContents.send('update-recent-documents') 225 | } 226 | } -------------------------------------------------------------------------------- /migrations/001-setup.js: -------------------------------------------------------------------------------- 1 | module.exports.up = function(knex, Promise) { 2 | let characterQuery = knex.schema.hasTable('Characters').then((exists) => { 3 | if(!exists) { 4 | return knex.schema.createTable('Characters', function(table) { 5 | table.string('name') 6 | table.increments('id').primary() 7 | table.timestamps(false, true) 8 | }) 9 | } else { 10 | return Promise.resolve(true) 11 | } 12 | }) 13 | 14 | let valuesQuery = knex.schema.hasTable('Values').then((exists) => { 15 | if(!exists) { 16 | return knex.schema.createTable('Values', function(table) { 17 | table.string('name') 18 | table.increments('id').primary() 19 | table.string('uuid') 20 | table.timestamps(false, true) 21 | }) 22 | } else { 23 | return Promise.resolve(true) 24 | } 25 | }) 26 | 27 | let collectionsQuery = knex.schema.hasTable('Collections').then((exists) => { 28 | if(!exists) { 29 | return knex.schema.createTable('Collections', function(table) { 30 | table.string('name') 31 | table.increments('id').primary() 32 | table.timestamps(false, true) 33 | }) 34 | } else { 35 | return Promise.resolve(true) 36 | } 37 | }) 38 | 39 | let characterValuesQuery = knex.schema.hasTable('CharacterValues').then((exists) => { 40 | if(!exists) { 41 | return knex.schema.createTable('CharacterValues',function(table){ 42 | table.increments('id').primary() 43 | table.integer('characterID').references('id').inTable('Characters') 44 | table.integer('valueID').references('id').inTable('Values') 45 | table.integer('wins') 46 | table.integer('losses') 47 | table.integer('battleCount') 48 | table.float('score') 49 | table.timestamps(false, true) 50 | }) 51 | } else { 52 | return Promise.resolve(true) 53 | } 54 | }) 55 | 56 | let valuesCollectionsQuery = knex.schema.hasTable('ValuesCollections').then((exists) => { 57 | if(!exists) { 58 | return knex.schema.createTable('ValuesCollections',function(table){ 59 | table.increments('id').primary() 60 | table.string('name') 61 | table.integer('valueID').references('id').inTable('Values') 62 | table.timestamps(false, true) 63 | }) 64 | } else { 65 | return Promise.resolve(true) 66 | } 67 | }) 68 | 69 | let valuesBattleOutcomesQuery = knex.schema.hasTable('ValuesBattleOutcomes').then((exists) => { 70 | if(!exists) { 71 | return knex.schema.createTable('ValuesBattleOutcomes',function(table){ 72 | table.increments('id').primary() 73 | table.integer('characterID').references('id').inTable('Characters') 74 | table.integer('loser').references('id').inTable('Values') 75 | table.integer('winner').references('id').inTable('Values') 76 | table.timestamps(false, true) 77 | }) 78 | } else { 79 | return Promise.resolve(true) 80 | } 81 | }) 82 | 83 | let valuesBattleFavorites = knex.schema.hasTable('ValuesBattleFavorites').then((exists) => { 84 | if(!exists) { 85 | return knex.schema.createTable('ValuesBattleFavorites',function(table){ 86 | table.increments('id').primary() 87 | table.integer('characterID').references('id').inTable('Characters') 88 | table.integer('value1ID').references('id').inTable('Values') 89 | table.integer('value2ID').references('id').inTable('Values') 90 | table.timestamps(false, true) 91 | }) 92 | } else { 93 | return Promise.resolve(true) 94 | } 95 | }) 96 | 97 | return Promise.all([ 98 | characterQuery, 99 | valuesQuery, 100 | collectionsQuery, 101 | characterValuesQuery, 102 | valuesCollectionsQuery, 103 | valuesBattleOutcomesQuery 104 | ]) 105 | } 106 | 107 | module.exports.down = function(knex, Promise) { 108 | 109 | } -------------------------------------------------------------------------------- /migrations/002-favorites.js: -------------------------------------------------------------------------------- 1 | module.exports.up = function(knex, Promise) { 2 | let ValueComparisonFavorites = knex.schema.hasTable('ValueComparisonFavorites').then((exists) => { 3 | if(!exists) { 4 | return knex.schema.createTable('ValueComparisonFavorites', function(table) { 5 | table.integer('character1ID').references('id').inTable('Characters') 6 | table.integer('value1ID').references('id').inTable('Values') 7 | 8 | table.integer('character2ID').references('id').inTable('Characters') 9 | table.integer('value2ID').references('id').inTable('Values') 10 | 11 | table.timestamps(false, true) 12 | table.increments('id').primary() 13 | }) 14 | } else { 15 | return Promise.resolve(true) 16 | } 17 | }) 18 | 19 | let CharacterValueFavorites = knex.schema.hasTable('CharacterValueFavorites').then((exists) => { 20 | if(!exists) { 21 | return knex.schema.createTable('CharacterValueFavorites', function(table) { 22 | table.integer('characterID').references('id').inTable('Characters') 23 | table.integer('valueID').references('id').inTable('Values') 24 | table.timestamps(false, true) 25 | table.increments('id').primary() 26 | }) 27 | } else { 28 | return Promise.resolve(true) 29 | } 30 | }) 31 | 32 | return Promise.all([ 33 | ValueComparisonFavorites, 34 | CharacterValueFavorites 35 | ]) 36 | } 37 | 38 | module.exports.down = function(knex, Promise) { 39 | 40 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "characterizer", 3 | "version": "0.4.0", 4 | "description": "The best app to train character values and generate conflicts, dilemmas, and common ground between characters.", 5 | "main": "main.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/wonderunit/characterizer.git" 9 | }, 10 | "scripts": { 11 | "start": "electron .", 12 | "postinstall": "install-app-deps", 13 | "pack": "electron-builder --dir", 14 | "dist": "electron-builder", 15 | "dist:mac": "build -m", 16 | "dist:win": "build -w", 17 | "dist:linux": "build -l" 18 | }, 19 | "devDependencies": { 20 | "electron": "4.0.0-beta.7", 21 | "electron-builder": "20.36.2" 22 | }, 23 | "dependencies": { 24 | "electron-updater": "4.0.4", 25 | "knex": "^0.15.2", 26 | "nan": "^2.7.0", 27 | "sqlite3": "4.0.4", 28 | "trash": "^4.0.1", 29 | "electron-is-dev": "1.0.1" 30 | }, 31 | "build": { 32 | "asar": true, 33 | "appId": "com.wonderunit.characterizer", 34 | "mac": { 35 | "icon": "build/icon.icns" 36 | }, 37 | "dmg": { 38 | "background": "build/background.png", 39 | "icon": "build/icon.icns", 40 | "iconSize": 140, 41 | "contents": [ 42 | { 43 | "x": 120, 44 | "y": 250 45 | }, 46 | { 47 | "x": 420, 48 | "y": 250, 49 | "type": "link", 50 | "path": "/Applications" 51 | } 52 | ] 53 | }, 54 | "win": { 55 | "icon": "build/icon.ico" 56 | }, 57 | "files": [ 58 | "**/*", 59 | "!*.md", 60 | "!README.md" 61 | ], 62 | "nsis": { 63 | "perMachine": true 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /update.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Auto Update 6 | 7 | 8 |

Downloading …

9 | 10 |
11 |
12 |
13 |
14 |
15 | 0% 16 |
17 |
18 | 19 |

Release Notes

20 |
21 | 22 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /welcome-window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Characterizer 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 | 19 | 20 | --------------------------------------------------------------------------------