Scene Metrics Genre additions.

This commit is contained in:
Nick Beckley 2026-06-03 14:05:47 +01:00
parent b96e295f53
commit 4989724ae0
27 changed files with 770 additions and 55 deletions

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTW/UMBD9K5HP69R2HCfOrdqCWBXoClaAuE08E2o1dVaOU0DV/nfk7CIoNy6WNfKbeR/jZxbgkVjHro/HV08U0sw2LPm1pIQyXBguqoNUnWg6pUrRVE1jm69sw/wt/WQda7Rz0lniFRniGkXN+2pwHJGsFBKFqnXuCd9m1j0z8CXSk3dUTvMnirOfAuvYZx9w+p5ngy/dOC1YxmmkXZgTBJfJvN9tb/nb6/3hbn9+NdOcwaVH1rG2r6zswXLqXc+1bomDdooPvR5AtAJFk0mAL31IFAOM5YwPf+bjlAIl16lSVaXgyrLThiEkyJR7mOnw85hZrBbd5PpmLa/X7pk9UWSd2vw289zuyo3+6nHuFz/i1flMMD9klcc4HSkmT6snq8xxhOSnUFwmbUdPIbENu/tY7EdIwxQfXxj18ax/9x/yD3n6qx/klkS4nZaQWMeklmzD3oG794GK3Q3rmJCksGqJD0oarlsEDnVluTGVGUg626iM2U4h+bBMy1zsQqJvcRXAOvYaxpnO1K+ju/eJXFpiVvXFZB77OOHiUvHHfylKUVZCZNCSjksqPhD6SC5R1neIy6XfPxBl1AraLjFSSMUe0n3xBuZ71jELrZNgwQIY5waqBQ0CBRjRIBk0hFphg9pqNNoKZzT0UiPUsh9yhW0Y5biLdcN05SoLBnjfC+C6R+J9IxzXUhlraiNq4174WNyNGQeuFZqw4pYG5NpK4FZXFceqhkaZoWnU8Jcll/SlyAYjPZ3/ysrAILaERnKlZc21tZK3tm5437S97U1LtcucbykGGv8y6p13cZqnIRWX5SleWPdhCfm/F+siffeB/1gzupncA8UiZww+5PW+xHo6nU6/AAAA//8DAByDI8o2BAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTwW7cOAyGX8XQ2XIkWZYt34LJLjpIuxm0QVv0Rot0R6gjD2Q5bRHMuy/kmSLt3vYm/BLJjz+pFxbgiVjPbk+nv54ppIWVLPlNUkIZLgwX9aNUvWh7pSrRmrpT8gsrmb+nn6xnrXZOOku8JkNco2j4UI+OI5KVQqJQjc454evC+hcGvkJ69o6qeflIcfFzYD375APO33Nt8JWb5hWrOE+0D0uC4DLMP/vdPX97e3h8OFxeLbTk4Moj61k31FYOYDkNbuBad8RBO8XHQY8gOoGizRDgKx8SxQBTteC31/o4p0DJ9apSdSW4suxcMoQEGXmAhR5/njLFZtFd1stN3o79C3umyHpV/jLzku7GTf7GzU9PEPBm9MEvR1ayU5xPFJOnzY6tw2mC5OdQXIvsJk8hsZI9fCgOE6Rxjk9/ePTh0vr+f3T+DtzRByr2d6xnQpLCuiM+Kmm47hA4NLXlxtRmJOlsqyQr2W4OyYd1XpdiHxJ9jRsl69nfMC104buN7ugTubTGjP7Z5GL0w6fdjFkQrGSHOOPqUvFqtxSVqGqRLx/WdFpT8Z7QR3KJck+Pcb2m/0+IMmoL2q0xUkjFAdKxeAPLkfXMQuckWLAAxrmRGkGjQAFGtEgGDaFW2KK2Go22whkNg9QIjRzGrGTuPN1iW6hmEOBsp7mzznHdWOTWAXA3CNs2Cgx0GfHV1uJhynHgOqEJa25pRK6tBG51XXOsG2iVGdtWjb9Zcp24FNlvpOfL19gIDGJHaCRXWjZcWyt5Z5uWD2032MF01LjMfE8x0PSbUe+8i/Myj6m4Lkzxh3Xv15C/d7Etz3cf+I9tZHez+0axyCMHH/I2X6d8Pp/P/wIAAP//AwBVXz3zJQQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzm6Juht2iCzf1Il60QW8UZxQL0VILito0MPa/F9Rukbi33ISBZvjOM+/MG4n+iKQj96fTL2eMeSE1yeMWEkwYygxl8sBFx9qOs0YwI1vp/iY1GZ/wG+mIVSHw4JBKNEgVME17OQQKgI4zDkxoVWr6zwvp3ogfG8DzGLCZlz8xLeMcSUf+GiPMX8vbfmzCNK/QpHnCXVyyj6GI+WP38ER/v98fnvfXvxZcSnIzAulI0NKjVkAd2J4qDj3tVdtTBlI63jKPwl7TxpgxRT81C3z5/j7MOWIOnWiEbBgVjlxqAj77Irn3Cx6+nYqKDdFjiddbePvs3sgZE+lE/R/Ma7m7MI13y9pPeMbp5NOC6S7Mx6OPQGpySvMJUx5xw3LG1M/LmAvREKxvW86h9VZpC63UQlqQDKFlVoMD540ygwhaD9C3zqpBgeAKDCrVl0Y3cNPk8zjH6qb9YRoxZlKT55dqP/k8zOn4Dv3LlejuJ4AW2aQjkmlprEUlpfTO9IIhWBOMEX3wzA69QB6Qaaes45Zx6QRzUhvbWvDMBVKTDz68jhGr3SPpCOMoQLZIB8ENVS146rV01BhpBuTBWcFJTR7mmMe4zutS7WLGz2nrl3TkVz8teO30PoXXMWPIayoQPplixn2aYQ25+m4AzhrWSMZK0ppPa64+IowJQ8aC45DWW73/pQgjtqSHNSWMudr7/Fr95pdX0hHn28C98857E8KAmuHAgHnDLKABg6AEWFBOgVGOBaN8zxV4zfuhREhNsPit2izOJfYtd5YiV5wq2w60N5ZTjm5AXfYS/TuO1fNU8nxomUKQ1OEAVDnuqVNSUpDaW2EGa8XwA5KbWTgrgAHP12XdFBiAFsFwKhTXVDnHaeu0pb1te9ebFnUomp8wRZx+APVhDGle5iFXN69V79B9XGM5ONXmu69jpP9sM3qcwxdMVZmxH2PZr9tYLzU5ol/WhMftXnVvZF+WqzpsZ0vwRiuha/KSfcrr6RaWtpFOqcvlcvkXAAD//wMAUD50S/QEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G3qOumXsSLtOiN5IxsIVpqQVGbBsb+94DaDRL31psw0DszfOadeSPRHZD05PZ4/OWEMS+kJnncQoIJTZmmTO656FnXc9ZwI5VQ8h9Sk/ERv5KeGBUCDxapRI1UAWupl0OgAGg548BEq0pO97KQ/o24sQE8jQGbefmEaRnnSHry1xhh/lJqu7EJ07xCk+YJH+KSXQylmT8f7h7pH7e7/dPu8teCSxE3I5CehFY6bBVQC8ZTxcFTrzpPGUhpecccCnORjTFjim5qFvj8oz7MOWIOvWiEbBgVlpxrAi670rJ3C+6/HksXG6L7Eq+38PbZv5ETJtKL+jvMS7qbMI03eT5OeMLp6NKC6SbMh4OLQGpyTPMRUx5xw7K9dJpcHudYXYvdTSPGTGry9FztJpeHOR3esXq+IHj4HwROmDzpiWSt1MagklI6q71gCEYHrYUPjpnBC+QBWWuVsdwwLq1gVrbadAYcs4HU5GWavZt+XzZ+VjDfATNCC1RohdLaBoayw6E1yMBp7XUXLMjOCNVBJ511jluPJlgdikE+uPA6Rqwe7klPGEcBskM6CK6p6sBR10pLtZZ6QB6sEZzU5G6OeYzrvC7VQ8z4kjZ+pCe/umnBC7nbFF7HjCGvqUD9W5diuzTDGnL1wwGcNayRjBXRmo9rrj4ijAlDxoJ3n9Zrvv9IhBab6G5NCWOudi6/Vr+55bVQcV3gzpan6hAGbBkODJjTzABq0AhKgAFlFWhlWdDKea7AtdwPJUJqgsVw1eZxLblnMgzUc2sKE6RedoyCYq0QXniv2TuO1dNUdC50TCFIanEAqix31CopKcjWGaEHY8TwE5Kr+TgrgAFPl229dADQIWhOheItVdZy2tnWUG86b73usA2l50dMEaefQH0YQ5qXecjV1bvVO3Qf11guTrX5+MsY6b/bjO7n8BlTVWbsxlgW7DrWc00O6JY14WE7WP0b2ZXtqvbb3RK8aZVoa/KcXcrr8RqWppFWqfP5fP4GAAD//wMAdisCNPUEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUS2/bOBD+KwLPpkJSFCUK2IPhtFgjTWMk7u5ibxQ5jInIokFRToMi/31BWa6dooe9CaN5fY/hD9SrPaAGLQ+HT0fo44AWKLopxAgTmAhMii1lDakbSvKy4qSS9F+0QO4O3lCDKq411RJwAQIwN6TEbWE1NgYkJdQQVvLUUz0PqPmBlMsNHJ2G3A9/QRic71GD/na98a9ptnK57vxo8uA7WPdDVL1Oy3xdr+7wl+Vm+7A5ZQ0wpOLcGdQgXRYKSm6wNFWLOTUtbnndYmKKQtKaKGDVqcz1EUKvunwwL5f5xsceom5YzoqcYCbR+wIZFVVauVUDbN8OaYuJotsUX0zh6bP5gY4QUMMWZzJP7W505272Qzu6ztxEFZ4h2qD28OrDCxxVhxboEPwBQnQwcfPwlF1WoiQnOROMELRA90rvXA/Z+jZ76BJgpWvCwRRYgjWYS6qw5EWBTVGqiglbVcyiBbqD0EN31fXe6eAHb2M2U559mLPyfXT96MchW/cRnoOKp7rPqhsALdBmbDs37B5BmbetfxzTv37sEpbVGAL0MduouMv+VMMONUiqWlMllVRKaG2hJGCJIUqQyoAwAgxnpjJcciO4JFpw1VJuVElbmyJogR7GeBjjmf/vV0tsg9vvwVw22E4cbzoVrQ/7tYE+OuuSNOeMyVBdN6HK5p6rzkEfP5CMGkQoMFPUgC2jAvPaKKzKQmIhCmGBalkxeiLMuufxJ0/EWsMLW9a1aGVLCTBb1ZxUtiVG20JLU7ZQsYpUNdRSE9KWnNat1MQALblJ6II3o46/WqGYBHoc+3Sd2TrBfnU9/i74hZFN8NFr3yXBXAdPb0OE/e9UW/n9wQ8uJvx2lvbbAPe+T3/n+585m4uXPl5ikO4gm06PSQaFMC0ubCEwL1qLaysFVpyyumZlIblGC3Tr9QuELBlMuX7S5Oyph6dsGfTORdBxDGnyPxOok5yfzydz4SP/+mm78gGWh8Nijv5xTCSdmp0N8OFd+Tak9J0f4hXkGdoTdPa82JWdDBxPT5VLQWFMDUZQzDgtMZeS4lqWFW6rupWtqKHUF7tmj2BcAB2nftswpmkzs7/z5TJEZ5WOQzqeL15Pdpr9Oaf8uuSVbD+rT8OvpJutNHeiJFn26fRwTg76X+/m+/v7+38AAAD//wMAvph9rCgGAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUXW/jKBSG/4rFtXEBY2x81013tFGn02ianV3tHYbjFNWBCHBnqyr/fYSTqumM9mLvrGPe8/GcF16RU3tAPbo+HH5/BpciKlGyS4gRJjARmNRbynrS9ZRUrOas5fQfVCJ7Cy+oRy3XmmoJuAYBmBvS4KEeNTYGJCXUENbwnFPtIupfkbKVgWerofLxG4RovUM9+ss647/n2spWevKzqYKfYO1iUk7nZr6sV7f48/Vme785nYoQs7iyBvVIN7WChhssTTtgTs2AB94NmJi6lrQjClh7klmXIDg1VdE8vdc3PjlIumcVqyuCmUTHEhmVVG55UBG2L4fcxYLoJsfLJbx89q/oGQLqWfkG85TuSk/2ah+H2U7mavK7nXW7lXej3c1BpVy4RIfgDxCShQXOb9ap8PLZ73Y5H/qkpgioRFsIe+vUdPpxA6Oap/Tg57CQiebpv86gHqk5eVSiheQ0LWWL8zCryYJLv2gvCt8/FJtJpdGH/YclPZzYr/8H+julH62DYn2DekQoMFN3gEdGBeadUVg1tcRC1GIEqmXLKCrRyrtk3eznWKxdgt2Z2of+roN+tAl0mkOe6W+RzbYJ3sw6Fe8LpqQiVU1IFs3pMKfiKxgbQCfIQ2zDfM73k4QJtog+2Ql+wbOaQwCXio1Kj8UfKj6iHknVaaqkkkoJrUdoCIzEECVIa0AYAYYz0xouuRFcEi24Gig3qqHDmCOoRJBNViy+HnQzjGJguIaxxVxojgcpNVZCt11HJe2o+AC3uJ+yTumOcDA1ljAazCVVWPK6xqZuVMvE2LZsvOB0NgQlmfqlCf+MYM5e+nKy9tvsBp5P93jpUxjTgREUM04bzKWkuJNNi4e2G+QgOmh0nmzlXfRvIL9BGHy0KT8id9bZvZp+PvK+mVsIDqaL7dxZHXz0YyrOtiw+7Ovr7PIrViwW/W4d/ncxxo3XTxCKbCxl3cUyj8fj8QcAAAD//wMA8IdRlQwFAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227cNhD9FYGvFRXeRJF6c20HXaSOjeyiLfo2JEc2a5laUJSTwPC/F9LauaJAXwRhSM45c+bMPJEED0h6cnY8Xj5iKjOpSYlbSDChKdOUyQMXPTM95w23nCvb/U1qEt/hZ9KTTnnPvUUqUSNVgbXUycHTENByxgMTrVpzwu1M+icCsQn4GD020/wH5jlOifTkz5jC9HHFhtj4cVpCk6cRd2kukPxK5v3u/B39/ezmcH1zujXjvD5uYiA98a0EbFWgNnSOKh4cdco4yoKUlhsGKLrTs5gK5gRjM4f7r/hhKgmL70UjZMOosOS5JgEKrJQdzHj4fFxZbBJdrPF6C2+//RN5xEx6Ub+KeUr3xo/xzcPsljiGN9uX1OSYpyPmEnFT43pffSXBWcMaoQVjpCZX4O9iwmp3UV2Pa4ngDVMYJLU4BKosB2qVlDTIFjqhh64TA6nJO8wJx2+yXkWfp3kaSvUicvUdzv7s/DKBG3HFeAvjjKQmu3lfIAUYp4SXn9Av5ZTrkJf1+HxKJaZlWuZqlwreZng5f31/vuSMqVQ3UO6q32C+Iz2xYDwHCxZAez9gy3BggYFmXUAdNAYlQheUVUEry7xW4LgK0HI3rBFSk19XDfeL9zjPX9lsJhnHjUP10qfzMWIq38lIesI4iiAN0kFwTZUJQKGVlmot9YDc207wV5gD5FsspCeDcc4x751QlnsQFmQYmAx6sEJ0HahBtp0SWresAx+6TnAVZIce5CCZEaQmN3kKiy8/tlpuDbjJ0z/oyyoV6YlmbWs8OCe04gasQcW1MtpiZ9UATEnvgLcGnOMMWuatER64tqBcaIcvKl2m25jwIs7HET5/g2sa3chfPIDhA6gfrr/N8IAfp3z//mTj5v3lgdTkw5LWjVDtVot8jIl+0utEXy1jiYe7jBAwXE0Bf/YRruNSnSaUW9SiU7RToqXKDJYacIy2zPFWGtcalKQmF5O/x1ytDoOY1qn6kux6X51lfxcL+rLkld9fG4/rfXUzQhmm/PDdJvmmsB8FaITkbO1MwMfTNto46hAMBs2pULylylpOjW076jrjrNMGW78qdr2U41KqDxhiRl+2il+8uGGe36G//1mMVxe8WJSz1Wv70xbbpP1fS+y5Jg8I85LxYdvW/RPZpYR5Q75YTqO4S1dxHOOMfkphJr3VurGGiReC/3WNMy6ajmn7/Pz8/C8AAAD//wMAfy8DGxoGAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUTW/bOhD8KwLPpsIvUaJugdPiGXltjNZoi94o7qoholAGSaUNAv/3grKLNKeHdyGIFWc5OzPUCwn2EUlPro/Hd08YciIbkv1aEkxoyjRl8sBFz7qe85p3bcNa/p1siL/FZ9KTVjnHnUEqUSNVwBo6yNFRADSccWCiUaWn/ZFI/0KsrwGfvMN6Tl8wJj8H0pOvPsD8s9xtfe2meYE6zhPuQso2uELm4257S/+93h/u9udTCVMB1x5IT1wjLTYKqIF2oIrDQAfVDZSBlIZ3zKJozzAfMsZgpzrBw+v9MOeA2fWiFrJmVBhy2hCw2RbKg014eD4WFqtEN6W+Wcvrtn8hTxhJLzZ/xDy3u3KTv3pMw+InuDqv2aaHtK6jdXmOz2RDjnE+YsweV32uU8LHYXo+2PTw/nymbNO7X+iWjLCdl5BJT7gsA60CTZPNfg7VheN28hjy+jFHH5J3/9VLkg25+1ztJ5vHOT6+8ePzWebd/1D5g3X3PmC1uyE9YRwFyA7pKLimqgNLbSMN1VrqEbkzreBkQ7ZzyD4s85KqXcj4I64jkZ68t1PCM7/r6O59RpeXWOb8pkuu9nGGxeXq1UvOalZLxgpoycclV58QfESXsQxxiMul35e3EKHFCtouMWLI1d7m++ofm+5JT4ztHLfGGmu1cyM2DEcGzGrWAmrQCEpAC8oo0Mowp5UduALb8GEsFbIhWKJTrWmV3WAGJQU1IJEqaxvaoRkoDlpjiy1XvBB51bG6mwrOuo4pBEkNjkCV4ZYaJSUF2dhW6LFtxfiXJJc8cFYEBnw6v7uVgQboEDSnQvGGKmM47UzT0qEt1HSHjSucbzEGnP4S6oN3cU7zmKtLQqo30n1aQvl3VGtafvpAf60e3czuAWNVPLY+lKdysfV0Op1+AwAA//8DAIAUdzaCBAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTwW7cNhD9FWHOokxSFCXqZqxTdOEmXiSLtOiN4oxiwjK1oCgngbH/HlC7QeLeeiGIAd/Me28eXyHYZ4Iebk+ndy8U0gIlJL+VJJeacc14fRSy510vRCU6U0vV/Qsl+Hv6Dj20yjnhDLGaNDGFvGFDPTqGSEZwgVw2Kve0XxboX8H6CunFO6rm5TPFxc8BevjbB5y/5tnWV26aV6ziPNE+LMkGl8l82O/u2V+3h+PD4fJqoSWDK4/Qg2tqS41CZrAdmBI4sEF1A+NY10Z03JJsLzAfEsVgp2rBp1/zcU6BkutlJeuKM2ngXALaZDPlwS50/H7KLDaL7nK93MrbtX+FF4rQy/KnmZd2N27yN8/LsPoJby5nsstTVnmK84li8rR5ssmcJpv8HIrrpN3kKSQo4eFTcZhsGuf4/MaoTxf9+/8h/5inv/tGbk2Eu3kNCXoQikMJ76179IGK/R30wAVJrDtioxSaqQ4ts01tmNa1Hkk400oBJezmkHxY53Up9iHRl7gJgB7+sNNCF+q30T36RC6tMav6R+csHOKMq0vFL/8Fr3hV88zkYU2nNRUfCX0klyjrO8b12u8/EKnlBtqtMVJIxcGmx+JPuzxCD8Z2TlhjjbXauZEaTiNHbjVvkTRqQiWxRWUUamW408oOQqFtxDDmCpRAed3FljDsmsE41zBRY8uUVpYNshFMkhxpGFEM9Vsfi4cp46zruCKsmaERmTLCMqPqmmHd2FbqsW3l+Jsl1+0Lng1Gern8lY2BRuwItWBSiYYpYwTrTNOyoe0GM+iOGpc531MMNP1m1Hvv4rzMYyqu4SneWPdxDfm/F1uQvvrAvm07upvdE8Ui79j6kON9Xev5fD7/AAAA//8DAFPJlSU2BAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTy27cOBD8FYFnUeZLlKibMd5FBk7Wg8RIFnuj2K0MEZkcUJSTwJh/X1AzQeLcciOKrO7qquYLCfYJyUBuT6e/njHkhdQk+w0STGjKNGXykYuB9QPnDTe87aX8j9TE3+N3MpBOOcedQSpRI1XAWjrKyVEANJxxYKJVpab9vJDhhVjfAD57h01cPmJafAxkIJ98gPi19La+cXNcoUlxxn1Ysg2uiPlnv7unb28Pjw+Hy6sFl0JuPJCBuFZabBVQA91IFYeRjqofKQMpDe+ZRdFdaD5kTMHOzQJffvaHmANmN4hGyIZRYci5JmCzLZJHu+Dj91NRsVl0V/B6g7fj8EKeMZFB1D/MvJS7cbO/cfHpyQa4mXzwy5HU5JTiCVP2uNmxTTjPNvsYqmuT3ewxZFKThw/VYbZ5iunplUcfLqPv/2Dyd9YdfcBqf0cGwjgKkD3SSXBNVQ+W2lYaqrXUE3JnOsFJTXYxZB/WuC7VPmT8nDaVZCB/23nBi77b5I4+o8trKtL/1SVr/ObzLkIBGKnJIUVYXa5+2s1ZwxrJyuXDmk9rrt4j+IQuY5npMa3X8r9RhBYbabemhCFXB5uP1Ru7HMlAjO0dt8Yaa7VzE7YMJwbMatYBatAISkAHyijQyjCnlR25AtvycSpI0V3SrbaFGpXU3eSAWpAjVc6NdGRtT7FTLWMgrOzGV7ZWD3PhWdczhSCpwQmoMtxSo6SkIFvbCT11nZh+seSaOGfFb8Dny9fYFGiAHkFzKhRvqTKG0960HR27fjSj7rF1RfM9poDzL0a98y7FJU65ui5M9cq692so37valuerD/TbFtlddF8wVSVy60PZ5mvK5/P5/D8AAAD//wMAsOqwDyUEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cRgyG/4owZ4083x+6GXaLLtzUi3jRBr1RM1QsRCstRqNNA2P/ezHaLRL3lptAiJyXD1/yjUxwRNKS+9PplzNOeSE1ycMWEkwYygxl8sBFq1wrZOOMdlyzv0lNhif8RlpiVQg8eKQSDVIVmaad7AONET1nPDKhVakJnxfSvhEYmojnIWAzL39iWoZ5Ii35a5ji/LW8DUMTxnmNTZpH3E1LhikUMX/sHp7o7/f7w/P++teCS0luhkhaEpkHHqShQVpPVe849egUlb3vOEfvQNlr2jBlTBOMzRK/fH8/znnCHFrRCNkwKjy51CRChiK5gwUP305FxYboscTrLbx9tm/kjIm0ov4P5rXcXRiHu2XtRjzjeIK0YLoL8/EIUyQ1OaX5hCkPuGE5Y+rmZciFaAgWnOM8OrBK2+ikFtJGyTA6ZnX00YNRphdB6z52zlvVqyi4igaV6kqjG7hxhDzMU3XT/jAOOGVSk+eXaj9C7ud0fIf+5Up09xNAi2zSEsm0NNaiklKCN51gGK0JxoguALN9J5AHZNor67llXHrBvNTGOhuB+UBq8gHC6zBhtXskLWEcRZQOaS+4ocpFoKClp8ZI0yMP3gpOavIwT3mY1nldqt2U8XPa+iUt+RXGBa+d3qfwOmQMeU0FwidTzLhPc1xDrr4bgLOGNZKxkrTm05qrjxiHhCFjwXFI663e/1KEEVvSw5oSTrnaQ36tfoPllbTEgwscPHgAE0KPmmHPIgPDbEQTDUYloo3Kq2iUZ8Eo6LiKoHnXlwipCRa/VZvFA6hOMY5Uem6pspZRr8rGaYXAhfaC8Xccq+ex5EFwTGGU1GMfqfIcqFdS0ig1WGF6a0X/A5KbWfhWLOL5uqybAhOjw2g4FYprqrzn1HltaWdd5zvjUIei+QnThOMPoD4MIc3L3Ofq5rXqHbqP61QOTrX57usw0X+2GT3O4QumqswYhqns122sl5ocEZY14XG7V+0b2Zflqg7b2bK80U7qmrxkSHk93cKeNU44c7lcLv8CAAD//wMA6GRgTPQEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/jNhCG/4rAs6iQEkWJuqVOFzWy2Rgbd1v0NiJHDhGZNCgq2yDwf19QdhBnFz30Joz4zsczL/lKHOyRdOT6cPj9GV2cSE6iXUIlKyVlkrJqy8tOtF1ZFUq0Vcvqf0hO7C2+kI40QmuuFdIKJVJhWE37atDUGFScccPKWqScsJtI90rAFgafrcbCT98wTNY70pG/rDP+e6oNttCjn00R/IhrN0VwOjXzZb26pZ+vN9v7zenUhFMSF9aQjhimgOtKUl01ioqh5VRhK2g1qJ5zVC2I5iSzLmJwMBaTeXqvb3x0GHVXFmVVMFoqcsyJgQip5R4m3L4cUhcLopsUz5fw8tm9kmcMpCvzN5indFd6tFf7qZ/taK5Gv9tZt1t5N9jdHCCmwjk5BH/AEC0ucH6zDsLLZ7/bpXzkE4wTkpxsMeytg/H04wYHmMf44OewkJnM03+dIR2BOXqSk4XkOC5ls/Mwq9Gii79oLwrfP2SbEeLgw/7Dkh5O7Nf/A/0d6EfrMFvfkI4wjqWpWqRDySUVrQEKdaWolJUckGvVlJzkZOVdtG7285StXcTdmdqH/q6DfrQRdZxDmulvmcy2Cd7MOmbvC+asYEXFWBLN8TDH7CsaG1BHTENsw3zO95OklOUi+mRH/AXPag4BXcw2EB+zP2B6JB1R0GoOChSA1HrAmuHADAPJGoPSSDSiNI0RShgpFNNSQM+FgZr3Q4qQnGAyWXbytSgr1g6GSkBGhdI9batGUl0DSFVr0ff1B7jZ/Zh0oFsm0FRU4WCoUByoElVFTVVDU8qhacrhgtPZEJwl6pcm/HNCc/bSl5O132Y3+Hy6x0uf0pgWjeS0FLymQilOW1U3tG/aXvWyxVqnyVbeTf4N5DcMvZ9sTI/InXV2D+PPR943c4vB4XixnTurg5/8ELOzLbMP+/o6u/SKZYtFv1tH/12MceP1E4YsGQusu1jm8Xg8/gAAAP//AwBBBoIlDAUAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS/zQzbBb1HVTL+JFWvQ2Ike2EC21oKhNA2P/e0DtBol7600Y6J0ZPvPOvJEIByQduT0efzlhzAupSR63kGBCU6Ypk3suOmU7IRsrmNOG/UNqMj7iV9IRo7zn3iGVqJGqwFray8HTENBxxgMTrSo54WUh3RuBsQl4Gj028/IJ0zLOkXTkrzGG+UupDWPjp3kNTZonfIhLhuhLM38+3D3SP253+6fd5a8FlyJuxkA6EpgD7qWmXhpH1WA5dWgVlYPrOUdnQZmLbIwZU4SpWcLnH/XDnCNm34lGyIZR4ci5JgEylJZ7WHD/9Vi62BDdl3i9hbfP7o2cMJFO1N9hXtLd+Gm8yfNxwhNOR0gLphs/Hw4QA6nJMc1HTHnEDcv20mmCPM6xuha7m0aMmdTk6bnaTZCHOR3esXq+IHj4HwROmHrSEclaqY1BJaUEp3vBMBjttRa9B2aGXiD3yFqnjOOGcekEc7LVxpoAzHlSk5dp7mH6fdn4OcF6G5gRWqBCJ5TWzjOUFofWIAugda+td0FaI5QNVoID4K5H4532xSAfwL+OEauHe9IRxlEEaZEOgmuqbAAKrXRUa6kH5N4ZwUlN7uaYx7jO61I9xIwvaeNHOvIrTAteyN0m/zpm9HlNBerfuhTbpTmsPlc/HMBZwxrJWBGt+bjm6iOGMaHPWPDu03rN9x+J0GIT3a0pYczVDvJr9Rssr4UKWM/Bladq7wdsGQ4sMNDMBNRBY1AimKCcClo55rWCnqsALe+HEiE1wWK4avO4RaZbDo4aN3CqbAvUDU5TztC5wUFv2PCOY/U0FR14yxQGSR0OgSrHgTolJQ2yBSP0YIwouu9IrubjrAAOeLps69aBDsFi0JwKxVuqnOPUutbQ3tje9dpi60vPj5giTj+B+jD6NC/zkKurd6t36D6usVycavPxlzHSf7cZ3c/+M6aqzBjGWBbsOtZzTQ4Iy5rwsB2s7o3synZV++1uGd60VrY1ec6Q8nq8hh1rrLD6fD6fvwEAAP//AwBMsVlF9QQAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUS2/bOBD+KwLPokJS1IMC9mA4LWqkaYzE3V30RpHDmIgsGhTlNAj83xeU5dopetibMJrX9xi+o17uADVosd9/OkAfBpSiYKcQI6zEpMQk31DW8LphPMsFKwpS/0ApsnfwhhpUcaWoEoBzKAFzTQrc5kZhrUFQQjVhBY895fOAmnckbabhYBVkbvgb/GBdjxr0j+21e42zpc1U50adedfBqh+C7FVc5ttqeYe/Ltabh/Upa4AhFmdWowZpIiRVeYlVXgnMTU2xgJrj3IiWUhC15NWpzPYBfC+7bNAvl/nahR6CaljG8oxgJtAxRVoGGVdu5QCbt33cYqLoNsbTKTx9Nu/oAB41LD2TeWp3ozp7sxva0Xb6Jkj/DMF4uYNX51/gIDuUor13e/DBwsTNw1NyWYmSjGSsZISgFN1LtbU9JKvb5KGLgKWqCQedYwFGYy6oxILnOdZ5IStWmqpiBqXoDnwP3VXXe6u8G5wJyUx58mHO0vXB9qMbh2TVB3j2MpzqPstuAJSi9dh2dtg+gtRvG/c4xn/92EUsy9F76EOylmGbfJHDFjVIyFpRKaSQslTKQEHAEE1kSSoNpS5Bc6YrzQXXJRdElVy2lGtZ0NbECErRwxj2Yzjz//NqiY23ux3oywabieN1J4NxfrfS0AdrbJTmnDEZqusmVMncc9lZ6MMHklGDCAWm8xqwYbTEvNYSyyIXuCzz0gBVomL0RJixz+MvnogxmuemqOuyFS0lwExVc1KZlmhlciV00ULFKlLVUAtFSFtwWrdCEQ204Dqi806PKvxuhXwS6HHs43Umqwj71fb4Z8kvjKy9C065LgpmO3h6GwLs/qTa0u32brAh4jeztN8HuHd9/Dvf/8zZXLxw4RKDeAfJdHoF10CNYbjSUGFeCIlFbgzmpCCtqVql8gKl6NapF/BJNJi0/aTJ2VMPT8nCq60NoMLo4+R/J1AnOT+fT+bCR/bt02bpPCz2+3SO/nWIJJ2anQ3w4V35PsT0rRvCFeQZ2hN05rzYlZ00HE5PlY3BUusadEkx47TAXAiKa1FUuK3qVrRlDYW62DV5BG09qDD12/gxTpuZ/ZMvFz5YI1UY4vF8dWqy0+zPOeX3Ja9k+1V9Gn4l3WyluRMl0bJPp4dzctD/ejePx+PxPwAAAP//AwBie1/EKAYAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227cNhD9FYGvFRXexIveXNtBF6ljI7toi76NyJHNWqYWFOUkMPzvhbR2rijQF0EYknPOnDkzTyTBA5KOnB2Pl4+YykxqUuIWEkxoyjRl8sBFp2wnRaO44UrIv0lN4jv8TDpilPfcO6QSNVIVWEt7OXgaAjrOeGCiVWtOuJ1J90QgNgEfo8dmmv/APMcpkY78GVOYPq7YEBs/Tkto8jTiLs0Fkl/JvN+dv6O/n90crm9Ot2ac18dNDKQjgTngXmrqpXFUDZZTh1ZRObiec3QWlDk9i6lgTjA2c7j/ih+mkrD4TjRCNowKR55rEqDASrmHGQ+fjyuLTaKLNV5v4e23eyKPmEkn6lcxT+ne+DG+eZj7JY7hzfYlNTnm6Yi5RNzUuN5XX0lw1rBGaMEYqckV+LuYsNpdVNfjWiJ4yxQGSR0OgSrHgTolJQ2yBSP0YIwYSE3eYU44fpP1Kvo8zdNQqheRq+9w9mfnlwn6EVeMtzDOSGqym/cFUoBxSnj5Cf1STrkOeVmPz6dUYlqmZa52qeBthpfz1/fnS86YSnUD5a76DeY70hEH1nNw4AC09wO2DAcWGGhmAuqgMSgRTFBOBa0c81pBz1WAlvfDGiE1+XXVcL94j/P8lc1mknHcOFQvfTofI6bynYykI4yjCNIiHQTXVNkAFFrpqNZSD8i9M4K/whwg32IhHRls3/fM+14oxz0IBzIMTAY9OCGMATXI1iihdcsM+GCM4CpIgx7kIJkVpCY3eQqLLz+2Wm4NuMnTP+jLKhXpiGZtaz30vdCKW3AWFdfKaofGqQGYkr4H3lroe86gZd5Z4YFrB6oP7fBFpct0GxNexPk4wudvcG2jG/mLB7B8APXD9bcZHvDjlO/fn2zcvL88kJp8WNK6EardapGPMdFPep3oq2Us8XCXEQKGqyngzz7CdVyqbUINZ9JohVQMXFDVBkYhDJJaz53XUljhJanJxeTvMVerwyCmdaq+JLveV2fZ38WCvix55ffXxuN6X92MUIYpP3y3Sb4p7EcBGiE5WzsT8PG0jTaOOgSLQXMqFG+pco5T61pDe2N712uLrV8Vu17KcSnVBwwxoy9bxS9e3DDP79Df/yzGqwteLMrZ6rX9aYtt0v6vJfZckweEecn4sG3r7onsUsK8IV8sp1Hcpas4jnFGP6Uwk84qyxvGjHph+J/3WiMbo5h8fn5+/hcAAP//AwD/LioFGwYAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTwW7cOAyGX8Xg2XIkWZYt34JJFzvIthm0g+5ib7JIN0IceSDLaYtg3r2QZ4o2e9uLIBAi+f8fqVcI9pmgh9vT6d0LhbRACclvIcmlZlwzXh+F7FXX17JSouG1rv+FEvw9fYceWuWccIZYTZqYQt6woR4dQyQjuEAuG5Vr2i8L9K9gfYX04h1V8/KZ4uLnAD387QPOX3Nv6ys3zStWcZ5oH5Zkg8tiPux39+yv28Px4XB5tdCSkyuP0ANyY4WrNXN1a5gaO8EMdYrVoxmEINNZ1V7SfEgUg52qBZ9+9cc5BUqul5WsK86kgXMJaJPNkge70PH7KavYEN3leLmFt2v/Ci8UoZflT5iXcjdu8jfPy7D6CW8uZ7LLU3Z5ivOJYvK0MdlsTpNNfg7FtdNu8hQSlPDwqThMNo1zfH4D6tPF//5/2D/m7u++kVsT4W5eQ4IehOJQwnvrHn2gYn8HPXBBEuuO2CiFZqpDy2xTG6Z1rUcSzrRSQAm7OSQf1nldin1I9CVuBqCHP+y00EX6bXSPPpFLa8yu/tF5Fw5xxtWl4hd/wSte1TwreVjTaU3FR0IfySXK/o5xvdb7T4rUckvarTFSSMXBpsfiT7s8Qg/Gdk5YY4212rmRGk4jR241b5E0akIlsUVlFGpluNPKDkKhbcQw5giUQHncxbZh1ikp7YAMeWOZQt1kxCPTyFHhiHrg+IZj8TBd8jquCGtmaESmjLDMqLpmWDe2lXpsWzn+huQ6fcEzYKSXy1/ZFGjEjlALJpVomDJGsM40LRvabjCD7qhxWfM9xUDTb6DeexfnZR5TcV2e4g26j2vI/73YFumrD+zbNqO72T1RLPKMrQ95va9jPZ/P5x8AAAD//wMAJohFAjYEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUTW/bOhD8KwLPpsIvUaJugdPiGXltjNZoi95W5KoholAGSaUNAv/3grKLNKeHdyGIFWc5OzPUCwnwiKQn18fjuycMOZENyX4tCSY0ZZoyeeCiV10vRa245Kxrv5MN8bf4THrSKmu5NUglaqTKsYYOcrTUOTScccdEo0pP+JFI/0LA1w6fvMV6Tl8wJj8H0pOvPrj5Z7kbfG2neXF1nCfchZQh2ELm4257S/+93h/u9udTCVMB196RnjhmgFupqZWtoWrsODXYKSpHM3COpgPVnmE+ZIwBpjq5h9f73ZwDZtuLWsiaUWHIaUMcZCiUB0h4eD4WFqtEN6W+Wcvrtn8hTxhJLzZ/xDy3u7KTv3pMw+Ind3VeM6SHtK4j2DzHZ7IhxzgfMWaPqz7XKeHjMD0fID28P58p2/TuF9olo9vOS8ikJ1yWgVaBpgmyn0N14bidPIa8fszRh+Ttf/WSZEPuPlf7CfI4x8c3fnw+y7z7Hyp/AHvvA1a7G9ITxlE42SEdBddUdQ4oNNJQraUekVvTCk42ZDuH7MMyL6nahYw/4joS6cl7mBKe+V1He+8z2rzEMuc3XXK1j7NbbK5eveSsZrVkrICWfFxy9Qmdj2gzliEOcbn0+/IWIrRYQdslRgy52kO+r/6BdE96YqCzHAwYAG3tiA3DkTkGmrUOtdPolHCtU0Y5rQyzWsHAlYOGD2OpkA3BEp1qTasZO902wCh0CFShUXSQTlAOgrFWO2XbQuRVx+puKjiwHVPoJDU4OqoMB2qUlNTJBlqhx7YV41+SXPLAWRHY4dP53a0MtHMdOs2pULyhyhhOO9O0dGi7wQy6w8YWzrcYA05/CfXB2zineczVJSHVG+k+LaH8O6o1LT99oL9Wj25m+4CxKh6DD+WpXGw9nU6n3wAAAP//AwCCZsjSggQAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTy27cOBD8FYFnUeZLlKibMd5FBk7Wg8RIgtwoditDRCYHFOUkMObfF9RM4GRveyOKrO7qquYLCfYJyUBuT6e/njHkhdQk+w0STGjKNGXykYtB9YMUjeJGil5/ITXx9/iTDKRTznFnkErUSBWwlo5ychQADWccmGhVqWm/LmR4IdY3gM/eYROXj5gWHwMZyCcfIH4vva1v3BxXaFKccR+WbIMrYv7Z7+7p29vD48Ph8mrBpZAbD2QgwIzlTmrqZGeomnpODfaKysmMnKPpreouNB8ypmDnZoFvr/0h5oDZDaIRsmFUGHKuCdhsi+TRLvj481RUbBbdFbze4O04vJBnTGQQ9S8zL+Vu3OxvXHx6sgFuJh/8ciQ1OaV4wpQ9bnZsE86zzT6G6tpkN3sMmdTk4UN1mG2eYnr6w6MPl9H3/2Pyd9YdfcBqf0cGwjgKkD3SSXBNVQ+W2lYaqrXUE3JnOsFJTXYxZB/WuC7VPmT8mjaVZCB/23nBi77b5I4+o8trKtI/65I1/vB5F6EAjNTkkCKsLlevdnPWsEaycvmw5tOaq/cIPqHLWGZ6TOu1/H8oQouNtFtTwpCrg83H6o1djmQgxvaOW2ONtdq5CVuGEwNmNesANWgEJaADZRRoZZjTyo5cgW35OBWk6C7pVttCcaNGPnWWguBA1TiOtEcOFLvWKKXZ1Mki8dXW6mEuPOt6phAkNTgBVYZbapSUFGRrO6GnrhPTb5ZcE+es+A34fPkamwIN0CNoToXiLVXGcNqbtqNj149m1D22rmi+xxRw/s2od96luMQpV9eFqf6w7v0ayveutuX57gP9sUV2F903TFWJ3PpQtvma8vl8Pv8LAAD//wMA/b2d/iUEAAA=

Binary file not shown.

View File

@ -14,7 +14,7 @@ public sealed class ProjectsController(IProjectService projects) : Controller
return model is null ? NotFound() : View(model);
}
public IActionResult Create() => View("Edit", new ProjectEditViewModel());
public async Task<IActionResult> Create() => View("Edit", await projects.GetCreateAsync());
public async Task<IActionResult> Edit(int id)
{
@ -28,6 +28,11 @@ public sealed class ProjectsController(IProjectService projects) : Controller
{
if (!ModelState.IsValid)
{
if (model.ProjectID == 0)
{
await projects.PopulateGenrePresetOptionsAsync(model);
}
return View("Edit", model);
}

View File

@ -8,7 +8,9 @@ public interface IProjectRepository
{
Task<IReadOnlyList<Project>> ListAsync();
Task<Project?> GetAsync(int projectId);
Task<IReadOnlyList<GenreMetricPreset>> ListGenreMetricPresetsAsync();
Task<int> SaveAsync(Project project);
Task<int> SaveAsync(Project project, string? genreMetricPresetKey);
Task ArchiveAsync(int projectId);
}
@ -1135,12 +1137,21 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) :
return await connection.QuerySingleOrDefaultAsync<Project>("dbo.Project_Get", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
}
public async Task<int> SaveAsync(Project project)
public async Task<IReadOnlyList<GenreMetricPreset>> ListGenreMetricPresetsAsync()
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<GenreMetricPreset>("dbo.GenreMetricPreset_List", commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public Task<int> SaveAsync(Project project) => SaveAsync(project, null);
public async Task<int> SaveAsync(Project project, string? genreMetricPresetKey)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.Project_Save",
new { project.ProjectID, project.ProjectName, project.Description },
new { project.ProjectID, project.ProjectName, project.Description, GenreMetricPresetKey = genreMetricPresetKey },
commandType: CommandType.StoredProcedure);
}

View File

@ -10,6 +10,16 @@ public sealed class Project
public bool IsArchived { get; set; }
}
public sealed class GenreMetricPreset
{
public int GenreMetricPresetID { get; set; }
public string PresetKey { get; set; } = string.Empty;
public string PresetName { get; set; } = string.Empty;
public string? Description { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; }
}
public sealed class Book
{
public int BookID { get; set; }

View File

@ -12,7 +12,9 @@ public interface IProjectService
{
Task<IReadOnlyList<Project>> ListAsync();
Task<ProjectDetailViewModel?> GetDetailAsync(int projectId);
Task<ProjectEditViewModel> GetCreateAsync();
Task<ProjectEditViewModel?> GetEditAsync(int projectId);
Task PopulateGenrePresetOptionsAsync(ProjectEditViewModel model);
Task<int> SaveAsync(ProjectEditViewModel model);
Task ArchiveAsync(int projectId);
}
@ -238,6 +240,13 @@ public sealed class ProjectService(IProjectRepository projects, IBookRepository
};
}
public async Task<ProjectEditViewModel> GetCreateAsync()
{
var model = new ProjectEditViewModel();
await PopulateGenrePresetOptionsAsync(model);
return model;
}
public async Task<ProjectEditViewModel?> GetEditAsync(int projectId)
{
var project = await projects.GetAsync(projectId);
@ -249,12 +258,20 @@ public sealed class ProjectService(IProjectRepository projects, IBookRepository
};
}
public async Task PopulateGenrePresetOptionsAsync(ProjectEditViewModel model)
{
var presets = await projects.ListGenreMetricPresetsAsync();
model.GenreMetricPresetOptions = presets
.Select(x => new SelectListItem(x.PresetName, x.PresetKey, string.Equals(x.PresetKey, model.GenreMetricPresetKey, StringComparison.OrdinalIgnoreCase)))
.ToList();
}
public Task<int> SaveAsync(ProjectEditViewModel model) => projects.SaveAsync(new Project
{
ProjectID = model.ProjectID,
ProjectName = model.ProjectName,
Description = model.Description
});
}, model.ProjectID == 0 ? model.GenreMetricPresetKey : null);
public Task ArchiveAsync(int projectId) => projects.ArchiveAsync(projectId);
}

View File

@ -0,0 +1,669 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.GenreMetricPresets', N'U') IS NULL
BEGIN
CREATE TABLE dbo.GenreMetricPresets
(
GenreMetricPresetID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_GenreMetricPresets PRIMARY KEY,
PresetKey nvarchar(50) NOT NULL,
PresetName nvarchar(100) NOT NULL,
Description nvarchar(500) NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_GenreMetricPresets_IsActive DEFAULT 1,
CONSTRAINT UQ_GenreMetricPresets_PresetKey UNIQUE(PresetKey)
);
END;
GO
IF OBJECT_ID(N'dbo.GenreMetricPresetMetrics', N'U') IS NULL
BEGIN
CREATE TABLE dbo.GenreMetricPresetMetrics
(
GenreMetricPresetID int NOT NULL,
MetricTypeID int NOT NULL,
SortOrder int NOT NULL,
CONSTRAINT PK_GenreMetricPresetMetrics PRIMARY KEY(GenreMetricPresetID, MetricTypeID),
CONSTRAINT FK_GenreMetricPresetMetrics_Presets FOREIGN KEY(GenreMetricPresetID) REFERENCES dbo.GenreMetricPresets(GenreMetricPresetID),
CONSTRAINT FK_GenreMetricPresetMetrics_MetricTypes FOREIGN KEY(MetricTypeID) REFERENCES dbo.SceneMetricTypes(MetricTypeID)
);
END;
GO
MERGE dbo.GenreMetricPresets AS target
USING (VALUES
(N'Custom', N'Custom / No preset', N'Use the core PlotLine metrics only.', 10),
(N'CrimeThriller', N'Crime / Thriller', N'Suspense, danger, investigation and clues.', 20),
(N'Romance', N'Romance', N'Emotional intimacy, conflict, trust, longing and chemistry.', 30),
(N'Fantasy', N'Fantasy', N'Wonder, world building, magic, politics and quest progress.', 40),
(N'Horror', N'Horror', N'Dread, fear, isolation, gore and helplessness.', 50),
(N'HistoricalFiction', N'Historical Fiction', N'Immersion, authenticity and social or political pressure.', 60),
(N'LiteraryFiction', N'Literary Fiction', N'Character growth, internal conflict, subtext and theme.', 70)
) AS source (PresetKey, PresetName, Description, SortOrder)
ON target.PresetKey = source.PresetKey
WHEN MATCHED THEN
UPDATE SET PresetName = source.PresetName,
Description = source.Description,
SortOrder = source.SortOrder,
IsActive = 1
WHEN NOT MATCHED THEN
INSERT (PresetKey, PresetName, Description, SortOrder, IsActive)
VALUES (source.PresetKey, source.PresetName, source.Description, source.SortOrder, 1);
GO
MERGE dbo.SceneMetricTypes AS target
USING (VALUES
(N'Suspense', N'Rising uncertainty and anticipation of danger.', 5, 210),
(N'Danger', N'Immediate physical, emotional, or strategic risk.', 5, 220),
(N'Investigation Progress', N'How far the truth-finding or case-solving thread advances.', 5, 230),
(N'Clue Density', N'How much meaningful clue material appears in the scene.', 4, 240),
(N'Threat Proximity', N'How close the active threat feels to the characters.', 5, 250),
(N'Emotional Intimacy', N'Emotional closeness, vulnerability, and connection.', 5, 310),
(N'Relationship Conflict', N'Tension, friction, or opposing needs within the relationship.', 5, 320),
(N'Trust', N'How much trust is present, earned, tested, or repaired.', 5, 330),
(N'Longing', N'Yearning, desire, separation, or unresolved emotional pull.', 5, 340),
(N'Chemistry', N'Spark, attraction, and interpersonal charge.', 5, 350),
(N'Wonder', N'Sense of awe, strangeness, discovery, or enchantment.', 5, 410),
(N'World Building', N'How much setting, culture, history, or rules are developed.', 4, 420),
(N'Magic Presence', N'How strongly magic or the fantastic shapes the scene.', 4, 430),
(N'Political Intrigue', N'Power manoeuvring, faction pressure, and strategic schemes.', 5, 440),
(N'Quest Progress', N'How much the central journey or mission advances.', 5, 450),
(N'Dread', N'Slow-building unease and expectation of something terrible.', 5, 510),
(N'Fear', N'Immediate fear or panic experienced by characters or reader.', 5, 520),
(N'Isolation', N'How alone, cut off, or unsupported characters feel.', 5, 530),
(N'Gore', N'Graphic bodily harm or visceral horror content.', 1, 540),
(N'Helplessness', N'Loss of control, trapped choices, or inability to act.', 5, 550),
(N'Historical Immersion', N'How strongly the scene evokes its period and setting.', 5, 610),
(N'Social Pressure', N'Expectations, class, family, reputation, and public constraint.', 5, 620),
(N'Political Tension', N'Period power pressure, public conflict, or institutional risk.', 5, 630),
(N'Authenticity', N'How heavily the scene relies on period-specific detail or accuracy.', 5, 640),
(N'Cultural Conflict', N'Conflict arising from customs, values, identity, or social change.', 5, 650),
(N'Character Growth', N'Visible internal change, self-knowledge, or development.', 5, 710),
(N'Internal Conflict', N'Inner contradiction, dilemma, doubt, or self-opposition.', 5, 720),
(N'Subtext', N'Unspoken meaning, implication, and tension beneath dialogue or action.', 5, 730),
(N'Theme Strength', N'How strongly the scene expresses or complicates the work''s themes.', 5, 740),
(N'Emotional Resonance', N'Aftertaste, depth, and lingering emotional impact.', 5, 750)
) AS source (MetricName, Description, DefaultValue, SortOrder)
ON target.ProjectID IS NULL AND target.MetricName = source.MetricName
WHEN MATCHED THEN
UPDATE SET Description = source.Description,
MinValue = 1,
MaxValue = 10,
DefaultValue = source.DefaultValue,
SortOrder = source.SortOrder,
IsActive = 1
WHEN NOT MATCHED THEN
INSERT (ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive)
VALUES (NULL, source.MetricName, source.Description, 1, 10, source.DefaultValue, source.SortOrder, 1);
GO
DELETE gpmm
FROM dbo.GenreMetricPresetMetrics gpmm
INNER JOIN dbo.GenreMetricPresets gmp ON gmp.GenreMetricPresetID = gpmm.GenreMetricPresetID;
GO
INSERT dbo.GenreMetricPresetMetrics (GenreMetricPresetID, MetricTypeID, SortOrder)
SELECT gmp.GenreMetricPresetID, smt.MetricTypeID, rows.SortOrder
FROM
(
VALUES
(N'CrimeThriller', N'Suspense', 210),
(N'CrimeThriller', N'Danger', 220),
(N'CrimeThriller', N'Investigation Progress', 230),
(N'CrimeThriller', N'Clue Density', 240),
(N'CrimeThriller', N'Threat Proximity', 250),
(N'Romance', N'Emotional Intimacy', 310),
(N'Romance', N'Relationship Conflict', 320),
(N'Romance', N'Trust', 330),
(N'Romance', N'Longing', 340),
(N'Romance', N'Chemistry', 350),
(N'Fantasy', N'Wonder', 410),
(N'Fantasy', N'World Building', 420),
(N'Fantasy', N'Magic Presence', 430),
(N'Fantasy', N'Political Intrigue', 440),
(N'Fantasy', N'Quest Progress', 450),
(N'Horror', N'Dread', 510),
(N'Horror', N'Fear', 520),
(N'Horror', N'Isolation', 530),
(N'Horror', N'Gore', 540),
(N'Horror', N'Helplessness', 550),
(N'HistoricalFiction', N'Historical Immersion', 610),
(N'HistoricalFiction', N'Social Pressure', 620),
(N'HistoricalFiction', N'Political Tension', 630),
(N'HistoricalFiction', N'Authenticity', 640),
(N'HistoricalFiction', N'Cultural Conflict', 650),
(N'LiteraryFiction', N'Character Growth', 710),
(N'LiteraryFiction', N'Internal Conflict', 720),
(N'LiteraryFiction', N'Subtext', 730),
(N'LiteraryFiction', N'Theme Strength', 740),
(N'LiteraryFiction', N'Emotional Resonance', 750)
) rows (PresetKey, MetricName, SortOrder)
INNER JOIN dbo.GenreMetricPresets gmp ON gmp.PresetKey = rows.PresetKey
INNER JOIN dbo.SceneMetricTypes smt ON smt.ProjectID IS NULL AND smt.MetricName = rows.MetricName
WHERE NOT EXISTS
(
SELECT 1
FROM dbo.GenreMetricPresetMetrics existing
WHERE existing.GenreMetricPresetID = gmp.GenreMetricPresetID
AND existing.MetricTypeID = smt.MetricTypeID
);
GO
CREATE OR ALTER PROCEDURE dbo.GenreMetricPreset_List
AS
BEGIN
SET NOCOUNT ON;
SELECT GenreMetricPresetID, PresetKey, PresetName, Description, SortOrder, IsActive
FROM dbo.GenreMetricPresets
WHERE IsActive = 1
ORDER BY SortOrder, PresetName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ProjectMetricPreset_Apply
@ProjectID int,
@GenreMetricPresetKey nvarchar(50) = N'Custom'
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM dbo.Projects WHERE ProjectID = @ProjectID AND IsArchived = 0)
RETURN;
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID)
BEGIN
INSERT dbo.ProjectTimelineSettings
(ProjectID, ShowSceneCards, ShowMetricShape, ShowPlotLines, ShowStoryAssets, ShowCharacterAppearances, ShowWarnings)
VALUES
(@ProjectID, 1, 1, 1, 1, 1, 1);
END;
DECLARE @PresetID int =
(
SELECT GenreMetricPresetID
FROM dbo.GenreMetricPresets
WHERE PresetKey = COALESCE(NULLIF(LTRIM(RTRIM(@GenreMetricPresetKey)), N''), N'Custom')
AND IsActive = 1
);
DELETE FROM dbo.ProjectTimelineMetricSettings
WHERE ProjectID = @ProjectID;
INSERT dbo.ProjectTimelineMetricSettings (ProjectID, MetricTypeID, SortOrder)
SELECT @ProjectID, mt.MetricTypeID, mt.SortOrder
FROM dbo.SceneMetricTypes mt
WHERE mt.ProjectID IS NULL
AND mt.IsActive = 1
AND mt.MetricName IN
(
N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness',
N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery'
)
UNION
SELECT @ProjectID, mt.MetricTypeID, gpmm.SortOrder
FROM dbo.GenreMetricPresetMetrics gpmm
INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = gpmm.MetricTypeID
WHERE gpmm.GenreMetricPresetID = @PresetID
AND mt.IsActive = 1
AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID);
END;
GO
CREATE OR ALTER PROCEDURE dbo.Project_Save
@ProjectID int = NULL,
@ProjectName nvarchar(200),
@Description nvarchar(max) = NULL,
@GenreMetricPresetKey nvarchar(50) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @ProjectID IS NULL OR @ProjectID = 0
BEGIN
INSERT dbo.Projects (ProjectName, Description)
VALUES (@ProjectName, @Description);
SET @ProjectID = CAST(SCOPE_IDENTITY() AS int);
EXEC dbo.ProjectMetricPreset_Apply @ProjectID = @ProjectID, @GenreMetricPresetKey = @GenreMetricPresetKey;
SELECT @ProjectID AS ProjectID;
RETURN;
END;
UPDATE dbo.Projects
SET ProjectName = @ProjectName,
Description = @Description,
UpdatedDate = SYSUTCDATETIME()
WHERE ProjectID = @ProjectID;
SELECT @ProjectID AS ProjectID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneMetricType_List
@ProjectID int = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @HasMetricSettings bit =
CASE WHEN @ProjectID IS NOT NULL AND EXISTS (SELECT 1 FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID) THEN 1 ELSE 0 END;
SELECT MetricTypeID, ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive
FROM dbo.SceneMetricTypes mt
WHERE mt.IsActive = 1
AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID)
AND
(
@ProjectID IS NULL
OR (@HasMetricSettings = 1 AND EXISTS
(
SELECT 1
FROM dbo.ProjectTimelineMetricSettings ptms
WHERE ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = mt.MetricTypeID
))
OR (@HasMetricSettings = 0 AND mt.MetricName IN
(
N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness',
N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery'
))
)
ORDER BY mt.SortOrder, mt.MetricName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneMetric_EnsureDefaults
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ProjectID int;
SELECT @ProjectID = b.ProjectID
FROM dbo.Scenes s
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE s.SceneID = @SceneID;
DECLARE @HasMetricSettings bit =
CASE WHEN EXISTS (SELECT 1 FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID) THEN 1 ELSE 0 END;
INSERT dbo.SceneMetricValues (SceneID, MetricTypeID, Value, Notes)
SELECT @SceneID, mt.MetricTypeID, mt.DefaultValue, NULL
FROM dbo.SceneMetricTypes mt
WHERE mt.IsActive = 1
AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID)
AND
(
(@HasMetricSettings = 1 AND EXISTS
(
SELECT 1
FROM dbo.ProjectTimelineMetricSettings ptms
WHERE ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = mt.MetricTypeID
))
OR (@HasMetricSettings = 0 AND mt.MetricName IN
(
N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness',
N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery'
))
)
AND NOT EXISTS
(
SELECT 1
FROM dbo.SceneMetricValues smv
WHERE smv.SceneID = @SceneID AND smv.MetricTypeID = mt.MetricTypeID
);
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneMetric_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.SceneMetric_EnsureDefaults @SceneID = @SceneID;
DECLARE @ProjectID int;
SELECT @ProjectID = b.ProjectID
FROM dbo.Scenes s
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE s.SceneID = @SceneID;
DECLARE @HasMetricSettings bit =
CASE WHEN EXISTS (SELECT 1 FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID) THEN 1 ELSE 0 END;
SELECT mt.MetricTypeID, mt.ProjectID, mt.MetricName, mt.Description, mt.MinValue, mt.MaxValue,
mt.DefaultValue, mt.SortOrder, mt.IsActive, smv.SceneID, smv.Value, smv.Notes
FROM dbo.SceneMetricTypes mt
INNER JOIN dbo.SceneMetricValues smv ON smv.MetricTypeID = mt.MetricTypeID AND smv.SceneID = @SceneID
WHERE mt.IsActive = 1
AND
(
(@HasMetricSettings = 1 AND EXISTS
(
SELECT 1
FROM dbo.ProjectTimelineMetricSettings ptms
WHERE ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = mt.MetricTypeID
))
OR (@HasMetricSettings = 0 AND mt.MetricName IN
(
N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness',
N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery'
))
)
ORDER BY mt.SortOrder, mt.MetricName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Analytics_StoryHealth_Remainder
@ProjectID int,
@BookID int = NULL,
@StartChapterNumber decimal(10,2) = NULL,
@EndChapterNumber decimal(10,2) = NULL,
@StartSceneNumber decimal(10,2) = NULL,
@EndSceneNumber decimal(10,2) = NULL
AS
BEGIN
SET NOCOUNT ON;
;WITH EventScope AS
(
SELECT pt.PlotThreadID, MAX(nso.GlobalSceneIndex) AS LastTouchedIndex, MAX(nso.SceneID) AS LastTouchedSceneID,
MAX(CONCAT(N'Ch ', nso.ChapterNumber, N' / Scene ', nso.SceneNumber, N': ', nso.SceneTitle)) AS LastTouchedSceneLabel
FROM dbo.PlotThreads pt
INNER JOIN dbo.ThreadEvents te ON te.PlotThreadID = pt.PlotThreadID
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = te.SceneID
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
GROUP BY pt.PlotThreadID
)
SELECT pt.PlotThreadID, pt.ThreadTitle, pl.PlotLineID, pl.PlotLineName, pt.Importance,
ts.StatusName AS ThreadStatusName, ts.IsOpenStatus, ts.IsResolvedStatus,
pt.IntroducedSceneID, pt.PlannedResolutionSceneID, pt.ActualResolutionSceneID,
EventScope.LastTouchedSceneID, EventScope.LastTouchedSceneLabel,
CASE WHEN ts.IsOpenStatus = 1 AND pt.Importance >= 8 THEN 1 ELSE 0 END AS IsHighImportanceOpen,
CASE WHEN ts.IsOpenStatus = 1 AND EventScope.LastTouchedIndex IS NOT NULL
AND (SELECT MAX(GlobalSceneIndex) FROM dbo.NarrativeSceneOrder WHERE ProjectID = @ProjectID AND (@BookID IS NULL OR BookID = @BookID)) - EventScope.LastTouchedIndex > 8 THEN 1 ELSE 0 END AS IsDormant,
CASE WHEN EXISTS (
SELECT 1 FROM dbo.ThreadEvents te INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
WHERE te.PlotThreadID = pt.PlotThreadID AND tet.TypeName = N'Clue Planted'
) AND NOT EXISTS (
SELECT 1 FROM dbo.ThreadEvents te INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
WHERE te.PlotThreadID = pt.PlotThreadID AND tet.TypeName IN (N'Payoff', N'Question Answered', N'Revealed', N'Resolved')
) THEN 1 ELSE 0 END AS HasClueWithoutPayoff,
CASE WHEN EXISTS (
SELECT 1 FROM dbo.ThreadEvents te INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
WHERE te.PlotThreadID = pt.PlotThreadID AND tet.TypeName = N'Question Raised'
) AND NOT EXISTS (
SELECT 1 FROM dbo.ThreadEvents te INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
WHERE te.PlotThreadID = pt.PlotThreadID AND tet.TypeName = N'Question Answered'
) THEN 1 ELSE 0 END AS HasQuestionWithoutAnswer
FROM dbo.PlotThreads pt
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID
LEFT JOIN EventScope ON EventScope.PlotThreadID = pt.PlotThreadID
WHERE pl.ProjectID = @ProjectID AND pt.IsArchived = 0 AND pl.IsArchived = 0
AND (@BookID IS NULL OR pl.BookID IS NULL OR pl.BookID = @BookID)
ORDER BY ts.IsOpenStatus DESC, pt.Importance DESC, pl.PlotLineName, pt.ThreadTitle;
;WITH LastAssetEvent AS
(
SELECT ae.StoryAssetID, MAX(nso.GlobalSceneIndex) AS LastEventIndex, MAX(ae.SceneID) AS LastEventSceneID,
MAX(CONCAT(N'Ch ', nso.ChapterNumber, N' / Scene ', nso.SceneNumber, N': ', nso.SceneTitle)) AS LastEventSceneLabel
FROM dbo.AssetEvents ae
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = ae.SceneID
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
GROUP BY ae.StoryAssetID
)
SELECT sa.StoryAssetID, sa.AssetName, ak.KindName, sa.Importance, sa.CurrentStateID,
ast.StateName AS CurrentStateName, sa.CurrentLocationID, location.LocationPath AS CurrentLocationPath,
sa.IsResolved, LastAssetEvent.LastEventSceneID, LastAssetEvent.LastEventSceneLabel,
CASE WHEN EXISTS (SELECT 1 FROM dbo.AssetDependencies ad WHERE ad.IsArchived = 0 AND (ad.SourceAssetID = sa.StoryAssetID OR ad.TargetAssetID = sa.StoryAssetID)) THEN 1 ELSE 0 END AS HasDependencies,
CASE WHEN EXISTS (
SELECT 1 FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID
WHERE cw.ProjectID = @ProjectID AND cw.EntityType IN (N'StoryAsset', N'AssetDependency') AND cw.EntityID IN (sa.StoryAssetID)
AND wt.TypeName LIKE N'Asset%' AND cw.IsDismissed = 0 AND cw.IsIntentional = 0
) THEN 1 ELSE 0 END AS HasDependencyWarnings
FROM dbo.StoryAssets sa
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = sa.CurrentLocationID
LEFT JOIN LastAssetEvent ON LastAssetEvent.StoryAssetID = sa.StoryAssetID
WHERE sa.ProjectID = @ProjectID AND sa.IsArchived = 0
ORDER BY sa.IsResolved, sa.Importance DESC, sa.AssetName;
SELECT ws.SeverityName, COUNT(*) AS WarningCount
FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID
WHERE cw.ProjectID = @ProjectID AND (@BookID IS NULL OR cw.BookID = @BookID)
AND cw.IsDismissed = 0 AND cw.IsIntentional = 0
GROUP BY ws.SeverityName, ws.SortOrder
ORDER BY ws.SortOrder DESC;
SELECT TOP (12) wt.TypeName AS WarningTypeName, COUNT(*) AS WarningCount
FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID
WHERE cw.ProjectID = @ProjectID AND (@BookID IS NULL OR cw.BookID = @BookID)
AND cw.IsDismissed = 0 AND cw.IsIntentional = 0
GROUP BY wt.TypeName
ORDER BY COUNT(*) DESC, wt.TypeName;
SELECT TOP (12) cw.SceneID, s.SceneNumber, s.SceneTitle, c.ChapterNumber, b.BookTitle,
COUNT(*) AS WarningCount, MAX(ws.SortOrder) AS HighestSeveritySort
FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID
INNER JOIN dbo.Scenes s ON s.SceneID = cw.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE cw.ProjectID = @ProjectID AND cw.SceneID IS NOT NULL AND (@BookID IS NULL OR cw.BookID = @BookID)
AND cw.IsDismissed = 0 AND cw.IsIntentional = 0
GROUP BY cw.SceneID, s.SceneNumber, s.SceneTitle, c.ChapterNumber, b.BookTitle
ORDER BY COUNT(*) DESC, MAX(ws.SortOrder) DESC;
;WITH ScopeScenes AS
(
SELECT nso.SceneID, nso.BookID, nso.BookTitle, nso.ChapterID, nso.ChapterNumber, nso.SceneNumber, nso.SceneTitle,
COALESCE(s.POVCharacterID, c.POVCharacterID) AS POVCharacterID
FROM dbo.NarrativeSceneOrder nso
INNER JOIN dbo.Scenes s ON s.SceneID = nso.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = nso.ChapterID
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber)
AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber)
AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber)
AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber)
)
SELECT COALESCE(ch.CharacterName, N'No POV') AS POVName, ss.POVCharacterID, COUNT(*) AS SceneCount
FROM ScopeScenes ss
LEFT JOIN dbo.Characters ch ON ch.CharacterID = ss.POVCharacterID
GROUP BY ss.POVCharacterID, ch.CharacterName
ORDER BY COUNT(*) DESC, POVName;
;WITH ScopeScenes AS
(
SELECT nso.SceneID, nso.ChapterID
FROM dbo.NarrativeSceneOrder nso
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
)
SELECT c.ChapterID, c.ChapterNumber, c.ChapterTitle, COUNT(DISTINCT COALESCE(s.POVCharacterID, c.POVCharacterID)) AS POVCount
FROM dbo.Chapters c
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID
WHERE c.ChapterID IN (SELECT ChapterID FROM ScopeScenes)
GROUP BY c.ChapterID, c.ChapterNumber, c.ChapterTitle
HAVING COUNT(DISTINCT COALESCE(s.POVCharacterID, c.POVCharacterID)) > 1
ORDER BY c.ChapterNumber;
SELECT ch.CharacterID, ch.CharacterName, COUNT(DISTINCT sc.SceneID) AS SceneCount
FROM dbo.Characters ch
LEFT JOIN dbo.SceneCharacters sc ON sc.CharacterID = ch.CharacterID
LEFT JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = sc.SceneID
WHERE ch.ProjectID = @ProjectID AND ch.IsArchived = 0 AND (@BookID IS NULL OR nso.BookID = @BookID OR sc.SceneID IS NULL)
GROUP BY ch.CharacterID, ch.CharacterName
ORDER BY COUNT(DISTINCT sc.SceneID) DESC, ch.CharacterName;
;WITH SceneCharacterCounts AS
(
SELECT nso.SceneID, nso.SceneNumber, nso.SceneTitle, nso.ChapterNumber, COUNT(sc.SceneCharacterID) AS CharacterCount
FROM dbo.NarrativeSceneOrder nso
LEFT JOIN dbo.SceneCharacters sc ON sc.SceneID = nso.SceneID
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
GROUP BY nso.SceneID, nso.SceneNumber, nso.SceneTitle, nso.ChapterNumber
)
SELECT TOP (12) * FROM SceneCharacterCounts WHERE CharacterCount >= 5 ORDER BY CharacterCount DESC, ChapterNumber, SceneNumber;
;WITH ScopeScenes AS
(
SELECT nso.SceneID, s.PrimaryLocationID
FROM dbo.NarrativeSceneOrder nso
INNER JOIN dbo.Scenes s ON s.SceneID = nso.SceneID
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
)
SELECT COALESCE(lp.LocationName, N'No location') AS LocationName, lp.LocationID, COALESCE(lp.LocationPath, N'No location') AS LocationPath,
COUNT(*) AS SceneCount
FROM ScopeScenes ss
LEFT JOIN dbo.LocationPaths lp ON lp.LocationID = ss.PrimaryLocationID
GROUP BY lp.LocationID, lp.LocationName, lp.LocationPath
ORDER BY COUNT(*) DESC, LocationName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Analytics_StoryHealth
@ProjectID int,
@BookID int = NULL,
@StartChapterNumber decimal(10,2) = NULL,
@EndChapterNumber decimal(10,2) = NULL,
@StartSceneNumber decimal(10,2) = NULL,
@EndSceneNumber decimal(10,2) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @HasMetricSettings bit =
CASE WHEN EXISTS (SELECT 1 FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID) THEN 1 ELSE 0 END;
;WITH ScopeScenes AS
(
SELECT nso.*
FROM dbo.NarrativeSceneOrder nso
WHERE nso.ProjectID = @ProjectID
AND (@BookID IS NULL OR nso.BookID = @BookID)
AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber)
AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber)
AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber)
AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber)
)
SELECT ss.SceneID, ss.BookID, ss.BookTitle, ss.ChapterID, ss.ChapterNumber, ss.ChapterTitle,
ss.SceneNumber, ss.SceneTitle, ss.GlobalSceneIndex, ss.BookSceneIndex, smt.MetricTypeID,
smt.MetricName, smv.Value, smt.MinValue, smt.MaxValue,
CASE WHEN smt.MaxValue = smt.MinValue THEN 0 ELSE CAST(((smv.Value - smt.MinValue) * 100.0) / (smt.MaxValue - smt.MinValue) AS int) END AS [Percent]
FROM ScopeScenes ss
INNER JOIN dbo.SceneMetricValues smv ON smv.SceneID = ss.SceneID
INNER JOIN dbo.SceneMetricTypes smt ON smt.MetricTypeID = smv.MetricTypeID
WHERE smt.IsActive = 1
AND
(
(@HasMetricSettings = 1 AND EXISTS
(
SELECT 1
FROM dbo.ProjectTimelineMetricSettings ptms
WHERE ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = smt.MetricTypeID
))
OR (@HasMetricSettings = 0 AND smt.MetricName IN
(
N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Romance',
N'Violence', N'Sexual Charge', N'Mystery', N'Hope / Lightness', N'Comedy', N'Darkness'
))
)
ORDER BY smt.SortOrder, ss.GlobalSceneIndex;
;WITH ScopeScenes AS
(
SELECT nso.SceneID, nso.ChapterID
FROM dbo.NarrativeSceneOrder nso
WHERE nso.ProjectID = @ProjectID
AND (@BookID IS NULL OR nso.BookID = @BookID)
AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber)
AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber)
AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber)
AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber)
)
SELECT rs.RevisionStatusID, rs.StatusName, COUNT(s.SceneID) AS SceneCount,
SUM(CASE WHEN c.ChapterID IS NOT NULL THEN 1 ELSE 0 END) AS ChapterCount
FROM dbo.RevisionStatuses rs
LEFT JOIN dbo.Scenes s ON s.RevisionStatusID = rs.RevisionStatusID AND s.SceneID IN (SELECT SceneID FROM ScopeScenes)
LEFT JOIN dbo.Chapters c ON c.RevisionStatusID = rs.RevisionStatusID AND c.ChapterID IN (SELECT DISTINCT ChapterID FROM ScopeScenes)
GROUP BY rs.RevisionStatusID, rs.StatusName, rs.SortOrder
ORDER BY rs.SortOrder;
;WITH ScopeScenes AS
(
SELECT nso.SceneID, nso.BookID, nso.BookTitle, nso.ChapterID, nso.ChapterNumber, nso.SceneNumber, nso.SceneTitle
FROM dbo.NarrativeSceneOrder nso
WHERE nso.ProjectID = @ProjectID
AND (@BookID IS NULL OR nso.BookID = @BookID)
AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber)
AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber)
AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber)
AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber)
)
SELECT ss.SceneID, ss.BookID, ss.BookTitle, ss.ChapterID, ss.ChapterNumber, ss.SceneNumber, ss.SceneTitle,
rs.StatusName AS RevisionStatusName
FROM ScopeScenes ss
INNER JOIN dbo.Scenes s ON s.SceneID = ss.SceneID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
WHERE rs.StatusName IN (N'Needs Work', N'Cut Candidate', N'Drafted')
ORDER BY rs.SortOrder, ss.BookID, ss.ChapterNumber, ss.SceneNumber;
;WITH ScopeScenes AS
(
SELECT nso.SceneID
FROM dbo.NarrativeSceneOrder nso
WHERE nso.ProjectID = @ProjectID
AND (@BookID IS NULL OR nso.BookID = @BookID)
AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber)
AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber)
AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber)
AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber)
),
Total AS (SELECT COUNT(*) AS TotalScenes FROM ScopeScenes)
SELECT spt.ScenePurposeTypeID, spt.PurposeName, COUNT(sp.SceneID) AS SceneCount,
CASE WHEN Total.TotalScenes = 0 THEN 0 ELSE CAST(COUNT(sp.SceneID) * 100.0 / Total.TotalScenes AS decimal(5,1)) END AS PercentOfScenes
FROM dbo.ScenePurposeTypes spt
CROSS JOIN Total
LEFT JOIN dbo.ScenePurposes sp ON sp.ScenePurposeTypeID = spt.ScenePurposeTypeID AND sp.SceneID IN (SELECT SceneID FROM ScopeScenes)
WHERE spt.IsArchived = 0
GROUP BY spt.ScenePurposeTypeID, spt.PurposeName, spt.SortOrder, Total.TotalScenes
ORDER BY spt.SortOrder;
;WITH ScopeScenes AS
(
SELECT nso.SceneID, nso.BookID, nso.BookTitle, nso.ChapterID, nso.ChapterNumber, nso.SceneNumber, nso.SceneTitle
FROM dbo.NarrativeSceneOrder nso
INNER JOIN dbo.Scenes s ON s.SceneID = nso.SceneID
WHERE nso.ProjectID = @ProjectID
AND (@BookID IS NULL OR nso.BookID = @BookID)
AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber)
AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber)
AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber)
AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber)
AND (
NOT EXISTS (SELECT 1 FROM dbo.ScenePurposes sp WHERE sp.SceneID = nso.SceneID)
OR ISNULL(NULLIF(LTRIM(RTRIM(s.SceneOutcomeNotes)), N''), N'') = N''
)
)
SELECT ss.SceneID, ss.BookID, ss.BookTitle, ss.ChapterID, ss.ChapterNumber, ss.SceneNumber, ss.SceneTitle,
CASE WHEN NOT EXISTS (SELECT 1 FROM dbo.ScenePurposes sp WHERE sp.SceneID = ss.SceneID) THEN 1 ELSE 0 END AS MissingPurpose,
CASE WHEN ISNULL(NULLIF(LTRIM(RTRIM(s.SceneOutcomeNotes)), N''), N'') = N'' THEN 1 ELSE 0 END AS MissingOutcome
FROM ScopeScenes ss
INNER JOIN dbo.Scenes s ON s.SceneID = ss.SceneID
ORDER BY ss.BookID, ss.ChapterNumber, ss.SceneNumber;
EXEC dbo.Analytics_StoryHealth_Remainder @ProjectID, @BookID, @StartChapterNumber, @EndChapterNumber, @StartSceneNumber, @EndSceneNumber;
END;
GO

View File

@ -14,6 +14,11 @@ public sealed class ProjectEditViewModel
public string ProjectName { get; set; } = string.Empty;
public string? Description { get; set; }
[Display(Name = "Genre / Metric Preset")]
public string GenreMetricPresetKey { get; set; } = "Custom";
public IReadOnlyList<SelectListItem> GenreMetricPresetOptions { get; set; } = [];
}
public sealed class ProjectDetailViewModel

View File

@ -23,6 +23,14 @@
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description" class="form-control" rows="5"></textarea>
</div>
@if (Model.ProjectID == 0)
{
<div class="mb-3">
<label asp-for="GenreMetricPresetKey" class="form-label"></label>
<select asp-for="GenreMetricPresetKey" asp-items="Model.GenreMetricPresetOptions" class="form-select"></select>
<div class="form-text">Choose a starting set of scene metrics for this project. You can adjust timeline metrics later in Timeline Settings.</div>
</div>
}
<button class="btn btn-primary" type="submit">Save project</button>
</form>

View File

@ -100,7 +100,7 @@
</div>
<span class="soft-count">@Model.MetricOptions.Count metrics</span>
</div>
<p class="muted">These metrics appear on the timeline graph only. Story Health can continue using the full metric set.</p>
<p class="muted">These metrics appear on the timeline graph and Story Health metric shape for this project.</p>
<span asp-validation-for="SelectedMetricTypeIDs" class="text-danger"></span>
<div class="timeline-settings-grid timeline-metric-settings-grid">
@foreach (var metric in Model.MetricOptions)