Code Monkey home page Code Monkey logo

pf2e-encounters's Introduction

PF2e Encounter builder

This tool helps building encounters for the popular RPG system, you can browse a list of all creatures or the ones matching the specified criteria, creatures added to the encounter can also be modified to account for their Weak and Elite variants other than their base stat block. Please refer to the Core Rulebook for the rules.

Why

There are already several similar tools available, however, none that I found offered the choice for Weak and Elite variants which is an very handy rule to balance power in an encounter and often oversighted, furthermore they all lacked fine grained filters.

I also wanted to improve my Javascript and Quasar skills, thus this tool was born out of necessity and "sport".


Do you like my work? Help me dedicate more time to my passion by supporting, no matter the size every cup of coffee brings me to the desk!

Buy Me A Coffee

pf2e-encounters's People

Contributors

imgurbot12 avatar jeffreyzacher avatar jocarnail avatar maxiride avatar sevzero avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

pf2e-encounters's Issues

Export/import from file

It could be nice to have the option to export and import encounters from a file.

"exporting" would be the same as downloading the file. And "importing" would be uploading a file.

It could use a simple json format like one used with browser storage so both task could reuse part of the code.

This could be used as a more secure way to store encounters for the long term as browser's storage could be erased by accident more easily.

Add a license

  • License for the project code
  • License reference for the project content (aka the creatures data)

Add view stats to selected monsters

Discussed in #37

Originally posted by masterman15 August 27, 2021
Could you add the 'view stats' magnifying glass button to the monsters selected on the right side of the screen? This would mean I wouldn't have to go back to searching for creatures after I select them if I want to see their stats.

Great tool.

Sorting by Level weirdness

Since early levels are single digit, sorting by level creates an awkward assortment where levels 10 through 19 appear between levels 1 and 2, and level 20+ appear between 2 and 3. Making all levels 2 digits (-01, 00, 01, etc) might fix that, but I don't know how the data is generated, and thus have no idea how feasible that would be considering the breadth of the full bestiary.

Sharable encounter links

We could assemble an URL encoded string to pre fill the encounter details. Thus allowing sharing and bookmarking encounter setups.

Filter exclude

Add feature to exclude elements from search/filter. Example is in the drop down menus here in GitHub (alt-click to exclude)

Hotfix: Missing Monsters/NPCs

Hello! Love this project and I'm sad to see it hasn't been updated in a while, but I managed a pseudo-hotfix to include all of the most recent monsters/npcs if anyone is interested.

I wrote this quick python script to scrape the contents from aonprd's elasticsearch instance by reverse engineering their api-calls. It should hopefully generate a complete manifest of all available monsters/npcs. Hope it's useful for future updates in regards to fixing the current scraper implementation and helpful to anyone using this project in the future.

For any users, the generated creatures.json file just needs to be placed within src/webui/public/ folder.
The output could potentially just be added to the existing gh-pages site in order to include all of the current existing content.

"""
Monsters JSON Scraper
"""
import os
import json
import dataclasses
from dataclasses import dataclass, field
from typing import List, Set

import requests

#** Variables **#

#: user-agent to use when making requests
USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36'

#: cache for raw-npcs results
NPCS_CACHE = 'npcs-raw.json'

#: cache for raw-monsters results
MONSTERS_CACHE = 'monsters-raw.json'

#: aeonprd hostname
AEONPRD = '2e.aonprd.com'

#: aeonprd elatasearch hostname
AEONPRD_ELASTIC = 'https://elasticsearch.aonprd.com/aon/_search'

#: pre-calculated aonprd elastic lookup query-params
ELASTIC_PARAM = {'track_total_hits': 'true'}

#: pre-calculated headers to include during elasticsearch lookup
ELASTIC_HEADERS = {
    'user-agent':     USER_AGENT,
    'accept':         '*/*',
    'origin':         f'https://{AEONPRD}',
    'referer':        f'https://{AEONPRD}',
    'sec-fetch-dest': 'empty',
    'sec-fetch-mode': 'cors',
    'sec-fetch-site': 'same-site',
    'sec-gpc':        1,
}

#** Functions **#

def creature_data(npcs: bool = False) -> dict:
    """retrieve raw monster-data from aonprd elasticsearch"""
    # retrieve from file-cache
    cache = NPCS_CACHE if npcs else MONSTERS_CACHE
    if os.path.exists(cache):
        with open(cache, 'r') as f:
            return json.load(f)
    # construct query json 
    json_post = {
        "query":{
            "function_score":{
                "query":{
                    "bool":{
                        "filter":[
                            {"query_string":{
                                "query":f"category:creature npc:{str(npcs).lower()} ",
                                "default_operator":"AND",
                                "fields":["name","text^0.1","trait_raw","type"]
                            }}
                        ],
                        "must_not":[{"term":{"exclude_from_search":True}}]
                    }
                },
                "boost_mode":"multiply",
                "functions":[
                    {"filter":{"terms":{"type":["Ancestry","Class"]}},"weight":1.1},
                    {"filter":{"terms":{"type":["Trait"]}},"weight":1.05}
                ]
            }
        },
        "size":    10000,
        "sort":    ["_score","_doc"],
        "_source": {"excludes":["text"]},
        "aggs":    {
            "group1":{
                "composite":{
                    "sources":[{"field1":{"terms":{"field":"type","missing_bucket":True}}}],
                    "size":10000
                }
            }
        }
    }
    # make request and write to cache
    res = requests.post(AEONPRD_ELASTIC, params=ELASTIC_PARAM, json=json_post)
    with open(cache, 'wb') as f:
        f.write(res.content)
    return res.json()

def parse_creatures(data: dict) -> List['Creature']:
    """parse through raw monster data to retrieve imporant details"""
    # parse creatures from raw data
    creatures = []
    for hit in data['hits']['hits']:
        attrs   = hit['_source']
        name    = attrs['name']
        id      = attrs['url'].rsplit('=', 1)[1]
        if not id.isdigit():
            raise RuntimeError(f'failed to parse {name!r}')
        npc      = attrs.get('npc', False)
        align    = attrs['alignment']
        traits   = set(attrs['trait']) 
        images   = attrs.get('image', [])
        creature = 'NPC' if npc else attrs['type'] 
        if align in traits:
            traits.remove(align)
        creatures.append(Creature(
            id=id,
            name=name,
            level=attrs['level'],
            alignment=align,
            creature_type=creature,
            size=attrs['size'][0],
            rarity=attrs['rarity'],
            lore=attrs.get('summary', ''),
            family=attrs.get('creature_family', ''),
            image_url=images[0] if images else '',
            npc=npc,
            traits=traits,
        ))
    return creatures

def generate_data(creatures: List['Creature']) -> 'Data':
    """generate metadata and pass into data object"""
    # generate meta-data from collected monsters
    meta = Metadata(total=len(creatures))
    for creature in creatures:
        meta.min_level = min(meta.min_level, creature.level)
        meta.max_level = max(meta.max_level, creature.level)
        meta.alignments.add(creature.alignment)
        meta.creature_types.add(creature.creature_type)
        meta.rarities.add(creature.rarity)
        meta.sizes.add(creature.size)
        meta.traits |= creature.traits
        if creature.family:
            meta.families.add(creature.family)
    return Data(creatures, meta)

#** Classes **#

@dataclass
class Metadata:
    min_level:      int       = 0
    max_level:      int       = 0 
    total:          int       = 0
    families:       Set[str] = field(default_factory=set)
    alignments:     Set[str] = field(default_factory=set)
    creature_types: Set[str] = field(default_factory=set)
    traits:         Set[str] = field(default_factory=set)
    rarities:       Set[str] = field(default_factory=set)
    sizes:          Set[str] = field(default_factory=set)

@dataclass
class Creature:
    id:            str
    name:          str
    creature_type: str
    level:         int
    alignment:     str
    size:          str
    rarity:        str
    lore:          str
    family:        str
    image_url:     str
    npc:           bool     = False
    traits:        Set[str] = field(default_factory=set)

@dataclass(repr=False)
class Data:
    creatures: List[Creature] = field(default_factory=list)
    metadata:  Metadata       = field(default_factory=Metadata)

    def __repr__(self) -> str:
        return f'Creatures(found={len(self.creatures)}, meta={self.metadata!r})'

class JsonEncoder(json.JSONEncoder):
    """custom json encoder to convert sets to supported list-type"""
    def default(self, obj):
        if isinstance(obj, set):
            return sorted(list(obj))
        if dataclasses.is_dataclass(obj):
            return dataclasses.asdict(obj)
        return super().default(obj)

#** Init **#

data     = creature_data(npcs=False)
monsters = parse_creatures(data)

data = creature_data(npcs=True)
npcs = parse_creatures(data)

collected = generate_data(monsters + npcs)
with open('creatures.json', 'w') as f:
    json.dump(collected, f, cls=JsonEncoder)

Edit: a few quality of life changes like sorting the sets once back into a list and making NPCs a different creature-type

Show exact values for encounter XP and creatures XP cost

Currently the progress bar doesn't show the exact value of the XP of the encounter:

  • add a label where to show the exact amount for fine tuning

It would also be convenient to know the XP cost of a creature added to the encounter, show sum for each variant

Add dummy creatures to encounter

It would be useful to have the ability to add dummy creatures (with just a level and custom name) to the encounter panel, so that homebrew creatures can be used in this encounter builder, without having to add the ability to add custom creatures into the search area.

Show Traits in the table

Traits are an array of values, hence it's needed a custom override of that specific column for proper rendering

image

Favicon

Would be nice to have a favicon other than the default Quasar one.

Add random creature given a set of filters

Will just copy paste the comment from reddit https://www.reddit.com/r/Pathfinder_RPG/comments/kwkpwz/a_more_comprehensive_pathfinder_2e_encounter/gj4wwus?utm_source=share&utm_medium=web2x&context=3:

Great tool, this will definitely be my new go to. I do have two items to bring up though (of minor concern):
Double clicking wasn't immediately obvious despite it being in the tour, maybe a tooltip or something the first time you try clicking and waiting?
It'd be nice to have a quick-add for a creature of X level. Like a button that just puts a level X monster in there where you can adjust the level freely.

I'll take the input and try to brainstorm what can be done, a tooltip as you suggested might work, maybe with a little delay to avoid it being too invasive
Interesting feature, maybe it could be paired with "a random creature from the current result set" other than level alone.
To elaborate more the input is the desired level (it could be a range too why not! =) ) and then a random creature gets picked up, (and here I add my new idee built upon yours) optionally picking it up from the current result set.


Idea

  • Pick a random creature from the available choices in the table which is updated with the given filters essentially realizing the feature: "pick a random creature given certain filters"

Monster exp value cap.

The value of the monster does not change by changing it to elite or weak if the monster is jet over 4+ lv over the party. It may be appropriate to have a distinction that marks that it is not because the monster is "just" and extreme challenge, but beyond.
Eg: You put a dragon og challenge 10 to a grup of lv 1 pcs.

API brainstorming

What if an application knows the party size and PC levels and want to estimate the difficulty of the encounter?

Implementing a basic JSON api that takes in the party data and some monster's ID and outputs the challenge level should be trivial.

Add reset encounter button

Add button to remove all monster from encounter.

Also: add button to remove single monster from encounter directly, instead of reducing it to 0 unit with - button.

Missing NPCs

Does not include NPCs from GMG, like Advisor, Guard, Tempest-Sun Mage, or Hellknights

Mobile responsiviness

Brainstorming:

  • Bar should be vertical
  • Table might be cards instead of rows
  • Filters should be in a separate side menu

Save and retrieve encounters

By using the browser local storage we could implement a save and retrieve later mechanism in the event that a user wants to brainstorm different scenarios or save for later an encounter setup.

Filter by creature environment

There was an user made excel file which pared creatures to an environment based on PF1 information.

Will need to find it out and talk to the author.

Bestiary 3 creatures missing from tool

As title suggests, it seems that most if not all creatures from Bestiary 3 are missing. I know this hasn't been updated in awhile, but hopefully that side could be updated.

Custom NPC

From Reddit:

https://www.reddit.com/r/Pathfinder2e/comments/kwkn15/comment/i950vmm/?utm_source=share&utm_medium=web2x&context=3

A feature suggestion: As a GM I usually create a playable character and make them fight the players (I do this when the NPC from the Game Mastery Guide or any humanoid monster in the manual is too generic for my needs). It would be cool if you included an option for this kind of characters. Of course, it would be using the generic stats for a playable character for any level. If it's too complicated for you that's OK, but if you can do it, it would be nice if you do

BTW, this is a great tool, I like it

Show precise exp value of encounter

The bar at the top doesn't show clearly the amount of exp budget in use. There is no scale and the threshold cover a wide amount of bar, making it difficult to see if the budget is the correct one.
I suggest the reduction in horizontal size of the bar and addition of a numeric output in the freed-up space.

Option to invert filters

As of now the filters work in a OR fashion, it might be handy to have a toggle or other meanings to invert the selection so that the results shown are all except the one matching the filter.

Calculate the exp to award players for the encounter

For parties with size other than 4, awarded exp is different than calculated exp. Could you display exp to award for an encounter in calculations? For example: 5man party of level 1, against 5 giant rats have 100xp (since each rat is 20xp) however when rewarding exp, you give 80xp instead of 100xp.

I have no idea how to formulate this programmatically (I'm fairly new to pf2e with no experience) but I'm copy pasting the relevant rules from Core Rule book.

Different Party Sizes (CRB 489)
For each additional character in the party beyond the
fourth, increase your XP budget by the amount shown
in the Character Adjustment value for your encounter in
Table 10–1: Encounter Budget. If you have fewer than
four characters, use the same process in reverse: for each
missing character, remove that amount of XP from your
XP budget. Note that if you adjust your XP budget to
account for party size, the XP awards for the encounter
don’t change—you’ll always award the amount of XP
listed for a group of four characters.

PARTY SIZE (CRB 508)
The rules for advancement assume a group of four PCs.
The rules for encounters (page 489) describe how to
accommodate groups of a different size, but the XP awards
don’t change—always award the amount of XP listed for a
group of four characters.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.