├── frangiclave ├── __init__.py ├── bot │ ├── __init__.py │ ├── templates │ │ ├── __init__.py │ │ ├── recipe.py │ │ ├── verb.py │ │ ├── ending.py │ │ ├── legacy.py │ │ ├── deck.py │ │ ├── base.py │ │ ├── element.py │ │ └── search_results.py │ └── client.py ├── game │ ├── __init__.py │ └── importer.py ├── compendium │ ├── __init__.py │ ├── ending_flavour.py │ ├── base.py │ ├── file.py │ ├── utils.py │ ├── ending.py │ ├── linked_recipe_details.py │ ├── verb.py │ ├── game_content.py │ ├── slot_specification.py │ ├── legacy.py │ ├── deck.py │ ├── element.py │ └── recipe.py ├── static │ ├── img │ │ ├── felt.png │ │ └── knock.png │ ├── frangiclave.js │ └── style.css ├── config.py ├── templates │ ├── 404.tpl.html │ ├── search.tpl.html │ ├── verb.tpl.html │ ├── ending.tpl.html │ ├── index.tpl.html │ ├── deck.tpl.html │ ├── legacy.tpl.html │ ├── base.tpl.html │ ├── recipe.tpl.html │ ├── element.tpl.html │ └── macros.tpl.html ├── main_bot.py ├── main.py ├── search.py ├── csjson.py ├── server.py └── exporter.py ├── _version.py ├── patch ├── CultistSimulator │ ├── .gitignore │ └── README.txt ├── MonoMod │ ├── .gitignore │ └── README.txt ├── .gitignore ├── PatchCultistSimulator.cmd ├── CultistSimulator.Modding.sln ├── CultistSimulator.Modding.sln.DotSettings.user └── CultistSimulator.Modding.mm │ ├── Properties │ └── AssemblyInfo.cs │ ├── Texture2DExtensions.cs │ ├── ResourcesManager.cs │ ├── SplashAnimation.cs │ └── CultistSimulator.Modding.mm.csproj ├── MANIFEST.in ├── config.default.toml ├── README.md ├── Makefile ├── setup.py ├── .gitignore └── LICENSE.md /frangiclave/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frangiclave/bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frangiclave/game/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_version.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.0.0' 2 | -------------------------------------------------------------------------------- /frangiclave/bot/templates/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frangiclave/compendium/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /patch/CultistSimulator/.gitignore: -------------------------------------------------------------------------------- 1 | *.dll 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE.md 2 | -------------------------------------------------------------------------------- /patch/MonoMod/.gitignore: -------------------------------------------------------------------------------- 1 | *.dll 2 | *.exe 3 | -------------------------------------------------------------------------------- /patch/MonoMod/README.txt: -------------------------------------------------------------------------------- 1 | Place all MonoMod files in this directory. 2 | -------------------------------------------------------------------------------- /patch/.gitignore: -------------------------------------------------------------------------------- 1 | CultistSimulator.Modding.mm/bin 2 | CultistSimulator.Modding.mm/obj 3 | .vs/ 4 | -------------------------------------------------------------------------------- /patch/CultistSimulator/README.txt: -------------------------------------------------------------------------------- 1 | Place all DLL files from Cultist Simulator in this directory. 2 | -------------------------------------------------------------------------------- /frangiclave/static/img/felt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frangiclave/frangiclave-compendium/HEAD/frangiclave/static/img/felt.png -------------------------------------------------------------------------------- /frangiclave/static/img/knock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frangiclave/frangiclave-compendium/HEAD/frangiclave/static/img/knock.png -------------------------------------------------------------------------------- /frangiclave/config.py: -------------------------------------------------------------------------------- 1 | import toml 2 | 3 | DEFAULT_CONFIG_FILE_NAME = 'config.toml' 4 | 5 | 6 | def load_config(config_file_name): 7 | with open(config_file_name) as config_file: 8 | return toml.load(config_file) 9 | -------------------------------------------------------------------------------- /frangiclave/compendium/ending_flavour.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class EndingFlavour(Enum): 5 | NONE = 'None' 6 | GRAND = 'Grand' 7 | MELANCHOLY = 'Melancholy' 8 | PALE = 'Pale' 9 | VILE = 'Vile' 10 | XXX = 'XXX' 11 | -------------------------------------------------------------------------------- /frangiclave/templates/404.tpl.html: -------------------------------------------------------------------------------- 1 | {% extends "base.tpl.html" %} 2 | {% block title %}Page Not Found{% endblock %} 3 | {% block description %}Nothing? Nothing.{% endblock %} 4 | {% block content %} 5 |
Nothing.
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /config.default.toml: -------------------------------------------------------------------------------- 1 | [frangiclave] 2 | 3 | BASE_URL = "http://localhost:5000" 4 | 5 | DEBUG = false 6 | 7 | GAME_DIRECTORY = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Cultist Simulator\\" 8 | 9 | IMAGES_DIRECTORY = "images" 10 | 11 | READ_ONLY = false 12 | 13 | SLACK_BOT_TOKEN = "" 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | frangiclave-compendium 2 | ====================== 3 | 4 | *A web-based tool for viewing Cultist Simulator data.* 5 | 6 | **frangiclave-compendium** is a Python web application for browsing game data from Cultist Simulator in a convenient web interface. 7 | 8 | License:  9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: run 2 | 3 | run: 4 | #uwsgi --http-socket 127.0.0.1:5777 --master --processes 4 --enable-threads --virtualenv=./venv/ --mount /=frangiclave.main:init --manage-script-name 5 | uwsgi -s /tmp/frangiclave.sock --master --processes 4 --enable-threads --virtualenv=./venv/ --manage-script-name --mount /frangiclave=frangiclave.main:app --chmod-socket=777 6 | -------------------------------------------------------------------------------- /frangiclave/static/frangiclave.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $('.section-title').click(function () { 3 | $(this).next('.section-list').slideToggle(); 4 | }); 5 | 6 | $('#action-load').click(function () { 7 | $.get('/load/'); 8 | }); 9 | 10 | var activeSectionItem = document.getElementById('section-item-active'); 11 | if (activeSectionItem != null) { 12 | activeSectionItem.scrollIntoView(); 13 | window.scrollTo(0, 0); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /frangiclave/bot/templates/recipe.py: -------------------------------------------------------------------------------- 1 | from frangiclave.bot.templates.base import URL_FORMAT, make_section, DIVIDER 2 | from frangiclave.compendium.recipe import Recipe 3 | 4 | 5 | def make_recipe(recipe: Recipe): 6 | return [ 7 | make_section('*Recipe: {}*'.format(URL_FORMAT.format('recipe', recipe.recipe_id))), 8 | DIVIDER, 9 | make_section( 10 | f'*_Label:_* {recipe.label}\n' 11 | f'*_Start Description:_* {recipe.start_description}\n' 12 | f'*_Description:_* {recipe.description}' 13 | ) 14 | ] 15 | -------------------------------------------------------------------------------- /frangiclave/main_bot.py: -------------------------------------------------------------------------------- 1 | from frangiclave.bot.client import BotClient 2 | from frangiclave.config import DEFAULT_CONFIG_FILE_NAME, load_config 3 | 4 | 5 | def main(): 6 | # Load the configuration 7 | print('Loading configuration...') 8 | config = load_config(DEFAULT_CONFIG_FILE_NAME) 9 | 10 | # Set up the bot and keep listening for events forever 11 | print('Initializing bot client...') 12 | client = BotClient(config['frangiclave']['SLACK_BOT_TOKEN']) 13 | print('Bot client ready. Listening...') 14 | client.run() 15 | 16 | 17 | if __name__ == '__main__': 18 | main() 19 | -------------------------------------------------------------------------------- /patch/PatchCultistSimulator.cmd: -------------------------------------------------------------------------------- 1 | @set CS_DIR=C:\Program Files (x86)\Steam\steamapps\common\Cultist Simulator\cultistsimulator_Data\Managed\ 2 | 3 | @set OLD_CD=%CD% 4 | @set ASSEMBLY=Assembly-CSharp.dll 5 | @set ASSEMBLY_MOD=MONOMODDED_%ASSEMBLY% 6 | @set MONOMOD_DIR=MonoMod 7 | @set PATCH_DIR=%~dp0 8 | @set BUILD_DIR=%PATCH_DIR%\CultistSimulator.Modding.mm\bin\Release 9 | 10 | @copy /y "%PATCH_DIR%\%MONOMOD_DIR%\*" "%BUILD_DIR%" 11 | @cd %BUILD_DIR% 12 | @MonoMod "%ASSEMBLY%" 13 | 14 | @copy /y "%ASSEMBLY_MOD%" "%CS_DIR%\%ASSEMBLY%" 15 | @copy /y "%PATCH_DIR%\%MONOMOD_DIR%\*" "%CS_DIR%" 16 | 17 | @cd %OLD_CD% 18 | -------------------------------------------------------------------------------- /frangiclave/compendium/base.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | from sqlalchemy import create_engine, Column, Integer 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.orm import sessionmaker 6 | 7 | engine = create_engine('sqlite:///frangiclave.db') 8 | Session = sessionmaker(bind=engine) 9 | 10 | Base = declarative_base(engine) 11 | 12 | 13 | @contextmanager 14 | def get_session(): 15 | """Provide a transactional scope around a series of operations.""" 16 | session = Session() 17 | try: 18 | yield session 19 | session.commit() 20 | except: 21 | session.rollback() 22 | raise 23 | finally: 24 | session.close() 25 | -------------------------------------------------------------------------------- /frangiclave/bot/templates/verb.py: -------------------------------------------------------------------------------- 1 | from frangiclave.bot.templates.base import make_section, DIVIDER, IMAGE_FORMAT, \ 2 | URL_FORMAT, get_image_if_reachable 3 | from frangiclave.compendium.verb import Verb 4 | 5 | 6 | def make_verb(verb: Verb): 7 | image = get_image_if_reachable(get_verb_art(verb)) 8 | image_alt = None 9 | if image is not None: 10 | image_alt = verb.verb_id 11 | return [ 12 | make_section('*verb: {}*'.format(URL_FORMAT.format('verb', verb.verb_id))), 13 | DIVIDER, 14 | make_section( 15 | f'*_Label:_* {verb.label}\n' 16 | f'*_Description:_* {verb.description}', 17 | image, 18 | image_alt 19 | ) 20 | ] 21 | 22 | 23 | def get_verb_art(verb: Verb): 24 | return IMAGE_FORMAT.format('icons100/verbs', verb.verb_id) 25 | -------------------------------------------------------------------------------- /frangiclave/bot/templates/ending.py: -------------------------------------------------------------------------------- 1 | from frangiclave.bot.templates.base import make_section, DIVIDER, \ 2 | IMAGE_FORMAT, URL_FORMAT, get_image_if_reachable 3 | from frangiclave.compendium.ending import Ending 4 | 5 | 6 | def make_ending(ending: Ending): 7 | image = get_image_if_reachable(get_ending_art(ending)) 8 | image_alt = None 9 | if image is not None: 10 | image_alt = ending.ending_id 11 | return [ 12 | make_section('*ending: {}*'.format(URL_FORMAT.format('ending', ending.ending_id))), 13 | DIVIDER, 14 | make_section( 15 | f'*_Title:_* {ending.title}\n' 16 | f'*_Description:_* {ending.description}', 17 | image, 18 | image_alt 19 | ) 20 | ] 21 | 22 | 23 | def get_ending_art(ending: Ending): 24 | return IMAGE_FORMAT.format('endingArt', ending.image) 25 | -------------------------------------------------------------------------------- /frangiclave/main.py: -------------------------------------------------------------------------------- 1 | from frangiclave.compendium.base import Base 2 | from frangiclave.config import DEFAULT_CONFIG_FILE_NAME, load_config 3 | from frangiclave.server import app 4 | 5 | 6 | def init_db(): 7 | # Load all the models 8 | # noinspection PyUnresolvedReferences 9 | from frangiclave.compendium import deck, element, file, legacy, recipe, verb 10 | 11 | # Create the database tables 12 | Base.metadata.create_all() 13 | 14 | 15 | def init(): 16 | # Load the configuration 17 | config = load_config(DEFAULT_CONFIG_FILE_NAME) 18 | 19 | # Initialize the database connection 20 | init_db() 21 | 22 | # Set the global variables 23 | app.config.update(config['frangiclave']) 24 | 25 | return app 26 | 27 | 28 | def main(): 29 | # Run the app 30 | app.run() 31 | 32 | 33 | init() 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /patch/CultistSimulator.Modding.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultistSimulator.Modding.mm", "CultistSimulator.Modding.mm\CultistSimulator.Modding.mm.csproj", "{E77B3CEF-3B80-4748-A00A-A92659BB281D}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {E77B3CEF-3B80-4748-A00A-A92659BB281D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {E77B3CEF-3B80-4748-A00A-A92659BB281D}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {E77B3CEF-3B80-4748-A00A-A92659BB281D}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {E77B3CEF-3B80-4748-A00A-A92659BB281D}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /frangiclave/compendium/file.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from sqlalchemy import Column, Integer, String, Enum as EnumType 4 | 5 | from frangiclave.compendium.base import Base 6 | 7 | 8 | class FileCategory(Enum): 9 | DECKS = 'decks' 10 | ELEMENTS = 'elements' 11 | ENDINGS = 'endings' 12 | LEGACIES = 'legacies' 13 | RECIPES = 'recipes' 14 | VERBS = 'verbs' 15 | 16 | 17 | class FileGroup(Enum): 18 | CORE = 'core' 19 | MORE = 'more' 20 | 21 | 22 | class File(Base): 23 | __tablename__ = 'files' 24 | 25 | id = Column(Integer, primary_key=True) 26 | category = Column(EnumType(FileCategory, name='file_category')) 27 | group = Column(EnumType(FileGroup, name='file_group')) 28 | name = Column(String) 29 | 30 | def __init__(self, category: FileCategory, group: FileGroup, name: str): 31 | self.category = category 32 | self.group = group 33 | self.name = name 34 | -------------------------------------------------------------------------------- /frangiclave/bot/templates/legacy.py: -------------------------------------------------------------------------------- 1 | from frangiclave.bot.templates.base import make_section, DIVIDER, IMAGE_FORMAT, \ 2 | URL_FORMAT, get_image_if_reachable 3 | from frangiclave.compendium.legacy import Legacy 4 | 5 | 6 | def make_legacy(legacy: Legacy): 7 | image = get_image_if_reachable(get_legacy_art(legacy)) 8 | image_alt = None 9 | if image is not None: 10 | image_alt = legacy.legacy_id 11 | return [ 12 | make_section('*Legacy: {}*'.format(URL_FORMAT.format('legacy', legacy.legacy_id))), 13 | DIVIDER, 14 | make_section( 15 | f'*_Label:_* {legacy.label}\n' 16 | f'*_Description:_* {legacy.description}\n' 17 | f'*_Start Description:_* {legacy.start_description}', 18 | image, 19 | image_alt 20 | ) 21 | ] 22 | 23 | 24 | def get_legacy_art(legacy: Legacy): 25 | return IMAGE_FORMAT.format('icons100/legacies', legacy.image) 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import io 4 | import os 5 | 6 | from setuptools import find_packages, setup 7 | 8 | from _version import __version__ 9 | 10 | here = os.path.abspath(os.path.dirname(__file__)) 11 | 12 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 13 | long_description = '\n' + f.read() 14 | 15 | setup( 16 | name='frangiclave-compendium', 17 | version=__version__, 18 | description='A tool for viewing Cultist Simulator data.', 19 | long_description=long_description, 20 | author='Lyrositor', 21 | python_requires='>=3.6.0', 22 | url='https://github.com/frangiclave/frangiclave-compendium', 23 | packages=find_packages(exclude=('tests',)), 24 | install_requires=[ 25 | 'flask', 26 | 'slackclient', 27 | 'sqlalchemy', 28 | 'toml', 29 | 'jsom==0.0.5' 30 | ], 31 | extras_require={}, 32 | include_package_data=True, 33 | license='CC0' 34 | ) 35 | -------------------------------------------------------------------------------- /frangiclave/bot/templates/deck.py: -------------------------------------------------------------------------------- 1 | from frangiclave.bot.templates.base import make_section, DIVIDER, URL_FORMAT 2 | from frangiclave.compendium.deck import Deck 3 | 4 | 5 | def make_deck(deck: Deck): 6 | draw_messages = '\n'.join(f'•No results found.
{% endif %} 21 | {% for result in results %} 22 |Label: {{ m.localised(verb.label) }}
23 | 24 |Description: {{ m.localised(verb.description) }}
25 | 26 |At Start? {{ m.yes_no(verb.at_start) }}
27 | 28 |Comments: {{ m.multiline(m.optional(verb.comments)) }}
29 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /frangiclave/bot/templates/element.py: -------------------------------------------------------------------------------- 1 | from frangiclave.bot.templates.base import make_section, DIVIDER, IMAGE_FORMAT, \ 2 | URL_FORMAT, get_image_if_reachable 3 | from frangiclave.compendium.element import Element 4 | 5 | 6 | def make_element(element: Element): 7 | image = get_image_if_reachable(get_element_art(element)) 8 | image_alt = None 9 | if image is not None: 10 | image_alt = element.element_id 11 | decay_to = None 12 | if element.decay_to: 13 | decay_to = URL_FORMAT.format('element', element.decay_to.element_id) 14 | return [ 15 | make_section('*Element: {}*'.format(URL_FORMAT.format('element', element.element_id))), 16 | DIVIDER, 17 | make_section( 18 | f'*_Label:_* {element.label}\n' 19 | f'*_Description:_* {element.description}', 20 | image, 21 | image_alt 22 | ) 23 | ] 24 | 25 | 26 | def get_element_art(element: Element): 27 | if element.no_art_needed: 28 | image_id = '_x' 29 | else: 30 | image_id = element.icon if element.icon else element.element_id 31 | return IMAGE_FORMAT.format( 32 | 'icons40/aspects' if element.is_aspect else 'elementArt', 33 | image_id 34 | ) 35 | -------------------------------------------------------------------------------- /frangiclave/templates/ending.tpl.html: -------------------------------------------------------------------------------- 1 | {% extends "base.tpl.html" %} 2 | {% import "macros.tpl.html" as m with context %} 3 | {% block title %}Ending: {{ ending.ending_id }}{% endblock %} 4 | {% block description %}{{ ending.description }}{% endblock %} 5 | {% block content %} 6 |
20 |
21 | Title: {{ m.localised(ending.title) }}
22 | 23 |Description: {{ m.localised(ending.description, True) }}
24 | 25 |Ending Flavour: {{ ending.flavour.value }}
26 | 27 |Animation: {{ ending.animation }}
28 | 29 |Achievement: {{ m.optional(ending.achievement) }}
30 | {% endblock %} 31 | 32 | -------------------------------------------------------------------------------- /frangiclave/templates/index.tpl.html: -------------------------------------------------------------------------------- 1 | {% extends "base.tpl.html" %} 2 | {% block title %}Home{% endblock %} 3 | {% block description %}Welcome, friend, to the Frangiclave Compendium, the premier source of forbidden knowledge for the discerning occultist.{% endblock %} 4 | {% block content %} 5 |
6 | Welcome, friend, to the Frangiclave Compendium, the premier source of forbidden knowledge for the discerning occultist.
7 | 8 |The Frangiclave Compendium is an open-source repository for information about the contents of the game Cultist Simulator, as extracted from the game's files. Here you can browse the decks, elements, legacies, recipes and verbs included in the game. All DLC content is also included.
9 | 10 |Source: frangiclave/frangiclave-compendium 11 | 12 |
Hosted by Lyrositor.
13 | 14 |If you'd like to donate, feel free to buy me a coffee.
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /patch/CultistSimulator.Modding.mm/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("CultistSimulator.Modding.mm")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("CultistSimulator.Modding.mm")] 12 | [assembly: AssemblyCopyright("Copyright © 2018")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("E77B3CEF-3B80-4748-A00A-A92659BB281D")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /patch/CultistSimulator.Modding.mm/Texture2DExtensions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace CultistSimulator.Modding.mm 4 | { 5 | /** 6 | * Source: https://answers.unity.com/questions/683772/export-sprite-sheets.html 7 | */ 8 | static class Texture2DExtensions 9 | { 10 | public static Texture2D CropTexture(this Texture2D source, int left, int top, int width, int height) 11 | { 12 | if (left < 0) { 13 | width += left; 14 | left = 0; 15 | } 16 | if (top < 0) { 17 | height += top; 18 | top = 0; 19 | } 20 | if (left + width > source.width) { 21 | width = source.width - left; 22 | } 23 | if (top + height > source.height) { 24 | height = source.height - top; 25 | } 26 | 27 | if (width <= 0 || height <= 0) { 28 | return null; 29 | } 30 | 31 | Color[] sourceColor = source.GetPixels(0); 32 | Texture2D croppedTexture = new Texture2D(width, height, TextureFormat.RGBA32, false); 33 | 34 | int area = width * height; 35 | Color[] pixels = new Color[area]; 36 | 37 | int i = 0; 38 | for (int y = 0; y < height; y++) { 39 | int sourceIndex = (y + top) * source.width + left; 40 | for (int x = 0; x < width; x++) { 41 | pixels[i++] = sourceColor[sourceIndex++]; 42 | } 43 | } 44 | 45 | croppedTexture.SetPixels(pixels); 46 | croppedTexture.Apply(); 47 | 48 | return croppedTexture; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /frangiclave/templates/deck.tpl.html: -------------------------------------------------------------------------------- 1 | {% extends "base.tpl.html" %} 2 | {% import "macros.tpl.html" as m with context %} 3 | {% block title %}Deck: {{ deck.deck_id }}{% endblock %} 4 | {% block description %}{{ deck.description }}{% endblock %} 5 | {% block content %} 6 |Label: {{ m.localised(deck.label) }}
21 | 22 |Description: {{ m.localised(deck.description, True) }}
23 | 24 |Draw Messages: {% if not deck.draw_messages %}None{% endif %}
25 |Default Draw Messages: {% if not deck.default_draw_messages %}None{% endif %}
30 |Cards:
35 |Default Card: {{ m.element(deck.default_card.element_id) }}
40 | 41 |Reset on Exhaustion? {{ m.yes_no(deck.reset_on_exhaustion) }}
42 | 43 |Comments: {{ m.multiline(m.optional(deck.comments)) }}
44 | 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # PyCharm 108 | .idea/ 109 | 110 | # Frangiclave 111 | config.toml 112 | frangiclave.db 113 | frangiclave/static/images 114 | -------------------------------------------------------------------------------- /frangiclave/templates/legacy.tpl.html: -------------------------------------------------------------------------------- 1 | {% extends "base.tpl.html" %} 2 | {% import "macros.tpl.html" as m with context %} 3 | {% block title %}Legacy: {{ legacy.legacy_id }}{% endblock %} 4 | {% block description %}{{ legacy.description }}{% endblock %} 5 | {% block content %} 6 |Label: {{ m.localised(legacy.label) }}
23 | 24 |Start Description: {{ m.localised(legacy.start_description, True) }}
25 | 26 |Description: {{ m.localised(legacy.description, True) }}
27 | 28 |From Ending: {{ legacy.from_ending }}
29 | 30 |Excludes on Ending: {% if not legacy.excludes_on_ending %}None{% endif %}
31 |Available Without Ending Match? {{ m.yes_no(legacy.available_without_ending_match) }}
34 | 35 |Starting Verb ID: {{ m.optional(legacy.starting_verb_id) }}
36 | 37 |Legacy Effect: {{ m.element_list(legacy.effects) }}
38 | 39 |Comments: {{ m.multiline(m.optional(legacy.comments)) }}
40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /frangiclave/compendium/ending.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict, Any 2 | 3 | from sqlalchemy import Column, Enum as EnumType, String, Integer 4 | 5 | from frangiclave.compendium.base import Base, Session 6 | from frangiclave.compendium.ending_flavour import EndingFlavour 7 | from frangiclave.compendium.file import File 8 | from frangiclave.compendium.game_content import GameContents 9 | from frangiclave.compendium.utils import get 10 | 11 | 12 | class Ending(Base): 13 | __tablename__ = 'endings' 14 | 15 | id = Column(Integer, primary_key=True) 16 | 17 | ending_id: int = Column(String, unique=True) 18 | title: Optional[str] = Column(String, nullable=True) 19 | description: str = Column(String) 20 | image: str = Column(String) 21 | flavour: EndingFlavour = Column(EnumType(EndingFlavour, name='flavour')) 22 | animation: str = Column(String) 23 | achievement: Optional[str] = Column(String, nullable=True) 24 | 25 | @classmethod 26 | def from_data( 27 | cls, 28 | file: File, 29 | data: Dict[str, Any], 30 | translations: Dict[str, Dict[str, Any]], 31 | game_contents: GameContents 32 | ) -> 'Ending': 33 | e = game_contents.get_ending(get(data, 'id')) 34 | e.file = file 35 | e.title = get(data, 'label', translations=translations) 36 | e.description = get(data, 'description', translations=translations) 37 | e.image = get(data, 'image') 38 | flavour = get(data, 'flavour', 'None') 39 | e.flavour = EndingFlavour( 40 | flavour[0].upper() + flavour[1:] # Workaround for a broken ending 41 | ) 42 | e.animation = get(data, 'anim') 43 | e.achievement = get(data, 'achievement') 44 | return e 45 | 46 | @classmethod 47 | def get_by_ending_id(cls, session: Session, ending_id: str) -> 'Ending': 48 | return session.query(cls).filter(cls.ending_id == ending_id).one() 49 | -------------------------------------------------------------------------------- /frangiclave/bot/templates/search_results.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | from typing import Any, Dict, List 3 | 4 | from frangiclave.bot.templates.base import make_section, DIVIDER, IMAGE_FORMAT, \ 5 | URL_FORMAT, get_image_if_reachable 6 | from frangiclave.bot.templates.element import get_element_art 7 | from frangiclave.bot.templates.ending import get_ending_art 8 | from frangiclave.bot.templates.legacy import get_legacy_art 9 | from frangiclave.bot.templates.verb import get_verb_art 10 | from frangiclave.compendium.base import get_session 11 | from frangiclave.compendium.element import Element 12 | from frangiclave.compendium.ending import Ending 13 | from frangiclave.compendium.legacy import Legacy 14 | from frangiclave.compendium.verb import Verb 15 | 16 | MAX_RESULTS = 5 17 | 18 | 19 | def make_search_results(keywords: str, results: List[Dict[str, Any]]): 20 | blocks = [ 21 | _make_header(len(results), keywords), 22 | ] 23 | if results: 24 | blocks.append(DIVIDER) 25 | for result in results[:MAX_RESULTS]: 26 | blocks += [_make_result(result), DIVIDER] 27 | return blocks 28 | 29 | 30 | def _make_header(num_results: int, keywords: str): 31 | text = 'Found *{} results* for *{}*.'.format(num_results, keywords) 32 | if num_results > MAX_RESULTS: 33 | text += ' Only showing first {} results.'.format(MAX_RESULTS) 34 | return make_section(text) 35 | 36 | 37 | def _make_result(result: Dict[str, Any]): 38 | text = "*{}: {}*\n{}".format( 39 | result['type'].capitalize(), 40 | URL_FORMAT.format(result['type'], result['id']), 41 | '\n'.join('• ' + m for m in result['matches']) 42 | ) 43 | image = None 44 | image_alt = None 45 | with get_session() as session: 46 | if result['type'] == 'element': 47 | element = Element.get_by_element_id(session, result['id']) 48 | image = get_element_art(element) 49 | elif result['type'] == 'legacy': 50 | legacy = Legacy.get_by_legacy_id(session, result['id']) 51 | image = get_legacy_art(legacy) 52 | elif result['type'] == 'verb': 53 | verb = Verb.get_by_verb_id(session, result['id']) 54 | image = get_verb_art(verb) 55 | elif result['type'] == 'ending': 56 | ending = Ending.get_by_ending_id(session, result['id']) 57 | image = get_ending_art(ending) 58 | 59 | image = get_image_if_reachable(image) 60 | if image is not None: 61 | image_alt = result['id'] 62 | 63 | return make_section(text, image, image_alt) 64 | -------------------------------------------------------------------------------- /frangiclave/compendium/linked_recipe_details.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Dict, Type, List, Union 2 | 3 | from sqlalchemy import Column, ForeignKey, Integer, Boolean, String 4 | from sqlalchemy.ext.declarative import declared_attr 5 | from sqlalchemy.orm import relationship 6 | 7 | from frangiclave.compendium.game_content import GameContents 8 | from frangiclave.compendium.utils import get, to_bool 9 | 10 | 11 | if TYPE_CHECKING: 12 | from frangiclave.compendium.element import Element 13 | from frangiclave.compendium.recipe import Recipe 14 | 15 | 16 | class LinkedRecipeDetails: 17 | 18 | chance: int = Column(Integer) 19 | additional: bool = Column(Boolean) 20 | 21 | @declared_attr 22 | def recipe_id(self) -> Column: 23 | return Column(Integer, ForeignKey('recipes.id')) 24 | 25 | @declared_attr 26 | def recipe(self) -> 'Recipe': 27 | return relationship('Recipe', foreign_keys=self.recipe_id) 28 | 29 | @classmethod 30 | def from_data( 31 | cls, 32 | data: Dict[str, Any], 33 | challenge_cls: Type['LinkedRecipeChallengeRequirement'], 34 | game_contents: GameContents 35 | ) -> 'LinkedRecipeDetails': 36 | lr = cls() 37 | lr.recipe = game_contents.get_recipe(data['id']) 38 | lr.chance = int(data['chance']) if 'chance' in data else 100 39 | lr.additional = get(data, 'additional', False, to_bool) 40 | lr.challenges = challenge_cls.from_data( 41 | get(data, 'challenges', {}), game_contents 42 | ) 43 | return lr 44 | 45 | 46 | class LinkedRecipeChallengeRequirement: 47 | 48 | id = Column(Integer, primary_key=True) 49 | 50 | @declared_attr 51 | def element_id(self) -> Column: 52 | return Column(Integer, ForeignKey('elements.id')) 53 | 54 | @declared_attr 55 | def element(self) -> 'Element': 56 | return relationship('Element') 57 | 58 | convention = Column(String) 59 | 60 | @classmethod 61 | def from_data( 62 | cls, val: Union[str, Dict[str, str]], game_contents: GameContents 63 | ) -> List['LinkedRecipeChallengeRequirement']: 64 | return [ 65 | cls( 66 | element=game_contents.get_element(element_id), 67 | convention=convention 68 | ) 69 | for element_id, convention in val.items() 70 | ] if isinstance(val, dict) else [ 71 | cls( 72 | element=game_contents.get_element(val), 73 | convention='base' 74 | ) 75 | ] 76 | -------------------------------------------------------------------------------- /frangiclave/compendium/verb.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | 3 | from sqlalchemy import Column, Integer, String, Boolean, ForeignKey 4 | from sqlalchemy.orm import relationship 5 | 6 | from frangiclave.compendium.base import Base, Session 7 | from frangiclave.compendium.file import File 8 | from frangiclave.compendium.game_content import GameContentMixin, GameContents 9 | from frangiclave.compendium.slot_specification import SlotSpecification 10 | from frangiclave.compendium.utils import to_bool, get 11 | 12 | 13 | class Verb(Base, GameContentMixin): 14 | __tablename__ = 'verbs' 15 | 16 | id = Column(Integer, primary_key=True) 17 | verb_id: str = Column(String, unique=True) 18 | 19 | label: Optional[str] = Column(String, nullable=True) 20 | description: Optional[str] = Column(String, nullable=True) 21 | at_start: bool = Column(Boolean) 22 | primary_slot_specification_id: int = Column( 23 | Integer, ForeignKey('verbs_slot_specifications.id') 24 | ) 25 | primary_slot_specification: Optional['VerbSlotSpecification'] = \ 26 | relationship( 27 | 'VerbSlotSpecification', 28 | foreign_keys=primary_slot_specification_id 29 | ) 30 | recipes = relationship('Recipe', back_populates='action') 31 | comments: Optional[str] = Column(String, nullable=True) 32 | 33 | @classmethod 34 | def from_data( 35 | cls, 36 | file: File, 37 | data: Dict[str, Any], 38 | translations: Dict[str, Dict[str, Any]], 39 | game_contents: GameContents 40 | ): 41 | r = game_contents.get_verb(data['id']) 42 | r.file = file 43 | r.label = get(data, 'label', translations=translations) 44 | r.description = get(data, 'description', translations=translations) 45 | r.at_start = get(data, 'atStart', False, to_bool) 46 | if 'slots' in data and data['slots']: 47 | r.primary_slot_specification = VerbSlotSpecification.from_data( 48 | data['slots'][0], 49 | { 50 | c: c_transformation["slots"][0] 51 | for c, c_transformation in translations.items() 52 | if "slots" in c_transformation 53 | }, 54 | game_contents 55 | ) 56 | r.comments = get(data, 'comments', None) 57 | return r 58 | 59 | @classmethod 60 | def get_by_verb_id(cls, session: Session, verb_id: str) -> 'Verb': 61 | return session.query(cls).filter(cls.verb_id == verb_id).one() 62 | 63 | 64 | class VerbSlotSpecification(Base, SlotSpecification): 65 | __tablename__ = 'verbs_slot_specifications' 66 | 67 | id = Column(Integer, primary_key=True) 68 | -------------------------------------------------------------------------------- /patch/CultistSimulator.Modding.mm/ResourcesManager.cs: -------------------------------------------------------------------------------- 1 | using MonoMod; 2 | using System.IO; 3 | using Noon; 4 | using UnityEngine; 5 | 6 | #pragma warning disable CS0626 7 | 8 | namespace CultistSimulator.Modding.mm 9 | { 10 | [MonoModPatch("global::ResourcesManager")] 11 | public class ResourcesManager 12 | { 13 | public static extern Sprite orig_GetSpriteForVerbLarge(string verbId); 14 | 15 | public static Sprite GetSpriteForVerbLarge(string verbId) 16 | { 17 | return GetSprite("icons100/verbs/", verbId); 18 | } 19 | 20 | public static extern Sprite orig_GetSpriteForElement(string imageName); 21 | 22 | public static Sprite GetSpriteForElement(string imageName) 23 | { 24 | return GetSprite("elementArt/", imageName); 25 | } 26 | 27 | public static extern Sprite orig_GetSpriteForElement(string imageName, int animFrame); 28 | 29 | public static Sprite GetSpriteForElement(string imageName, int animFrame) 30 | { 31 | return GetSprite("elementArt/anim/", string.Concat(imageName, "_", animFrame)); 32 | } 33 | 34 | public static extern Sprite orig_GetSpriteForCardBack(string backId); 35 | 36 | public static Sprite GetSpriteForCardBack(string backId) 37 | { 38 | return GetSprite("cardBacks/", backId); 39 | } 40 | 41 | public static extern Sprite orig_GetSpriteForAspect(string aspectId); 42 | 43 | public static Sprite GetSpriteForAspect(string aspectId) 44 | { 45 | return GetSprite("icons40/aspects/", aspectId); 46 | } 47 | 48 | public static extern Sprite orig_GetSpriteForLegacy(string legacyImage); 49 | 50 | public static Sprite GetSpriteForLegacy(string legacyImage) 51 | { 52 | return GetSprite("icons100/legacies/", legacyImage); 53 | } 54 | 55 | public static extern Sprite orig_GetSpriteForEnding(string endingImage); 56 | 57 | public static Sprite GetSpriteForEnding(string endingImage) 58 | { 59 | return GetSprite("endingArt/en/", endingImage); 60 | } 61 | 62 | private static Sprite GetSprite(string folder, string file) 63 | { 64 | NoonUtility.Log("Loading " + folder + file); 65 | // Check if a local image exists; if it does, load it first 66 | string localPath = Application.streamingAssetsPath + "/" + folder + file + ".png"; 67 | if (File.Exists(localPath)) 68 | { 69 | var fileData = File.ReadAllBytes(localPath); 70 | var texture = new Texture2D(2, 2); 71 | texture.LoadImage(fileData); 72 | return Sprite.Create( 73 | texture, new Rect(0.0f, 0.0f, texture.width, texture.height), new Vector2(0.5f, 0.5f)); 74 | } 75 | 76 | // Try to load the image from the packed resources next, and show the placeholder if not found 77 | Sprite sprite = Resources.Load
41 |
22 | {% endif %}
23 |
24 | Label: {{ m.localised(recipe.label) }}
25 | 26 |Start Description: {{ m.localised(recipe.start_description) }}
27 | 28 |Description: {{ m.localised(recipe.description, True) }}
29 | 30 |Action: {% if recipe.action_id %}{{ m.verb(recipe.action.verb_id) }}{% else %}None{% endif %}
31 | 32 |Requirements: {{ m.element_list(recipe.requirements) }}
33 | 34 |Table Requirements: {{ m.element_list(recipe.table_requirements) }}
35 | 36 |Extant Requirements: {{ m.element_list(recipe.extant_requirements) }}
37 | 38 |Effects: {{ m.element_list(recipe.effects) }}
39 | 40 |Aspects: {{ m.element_list(recipe.aspects) }}
41 | 42 |Mutation Effects: {% if not recipe.mutation_effects %}None{% endif %}
43 |Purge: {{ m.element_list(recipe.purge) }}
52 | 53 |Halt Verbs: {% if not recipe.halt_verb %}None{% endif %}
54 |Delete Verbs: {% if not recipe.delete_verb %}None{% endif %}
57 |Alternative Recipes: {% if not recipe.alternative_recipes %}Nothing{% endif %}
60 | {{ m.linked_recipe_details(recipe.alternative_recipes) }} 61 | 62 |Linked Recipes: {% if not recipe.linked_recipes %}Nothing{% endif %}
63 | {{ m.linked_recipe_details(recipe.linked_recipes) }} 64 | 65 |From Recipes: {% if not recipe.from_recipes %}Nothing{% endif %}
66 |Slots: {% if not recipe.slot_specifications %}None{% endif %}
69 | {{ m.slot_specifications(recipe.slot_specifications) }} 70 | 71 |Warmup: {{ recipe.warmup }}
72 | 73 |Maximum Executions: {{ recipe.max_executions }}
74 | 75 |Deck Effect: {% if recipe.deck_effect %}
Internal Deck: {{ m.deck(recipe.internal_deck.deck_id) }}
78 | 79 |Ending Flag: {{ m.ending(recipe.ending_flag) }}
80 | 81 |Signal Ending Flavour: {{ recipe.signal_ending_flavour.value }}
82 | 83 |Portal Effect: {{ recipe.portal_effect.value }}
84 | 85 |Craftable? {{ m.yes_no(recipe.craftable) }}
86 | 87 |Hint Only? {{ m.yes_no(recipe.hint_only) }}
88 | 89 |Signal Important Loop? {{ m.yes_no(recipe.signal_important_loop) }}
90 | 91 |Comments: {{ m.multiline(m.optional(recipe.comments)) }}
92 | 93 | {% endblock %} 94 | -------------------------------------------------------------------------------- /frangiclave/templates/element.tpl.html: -------------------------------------------------------------------------------- 1 | {% extends "base.tpl.html" %} 2 | {% import "macros.tpl.html" as m with context %} 3 | {% block title %}Element: {{ element.element_id }}{% endblock %} 4 | {% block description %}{{ element.description }}{% endblock %} 5 | {% block content %} 6 |Label: {{ m.localised(element.label) }}
26 | 27 |Description: {{ m.localised(element.description, True) }}
28 | 29 |Aspects: {{ m.aspect_list(element.aspects) }}
30 | 31 |Induces: {% if not element.induces %}Nothing{% endif %}
32 | {{ m.linked_recipe_details(element.induces) }} 33 | 34 |Slots: {% if not element.child_slots %}None{% endif %}
35 | {{ m.slot_specifications(element.child_slots) }} 36 | 37 |Cross Triggers: {% if not element.x_triggers %}None{% endif %}
38 |Triggered By: {% if not element.triggered_by %}None{% endif %}
41 |Triggered With: {% if not element.triggered_with %}None{% endif %}
44 |Requirement for Recipes: {% if not element.requirement_for_recipes %}None{% endif %}
47 |Effect of Recipes: {% if not element.effect_of_recipes %}None{% endif %}
50 |Lifetime: {% if element.lifetime %}{{ element.lifetime }}{% else %}None{% endif %}
53 | 54 |Decay To: {{ m.element(element.decay_to.element_id) }}
55 | 56 |Decay From: {% if not element.decay_from %}None{% endif %}
57 |Aspect? {% if element.is_aspect %}Yes{% else %}No{% endif %}
60 |Unique? {% if element.unique %}Yes{% else %}No{% endif %}
63 | 64 |Uniqueness Group: {{ m.optional(element.uniqueness_group) }}
65 | 66 |Hidden? {{ m.yes_no(element.is_hidden) }}
67 | 68 |No Art Needed? {{ m.yes_no(element.no_art_needed) }}
69 | 70 |Resaturate? {{ m.yes_no(element.resaturate) }}
71 | 72 |Override Verb Icon: {{ m.optional(element.verb_icon) }}
73 | 74 |Animation Frames: {{ element.animation_frames }}
75 | 76 |In Decks: {% if not element.in_decks and not element.in_decks_default %}None{% endif %}
77 |Comments: {{ m.optional(element.comments) }}
80 | {% endblock %} 81 | 82 | -------------------------------------------------------------------------------- /frangiclave/game/importer.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import platform 3 | from typing import Type, Iterable, Dict, Any, List, Tuple 4 | 5 | from jsom import JsomParser, ALL_WARNINGS 6 | 7 | from frangiclave.compendium.base import get_session, Base 8 | from frangiclave.compendium.deck import Deck 9 | from frangiclave.compendium.element import Element 10 | from frangiclave.compendium.ending import Ending 11 | from frangiclave.compendium.file import File, FileCategory, FileGroup 12 | from frangiclave.compendium.game_content import GameContentMixin, GameContents 13 | from frangiclave.compendium.legacy import Legacy 14 | 15 | from frangiclave.compendium.recipe import Recipe 16 | from frangiclave.compendium.verb import Verb 17 | 18 | DATA_DIR = ( 19 | 'cultistsimulator_Data' if platform.system() == 'Windows' else 'CS_Data' 20 | ) 21 | 22 | ADDITIONAL_CULTURES = ('ru', 'zh-hans') 23 | 24 | jsom = JsomParser(ignore_warnings=ALL_WARNINGS) 25 | 26 | 27 | def import_game_data(game_dir: str): 28 | assets_dir = Path(game_dir)/DATA_DIR/'StreamingAssets' 29 | content_dir = assets_dir/'content' 30 | with get_session() as session: 31 | 32 | # Load the content from the regular files 33 | game_contents = GameContents() 34 | for group in FileGroup: 35 | decks = _load_content( 36 | Deck, 37 | content_dir, 38 | group, 39 | FileCategory.DECKS, 40 | game_contents 41 | ) 42 | elements = _load_content( 43 | Element, 44 | content_dir, 45 | group, 46 | FileCategory.ELEMENTS, 47 | game_contents 48 | ) 49 | endings = _load_content( 50 | Ending, 51 | content_dir, 52 | group, 53 | FileCategory.ENDINGS, 54 | game_contents 55 | ) 56 | legacies = _load_content( 57 | Legacy, 58 | content_dir, 59 | group, 60 | FileCategory.LEGACIES, 61 | game_contents 62 | ) 63 | recipes = _load_content( 64 | Recipe, 65 | content_dir, 66 | group, 67 | FileCategory.RECIPES, 68 | game_contents 69 | ) 70 | verbs = _load_content( 71 | Verb, 72 | content_dir, 73 | group, 74 | FileCategory.VERBS, 75 | game_contents 76 | ) 77 | 78 | # Create the dynamically generated secondary tables 79 | Base.metadata.create_all() 80 | 81 | session.add_all(decks) 82 | session.add_all(elements) 83 | session.add_all(endings) 84 | session.add_all(legacies) 85 | session.add_all(recipes) 86 | session.add_all(verbs) 87 | 88 | 89 | def _load_content( 90 | content_class: Type[GameContentMixin], 91 | content_dir: Path, 92 | group: FileGroup, 93 | category: FileCategory, 94 | game_contents: GameContents 95 | ) -> List[GameContentMixin]: 96 | content = [] 97 | for file_name, file_data in _load_json_data( 98 | content_dir/group.value/category.value 99 | ): 100 | file = File(category, group, str(file_name)) 101 | 102 | translations = {} 103 | for culture in ADDITIONAL_CULTURES: 104 | localised_path = content_dir/f'{group.value}_{culture}'/category.value/str(file_name) 105 | if not localised_path.exists(): 106 | continue 107 | with localised_path.open(encoding='utf-8') as f: 108 | translations[culture] = { 109 | e["id"]: e for e in jsom.load(f)[category.value] 110 | } 111 | 112 | for i, data in enumerate(file_data[category.value]): 113 | entity_id = data["id"] 114 | entity_translations = {} 115 | for culture, entities in translations.items(): 116 | if entity_id in entities: 117 | entity_translations[culture] = entities[entity_id] 118 | content.append(content_class.from_data( 119 | file, data, entity_translations, game_contents 120 | )) 121 | return content 122 | 123 | 124 | def _load_json_data( 125 | category_dir: Path 126 | ) -> Iterable[Tuple[Path, Dict[str, Any]]]: 127 | for json_file_path in sorted(category_dir.glob('*.json')): 128 | with json_file_path.open(encoding='utf-8') as f: 129 | yield json_file_path.name, jsom.load(f) 130 | -------------------------------------------------------------------------------- /frangiclave/bot/client.py: -------------------------------------------------------------------------------- 1 | import time 2 | import traceback 3 | from typing import Any, Dict, List, Optional 4 | 5 | from slackclient import SlackClient 6 | 7 | from frangiclave.bot.templates.deck import make_deck 8 | from frangiclave.bot.templates.element import make_element 9 | from frangiclave.bot.templates.ending import make_ending 10 | from frangiclave.bot.templates.legacy import make_legacy 11 | from frangiclave.bot.templates.recipe import make_recipe 12 | from frangiclave.bot.templates.verb import make_verb 13 | from frangiclave.bot.templates.search_results import make_search_results 14 | from frangiclave.compendium.base import get_session 15 | from frangiclave.compendium.deck import Deck 16 | from frangiclave.compendium.element import Element 17 | from frangiclave.compendium.ending import Ending 18 | from frangiclave.compendium.legacy import Legacy 19 | from frangiclave.compendium.recipe import Recipe 20 | from frangiclave.compendium.verb import Verb 21 | from frangiclave.search import search_compendium, search_compendium_by_id 22 | 23 | RTM_READ_DELAY = 0.5 24 | PROFILE_PICTURE = \ 25 | 'https://www.frangiclave.net/static/images/elementArt/toolknockf.png' 26 | 27 | 28 | class BotClient: 29 | 30 | def __init__(self, slack_bot_token: str): 31 | self.slack_client = SlackClient(slack_bot_token) 32 | connected = self.slack_client.rtm_connect(auto_reconnect=True) 33 | if not connected: 34 | raise Exception('Failed to connect to Slack workspace') 35 | self.bot_id = self.slack_client.api_call('auth.test')['user_id'] 36 | 37 | def process_event( 38 | self, 39 | event: Dict[str, Any] 40 | ): 41 | if 'bot_id' in event and event['bot_id'] == self.bot_id: 42 | return 43 | if event['type'] == 'message': 44 | if 'subtype' in event and event['subtype'] in ( 45 | 'bot_message', 'message_changed' 46 | ): 47 | return 48 | text = event['text'] 49 | if text.startswith('?'): 50 | bits = text.split(maxsplit=1) 51 | command = bits[0][1:] 52 | params = bits[1] if len(bits) > 1 else None 53 | self.process_command(event, command, params) 54 | 55 | def process_command( 56 | self, 57 | event: Dict[str, Any], 58 | command: str, 59 | params: str 60 | ): 61 | if command in ('', 'g', 'get'): 62 | with get_session() as session: 63 | result = search_compendium_by_id(session, params.strip()) 64 | if not result: 65 | self.reply(event, 'No results found.') 66 | elif isinstance(result, Deck): 67 | self.reply(event, None, make_deck(result)) 68 | elif isinstance(result, Element): 69 | self.reply(event, None, make_element(result)) 70 | elif isinstance(result, Legacy): 71 | self.reply(event, None, make_legacy(result)) 72 | elif isinstance(result, Recipe): 73 | self.reply(event, None, make_recipe(result)) 74 | elif isinstance(result, Verb): 75 | self.reply(event, None, make_verb(result)) 76 | elif isinstance(result, Ending): 77 | self.reply(event, None, make_ending(result)) 78 | elif command in ('s', 'search'): 79 | keywords = params.strip() 80 | with get_session() as session: 81 | results = search_compendium(session, keywords) 82 | self.reply(event, None, make_search_results(keywords, results)) 83 | 84 | def reply( 85 | self, 86 | event: Dict[str, Any], 87 | text: Optional[str] = None, 88 | blocks: List[Dict[str, Any]] = None 89 | ): 90 | result = self.slack_client.api_call( 91 | 'chat.postMessage', 92 | channel=event['channel'], 93 | text=text, 94 | icon_url=PROFILE_PICTURE, 95 | thread_ts=event['thread_ts'] if 'thread_ts' in event else None, 96 | blocks=blocks 97 | ) 98 | if not result['ok']: 99 | print('Failed to reply: {}'.format(result)) 100 | 101 | def run(self): 102 | while self.slack_client.server.connected: 103 | events = self.slack_client.rtm_read() 104 | for event in events: 105 | # noinspection PyBroadException 106 | try: 107 | self.process_event(event) 108 | except: 109 | print('Failed to process event:', event) 110 | traceback.print_exc() 111 | time.sleep(RTM_READ_DELAY) 112 | -------------------------------------------------------------------------------- /frangiclave/templates/macros.tpl.html: -------------------------------------------------------------------------------- 1 | {% macro deck(id) -%}{{ optional_link('/deck/', id) }}{%- endmacro %} 2 | {% macro element(id) -%}{{ optional_link('/element/', id) }}{%- endmacro %} 3 | {% macro ending(id) -%}{{ optional_link('/ending/', id) }}{%- endmacro %} 4 | {% macro legacy(id) -%}{{ optional_link('/legacy/', id) }}{%- endmacro %} 5 | {% macro recipe(id) -%}{{ optional_link('/recipe/', id) }}{%- endmacro %} 6 | {% macro verb(id) -%}{{ optional_link('/verb/', id) }}{%- endmacro %} 7 | 8 | {% macro element_quantity(element, quantity) -%}{{ quantity }}{% if element.is_aspect %}{{ icons40(element) }}{% else %}{{ elementArt(element) }}{% endif %}{%- endmacro %} 9 | 10 | 11 | {% macro image(folder, element) -%}