mirror of
https://github.com/Steffo99/patched-porobot.git
synced 2024-12-22 17:44:22 +00:00
Merge branch 'main' into reqeast
# Conflicts: # Cargo.toml # src/data/corebundle/mod.rs
This commit is contained in:
commit
cf7268ebdb
67 changed files with 90729 additions and 746 deletions
|
@ -1 +0,0 @@
|
|||
.gitignore
|
18
.dockerignore
Normal file
18
.dockerignore
Normal file
|
@ -0,0 +1,18 @@
|
|||
/.devcontainer/
|
||||
/.github/
|
||||
/.idea/
|
||||
/.vscode/
|
||||
/benches/
|
||||
/examples/
|
||||
/media/
|
||||
/target/
|
||||
/tests/
|
||||
/.dockerignore
|
||||
/.gitignore
|
||||
/Dockerfile
|
||||
/icon.png
|
||||
/LICENSE
|
||||
/README.md
|
||||
/riot.txt
|
||||
/rustfmt.toml
|
||||
**/*.rs.bk
|
53
.github/workflows/docker.yml
vendored
53
.github/workflows/docker.yml
vendored
|
@ -1,53 +0,0 @@
|
|||
name: "Automated release"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
publish-docker:
|
||||
name: "🐳 Build and publish Docker image on ghcr.io"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout repository"
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: "❓ Find the release semantic version"
|
||||
id: semver
|
||||
uses: Steffo99/actions-semver@v0.3.4
|
||||
with:
|
||||
string: ${{ github.ref_name }}
|
||||
|
||||
- name: "🔨 Setup Buildx"
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: "🔑 Login to GitHub Containers"
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "🔽 Find the lowercase name of the repository"
|
||||
id: reponame
|
||||
uses: ASzc/change-string-case-action@v2
|
||||
with:
|
||||
string: ${{ github.repository }}
|
||||
|
||||
- name: "🏗 Build and push the Docker image"
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
tags: >-
|
||||
ghcr.io/${{ steps.reponame.outputs.lowercase }}:${{ steps.semver.outputs.precedence }},
|
||||
ghcr.io/${{ steps.reponame.outputs.lowercase }}:${{ steps.semver.outputs.core }},
|
||||
ghcr.io/${{ steps.reponame.outputs.lowercase }}:${{ steps.semver.outputs.pair }},
|
||||
ghcr.io/${{ steps.reponame.outputs.lowercase }}:${{ steps.semver.outputs.major }},
|
||||
ghcr.io/${{ steps.reponame.outputs.lowercase }}:latest
|
||||
push: true
|
||||
|
||||
- name: "🌐 Create release"
|
||||
uses: ncipollo/release-action@v1.10.0
|
||||
with:
|
||||
draft: true
|
||||
generateReleaseNotes: true
|
11
.github/workflows/release.yml
vendored
Normal file
11
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
name: "Release new version"
|
||||
|
||||
on:
|
||||
# Creation of a new tag starting with v
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
ghcrio:
|
||||
uses: Steffo99/.github/.github/workflows/buildrelease-docker.yml@main
|
37
.github/workflows/test.yml
vendored
Normal file
37
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
name: "Test suite"
|
||||
|
||||
on:
|
||||
# Modification of a project file in the main branch
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "src/**"
|
||||
- "tests/**"
|
||||
- "benches/**"
|
||||
- "examples/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
# Pull request to the main branch modifying a project file
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "src/**"
|
||||
- "tests/**"
|
||||
- "benches/**"
|
||||
- "examples/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
# Triggered by a new release
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
clippy:
|
||||
uses: Steffo99/.github/.github/workflows/test-cargo-clippy.yml@main
|
||||
|
||||
test:
|
||||
uses: Steffo99/.github/.github/workflows/test-cargo-test.yml@main
|
32
.github/workflows/tests.yml
vendored
32
.github/workflows/tests.yml
vendored
|
@ -1,32 +0,0 @@
|
|||
name: "Continuous integration"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_call:
|
||||
|
||||
|
||||
jobs:
|
||||
cargotest:
|
||||
name: "Run checks and tests on the repository"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout repository"
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: "Run cargo clippy"
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features
|
||||
|
||||
- name: "Run cargo test"
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,5 +1,3 @@
|
|||
/data/
|
||||
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
|
@ -3,7 +3,7 @@
|
|||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<Languages>
|
||||
<language minSize="67" name="Rust" />
|
||||
<language minSize="79" name="Rust" />
|
||||
</Languages>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PWA">
|
||||
<option name="wasEnabledAtLeastOnce" value="true" />
|
||||
</component>
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
|
|
1153
Cargo.lock
generated
1153
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
37
Cargo.toml
37
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "patched_porobot"
|
||||
version = "0.7.0"
|
||||
version = "0.9.2"
|
||||
authors = ["Stefano Pigozzi <me@steffo.eu>"]
|
||||
edition = "2021"
|
||||
description = "Legends of Runeterra card database utilities and bots"
|
||||
|
@ -9,39 +9,46 @@ license = "AGPL-3.0-or-later"
|
|||
keywords = ["game", "deserialization", "legends-of-runeterra", "bot", "search"]
|
||||
categories = ["games", "parser-implementations"]
|
||||
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
cargo-args = ["--bins", "--lib"]
|
||||
cargo-args = ["--bins"]
|
||||
rustdoc-args = ["--document-private-items"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
# required
|
||||
# base
|
||||
log = { version = "0.4.17" }
|
||||
itertools = { version = "0.10.3" }
|
||||
regex = { version = "1.6.0" }
|
||||
lazy_static = { version = "1.4.0" }
|
||||
data-encoding = { version = "2.3.2" }
|
||||
varint-rs = { version = "2.2.0" }
|
||||
glob = { version = "0.3.0" }
|
||||
reqwest = { version = "0.11.11", features = ["rustls-tls", "json"], default-features = false }
|
||||
# exec
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
# data
|
||||
serde = { version = "1.0.140", features = ["derive"] }
|
||||
serde_json = { version = "1.0.82" }
|
||||
# optional
|
||||
reqwest = { version = "0.11.11", features = ["json"], optional = true }
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
glob = { version = "0.3.0", optional = true }
|
||||
tantivy = { version = "0.18.0", optional = true }
|
||||
teloxide = { version = "0.10.1", optional = true }
|
||||
tokio = { version = "1.20.1", features = ["rt-multi-thread", "macros"], optional = true }
|
||||
# search
|
||||
tantivy = { version = "0.19.1", optional = true }
|
||||
# telegram
|
||||
teloxide = { version = "0.12.0", features = ["rustls", "ctrlc_handler", "auto-send"], default-features = false, optional = true }
|
||||
tokio = { version = "1.20.3", features = ["rt-multi-thread", "macros"], optional = true }
|
||||
md5 = { version = "0.7.0", optional = true }
|
||||
rand = { version = "0.8.5", optional = true }
|
||||
# discord
|
||||
serenity = { version = "0.11.5", features = ["client", "cache", "gateway", "rustls_backend", "model"], default-features = false, optional = true }
|
||||
anyhow = { version = "^1.0.68", optional = true }
|
||||
# matrix
|
||||
|
||||
|
||||
[features]
|
||||
# data = [] # Always included
|
||||
fetch = ["reqwest"]
|
||||
exec = ["pretty_env_logger", "glob"]
|
||||
exec = ["pretty_env_logger"]
|
||||
search = ["tantivy"]
|
||||
telegram = ["exec", "search", "teloxide", "reqwest", "tokio", "md5"]
|
||||
discord = ["exec", "search"]
|
||||
telegram = ["exec", "search", "teloxide", "tokio", "md5", "rand"]
|
||||
discord = ["exec", "search", "serenity", "tokio", "anyhow"]
|
||||
matrix = ["exec", "search"]
|
||||
|
||||
|
||||
|
|
89
Dockerfile
89
Dockerfile
|
@ -1,23 +1,80 @@
|
|||
FROM rust:1.64 AS labels
|
||||
FROM --platform=${BUILDPLATFORM} rust:1.68-bullseye AS builder
|
||||
ARG BUILDPLATFORM
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade --assume-yes
|
||||
|
||||
RUN \
|
||||
mkdir .cargo && \
|
||||
echo '[net]' >> .cargo/config.toml && \
|
||||
echo 'git-fetch-with-cli = true' >> .cargo/config.toml && \
|
||||
echo >> .cargo/config.toml && \
|
||||
if [ "${BUILDPLATFORM}" != "${TARGETPLATFORM}" ]; then \
|
||||
if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
|
||||
apt-get install --assume-yes gcc-x86-64-linux-gnu; \
|
||||
echo '[target.x86_64-unknown-linux-gnu]' >> .cargo/config.toml; \
|
||||
echo 'linker = "x86-64-linux-gnu-gcc"' >> .cargo/config.toml; \
|
||||
echo >> .cargo/config.toml; \
|
||||
fi && \
|
||||
if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
|
||||
apt-get install --assume-yes gcc-aarch64-linux-gnu; \
|
||||
echo '[target.aarch64-unknown-linux-gnu]' >> .cargo/config.toml; \
|
||||
echo 'linker = "aarch64-linux-gnu-gcc"' >> .cargo/config.toml; \
|
||||
echo >> .cargo/config.toml; \
|
||||
fi && \
|
||||
if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
|
||||
apt-get install --assume-yes gcc-arm-linux-gnueabihf; \
|
||||
echo '[target.armv7-unknown-linux-gnueabihf]' >> .cargo/config.toml; \
|
||||
echo 'linker = "arm-linux-gnueabihf-gcc"' >> .cargo/config.toml; \
|
||||
echo >> .cargo/config.toml; \
|
||||
fi \
|
||||
fi
|
||||
|
||||
RUN \
|
||||
if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
|
||||
RUSTTARGET=x86_64-unknown-linux-gnu; \
|
||||
fi && \
|
||||
if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
|
||||
RUSTTARGET=aarch64-unknown-linux-gnu; \
|
||||
fi && \
|
||||
if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
|
||||
RUSTTARGET=armv7-unknown-linux-gnueabihf; \
|
||||
fi && \
|
||||
rustup target add ${RUSTTARGET}
|
||||
|
||||
WORKDIR /usr/src/patched_porobot/
|
||||
COPY ./ ./
|
||||
|
||||
RUN \
|
||||
if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
|
||||
RUSTTARGET=x86_64-unknown-linux-gnu; \
|
||||
fi && \
|
||||
if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
|
||||
RUSTTARGET=aarch64-unknown-linux-gnu; \
|
||||
fi && \
|
||||
if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
|
||||
RUSTTARGET=armv7-unknown-linux-gnueabihf; \
|
||||
fi && \
|
||||
cargo build --all-features --bins --release --target=${RUSTTARGET}
|
||||
|
||||
#############################################################################
|
||||
|
||||
FROM --platform=${TARGETPLATFORM} rust:1.68-slim-bullseye AS final
|
||||
|
||||
WORKDIR /usr/src/patched_porobot/
|
||||
COPY --from=builder \
|
||||
/usr/src/patched_porobot/target/*/release/patched_porobot_discord \
|
||||
/usr/src/patched_porobot/target/*/release/patched_porobot_telegram \
|
||||
/usr/src/patched_porobot/target/*/release/patched_porobot_matrix \
|
||||
/usr/bin/
|
||||
|
||||
ENTRYPOINT []
|
||||
CMD []
|
||||
|
||||
LABEL org.opencontainers.image.title="Patched Porobot"
|
||||
LABEL org.opencontainers.image.description="Legends of Runeterra card database utilities and bots"
|
||||
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
|
||||
LABEL org.opencontainers.image.url="https://github.com/Steffo99/patched-porobot"
|
||||
LABEL org.opencontainers.image.authors="Stefano Pigozzi <me@steffo.eu>"
|
||||
|
||||
|
||||
FROM labels AS files
|
||||
|
||||
WORKDIR /usr/src/patched_porobot
|
||||
COPY . .
|
||||
|
||||
|
||||
FROM files AS install
|
||||
|
||||
RUN cargo install --path . --all-features --bins
|
||||
|
||||
|
||||
FROM install AS environment
|
||||
|
||||
ENV RUST_LOG "warn,patched_porobot=info,patched_porobot_telegram=info,patched_porobot_discord=info,patched_porobot_matrix=info"
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
# ![](icon.png) patched-porobot
|
||||
# ![](icon.png) Patched Porobot
|
||||
|
||||
Legends of Runeterra card library and bots
|
||||
|
||||
\[ **[Telegram]** | [Documentation] \]
|
||||
\[ **[Telegram]** | **[Discord]** | [Documentation] \]
|
||||
|
||||
[Telegram]: https://t.me/patchedporobot
|
||||
[Discord]: https://discord.com/api/oauth2/authorize?client_id=1071989978743193672&scope=applications.commands
|
||||
[Documentation]: https://docs.rs/crate/patched_porobot/latest
|
||||
|
||||
## Legal
|
||||
|
|
1
data/core-en_us/COPYRIGHT
Normal file
1
data/core-en_us/COPYRIGHT
Normal file
|
@ -0,0 +1 @@
|
|||
Copyright Riot Games, Inc. 2019
|
40
data/core-en_us/README.md
Normal file
40
data/core-en_us/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
metadata.json
|
||||
{
|
||||
"locales": ["{string}", ...],
|
||||
"clientHash": "{string}"
|
||||
"gameplayDataHash": "{string}",
|
||||
"timestamp": "{YYYYMMDDhhmm}",
|
||||
"patchlineRef": "{string}"
|
||||
}
|
||||
|
||||
cards.json
|
||||
[
|
||||
{
|
||||
"id": "{cardCode}",
|
||||
"idComponents":
|
||||
{
|
||||
"set": "setNumber",
|
||||
"region":
|
||||
{
|
||||
"id": "{shortRegionCode}",
|
||||
"name": "{regionName}"
|
||||
}
|
||||
}
|
||||
"name": "{name}",
|
||||
"type": "{type}",
|
||||
"subType": "{subType}",
|
||||
"superType": "{superType}",
|
||||
"description": "{description}",
|
||||
"keywords": [],
|
||||
"associatedCards": [{cardCode}, ...]
|
||||
"health": "{health}",
|
||||
"attack": "{attack}",
|
||||
"cost": "{cost}",
|
||||
"assets":
|
||||
{
|
||||
"gameAbsolutePath": "http://{cdn}/{bundleName}/set1/en_us/img/card/game/{cardCode}.png"
|
||||
}
|
||||
|
||||
},
|
||||
{...}
|
||||
]
|
743
data/core-en_us/en_us/data/globals-en_us.json
Normal file
743
data/core-en_us/en_us/data/globals-en_us.json
Normal file
|
@ -0,0 +1,743 @@
|
|||
{
|
||||
"vocabTerms": [
|
||||
{
|
||||
"description": "When you summon this, it gets its allegiance bonus if the top card of your deck matches its region.",
|
||||
"name": "Allegiance",
|
||||
"nameRef": "Allegiance"
|
||||
},
|
||||
{
|
||||
"description": "Automatically equips this item from hand or play when summoned, creating it first if needed.",
|
||||
"name": "Auto-Equip",
|
||||
"nameRef": "AutoEquip"
|
||||
},
|
||||
{
|
||||
"description": "Pick the next Moon Weapon for Aphelios.",
|
||||
"name": "Phase",
|
||||
"nameRef": "Phase"
|
||||
},
|
||||
{
|
||||
"description": "Create a random Blade Fragment still needed to restore the blade. Once you’ve played all 3, create the Blade of the Exile.",
|
||||
"name": "Reforge",
|
||||
"nameRef": "Reforge"
|
||||
},
|
||||
{
|
||||
"description": "Immediately draw 1 of each Ascended ally. For the rest of the game, level 2 Ascended allies are level 3.",
|
||||
"name": "Restore the Sun Disc",
|
||||
"nameRef": "SunDiscRestore"
|
||||
},
|
||||
{
|
||||
"description": "Effect when unit strikes with an attack",
|
||||
"name": "Attack Strike",
|
||||
"nameRef": "AttackStrike"
|
||||
},
|
||||
{
|
||||
"description": "For each Spawn:\nSummon a 1|1 Tentacle, or if you already have one, grant your strongest Tentacle +1|+1.",
|
||||
"name": "Spawn",
|
||||
"nameRef": "Spawn"
|
||||
},
|
||||
{
|
||||
"description": "Highest Power, with ties broken by highest Health then highest Cost.",
|
||||
"name": "Strongest",
|
||||
"nameRef": "Strongest"
|
||||
},
|
||||
{
|
||||
"description": "Obliterate X non-champion cards from the bottom of your deck.",
|
||||
"name": "Toss",
|
||||
"nameRef": "Toss"
|
||||
},
|
||||
{
|
||||
"description": "Start a free attack with that many summoned Blades.",
|
||||
"name": "Blade Dance",
|
||||
"nameRef": "BladeDance"
|
||||
},
|
||||
{
|
||||
"description": "You behold something if you have it in play or hand.",
|
||||
"name": "Behold",
|
||||
"nameRef": "Behold"
|
||||
},
|
||||
{
|
||||
"description": "This is how much Mana you need to spend to play this card.",
|
||||
"name": "Cost",
|
||||
"nameRef": "Cost"
|
||||
},
|
||||
{
|
||||
"description": "Get this effect when the round ends.",
|
||||
"name": "Round End",
|
||||
"nameRef": "RoundEnd"
|
||||
},
|
||||
{
|
||||
"description": "This is how much damage the unit can withstand. If it reaches zero, the unit dies.",
|
||||
"name": "Health",
|
||||
"nameRef": "Health"
|
||||
},
|
||||
{
|
||||
"description": "Choose one of two random options from a depleting pool of equipment and equip it to this ally. If the ally wasn't played from hand, it equips a random equipment instead.",
|
||||
"name": "Improvise",
|
||||
"nameRef": "Improvise"
|
||||
},
|
||||
{
|
||||
"description": "This is how much damage the unit deals when it strikes.",
|
||||
"name": "Power",
|
||||
"nameRef": "Power"
|
||||
},
|
||||
{
|
||||
"description": "A card activates its Flow on Round Start if you played 2+ spells or skills last round.",
|
||||
"name": "Flow",
|
||||
"nameRef": "Flow"
|
||||
},
|
||||
{
|
||||
"description": "Get this effect when this unit attacks.",
|
||||
"name": "Attack",
|
||||
"nameRef": "Attack"
|
||||
},
|
||||
{
|
||||
"description": "A unit is buffed when its Power or Health is increased or it gains a new keyword.",
|
||||
"name": "Buffed",
|
||||
"nameRef": "Buffed"
|
||||
},
|
||||
{
|
||||
"description": "This champion counts as one of your deck's regions. During deckbuilding, you may add the specified cards to your deck regardless of region. Origins may also have an effect that begins at Start of Game.",
|
||||
"name": "Origin",
|
||||
"nameRef": "Origin"
|
||||
},
|
||||
{
|
||||
"description": "Get this effect when a unit attempts to deal damage using its Power, either at the end of battle or with spells. Units with 0 Power can't strike.",
|
||||
"name": "Strike",
|
||||
"nameRef": "Strike"
|
||||
},
|
||||
{
|
||||
"description": "Effect when unit Strikes the enemy Nexus.",
|
||||
"name": "Nexus Strike",
|
||||
"nameRef": "NexusStrike"
|
||||
},
|
||||
{
|
||||
"description": "Activates if allies have struck for 5+ damage at least 4 times this game.",
|
||||
"name": "Reputation",
|
||||
"nameRef": "Reputation"
|
||||
},
|
||||
{
|
||||
"description": "Equipping an Item to a unit grants it the listed bonuses. If the unit leaves play, the Item will return to your hand. You may play each item at most once per round.",
|
||||
"name": "Equip",
|
||||
"nameRef": "Equip"
|
||||
},
|
||||
{
|
||||
"description": "Get this effect when the round starts.",
|
||||
"name": "Round Start",
|
||||
"nameRef": "RoundStart"
|
||||
},
|
||||
{
|
||||
"description": "The opponent in The Path of Champions.",
|
||||
"name": "Foe",
|
||||
"nameRef": "Foe"
|
||||
},
|
||||
{
|
||||
"description": "Grant an ally +1|+1. If the ally is equipped, grant it to their item instead.",
|
||||
"name": "Forge",
|
||||
"nameRef": "Forge"
|
||||
},
|
||||
{
|
||||
"description": "Get this effect when you play this unit from hand.",
|
||||
"name": "Play",
|
||||
"nameRef": "Play"
|
||||
},
|
||||
{
|
||||
"description": "In play, in hand, in deck, in discard, and even if created/summoned later.",
|
||||
"name": "Everywhere",
|
||||
"nameRef": "Everywhere"
|
||||
},
|
||||
{
|
||||
"description": "Makes a Countdown landmark count down that many times",
|
||||
"name": "Advance",
|
||||
"nameRef": "Advance"
|
||||
},
|
||||
{
|
||||
"description": "Round Start: I count down 1. At 0, activate the Countdown effect, then destroy me.",
|
||||
"name": "Countdown",
|
||||
"nameRef": "Countdown"
|
||||
},
|
||||
{
|
||||
"description": "If you don't have one, gain the attack token. You can attack this round.",
|
||||
"name": "Rally",
|
||||
"nameRef": "Rally"
|
||||
},
|
||||
{
|
||||
"description": "When you kill a unit via damage, kill effect, or striking it with an ally. (Self-killing, like from Ephemeral, doesn't count.)",
|
||||
"name": "Slay",
|
||||
"nameRef": "Slay"
|
||||
},
|
||||
{
|
||||
"description": "Remove all keywords, abilities, and ongoing effects. Doesn't affect damage or subtype.",
|
||||
"name": "Silence",
|
||||
"nameRef": "Silence"
|
||||
},
|
||||
{
|
||||
"description": "Create in hand 1 of 3 randomly selected cards.",
|
||||
"name": "Manifest",
|
||||
"nameRef": "Manifest"
|
||||
},
|
||||
{
|
||||
"description": "Pick a card from among 3 in your deck. Shuffle the deck and put that card on top.",
|
||||
"name": "Predict",
|
||||
"nameRef": "Predict"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
{
|
||||
"description": "Inflicts damage beyond what would kill the target(s) to the enemy Nexus.",
|
||||
"name": "Overwhelm",
|
||||
"nameRef": "SpellOverwhelm"
|
||||
},
|
||||
{
|
||||
"description": "Can be played whenever you may act. Happens instantly and allows you to continue to play other cards.",
|
||||
"name": "Burst",
|
||||
"nameRef": "Burst"
|
||||
},
|
||||
{
|
||||
"description": "Round Start: I count down 1. At 0, activate the Countdown effect, then destroy me.",
|
||||
"name": "Countdown",
|
||||
"nameRef": "Countdown"
|
||||
},
|
||||
{
|
||||
"description": "Get this effect when you play this unit from hand.",
|
||||
"name": "Play",
|
||||
"nameRef": "PlaySkillMark"
|
||||
},
|
||||
{
|
||||
"description": "Landmarks take up a space on the board. They can't attack, block, or take damage.",
|
||||
"name": "Landmark",
|
||||
"nameRef": "LandmarkVisualOnly"
|
||||
},
|
||||
{
|
||||
"description": "A Captured card is removed from the game. It returns when the Capturing unit leaves play.",
|
||||
"name": "Capture",
|
||||
"nameRef": "Capture"
|
||||
},
|
||||
{
|
||||
"description": "Get this effect when this unit attacks.",
|
||||
"name": "Attack",
|
||||
"nameRef": "AttackSkillMark"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "Shurima",
|
||||
"nameRef": "Shurima"
|
||||
},
|
||||
{
|
||||
"description": "Attach me to an ally to give it my stats and keywords while I'm attached. When that ally leaves play, Recall me.",
|
||||
"name": "Attach",
|
||||
"nameRef": "Attach"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "Noxus",
|
||||
"nameRef": "Noxus"
|
||||
},
|
||||
{
|
||||
"description": "Fleeting cards discard from hand when the round ends.",
|
||||
"name": "Fleeting",
|
||||
"nameRef": "Fleeting"
|
||||
},
|
||||
{
|
||||
"description": "Missing Translation",
|
||||
"name": "Missing Translation",
|
||||
"nameRef": "ClobberNoEmptySlotRequirement"
|
||||
},
|
||||
{
|
||||
"description": "Draw a non-champion card from the bottom of the enemy deck.",
|
||||
"name": "Nab",
|
||||
"nameRef": "Nab"
|
||||
},
|
||||
{
|
||||
"description": "Can be played outside combat or when no other spells or skills are pending. Happens instantly and allows you to continue to play other cards.",
|
||||
"name": "Focus",
|
||||
"nameRef": "Focus"
|
||||
},
|
||||
{
|
||||
"description": "You're Enlightened when you have 10 max mana.",
|
||||
"name": "Enlightened",
|
||||
"nameRef": "Enlightened"
|
||||
},
|
||||
{
|
||||
"description": "Pick a Celestial card from among 3 to create in hand.",
|
||||
"name": "Invoke",
|
||||
"nameRef": "Invoke"
|
||||
},
|
||||
{
|
||||
"description": "A card activates its Flow on Round Start if you played 2+ spells or skills last round.",
|
||||
"name": "Flow",
|
||||
"nameRef": "Flow"
|
||||
},
|
||||
{
|
||||
"description": "Attaches to another card in a deck. When that card is drawn, activate the effect.",
|
||||
"name": "Boon",
|
||||
"nameRef": "Boon"
|
||||
},
|
||||
{
|
||||
"description": "Attaches to another card in a deck. When that card is drawn, activate the effect.",
|
||||
"name": "Trap",
|
||||
"nameRef": "Autoplay"
|
||||
},
|
||||
{
|
||||
"description": "Heal your Nexus for the amount of damage dealt",
|
||||
"name": "Drain",
|
||||
"nameRef": "Drain"
|
||||
},
|
||||
{
|
||||
"description": "These abilities take effect when the unit dies.",
|
||||
"name": "Last Breath",
|
||||
"nameRef": "LastBreath"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "Demacia",
|
||||
"nameRef": "Demacia"
|
||||
},
|
||||
{
|
||||
"description": "A champion levels up once this condition is met, even in hand or deck.",
|
||||
"name": "Level Up",
|
||||
"nameRef": "LevelUp"
|
||||
},
|
||||
{
|
||||
"description": "",
|
||||
"name": "Bandle City",
|
||||
"nameRef": "BandleCity"
|
||||
},
|
||||
{
|
||||
"description": "Can be played whenever you may act. Happens after your opponent has a chance to react.",
|
||||
"name": "Fast",
|
||||
"nameRef": "Fast"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "Bilgewater",
|
||||
"nameRef": "Bilgewater"
|
||||
},
|
||||
{
|
||||
"description": "",
|
||||
"name": "Runeterra",
|
||||
"nameRef": "Runeterra"
|
||||
},
|
||||
{
|
||||
"description": "Return a unit to hand and remove all effects applied to it.",
|
||||
"name": "Recall",
|
||||
"nameRef": "Recall"
|
||||
},
|
||||
{
|
||||
"description": "Lowest Power, with ties broken by lowest Health then lowest Cost",
|
||||
"name": "Weakest",
|
||||
"nameRef": "Weakest"
|
||||
},
|
||||
{
|
||||
"description": "Attacking with a support unit will buff the unit to its right.",
|
||||
"name": "Support",
|
||||
"nameRef": "Support"
|
||||
},
|
||||
{
|
||||
"description": "Can be played outside of combat when no spells or skills are pending. Happens after your opponent has a chance to react.",
|
||||
"name": "Slow",
|
||||
"nameRef": "Slow"
|
||||
},
|
||||
{
|
||||
"description": "Completely removed from the game. Doesn't cause Last Breath and can't be revived.",
|
||||
"name": "Obliterate",
|
||||
"nameRef": "Obliterate"
|
||||
},
|
||||
{
|
||||
"description": "These abilities trigger when you resolve a spell.",
|
||||
"name": "Imbue",
|
||||
"nameRef": "Imbue"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "Targon",
|
||||
"nameRef": "MtTargon"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "Shadow Isles",
|
||||
"nameRef": "ShadowIsles"
|
||||
},
|
||||
{
|
||||
"description": "Missing Translation",
|
||||
"name": "Missing Translation",
|
||||
"nameRef": "AuraVisualFakeKeyword"
|
||||
},
|
||||
{
|
||||
"description": "Equip to a unit to grant the listed bonuses. If the unit leaves play, the equipment will return to your hand. You may play each equipment at most once per round.",
|
||||
"name": "Equipment",
|
||||
"nameRef": "Equipment"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "Ionia",
|
||||
"nameRef": "Ionia"
|
||||
},
|
||||
{
|
||||
"description": "Bonus if this is NOT the first card you play in a round.",
|
||||
"name": "Nightfall",
|
||||
"nameRef": "Nightfall"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "Piltover & Zaun",
|
||||
"nameRef": "PiltoverZaun"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "Freljord",
|
||||
"nameRef": "Freljord"
|
||||
},
|
||||
{
|
||||
"description": "When I'm summoned, refill 1 spell mana.",
|
||||
"name": "Attune",
|
||||
"nameRef": "Attune"
|
||||
},
|
||||
{
|
||||
"description": "Bonus if this is the FIRST card you play in a round.",
|
||||
"name": "Daybreak",
|
||||
"nameRef": "Daybreak"
|
||||
},
|
||||
{
|
||||
"description": "Can block Elusive units",
|
||||
"name": "Blocks Elusive",
|
||||
"nameRef": "BlocksElusive"
|
||||
},
|
||||
{
|
||||
"description": "Missing Translation",
|
||||
"name": "Missing Translation",
|
||||
"nameRef": "SilenceIndividualKeyword"
|
||||
},
|
||||
{
|
||||
"description": "A unit's spell-like effect that allows enemy reactions.",
|
||||
"name": "Skill",
|
||||
"nameRef": "Skill"
|
||||
},
|
||||
{
|
||||
"description": "A card triggers its plunder ability when played if you damaged the enemy Nexus this round.",
|
||||
"name": "Plunder",
|
||||
"nameRef": "Plunder"
|
||||
},
|
||||
{
|
||||
"description": "While attacking, it strikes both before AND at the same time as its blocker.",
|
||||
"name": "Double Attack",
|
||||
"nameRef": "DoubleStrike"
|
||||
},
|
||||
{
|
||||
"description": "The enemy can challenge this unit, forcing it to block.",
|
||||
"name": "Vulnerable",
|
||||
"nameRef": "Vulnerable"
|
||||
},
|
||||
{
|
||||
"description": "Can only be blocked by an Elusive unit.",
|
||||
"name": "Elusive",
|
||||
"nameRef": "Elusive"
|
||||
},
|
||||
{
|
||||
"description": "Remove a unit from combat. It can't attack or block for the rest of the round.",
|
||||
"name": "Stun",
|
||||
"nameRef": "Stun"
|
||||
},
|
||||
{
|
||||
"description": "Each round, the first time an allied card targets me, grant me +1|+1.",
|
||||
"name": "Fated",
|
||||
"nameRef": "Fated"
|
||||
},
|
||||
{
|
||||
"description": "When I kill a unit, grant me +1|+1.",
|
||||
"name": "Fury",
|
||||
"nameRef": "Fury"
|
||||
},
|
||||
{
|
||||
"description": "Negates the next damage the unit would take. Lasts one round.",
|
||||
"name": "Barrier",
|
||||
"nameRef": "Barrier"
|
||||
},
|
||||
{
|
||||
"description": "Can't attack or block.",
|
||||
"name": "Immobile",
|
||||
"nameRef": "Immobile"
|
||||
},
|
||||
{
|
||||
"description": "After I die, for the rest of the game when allies attack, hallow your first attacker giving it +1|+0 that round",
|
||||
"name": "Hallowed",
|
||||
"nameRef": "Hallowed"
|
||||
},
|
||||
{
|
||||
"description": "I have +2|+2 once you've had Units with 6+ other unique positive keywords in play this game.",
|
||||
"name": "Evolve",
|
||||
"nameRef": "Evolve"
|
||||
},
|
||||
{
|
||||
"description": "Set a unit's Power to 0 this round. It can be changed after.",
|
||||
"name": "Frostbite",
|
||||
"nameRef": "Frostbite"
|
||||
},
|
||||
{
|
||||
"description": "Excess damage I deal to my blocker is dealt to the enemy Nexus.",
|
||||
"name": "Overwhelm",
|
||||
"nameRef": "Overwhelm"
|
||||
},
|
||||
{
|
||||
"description": "While attacking, strikes before its blocker.",
|
||||
"name": "Quick Attack",
|
||||
"nameRef": "QuickStrike"
|
||||
},
|
||||
{
|
||||
"description": "Takes 1 less damage from all sources.",
|
||||
"name": "Tough",
|
||||
"nameRef": "Tough"
|
||||
},
|
||||
{
|
||||
"description": "Heals fully at the end of each round.",
|
||||
"name": "Regeneration",
|
||||
"nameRef": "Regeneration"
|
||||
},
|
||||
{
|
||||
"description": "Removes all text and keywords from a unit.",
|
||||
"name": "Silenced",
|
||||
"nameRef": "Silenced"
|
||||
},
|
||||
{
|
||||
"description": "Negates the next enemy spell or skill that would affect me.",
|
||||
"name": "SpellShield",
|
||||
"nameRef": "SpellShield"
|
||||
},
|
||||
{
|
||||
"description": "Damage this unit deals heals its Nexus that amount.",
|
||||
"name": "Lifesteal",
|
||||
"nameRef": "Lifesteal"
|
||||
},
|
||||
{
|
||||
"description": "When you play a created card, grant me +1|+0.",
|
||||
"name": "Augment",
|
||||
"nameRef": "Augment"
|
||||
},
|
||||
{
|
||||
"description": "When this strikes while attacking, it deals 1 to the enemy Nexus. This keyword can stack.",
|
||||
"name": "Impact",
|
||||
"nameRef": "Impact"
|
||||
},
|
||||
{
|
||||
"description": "The first time only Scout units attack each round, ready your attack.",
|
||||
"name": "Scout",
|
||||
"nameRef": "Scout"
|
||||
},
|
||||
{
|
||||
"description": "This unit dies when it strikes or when the round ends.",
|
||||
"name": "Ephemeral",
|
||||
"nameRef": "Ephemeral"
|
||||
},
|
||||
{
|
||||
"description": "When you attack while I'm on top of your deck, I Lurk, granting Lurker allies everywhere +1|+0. Max once per round.",
|
||||
"name": "Lurk",
|
||||
"nameRef": "Lurker"
|
||||
},
|
||||
{
|
||||
"description": "I strike with my Health instead of my Power.",
|
||||
"name": "Formidable",
|
||||
"nameRef": "Formidable"
|
||||
},
|
||||
{
|
||||
"description": "Can choose which enemy unit blocks.",
|
||||
"name": "Challenger",
|
||||
"nameRef": "Challenger"
|
||||
},
|
||||
{
|
||||
"description": "Can only be blocked by enemies with 3 or more Power.",
|
||||
"name": "Fearsome",
|
||||
"nameRef": "Fearsome"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "Can't Block",
|
||||
"nameRef": "CantBlock"
|
||||
},
|
||||
{
|
||||
"description": "",
|
||||
"name": "Deep",
|
||||
"nameRef": "Deep"
|
||||
}
|
||||
],
|
||||
"regions": [
|
||||
{
|
||||
"abbreviation": "NX",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-noxus.png",
|
||||
"name": "Noxus",
|
||||
"nameRef": "Noxus"
|
||||
},
|
||||
{
|
||||
"abbreviation": "DE",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-demacia.png",
|
||||
"name": "Demacia",
|
||||
"nameRef": "Demacia"
|
||||
},
|
||||
{
|
||||
"abbreviation": "Jhin",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-jhin.png",
|
||||
"name": "Jhin",
|
||||
"nameRef": "Jhin"
|
||||
},
|
||||
{
|
||||
"abbreviation": "Varus",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-varus.png",
|
||||
"name": "Varus",
|
||||
"nameRef": "Varus"
|
||||
},
|
||||
{
|
||||
"abbreviation": "Jax",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-jax.png",
|
||||
"name": "Jax",
|
||||
"nameRef": "Jax"
|
||||
},
|
||||
{
|
||||
"abbreviation": "Kayn",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-kayn.png",
|
||||
"name": "Kayn",
|
||||
"nameRef": "Kayn"
|
||||
},
|
||||
{
|
||||
"abbreviation": "Evelynn",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-evelynn.png",
|
||||
"name": "Evelynn",
|
||||
"nameRef": "Evelynn"
|
||||
},
|
||||
{
|
||||
"abbreviation": "Bard",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-bard.png",
|
||||
"name": "Bard",
|
||||
"nameRef": "Bard"
|
||||
},
|
||||
{
|
||||
"abbreviation": "RU",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-runeterra.png",
|
||||
"name": "Runeterra",
|
||||
"nameRef": "Runeterra"
|
||||
},
|
||||
{
|
||||
"abbreviation": "FR",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-freljord.png",
|
||||
"name": "Freljord",
|
||||
"nameRef": "Freljord"
|
||||
},
|
||||
{
|
||||
"abbreviation": "SI",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-shadowisles.png",
|
||||
"name": "Shadow Isles",
|
||||
"nameRef": "ShadowIsles"
|
||||
},
|
||||
{
|
||||
"abbreviation": "MT",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-targon.png",
|
||||
"name": "Targon",
|
||||
"nameRef": "Targon"
|
||||
},
|
||||
{
|
||||
"abbreviation": "IO",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-ionia.png",
|
||||
"name": "Ionia",
|
||||
"nameRef": "Ionia"
|
||||
},
|
||||
{
|
||||
"abbreviation": "SH",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-shurima.png",
|
||||
"name": "Shurima",
|
||||
"nameRef": "Shurima"
|
||||
},
|
||||
{
|
||||
"abbreviation": "BW",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-bilgewater.png",
|
||||
"name": "Bilgewater",
|
||||
"nameRef": "Bilgewater"
|
||||
},
|
||||
{
|
||||
"abbreviation": "PZ",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-piltoverzaun.png",
|
||||
"name": "Piltover & Zaun",
|
||||
"nameRef": "PiltoverZaun"
|
||||
},
|
||||
{
|
||||
"abbreviation": "BC",
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/regions/icon-bandlecity.png",
|
||||
"name": "Bandle City",
|
||||
"nameRef": "BandleCity"
|
||||
}
|
||||
],
|
||||
"spellSpeeds": [
|
||||
{
|
||||
"name": "Slow",
|
||||
"nameRef": "Slow"
|
||||
},
|
||||
{
|
||||
"name": "Burst",
|
||||
"nameRef": "Burst"
|
||||
},
|
||||
{
|
||||
"name": "Fast",
|
||||
"nameRef": "Fast"
|
||||
}
|
||||
],
|
||||
"rarities": [
|
||||
{
|
||||
"name": "COMMON",
|
||||
"nameRef": "Common"
|
||||
},
|
||||
{
|
||||
"name": "RARE",
|
||||
"nameRef": "Rare"
|
||||
},
|
||||
{
|
||||
"name": "EPIC",
|
||||
"nameRef": "Epic"
|
||||
},
|
||||
{
|
||||
"name": "Champion",
|
||||
"nameRef": "Champion"
|
||||
},
|
||||
{
|
||||
"name": "None",
|
||||
"nameRef": "None"
|
||||
}
|
||||
],
|
||||
"sets": [
|
||||
{
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/sets/set3_crispmip.png",
|
||||
"name": "Call of the Mountain",
|
||||
"nameRef": "Set3"
|
||||
},
|
||||
{
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/sets/set5_crispmip.png",
|
||||
"name": "Beyond the Bandlewood",
|
||||
"nameRef": "Set5"
|
||||
},
|
||||
{
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/sets/set1_crispmip.png",
|
||||
"name": "Foundations",
|
||||
"nameRef": "Set1"
|
||||
},
|
||||
{
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/sets/set2_crispmip.png",
|
||||
"name": "Rising Tides",
|
||||
"nameRef": "Set2"
|
||||
},
|
||||
{
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/sets/set6ab_crispmip.png",
|
||||
"name": "Worldwalker",
|
||||
"nameRef": "Set6"
|
||||
},
|
||||
{
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/sets/set4_crispmip.png",
|
||||
"name": "Empires of the Ascended",
|
||||
"nameRef": "Set4"
|
||||
},
|
||||
{
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/sets/setevent_crispmip.png",
|
||||
"name": "Events",
|
||||
"nameRef": "SetEvent"
|
||||
},
|
||||
{
|
||||
"iconAbsolutePath": "http://dd.b.pvp.net/3_17_0/core/en_us/img/sets/set6cde_crispmip.png",
|
||||
"name": "The Darkin Saga",
|
||||
"nameRef": "Set6cde"
|
||||
}
|
||||
]
|
||||
}
|
5
data/core-en_us/metadata.json
Normal file
5
data/core-en_us/metadata.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"locales": [
|
||||
"en_us"
|
||||
]
|
||||
}
|
1
data/set1-en_us/COPYRIGHT
Normal file
1
data/set1-en_us/COPYRIGHT
Normal file
|
@ -0,0 +1 @@
|
|||
Copyright Riot Games, Inc. 2019
|
40
data/set1-en_us/README.md
Normal file
40
data/set1-en_us/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
metadata.json
|
||||
{
|
||||
"locales": ["{string}", ...],
|
||||
"clientHash": "{string}"
|
||||
"gameplayDataHash": "{string}",
|
||||
"timestamp": "{YYYYMMDDhhmm}",
|
||||
"patchlineRef": "{string}"
|
||||
}
|
||||
|
||||
cards.json
|
||||
[
|
||||
{
|
||||
"id": "{cardCode}",
|
||||
"idComponents":
|
||||
{
|
||||
"set": "setNumber",
|
||||
"region":
|
||||
{
|
||||
"id": "{shortRegionCode}",
|
||||
"name": "{regionName}"
|
||||
}
|
||||
}
|
||||
"name": "{name}",
|
||||
"type": "{type}",
|
||||
"subType": "{subType}",
|
||||
"superType": "{superType}",
|
||||
"description": "{description}",
|
||||
"keywords": [],
|
||||
"associatedCards": [{cardCode}, ...]
|
||||
"health": "{health}",
|
||||
"attack": "{attack}",
|
||||
"cost": "{cost}",
|
||||
"assets":
|
||||
{
|
||||
"gameAbsolutePath": "http://{cdn}/{bundleName}/set1/en_us/img/card/game/{cardCode}.png"
|
||||
}
|
||||
|
||||
},
|
||||
{...}
|
||||
]
|
17725
data/set1-en_us/en_us/data/set1-en_us.json
Normal file
17725
data/set1-en_us/en_us/data/set1-en_us.json
Normal file
File diff suppressed because it is too large
Load diff
5
data/set1-en_us/metadata.json
Normal file
5
data/set1-en_us/metadata.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"locales": [
|
||||
"en_us"
|
||||
]
|
||||
}
|
1
data/set2-en_us/COPYRIGHT
Normal file
1
data/set2-en_us/COPYRIGHT
Normal file
|
@ -0,0 +1 @@
|
|||
Copyright Riot Games, Inc. 2019
|
40
data/set2-en_us/README.md
Normal file
40
data/set2-en_us/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
metadata.json
|
||||
{
|
||||
"locales": ["{string}", ...],
|
||||
"clientHash": "{string}"
|
||||
"gameplayDataHash": "{string}",
|
||||
"timestamp": "{YYYYMMDDhhmm}",
|
||||
"patchlineRef": "{string}"
|
||||
}
|
||||
|
||||
cards.json
|
||||
[
|
||||
{
|
||||
"id": "{cardCode}",
|
||||
"idComponents":
|
||||
{
|
||||
"set": "setNumber",
|
||||
"region":
|
||||
{
|
||||
"id": "{shortRegionCode}",
|
||||
"name": "{regionName}"
|
||||
}
|
||||
}
|
||||
"name": "{name}",
|
||||
"type": "{type}",
|
||||
"subType": "{subType}",
|
||||
"superType": "{superType}",
|
||||
"description": "{description}",
|
||||
"keywords": [],
|
||||
"associatedCards": [{cardCode}, ...]
|
||||
"health": "{health}",
|
||||
"attack": "{attack}",
|
||||
"cost": "{cost}",
|
||||
"assets":
|
||||
{
|
||||
"gameAbsolutePath": "http://{cdn}/{bundleName}/set1/en_us/img/card/game/{cardCode}.png"
|
||||
}
|
||||
|
||||
},
|
||||
{...}
|
||||
]
|
7417
data/set2-en_us/en_us/data/set2-en_us.json
Normal file
7417
data/set2-en_us/en_us/data/set2-en_us.json
Normal file
File diff suppressed because it is too large
Load diff
5
data/set2-en_us/metadata.json
Normal file
5
data/set2-en_us/metadata.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"locales": [
|
||||
"en_us"
|
||||
]
|
||||
}
|
1
data/set3-en_us/COPYRIGHT
Normal file
1
data/set3-en_us/COPYRIGHT
Normal file
|
@ -0,0 +1 @@
|
|||
Copyright Riot Games, Inc. 2019
|
40
data/set3-en_us/README.md
Normal file
40
data/set3-en_us/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
metadata.json
|
||||
{
|
||||
"locales": ["{string}", ...],
|
||||
"clientHash": "{string}"
|
||||
"gameplayDataHash": "{string}",
|
||||
"timestamp": "{YYYYMMDDhhmm}",
|
||||
"patchlineRef": "{string}"
|
||||
}
|
||||
|
||||
cards.json
|
||||
[
|
||||
{
|
||||
"id": "{cardCode}",
|
||||
"idComponents":
|
||||
{
|
||||
"set": "setNumber",
|
||||
"region":
|
||||
{
|
||||
"id": "{shortRegionCode}",
|
||||
"name": "{regionName}"
|
||||
}
|
||||
}
|
||||
"name": "{name}",
|
||||
"type": "{type}",
|
||||
"subType": "{subType}",
|
||||
"superType": "{superType}",
|
||||
"description": "{description}",
|
||||
"keywords": [],
|
||||
"associatedCards": [{cardCode}, ...]
|
||||
"health": "{health}",
|
||||
"attack": "{attack}",
|
||||
"cost": "{cost}",
|
||||
"assets":
|
||||
{
|
||||
"gameAbsolutePath": "http://{cdn}/{bundleName}/set1/en_us/img/card/game/{cardCode}.png"
|
||||
}
|
||||
|
||||
},
|
||||
{...}
|
||||
]
|
11899
data/set3-en_us/en_us/data/set3-en_us.json
Normal file
11899
data/set3-en_us/en_us/data/set3-en_us.json
Normal file
File diff suppressed because it is too large
Load diff
5
data/set3-en_us/metadata.json
Normal file
5
data/set3-en_us/metadata.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"locales": [
|
||||
"en_us"
|
||||
]
|
||||
}
|
1
data/set4-en_us/COPYRIGHT
Normal file
1
data/set4-en_us/COPYRIGHT
Normal file
|
@ -0,0 +1 @@
|
|||
Copyright Riot Games, Inc. 2019
|
40
data/set4-en_us/README.md
Normal file
40
data/set4-en_us/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
metadata.json
|
||||
{
|
||||
"locales": ["{string}", ...],
|
||||
"clientHash": "{string}"
|
||||
"gameplayDataHash": "{string}",
|
||||
"timestamp": "{YYYYMMDDhhmm}",
|
||||
"patchlineRef": "{string}"
|
||||
}
|
||||
|
||||
cards.json
|
||||
[
|
||||
{
|
||||
"id": "{cardCode}",
|
||||
"idComponents":
|
||||
{
|
||||
"set": "setNumber",
|
||||
"region":
|
||||
{
|
||||
"id": "{shortRegionCode}",
|
||||
"name": "{regionName}"
|
||||
}
|
||||
}
|
||||
"name": "{name}",
|
||||
"type": "{type}",
|
||||
"subType": "{subType}",
|
||||
"superType": "{superType}",
|
||||
"description": "{description}",
|
||||
"keywords": [],
|
||||
"associatedCards": [{cardCode}, ...]
|
||||
"health": "{health}",
|
||||
"attack": "{attack}",
|
||||
"cost": "{cost}",
|
||||
"assets":
|
||||
{
|
||||
"gameAbsolutePath": "http://{cdn}/{bundleName}/set1/en_us/img/card/game/{cardCode}.png"
|
||||
}
|
||||
|
||||
},
|
||||
{...}
|
||||
]
|
14914
data/set4-en_us/en_us/data/set4-en_us.json
Normal file
14914
data/set4-en_us/en_us/data/set4-en_us.json
Normal file
File diff suppressed because it is too large
Load diff
5
data/set4-en_us/metadata.json
Normal file
5
data/set4-en_us/metadata.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"locales": [
|
||||
"en_us"
|
||||
]
|
||||
}
|
1
data/set5-en_us/COPYRIGHT
Normal file
1
data/set5-en_us/COPYRIGHT
Normal file
|
@ -0,0 +1 @@
|
|||
Copyright Riot Games, Inc. 2019
|
40
data/set5-en_us/README.md
Normal file
40
data/set5-en_us/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
metadata.json
|
||||
{
|
||||
"locales": ["{string}", ...],
|
||||
"clientHash": "{string}"
|
||||
"gameplayDataHash": "{string}",
|
||||
"timestamp": "{YYYYMMDDhhmm}",
|
||||
"patchlineRef": "{string}"
|
||||
}
|
||||
|
||||
cards.json
|
||||
[
|
||||
{
|
||||
"id": "{cardCode}",
|
||||
"idComponents":
|
||||
{
|
||||
"set": "setNumber",
|
||||
"region":
|
||||
{
|
||||
"id": "{shortRegionCode}",
|
||||
"name": "{regionName}"
|
||||
}
|
||||
}
|
||||
"name": "{name}",
|
||||
"type": "{type}",
|
||||
"subType": "{subType}",
|
||||
"superType": "{superType}",
|
||||
"description": "{description}",
|
||||
"keywords": [],
|
||||
"associatedCards": [{cardCode}, ...]
|
||||
"health": "{health}",
|
||||
"attack": "{attack}",
|
||||
"cost": "{cost}",
|
||||
"assets":
|
||||
{
|
||||
"gameAbsolutePath": "http://{cdn}/{bundleName}/set1/en_us/img/card/game/{cardCode}.png"
|
||||
}
|
||||
|
||||
},
|
||||
{...}
|
||||
]
|
15596
data/set5-en_us/en_us/data/set5-en_us.json
Normal file
15596
data/set5-en_us/en_us/data/set5-en_us.json
Normal file
File diff suppressed because it is too large
Load diff
5
data/set5-en_us/metadata.json
Normal file
5
data/set5-en_us/metadata.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"locales": [
|
||||
"en_us"
|
||||
]
|
||||
}
|
1
data/set6-en_us/COPYRIGHT
Normal file
1
data/set6-en_us/COPYRIGHT
Normal file
|
@ -0,0 +1 @@
|
|||
Copyright Riot Games, Inc. 2019
|
40
data/set6-en_us/README.md
Normal file
40
data/set6-en_us/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
metadata.json
|
||||
{
|
||||
"locales": ["{string}", ...],
|
||||
"clientHash": "{string}"
|
||||
"gameplayDataHash": "{string}",
|
||||
"timestamp": "{YYYYMMDDhhmm}",
|
||||
"patchlineRef": "{string}"
|
||||
}
|
||||
|
||||
cards.json
|
||||
[
|
||||
{
|
||||
"id": "{cardCode}",
|
||||
"idComponents":
|
||||
{
|
||||
"set": "setNumber",
|
||||
"region":
|
||||
{
|
||||
"id": "{shortRegionCode}",
|
||||
"name": "{regionName}"
|
||||
}
|
||||
}
|
||||
"name": "{name}",
|
||||
"type": "{type}",
|
||||
"subType": "{subType}",
|
||||
"superType": "{superType}",
|
||||
"description": "{description}",
|
||||
"keywords": [],
|
||||
"associatedCards": [{cardCode}, ...]
|
||||
"health": "{health}",
|
||||
"attack": "{attack}",
|
||||
"cost": "{cost}",
|
||||
"assets":
|
||||
{
|
||||
"gameAbsolutePath": "http://{cdn}/{bundleName}/set1/en_us/img/card/game/{cardCode}.png"
|
||||
}
|
||||
|
||||
},
|
||||
{...}
|
||||
]
|
7934
data/set6-en_us/en_us/data/set6-en_us.json
Normal file
7934
data/set6-en_us/en_us/data/set6-en_us.json
Normal file
File diff suppressed because it is too large
Load diff
5
data/set6-en_us/metadata.json
Normal file
5
data/set6-en_us/metadata.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"locales": [
|
||||
"en_us"
|
||||
]
|
||||
}
|
1
data/set6cde-en_us/COPYRIGHT
Normal file
1
data/set6cde-en_us/COPYRIGHT
Normal file
|
@ -0,0 +1 @@
|
|||
Copyright Riot Games, Inc. 2019
|
40
data/set6cde-en_us/README.md
Normal file
40
data/set6cde-en_us/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
metadata.json
|
||||
{
|
||||
"locales": ["{string}", ...],
|
||||
"clientHash": "{string}"
|
||||
"gameplayDataHash": "{string}",
|
||||
"timestamp": "{YYYYMMDDhhmm}",
|
||||
"patchlineRef": "{string}"
|
||||
}
|
||||
|
||||
cards.json
|
||||
[
|
||||
{
|
||||
"id": "{cardCode}",
|
||||
"idComponents":
|
||||
{
|
||||
"set": "setNumber",
|
||||
"region":
|
||||
{
|
||||
"id": "{shortRegionCode}",
|
||||
"name": "{regionName}"
|
||||
}
|
||||
}
|
||||
"name": "{name}",
|
||||
"type": "{type}",
|
||||
"subType": "{subType}",
|
||||
"superType": "{superType}",
|
||||
"description": "{description}",
|
||||
"keywords": [],
|
||||
"associatedCards": [{cardCode}, ...]
|
||||
"health": "{health}",
|
||||
"attack": "{attack}",
|
||||
"cost": "{cost}",
|
||||
"assets":
|
||||
{
|
||||
"gameAbsolutePath": "http://{cdn}/{bundleName}/set1/en_us/img/card/game/{cardCode}.png"
|
||||
}
|
||||
|
||||
},
|
||||
{...}
|
||||
]
|
12056
data/set6cde-en_us/en_us/data/set6cde-en_us.json
Normal file
12056
data/set6cde-en_us/en_us/data/set6cde-en_us.json
Normal file
File diff suppressed because it is too large
Load diff
5
data/set6cde-en_us/metadata.json
Normal file
5
data/set6cde-en_us/metadata.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"locales": [
|
||||
"en_us"
|
||||
]
|
||||
}
|
|
@ -1,7 +1,119 @@
|
|||
//! This bot isn't yet available.
|
||||
//! # [Patched Porobot#7556]
|
||||
//!
|
||||
//! Bot for searching and sending Legends of Runeterra cards and decks in Discord channels
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! [Patched Porobot#7556] is based on slash commands; you can add it to your server by clicking on the following link:
|
||||
//!
|
||||
//! * [Add to Server](https://discord.com/api/oauth2/authorize?client_id=1071989978743193672&scope=applications.commands)
|
||||
//!
|
||||
//! After adding it to your server, you can use its commands in channels by entering `/` in the message box and then selecting the command you want to send to the bot.
|
||||
//!
|
||||
//! ### Card queries
|
||||
//!
|
||||
//! You can search for a card by specifying the `/card` command and inserting the card's name as the `query` parameter:
|
||||
//!
|
||||
//! ```text
|
||||
//! /card query:shadowshift
|
||||
//! ```
|
||||
//!
|
||||
//! You can specify multiple words to find the card that contains all of them:
|
||||
//!
|
||||
//! ```text
|
||||
//! /card query:mighty poro
|
||||
//! ```
|
||||
//!
|
||||
//! Terms will be searched in multiple fields, so to find [Daring Poro](https://leagueoflegends.fandom.com/wiki/Daring_Poro_(Legends_of_Runeterra)) you may search for:
|
||||
//!
|
||||
//! ```text
|
||||
//! /card query:piltover poro
|
||||
//! ```
|
||||
//!
|
||||
//! Since different Champion cards have the same name, you may disambiguate them by entering `level:N` as part of the query:
|
||||
//!
|
||||
//! ```
|
||||
//! /card query:braum level:2
|
||||
//! ```
|
||||
//!
|
||||
//! #### Conjunctions
|
||||
//!
|
||||
//! By default, all terms in the query are joined by `AND` conjuctions, meaning that only cards containing **all** of the terms are retrieved.
|
||||
//!
|
||||
//! If you want to find cards matching **any** of the terms, you'll have to manually join them with the `OR` conjuction:
|
||||
//!
|
||||
//! ```text
|
||||
//! /card query:progress OR heimerdinger
|
||||
//! ```
|
||||
//!
|
||||
//! To have both `AND`s and `OR`s in the same query you'll need to specify all of them explicitly:
|
||||
//!
|
||||
//! ```text
|
||||
//! /card query:von AND yipp OR cat
|
||||
//! ```
|
||||
//!
|
||||
//! #### Fields
|
||||
//!
|
||||
//! You can perform searches about specific card properties:
|
||||
//!
|
||||
//! ```text
|
||||
//! /card query:cost:4 attack:7 health:7
|
||||
//! ```
|
||||
//!
|
||||
//! Conjunctions are supported even when searching by specific fields:
|
||||
//!
|
||||
//! ```text
|
||||
//! /card query:name:Bard OR description:Chime
|
||||
//! ```
|
||||
//!
|
||||
//! ##### Supported fields
|
||||
//!
|
||||
//! [Patched Porobot#7556] supports the various fields for searching cards: see [**this table**](patched_porobot::search::cardsearch::CardSearchEngine::schema) for a list of all of them!
|
||||
//!
|
||||
//! #### Ranges
|
||||
//!
|
||||
//! Finally, you can request specific ranges for your search using square brackets and the `TO` keyword:
|
||||
//!
|
||||
//! ```text
|
||||
//! @patchedporobot attack:[8 TO 12]
|
||||
//! ```
|
||||
//!
|
||||
//! #### Query parser
|
||||
//!
|
||||
//! Since [Patched Porobot#7556] uses [`tantivy`] internally, you might find more information on even more advanced queries in the [documentation of their `QueryParser`](tantivy::query::QueryParser)!
|
||||
//!
|
||||
//! ### Deck parsing
|
||||
//!
|
||||
//! You can have [Patched Porobot#7556] display a deck and its cards by specifying the `/deck` command and inserting the deck code as the `code` parameter:
|
||||
//!
|
||||
//! ```text
|
||||
//! /deck code:CIBQCAICAQAQGBQIBEBAMBAJBMGBUHJNGE4AEAIBAIYQEAQGEU2QCAIBAIUQ
|
||||
//! ```
|
||||
//!
|
||||
//! #### Named decks
|
||||
//!
|
||||
//! Optionally, you may add a name to your deck, which will be displayed above the deck code:
|
||||
//!
|
||||
//! ```text
|
||||
//! /deck code:CIBQCAICAQAQGBQIBEBAMBAJBMGBUHJNGE4AEAIBAIYQEAQGEU2QCAIBAIUQ name:Gimbo's Depths
|
||||
//! ```
|
||||
//!
|
||||
//! ### Permissions
|
||||
//!
|
||||
//! You can configure the bot's permissions by using Discord's Command Permissions system!
|
||||
//!
|
||||
//! You can access it via `SERVER NAME` → `Server Settings` → `Integrations` → [Patched Porobot#7556] `Manage`.
|
||||
//!
|
||||
//! See the following flowchart to understand how Command Permissions work:
|
||||
//!
|
||||
//! * [Flowchart](https://cdn.discordapp.com/attachments/697138785317814292/1042878162901672048/flowchart-for-new-permissions.png)
|
||||
//!
|
||||
//! [Patched Porobot#7556]: https://discord.com/api/oauth2/authorize?client_id=1071989978743193672&scope=applications.commands
|
||||
|
||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/Steffo99/patched-porobot/main/icon.png")]
|
||||
|
||||
fn main() {
|
||||
todo!();
|
||||
#[doc(hidden)]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
patched_porobot::discord::main::main().await;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ impl CoreBundle {
|
|||
let locale = metadata.locale().ok_or(LoadingError::GettingLocale)?;
|
||||
|
||||
let globals_path = &bundle_path
|
||||
.join(&locale)
|
||||
.join(locale)
|
||||
.join("data")
|
||||
.join(format!("globals-{}.json", &locale));
|
||||
|
||||
|
@ -61,6 +61,23 @@ impl CoreBundle {
|
|||
}
|
||||
|
||||
|
||||
/// Create [`globals::LocalizedGlobalsIndexes`] from the core bundle in the current working directory.
|
||||
///
|
||||
/// This function tries to load data from the first directory matching the [glob] `./data/core-*`.
|
||||
pub fn create_globalindexes_from_wd() -> globals::LocalizedGlobalsIndexes {
|
||||
let path = glob::glob("./data/core-*")
|
||||
.expect("glob to be a valid glob")
|
||||
.filter_map(Some)
|
||||
.find_map(Result::ok)
|
||||
.expect("a valid core bundle to exist");
|
||||
|
||||
let core = CoreBundle::load(&path)
|
||||
.expect("to be able to load `core-en_us` bundle");
|
||||
|
||||
globals::LocalizedGlobalsIndexes::from(core.globals)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -2,13 +2,15 @@
|
|||
|
||||
use super::format::DeckCodeFormat;
|
||||
use crate::data::deckcode::version::{DeckCodeVersion, DeckCodeVersioned};
|
||||
use crate::data::setbundle::card::{Card, CardIndex};
|
||||
use crate::data::setbundle::code::CardCode;
|
||||
use crate::data::setbundle::region::CardRegion;
|
||||
use crate::data::setbundle::set::CardSet;
|
||||
use itertools::Itertools;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use varint_rs::{VarintReader, VarintWriter};
|
||||
use crate::data::setbundle::supertype::CardSupertype;
|
||||
|
||||
/// A unshuffled Legends of Runeterra card deck.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
@ -408,6 +410,170 @@ impl Deck {
|
|||
|
||||
Ok(Self::encode_code(&cursor.into_inner()))
|
||||
}
|
||||
|
||||
/// Get an [`Iterator`] of all Champion [`Card`]s in the deck.
|
||||
pub fn champions<'a>(&'a self, cards: &'a CardIndex) -> impl Iterator<Item = &'a Card> {
|
||||
self.contents.keys()
|
||||
.filter_map(|cc| cc.to_card(cards))
|
||||
.filter(|c| c.supertype == CardSupertype::Champion)
|
||||
}
|
||||
|
||||
/// Count the number of copies of the given card present in this deck.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use patched_porobot::deck;
|
||||
/// use patched_porobot::data::deckcode::deck::Deck;
|
||||
/// use patched_porobot::data::setbundle::code::CardCode;
|
||||
///
|
||||
/// let d: Deck = deck![
|
||||
/// "01DE002": 40,
|
||||
/// ];
|
||||
///
|
||||
/// let copies = d.copies_of(&CardCode::from("01DE002".to_string()));
|
||||
/// assert_eq!(copies, 40);
|
||||
/// ```
|
||||
pub fn copies_of(&self, code: &CardCode) -> u32 {
|
||||
self.contents.get(code).map_or(0, ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the number of cards in the deck.
|
||||
///
|
||||
/// In the *Standard* and *Singleton* formats, this is never more than 40.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use patched_porobot::deck;
|
||||
/// use patched_porobot::data::deckcode::deck::Deck;
|
||||
/// use patched_porobot::data::setbundle::card::CardIndex;
|
||||
/// use patched_porobot::data::setbundle::create_cardindex_from_wd;
|
||||
///
|
||||
/// let index: CardIndex = create_cardindex_from_wd();
|
||||
/// let deck: Deck = deck!("CECQCAQCA4AQIAYKAIAQGLRWAQAQECAPEUXAIAQDAEBQOCIBAIAQEMJYAA");
|
||||
/// assert_eq!(deck.card_count(), 40);
|
||||
///
|
||||
pub fn card_count(&self) -> u32 {
|
||||
self.contents.values().sum()
|
||||
}
|
||||
|
||||
/// Get the number of champion cards in the deck.
|
||||
///
|
||||
/// In the *Standard* and *Singleton* format, this is never more than 6.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use patched_porobot::deck;
|
||||
/// use patched_porobot::data::deckcode::deck::Deck;
|
||||
/// use patched_porobot::data::setbundle::card::CardIndex;
|
||||
/// use patched_porobot::data::setbundle::create_cardindex_from_wd;
|
||||
///
|
||||
/// let index: CardIndex = create_cardindex_from_wd();
|
||||
/// let deck: Deck = deck!("CECQCAQCA4AQIAYKAIAQGLRWAQAQECAPEUXAIAQDAEBQOCIBAIAQEMJYAA");
|
||||
/// assert_eq!(deck.champions_count(&index), 6);
|
||||
/// ```
|
||||
pub fn champions_count(&self, cards: &CardIndex) -> u32 {
|
||||
self.champions(cards)
|
||||
.map(|c| &c.code)
|
||||
.map(|cc| self.copies_of(cc))
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Find the first possible set of regions that the [`Deck`] fits in.
|
||||
///
|
||||
/// Lower amounts of regions are preferred: the limit will be increased from 1 up to `limit` until a valid solution is found.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This method traverses the tree of possible region selections until one is found.
|
||||
///
|
||||
/// The time required to run this function may grow exponentially with the amount of cards in the deck!
|
||||
pub fn regions(&self, card_index: &CardIndex, limit: usize) -> Option<HashSet<CardRegion>> {
|
||||
let cards: Vec<&Card> = self.contents.keys()
|
||||
.flat_map(|cc| cc.to_card(card_index))
|
||||
.collect();
|
||||
|
||||
for n in 1..=limit {
|
||||
let result = Self::regions_recursive_first_limit(cards.as_slice(), HashSet::new(), n);
|
||||
|
||||
if result.is_some() {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Find the first possible set of regions that covers all given [`Card`]s.
|
||||
///
|
||||
/// This function is *recursive*: the `cards` parameter holds the [`Card`]s left to process, while the `regions` parameter holds the regions found so far, and the `limit` parameter holds the size of the `regions` set to stop the recursion at.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This method traverses the tree of possible region selections until one is found.
|
||||
///
|
||||
/// The time required to run this function may grow exponentially with the size of `cards`!
|
||||
fn regions_recursive_first_limit(cards: &[&Card], regions: HashSet<CardRegion>, limit: usize) -> Option<HashSet<CardRegion>> {
|
||||
match cards.get(0) {
|
||||
None => Some(regions),
|
||||
Some(card) => {
|
||||
card.regions.iter()
|
||||
.map(|region| {
|
||||
match region {
|
||||
CardRegion::Unsupported => {
|
||||
None
|
||||
}
|
||||
CardRegion::Runeterra => {
|
||||
Self::regions_recursive_first_limit(&cards[1..], regions.clone(), limit)
|
||||
}
|
||||
_ => {
|
||||
let mut regions = regions.clone();
|
||||
let inserted = regions.insert(*region);
|
||||
match inserted && regions.len() > limit {
|
||||
true => None,
|
||||
false => Self::regions_recursive_first_limit(&cards[1..], regions, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.find(Option::is_some)
|
||||
.unwrap_or(None)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the [`Deck`] is legal for play in the *Standard* format.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// - `None` if the deck is not legal for *Standard* play.
|
||||
/// - `Some(regions)` if the deck is legal for *Standard* play considering the specified region set.
|
||||
pub fn standard(&self, cards: &CardIndex) -> Option<HashSet<CardRegion>> {
|
||||
let copies_limit = self.contents.values().all(|n| n <= &3);
|
||||
let cards_limit = self.card_count() == 40;
|
||||
let champions_limit = self.champions_count(cards) <= 6;
|
||||
let regions = self.regions(cards, 2);
|
||||
|
||||
match copies_limit && cards_limit && champions_limit {
|
||||
false => None,
|
||||
true => regions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the [`Deck`] is legal for play in the *Singleton* format.
|
||||
pub fn singleton(&self, cards: &CardIndex) -> Option<HashSet<CardRegion>> {
|
||||
let copies_limit = self.contents.values().all(|n| n <= &1);
|
||||
let cards_limit = self.card_count() == 40;
|
||||
let champions_limit = self.champions_count(cards) <= 6;
|
||||
let regions = self.regions(cards, 3);
|
||||
|
||||
match copies_limit && cards_limit && champions_limit {
|
||||
false => None,
|
||||
true => regions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error occoured while decoding a [`Deck`] from a code.
|
||||
|
@ -448,9 +614,26 @@ pub type DeckDecodingResult<T> = Result<T, DeckDecodingError>;
|
|||
/// The [`Result`] of a [`Deck`] **encoding** operation, for example [`Deck::to_code`].
|
||||
pub type DeckEncodingResult<T> = Result<T, DeckEncodingError>;
|
||||
|
||||
/// Macro to build a deck from card code strings and quantities.
|
||||
/// Macro to build a deck.
|
||||
///
|
||||
/// # Example
|
||||
/// Useful to quickly build [`Deck`]s from trusted input, such as when creating tests.
|
||||
///
|
||||
/// It can build a deck:
|
||||
///
|
||||
/// - from a deck code;
|
||||
/// - from card code strings and quantities.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the deck code is not valid.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use patched_porobot::deck;
|
||||
///
|
||||
/// let _my_deck = deck!("CECQCAQCA4AQIAYKAIAQGLRWAQAQECAPEUXAIAQDAEBQOCIBAIAQEMJYAA");
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// use patched_porobot::deck;
|
||||
|
@ -480,13 +663,17 @@ pub type DeckEncodingResult<T> = Result<T, DeckEncodingError>;
|
|||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! deck {
|
||||
[$($cd:literal: $qty:literal),* $(,)?] => {
|
||||
[ $($cd:literal: $qty:expr),* $(,)? ] => {
|
||||
$crate::data::deckcode::deck::Deck {
|
||||
contents: std::collections::HashMap::from([
|
||||
$(($crate::data::setbundle::code::CardCode { full: $cd.to_string() }, $qty),)*
|
||||
])
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
( $code:expr ) => {
|
||||
$crate::data::deckcode::deck::Deck::from_code($code).expect("deck code created with deck!() to be valid")
|
||||
};
|
||||
}
|
||||
|
||||
#[rustfmt::skip::macros(test_de_ser, test_ser_de)]
|
||||
|
@ -653,5 +840,64 @@ mod tests {
|
|||
"02BW012": 69,
|
||||
]);
|
||||
|
||||
//test_ser_de!(test_ser_de_, deck![]);
|
||||
// test_ser_de!(test_ser_de_, deck![]);
|
||||
|
||||
macro_rules! test_legality {
|
||||
( $id:ident, $deck:expr, $check:path, $assert:expr ) => {
|
||||
#[test]
|
||||
fn $id() {
|
||||
let index = $crate::data::setbundle::create_cardindex_from_wd();
|
||||
let deck: Deck = $deck;
|
||||
let result = $check(&deck, &index).is_some();
|
||||
assert_eq!(result, $assert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test_legality!(
|
||||
test_legality_standard_lonelyporo1,
|
||||
deck!("CEAAAAIBAEAQQ"),
|
||||
Deck::standard, false
|
||||
);
|
||||
test_legality!(
|
||||
test_legality_standard_twistedshrimp,
|
||||
deck!("CICACBAFAEBAGBQICABQCBJLF4YQOAQGAQEQYEQUDITAAAIBAMCQO"),
|
||||
Deck::standard, true
|
||||
);
|
||||
test_legality!(
|
||||
test_legality_standard_poros,
|
||||
deck!("CQDQCAQBAMAQGAICAECACDYCAECBIFYCAMCBEEYCAUFIYANAAEBQCAIICA2QCAQBAEVTSAA"),
|
||||
Deck::standard, true
|
||||
);
|
||||
test_legality!(
|
||||
test_legality_standard_sand,
|
||||
deck!("CMBAGBAHANTXEBQBAUCAOFJGFIYQEAIBAUOQIBAHGM5HM6ICAECAOOYCAECRSGY"),
|
||||
Deck::standard, true
|
||||
);
|
||||
|
||||
test_legality!(
|
||||
test_legality_singleton_lonelyporo1,
|
||||
deck!("CEAAAAIBAEAQQ"),
|
||||
Deck::singleton, false
|
||||
);
|
||||
test_legality!(
|
||||
test_legality_singleton_twistedshrimp,
|
||||
deck!("CICACBAFAEBAGBQICABQCBJLF4YQOAQGAQEQYEQUDITAAAIBAMCQO"),
|
||||
Deck::singleton, false
|
||||
);
|
||||
test_legality!(
|
||||
test_legality_singleton_poros,
|
||||
deck!("CQDQCAQBAMAQGAICAECACDYCAECBIFYCAMCBEEYCAUFIYANAAEBQCAIICA2QCAQBAEVTSAA"),
|
||||
Deck::singleton, false
|
||||
);
|
||||
test_legality!(
|
||||
test_legality_singleton_sand,
|
||||
deck!("CMBAGBAHANTXEBQBAUCAOFJGFIYQEAIBAUOQIBAHGM5HM6ICAECAOOYCAECRSGY"),
|
||||
Deck::singleton, false
|
||||
);
|
||||
test_legality!(
|
||||
test_legality_singleton_paltri,
|
||||
deck!("CQAAADABAICACAIFBLAACAIFAEHQCBQBEQBAGBADAQBAIAIKBUBAKBAWDUBQIBACA4GAMAIBAMCAYHJBGADAMBAOCQKRMKBLA4AQIAQ3D4QSIKZYBACAODJ3JRIW3AABQIAYUAI"),
|
||||
Deck::singleton, true
|
||||
);
|
||||
}
|
||||
|
|
|
@ -77,9 +77,6 @@ impl DeckCodeVersioned for CardRegion {
|
|||
CardRegion::BandleCity => Some(DeckCodeVersion::V4),
|
||||
|
||||
CardRegion::Runeterra => Some(DeckCodeVersion::V5),
|
||||
CardRegion::Jhin => Some(DeckCodeVersion::V5),
|
||||
CardRegion::Evelynn => Some(DeckCodeVersion::V5),
|
||||
CardRegion::Bard => Some(DeckCodeVersion::V5),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
|
|
|
@ -30,14 +30,12 @@ pub struct CardArt {
|
|||
}
|
||||
|
||||
impl CardArt {
|
||||
/// URL to the `.jpg` image of the `en_us` locale of the rendered card, via `poro.steffo.eu`.
|
||||
///
|
||||
/// Please do not overload this endpoint, as it currently does not use a CDN!
|
||||
/// URL to the `.jpg` image of the `en_us` locale of the rendered card, via my custom S3 mirror.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```text
|
||||
/// https://poro.steffo.eu/set1-en_us/en_us/img/cards/01DE001.jpg
|
||||
/// https://objectstorage.eu-milan-1.oraclecloud.com/n/axxdmk4y92aq/b/porobot-storage/o/set1-en_us/en_us/img/cards/01DE001.jpg
|
||||
/// ```
|
||||
///
|
||||
pub fn card_jpg(&self) -> String {
|
||||
|
@ -51,19 +49,17 @@ impl CardArt {
|
|||
GET_JPG
|
||||
.replace_all(
|
||||
&self.card_png,
|
||||
"https://poro.steffo.eu/$bundle-$locale/$locale/img/cards/$code.jpg",
|
||||
"https://objectstorage.eu-milan-1.oraclecloud.com/n/axxdmk4y92aq/b/porobot-storage/o/$bundle-$locale/$locale/img/cards/$code.jpg",
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// URL to the `.jpg` image of the `en_us` locale of the full card art, via `poro.steffo.eu`.
|
||||
///
|
||||
/// Please do not overload this endpoint, as it currently does not use a CDN!
|
||||
/// URL to the `.jpg` image of the `en_us` locale of the full card art, via my custom S3 mirror.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```text
|
||||
/// https://poro.steffo.eu/set1-en_us/en_us/img/cards/01DE001-full.jpg
|
||||
/// https://objectstorage.eu-milan-1.oraclecloud.com/n/axxdmk4y92aq/b/porobot-storage/o/set1-en_us/en_us/img/cards/01DE001-full.jpg
|
||||
/// ```
|
||||
///
|
||||
pub fn full_jpg(&self) -> String {
|
||||
|
@ -77,7 +73,7 @@ impl CardArt {
|
|||
GET_JPG
|
||||
.replace_all(
|
||||
&self.full_png,
|
||||
"https://poro.steffo.eu/$bundle-$locale/$locale/img/cards/$code.jpg",
|
||||
"https://objectstorage.eu-milan-1.oraclecloud.com/n/axxdmk4y92aq/b/porobot-storage/o/$bundle-$locale/$locale/img/cards/$code.jpg",
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
@ -103,11 +99,11 @@ mod tests {
|
|||
|
||||
assert_eq!(
|
||||
art.card_jpg(),
|
||||
"https://poro.steffo.eu/set1-en_us/en_us/img/cards/01DE001.jpg"
|
||||
"https://objectstorage.eu-milan-1.oraclecloud.com/n/axxdmk4y92aq/b/porobot-storage/o/set1-en_us/en_us/img/cards/01DE001.jpg"
|
||||
);
|
||||
assert_eq!(
|
||||
art.full_jpg(),
|
||||
"https://poro.steffo.eu/set1-en_us/en_us/img/cards/01DE001-full.jpg"
|
||||
"https://objectstorage.eu-milan-1.oraclecloud.com/n/axxdmk4y92aq/b/porobot-storage/o/set1-en_us/en_us/img/cards/01DE001-full.jpg"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,7 +142,7 @@ pub struct Card {
|
|||
/// The subtypes the card belongs to, such as *Poro* or *Yordle*.
|
||||
pub subtypes: Vec<CardSubtype>,
|
||||
|
||||
/// The supertype the card belongs to, such as *Champion*.
|
||||
/// The supertype the card belongs to, such as [`Champion`](CardSupertype::Champion) or [`None`](CardSupertype::None).
|
||||
pub supertype: CardSupertype,
|
||||
}
|
||||
|
||||
|
@ -271,7 +271,7 @@ mod tests {
|
|||
localized_flavor_text: String::from("The priestess' pupils were blown wide, and her hand trembled with nervous excitement. She was ready. This was the single moment Evelynn craved more than any other. She grinned, and slowly shed her visage. Then, as always, the screaming began."),
|
||||
artist_name: String::from("Kudos Productions"),
|
||||
subtypes: vec![],
|
||||
supertype: String::from("Champion"),
|
||||
supertype: CardSupertype::Champion,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use crate::data::setbundle::card::{Card, CardIndex};
|
||||
|
||||
/// The internal code of a [Card](super::card::Card).
|
||||
/// The internal code of a [`Card`].
|
||||
///
|
||||
/// It is a ASCII string composed of the following segments:
|
||||
/// - `0..2`: set;
|
||||
|
@ -67,8 +67,13 @@ impl CardCode {
|
|||
/// The token segment of the code.
|
||||
///
|
||||
/// In valid codes, it may either be an empty string, or 2-ASCII-characters long.
|
||||
pub fn token(&self) -> &str {
|
||||
&self.full[7..9]
|
||||
pub fn token(&self) -> Option<&str> {
|
||||
if self.full.len() >= 9 {
|
||||
Some(&self.full[7..9])
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new card code given the set and region strings and the card number.
|
||||
|
@ -98,3 +103,10 @@ impl From<String> for CardCode {
|
|||
CardCode { full }
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the card code from a [`Card`].
|
||||
impl From<Card> for CardCode {
|
||||
fn from(c: Card) -> Self {
|
||||
c.code
|
||||
}
|
||||
}
|
|
@ -359,6 +359,85 @@ impl CardKeyword {
|
|||
) -> Option<&'hm LocalizedCardKeyword> {
|
||||
hm.get(self)
|
||||
}
|
||||
|
||||
/// Get the Discord emoji code associated with this [`CardKeyword`].
|
||||
pub fn discord_emoji(&self) -> &'static str {
|
||||
match self {
|
||||
CardKeyword::DoubleAttack => "<:doublestrike:1056023011590942770>",
|
||||
CardKeyword::Ephemeral => "<:ephemeral:1056023006876545105>",
|
||||
CardKeyword::Equipment => "<:equipment:1056022999184183317>",
|
||||
CardKeyword::Fast => "<:fast:1056022992536219728>",
|
||||
CardKeyword::Fated => "<:fated:1056022989507940414>",
|
||||
CardKeyword::Fearsome => "<:fearsome:1056022987041673367>",
|
||||
CardKeyword::Focus => "<:focus:1056022982377615390>",
|
||||
CardKeyword::Formidable => "<:formidable:1056022980007837826>",
|
||||
CardKeyword::Frostbite => "<:frostbite:1056022975436038164>",
|
||||
CardKeyword::Fury => "<:fury:1056022973636694076>",
|
||||
CardKeyword::ClobberNoEmptySlotRequirement => "",
|
||||
CardKeyword::Hallowed => "<:hallowed:1056022965914968074>",
|
||||
CardKeyword::Immobile => "<:immobile:1056022961582248026>",
|
||||
CardKeyword::Impact => "<:impact:1056022959279591485>",
|
||||
CardKeyword::Landmark => "<:landmarkvisualonly:1056022936080883783>",
|
||||
CardKeyword::LastBreath => "<:lastbreath:1056022933736263680>",
|
||||
CardKeyword::Lifesteal => "<:lifesteal:1056022931395842160>",
|
||||
CardKeyword::Lurk => "<:lurker:1056022929357426740>",
|
||||
CardKeyword::Overwhelm => "<:overwhelm:1056022921639907420>",
|
||||
CardKeyword::SpellOverwhelm => "<:overwhelm:1056022921639907420>",
|
||||
CardKeyword::QuickAttack => "<:quickstrike:1056022909535125548>",
|
||||
CardKeyword::Regeneration => "<:regeneration:1056022897396809769>",
|
||||
CardKeyword::CantBlock => "<:reckless:1056022900651602092>",
|
||||
CardKeyword::Scout => "<:scout:1056022889389903962>",
|
||||
CardKeyword::Silenced => "<:silenced:1056022882121158657>",
|
||||
CardKeyword::SilenceIndividualKeyword => "<:silenced:1056022882121158657>",
|
||||
CardKeyword::Skill => "<:skillmark:1056022880158228600>",
|
||||
CardKeyword::Slow => "<:slow:1056022877868142662>",
|
||||
CardKeyword::SpellShield => "<:spellshield:1056022875565465640>",
|
||||
CardKeyword::Stun => "<:stunned:1056022873279561759>",
|
||||
CardKeyword::Tough => "<:tough:1056022863066431591>",
|
||||
CardKeyword::Vulnerable => "<:vulnerable:1056022853411143691>",
|
||||
CardKeyword::Attach => "<:attach:1056024270712614974>",
|
||||
CardKeyword::Attune => "<:attune:1056024273401171999>",
|
||||
CardKeyword::Augment => "<:augment:1056024275628335114>",
|
||||
CardKeyword::AuraVisualFakeKeyword => "<:aura:1056024278212038756>",
|
||||
CardKeyword::Barrier => "<:barrier:1056024286177013900>",
|
||||
CardKeyword::Burst => "<:burst:1056024291457638492>",
|
||||
// CardKeyword::??? => "<:capture:1056024295190577153>",
|
||||
CardKeyword::Challenger => "<:challenger:1056024299179347988>",
|
||||
CardKeyword::Deep => "<:deep:1056024321593720923>",
|
||||
CardKeyword::Elusive => "<:elusive:1056024324110299176>",
|
||||
CardKeyword::Evolve => "<:evolve:1056024326572355654>",
|
||||
CardKeyword::Fleeting => "<:fleeting:1056024328753397862>",
|
||||
CardKeyword::Imbue => "<:imbue:1056024724314001449>",
|
||||
CardKeyword::Countdown => "<:fleeting:1056024328753397862>", // TODO: Is this correct?
|
||||
CardKeyword::OnPlay => "<:skillmark:1056022880158228600>",
|
||||
CardKeyword::Shurima => "<:shurima:1056022884616765500>",
|
||||
CardKeyword::Noxus => "<:noxus:1056022924169064498>",
|
||||
CardKeyword::Demacia => "<:demacia:1056023014128484412>",
|
||||
CardKeyword::Runeterra => "<:runeterra:1056022895031238727>",
|
||||
CardKeyword::Targon => "<:targon:1056022866174418944>",
|
||||
CardKeyword::ShadowIsles => "<:shadowisles:1056022886848135292>",
|
||||
CardKeyword::PiltoverZaun => "<:piltoverzaun:1056022918959734835>",
|
||||
CardKeyword::Ionia => "<:ionia:1056022949569777708>",
|
||||
CardKeyword::BandleCity => "<:bandlecity:1056024280493735976>",
|
||||
CardKeyword::Bilgewater => "<:bilgewater:1056024288215437484>",
|
||||
CardKeyword::Nab => "",
|
||||
CardKeyword::Enlightened => "",
|
||||
CardKeyword::Invoke => "",
|
||||
CardKeyword::Boon => "",
|
||||
CardKeyword::Trap => "",
|
||||
CardKeyword::Drain => "",
|
||||
CardKeyword::Recall => "",
|
||||
CardKeyword::Weakest => "",
|
||||
CardKeyword::Support => "",
|
||||
CardKeyword::Obliterate => "",
|
||||
CardKeyword::Nightfall => "",
|
||||
CardKeyword::Daybreak => "",
|
||||
CardKeyword::Plunder => "",
|
||||
CardKeyword::BlockElusive => "",
|
||||
CardKeyword::Flow => "",
|
||||
CardKeyword::Unsupported => "<:invaliddeck:1056022952396730438>",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
use super::anybundle::metadata::BundleMetadata;
|
||||
use crate::data::anybundle::outcomes::{LoadingError, LoadingResult};
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub mod art;
|
||||
pub mod card;
|
||||
|
@ -45,7 +45,7 @@ impl SetBundle {
|
|||
let mut json_filename = name.to_os_string();
|
||||
json_filename.push(".json");
|
||||
|
||||
&bundle_path.join(&locale).join("data").join(&json_filename)
|
||||
&bundle_path.join(locale).join("data").join(&json_filename)
|
||||
};
|
||||
|
||||
let cards = File::open(data_path).map_err(LoadingError::OpeningFile)?;
|
||||
|
@ -115,3 +115,37 @@ mod tests {
|
|||
test_fetch!(test_fetch_latest_en_us_set6, "latest", "en_us", "set6");
|
||||
test_fetch!(test_fetch_latest_en_us_set6cde, "latest", "en_us", "set6cde");
|
||||
}
|
||||
|
||||
|
||||
/// Create a [`card::CardIndex`] from set bundles in the given paths.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If any of the required files cannot be loaded (see [`SetBundle::load`]).
|
||||
pub fn create_cardindex_from_paths(paths: impl Iterator<Item = PathBuf>) -> card::CardIndex {
|
||||
let mut index = card::CardIndex::new();
|
||||
for path in paths {
|
||||
let set = SetBundle::load(&path).expect("to be able to load SetBundle");
|
||||
for card in set.cards {
|
||||
index.insert(card.code.clone(), card);
|
||||
}
|
||||
};
|
||||
index
|
||||
}
|
||||
|
||||
/// Create a [`card::CardIndex`] from set bundles in the current working directory.
|
||||
///
|
||||
/// This function tries to load data from any directory matching the [glob] `./data/set*-*`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// See [`create_cardindex_from_paths`].
|
||||
pub fn create_cardindex_from_wd() -> card::CardIndex {
|
||||
let paths = glob::glob("./data/set*-*")
|
||||
.expect("glob to be a valid glob")
|
||||
.filter_map(Some)
|
||||
.filter_map(Result::ok);
|
||||
|
||||
create_cardindex_from_paths(paths)
|
||||
}
|
||||
|
||||
|
|
|
@ -32,22 +32,15 @@ pub enum CardRegion {
|
|||
/// Runeterra.
|
||||
Runeterra,
|
||||
|
||||
/// Origin: The Virtuoso.
|
||||
Jhin,
|
||||
/// Origin: Agony's Embrace.
|
||||
Evelynn,
|
||||
/// Origin: The Wandering Caretaker.
|
||||
Bard,
|
||||
|
||||
/// Unsupported region.
|
||||
#[serde(other)]
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl CardRegion {
|
||||
/// Get the [LocalizedCardRegion] associated with this [CardRegion].
|
||||
/// Get the [`LocalizedCardRegion`] associated with this [`CardRegion`].
|
||||
///
|
||||
/// Returns [Option::None] if no matching [LocalizedCardRegion] was found, for example for [CardRegion::Unsupported] regions.
|
||||
/// Returns [`None`] if no matching [`LocalizedCardRegion`] was found, for example for [`CardRegion::Unsupported`] regions.
|
||||
///
|
||||
/// Equivalent to calling [LocalizedCardRegionIndex::get].
|
||||
pub fn localized<'hm>(
|
||||
|
@ -79,7 +72,7 @@ impl CardRegion {
|
|||
|
||||
/// Get the short code of this [`CardRegion`].
|
||||
///
|
||||
/// If the region has no short code, it will return [`Option::None`].
|
||||
/// If the region has no short code, it will return [`None`].
|
||||
pub fn to_code(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Demacia => Some("DE".to_string()),
|
||||
|
@ -96,6 +89,24 @@ impl CardRegion {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the Discord emoji code associated with this [`CardRegion`].
|
||||
pub fn discord_emoji(&self) -> &'static str {
|
||||
match self {
|
||||
CardRegion::Noxus => "<:noxus:1056022924169064498>",
|
||||
CardRegion::Demacia => "<:demacia:1056023014128484412>",
|
||||
CardRegion::Freljord => "<:freljord:1056024331437735936>",
|
||||
CardRegion::ShadowIsles => "<:shadowisles:1056022886848135292>",
|
||||
CardRegion::Targon => "<:targon:1056022866174418944>",
|
||||
CardRegion::Ionia => "<:ionia:1056022949569777708>",
|
||||
CardRegion::Bilgewater => "<:bilgewater:1056024288215437484>",
|
||||
CardRegion::Shurima => "<:shurima:1056022884616765500>",
|
||||
CardRegion::PiltoverZaun => "<:piltoverzaun:1056022918959734835>",
|
||||
CardRegion::BandleCity => "<:bandlecity:1056024280493735976>",
|
||||
CardRegion::Runeterra => "<:runeterra:1056022895031238727>",
|
||||
CardRegion::Unsupported => "<:invaliddeck:1056022952396730438>",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the [`CardRegion`] from its internal id.
|
||||
|
@ -122,7 +133,7 @@ impl From<u32> for CardRegion {
|
|||
|
||||
/// Get the internal id of this [`CardRegion`].
|
||||
///
|
||||
/// If the region has no internal id, it will return [`Result::Err`].
|
||||
/// If the region has no internal id, it will return [`Err`].
|
||||
impl TryFrom<CardRegion> for u32 {
|
||||
type Error = ();
|
||||
|
||||
|
@ -169,8 +180,5 @@ mod tests {
|
|||
test_deserialization!(deserialize_piltoverzaun, r#""PiltoverZaun""#, CardRegion::PiltoverZaun);
|
||||
test_deserialization!(deserialize_bandlecity, r#""BandleCity""#, CardRegion::BandleCity);
|
||||
test_deserialization!(deserialize_runeterra, r#""Runeterra""#, CardRegion::Runeterra);
|
||||
test_deserialization!(deserialize_jhin, r#""Jhin""#, CardRegion::Jhin);
|
||||
test_deserialization!(deserialize_evelynn, r#""Evelynn""#, CardRegion::Evelynn);
|
||||
test_deserialization!(deserialize_bard, r#""Bard""#, CardRegion::Bard);
|
||||
test_deserialization!(deserialize_fallback, r#""Xyzzy""#, CardRegion::Unsupported);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use crate::data::corebundle::speed::{LocalizedSpellSpeed, LocalizedSpellSpeedIndex};
|
||||
|
||||
/// A possible [Spell](super::type::CardType::Spell) speed.
|
||||
/// A possible [`Spell`](super::type::CardType::Spell) speed.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum SpellSpeed {
|
||||
/// Non-spell cards have this speed.
|
||||
|
@ -17,11 +17,11 @@ pub enum SpellSpeed {
|
|||
}
|
||||
|
||||
impl SpellSpeed {
|
||||
/// Get the [LocalizedSpellSpeed] associated with this [SpellSpeed].
|
||||
/// Get the [`LocalizedSpellSpeed`] associated with this [`SpellSpeed`].
|
||||
///
|
||||
/// Returns [Option::None] if no matching [LocalizedSpellSpeed] was found, for example spell speeds missing from the index.
|
||||
/// Returns [`None`] if no matching [`LocalizedSpellSpeed`] was found, for example spell speeds missing from the index.
|
||||
///
|
||||
/// Equivalent to calling [LocalizedSpellSpeedIndex::get].
|
||||
/// Equivalent to calling [`LocalizedSpellSpeedIndex::get`].
|
||||
pub fn localized<'hm>(
|
||||
&self,
|
||||
hm: &'hm LocalizedSpellSpeedIndex,
|
||||
|
|
|
@ -1,8 +1,55 @@
|
|||
//! Module defining [CardSupertype].
|
||||
|
||||
/// A supertype of a [Card](super::card::Card), such as *Champion*.
|
||||
///
|
||||
/// Capitalization of the various supertypes is inconsistent.
|
||||
///
|
||||
/// TODO: As soon as all supertypes are known, make this a enum.
|
||||
pub type CardSupertype = String;
|
||||
/// A supertype of a [`Card`](super::card::Card), such as *Champion*.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum CardSupertype {
|
||||
/// No supertype, like most cards in the game.
|
||||
#[serde(rename = "")]
|
||||
None,
|
||||
/// A [Champion](super::rarity::CardRarity::Champion).
|
||||
#[serde(alias = "Champion")]
|
||||
Champion,
|
||||
/// A supertype of an unknown type.
|
||||
#[serde(other)]
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl From<&CardSupertype> for &'static str {
|
||||
fn from(cs: &CardSupertype) -> Self {
|
||||
match cs {
|
||||
CardSupertype::None => "",
|
||||
CardSupertype::Champion => "Champion",
|
||||
CardSupertype::Unsupported => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CardSupertype> for String {
|
||||
fn from(cs: &CardSupertype) -> Self {
|
||||
<&CardSupertype as Into<&'static str>>::into(cs).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::CardSupertype;
|
||||
|
||||
macro_rules! test_deserialization {
|
||||
( $id:ident, $src:literal, $res:expr ) => {
|
||||
#[test]
|
||||
fn $id() {
|
||||
assert_eq!(
|
||||
serde_json::de::from_str::<'static, CardSupertype>($src).unwrap(),
|
||||
$res
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test_deserialization!(deserialize_none, r#""""#, CardSupertype::None);
|
||||
test_deserialization!(deserialize_champion_uppercase, r#""CHAMPION""#, CardSupertype::Champion);
|
||||
test_deserialization!(deserialize_champion_titlecase, r#""Champion""#, CardSupertype::Champion);
|
||||
test_deserialization!(deserialize_unsupported, r#""sUs""#, CardSupertype::Unsupported);
|
||||
}
|
||||
|
|
|
@ -30,19 +30,25 @@ pub enum CardType {
|
|||
Unsupported,
|
||||
}
|
||||
|
||||
impl From<&CardType> for String {
|
||||
impl From<&CardType> for &'static str {
|
||||
fn from(r#type: &CardType) -> Self {
|
||||
match r#type {
|
||||
CardType::Spell => String::from("Spell"),
|
||||
CardType::Unit => String::from("Unit"),
|
||||
CardType::Ability => String::from("Ability"),
|
||||
CardType::Landmark => String::from("Landmark"),
|
||||
CardType::Trap => String::from("Trap"),
|
||||
CardType::Unsupported => String::from("Unknown"),
|
||||
CardType::Spell => "Spell",
|
||||
CardType::Unit => "Unit",
|
||||
CardType::Ability => "Ability",
|
||||
CardType::Landmark => "Landmark",
|
||||
CardType::Trap => "Trap",
|
||||
CardType::Unsupported => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CardType> for String {
|
||||
fn from(cs: &CardType) -> Self {
|
||||
<&CardType as Into<&'static str>>::into(cs).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::CardType;
|
||||
|
|
393
src/discord/handler.rs
Normal file
393
src/discord/handler.rs
Normal file
|
@ -0,0 +1,393 @@
|
|||
//! Module containing the [`EventHandler`] and its associated functions.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::env;
|
||||
use itertools::Itertools;
|
||||
use serenity::builder::EditInteractionResponse;
|
||||
use serenity::prelude::*;
|
||||
use serenity::model::prelude::*;
|
||||
use serenity::model::application::interaction::{InteractionResponseType, Interaction};
|
||||
use serenity::model::application::interaction::application_command::CommandDataOptionValue;
|
||||
use crate::data::deckcode::deck::Deck;
|
||||
use crate::data::deckcode::format::DeckCodeFormat;
|
||||
use crate::data::setbundle::r#type::CardType;
|
||||
use crate::data::setbundle::rarity::CardRarity;
|
||||
use crate::data::setbundle::region::CardRegion;
|
||||
use crate::data::setbundle::set::CardSet;
|
||||
use crate::data::setbundle::supertype::CardSupertype;
|
||||
use crate::search::cardsearch::CardSearchEngine;
|
||||
|
||||
/// Event handler for the bot.
|
||||
///
|
||||
/// Contains the functions that process events received by Discord.
|
||||
pub struct EventHandler;
|
||||
|
||||
const WELCOME_MESSAGE: &str = r#"
|
||||
👋 Hi! I'm a robotic poro who can search for Legends of Runeterra cards to send them in chats!
|
||||
|
||||
To search for a card, enter `/card` in a channel where I am enabled, then specify **your search query** as the `query` parameter, like this:
|
||||
```text
|
||||
/card query:mighty poro
|
||||
```
|
||||
After a while, I'll send in the channel the best match I can find for your query!
|
||||
|
||||
You can also perform more **complex queries**, such as this one:
|
||||
```text
|
||||
/card query:cost:4 AND attack:7 AND health:7
|
||||
```
|
||||
To read all details on the queries you can ask me to perform, visit the documentation at: <https://docs.rs/patched_porobot/latest/patched_porobot_discord>
|
||||
|
||||
Additionally, you can send me the `/deck` command together with a deck code to send the full deck details in chat, like this:
|
||||
```
|
||||
/deck code:CECQCAQCA4AQIAYKAIAQGLRWAQAQECAPEUXAIAQDAEBQOCIBAIAQEMJYAA
|
||||
```
|
||||
Have a fun time playing Legends of Runeterra!
|
||||
|
||||
_Patched Porobot isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties. Riot Games, and all associated properties are trademarks or registered trademarks of Riot Games, Inc._
|
||||
"#;
|
||||
|
||||
impl EventHandler {
|
||||
/// Handle the `/help` command.
|
||||
pub fn command_help(response: &mut EditInteractionResponse) -> &mut EditInteractionResponse {
|
||||
response.content(WELCOME_MESSAGE);
|
||||
response
|
||||
}
|
||||
|
||||
/// Handle the `/card` command.
|
||||
pub fn command_card<'r>(ctx: &Context, response: &'r mut EditInteractionResponse, options: HashMap<String, Option<CommandDataOptionValue>>) -> &'r mut EditInteractionResponse {
|
||||
let typemap = ctx.data.try_read().expect("to be able to acquire read lock on CardSearchEngine");
|
||||
let engine = typemap.get::<CardSearchEngine>().expect("CardSearchEngine to be in the TypeMap");
|
||||
|
||||
let query = match options.get("query") {
|
||||
Some(q) => q,
|
||||
None => return response.content(":warning: Missing `query` parameter."),
|
||||
};
|
||||
|
||||
let query = match query {
|
||||
Some(q) => q,
|
||||
None => return response.content(":warning: Empty `query` parameter."),
|
||||
};
|
||||
|
||||
let query = match query {
|
||||
CommandDataOptionValue::String(q) => q,
|
||||
_ => return response.content(":warning: Invalid `query` parameter type."),
|
||||
};
|
||||
|
||||
let result = match engine.query(query, 1) {
|
||||
Ok(r) => r,
|
||||
Err(_) => return response.content(":warning: Invalid card search query syntax."),
|
||||
};
|
||||
|
||||
let result = result.get(0);
|
||||
|
||||
match result {
|
||||
Some(card) => {
|
||||
let response = match card.main_art() {
|
||||
Some(art) => response.content(art.card_png.clone()),
|
||||
None => response.content(card.name.clone()),
|
||||
};
|
||||
let response = response.embed(|e| {
|
||||
e.title(card.name.clone());
|
||||
|
||||
if !card.localized_description_text.is_empty() {
|
||||
e.description(card.localized_description_text.clone());
|
||||
}
|
||||
|
||||
if !card.keywords.is_empty() {
|
||||
e.field("Keywords", card.keywords.iter().map(|r|
|
||||
format!(
|
||||
"{} {}",
|
||||
r.discord_emoji(),
|
||||
r.localized(&engine.globals.keywords).map_or_else(|| String::from("Missing translation"), |l| l.name.clone())
|
||||
)
|
||||
).join(", "), true);
|
||||
}
|
||||
|
||||
e.field("Mana cost", format!("{} mana", card.cost), true);
|
||||
|
||||
if card.r#type == CardType::Unit {
|
||||
e.field("Stats", format!("{} | {}", card.attack, card.health), true);
|
||||
}
|
||||
|
||||
e.field("Types", {
|
||||
let mut vec = Vec::new();
|
||||
if card.supertype != CardSupertype::None {
|
||||
vec.push(String::from(&card.supertype));
|
||||
}
|
||||
vec.push(String::from(&card.r#type));
|
||||
for subtype in card.subtypes.iter() {
|
||||
vec.push(subtype.to_owned())
|
||||
};
|
||||
vec
|
||||
}.join(", "), true);
|
||||
|
||||
e.field("Regions", card.regions.iter().map(|r| match r {
|
||||
CardRegion::Noxus => "<:noxus:1056022924169064498> Noxus",
|
||||
CardRegion::Demacia => "<:demacia:1056023014128484412> Demacia",
|
||||
CardRegion::Freljord => "<:freljord:1056024331437735936> Freljord",
|
||||
CardRegion::ShadowIsles => "<:shadowisles:1056022886848135292> Shadow Isles",
|
||||
CardRegion::Targon => "<:targon:1056022866174418944> Targon",
|
||||
CardRegion::Ionia => "<:ionia:1056022949569777708> Ionia",
|
||||
CardRegion::Bilgewater => "<:bilgewater:1056024288215437484> Bilgewater",
|
||||
CardRegion::Shurima => "<:shurima:1056022884616765500> Shurima",
|
||||
CardRegion::PiltoverZaun => "<:piltoverzaun:1056022918959734835> Piltover & Zaun",
|
||||
CardRegion::BandleCity => "<:bandlecity:1056024280493735976> Bandle City",
|
||||
CardRegion::Runeterra => "<:runeterra:1056022895031238727> Runeterra",
|
||||
CardRegion::Unsupported => "<:invaliddeck:1056022952396730438> Unknown",
|
||||
}).join(", "), false);
|
||||
|
||||
e.field("Set", match card.set {
|
||||
CardSet::Foundations => "<:foundations:1071644734667366410> Foundations",
|
||||
CardSet::RisingTides => "<:rising_tides:1071644736126976160> Rising Tides",
|
||||
CardSet::CallOfTheMountain => "<:call_of_the_mountain:1071644738555478076> Call of the Mountain",
|
||||
CardSet::EmpiresOfTheAscended => "<:empires_of_the_ascended:1071644740342255616> Empires of the Ascended",
|
||||
CardSet::BeyondTheBandlewood => "<:beyond_the_bandlewood:1071644742640750734> Beyond the Bandlewood",
|
||||
CardSet::Worldwalker => "<:worldwalker:1071644743798370315> Worldwalker",
|
||||
CardSet::TheDarkinSaga => "<:the_darkin_saga:1071644746411417610> The Darkin Saga",
|
||||
CardSet::Events => "Events", // TODO: Add icon
|
||||
CardSet::Unsupported => "<:invaliddeck:1056022952396730438> Unknown",
|
||||
}, true);
|
||||
|
||||
e.field("Rarity", match card.supertype {
|
||||
CardSupertype::Champion => "<:champion:1056024303856001034> Champion",
|
||||
_ => match card.rarity {
|
||||
CardRarity::None => "None",
|
||||
CardRarity::Common => "<:common:1056024315046412358> Common",
|
||||
CardRarity::Rare => "<:rare:1056022907433799690> Rare",
|
||||
CardRarity::Epic => "<:epic:1056023004028608622> Epic",
|
||||
CardRarity::Champion => "<:champion:1056024303856001034> Champion",
|
||||
}
|
||||
}, true);
|
||||
|
||||
e.color(match card.supertype {
|
||||
CardSupertype::Champion => 0x81541f,
|
||||
_ => match card.rarity {
|
||||
CardRarity::None => 0x202225,
|
||||
CardRarity::Common => 0x1e6a49,
|
||||
CardRarity::Rare => 0x244778,
|
||||
CardRarity::Epic => 0x502970,
|
||||
CardRarity::Champion => 0x81541f,
|
||||
}
|
||||
});
|
||||
|
||||
if !card.localized_flavor_text.is_empty() {
|
||||
e.footer(|f| f.text(card.localized_flavor_text.clone()));
|
||||
}
|
||||
|
||||
if let Some(art) = card.main_art() {
|
||||
e
|
||||
.field("Illustration by", card.artist_name.clone(), false)
|
||||
.image(art.full_png.clone());
|
||||
};
|
||||
|
||||
e
|
||||
});
|
||||
response
|
||||
}
|
||||
None => {
|
||||
response.content(":warning: No cards found.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle the `/deck` command.
|
||||
pub fn command_deck<'r>(ctx: &Context, response: &'r mut EditInteractionResponse, options: HashMap<String, Option<CommandDataOptionValue>>) -> &'r mut EditInteractionResponse {
|
||||
let typemap = ctx.data.try_read().expect("to be able to acquire read lock on CardSearchEngine");
|
||||
let engine = typemap.get::<CardSearchEngine>().expect("CardSearchEngine to be in the TypeMap");
|
||||
|
||||
let code = match options.get("code") {
|
||||
Some(c) => c,
|
||||
None => return response.content(":warning: Missing `code` parameter."),
|
||||
};
|
||||
|
||||
let code = match code {
|
||||
Some(c) => c,
|
||||
None => return response.content(":warning: Empty `code` parameter."),
|
||||
};
|
||||
|
||||
let code = match code {
|
||||
CommandDataOptionValue::String(c) => c,
|
||||
_ => return response.content(":warning: Invalid `code` parameter type."),
|
||||
};
|
||||
|
||||
let deck = match Deck::from_code(code) {
|
||||
Ok(deck) => deck,
|
||||
_ => return response.content(":warning: Invalid deck code."),
|
||||
};
|
||||
|
||||
let name = match options.get("name") {
|
||||
Some(Some(CommandDataOptionValue::String(n))) => Some(n),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
response.content(
|
||||
match name {
|
||||
Some(name) => format!("__**{}**__\n```text\n{}\n```", name, deck.to_code(DeckCodeFormat::F1).expect("to be able to serialize the deck code")),
|
||||
None => format!("```text\n{}\n```", deck.to_code(DeckCodeFormat::F1).expect("to be able to serialize the deck code")),
|
||||
});
|
||||
|
||||
let (format, regions) = if let Some(regions) = deck.standard(&engine.cards) {
|
||||
("<:neutral:1056022926660481094> Standard", regions)
|
||||
} else if let Some(regions) = deck.singleton(&engine.cards) {
|
||||
("<:neutral:1056022926660481094> Singleton", regions)
|
||||
} else {
|
||||
("<:invaliddeck:1056022952396730438> Unknown", HashSet::new())
|
||||
};
|
||||
|
||||
response.embed(|e| {
|
||||
e.description(
|
||||
deck.contents.iter()
|
||||
.map(|(cc, qty)| {
|
||||
(cc.to_card(&engine.cards), qty)
|
||||
})
|
||||
.map(|(c, qty)| {
|
||||
let name = match c {
|
||||
None => String::from("<:invaliddeck:1056022952396730438> Unknown card"),
|
||||
Some(c) => c.name.clone(),
|
||||
};
|
||||
format!("**{}×** {}", qty, name)
|
||||
})
|
||||
.join("\n")
|
||||
);
|
||||
|
||||
e.field("Format", format, true);
|
||||
|
||||
if !regions.is_empty() {
|
||||
e.field("Regions",
|
||||
regions
|
||||
.iter()
|
||||
.map(|region| format!(
|
||||
"{} {}",
|
||||
region.discord_emoji(),
|
||||
region.localized(&engine.globals.regions)
|
||||
.map_or_else(|| String::from("Missing translation"), |l| l.name.clone())
|
||||
))
|
||||
.join(", "),
|
||||
false);
|
||||
}
|
||||
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
/// Register the Slash Commands supported by this bot.
|
||||
///
|
||||
/// If `SERENITY_DEV_GUILD_ID` is set, register them as guild commands to avoid caching, otherwise, register them as global commands.
|
||||
pub async fn register_commands(ctx: &Context) -> anyhow::Result<()> {
|
||||
match env::var("SERENITY_DEV_GUILD_ID") {
|
||||
Ok(guild) => {
|
||||
let guild: u64 = guild.parse().expect("SERENITY_DEV_GUILD_ID to be valid");
|
||||
let guild: GuildId = guild.into();
|
||||
|
||||
guild.create_application_command(&ctx.http, |c| c
|
||||
.name("card")
|
||||
.description("Search and send a card in the chat.")
|
||||
.create_option(|o| o
|
||||
.kind(command::CommandOptionType::String)
|
||||
.name("query")
|
||||
.description("The query to send to the card search engine.")
|
||||
.required(true)
|
||||
)
|
||||
).await?;
|
||||
guild.create_application_command(&ctx.http, |c| c
|
||||
.name("deck")
|
||||
.description("Send a deck in the chat.")
|
||||
.create_option(|o| o
|
||||
.kind(command::CommandOptionType::String)
|
||||
.name("code")
|
||||
.description("The code of the deck to send.")
|
||||
.required(true)
|
||||
)
|
||||
.create_option(|o| o
|
||||
.kind(command::CommandOptionType::String)
|
||||
.name("name")
|
||||
.description("The name of the deck.")
|
||||
.required(false)
|
||||
)
|
||||
).await?;
|
||||
guild.create_application_command(&ctx.http, |c| c
|
||||
.name("help")
|
||||
.description("View the help message.")
|
||||
).await?;
|
||||
}
|
||||
Err(_) => {
|
||||
command::Command::create_global_application_command(&ctx.http, |c| c
|
||||
.name("card")
|
||||
.description("Search and send a card in the chat.")
|
||||
.create_option(|o| o
|
||||
.kind(command::CommandOptionType::String)
|
||||
.name("query")
|
||||
.description("The query to send to the card search engine.")
|
||||
.required(true)
|
||||
)
|
||||
).await?;
|
||||
command::Command::create_global_application_command(&ctx.http, |c| c
|
||||
.name("deck")
|
||||
.description("Send a deck in the chat.")
|
||||
.create_option(|o| o
|
||||
.kind(command::CommandOptionType::String)
|
||||
.name("code")
|
||||
.description("The code of the deck to send.")
|
||||
.required(true)
|
||||
)
|
||||
.create_option(|o| o
|
||||
.kind(command::CommandOptionType::String)
|
||||
.name("name")
|
||||
.description("The name of the deck.")
|
||||
.required(false)
|
||||
)
|
||||
).await?;
|
||||
command::Command::create_global_application_command(&ctx.http, |c| c
|
||||
.name("help")
|
||||
.description("View the help message.")
|
||||
).await?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[serenity::async_trait]
|
||||
impl serenity::client::EventHandler for EventHandler {
|
||||
async fn ready(&self, ctx: Context, ready: Ready) {
|
||||
log::debug!("Received ready event from the gateway");
|
||||
|
||||
EventHandler::register_commands(&ctx).await.expect("to be able to register commands");
|
||||
|
||||
log::info!("{} is ready!", &ready.user.name);
|
||||
}
|
||||
|
||||
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||
match interaction {
|
||||
Interaction::ApplicationCommand(command) => {
|
||||
let cmd_name = command.data.name.as_str();
|
||||
let cmd_opts: HashMap<String, Option<CommandDataOptionValue>> = command.data.options
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|option| (option.name, option.resolved))
|
||||
.collect();
|
||||
|
||||
log::info!("Received command: {}", &cmd_name);
|
||||
|
||||
command.create_interaction_response(&ctx.http, |r| r
|
||||
.interaction_response_data(|d| d
|
||||
.ephemeral(cmd_name == "help")
|
||||
)
|
||||
.kind(InteractionResponseType::DeferredChannelMessageWithSource)
|
||||
).await.expect("to be able to defer the response");
|
||||
|
||||
command.edit_original_interaction_response(
|
||||
&ctx.http,
|
||||
|response| match cmd_name {
|
||||
"card" => Self::command_card(&ctx, response, cmd_opts),
|
||||
"deck" => Self::command_deck(&ctx, response, cmd_opts),
|
||||
"help" => Self::command_help(response),
|
||||
_ => response.content(":warning: Unknown command."),
|
||||
}
|
||||
).await.expect("to be able to update the deferred response");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
41
src/discord/main.rs
Normal file
41
src/discord/main.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
//! Module defining the [`main`] function for `patched_porobot_discord`.
|
||||
|
||||
use std::env;
|
||||
use log::*;
|
||||
use serenity::prelude::*;
|
||||
use crate::data::corebundle::create_globalindexes_from_wd;
|
||||
use crate::data::setbundle::create_cardindex_from_wd;
|
||||
use crate::discord::handler::EventHandler;
|
||||
use crate::search::cardsearch::CardSearchEngine;
|
||||
|
||||
/// The function that `patched_porobot_discord` should run when it's started.
|
||||
pub async fn main() {
|
||||
pretty_env_logger::init();
|
||||
debug!("Logger initialized successfully!");
|
||||
|
||||
debug!("Creating LocalizedGlobalIndexes...");
|
||||
let globals = create_globalindexes_from_wd();
|
||||
debug!("Created LocalizedGlobalIndexes!");
|
||||
|
||||
debug!("Creating CardIndex...");
|
||||
let cards = create_cardindex_from_wd();
|
||||
debug!("Created CardIndex!");
|
||||
|
||||
debug!("Creating CardSearchEngine...");
|
||||
let engine = CardSearchEngine::new(globals, cards);
|
||||
debug!("Created CardSearchEngine!");
|
||||
|
||||
let token: String = env::var("SERENITY_TOKEN").expect("SERENITY_TOKEN to be set");
|
||||
let appid: u64 = env::var("SERENITY_APPID").expect("SERENITY_APPID to be set")
|
||||
.parse().expect("SERENITY_APPID to be valid");
|
||||
|
||||
Client::builder(&token, GatewayIntents::non_privileged())
|
||||
.event_handler(EventHandler)
|
||||
.type_map_insert::<CardSearchEngine>(engine)
|
||||
.application_id(appid)
|
||||
.await
|
||||
.expect("to be able to create the Discord client")
|
||||
.start_autosharded()
|
||||
.await
|
||||
.expect("to be able to start the Discord client");
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
//! Module providing utilities to be used in the `patched_porobot_discord` executable target.
|
||||
//!
|
||||
//! While adding new features to this module, remember that binaries [can only access the public API of the crate](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#binaries), as they considered a separate crate from the rest of the project.
|
||||
|
||||
pub mod handler;
|
||||
pub mod main;
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
//! Additionally, every one of the following features enables the compilation of an additional binary target:
|
||||
//!
|
||||
//! - [`telegram`] enables the compilation of `patched_porobot_telegram`, a [Telegram inline bot](https://core.telegram.org/bots/api) allowing users to search and send cards in any Telegram chat;
|
||||
//! - ~~[`discord`] enables the compilation of `patched_porobot_discord`, a [Discord bot](https://discord.com/developers/docs/intro#bots-and-apps) allowing Discord servers the bot is added to to search and send cards in their channels~~;
|
||||
//! - [`discord`] enables the compilation of `patched_porobot_discord`, a [Discord bot](https://discord.com/developers/docs/intro#bots-and-apps) allowing Discord servers the bot is added to to search and send cards in their channels;
|
||||
//! - ~~[`matrix`] enables the compilation of `patched_porobot_matrix`, a Matrix bot parsing messages in the rooms where it is added to to send details about the cards mentioned in messages~~.
|
||||
//!
|
||||
//! # Legal
|
||||
|
|
|
@ -9,6 +9,8 @@ use tantivy::query::{QueryParser, QueryParserError};
|
|||
use tantivy::schema::{Field, NumericOptions, Schema, TextOptions};
|
||||
use tantivy::tokenizer::TextAnalyzer;
|
||||
use tantivy::{Document, Index, IndexReader, IndexWriter};
|
||||
use crate::data::setbundle::r#type::CardType;
|
||||
use crate::data::setbundle::supertype::CardSupertype;
|
||||
|
||||
/// The search engine.
|
||||
///
|
||||
|
@ -109,7 +111,7 @@ impl CardSearchEngine {
|
|||
/// |---------------|----------------------------------|-------------|
|
||||
/// | `code` | [code](Self::options_code) | The internal [card code](Card::code), such as `01IO012`. |
|
||||
/// | `name` | [text](Self::options_text) | The [name of the card](Card::name). |
|
||||
/// | `type` | [keyword](Self::options_keyword) | The [type of the card](Card::type), such as `Unit`. |
|
||||
/// | `type` | [keyword](Self::options_keyword) | The [type of the card](Card::r#type), such as `Unit`. |
|
||||
/// | `set` | [keyword](Self::options_keyword) | The [set the card belongs to](Card::set), such as `Beyond the Bandlewood`. |
|
||||
/// | `rarity` | [keyword](Self::options_keyword) | The [rarity of the card](Card::rarity), such as `Rare`, or `Champion`. |
|
||||
/// | `collectible` | [number](Self::options_number) | `1` if the [card is collectible](Card::collectible), `0` otherwise. |
|
||||
|
@ -123,6 +125,8 @@ impl CardSearchEngine {
|
|||
/// | `levelup` | [text](Self::options_text) | The [level up text of the champion](Card::localized_levelup_text). |
|
||||
/// | `flavor` | [text](Self::options_text) | The [flavor text of the card](Card::localized_flavor_text). |
|
||||
/// | `artist` | [text](Self::options_text) | The [artist(s) of the card's illustration](Card::artist_name). |
|
||||
/// | `subtypes` | [text](Self::options_text) | The [subtypes of the card](Card::subtypes), such as `Poro` or `Yordle`. |
|
||||
/// | `level` | [number](Self::options_number) | `0` if a non-champion, `1` if not leveled, `2` if leveled, `3` if ascended. |
|
||||
///
|
||||
/// Use [Self::schema_fields] to create the [CardSchemaFields] object containing all of them.
|
||||
///
|
||||
|
@ -145,15 +149,15 @@ impl CardSearchEngine {
|
|||
schema_builder.add_text_field("regions", options_keyword.clone());
|
||||
schema_builder.add_u64_field("attack", options_number.clone());
|
||||
schema_builder.add_u64_field("cost", options_number.clone());
|
||||
schema_builder.add_u64_field("health", options_number);
|
||||
schema_builder.add_u64_field("health", options_number.clone());
|
||||
schema_builder.add_text_field("spellspeed", options_keyword.clone());
|
||||
schema_builder.add_text_field("keywords", options_keyword.clone());
|
||||
schema_builder.add_text_field("description", options_text.clone());
|
||||
schema_builder.add_text_field("levelup", options_text.clone());
|
||||
schema_builder.add_text_field("flavor", options_text.clone());
|
||||
schema_builder.add_text_field("artist", options_text);
|
||||
schema_builder.add_text_field("subtypes", options_keyword.clone());
|
||||
schema_builder.add_text_field("supertype", options_keyword);
|
||||
schema_builder.add_text_field("subtypes", options_keyword);
|
||||
schema_builder.add_u64_field("level", options_number);
|
||||
|
||||
schema_builder.build()
|
||||
}
|
||||
|
@ -212,9 +216,9 @@ impl CardSearchEngine {
|
|||
subtypes: schema
|
||||
.get_field("subtypes")
|
||||
.expect("schema to have a 'subtypes' field"),
|
||||
supertype: schema
|
||||
.get_field("supertype")
|
||||
.expect("schema to have a 'supertype' field"),
|
||||
level: schema
|
||||
.get_field("level")
|
||||
.expect("schema to have a 'level' field"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,7 +254,7 @@ impl CardSearchEngine {
|
|||
use tantivy::doc;
|
||||
|
||||
doc!(
|
||||
fields.code => card.code.full,
|
||||
fields.code => card.code.clone().full,
|
||||
fields.name => card.name,
|
||||
fields.r#type => String::from(&card.r#type),
|
||||
fields.set => card.set
|
||||
|
@ -261,7 +265,7 @@ impl CardSearchEngine {
|
|||
.localized(&globals.rarities)
|
||||
.map(|cr| cr.name.to_owned())
|
||||
.unwrap_or_else(String::new),
|
||||
fields.collectible => if card.collectible {1u64} else {0u64},
|
||||
fields.collectible => u64::from(card.collectible),
|
||||
fields.regions => card.regions.iter()
|
||||
.map(|region| region
|
||||
.localized(&globals.regions)
|
||||
|
@ -282,18 +286,35 @@ impl CardSearchEngine {
|
|||
.unwrap_or_else(String::new))
|
||||
.join(" "),
|
||||
fields.description => card.localized_description_text,
|
||||
fields.levelup => card.localized_levelup_text,
|
||||
fields.levelup => card.localized_levelup_text.clone(),
|
||||
fields.flavor => card.localized_flavor_text,
|
||||
fields.artist => card.artist_name,
|
||||
fields.subtypes => card.subtypes.join(" "),
|
||||
fields.supertype => card.supertype,
|
||||
fields.level => {
|
||||
if card.r#type != CardType::Unit || card.supertype != CardSupertype::Champion {
|
||||
0u64
|
||||
}
|
||||
else if card.subtypes.contains(&"ASCENDED".to_string()) {
|
||||
if card.localized_levelup_text.contains(&"Sun Disc".to_string()) {
|
||||
2u64
|
||||
} else if card.localized_levelup_text.is_empty() {
|
||||
3u64
|
||||
} else {
|
||||
1u64
|
||||
}
|
||||
} else if card.localized_levelup_text.is_empty() {
|
||||
2u64
|
||||
} else {
|
||||
1u64
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Build the [QueryParser] of the search engine.
|
||||
fn parser(index: &Index, fields: CardSchemaFields) -> QueryParser {
|
||||
let mut parser = QueryParser::for_index(
|
||||
&index,
|
||||
index,
|
||||
vec![
|
||||
fields.code,
|
||||
fields.name,
|
||||
|
@ -303,7 +324,6 @@ impl CardSearchEngine {
|
|||
fields.flavor,
|
||||
fields.artist,
|
||||
fields.subtypes,
|
||||
fields.supertype,
|
||||
],
|
||||
);
|
||||
parser.set_conjunction_by_default();
|
||||
|
@ -381,7 +401,7 @@ struct CardSchemaFields {
|
|||
pub code: Field,
|
||||
/// [Card::name].
|
||||
pub name: Field,
|
||||
/// English [Card::type].
|
||||
/// English [Card::r#type].
|
||||
pub r#type: Field,
|
||||
/// Localized [Card::set].
|
||||
pub set: Field,
|
||||
|
@ -411,6 +431,11 @@ struct CardSchemaFields {
|
|||
pub artist: Field,
|
||||
/// Space-separated [Card::subtypes].
|
||||
pub subtypes: Field,
|
||||
/// [Card::supertype].
|
||||
pub supertype: Field,
|
||||
/// Level of the champion. 0 if not a champion. 1 if not leveled. 2 if leveled. 3 if ascended.
|
||||
pub level: Field,
|
||||
}
|
||||
|
||||
#[cfg(feature = "discord")]
|
||||
impl serenity::prelude::TypeMapKey for CardSearchEngine {
|
||||
type Value = CardSearchEngine;
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
//!
|
||||
//! [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style
|
||||
|
||||
use std::cmp::Ordering::{Equal, Greater, Less};
|
||||
use std::collections::HashSet;
|
||||
use crate::data::corebundle::globals::LocalizedGlobalsIndexes;
|
||||
use crate::data::corebundle::keyword::LocalizedCardKeywordIndex;
|
||||
use crate::data::corebundle::region::LocalizedCardRegionIndex;
|
||||
|
@ -96,18 +98,22 @@ fn display_regions(regions: &[CardRegion], hm: &LocalizedCardRegionIndex) -> Str
|
|||
fn display_types(r#type: &CardType, supertype: &CardSupertype, subtypes: &[CardSubtype]) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
if supertype != "" {
|
||||
result.push_str(&*format!("<i>{}</i> › ", escape(&supertype),));
|
||||
};
|
||||
result.push_str(
|
||||
match supertype {
|
||||
CardSupertype::Champion => "<i>Champion</i> › ",
|
||||
CardSupertype::Unsupported => "<i>Unknown</i> › ",
|
||||
_ => "",
|
||||
}
|
||||
);
|
||||
|
||||
result.push_str(&*format!("<i>{}</i>", escape(&*String::from(r#type)),));
|
||||
result.push_str(&format!("<i>{}</i>", escape(&String::from(r#type)),));
|
||||
|
||||
if subtypes.len() > 0 {
|
||||
result.push_str(&*format!(
|
||||
if !subtypes.is_empty() {
|
||||
result.push_str(&format!(
|
||||
" › {}",
|
||||
subtypes
|
||||
.iter()
|
||||
.map(|subtype| format!("<i>{}</i>", escape(&subtype)))
|
||||
.map(|subtype| format!("<i>{}</i>", escape(subtype)))
|
||||
.join(", ")
|
||||
))
|
||||
}
|
||||
|
@ -135,10 +141,10 @@ fn display_keywords(keywords: &[CardKeyword], hm: &LocalizedCardKeywordIndex) ->
|
|||
///
|
||||
/// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style
|
||||
fn display_description(description: &String) -> String {
|
||||
if description == "" {
|
||||
if description.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!("{}\n\n", escape(&description))
|
||||
format!("{}\n\n", escape(description))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,10 +152,10 @@ fn display_description(description: &String) -> String {
|
|||
///
|
||||
/// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style
|
||||
fn display_levelup(levelup: &String) -> String {
|
||||
if levelup == "" {
|
||||
if levelup.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!("<u>Level up</u>: {}\n\n", escape(&levelup))
|
||||
format!("<u>Level up</u>: {}\n\n", escape(levelup))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,33 +163,79 @@ fn display_levelup(levelup: &String) -> String {
|
|||
///
|
||||
/// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style
|
||||
pub fn display_deck(index: &CardIndex, deck: &Deck, code: &str, name: &Option<&str>) -> String {
|
||||
// TODO: optimize this
|
||||
let cards = deck
|
||||
.contents
|
||||
.keys()
|
||||
.sorted_by(|a, b| {
|
||||
let card_a = index.get(a).expect("card to exist in the index");
|
||||
let card_b = index.get(b).expect("card to exist in the index");
|
||||
.map(|k| (
|
||||
index.get(k),
|
||||
deck.contents.get(k).expect("CardCode from Deck to have a quantity"),
|
||||
))
|
||||
.sorted_by(|(opt_a, _), (opt_b, _)| {
|
||||
if opt_a.is_none() && opt_b.is_none() {
|
||||
return Equal;
|
||||
}
|
||||
if opt_b.is_none() {
|
||||
return Greater;
|
||||
}
|
||||
else if opt_a.is_none() {
|
||||
return Less;
|
||||
}
|
||||
|
||||
let card_a = opt_a.expect("opt_a to be Some");
|
||||
let card_b = opt_b.expect("opt_b to be Some");
|
||||
|
||||
card_a
|
||||
.cost
|
||||
.cmp(&card_b.cost)
|
||||
.then(card_a.name.cmp(&card_b.name))
|
||||
})
|
||||
.map(|k| {
|
||||
let card = index.get(k).expect("card to exist in the index");
|
||||
let quantity = deck.contents.get(k).unwrap();
|
||||
.map(|(card, quantity)| {
|
||||
let name = match card {
|
||||
None => "<i>Unknown Card</i>".to_string(),
|
||||
Some(card) => match card.supertype {
|
||||
CardSupertype::Champion => format!("<u>{}</u>", escape(&card.name)),
|
||||
_ => escape(&card.name),
|
||||
}
|
||||
};
|
||||
|
||||
if card.supertype == "Champion" {
|
||||
format!("<b>{}×</b> <u>{}</u>", &quantity, &card.name)
|
||||
} else {
|
||||
format!("<b>{}×</b> {}", &quantity, &card.name)
|
||||
}
|
||||
format!("<b>{}×</b> {}", &quantity, &name)
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
let mut tags: Vec<&'static str> = vec![];
|
||||
|
||||
let regions = if let Some(regions) = deck.standard(index) {
|
||||
tags.push("#Standard");
|
||||
regions
|
||||
} else if let Some(regions) = deck.singleton(index) {
|
||||
tags.push("#Singleton");
|
||||
regions
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
|
||||
for region in regions {
|
||||
tags.push(match region {
|
||||
CardRegion::Noxus => "#Noxus",
|
||||
CardRegion::Demacia => "#Demacia",
|
||||
CardRegion::Freljord => "#Freljord",
|
||||
CardRegion::ShadowIsles => "#ShadowIsles",
|
||||
CardRegion::Targon => "#Targon",
|
||||
CardRegion::Ionia => "#Ionia",
|
||||
CardRegion::Bilgewater => "#Bilgewater",
|
||||
CardRegion::Shurima => "#Shurima",
|
||||
CardRegion::PiltoverZaun => "#PiltoverZaun",
|
||||
CardRegion::BandleCity => "#BandleCity",
|
||||
CardRegion::Runeterra => "#Runeterra",
|
||||
CardRegion::Unsupported => "<i>Unknown</i>",
|
||||
})
|
||||
}
|
||||
|
||||
let tags = tags.join(", ");
|
||||
let tags = if !tags.is_empty() { format!("{}\n", &tags) } else { "".to_string() };
|
||||
|
||||
match name {
|
||||
Some(name) => format!("<b><u>{}</u></b>\n<code>{}</code>\n\n{}", &name, &code, &cards),
|
||||
None => format!("<code>{}</code>\n\n{}", &code, &cards),
|
||||
Some(name) => format!("<b><u>{}</u></b>\n<code>{}</code>\n{}\n{}", &name, &code, &tags, &cards),
|
||||
None => format!("<code>{}</code>\n{}\n{}", &code, &tags, &cards),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,9 @@ use lazy_static::lazy_static;
|
|||
use regex::Regex;
|
||||
|
||||
/// Handle inline queries by searching cards on the [CardSearchEngine].
|
||||
#[allow(clippy::never_loop)]
|
||||
pub fn inline_query_handler(
|
||||
crystal: String,
|
||||
engine: CardSearchEngine,
|
||||
) -> Handler<'static, DependencyMap, ResponseResult<()>, DpHandlerDescription> {
|
||||
Update::filter_inline_query().chain(dptree::endpoint(move |query: InlineQuery, bot: Bot| {
|
||||
|
@ -22,10 +24,10 @@ pub fn inline_query_handler(
|
|||
|
||||
// It's not a real loop, it's just to make the code flow more tolerable.
|
||||
let payload: AnswerInlineQuery = loop {
|
||||
if query.query.len() == 0 {
|
||||
if query.query.is_empty() {
|
||||
debug!("Empty query specified.");
|
||||
break AnswerInlineQuery {
|
||||
inline_query_id: query.id.clone(),
|
||||
inline_query_id: query.id,
|
||||
results: vec![],
|
||||
cache_time: None,
|
||||
is_personal: Some(false),
|
||||
|
@ -41,14 +43,14 @@ pub fn inline_query_handler(
|
|||
|
||||
if let Some(deck_captures) = DECK_RE.captures(&query.query) {
|
||||
if let Some(deck_code) = deck_captures.name("code") {
|
||||
if let Ok(deck) = Deck::from_code(&deck_code.as_str()) {
|
||||
if let Ok(deck) = Deck::from_code(deck_code.as_str()) {
|
||||
|
||||
debug!("Parsed deck successfully!");
|
||||
let name = deck_captures.name("name").map(|m| m.as_str());
|
||||
|
||||
break AnswerInlineQuery {
|
||||
inline_query_id: query.id.clone(),
|
||||
results: vec![deck_to_inlinequeryresult(&engine.cards, &deck, &name)],
|
||||
results: vec![deck_to_inlinequeryresult(&crystal, &engine.cards, &deck, &name)],
|
||||
cache_time: None,
|
||||
is_personal: Some(false),
|
||||
next_offset: None,
|
||||
|
@ -62,7 +64,7 @@ pub fn inline_query_handler(
|
|||
debug!("Querying the card search engine...");
|
||||
let results = engine.query(&query.query, 50);
|
||||
|
||||
if let Err(_) = results {
|
||||
if results.is_err() {
|
||||
debug!("Invalid card search query syntax.");
|
||||
break AnswerInlineQuery {
|
||||
inline_query_id: query.id.clone(),
|
||||
|
@ -95,7 +97,7 @@ pub fn inline_query_handler(
|
|||
inline_query_id: query.id.clone(),
|
||||
results: results
|
||||
.iter()
|
||||
.map(|card| card_to_inlinequeryresult(&engine.globals, card))
|
||||
.map(|card| card_to_inlinequeryresult(&crystal, &engine.globals, card))
|
||||
.collect_vec(),
|
||||
cache_time: Some(300),
|
||||
is_personal: Some(false),
|
||||
|
@ -117,7 +119,7 @@ pub fn inline_query_handler(
|
|||
}))
|
||||
}
|
||||
|
||||
const WELCOME_MESSAGE: &'static str = r#"
|
||||
const WELCOME_MESSAGE: &str = r#"
|
||||
👋 Hi! I'm a robotic poro who can search for Legends of Runeterra cards to send them in chats!
|
||||
|
||||
To search for a card, enter <b>my username</b> in any chat, followed by <b>your search query</b>, like this:
|
||||
|
@ -145,7 +147,7 @@ pub fn message_handler() -> Handler<'static, DependencyMap, ResponseResult<()>,
|
|||
info!("Handling private message: `{:?}`", &message.text());
|
||||
|
||||
let payload = SendMessage {
|
||||
chat_id: Recipient::Id(message.chat.id.clone()),
|
||||
chat_id: Recipient::Id(message.chat.id),
|
||||
text: WELCOME_MESSAGE.to_string(),
|
||||
parse_mode: Some(ParseMode::Html),
|
||||
entities: None,
|
||||
|
@ -155,6 +157,7 @@ pub fn message_handler() -> Handler<'static, DependencyMap, ResponseResult<()>,
|
|||
reply_to_message_id: None,
|
||||
allow_sending_without_reply: None,
|
||||
reply_markup: None,
|
||||
message_thread_id: None,
|
||||
};
|
||||
|
||||
async move {
|
||||
|
|
|
@ -14,13 +14,14 @@ use teloxide::types::{
|
|||
|
||||
/// Convert a [Card] into a [InlineQueryResult].
|
||||
pub fn card_to_inlinequeryresult(
|
||||
crystal: &str,
|
||||
globals: &LocalizedGlobalsIndexes,
|
||||
card: &Card,
|
||||
) -> InlineQueryResult {
|
||||
InlineQueryResult::Photo(InlineQueryResultPhoto {
|
||||
id: card.code.full.to_owned(),
|
||||
id: format!("{}:{}", &crystal, &card.code.full),
|
||||
title: Some(card.name.to_owned()),
|
||||
caption: Some(display_card(&globals, &card)),
|
||||
caption: Some(display_card(globals, card)),
|
||||
parse_mode: Some(ParseMode::Html),
|
||||
photo_url: card
|
||||
.main_art()
|
||||
|
@ -44,19 +45,24 @@ pub fn card_to_inlinequeryresult(
|
|||
}
|
||||
|
||||
/// Convert a [Deck] with an optional name into a [InlineQueryResult].
|
||||
pub fn deck_to_inlinequeryresult(index: &CardIndex, deck: &Deck, name: &Option<&str>) -> InlineQueryResult {
|
||||
pub fn deck_to_inlinequeryresult(
|
||||
crystal: &str,
|
||||
index: &CardIndex,
|
||||
deck: &Deck,
|
||||
name: &Option<&str>
|
||||
) -> InlineQueryResult {
|
||||
let code = deck
|
||||
.to_code(DeckCodeFormat::F1)
|
||||
.expect("serialized deck to deserialize properly");
|
||||
|
||||
InlineQueryResult::Article(InlineQueryResultArticle {
|
||||
id: format!("{:x}", md5::compute(&code)),
|
||||
id: format!("{}:{:x}", &crystal, md5::compute(&code)),
|
||||
title: match &name {
|
||||
Some(name) => format!(r#"Deck "{}" with {} cards"#, name, deck.contents.len()),
|
||||
None => format!("Deck with {} cards", deck.contents.len())
|
||||
},
|
||||
input_message_content: InputMessageContent::Text(InputMessageContentText {
|
||||
message_text: display_deck(index, deck, &code, &name),
|
||||
message_text: display_deck(index, deck, &code, name),
|
||||
parse_mode: Some(ParseMode::Html),
|
||||
entities: None,
|
||||
disable_web_page_preview: Some(true),
|
||||
|
|
|
@ -1,58 +1,29 @@
|
|||
//! This module defines the [`main`] function for `patched_porobot_telegram`.
|
||||
//! Module defining the [`main`] function for `patched_porobot_telegram`.
|
||||
|
||||
use crate::data::corebundle::globals::LocalizedGlobalsIndexes;
|
||||
use crate::data::corebundle::CoreBundle;
|
||||
use crate::data::setbundle::card::{Card, CardIndex};
|
||||
use crate::data::setbundle::SetBundle;
|
||||
use crate::data::corebundle::create_globalindexes_from_wd;
|
||||
use crate::data::setbundle::create_cardindex_from_wd;
|
||||
use crate::search::cardsearch::CardSearchEngine;
|
||||
use crate::telegram::handler::{inline_query_handler, message_handler};
|
||||
use glob::glob;
|
||||
use log::*;
|
||||
use std::path::PathBuf;
|
||||
use rand::Rng;
|
||||
use teloxide::prelude::*;
|
||||
|
||||
/// The main function that `patched_porobot_telegram` should run when it's started.
|
||||
/// The function that `patched_porobot_telegram` should run when it's started.
|
||||
pub async fn main() {
|
||||
pretty_env_logger::init();
|
||||
debug!("Logger initialized successfully!");
|
||||
|
||||
debug!("Loading core bundle...");
|
||||
let core = CoreBundle::load(&*PathBuf::from("./data/core-en_us"))
|
||||
.expect("to be able to load `core-en_us` bundle");
|
||||
debug!("Loaded core bundle successfully!");
|
||||
debug!("Creating LocalizedGlobalIndexes...");
|
||||
let globals = create_globalindexes_from_wd();
|
||||
debug!("Created LocalizedGlobalIndexes!");
|
||||
|
||||
debug!("Loading set bundles...");
|
||||
let setpaths = glob("./data/set*-*")
|
||||
.expect("setglob to be a valid glob")
|
||||
.into_iter()
|
||||
.filter(|sp| sp.is_ok())
|
||||
.map(|sp| sp.unwrap());
|
||||
let mut cards: Vec<Card> = vec![];
|
||||
for setpath in setpaths {
|
||||
debug!("Loading {:?}...", &setpath);
|
||||
let set = SetBundle::load(&setpath).expect(&*format!(
|
||||
"to be able to load {:?} as a set bundle",
|
||||
&setpath
|
||||
));
|
||||
let mut setcards = set.cards;
|
||||
cards.append(&mut setcards);
|
||||
}
|
||||
debug!("Loaded {} cards!", &cards.len());
|
||||
debug!("Creating CardIndex...");
|
||||
let cards = create_cardindex_from_wd();
|
||||
debug!("Created CardIndex!");
|
||||
|
||||
debug!("Indexing globals...");
|
||||
let globals = LocalizedGlobalsIndexes::from(core.globals);
|
||||
debug!("Indexed globals!");
|
||||
|
||||
let mut index = CardIndex::new();
|
||||
for card in cards {
|
||||
index.insert(card.code.clone(), card);
|
||||
}
|
||||
let cards = index;
|
||||
debug!("Indexed cards!");
|
||||
|
||||
debug!("Creating search engine...");
|
||||
debug!("Creating CardSearchEngine...");
|
||||
let engine = CardSearchEngine::new(globals, cards);
|
||||
debug!("Created search engine!");
|
||||
debug!("Created CardSearchEngine!");
|
||||
|
||||
debug!("Creating Telegram bot with parameters from the environment...");
|
||||
let bot = Bot::from_env();
|
||||
|
@ -63,9 +34,14 @@ pub async fn main() {
|
|||
.expect("Telegram bot parameters to be valid");
|
||||
debug!("Created Telegram bot!");
|
||||
|
||||
debug!("Generating crystal for this run...");
|
||||
let rng = rand::thread_rng();
|
||||
let crystal: String = String::from_utf8(rng.sample_iter(&rand::distributions::Alphanumeric).take(6).collect()).unwrap();
|
||||
debug!("Generated crystal: {}", &crystal);
|
||||
|
||||
debug!("Creating handlers...");
|
||||
let handler = dptree::entry()
|
||||
.branch(inline_query_handler(engine))
|
||||
.branch(inline_query_handler(crystal, engine))
|
||||
.branch(message_handler());
|
||||
debug!("Created handlers!");
|
||||
|
||||
|
|
Loading…
Reference in a new issue