From fd3b5c43ee8139d558df5389031a311aa7993488 Mon Sep 17 00:00:00 2001 From: Brodin Date: Mon, 26 Aug 2024 14:18:04 -0400 Subject: [PATCH 01/22] adding setup info --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 9f60652..a09489e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,41 @@ This is a database for hosting Blood on the Clocktower custom scripts. You can v - Filter scripts based on required characters or characters to exclude. - Option to vote for your favourite scripts +## Local Setup + +### Setting up Virtualenv + +You can install without virtualenv but it's recommended to use this method. + + python3 -m venv ./venv + source ./venv/bin/activate + +To install your dependencies, you'll be using `poetry`: + + pip3 install poetry + poetry install + +Note: I was getting stuck in the install and running `export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring` seemed to fix it. + +### Setting the Config + +By default, `manage.py` looks for the file `botc/local.py`. You'll need to duplicate the `botc/settings.py` file and add your local settings there. + +Be sure to add `SECRET_KEY=` with your own random string to the file. [You can generate one here](https://randomkeygen.com/). + +You'll also need to set `DATABASES` to the values that match your database. + +### Running and Migration + +Run this command to get the app to run on your local browser at `localhost:8000`: + + python3 manage.py migrate + python3 manage.py runserver + +To create an admin account, run the following command and you can login at `localhost:8000/admin` + + python3 manage.py createsuperuser + ## Acknowledgements This site is not affiliated with The Pandemonium Institute. All roles and characters are the property of Steven Medway and The Pandemonium Institute. From cefe7f0a5d473466f53dd6d12f87d9cab7aafeb3 Mon Sep 17 00:00:00 2001 From: Geoff Thomas Date: Mon, 7 Oct 2024 22:18:11 +0100 Subject: [PATCH 02/22] The start of the great homebrew project --- botc/urls.py | 6 +++++- homebrew/__init__.py | 0 homebrew/urls.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 homebrew/__init__.py create mode 100644 homebrew/urls.py diff --git a/botc/urls.py b/botc/urls.py index 1ec65f4..1e895c6 100644 --- a/botc/urls.py +++ b/botc/urls.py @@ -17,4 +17,8 @@ from django.contrib import admin from django.urls import include, path -urlpatterns = [path("admin/", admin.site.urls), path("", include("scripts.urls"))] +urlpatterns = [ + path("admin/", admin.site.urls), + path("", include("scripts.urls")), + path("homebrew/", include("homebrew.urls")), +] diff --git a/homebrew/__init__.py b/homebrew/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/homebrew/urls.py b/homebrew/urls.py new file mode 100644 index 0000000..cf424df --- /dev/null +++ b/homebrew/urls.py @@ -0,0 +1,40 @@ +from django.urls import path +from scripts import views + +# Routers provide an easy way of automatically determining the URL conf. +urlpatterns = [ + path("", views.ScriptsListView.as_view()), + path("comment//edit", views.CommentEditView.as_view(), name="edit_comment"), + path("comment/new", views.CommentCreateView.as_view(), name="create_comment"), + path("script/", views.ScriptView.as_view(), name="script"), + path( + "script///similar", + views.get_similar_scripts, + name="similar", + ), + path("script//", views.ScriptView.as_view(), name="script"), + path("script///vote", views.vote_for_script, name="vote"), + path( + "script///favourite", + views.favourite_script, + name="favourite", + ), + path( + "script///delete", + views.ScriptDeleteView.as_view(), + name="delete_script", + ), + path( + "script///download", + views.download_json, + name="download_json", + ), + path( + "script///download_pdf", + views.download_pdf, + name="download_pdf", + ), + path("script/search", views.AdvancedSearchView.as_view(), name="advanced_search"), + path("script/search/results", views.AdvancedSearchResultsView.as_view()), + path("script/upload", views.ScriptUploadView.as_view(), name="upload"), +] \ No newline at end of file From 384fbd4d678e24ab0666497afe6fb58730fac58f Mon Sep 17 00:00:00 2001 From: Geoff Thomas Date: Mon, 7 Oct 2024 23:58:56 +0100 Subject: [PATCH 03/22] Initial site breakdown --- botc/settings.py | 1 + homebrew/apps.py | 4 + homebrew/templates/homebrew/navbar.html | 49 ++++++++++++ homebrew/urls.py | 31 ++++---- homebrew/views.py | 28 +++++++ scripts/templates/base.html | 99 ++++--------------------- scripts/templates/navbar.html | 87 ++++++++++++++++++++++ 7 files changed, 199 insertions(+), 100 deletions(-) create mode 100644 homebrew/apps.py create mode 100644 homebrew/templates/homebrew/navbar.html create mode 100644 homebrew/views.py create mode 100644 scripts/templates/navbar.html diff --git a/botc/settings.py b/botc/settings.py index 3a0e200..eb01291 100644 --- a/botc/settings.py +++ b/botc/settings.py @@ -28,6 +28,7 @@ "django.contrib.staticfiles", "django.contrib.postgres", "scripts.apps.ScriptsConfig", + "homebrew.apps.HomebrewConfig", "versionfield", "django_tables2", "django_filters", diff --git a/homebrew/apps.py b/homebrew/apps.py new file mode 100644 index 0000000..7a12212 --- /dev/null +++ b/homebrew/apps.py @@ -0,0 +1,4 @@ +from django.apps import AppConfig + +class HomebrewConfig(AppConfig): + name = "homebrew" diff --git a/homebrew/templates/homebrew/navbar.html b/homebrew/templates/homebrew/navbar.html new file mode 100644 index 0000000..eda7ee0 --- /dev/null +++ b/homebrew/templates/homebrew/navbar.html @@ -0,0 +1,49 @@ +{% load i18n %} + + \ No newline at end of file diff --git a/homebrew/urls.py b/homebrew/urls.py index cf424df..bc7fd86 100644 --- a/homebrew/urls.py +++ b/homebrew/urls.py @@ -1,40 +1,41 @@ from django.urls import path -from scripts import views +from scripts import views as s_views +from homebrew import views as h_views # Routers provide an easy way of automatically determining the URL conf. urlpatterns = [ - path("", views.ScriptsListView.as_view()), - path("comment//edit", views.CommentEditView.as_view(), name="edit_comment"), - path("comment/new", views.CommentCreateView.as_view(), name="create_comment"), - path("script/", views.ScriptView.as_view(), name="script"), + path("", h_views.ScriptsListView.as_view()), + path("comment//edit", s_views.CommentEditView.as_view(), name="edit_comment"), + path("comment/new", s_views.CommentCreateView.as_view(), name="create_comment"), + path("script/", s_views.ScriptView.as_view(), name="script"), path( "script///similar", - views.get_similar_scripts, + s_views.get_similar_scripts, name="similar", ), - path("script//", views.ScriptView.as_view(), name="script"), - path("script///vote", views.vote_for_script, name="vote"), + path("script//", s_views.ScriptView.as_view(), name="script"), + path("script///vote", s_views.vote_for_script, name="vote"), path( "script///favourite", - views.favourite_script, + s_views.favourite_script, name="favourite", ), path( "script///delete", - views.ScriptDeleteView.as_view(), + s_views.ScriptDeleteView.as_view(), name="delete_script", ), path( "script///download", - views.download_json, + s_views.download_json, name="download_json", ), path( "script///download_pdf", - views.download_pdf, + s_views.download_pdf, name="download_pdf", ), - path("script/search", views.AdvancedSearchView.as_view(), name="advanced_search"), - path("script/search/results", views.AdvancedSearchResultsView.as_view()), - path("script/upload", views.ScriptUploadView.as_view(), name="upload"), + path("script/search", s_views.AdvancedSearchView.as_view(), name="advanced_search"), + path("script/search/results", s_views.AdvancedSearchResultsView.as_view()), + path("script/upload", s_views.ScriptUploadView.as_view(), name="upload"), ] \ No newline at end of file diff --git a/homebrew/views.py b/homebrew/views.py new file mode 100644 index 0000000..d8adc8f --- /dev/null +++ b/homebrew/views.py @@ -0,0 +1,28 @@ +from django_tables2.views import SingleTableMixin +from django_filters.views import FilterView +from scripts import models, filters, tables + +class ScriptsListView(SingleTableMixin, FilterView): + model = models.ScriptVersion + template_name = "scriptlist.html" + table_pagination = {"per_page": 20} + ordering = ["-pk"] + script_view = None + + def get_filterset_class(self): + if self.request.user.is_authenticated: + return filters.FavouriteScriptVersionFilter + return filters.ScriptVersionFilter + + def get_filterset_kwargs(self, filterset_class): + kwargs = super(ScriptsListView, self).get_filterset_kwargs(filterset_class) + if kwargs["data"] is None: + kwargs["data"] = {"latest": True} + return kwargs + + def get_table_class(self): + if self.request.user.is_authenticated: + return tables.UserScriptTable + return tables.ScriptTable + + diff --git a/scripts/templates/base.html b/scripts/templates/base.html index cf537c4..65f8b34 100644 --- a/scripts/templates/base.html +++ b/scripts/templates/base.html @@ -26,88 +26,11 @@ - + {% if "homebrew/" in request.path %} + {% include 'homebrew/navbar.html' %} + {% else %} + {% include 'navbar.html' %} + {% endif %} {% if BANNER %}
@@ -125,9 +48,15 @@
- This project is not affiliated with The Pandemonium Institute. - All roles, content are the property of Steven Medway and The Pandemonium Institute. - + {% if "homebrew/" in request.path %} + This project is not affiliated with The Pandemonium Institute. + All roles, content are the property of their respective owners. + + {% else %} + This project is not affiliated with The Pandemonium Institute. + All roles, content are the property of Steven Medway and The Pandemonium Institute. + + {% endif %}
diff --git a/scripts/templates/navbar.html b/scripts/templates/navbar.html new file mode 100644 index 0000000..4511119 --- /dev/null +++ b/scripts/templates/navbar.html @@ -0,0 +1,87 @@ +{% load i18n %} + + \ No newline at end of file From b490f3a63a3fe962255294484cd477f91482ed95 Mon Sep 17 00:00:00 2001 From: Geoff Thomas Date: Tue, 8 Oct 2024 23:53:14 +0100 Subject: [PATCH 04/22] Commonizing some elements and separating out some homebrew specific elements --- homebrew/filters.py | 50 + homebrew/models.py | 11 + homebrew/templates/homebrew/script.html | 290 +++ homebrew/urls.py | 13 +- homebrew/views.py | 13 +- poetry.lock | 183 +- pyproject.toml | 1 + scripts/filters.py | 24 +- scripts/fixtures/characters.json | 3137 +++++++++++++++++++++++ scripts/forms.py | 63 +- scripts/models.py | 26 +- scripts/templates/delete_comment.html | 6 +- scripts/templates/navbar.html | 2 +- scripts/views.py | 87 +- 14 files changed, 3831 insertions(+), 75 deletions(-) create mode 100644 homebrew/filters.py create mode 100644 homebrew/models.py create mode 100644 homebrew/templates/homebrew/script.html create mode 100644 scripts/fixtures/characters.json diff --git a/homebrew/filters.py b/homebrew/filters.py new file mode 100644 index 0000000..e7c4a42 --- /dev/null +++ b/homebrew/filters.py @@ -0,0 +1,50 @@ +import django_filters +from django import forms +from scripts import filters, models + +class HomebrewVersionFilter(filters.BaseScriptVersionFilter): + class Meta: + model = models.ScriptVersion + fields = [ + "search", + "script_type", + "include", + "exclude", + # "homebrewiness", + "author", + "mono_demon", + "all_scripts", + ] + + +class FavouriteHomebrewVersionFilter(HomebrewVersionFilter): + favourites = django_filters.filters.BooleanFilter( + method="display_favourites", widget=forms.CheckboxInput, label="Favourites" + ) + my_scripts = django_filters.filters.BooleanFilter( + method="filter_my_scripts", + widget=forms.CheckboxInput, + label="My Scripts", + ) + + def display_favourites(self, queryset, name, value): + if value: + return queryset.filter(favourites__user=self.request.user) + return queryset + + class Meta: + model = models.ScriptVersion + fields = [ + "search", + "script_type", + "include", + "exclude", + # "homebrewiness", + "author", + "mono_demon", + "favourites", + "my_scripts", + "all_scripts", + ] + + diff --git a/homebrew/models.py b/homebrew/models.py new file mode 100644 index 0000000..3c0b28b --- /dev/null +++ b/homebrew/models.py @@ -0,0 +1,11 @@ +from scripts import models as s_models + +class HomebrewCharacter(s_models.BaseCharacter): + """ + Model for homebrew characters. + + The BaseCharacter class has all the information required in currently, but is an abstract + class in case we want to change the inheritence hierarchy. + """ + pass + diff --git a/homebrew/templates/homebrew/script.html b/homebrew/templates/homebrew/script.html new file mode 100644 index 0000000..1ff7d95 --- /dev/null +++ b/homebrew/templates/homebrew/script.html @@ -0,0 +1,290 @@ +{% extends 'base.html' %} + +{% block content %} +{% load markdownify %} +{% load botc_script_tags %} +{% load bootstrap_icons %} + + + +
+
+

+ {{ script.name }} - v{{ script_version.version }} +

+
+
+ {% include "info.html" with record=script_version %} +
+
+ +{% if script_version.author or script_version.tags.all %} +
+ {% if script_version.author %} +
+

+ by {{ script_version.author }} +

+
+ {% endif %} + + {% if script_version.script.owner == user %} +
+

You

+
+ {% endif %} + + {% if script_version.tags.all|length > 0 %} + {% load botc_script_tags %} +
+ {% for tag in script_version.tags.all %} + {{ tag }} + {% endfor %} +
+ {% endif %} +
+{% endif %} + +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ {% if script_version.pdf %} + + {% endif %} + {% if not script.owner or user == script_version.script.owner %} + + {% endif %} +
+ +
+ {% if user.is_authenticated %} + {% if user.collections.count > 0 %} + {% script_not_in_user_collection user script_version as script_can_be_added_to_collection %} + {% if script_can_be_added_to_collection %} +
+ {% include "add_to_collection.html" %} +
+ {% endif %} + {% endif %} + {% if can_delete %} +
+ {% include "delete_script.html" %} +
+ {% endif %} +
+ {% include "favourite.html" with record=script_version %} +
+ {% endif %} +
+
+{% if user.is_staff %} +
+ Script ID: {{ script_version.pk }} +
+{% endif %} + + + +
+ {% if script_version.pdf %} +
+ +
+ {% endif %} + {% if script_version.notes %} +
+ {{ script_version.notes|markdownify }} +
+
+ {% else %} +
+ {% endif %} + {% for role in script_version.content %} + {% if role.id != "_meta" %} + {% character_type_change script_version.content forloop.counter0 as newline %} + {% if newline %} +
+ {% endif %} + {% character_colourisation role.id as character_colour %} +
  • {% convert_id_to_friendly_text role.id %}
  • + {% endif %} + {% endfor %} +
    + {% if changes.items|length > 0 %} +
    + {% for version, diffs in changes.items %} +

    {{ diffs.previous_version }} -> {{ version }}

    +
    +
    +
      + {% for role in diffs.additions %} +
    • + {% convert_id_to_friendly_text role.id %}
    • + {% endfor %} +
    +
    +
    +
      + {% for role in diffs.deletions %} +
    • - {% convert_id_to_friendly_text role.id %}
    • + {% endfor %} +
    +
    +
    + {% endfor %} +
    + {% endif %} +
    +
      + {% for collection in script_version.collections.all %} +
    • {{collection.name}}
    • + {% endfor %} +
    +
    + {% if script_version.pdf %} +
    + {% else %} +
    + {% endif %} + {% for comment_info in comments %} +
    + {% if comment_info.indent == 0 %} +
    + {% else %} +
    + {% endif %} + {{ comment_info.comment.comment|markdownify }} +
    +
    + {% if comment_info.comment.user == user %} + + {% include "delete_comment.html" with comment=comment_info.comment %} + {% endif %} + {% if user.is_authenticated %} + + {% endif %} +
    +
    + {% if user.is_authenticated %} +
    +
    +
    + {% csrf_token %} + +
    + +
    +
    +
    +
    +
    + {% if comment_info.indent > 0 %} +
    + {% else %} +
    + {% endif %} +
    + {% csrf_token %} + + + +
    + +
    +
    +
    +
    + {% endif %} + {% endfor %} + {% if user.is_authenticated %} + +
    +
    +
    + {% csrf_token %} + + +
    + +
    +
    +
    +
    + {% endif %} +
    + + +{% endblock %} diff --git a/homebrew/urls.py b/homebrew/urls.py index bc7fd86..66c62ad 100644 --- a/homebrew/urls.py +++ b/homebrew/urls.py @@ -7,13 +7,18 @@ path("", h_views.ScriptsListView.as_view()), path("comment//edit", s_views.CommentEditView.as_view(), name="edit_comment"), path("comment/new", s_views.CommentCreateView.as_view(), name="create_comment"), - path("script/", s_views.ScriptView.as_view(), name="script"), + path( + "comment//delete", + s_views.CommentDeleteView.as_view(), + name="delete_homebrew_comment", + ), + path("script/", h_views.HomebrewScriptView.as_view(), name="script"), path( "script///similar", s_views.get_similar_scripts, name="similar", ), - path("script//", s_views.ScriptView.as_view(), name="script"), + path("script//", h_views.HomebrewScriptView.as_view(), name="script"), path("script///vote", s_views.vote_for_script, name="vote"), path( "script///favourite", @@ -22,7 +27,7 @@ ), path( "script///delete", - s_views.ScriptDeleteView.as_view(), + h_views.HomebrewDeleteView.as_view(), name="delete_script", ), path( @@ -35,7 +40,5 @@ s_views.download_pdf, name="download_pdf", ), - path("script/search", s_views.AdvancedSearchView.as_view(), name="advanced_search"), - path("script/search/results", s_views.AdvancedSearchResultsView.as_view()), path("script/upload", s_views.ScriptUploadView.as_view(), name="upload"), ] \ No newline at end of file diff --git a/homebrew/views.py b/homebrew/views.py index d8adc8f..e1456d2 100644 --- a/homebrew/views.py +++ b/homebrew/views.py @@ -1,6 +1,7 @@ from django_tables2.views import SingleTableMixin from django_filters.views import FilterView -from scripts import models, filters, tables +from scripts import models, tables, views +from homebrew import filters class ScriptsListView(SingleTableMixin, FilterView): model = models.ScriptVersion @@ -11,8 +12,8 @@ class ScriptsListView(SingleTableMixin, FilterView): def get_filterset_class(self): if self.request.user.is_authenticated: - return filters.FavouriteScriptVersionFilter - return filters.ScriptVersionFilter + return filters.FavouriteHomebrewVersionFilter + return filters.HomebrewVersionFilter def get_filterset_kwargs(self, filterset_class): kwargs = super(ScriptsListView, self).get_filterset_kwargs(filterset_class) @@ -25,4 +26,10 @@ def get_table_class(self): return tables.UserScriptTable return tables.ScriptTable +class HomebrewScriptView(views.ScriptView): + template_name = "homebrew/script.html" + +class HomebrewDeleteView(views.ScriptDeleteView): + def determine_success_url(self, script): + return f"/homebrew/script/{script.pk}" diff --git a/poetry.lock b/poetry.lock index a7f285e..6956a13 100644 --- a/poetry.lock +++ b/poetry.lock @@ -17,6 +17,25 @@ typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] +[[package]] +name = "attrs" +version = "24.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + [[package]] name = "azure-core" version = "1.31.0" @@ -770,6 +789,41 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "jsonschema" +version = "4.23.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +files = [ + {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, + {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + [[package]] name = "markdown" version = "3.7" @@ -1082,6 +1136,21 @@ pytest = ">=7.0.0" docs = ["sphinx", "sphinx-rtd-theme"] testing = ["Django", "django-configurations (>=2.0)"] +[[package]] +name = "referencing" +version = "0.35.1" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + [[package]] name = "requests" version = "2.32.3" @@ -1121,6 +1190,118 @@ requests = ">=2.0.0" [package.extras] rsa = ["oauthlib[signedtoken] (>=3.0.0)"] +[[package]] +name = "rpds-py" +version = "0.20.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, + {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, + {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, + {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, + {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, + {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, + {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, + {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, + {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"}, + {file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"}, + {file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, + {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, + {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, + {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, +] + [[package]] name = "six" version = "1.16.0" @@ -1251,4 +1432,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "add32245fb597e3a6ae60525fcbd4691925e55d6e1d6adc20dd911ac4e6df026" +content-hash = "f11997ade583e6e21c1025f2454b19d6dcb4e656059b9ddaa45e9303c322f134" diff --git a/pyproject.toml b/pyproject.toml index 4999c68..6cbd6b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ Babel = "^2.13.1" django-markdownify = "^0.9.2" requests = "^2.31.0" django-cors-headers = "^4.3.0" +jsonschema = "^4.23.0" [tool.poetry.dev-dependencies] black = "*" diff --git a/scripts/filters.py b/scripts/filters.py index 2424acf..5c7916f 100644 --- a/scripts/filters.py +++ b/scripts/filters.py @@ -47,8 +47,7 @@ def exclude_characters(queryset, value): def name_to_id(name: str): return name.replace(" ", "_").replace("'", "").lower() - -class ScriptVersionFilter(filters.FilterSet): +class BaseScriptVersionFilter(filters.FilterSet): all_scripts = django_filters.filters.BooleanFilter( method="display_all_scripts", widget=forms.CheckboxInput, @@ -62,15 +61,6 @@ class ScriptVersionFilter(filters.FilterSet): ) author = django_filters.filters.CharFilter(method="search_authors", label="Author") search = django_filters.filters.CharFilter(method="search_scripts", label="Search") - tags = django_filters.filters.ModelMultipleChoiceFilter( - queryset=models.ScriptTag.objects.all().order_by("order"), - widget=widgets.BadgePillSelectMultiple, - ) - edition = django_filters.filters.ChoiceFilter( - label="Edition", - method="filter_edition", - choices=edition_choices, - ) mono_demon = django_filters.filters.BooleanFilter( method="filter_mono_demon_scripts", widget=forms.CheckboxInput, @@ -118,6 +108,18 @@ def search_authors(self, queryset, name, value): return queryset.filter(similarity__gt=0.3).order_by("-similarity") + +class ScriptVersionFilter(BaseScriptVersionFilter): + tags = django_filters.filters.ModelMultipleChoiceFilter( + queryset=models.ScriptTag.objects.all().order_by("order"), + widget=widgets.BadgePillSelectMultiple, + ) + edition = django_filters.filters.ChoiceFilter( + label="Edition", + method="filter_edition", + choices=edition_choices, + ) + def filter_edition(self, queryset, _, value): return queryset.filter(edition__lte=value) diff --git a/scripts/fixtures/characters.json b/scripts/fixtures/characters.json new file mode 100644 index 0000000..61061a1 --- /dev/null +++ b/scripts/fixtures/characters.json @@ -0,0 +1,3137 @@ +[ + { + "model": "scripts.character", + "pk": 1, + "fields": { + "character_id": "washerwoman", + "character_name": "Washerwoman", + "ability": "You start knowing that 1 of 2 players is a particular Townsfolk.", + "first_night_reminder": "Show the character token of a Townsfolk in play. Point to two players, one of which is that character.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Townsfolk,Wrong", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 33, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/washerwoman.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 2, + "fields": { + "character_id": "librarian", + "character_name": "Librarian", + "ability": "You start knowing that 1 of 2 players is a particular Outsider. (Or that zero are in play.)", + "first_night_reminder": "Show the character token of an Outsider in play. Point to two players, one of which is that character.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Outsider,Wrong", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 34, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/librarian.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 3, + "fields": { + "character_id": "investigator", + "character_name": "Investigator", + "ability": "You start knowing that 1 of 2 players is a particular Minion.", + "first_night_reminder": "Show the character token of a Minion in play. Point to two players, one of which is that character.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Minion,Wrong", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 35, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/investigator.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 4, + "fields": { + "character_id": "chef", + "character_name": "Chef", + "ability": "You start knowing how many pairs of evil players there are.", + "first_night_reminder": "Show the finger signal (0, 1, 2, …) for the number of pairs of neighbouring evil players.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 36, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/chef.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 5, + "fields": { + "character_id": "empath", + "character_name": "Empath", + "ability": "Each night, you learn how many of your 2 alive neighbours are evil.", + "first_night_reminder": "Show the finger signal (0, 1, 2) for the number of evil alive neighbours of the Empath.", + "other_night_reminder": "Show the finger signal (0, 1, 2) for the number of evil neighbours.", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 37, + "other_night_position": 53, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/empath.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 6, + "fields": { + "character_id": "fortuneteller", + "character_name": "Fortune Teller", + "ability": "Each night, choose 2 players: you learn if either is a Demon. There is a good player that registers as a Demon to you.", + "first_night_reminder": "The Fortune Teller points to two players. Give the head signal (nod yes, shake no) for whether one of those players is the Demon.", + "other_night_reminder": "The Fortune Teller points to two players. Show the head signal (nod 'yes', shake 'no') for whether one of those players is the Demon.", + "global_reminders": null, + "reminders": "Red herring", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 38, + "other_night_position": 54, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/fortuneteller.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 7, + "fields": { + "character_id": "undertaker", + "character_name": "Undertaker", + "ability": "Each night*, you learn which character died by execution today.", + "first_night_reminder": "", + "other_night_reminder": "If a player was executed today: Show that player’s character token.", + "global_reminders": null, + "reminders": "Executed", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 55, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/undertaker.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 8, + "fields": { + "character_id": "monk", + "character_name": "Monk", + "ability": "Each night*, choose a player (not yourself): they are safe from the Demon tonight.", + "first_night_reminder": "", + "other_night_reminder": "The previously protected player is no longer protected. The Monk points to a player not themself. Mark that player 'Protected'.", + "global_reminders": null, + "reminders": "Protected", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 12, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/monk.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 9, + "fields": { + "character_id": "ravenkeeper", + "character_name": "Ravenkeeper", + "ability": "If you die at night, you are woken to choose a player: you learn their character.", + "first_night_reminder": "", + "other_night_reminder": "If the Ravenkeeper died tonight: The Ravenkeeper points to a player. Show that player’s character token.", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 52, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/ravenkeeper.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 10, + "fields": { + "character_id": "virgin", + "character_name": "Virgin", + "ability": "The 1st time you are nominated, if the nominator is a Townsfolk, they are executed immediately.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "No ability", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/virgin.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 11, + "fields": { + "character_id": "slayer", + "character_name": "Slayer", + "ability": "Once per game, during the day, publicly choose a player: if they are the Demon, they die.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "No ability", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/slayer.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 12, + "fields": { + "character_id": "soldier", + "character_name": "Soldier", + "ability": "You are safe from the Demon.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/soldier.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 13, + "fields": { + "character_id": "mayor", + "character_name": "Mayor", + "ability": "If only 3 players live & no execution occurs, your team wins. If you die at night, another player might die instead.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/mayor.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 14, + "fields": { + "character_id": "butler", + "character_name": "Butler", + "ability": "Each night, choose a player (not yourself): tomorrow, you may only vote if they are voting too.", + "first_night_reminder": "The Butler points to a player. Mark that player as 'Master'.", + "other_night_reminder": "The Butler points to a player. Mark that player as 'Master'.", + "global_reminders": null, + "reminders": "Master", + "character_type": "Outsider", + "edition": 0, + "first_night_position": 39, + "other_night_position": 67, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/butler.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 15, + "fields": { + "character_id": "drunk", + "character_name": "Drunk", + "ability": "You do not know you are the Drunk. You think you are a Townsfolk character, but you are not.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": "Drunk", + "reminders": "", + "character_type": "Outsider", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/drunk.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 16, + "fields": { + "character_id": "recluse", + "character_name": "Recluse", + "ability": "You might register as evil & as a Minion or Demon, even if dead.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Outsider", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/recluse.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 17, + "fields": { + "character_id": "saint", + "character_name": "Saint", + "ability": "If you die by execution, your team loses.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Outsider", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/saint.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 18, + "fields": { + "character_id": "poisoner", + "character_name": "Poisoner", + "ability": "Each night, choose a player: they are poisoned tonight and tomorrow day.", + "first_night_reminder": "The Poisoner points to a player. That player is poisoned.", + "other_night_reminder": "The previously poisoned player is no longer poisoned. The Poisoner points to a player. That player is poisoned.", + "global_reminders": null, + "reminders": "Poisoned", + "character_type": "Minion", + "edition": 0, + "first_night_position": 17, + "other_night_position": 7, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/poisoner.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 19, + "fields": { + "character_id": "spy", + "character_name": "Spy", + "ability": "Each night, you see the Grimoire. You might register as good & as a Townsfolk or Outsider, even if dead.", + "first_night_reminder": "Show the Grimoire to the Spy for as long as they need.", + "other_night_reminder": "Show the Grimoire to the Spy for as long as they need.", + "global_reminders": null, + "reminders": "", + "character_type": "Minion", + "edition": 0, + "first_night_position": 49, + "other_night_position": 68, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/spy.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 20, + "fields": { + "character_id": "scarletwoman", + "character_name": "Scarlet Woman", + "ability": "If there are 5 or more players alive & the Demon dies, you become the Demon. (Travellers don’t count)", + "first_night_reminder": "", + "other_night_reminder": "If the Scarlet Woman became the Demon today: Show the 'You are' card, then the demon token.", + "global_reminders": null, + "reminders": "Demon", + "character_type": "Minion", + "edition": 0, + "first_night_position": 0, + "other_night_position": 19, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/scarletwoman.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 21, + "fields": { + "character_id": "baron", + "character_name": "Baron", + "ability": "There are extra Outsiders in play. [+2 Outsiders]", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Minion", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/baron.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 22, + "fields": { + "character_id": "imp", + "character_name": "Imp", + "ability": "Each night*, choose a player: they die. If you kill yourself this way, a Minion becomes the Imp.", + "first_night_reminder": "", + "other_night_reminder": "The Imp points to a player. That player dies. If the Imp chose themselves: Replace the character of 1 alive minion with a spare Imp token. Show the 'You are' card, then the Imp token.", + "global_reminders": null, + "reminders": "Dead", + "character_type": "Demon", + "edition": 0, + "first_night_position": 0, + "other_night_position": 24, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/imp.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 23, + "fields": { + "character_id": "bureaucrat", + "character_name": "Bureaucrat", + "ability": "Each night, choose a player (not yourself): their vote counts as 3 votes tomorrow.", + "first_night_reminder": "The Bureaucrat points to a player. Put the Bureaucrat's '3 votes' reminder by the chosen player's character token.", + "other_night_reminder": "The Bureaucrat points to a player. Put the Bureaucrat's '3 votes' reminder by the chosen player's character token.", + "global_reminders": null, + "reminders": "3 votes", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 1, + "other_night_position": 1, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/bureaucrat.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 24, + "fields": { + "character_id": "thief", + "character_name": "Thief", + "ability": "Each night, choose a player (not yourself): their vote counts negatively tomorrow.", + "first_night_reminder": "The Thief points to a player. Put the Thief's 'Negative vote' reminder by the chosen player's character token.", + "other_night_reminder": "The Thief points to a player. Put the Thief's 'Negative vote' reminder by the chosen player's character token.", + "global_reminders": null, + "reminders": "Negative vote", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 1, + "other_night_position": 1, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/thief.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 25, + "fields": { + "character_id": "gunslinger", + "character_name": "Gunslinger", + "ability": "Each day, after the 1st vote has been tallied, you may choose a player that voted: they die.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/gunslinger.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 26, + "fields": { + "character_id": "scapegoat", + "character_name": "Scapegoat", + "ability": "If a player of your alignment is executed, you might be executed instead.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/scapegoat.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 27, + "fields": { + "character_id": "beggar", + "character_name": "Beggar", + "ability": "You must use a vote token to vote. Dead players may choose to give you theirs. If so, you learn their alignment. You are sober & healthy.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/beggar.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 28, + "fields": { + "character_id": "grandmother", + "character_name": "Grandmother", + "ability": "You start knowing a good player & their character. If the Demon kills them, you die too.", + "first_night_reminder": "Show the marked character token. Point to the marked player.", + "other_night_reminder": "If the Grandmother’s grandchild was killed by the Demon tonight: The Grandmother dies.", + "global_reminders": null, + "reminders": "Grandchild", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 40, + "other_night_position": 51, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/grandmother.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 29, + "fields": { + "character_id": "sailor", + "character_name": "Sailor", + "ability": "Each night, choose an alive player: either you or they are drunk until dusk. You can't die.", + "first_night_reminder": "The Sailor points to a living player. Either the Sailor, or the chosen player, is drunk.", + "other_night_reminder": "The previously drunk player is no longer drunk. The Sailor points to a living player. Either the Sailor, or the chosen player, is drunk.", + "global_reminders": null, + "reminders": "Drunk", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 11, + "other_night_position": 4, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/sailor.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 30, + "fields": { + "character_id": "chambermaid", + "character_name": "Chambermaid", + "ability": "Each night, choose 2 alive players (not yourself): you learn how many woke tonight due to their ability.", + "first_night_reminder": "The Chambermaid points to two players. Show the number signal (0, 1, 2, …) for how many of those players wake tonight for their ability.", + "other_night_reminder": "The Chambermaid points to two players. Show the number signal (0, 1, 2, …) for how many of those players wake tonight for their ability.", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 51, + "other_night_position": 70, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/chambermaid.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 31, + "fields": { + "character_id": "exorcist", + "character_name": "Exorcist", + "ability": "Each night*, choose a player (different to last night): the Demon, if chosen, learns who you are then doesn't wake tonight.", + "first_night_reminder": "", + "other_night_reminder": "The Exorcist points to a player, different from the previous night. If that player is the Demon: Wake the Demon. Show the Exorcist token. Point to the Exorcist. The Demon does not act tonight.", + "global_reminders": null, + "reminders": "Chosen", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 21, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/exorcist.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 32, + "fields": { + "character_id": "innkeeper", + "character_name": "Innkeeper", + "ability": "Each night*, choose 2 players: they can't die tonight, but 1 is drunk until dusk.", + "first_night_reminder": "", + "other_night_reminder": "The previously protected and drunk players lose those markers. The Innkeeper points to two players. Those players are protected. One is drunk.", + "global_reminders": null, + "reminders": "Protected,Drunk", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 9, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/innkeeper.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 33, + "fields": { + "character_id": "gambler", + "character_name": "Gambler", + "ability": "Each night*, choose a player & guess their character: if you guess wrong, you die.", + "first_night_reminder": "", + "other_night_reminder": "The Gambler points to a player, and a character on their sheet. If incorrect, the Gambler dies.", + "global_reminders": null, + "reminders": "Dead", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 10, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/gambler.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 34, + "fields": { + "character_id": "gossip", + "character_name": "Gossip", + "ability": "Each day, you may make a public statement. Tonight, if it was true, a player dies.", + "first_night_reminder": "", + "other_night_reminder": "If the Gossip’s public statement was true: Choose a player not protected from dying tonight. That player dies.", + "global_reminders": null, + "reminders": "Dead", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 38, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/gossip.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 35, + "fields": { + "character_id": "courtier", + "character_name": "Courtier", + "ability": "Once per game, at night, choose a character: they are drunk for 3 nights & 3 days.", + "first_night_reminder": "The Courtier either shows a 'no' head signal, or points to a character on the sheet. If the Courtier used their ability: If that character is in play, that player is drunk.", + "other_night_reminder": "Reduce the remaining number of days the marked player is poisoned. If the Courtier has not yet used their ability: The Courtier either shows a 'no' head signal, or points to a character on the sheet. If the Courtier used their ability: If that character is in play, that player is drunk.", + "global_reminders": null, + "reminders": "Drunk 3,Drunk 2,Drunk 1,No ability", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 19, + "other_night_position": 8, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/courtier.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 36, + "fields": { + "character_id": "professor", + "character_name": "Professor", + "ability": "Once per game, at night*, choose a dead player: if they are a Townsfolk, they are resurrected.", + "first_night_reminder": "", + "other_night_reminder": "If the Professor has not used their ability: The Professor either shakes their head no, or points to a player. If that player is a Townsfolk, they are now alive.", + "global_reminders": null, + "reminders": "Alive,No ability", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 43, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/professor.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 37, + "fields": { + "character_id": "minstrel", + "character_name": "Minstrel", + "ability": "When a Minion dies by execution, all other players (except Travellers) are drunk until dusk tomorrow.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Everyone drunk", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/minstrel.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 38, + "fields": { + "character_id": "tealady", + "character_name": "Tea Lady", + "ability": "If both your alive neighbours are good, they can't die.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Can not die", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/tealady.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 39, + "fields": { + "character_id": "pacifist", + "character_name": "Pacifist", + "ability": "Executed good players might not die.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/pacifist.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 40, + "fields": { + "character_id": "fool", + "character_name": "Fool", + "ability": "The first time you die, you don't.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "No ability", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/fool.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 41, + "fields": { + "character_id": "tinker", + "character_name": "Tinker", + "ability": "You might die at any time.", + "first_night_reminder": "", + "other_night_reminder": "The Tinker might die.", + "global_reminders": null, + "reminders": "Dead", + "character_type": "Outsider", + "edition": 0, + "first_night_position": 0, + "other_night_position": 49, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/tinker.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 42, + "fields": { + "character_id": "moonchild", + "character_name": "Moonchild", + "ability": "When you learn that you died, publicly choose 1 alive player. Tonight, if it was a good player, they die.", + "first_night_reminder": "", + "other_night_reminder": "If the Moonchild used their ability to target a player today: If that player is good, they die.", + "global_reminders": null, + "reminders": "Dead", + "character_type": "Outsider", + "edition": 0, + "first_night_position": 0, + "other_night_position": 50, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/moonchild.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 43, + "fields": { + "character_id": "goon", + "character_name": "Goon", + "ability": "Each night, the 1st player to choose you with their ability is drunk until dusk. You become their alignment.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Drunk", + "character_type": "Outsider", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/goon.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 44, + "fields": { + "character_id": "lunatic", + "character_name": "Lunatic", + "ability": "You think you are a Demon, but you are not. The Demon knows who you are & who you choose at night.", + "first_night_reminder": "If 7 or more players: Show the Lunatic a number of arbitrary 'Minions', players equal to the number of Minions in play. Show 3 character tokens of arbitrary good characters. If the token received by the Lunatic is a Demon that would wake tonight: Allow the Lunatic to do the Demon actions. Place their 'attack' markers. Wake the Demon. Show the Demon’s real character token. Show them the Lunatic player. If the Lunatic attacked players: Show the real demon each marked player. Remove any Lunatic 'attack' markers.", + "other_night_reminder": "Allow the Lunatic to do the actions of the Demon. Place their 'attack' markers. If the Lunatic selected players: Wake the Demon. Show the 'attack' marker, then point to each marked player. Remove any Lunatic 'attack' markers.", + "global_reminders": null, + "reminders": "Attack 1,Attack 2,Attack 3", + "character_type": "Outsider", + "edition": 0, + "first_night_position": 8, + "other_night_position": 20, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/lunatic.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 45, + "fields": { + "character_id": "godfather", + "character_name": "Godfather", + "ability": "You start knowing which Outsiders are in play. If 1 died today, choose a player tonight: they die. [−1 or +1 Outsider]", + "first_night_reminder": "Show each of the Outsider tokens in play.", + "other_night_reminder": "If an Outsider died today: The Godfather points to a player. That player dies.", + "global_reminders": null, + "reminders": "Died today,Dead", + "character_type": "Minion", + "edition": 0, + "first_night_position": 21, + "other_night_position": 37, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/godfather.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 46, + "fields": { + "character_id": "devilsadvocate", + "character_name": "Devil's Advocate", + "ability": "Each night, choose a living player (different to last night): if executed tomorrow, they don't die.", + "first_night_reminder": "The Devil’s Advocate points to a living player. That player survives execution tomorrow.", + "other_night_reminder": "The Devil’s Advocate points to a living player, different from the previous night. That player survives execution tomorrow.", + "global_reminders": null, + "reminders": "Survives execution", + "character_type": "Minion", + "edition": 0, + "first_night_position": 22, + "other_night_position": 13, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/devilsadvocate.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 47, + "fields": { + "character_id": "assassin", + "character_name": "Assassin", + "ability": "Once per game, at night*, choose a player: they die, even if for some reason they could not.", + "first_night_reminder": "", + "other_night_reminder": "If the Assassin has not yet used their ability: The Assassin either shows the 'no' head signal, or points to a player. That player dies.", + "global_reminders": null, + "reminders": "Dead,No ability", + "character_type": "Minion", + "edition": 0, + "first_night_position": 0, + "other_night_position": 36, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/assassin.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 48, + "fields": { + "character_id": "mastermind", + "character_name": "Mastermind", + "ability": "If the Demon dies by execution (ending the game), play for 1 more day. If a player is then executed, their team loses.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Minion", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/mastermind.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 49, + "fields": { + "character_id": "zombuul", + "character_name": "Zombuul", + "ability": "Each night*, if no-one died today, choose a player: they die. The 1st time you die, you live but register as dead.", + "first_night_reminder": "", + "other_night_reminder": "If no-one died during the day: The Zombuul points to a player. That player dies.", + "global_reminders": null, + "reminders": "Died today,Dead", + "character_type": "Demon", + "edition": 0, + "first_night_position": 0, + "other_night_position": 25, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/zombuul.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 50, + "fields": { + "character_id": "pukka", + "character_name": "Pukka", + "ability": "Each night, choose a player: they are poisoned. The previously poisoned player dies then becomes healthy.", + "first_night_reminder": "The Pukka points to a player. That player is poisoned.", + "other_night_reminder": "The Pukka points to a player. That player is poisoned. The previously poisoned player dies. ", + "global_reminders": null, + "reminders": "Poisoned,Dead", + "character_type": "Demon", + "edition": 0, + "first_night_position": 28, + "other_night_position": 26, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/pukka.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 51, + "fields": { + "character_id": "shabaloth", + "character_name": "Shabaloth", + "ability": "Each night*, choose 2 players: they die. A dead player you chose last night might be regurgitated.", + "first_night_reminder": "", + "other_night_reminder": "One player that the Shabaloth chose the previous night might be resurrected. The Shabaloth points to two players. Those players die.", + "global_reminders": null, + "reminders": "Dead,Alive", + "character_type": "Demon", + "edition": 0, + "first_night_position": 0, + "other_night_position": 27, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/shabaloth.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 52, + "fields": { + "character_id": "po", + "character_name": "Po", + "ability": "Each night*, you may choose a player: they die. If your last choice was no-one, choose 3 players tonight.", + "first_night_reminder": "", + "other_night_reminder": "If the Po chose no-one the previous night: The Po points to three players. Otherwise: The Po either shows the 'no' head signal , or points to a player. Chosen players die", + "global_reminders": null, + "reminders": "Dead,3 attacks", + "character_type": "Demon", + "edition": 0, + "first_night_position": 0, + "other_night_position": 28, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/po.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 53, + "fields": { + "character_id": "apprentice", + "character_name": "Apprentice", + "ability": "On your 1st night, you gain a Townsfolk ability (if good), or a Minion ability (if evil).", + "first_night_reminder": "Show the Apprentice the 'You are' card, then a Townsfolk or Minion token. In the Grimoire, replace the Apprentice token with that character token, and put the Apprentice's 'Is the Apprentice' reminder by that character token.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Is the Apprentice", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 1, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/apprentice.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 54, + "fields": { + "character_id": "matron", + "character_name": "Matron", + "ability": "Each day, you may choose up to 3 sets of 2 players to swap seats. Players may not leave their seats to talk in private.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/matron.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 55, + "fields": { + "character_id": "judge", + "character_name": "Judge", + "ability": "Once per game, if another player nominated, you may choose to force the current execution to pass or fail.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "No ability", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/judge.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 56, + "fields": { + "character_id": "bishop", + "character_name": "Bishop", + "ability": "Only the Storyteller can nominate. At least 1 opposite player must be nominated each day.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Nominate good,Nominate evil", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/bishop.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 57, + "fields": { + "character_id": "voudon", + "character_name": "Voudon", + "ability": "Only you and the dead can vote. They don't need a vote token to do so. A 50% majority is not required.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/voudon.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 58, + "fields": { + "character_id": "clockmaker", + "character_name": "Clockmaker", + "ability": "You start knowing how many steps from the Demon to its nearest Minion.", + "first_night_reminder": "Show the hand signal for the number (1, 2, 3, etc.) of places from Demon to closest Minion.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 41, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/clockmaker.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 59, + "fields": { + "character_id": "dreamer", + "character_name": "Dreamer", + "ability": "Each night, choose a player (not yourself or Travellers): you learn 1 good and 1 evil character, 1 of which is correct.", + "first_night_reminder": "The Dreamer points to a player. Show 1 good and 1 evil character token; one of these is correct.", + "other_night_reminder": "The Dreamer points to a player. Show 1 good and 1 evil character token; one of these is correct.", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 42, + "other_night_position": 56, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/dreamer.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 60, + "fields": { + "character_id": "snakecharmer", + "character_name": "Snake Charmer", + "ability": "Each night, choose an alive player: a chosen Demon swaps characters & alignments with you & is then poisoned.", + "first_night_reminder": "The Snake Charmer points to a player. If that player is the Demon: swap the Demon and Snake Charmer character and alignments. Wake each player to inform them of their new role and alignment. The new Snake Charmer is poisoned.", + "other_night_reminder": "The Snake Charmer points to a player. If that player is the Demon: swap the Demon and Snake Charmer character and alignments. Wake each player to inform them of their new role and alignment. The new Snake Charmer is poisoned.", + "global_reminders": null, + "reminders": "Poisoned", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 20, + "other_night_position": 11, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/snakecharmer.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 61, + "fields": { + "character_id": "mathematician", + "character_name": "Mathematician", + "ability": "Each night, you learn how many players’ abilities worked abnormally (since dawn) due to another character's ability.", + "first_night_reminder": "Show the hand signal for the number (0, 1, 2, etc.) of players whose ability malfunctioned due to other abilities.", + "other_night_reminder": "Show the hand signal for the number (0, 1, 2, etc.) of players whose ability malfunctioned due to other abilities.", + "global_reminders": null, + "reminders": "Abnormal", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 52, + "other_night_position": 71, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/mathematician.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 62, + "fields": { + "character_id": "flowergirl", + "character_name": "Flowergirl", + "ability": "Each night*, you learn if a Demon voted today.", + "first_night_reminder": "", + "other_night_reminder": "Nod 'yes' or shake head 'no' for whether the Demon voted today. Place the 'Demon not voted' marker (remove 'Demon voted', if any).", + "global_reminders": null, + "reminders": "Demon voted,Demon not voted", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 57, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/flowergirl.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 63, + "fields": { + "character_id": "towncrier", + "character_name": "Town Crier", + "ability": "Each night*, you learn if a Minion nominated today.", + "first_night_reminder": "", + "other_night_reminder": "Nod 'yes' or shake head 'no' for whether a Minion nominated today. Place the 'Minion not nominated' marker (remove 'Minion nominated', if any).", + "global_reminders": null, + "reminders": "Minions not nominated,Minion nominated", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 58, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/towncrier.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 64, + "fields": { + "character_id": "oracle", + "character_name": "Oracle", + "ability": "Each night*, you learn how many dead players are evil.", + "first_night_reminder": "", + "other_night_reminder": "Show the hand signal for the number (0, 1, 2, etc.) of dead evil players.", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 59, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/oracle.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 65, + "fields": { + "character_id": "savant", + "character_name": "Savant", + "ability": "Each day, you may visit the Storyteller to learn 2 things in private: 1 is true & 1 is false.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/savant.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 66, + "fields": { + "character_id": "seamstress", + "character_name": "Seamstress", + "ability": "Once per game, at night, choose 2 players (not yourself): you learn if they are the same alignment.", + "first_night_reminder": "The Seamstress either shows a 'no' head signal, or points to two other players. If the Seamstress chose players , nod 'yes' or shake 'no' for whether they are of same alignment.", + "other_night_reminder": "If the Seamstress has not yet used their ability: the Seamstress either shows a 'no' head signal, or points to two other players. If the Seamstress chose players , nod 'yes' or shake 'no' for whether they are of same alignment.", + "global_reminders": null, + "reminders": "No ability", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 43, + "other_night_position": 60, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/seamstress.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 67, + "fields": { + "character_id": "philosopher", + "character_name": "Philosopher", + "ability": "Once per game, at night, choose a good character: gain that ability. If this character is in play, they are drunk.", + "first_night_reminder": "The Philosopher either shows a 'no' head signal, or points to a good character on their sheet. If they chose a character: Swap the out-of-play character token with the Philosopher token and add the 'Is the Philosopher' reminder. If the character is in play, place the drunk marker by that player.", + "other_night_reminder": "If the Philosopher has not used their ability: the Philosopher either shows a 'no' head signal, or points to a good character on their sheet. If they chose a character: Swap the out-of-play character token with the Philosopher token and add the 'Is the Philosopher' reminder. If the character is in play, place the drunk marker by that player.", + "global_reminders": null, + "reminders": "Drunk,Is the Philosopher", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 2, + "other_night_position": 2, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/philosopher.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 68, + "fields": { + "character_id": "artist", + "character_name": "Artist", + "ability": "Once per game, during the day, privately ask the Storyteller any yes/no question.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "No ability", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/artist.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 69, + "fields": { + "character_id": "juggler", + "character_name": "Juggler", + "ability": "On your 1st day, publicly guess up to 5 players' characters. That night, you learn how many you got correct.", + "first_night_reminder": "", + "other_night_reminder": "If today was the Juggler’s first day: Show the hand signal for the number (0, 1, 2, etc.) of 'Correct' markers. Remove markers.", + "global_reminders": null, + "reminders": "Correct", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 61, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/juggler.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 70, + "fields": { + "character_id": "sage", + "character_name": "Sage", + "ability": "If the Demon kills you, you learn that it is 1 of 2 players.", + "first_night_reminder": "", + "other_night_reminder": "If the Sage was killed by a Demon: Point to two players, one of which is that Demon.", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 0, + "first_night_position": 0, + "other_night_position": 42, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/sage.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 71, + "fields": { + "character_id": "mutant", + "character_name": "Mutant", + "ability": "If you are “mad” about being an Outsider, you might be executed.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Outsider", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/mutant.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 72, + "fields": { + "character_id": "sweetheart", + "character_name": "Sweetheart", + "ability": "When you die, 1 player is drunk from now on.", + "first_night_reminder": "", + "other_night_reminder": "Choose a player that is drunk.", + "global_reminders": null, + "reminders": "Drunk", + "character_type": "Outsider", + "edition": 0, + "first_night_position": 0, + "other_night_position": 41, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/sweetheart.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 73, + "fields": { + "character_id": "barber", + "character_name": "Barber", + "ability": "If you died today or tonight, the Demon may choose 2 players (not another Demon) to swap characters.", + "first_night_reminder": "", + "other_night_reminder": "If the Barber died today: Wake the Demon. Show the 'This character selected you' card, then Barber token. The Demon either shows a 'no' head signal, or points to 2 players. If they chose players: Swap the character tokens. Wake each player. Show 'You are', then their new character token.", + "global_reminders": null, + "reminders": "Haircuts tonight", + "character_type": "Outsider", + "edition": 0, + "first_night_position": 0, + "other_night_position": 40, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/barber.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 74, + "fields": { + "character_id": "klutz", + "character_name": "Klutz", + "ability": "When you learn that you died, publicly choose 1 alive player: if they are evil, your team loses.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Outsider", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/klutz.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 75, + "fields": { + "character_id": "eviltwin", + "character_name": "Evil Twin", + "ability": "You & an opposing player know each other. If the good player is executed, evil wins. Good can't win if you both live.", + "first_night_reminder": "Wake the Evil Twin and their twin. Confirm that they have acknowledged each other. Point to the Evil Twin. Show their Evil Twin token to the twin player. Point to the twin. Show their character token to the Evil Twin player.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Twin", + "character_type": "Minion", + "edition": 0, + "first_night_position": 23, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/eviltwin.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 76, + "fields": { + "character_id": "witch", + "character_name": "Witch", + "ability": "Each night, choose a player: if they nominate tomorrow, they die. If just 3 players live, you lose this ability.", + "first_night_reminder": "The Witch points to a player. If that player nominates tomorrow they die immediately.", + "other_night_reminder": "If there are 4 or more players alive: The Witch points to a player. If that player nominates tomorrow they die immediately.", + "global_reminders": null, + "reminders": "Cursed", + "character_type": "Minion", + "edition": 0, + "first_night_position": 24, + "other_night_position": 14, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/witch.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 77, + "fields": { + "character_id": "cerenovus", + "character_name": "Cerenovus", + "ability": "Each night, choose a player & a good character: they are “mad” they are this character tomorrow, or might be executed.", + "first_night_reminder": "The Cerenovus points to a player, then to a character on their sheet. Wake that player. Show the 'This character selected you' card, then the Cerenovus token. Show the selected character token. If the player is not mad about being that character tomorrow, they can be executed.", + "other_night_reminder": "The Cerenovus points to a player, then to a character on their sheet. Wake that player. Show the 'This character selected you' card, then the Cerenovus token. Show the selected character token. If the player is not mad about being that character tomorrow, they can be executed.", + "global_reminders": null, + "reminders": "Mad", + "character_type": "Minion", + "edition": 0, + "first_night_position": 25, + "other_night_position": 15, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/cerenovus.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 78, + "fields": { + "character_id": "pithag", + "character_name": "Pit-Hag", + "ability": "Each night*, choose a player & a character they become (if not-in-play). If a Demon is made, deaths tonight are arbitrary.", + "first_night_reminder": "", + "other_night_reminder": "The Pit-Hag points to a player and a character on the sheet. If this character is not in play, wake that player and show them the 'You are' card and the relevant character token. If the character is in play, nothing happens.", + "global_reminders": null, + "reminders": "", + "character_type": "Minion", + "edition": 0, + "first_night_position": 0, + "other_night_position": 16, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/pithag.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 79, + "fields": { + "character_id": "fanggu", + "character_name": "Fang Gu", + "ability": "Each night*, choose a player: they die. The 1st Outsider this kills becomes an evil Fang Gu & you die instead. [+1 Outsider]", + "first_night_reminder": "", + "other_night_reminder": "The Fang Gu points to a player. That player dies. Or, if that player was an Outsider and there are no other Fang Gu in play: The Fang Gu dies instead of the chosen player. The chosen player is now an evil Fang Gu. Wake the new Fang Gu. Show the 'You are' card, then the Fang Gu token. Show the 'You are' card, then the thumb-down 'evil' hand sign.", + "global_reminders": null, + "reminders": "Dead,Once", + "character_type": "Demon", + "edition": 0, + "first_night_position": 0, + "other_night_position": 29, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/fanggu.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 80, + "fields": { + "character_id": "vigormortis", + "character_name": "Vigormortis", + "ability": "Each night*, choose a player: they die. Minions you kill keep their ability & poison 1 Townsfolk neighbour. [−1 Outsider]", + "first_night_reminder": "", + "other_night_reminder": "The Vigormortis points to a player. That player dies. If a Minion, they keep their ability and one of their Townsfolk neighbours is poisoned.", + "global_reminders": null, + "reminders": "Dead,Has ability,Poisoned", + "character_type": "Demon", + "edition": 0, + "first_night_position": 0, + "other_night_position": 32, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/vigormortis.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 81, + "fields": { + "character_id": "nodashii", + "character_name": "No Dashii", + "ability": "Each night*, choose a player: they die. Your 2 Townsfolk neighbours are poisoned.", + "first_night_reminder": "", + "other_night_reminder": "The No Dashii points to a player. That player dies.", + "global_reminders": null, + "reminders": "Dead,Poisoned", + "character_type": "Demon", + "edition": 0, + "first_night_position": 0, + "other_night_position": 30, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/nodashii.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 82, + "fields": { + "character_id": "vortox", + "character_name": "Vortox", + "ability": "Each night*, choose a player: they die. Townsfolk abilities yield false info. Each day, if no-one is executed, evil wins.", + "first_night_reminder": "", + "other_night_reminder": "The Vortox points to a player. That player dies.", + "global_reminders": null, + "reminders": "Dead", + "character_type": "Demon", + "edition": 0, + "first_night_position": 0, + "other_night_position": 31, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/vortox.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 83, + "fields": { + "character_id": "barista", + "character_name": "Barista", + "ability": "Each night, until dusk, 1) a player becomes sober, healthy and gets true info, or 2) their ability works twice. They learn which.", + "first_night_reminder": "Choose a player, wake them and tell them which Barista power is affecting them. Treat them accordingly (sober/healthy/true info or activate their ability twice).", + "other_night_reminder": "Choose a player, wake them and tell them which Barista power is affecting them. Treat them accordingly (sober/healthy/true info or activate their ability twice).", + "global_reminders": null, + "reminders": "Sober & Healthy,Ability twice", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 1, + "other_night_position": 1, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/barista.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 84, + "fields": { + "character_id": "harlot", + "character_name": "Harlot", + "ability": "Each night*, choose a living player: if they agree, you learn their character, but you both might die.", + "first_night_reminder": "", + "other_night_reminder": "The Harlot points at any player. Then, put the Harlot to sleep. Wake the chosen player, show them the 'This character selected you' token, then the Harlot token. That player either nods their head yes or shakes their head no. If they nodded their head yes, wake the Harlot and show them the chosen player's character token. Then, you may decide that both players die.", + "global_reminders": null, + "reminders": "Dead", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 0, + "other_night_position": 1, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/harlot.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 85, + "fields": { + "character_id": "butcher", + "character_name": "Butcher", + "ability": "Each day, after the 1st execution, you may nominate again.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/butcher.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 86, + "fields": { + "character_id": "bonecollector", + "character_name": "Bone Collector", + "ability": "Once per game, at night, choose a dead player: they regain their ability until dusk.", + "first_night_reminder": "", + "other_night_reminder": "The Bone Collector either shakes their head no or points at any dead player. If they pointed at any dead player, put the Bone Collector's 'Has Ability' reminder by the chosen player's character token. (They may need to be woken tonight to use it.)", + "global_reminders": null, + "reminders": "No ability,Has ability", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 0, + "other_night_position": 1, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/bonecollector.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 87, + "fields": { + "character_id": "deviant", + "character_name": "Deviant", + "ability": "If you were funny today, you cannot die by exile.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Traveller", + "edition": 0, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/deviant.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 88, + "fields": { + "character_id": "noble", + "character_name": "Noble", + "ability": "You start knowing 3 players, 1 and only 1 of which is evil.", + "first_night_reminder": "Point to 3 players including one evil player, in no particular order.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Seen", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 44, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/noble.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 89, + "fields": { + "character_id": "bountyhunter", + "character_name": "Bounty Hunter", + "ability": "You start knowing 1 evil player. If the player you know dies, you learn another evil player tonight. [1 Townsfolk is evil]", + "first_night_reminder": "Point to 1 evil player. Wake the townsfolk who is evil and show them the 'You are' card and the thumbs down evil sign.", + "other_night_reminder": "If the known evil player has died, point to another evil player.", + "global_reminders": null, + "reminders": "Known", + "character_type": "Townsfolk", + "edition": 2, + "first_night_position": 46, + "other_night_position": 64, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/bountyhunter.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 90, + "fields": { + "character_id": "pixie", + "character_name": "Pixie", + "ability": "You start knowing 1 in-play Townsfolk. If you were mad that you were this character, you gain their ability when they die.", + "first_night_reminder": "Show the Pixie 1 in-play Townsfolk character token.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Mad,Has ability", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 29, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/pixie.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 91, + "fields": { + "character_id": "general", + "character_name": "General", + "ability": "Each night, you learn which alignment the Storyteller believes is winning: good, evil, or neither.", + "first_night_reminder": "Show the General thumbs up for good winning, thumbs down for evil winning or thumb to the side for neither.", + "other_night_reminder": "Show the General thumbs up for good winning, thumbs down for evil winning or thumb to the side for neither.", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 50, + "other_night_position": 69, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/general.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 92, + "fields": { + "character_id": "preacher", + "character_name": "Preacher", + "ability": "Each night, choose a player: a Minion, if chosen, learns this. All chosen Minions have no ability.", + "first_night_reminder": "The Preacher chooses a player. If a Minion is chosen, wake the Minion and show the 'This character selected you' card and then the Preacher token.", + "other_night_reminder": "The Preacher chooses a player. If a Minion is chosen, wake the Minion and show the 'This character selected you' card and then the Preacher token.", + "global_reminders": null, + "reminders": "At a sermon", + "character_type": "Townsfolk", + "edition": 2, + "first_night_position": 14, + "other_night_position": 6, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/preacher.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 93, + "fields": { + "character_id": "king", + "character_name": "King", + "ability": "Each night, if the dead outnumber the living, you learn 1 alive character. The Demon knows who you are.", + "first_night_reminder": "Wake the Demon, show them the 'This character selected you' card, show the King token and point to the King player.", + "other_night_reminder": "If there are more dead than living, show the King a character token of a living player.", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 10, + "other_night_position": 63, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/king.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 94, + "fields": { + "character_id": "balloonist", + "character_name": "Balloonist", + "ability": "Each night, you learn 1 player of each character type, until there are no more types to learn. [+1 Outsider]", + "first_night_reminder": "Choose a character type. Point to a player whose character is of that type. Place the Balloonist's Seen reminder next to that character.", + "other_night_reminder": "Choose a character type that does not yet have a Seen reminder next to a character of that type. Point to a player whose character is of that type, if there are any. Place the Balloonist's Seen reminder next to that character.", + "global_reminders": null, + "reminders": "Seen Townsfolk,Seen Outsider,Seen Minion,Seen Demon,Seen Traveller", + "character_type": "Townsfolk", + "edition": 2, + "first_night_position": 45, + "other_night_position": 62, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/balloonist.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 95, + "fields": { + "character_id": "cultleader", + "character_name": "Cult Leader", + "ability": "Each night, you become the alignment of an alive neighbour. If all good players choose to join your cult, your team wins.", + "first_night_reminder": "If the cult leader changed alignment, show them the thumbs up good signal of the thumbs down evil signal accordingly.", + "other_night_reminder": "If the cult leader changed alignment, show them the thumbs up good signal of the thumbs down evil signal accordingly.", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 2, + "first_night_position": 48, + "other_night_position": 66, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/cultleader.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 96, + "fields": { + "character_id": "lycanthrope", + "character_name": "Lycanthrope", + "ability": "Each night*, choose a living player: if good, they die, but they are the only player that can die tonight.", + "first_night_reminder": "", + "other_night_reminder": "The Lycanthrope points to a living player: if good, they die and no one else can die tonight.", + "global_reminders": null, + "reminders": "Dead", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 0, + "other_night_position": 22, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/lycanthrope.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 97, + "fields": { + "character_id": "amnesiac", + "character_name": "Amnesiac", + "ability": "You do not know what your ability is. Each day, privately guess what it is: you learn how accurate you are.", + "first_night_reminder": "Decide the Amnesiac's entire ability. If the Amnesiac's ability causes them to wake tonight: Wake the Amnesiac and run their ability.", + "other_night_reminder": "If the Amnesiac's ability causes them to wake tonight: Wake the Amnesiac and run their ability.", + "global_reminders": null, + "reminders": "?", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 32, + "other_night_position": 47, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/amnesiac.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 98, + "fields": { + "character_id": "nightwatchman", + "character_name": "Nightwatchman", + "ability": "Once per game, at night, choose a player: they learn who you are.", + "first_night_reminder": "The Nightwatchman may point to a player. Wake that player, show the 'This character selected you' card and the Nightwatchman token, then point to the Nightwatchman player.", + "other_night_reminder": "The Nightwatchman may point to a player. Wake that player, show the 'This character selected you' card and the Nightwatchman token, then point to the Nightwatchman player.", + "global_reminders": null, + "reminders": "No ability", + "character_type": "Townsfolk", + "edition": 2, + "first_night_position": 47, + "other_night_position": 65, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/nightwatchman.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 99, + "fields": { + "character_id": "engineer", + "character_name": "Engineer", + "ability": "Once per game, at night, choose which Minions or which Demon is in play.", + "first_night_reminder": "The Engineer shows a 'no' head signal, or points to a Demon or points to the relevant number of Minions. If the Engineer chose characters, replace the Demon or Minions with the choices, then wake the relevant players and show them the You are card and the relevant character tokens.", + "other_night_reminder": "The Engineer shows a 'no' head signal, or points to a Demon or points to the relevant number of Minions. If the Engineer chose characters, replace the Demon or Minions with the choices, then wake the relevant players and show them the 'You are' card and the relevant character tokens.", + "global_reminders": null, + "reminders": "No ability", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 13, + "other_night_position": 5, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/engineer.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 100, + "fields": { + "character_id": "fisherman", + "character_name": "Fisherman", + "ability": "Once per game, during the day, visit the Storyteller for some advice to help you win.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "No ability", + "character_type": "Townsfolk", + "edition": 2, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/fisherman.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 101, + "fields": { + "character_id": "huntsman", + "character_name": "Huntsman", + "ability": "Once per game, at night, choose a living player: the Damsel, if chosen, becomes a not-in-play Townsfolk. [+the Damsel]", + "first_night_reminder": "The Huntsman shakes their head 'no' or points to a player. If they point to the Damsel, wake that player, show the 'You are' card and a not-in-play character token.", + "other_night_reminder": "The Huntsman shakes their head 'no' or points to a player. If they point to the Damsel, wake that player, show the 'You are' card and a not-in-play character token.", + "global_reminders": null, + "reminders": "No ability", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 30, + "other_night_position": 45, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/huntsman.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 102, + "fields": { + "character_id": "alchemist", + "character_name": "Alchemist", + "ability": "You have a not-in-play Minion ability.", + "first_night_reminder": "Show the Alchemist a not-in-play Minion token", + "other_night_reminder": "", + "global_reminders": "Is the Alchemist", + "reminders": "", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 3, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/alchemist.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 103, + "fields": { + "character_id": "farmer", + "character_name": "Farmer", + "ability": "If you die at night, an alive good player becomes a Farmer.", + "first_night_reminder": "", + "other_night_reminder": "If a Farmer died tonight, choose another good player and make them the Farmer. Wake this player, show them the 'You are' card and the Farmer character token.", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 0, + "other_night_position": 48, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/farmer.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 104, + "fields": { + "character_id": "magician", + "character_name": "Magician", + "ability": "The Demon thinks you are a Minion. Minions think you are a Demon.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 5, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/magician.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 105, + "fields": { + "character_id": "choirboy", + "character_name": "Choirboy", + "ability": "If the Demon kills the King, you learn which player is the Demon. [+ the King]", + "first_night_reminder": "", + "other_night_reminder": "If the King was killed by the Demon, wake the Choirboy and point to the Demon player.", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 0, + "other_night_position": 44, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/choirboy.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 106, + "fields": { + "character_id": "poppygrower", + "character_name": "Poppy Grower", + "ability": "Minions & Demons do not know each other. If you die, they learn who each other are that night.", + "first_night_reminder": "Do not inform the Demon/Minions who each other are", + "other_night_reminder": "If the Poppy Grower has died, show the Minions/Demon who each other are.", + "global_reminders": null, + "reminders": "Evil wakes", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 4, + "other_night_position": 3, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/poppygrower.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 107, + "fields": { + "character_id": "atheist", + "character_name": "Atheist", + "ability": "The Storyteller can break the game rules & if executed, good wins, even if you are dead. [No evil characters]", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/atheist.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 108, + "fields": { + "character_id": "cannibal", + "character_name": "Cannibal", + "ability": "You have the ability of the recently killed executee. If they are evil, you are poisoned until a good player dies by execution.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Poisoned,Died today", + "character_type": "Townsfolk", + "edition": 1, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/cannibal.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 109, + "fields": { + "character_id": "snitch", + "character_name": "Snitch", + "ability": "Minions start knowing 3 not-in-play characters.", + "first_night_reminder": "After Minion info wake each Minion and show them three not-in-play character tokens. These may be the same or different to each other and the ones shown to the Demon.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Outsider", + "edition": 1, + "first_night_position": 7, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/snitch.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 110, + "fields": { + "character_id": "acrobat", + "character_name": "Acrobat", + "ability": "Each night*, if either good living neighbour is drunk or poisoned, you die.", + "first_night_reminder": "", + "other_night_reminder": "If a good living neighbour is drunk or poisoned, the Acrobat player dies.", + "global_reminders": null, + "reminders": "Dead", + "character_type": "Outsider", + "edition": 2, + "first_night_position": 0, + "other_night_position": 39, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/acrobat.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 111, + "fields": { + "character_id": "puzzlemaster", + "character_name": "Puzzlemaster", + "ability": "1 player is drunk, even if you die. If you guess (once) who it is, learn the Demon player, but guess wrong & get false info.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Drunk,Guess used", + "character_type": "Outsider", + "edition": 1, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/puzzlemaster.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 112, + "fields": { + "character_id": "heretic", + "character_name": "Heretic", + "ability": "Whoever wins, loses & whoever loses, wins, even if you are dead.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Outsider", + "edition": 1, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/heretic.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 113, + "fields": { + "character_id": "damsel", + "character_name": "Damsel", + "ability": "All Minions know you are in play. If a Minion publicly guesses you (once), your team loses.", + "first_night_reminder": "Wake all the Minions, show them the 'This character selected you' card and the Damsel token.", + "other_night_reminder": "If selected by the Huntsman, wake the Damsel, show 'You are' card and a not-in-play Townsfolk token.", + "global_reminders": null, + "reminders": "Guess used", + "character_type": "Outsider", + "edition": 1, + "first_night_position": 31, + "other_night_position": 46, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/damsel.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 114, + "fields": { + "character_id": "golem", + "character_name": "Golem", + "ability": "You may only nominate once per game. When you do, if the nominee is not the Demon, they die.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Can not nominate", + "character_type": "Outsider", + "edition": 1, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/golem.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 115, + "fields": { + "character_id": "politician", + "character_name": "Politician", + "ability": "If you were the player most responsible for your team losing, you change alignment & win, even if dead.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Outsider", + "edition": 2, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/politician.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 116, + "fields": { + "character_id": "widow", + "character_name": "Widow", + "ability": "On your 1st night, look at the Grimoire and choose a player: they are poisoned. 1 good player knows a Widow is in play.", + "first_night_reminder": "Show the Grimoire to the Widow for as long as they need. The Widow points to a player. That player is poisoned. Wake a good player. Show the 'These characters are in play' card, then the Widow character token.", + "other_night_reminder": "", + "global_reminders": "Knows", + "reminders": "Poisoned", + "character_type": "Minion", + "edition": 2, + "first_night_position": 18, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/widow.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 117, + "fields": { + "character_id": "fearmonger", + "character_name": "Fearmonger", + "ability": "Each night, choose a player. If you nominate & execute them, their team loses. All players know if you choose a new player.", + "first_night_reminder": "The Fearmonger points to a player. Place the Fear token next to that player and announce that a new player has been selected with the Fearmonger ability.", + "other_night_reminder": "The Fearmonger points to a player. If different from the previous night, place the Fear token next to that player and announce that a new player has been selected with the Fearmonger ability.", + "global_reminders": null, + "reminders": "Fear", + "character_type": "Minion", + "edition": 1, + "first_night_position": 26, + "other_night_position": 17, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/fearmonger.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 118, + "fields": { + "character_id": "psychopath", + "character_name": "Psychopath", + "ability": "Each day, before nominations, you may publicly choose a player: they die. If executed, you only die if you lose roshambo.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Minion", + "edition": 1, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/psychopath.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 119, + "fields": { + "character_id": "goblin", + "character_name": "Goblin", + "ability": "If you publicly claim to be the Goblin when nominated & are executed that day, your team wins.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Claimed", + "character_type": "Minion", + "edition": 2, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/goblin.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 121, + "fields": { + "character_id": "mezepheles", + "character_name": "Mezepheles", + "ability": "You start knowing a secret word. The 1st good player to say this word becomes evil that night.", + "first_night_reminder": "Show the Mezepheles their secret word.", + "other_night_reminder": "Wake the 1st good player that said the Mezepheles' secret word and show them the 'You are' card and the thumbs down evil signal.", + "global_reminders": null, + "reminders": "Turns evil,No ability", + "character_type": "Minion", + "edition": 1, + "first_night_position": 27, + "other_night_position": 18, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/mezepheles.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 122, + "fields": { + "character_id": "marionette", + "character_name": "Marionette", + "ability": "You think you are a good character but you are not. The Demon knows who you are. [You neighbour the Demon]", + "first_night_reminder": "Select one of the good players next to the Demon and place the Is the Marionette reminder token. Wake the Demon and show them the Marionette.", + "other_night_reminder": "", + "global_reminders": "Is the Marionette", + "reminders": "", + "character_type": "Minion", + "edition": 1, + "first_night_position": 12, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/marionette.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 123, + "fields": { + "character_id": "boomdandy", + "character_name": "Boomdandy", + "ability": "If you are executed, all but 3 players die. 1 minute later, the player with the most players pointing at them dies.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Minion", + "edition": 1, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/boomdandy.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 124, + "fields": { + "character_id": "lilmonsta", + "character_name": "Lil' Monsta", + "ability": "Each night, Minions choose who babysits Lil' Monsta's token & \"is the Demon\". A player dies each night*. [+1 Minion]", + "first_night_reminder": "Wake all Minions together, allow them to vote by pointing at who they want to babysit Lil' Monsta.", + "other_night_reminder": "Wake all Minions together, allow them to vote by pointing at who they want to babysit Lil' Monsta. Choose a player, that player dies.", + "global_reminders": "Is the Demon,Dead", + "reminders": "", + "character_type": "Demon", + "edition": 2, + "first_night_position": 15, + "other_night_position": 35, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/lilmonsta.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 125, + "fields": { + "character_id": "lleech", + "character_name": "Lleech", + "ability": "Each night*, choose a player: they die. You start by choosing an alive player: they are poisoned - you die if & only if they die.", + "first_night_reminder": "The Lleech points to a player. Place the Poisoned reminder token.", + "other_night_reminder": "The Lleech points to a player. That player dies.", + "global_reminders": null, + "reminders": "Dead,Poisoned", + "character_type": "Demon", + "edition": 1, + "first_night_position": 16, + "other_night_position": 34, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/lleech.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 126, + "fields": { + "character_id": "alhadikhia", + "character_name": "Al-Hadikhia", + "ability": "Each night*, choose 3 players (all players learn who): each silently chooses to live or die, but if all live, all die.", + "first_night_reminder": "", + "other_night_reminder": "The Al-Hadikhia chooses 3 players. Announce the first player, wake them to nod yes to live or shake head no to die, kill or resurrect accordingly, then put to sleep and announce the next player. If all 3 are alive after this, all 3 die.", + "global_reminders": null, + "reminders": "1,2,3,Chose death,Chose life", + "character_type": "Demon", + "edition": 1, + "first_night_position": 0, + "other_night_position": 33, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/alhadikhia.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 127, + "fields": { + "character_id": "legion", + "character_name": "Legion", + "ability": "Each night*, a player might die. Executions fail if only evil voted. You register as a Minion too. [Most players are Legion]", + "first_night_reminder": "", + "other_night_reminder": "Choose a player, that player dies.", + "global_reminders": null, + "reminders": "Dead,About to die", + "character_type": "Demon", + "edition": 1, + "first_night_position": 0, + "other_night_position": 23, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/legion.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 128, + "fields": { + "character_id": "leviathan", + "character_name": "Leviathan", + "ability": "If more than 1 good player is executed, you win. All players know you are in play. After day 5, evil wins.", + "first_night_reminder": "Place the Leviathan 'Day 1' marker. Announce 'The Leviathan is in play; this is Day 1.'", + "other_night_reminder": "Change the Leviathan Day reminder for the next day.", + "global_reminders": null, + "reminders": "Day 1,Day 2,Day 3,Day 4,Day 5,Good player executed", + "character_type": "Demon", + "edition": 1, + "first_night_position": 54, + "other_night_position": 73, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/leviathan.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 129, + "fields": { + "character_id": "riot", + "character_name": "Riot", + "ability": "Nominees die, but may nominate again immediately (on day 3, they must). After day 3, evil wins. [All Minions are Riot]", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Demon", + "edition": 1, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/riot.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 130, + "fields": { + "character_id": "gangster", + "character_name": "Gangster", + "ability": "Once per day, you may choose to kill an alive neighbour, if your other alive neighbour agrees.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Traveller", + "edition": 1, + "first_night_position": 0, + "other_night_position": 0, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/gangster.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 145, + "fields": { + "character_id": "doomsayer", + "character_name": "Doomsayer", + "ability": "If 4 or more players live, each living player may publicly choose (once per game) that a player of their own alignment dies.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Fabled", + "edition": 0, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/doomsayer.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 146, + "fields": { + "character_id": "angel", + "character_name": "Angel", + "ability": "Something bad might happen to whoever is most responsible for the death of a new player.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Protect,Something Bad", + "character_type": "Fabled", + "edition": 0, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/angel.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 148, + "fields": { + "character_id": "hellslibrarian", + "character_name": "Hell's Librarian", + "ability": "Something bad might happen to whoever talks when the Storyteller has asked for silence.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Something Bad", + "character_type": "Fabled", + "edition": 0, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/hellslibrarian.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 149, + "fields": { + "character_id": "revolutionary", + "character_name": "Revolutionary", + "ability": "2 neighboring players are known to be the same alignment. Once per game, one of them registers falsely.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Used", + "character_type": "Fabled", + "edition": 0, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/revolutionary.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 150, + "fields": { + "character_id": "fiddler", + "character_name": "Fiddler", + "ability": "Once per game, the Demon secretly chooses an opposing player: all players choose which of these 2 players win.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Fabled", + "edition": 0, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/fiddler.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 151, + "fields": { + "character_id": "toymaker", + "character_name": "Toymaker", + "ability": "The Demon may choose not to attack & must do this at least once per game. Evil players get normal starting info.", + "first_night_reminder": "", + "other_night_reminder": "If it is a night when a Demon attack could end the game, and the Demon is marked “Final night: No Attack,” then the Demon does not act tonight. (Do not wake them.)", + "global_reminders": null, + "reminders": "Final Night: No Attack", + "character_type": "Fabled", + "edition": 0, + "first_night_position": null, + "other_night_position": 1, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/toymaker.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 152, + "fields": { + "character_id": "fibbin", + "character_name": "Fibbin", + "ability": "Once per game, 1 good player might get false information.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Used", + "character_type": "Fabled", + "edition": 0, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/fibbin.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 153, + "fields": { + "character_id": "duchess", + "character_name": "Duchess", + "ability": "Each day, 3 players may choose to visit you. At night*, each visitor learns how many visitors are evil, but 1 gets false info.", + "first_night_reminder": "", + "other_night_reminder": "Wake each player marked “Visitor” or “False Info” one at a time. Show them the Duchess token, then fingers (1, 2, 3) equaling the number of evil players marked “Visitor” or, if you are waking the player marked “False Info,” show them any number of fingers except the number of evil players marked “Visitor.”", + "global_reminders": null, + "reminders": "Visitor,False Info", + "character_type": "Fabled", + "edition": 0, + "first_night_position": null, + "other_night_position": 1, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/duchess.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 155, + "fields": { + "character_id": "spiritofivory", + "character_name": "Spirit of Ivory", + "ability": "There can't be more than 1 extra evil player.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "No extra evil", + "character_type": "Fabled", + "edition": 0, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/spiritofivory.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 157, + "fields": { + "character_id": "stormcatcher", + "character_name": "Storm Catcher", + "ability": "Name a good character. If in play, they can only die by execution, but evil players learn which player it is.", + "first_night_reminder": "Mark a good player as \"Safe\". Wake each evil player and show them the marked player.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Safe", + "character_type": "Fabled", + "edition": 1, + "first_night_position": 1, + "other_night_position": null, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/stormcatcher.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 159, + "fields": { + "character_id": "buddhist", + "character_name": "Buddhist", + "ability": "For the first 2 minutes of each day, veteran players may not talk.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Fabled", + "edition": 0, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/buddhist.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 161, + "fields": { + "character_id": "sentinel", + "character_name": "Sentinel", + "ability": "There might be 1 extra or 1 fewer Outsider in play.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Fabled", + "edition": 0, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/sentinel.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 163, + "fields": { + "character_id": "djinn", + "character_name": "Djinn", + "ability": "Use the Djinn's special rule. All players know what it is.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Fabled", + "edition": 0, + "first_night_position": 0, + "other_night_position": null, + "image_url": "https://github.com/bra1n/townsquare/raw/develop/src/assets/icons/djinn.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 165, + "fields": { + "character_id": "organgrinder", + "character_name": "Organ Grinder", + "ability": "All players keep their eyes closed when voting & the vote tally is secret. Votes for you only count if you vote.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "About to die", + "character_type": "Minion", + "edition": 3, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1088959658468855949/og_tool_b.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 166, + "fields": { + "character_id": "vizier", + "character_name": "Vizier", + "ability": "All players know who you are. You can not die during the day. If good voted, you may choose to execute immediately.", + "first_night_reminder": "Announce 'The Vizier is in play' and state which player they are.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Minion", + "edition": 3, + "first_night_position": 54, + "other_night_position": null, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1099017517655146707/vizier.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 167, + "fields": { + "character_id": "knight", + "character_name": "Knight", + "ability": "You start knowing 2 players that are not the Demon.", + "first_night_reminder": "During setup, mark two non-Demon players with the Knight’s “Know” reminders.\r\nDuring the first night, wake the Knight. Point to the two players marked “Know”.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Know\r\nKnow", + "character_type": "Townsfolk", + "edition": 3, + "first_night_position": 33, + "other_night_position": null, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1110011131814223873/knight.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 168, + "fields": { + "character_id": "steward", + "character_name": "Steward", + "ability": "You start knowing 1 good player.", + "first_night_reminder": "While preparing the first night, put the “Know” reminder by any good character token.\r\nDuring the first night, wake the Steward. Point to the player marked “Know”. Put the Steward to sleep.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "Know", + "character_type": "Townsfolk", + "edition": 3, + "first_night_position": 33, + "other_night_position": null, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1110011173778227210/steward.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 169, + "fields": { + "character_id": "highpriestess", + "character_name": "High Priestess", + "ability": "Each night, learn which player the Storyteller believes you should talk to most.", + "first_night_reminder": "Point to a player.", + "other_night_reminder": "Point to a player.", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 3, + "first_night_position": 50, + "other_night_position": 69, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1121842325019172944/high_priestess.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 170, + "fields": { + "character_id": "harpy", + "character_name": "Harpy", + "ability": "Each night, choose 2 players: tomorrow, the 1st player is mad that the 2nd is evil, or both might die.", + "first_night_reminder": "Wake the Harpy; they point at one player, then another. Wake the 1st player the Harpy pointed to, show them the 'This character has selected you' card, show them the Harpy token, then point at the 2nd player the Harpy pointed to.", + "other_night_reminder": "Wake the Harpy; they point at one player, then another. Wake the 1st player the Harpy pointed to, show them the 'This character has selected you' card, show them the Harpy token, then point at the 2nd player the Harpy pointed to.", + "global_reminders": null, + "reminders": "Mad, 2nd", + "character_type": "Minion", + "edition": 3, + "first_night_position": 24, + "other_night_position": 14, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1131964883643138168/harpy.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 171, + "fields": { + "character_id": "plaguedoctor", + "character_name": "Plague Doctor", + "ability": "If you die, the Storyteller gains a not-in-play Minion ability.", + "first_night_reminder": "", + "other_night_reminder": "Storyteller Ability", + "global_reminders": null, + "reminders": "", + "character_type": "Outsider", + "edition": 3, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1144436621488431316/plaguedoctor.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 172, + "fields": { + "character_id": "shugenja", + "character_name": "Shugenja", + "ability": "You start knowing if your closest evil player is clockwise or anti-clockwise. If equidistant, this info is arbitrary.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 3, + "first_night_position": null, + "other_night_position": null, + "image_url": null, + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 173, + "fields": { + "character_id": "ojo", + "character_name": "Ojo", + "ability": "Each night*. Choose a character, they die. If they are not in play, the Storyteller chooses who dies.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Demon", + "edition": 3, + "first_night_position": null, + "other_night_position": null, + "image_url": null, + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 174, + "fields": { + "character_id": "hatter", + "character_name": "Hatter", + "ability": "If you died today or tonight, the Minion & Demon players may choose new Minion & Demon characters to be.", + "first_night_reminder": "", + "other_night_reminder": "If the Hatter died today: Wake the Minions and Demon. Show them the 'This Character Selected You' info token, then the Hatter token. Each player either shakes their head no or points to another character of the same type as their current character. If a second player would end up with the same character as another player, shake your head no and gesture for them to choose again. Put them to sleep. Change each player to the character they chose.", + "global_reminders": null, + "reminders": "\"Tea Party Tonight\"", + "character_type": "Outsider", + "edition": 3, + "first_night_position": 0, + "other_night_position": 40, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1179941106817957958/hatter.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 175, + "fields": { + "character_id": "kazali", + "character_name": "Kazali", + "ability": "Each night*, choose a player: they die. [You choose which players are Minions. -? to +? Outsiders]", + "first_night_reminder": "The Kazali points to a player and a Minion on the character sheet. They do this for as many Minions as should be in play. Change those players' tokens to the chosen Minion tokens in the Grim. Wake those players, show them the 'You Are' card, the Minions they have become, and a thumbs down.", + "other_night_reminder": "The Kazali points to a player. That player dies", + "global_reminders": null, + "reminders": "Dead", + "character_type": "Demon", + "edition": 3, + "first_night_position": 1, + "other_night_position": 35, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1190399554810556416/kazali.png", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 176, + "fields": { + "character_id": "villageidiot", + "character_name": "Village Idiot", + "ability": ".", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 3, + "first_night_position": null, + "other_night_position": null, + "image_url": null, + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 177, + "fields": { + "character_id": "ferryman", + "character_name": "Ferryman", + "ability": "On the final day, all dead players regain their vote token.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Fabled", + "edition": 3, + "first_night_position": null, + "other_night_position": null, + "image_url": null, + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 178, + "fields": { + "character_id": "yaggababble", + "character_name": "Yaggababble", + "ability": "You start knowing a secret phrase. For each time you said it publicly today, a player might die.", + "first_night_reminder": "Show the Yaggababble their secret phrase.", + "other_night_reminder": "Choose a number of players up to the total number of times the Yaggababble said their secret phrase publicly, those players die.", + "global_reminders": null, + "reminders": "Dead", + "character_type": "Demon", + "edition": 3, + "first_night_position": 2, + "other_night_position": 35, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1213233545498198036/yaggababble.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 179, + "fields": { + "character_id": "summoner", + "character_name": "Summoner", + "ability": "You get 3 bluffs. On the 3rd night, choose a player: they become an evil Demon of your choice. [No Demon]", + "first_night_reminder": "Show the 'These characters are not in play' card. Show 3 character tokens of good characters not in play.", + "other_night_reminder": "If it is the 3rd night, wake the Summoner. They point to a player and a Demon on the character sheet - that player becomes that Demon.", + "global_reminders": null, + "reminders": "Night 1\r\nNight 2\r\nNight 3", + "character_type": "Minion", + "edition": 3, + "first_night_position": 8, + "other_night_position": 19, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1220554038886793226/summoner.png?ex=660f5c", + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 180, + "fields": { + "character_id": "banshee", + "character_name": "Banshee", + "ability": "If the Demon kills you, all players learn this. From now on, you may nominate twice per day and vote twice per nomination.", + "first_night_reminder": "", + "other_night_reminder": "Announce that the Banshee has died.", + "global_reminders": null, + "reminders": "Has Ability", + "character_type": "Townsfolk", + "edition": 3, + "first_night_position": null, + "other_night_position": 42, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1233310948048113755/banshee.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 181, + "fields": { + "character_id": "ogre", + "character_name": "Ogre", + "ability": "On your 1st night, choose a player (not yourself): you become their alignment (you don't know which) even if drunk or poisoned.", + "first_night_reminder": "The Ogre points to a player (not themselves) and becomes their alignment.", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Outsider", + "edition": 3, + "first_night_position": 49, + "other_night_position": null, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1243440702344396862/ogre.png?ex=665224a5&i", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 182, + "fields": { + "character_id": "alsaahir", + "character_name": "Alsaahir", + "ability": "Once per day, if you publicly guess which players are Minion(s) and which are Demon(s), good wins.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Townsfolk", + "edition": 3, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1256111147589963857/alsaahir.png?ex=66818e", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 183, + "fields": { + "character_id": "zealot", + "character_name": "Zealot", + "ability": "If there are 5 or more players alive, you must vote for every nomination.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Outsider", + "edition": 3, + "first_night_position": null, + "other_night_position": null, + "image_url": "https://cdn.discordapp.com/attachments/708509594758152203/1266322399695994990/zealot.png", + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 184, + "fields": { + "character_id": "lordoftyphon", + "character_name": "Lord of Typhon", + "ability": "Each night*, choose a player: they die.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Demon", + "edition": 3, + "first_night_position": null, + "other_night_position": null, + "image_url": null, + "modifies_setup": true + } + }, + { + "model": "scripts.character", + "pk": 185, + "fields": { + "character_id": "boffin", + "character_name": "Boffin", + "ability": "The Demon (even if drunk or poisoned) has a not-in-play good character's ability. You both know which.", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Minion", + "edition": 3, + "first_night_position": null, + "other_night_position": null, + "image_url": null, + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 186, + "fields": { + "character_id": "bootlegger", + "character_name": "Bootlegger", + "ability": ".", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Fabled", + "edition": 3, + "first_night_position": null, + "other_night_position": null, + "image_url": null, + "modifies_setup": false + } + }, + { + "model": "scripts.character", + "pk": 187, + "fields": { + "character_id": "gardener", + "character_name": "Gardener", + "ability": ".", + "first_night_reminder": "", + "other_night_reminder": "", + "global_reminders": null, + "reminders": "", + "character_type": "Fabled", + "edition": 3, + "first_night_position": null, + "other_night_position": null, + "image_url": null, + "modifies_setup": false + } + } +] \ No newline at end of file diff --git a/scripts/forms.py b/scripts/forms.py index 5508920..a81ce8d 100644 --- a/scripts/forms.py +++ b/scripts/forms.py @@ -1,11 +1,14 @@ -import json as js - from django import forms from django.core.exceptions import ValidationError from django.core.validators import FileExtensionValidator +import jsonschema.exceptions from versionfield import Version -from scripts import constants, models, script_json, validators, widgets +from scripts import constants, models, validators, widgets, script_json +import json as js +import requests +import os +import jsonschema class JSONError(Exception): @@ -16,7 +19,7 @@ def tagOptions(): return models.ScriptTag.objects.filter(public=True) -class ScriptForm(forms.Form): +class BaseScriptForm(forms.Form): name = forms.CharField( max_length=constants.MAX_SCRIPT_NAME_LENGTH, required=False, label="Script name" ) @@ -29,12 +32,6 @@ class ScriptForm(forms.Form): version = forms.CharField( max_length=20, initial="1", validators=[validators.valid_version] ) - tags = forms.ModelMultipleChoiceField( - queryset=tagOptions(), - to_field_name="name", - required=False, - widget=forms.CheckboxSelectMultiple, - ) content = forms.FileField( label="JSON", validators=[FileExtensionValidator(["json"])] ) @@ -53,13 +50,29 @@ class ScriptForm(forms.Form): def __init__(self, *args, **kwargs): self.user = kwargs.pop("user") - super(ScriptForm, self).__init__(*args, **kwargs) + super(BaseScriptForm, self).__init__(*args, **kwargs) def clean(self): cleaned_data = super().clean() try: json = get_json_content(cleaned_data) + try: + schema_version = str(os.environ.get("JSON_SCHEMA_VERSION", "v3.33.2")) + schema_url = f"https://raw.githubusercontent.com/ThePandemoniumInstitute/botc-release/refs/tags/{schema_version}/script-schema.json" + schema = requests.get(schema_url, timeout=2) + try: + jsonschema.validate(json, schema.content) + except jsonschema.exceptions.ValidationError as e: + raise ValidationError( + f"This is not a valid script JSON. It does not conform to the schema at {schema_url}. \ + Error message: {e.message}" + ) + except jsonschema.exceptions.SchemaError as e: + pass + except requests.exceptions.Timeout: + pass + if not isinstance(json, list): raise ValidationError( f"This is not a valid script JSON. Script JSONs are lists of character objects." @@ -96,8 +109,6 @@ def clean(self): # f"Entered Name {entered_name} does not match script JSON name {json_name}" # ) - validators.validate_json(json) - # script_name = json_name if json_name else entered_name script_name = entered_name @@ -121,6 +132,22 @@ def clean(self): except JSONError: pass +class ScriptForm(BaseScriptForm): + tags = forms.ModelMultipleChoiceField( + queryset=tagOptions(), + to_field_name="name", + required=False, + widget=forms.CheckboxSelectMultiple, + ) + + def clean(self): + cleaned_data = super().clean() + try: + json = get_json_content(cleaned_data) + validators.validate_json(json) + except JSONError: + pass + def get_json_content(data): json_content = data.get("content", None) @@ -227,11 +254,17 @@ def __init__( class UpdateDatabaseForm(forms.Form): + # start = forms.IntegerField( + # min_value=0, max_value=models.ScriptVersion.objects.latest('pk').pk, required=True + # ) + # end = forms.IntegerField( + # min_value=0, max_value=models.ScriptVersion.objects.latest('pk').pk, required=True + # ) start = forms.IntegerField( - min_value=0, max_value=models.ScriptVersion.objects.latest('pk').pk, required=True + min_value=0, max_value=10000, required=True ) end = forms.IntegerField( - min_value=0, max_value=models.ScriptVersion.objects.latest('pk').pk, required=True + min_value=0, max_value=10000, required=True ) def clean(self): diff --git a/scripts/models.py b/scripts/models.py index 7bb32d1..41a2e83 100644 --- a/scripts/models.py +++ b/scripts/models.py @@ -251,21 +251,13 @@ class Meta: abstract = True -class Character(BaseCharacterInfo): - """ - Model for characters. - """ - +class BaseCharacter(BaseCharacterInfo): character_type = models.CharField(max_length=30, choices=CharacterType.choices) - edition = models.IntegerField(choices=Edition.choices) - first_night_position = models.IntegerField(blank=True, null=True) - other_night_position = models.IntegerField(blank=True, null=True) + first_night_position = models.FloatField(blank=True, null=True) + other_night_position = models.FloatField(blank=True, null=True) image_url = models.CharField(blank=True, null=True, max_length=100) modifies_setup = models.BooleanField(default=False) - class Meta: - permissions = [("update_characters", "Can update character information")] - def full_character_json(self) -> Dict: character_json = {} character_json["id"] = self.character_id @@ -286,6 +278,18 @@ def full_character_json(self) -> Dict: def __str__(self): return f"{self.character_name}" + class Meta: + abstract = True + +class Character(BaseCharacter): + """ + Model for characters. + """ + edition = models.IntegerField(choices=Edition.choices) + + class Meta: + permissions = [("update_characters", "Can update character information")] + class Translation(BaseCharacterInfo): """ diff --git a/scripts/templates/delete_comment.html b/scripts/templates/delete_comment.html index f396dd4..ee8bfaf 100644 --- a/scripts/templates/delete_comment.html +++ b/scripts/templates/delete_comment.html @@ -15,7 +15,11 @@
    - {% bootstrap_form form layout='horizontal' exclude="name,author,script_type,includes_characters,excludes_characters,edition,minimum_number_of_likes,minimum_number_of_favourites,minimum_number_of_comments,all_scripts" %} + {% bootstrap_form form layout='horizontal' exclude="name,author,script_type,includes_characters,excludes_characters,edition,minimum_number_of_likes,minimum_number_of_favourites,minimum_number_of_comments,all_scripts,include_hybrid,include_homebrew" %}
    {% buttons %} diff --git a/scripts/templates/script.html b/scripts/templates/script.html index f5ee742..cb2728a 100644 --- a/scripts/templates/script.html +++ b/scripts/templates/script.html @@ -220,9 +220,11 @@

    You

    History {% endif %} + {% if script_version.homebrewiness == 0 %} + {% endif %} {% if script_version.collections.count > 0 %}