Initial commit
This commit is contained in:
482
.gitignore
vendored
Normal file
482
.gitignore
vendored
Normal file
@@ -0,0 +1,482 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from `dotnet new gitignore`
|
||||
|
||||
# dotenv files
|
||||
.env
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Tye
|
||||
.tye/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
.idea/
|
||||
|
||||
##
|
||||
## Visual studio for Mac
|
||||
##
|
||||
|
||||
|
||||
# globs
|
||||
Makefile.in
|
||||
*.userprefs
|
||||
*.usertasks
|
||||
config.make
|
||||
config.status
|
||||
aclocal.m4
|
||||
install-sh
|
||||
autom4te.cache/
|
||||
*.tar.gz
|
||||
tarballs/
|
||||
test-results/
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
||||
28
AGENTS.md
Normal file
28
AGENTS.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Agent Guide
|
||||
|
||||
Also see the other related technical documentation: GOAL.md, TASKS.md
|
||||
|
||||
## Rules
|
||||
|
||||
- This is a Windows environment, WSL is not installed (i.e. sed is not available). You're running under PowerShell 7.5.4. Due to platform restrictions, file deletions are not possible. Replacing the entire file content via a context diff is a viable alternative.
|
||||
- PowerShell doesn't support bash-style heredocs. If complex scripts need to be executed, consider using python as a last resort. Run Python code using python -c with inline commands instead of python - <<'PY'.
|
||||
- If you find unexpected changes in the code (deletions, changes, diff results that were not communicated), never revert them and never restore the old state. Assume that those changes happened with intent.
|
||||
- Never use reset commands or equivalent rollback actions to discard local changes unless the user explicitly asks for that exact rollback. Assume that uncommunicated file changes are a possibility between each interaction with the user.
|
||||
- Prefer extracting code to a shared helper to be reused instead of duplicating code. Always keep high maintainability standards.
|
||||
- Keep changes as small as possible, design solutions that achieve the goals with minimal churn.
|
||||
- Always place each newly created class into its own file. The file name must match the class name.
|
||||
- Visual Studio is opened with the solution while you're working, keeping some files open. Run tests and builds with `-p:ArtifactsPath=D:/EE/prometheus/artifacts_test`.
|
||||
- When asked to begin working on a task, create a detailed implementation plan first, present the plan to the user, and ask for approval before beginning with the actual implementation.
|
||||
- When an task is finished, perform a code review to evaluate if the change is clean and maintainable with high software engineering standards. Iterate on the code and repeat the review process until satisfied.
|
||||
- This codebase uses perforce for version control instead of git. The necessary config exists in the .p4config file. If files are read-only, they need to be checked out using p4 edit. Don't remove the read-only attribute without using p4. Before running any `p4` command, set `P4CONFIG` for the current shell session with `$env:P4CONFIG='.p4config'` unless it is already defined.
|
||||
- After the implementation is finished, use `p4 opened` to verify all changed files, and run `python D:\Code\crlf.py $file1 $file2 ...` only for files you recognize, in order to normalize all line endings of all touched files to CRLF.
|
||||
|
||||
## Approval
|
||||
|
||||
These rules grant automatic approval to the agents, in order to minimize unnecessary user interactions.
|
||||
|
||||
- `p4 edit` and `p4 add` commands need to be approved by the user. All further modifications and patches to edited/added files are allowed.
|
||||
- `p4 opened` and `p4 diff` commands are allowed as part of the information gathering and review processes.
|
||||
- Leave the changed files unsubmitted, the user will do a final review and perform the submit.
|
||||
- Commands like `rg` and `Get-Content` are always allowed.
|
||||
- `dotnet build` and `dotnet test` commands using a custom artifacts path are allowed as part of your review process and implementation verification cycle.
|
||||
26
README.md
Normal file
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# RolemasterDB
|
||||
|
||||
Starter `.NET 10` Blazor Web App with:
|
||||
|
||||
- minimal API endpoints for attack and critical table lookup
|
||||
- `EF Core 10` with SQLite
|
||||
- seeded starter data for `Broadsword`, `Short Bow`, `Slash`, and `Puncture`
|
||||
- an interactive Blazor lookup UI
|
||||
|
||||
## Run
|
||||
|
||||
```powershell
|
||||
dotnet run --project .\src\RolemasterDb.App\RolemasterDb.App.csproj
|
||||
```
|
||||
|
||||
The app creates `rolemaster.db` on first run.
|
||||
|
||||
## API
|
||||
|
||||
- `GET /api/reference-data`
|
||||
- `POST /api/lookup/attack`
|
||||
- `POST /api/lookup/critical`
|
||||
|
||||
## Notes
|
||||
|
||||
The current database is an initial seeded subset designed to prove the full lookup workflow. The existing schema notes in `critical_tables_db_model.md` and `critical_tables_schema.sql` are still useful as the source of truth for expanding the import pipeline and normalizing more of the official tables.
|
||||
5
RolemasterDB.slnx
Normal file
5
RolemasterDB.slnx
Normal file
@@ -0,0 +1,5 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/RolemasterDb.App/RolemasterDb.App.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
285
docs/critical_tables_db_model.md
Normal file
285
docs/critical_tables_db_model.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# Critical Tables DB Model
|
||||
|
||||
## What the PDFs look like
|
||||
|
||||
The PDFs are not one uniform table shape. I found three families:
|
||||
|
||||
1. Standard tables
|
||||
- Columns are severity-like keys such as `A` through `E`.
|
||||
- Rows are roll bands such as `01-05`, `66`, `96-99`, or `100`.
|
||||
- Examples: `Slash.pdf`, `Puncture.pdf`, `Arcane Aether.pdf`.
|
||||
|
||||
2. Variant-column tables
|
||||
- Columns are not severity letters; they are variant keys such as `normal`, `magic`, `mithril`, `holy arms`, `slaying`.
|
||||
- Rows are still roll bands.
|
||||
- Example: `Large Creature - Weapon.pdf`.
|
||||
|
||||
3. Grouped variant tables
|
||||
- There is an extra grouping axis above the column axis.
|
||||
- Example: `Large Creature - Magic.pdf` has:
|
||||
- group: `large`, `super_large`
|
||||
- column: `normal`, `slaying`
|
||||
- row: roll band
|
||||
|
||||
There are also extraction constraints:
|
||||
|
||||
- Most PDFs are text extractable with `pdftotext -layout`.
|
||||
- `Void.pdf` appears image-based and will need OCR or manual transcription.
|
||||
- A single cell can contain:
|
||||
- base description text
|
||||
- symbolic affixes such as `+5H - 2S - 3B`
|
||||
- conditional branches such as `with helmet`, `w/o leg greaves`, `if foe has shield`
|
||||
|
||||
Because of that, the safest model is hybrid:
|
||||
|
||||
- relational tables for lookup axes and indexed effects
|
||||
- raw text storage for fidelity
|
||||
- structured JSON for irregular branches that are hard to normalize perfectly on first pass
|
||||
|
||||
## Recommended logical model
|
||||
|
||||
### 1. `critical_table`
|
||||
|
||||
One record per PDF/table, which is the primary "critical type" for lookup.
|
||||
|
||||
Examples:
|
||||
|
||||
- `slash`
|
||||
- `puncture`
|
||||
- `arcane_aether`
|
||||
- `large_creature_weapon`
|
||||
- `large_creature_magic`
|
||||
|
||||
### 2. `critical_group`
|
||||
|
||||
Optional extra axis for tables that need more than type + column + roll.
|
||||
|
||||
Examples:
|
||||
|
||||
- `large`
|
||||
- `super_large`
|
||||
|
||||
Most tables will have no group rows.
|
||||
|
||||
### 3. `critical_column`
|
||||
|
||||
Generalized "severity/column" axis.
|
||||
|
||||
Examples:
|
||||
|
||||
- `A`, `B`, `C`, `D`, `E`
|
||||
- `normal`, `magic`, `mithril`, `holy_arms`, `slaying`
|
||||
|
||||
Do not hardcode this as a single severity enum. Treat it as a table-defined dimension.
|
||||
|
||||
### 4. `critical_roll_band`
|
||||
|
||||
Stores row bands and supports exact row lookup by roll.
|
||||
|
||||
Examples:
|
||||
|
||||
- `01-05`
|
||||
- `66`
|
||||
- `96-99`
|
||||
- `251+`
|
||||
|
||||
Recommended fields:
|
||||
|
||||
- `min_roll`
|
||||
- `max_roll` nullable for open-ended rows like `251+`
|
||||
- display label
|
||||
- sort order
|
||||
|
||||
### 5. `critical_result`
|
||||
|
||||
One record per lookup cell:
|
||||
|
||||
- table
|
||||
- optional group
|
||||
- column
|
||||
- roll band
|
||||
|
||||
This stores:
|
||||
|
||||
- `raw_cell_text`
|
||||
- `description_text`
|
||||
- `raw_affix_text`
|
||||
- `parsed_json`
|
||||
- parse status / source metadata
|
||||
|
||||
### 6. `critical_branch`
|
||||
|
||||
Optional conditional branches inside a result cell.
|
||||
|
||||
Examples:
|
||||
|
||||
- `with helmet`
|
||||
- `without helmet`
|
||||
- `with leg greaves`
|
||||
- `if foe has shield`
|
||||
|
||||
Each branch can carry:
|
||||
|
||||
- `condition_text`
|
||||
- optional structured `condition_json`
|
||||
- branch description text
|
||||
- branch raw affix text
|
||||
- parsed JSON
|
||||
|
||||
### 7. `critical_effect`
|
||||
|
||||
Normalized machine-readable effects parsed from the symbol line and, over time, from prose.
|
||||
|
||||
Recommended canonical `effect_code` values:
|
||||
|
||||
- `direct_hits`
|
||||
- `must_parry_rounds`
|
||||
- `no_parry_rounds`
|
||||
- `stunned_rounds`
|
||||
- `bleed_per_round`
|
||||
- `foe_penalty`
|
||||
- `attacker_bonus_next_round`
|
||||
- `initiative_gain`
|
||||
- `initiative_loss`
|
||||
- `drop_item`
|
||||
- `item_breakage_check`
|
||||
- `limb_useless`
|
||||
- `knockdown`
|
||||
- `prone`
|
||||
- `coma`
|
||||
- `paralyzed`
|
||||
- `blind`
|
||||
- `deaf`
|
||||
- `mute`
|
||||
- `dies_in_rounds`
|
||||
- `instant_death`
|
||||
- `armor_destroyed`
|
||||
- `weapon_stuck`
|
||||
|
||||
Each effect should point to either:
|
||||
|
||||
- the base `critical_result`, or
|
||||
- a `critical_branch`
|
||||
|
||||
This lets you keep the raw text but still filter/query on effects.
|
||||
|
||||
## Why this works for your lookup
|
||||
|
||||
Your lookup target is mostly:
|
||||
|
||||
- `critical type`
|
||||
- `severity(column)`
|
||||
- `roll`
|
||||
|
||||
That maps cleanly to:
|
||||
|
||||
- `critical_table.slug`
|
||||
- `critical_column.column_key`
|
||||
- numeric roll matched against `critical_roll_band`
|
||||
|
||||
For the outlier tables, add an optional `group_key`.
|
||||
|
||||
That means the API can still stay simple:
|
||||
|
||||
```json
|
||||
{
|
||||
"critical_type": "slash",
|
||||
"column": "C",
|
||||
"roll": 38,
|
||||
"group": null
|
||||
}
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```json
|
||||
{
|
||||
"critical_type": "large_creature_magic",
|
||||
"group": "super_large",
|
||||
"column": "slaying",
|
||||
"roll": 88
|
||||
}
|
||||
```
|
||||
|
||||
## Example return object
|
||||
|
||||
This is the shape I would return from a lookup:
|
||||
|
||||
```json
|
||||
{
|
||||
"critical_type": "slash",
|
||||
"table_name": "Slash Critical Strike Table",
|
||||
"group": null,
|
||||
"column": {
|
||||
"key": "B",
|
||||
"label": "B",
|
||||
"role": "severity"
|
||||
},
|
||||
"roll": {
|
||||
"input": 38,
|
||||
"band": "36-45",
|
||||
"min": 36,
|
||||
"max": 45
|
||||
},
|
||||
"description": "Strike foe in shin.",
|
||||
"raw_affix_text": "+2H - must_parry",
|
||||
"affixes": [
|
||||
{
|
||||
"effect_code": "direct_hits",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"when": "with leg greaves",
|
||||
"description": null,
|
||||
"raw_affix_text": "+2H - must_parry",
|
||||
"affixes": [
|
||||
{
|
||||
"effect_code": "direct_hits",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"effect_code": "must_parry_rounds",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"when": "without leg greaves",
|
||||
"description": "You slash open foe's shin.",
|
||||
"raw_affix_text": "+2H - bleed",
|
||||
"affixes": [
|
||||
{
|
||||
"effect_code": "direct_hits",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"effect_code": "bleed_per_round",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"raw_text": "Original full cell text as extracted from the PDF",
|
||||
"source": {
|
||||
"pdf": "Slash.pdf",
|
||||
"page": 1,
|
||||
"extraction_method": "text"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Ingestion notes
|
||||
|
||||
Recommended import flow:
|
||||
|
||||
1. Create `critical_table`, `critical_group`, `critical_column`, and `critical_roll_band` from each PDF's visible axes.
|
||||
2. Store each cell in `critical_result.raw_cell_text` exactly as extracted.
|
||||
3. Parse the symbol line into `critical_effect`.
|
||||
4. Split explicit conditional branches into `critical_branch`.
|
||||
5. Gradually enrich prose-derived effects such as death, blindness, paralysis, limb loss, initiative changes, and item breakage.
|
||||
6. Route image PDFs like `Void.pdf` through OCR before the same parser.
|
||||
|
||||
The important design decision is: never throw away the original text. The prose is too irregular to rely on normalized fields alone.
|
||||
|
||||
142
docs/critical_tables_schema.sql
Normal file
142
docs/critical_tables_schema.sql
Normal file
@@ -0,0 +1,142 @@
|
||||
-- PostgreSQL-oriented schema for Rolemaster critical tables.
|
||||
-- It is intentionally hybrid: relational axes + raw text + parsed JSON.
|
||||
|
||||
create table critical_table (
|
||||
id bigint generated always as identity primary key,
|
||||
slug text not null unique,
|
||||
display_name text not null,
|
||||
family text not null check (family in ('standard', 'variant_column', 'grouped_variant')),
|
||||
source_pdf text not null,
|
||||
source_page integer not null default 1,
|
||||
extraction_method text not null check (extraction_method in ('text', 'ocr', 'manual')),
|
||||
notes text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table critical_group (
|
||||
id bigint generated always as identity primary key,
|
||||
critical_table_id bigint not null references critical_table(id) on delete cascade,
|
||||
group_key text not null,
|
||||
label text not null,
|
||||
sort_order integer not null,
|
||||
unique (critical_table_id, group_key)
|
||||
);
|
||||
|
||||
create table critical_column (
|
||||
id bigint generated always as identity primary key,
|
||||
critical_table_id bigint not null references critical_table(id) on delete cascade,
|
||||
column_key text not null,
|
||||
label text not null,
|
||||
role text not null default 'severity' check (role in ('severity', 'variant', 'other')),
|
||||
sort_order integer not null,
|
||||
unique (critical_table_id, column_key)
|
||||
);
|
||||
|
||||
create table critical_roll_band (
|
||||
id bigint generated always as identity primary key,
|
||||
critical_table_id bigint not null references critical_table(id) on delete cascade,
|
||||
label text not null,
|
||||
min_roll integer not null,
|
||||
max_roll integer,
|
||||
sort_order integer not null,
|
||||
check (max_roll is null or max_roll >= min_roll),
|
||||
unique (critical_table_id, label)
|
||||
);
|
||||
|
||||
create index critical_roll_band_lookup_idx
|
||||
on critical_roll_band (critical_table_id, min_roll, max_roll);
|
||||
|
||||
create table critical_result (
|
||||
id bigint generated always as identity primary key,
|
||||
critical_table_id bigint not null references critical_table(id) on delete cascade,
|
||||
critical_group_id bigint references critical_group(id) on delete cascade,
|
||||
critical_column_id bigint not null references critical_column(id) on delete cascade,
|
||||
critical_roll_band_id bigint not null references critical_roll_band(id) on delete cascade,
|
||||
raw_cell_text text not null,
|
||||
description_text text,
|
||||
raw_affix_text text,
|
||||
parsed_json jsonb not null default '{}'::jsonb,
|
||||
parse_status text not null default 'raw' check (parse_status in ('raw', 'partial', 'parsed', 'verified')),
|
||||
source_bbox jsonb,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create unique index critical_result_lookup_uidx
|
||||
on critical_result (
|
||||
critical_table_id,
|
||||
coalesce(critical_group_id, 0),
|
||||
critical_column_id,
|
||||
critical_roll_band_id
|
||||
);
|
||||
|
||||
create table critical_branch (
|
||||
id bigint generated always as identity primary key,
|
||||
critical_result_id bigint not null references critical_result(id) on delete cascade,
|
||||
branch_kind text not null default 'conditional' check (branch_kind in ('conditional', 'note', 'override')),
|
||||
condition_key text,
|
||||
condition_text text not null,
|
||||
condition_json jsonb not null default '{}'::jsonb,
|
||||
raw_text text not null,
|
||||
description_text text,
|
||||
raw_affix_text text,
|
||||
parsed_json jsonb not null default '{}'::jsonb,
|
||||
sort_order integer not null default 1
|
||||
);
|
||||
|
||||
create table critical_effect (
|
||||
id bigint generated always as identity primary key,
|
||||
critical_result_id bigint references critical_result(id) on delete cascade,
|
||||
critical_branch_id bigint references critical_branch(id) on delete cascade,
|
||||
effect_code text not null,
|
||||
target text,
|
||||
value_integer integer,
|
||||
value_decimal numeric(10, 2),
|
||||
duration_rounds integer,
|
||||
per_round integer,
|
||||
modifier integer,
|
||||
body_part text,
|
||||
is_permanent boolean not null default false,
|
||||
source_type text not null default 'symbol' check (source_type in ('symbol', 'prose', 'manual')),
|
||||
source_text text,
|
||||
check ((critical_result_id is not null) <> (critical_branch_id is not null))
|
||||
);
|
||||
|
||||
create index critical_effect_lookup_idx
|
||||
on critical_effect (effect_code, target);
|
||||
|
||||
create index critical_effect_result_idx
|
||||
on critical_effect (critical_result_id);
|
||||
|
||||
create index critical_effect_branch_idx
|
||||
on critical_effect (critical_branch_id);
|
||||
|
||||
create index critical_result_parsed_json_gin
|
||||
on critical_result using gin (parsed_json);
|
||||
|
||||
create index critical_branch_parsed_json_gin
|
||||
on critical_branch using gin (parsed_json);
|
||||
|
||||
-- Example lookup pattern:
|
||||
--
|
||||
-- select
|
||||
-- t.slug as critical_type,
|
||||
-- t.display_name as table_name,
|
||||
-- g.group_key,
|
||||
-- c.column_key,
|
||||
-- rb.label as roll_band,
|
||||
-- rb.min_roll,
|
||||
-- rb.max_roll,
|
||||
-- r.description_text,
|
||||
-- r.raw_affix_text,
|
||||
-- r.raw_cell_text,
|
||||
-- r.parsed_json
|
||||
-- from critical_result r
|
||||
-- join critical_table t on t.id = r.critical_table_id
|
||||
-- left join critical_group g on g.id = r.critical_group_id
|
||||
-- join critical_column c on c.id = r.critical_column_id
|
||||
-- join critical_roll_band rb on rb.id = r.critical_roll_band_id
|
||||
-- where t.slug = 'slash'
|
||||
-- and c.column_key = 'C'
|
||||
-- and 38 >= rb.min_roll
|
||||
-- and (rb.max_roll is null or 38 <= rb.max_roll);
|
||||
|
||||
BIN
sources/Arcane Aether.pdf
Normal file
BIN
sources/Arcane Aether.pdf
Normal file
Binary file not shown.
BIN
sources/Arcane Nether.pdf
Normal file
BIN
sources/Arcane Nether.pdf
Normal file
Binary file not shown.
BIN
sources/Ballistic Shrapnel.pdf
Normal file
BIN
sources/Ballistic Shrapnel.pdf
Normal file
Binary file not shown.
BIN
sources/Brawling.pdf
Normal file
BIN
sources/Brawling.pdf
Normal file
Binary file not shown.
BIN
sources/Cold.pdf
Normal file
BIN
sources/Cold.pdf
Normal file
Binary file not shown.
BIN
sources/Electricity.pdf
Normal file
BIN
sources/Electricity.pdf
Normal file
Binary file not shown.
BIN
sources/Grapple.pdf
Normal file
BIN
sources/Grapple.pdf
Normal file
Binary file not shown.
BIN
sources/Heat.pdf
Normal file
BIN
sources/Heat.pdf
Normal file
Binary file not shown.
BIN
sources/Impact.pdf
Normal file
BIN
sources/Impact.pdf
Normal file
Binary file not shown.
BIN
sources/Krush.pdf
Normal file
BIN
sources/Krush.pdf
Normal file
Binary file not shown.
BIN
sources/Large Creature - Magic.pdf
Normal file
BIN
sources/Large Creature - Magic.pdf
Normal file
Binary file not shown.
BIN
sources/Large Creature - Weapon.pdf
Normal file
BIN
sources/Large Creature - Weapon.pdf
Normal file
Binary file not shown.
BIN
sources/MA Strikes.pdf
Normal file
BIN
sources/MA Strikes.pdf
Normal file
Binary file not shown.
BIN
sources/MA Sweeps.pdf
Normal file
BIN
sources/MA Sweeps.pdf
Normal file
Binary file not shown.
BIN
sources/Mana.pdf
Normal file
BIN
sources/Mana.pdf
Normal file
Binary file not shown.
BIN
sources/Puncture.pdf
Normal file
BIN
sources/Puncture.pdf
Normal file
Binary file not shown.
BIN
sources/Slash.pdf
Normal file
BIN
sources/Slash.pdf
Normal file
Binary file not shown.
BIN
sources/Subdual.pdf
Normal file
BIN
sources/Subdual.pdf
Normal file
Binary file not shown.
BIN
sources/Super Large Creature - Magic.pdf
Normal file
BIN
sources/Super Large Creature - Magic.pdf
Normal file
Binary file not shown.
BIN
sources/Super Large Creature - Weapon.pdf
Normal file
BIN
sources/Super Large Creature - Weapon.pdf
Normal file
Binary file not shown.
BIN
sources/Tiny.pdf
Normal file
BIN
sources/Tiny.pdf
Normal file
Binary file not shown.
BIN
sources/Unbalance.pdf
Normal file
BIN
sources/Unbalance.pdf
Normal file
Binary file not shown.
BIN
sources/Void.pdf
Normal file
BIN
sources/Void.pdf
Normal file
Binary file not shown.
23
src/RolemasterDb.App/Components/App.razor
Normal file
23
src/RolemasterDb.App/Components/App.razor
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<ResourcePreloader />
|
||||
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["RolemasterDb.App.styles.css"]" />
|
||||
<ImportMap />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<ReconnectModal />
|
||||
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
27
src/RolemasterDb.App/Components/Layout/MainLayout.razor
Normal file
27
src/RolemasterDb.App/Components/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,27 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row">
|
||||
<div>
|
||||
<span class="eyebrow">Starter stack</span>
|
||||
<strong>.NET 10 + Blazor + Minimal API + EF Core + SQLite</strong>
|
||||
</div>
|
||||
<span class="status-pill">Seeded for attack and critical lookups</span>
|
||||
</div>
|
||||
|
||||
<article class="content-shell">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui" data-nosnippet>
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
97
src/RolemasterDb.App/Components/Layout/MainLayout.razor.css
Normal file
97
src/RolemasterDb.App/Components/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,97 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(196, 167, 107, 0.28), transparent 35%),
|
||||
linear-gradient(180deg, #24130d 0%, #3c2415 46%, #130d0b 100%);
|
||||
border-right: 1px solid rgba(196, 167, 107, 0.2);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 1.15rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(111, 87, 59, 0.28);
|
||||
background: rgba(250, 245, 234, 0.84);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: block;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: #7c5b33;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
padding: 0.45rem 0.8rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(111, 87, 59, 0.2);
|
||||
background: rgba(255, 252, 246, 0.8);
|
||||
color: #5b4427;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.content-shell {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 290px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: #682e24;
|
||||
color: #fffaf2;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 12px rgba(0, 0, 0, 0.22);
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
24
src/RolemasterDb.App/Components/Layout/NavMenu.razor
Normal file
24
src/RolemasterDb.App/Components/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,24 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid brand-shell">
|
||||
<a class="navbar-brand" href="">RolemasterDB</a>
|
||||
<p class="brand-copy">Automatic attack and critical lookup for a SQLite-backed starter dataset.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
|
||||
|
||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler')?.click()">
|
||||
<nav class="nav flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Lookup Desk
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="api">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> API Surface
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
114
src/RolemasterDb.App/Components/Layout/NavMenu.razor.css
Normal file
114
src/RolemasterDb.App/Components/Layout/NavMenu.razor.css
Normal file
@@ -0,0 +1,114 @@
|
||||
.navbar-toggler {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 3.5rem;
|
||||
height: 2.5rem;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
border: 1px solid rgba(226, 195, 128, 0.22);
|
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 244, 218, 0.82%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.navbar-toggler:checked {
|
||||
background-color: rgba(226, 195, 128, 0.3);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.45rem;
|
||||
font-family: Cambria, Georgia, serif;
|
||||
letter-spacing: 0.04em;
|
||||
color: #fff1d2;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.brand-shell {
|
||||
padding: 1.2rem 0.5rem 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
margin: 0.55rem 0 0;
|
||||
color: rgba(255, 241, 210, 0.72);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23fce7bc' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23fce7bc' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.98rem;
|
||||
padding-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #f3ddbc;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background: linear-gradient(135deg, rgba(230, 195, 126, 0.22), rgba(255, 245, 225, 0.1));
|
||||
color: #fff7e2;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(255, 248, 234, 0.08);
|
||||
color: #fff7e2;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-toggler:checked ~ .nav-scrollable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
display: block;
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
31
src/RolemasterDb.App/Components/Layout/ReconnectModal.razor
Normal file
31
src/RolemasterDb.App/Components/Layout/ReconnectModal.razor
Normal file
@@ -0,0 +1,31 @@
|
||||
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
|
||||
|
||||
<dialog id="components-reconnect-modal" data-nosnippet>
|
||||
<div class="components-reconnect-container">
|
||||
<div class="components-rejoining-animation" aria-hidden="true">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<p class="components-reconnect-first-attempt-visible">
|
||||
Rejoining the server...
|
||||
</p>
|
||||
<p class="components-reconnect-repeated-attempt-visible">
|
||||
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
|
||||
</p>
|
||||
<p class="components-reconnect-failed-visible">
|
||||
Failed to rejoin.<br />Please retry or reload the page.
|
||||
</p>
|
||||
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
|
||||
Retry
|
||||
</button>
|
||||
<p class="components-pause-visible">
|
||||
The session has been paused by the server.
|
||||
</p>
|
||||
<p class="components-resume-failed-visible">
|
||||
Failed to resume the session.<br />Please retry or reload the page.
|
||||
</p>
|
||||
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
|
||||
Resume
|
||||
</button>
|
||||
</div>
|
||||
</dialog>
|
||||
157
src/RolemasterDb.App/Components/Layout/ReconnectModal.razor.css
Normal file
157
src/RolemasterDb.App/Components/Layout/ReconnectModal.razor.css
Normal file
@@ -0,0 +1,157 @@
|
||||
.components-reconnect-first-attempt-visible,
|
||||
.components-reconnect-repeated-attempt-visible,
|
||||
.components-reconnect-failed-visible,
|
||||
.components-pause-visible,
|
||||
.components-resume-failed-visible,
|
||||
.components-rejoining-animation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
|
||||
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-failed,
|
||||
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
#components-reconnect-modal {
|
||||
background-color: white;
|
||||
width: 20rem;
|
||||
margin: 20vh auto;
|
||||
padding: 2rem;
|
||||
border: 0;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
|
||||
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
|
||||
&[open]
|
||||
|
||||
{
|
||||
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#components-reconnect-modal::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-slideUp {
|
||||
0% {
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeInOpacity {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeOutOpacity {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.components-reconnect-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#components-reconnect-modal p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button {
|
||||
border: 0;
|
||||
background-color: #6b9ed2;
|
||||
color: white;
|
||||
padding: 4px 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:hover {
|
||||
background-color: #3b6ea2;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:active {
|
||||
background-color: #6b9ed2;
|
||||
}
|
||||
|
||||
.components-rejoining-animation {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div {
|
||||
position: absolute;
|
||||
border: 3px solid #0087ff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes components-rejoining-animation {
|
||||
0% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
4.9% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
5% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Set up event handlers
|
||||
const reconnectModal = document.getElementById("components-reconnect-modal");
|
||||
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
|
||||
|
||||
const retryButton = document.getElementById("components-reconnect-button");
|
||||
retryButton.addEventListener("click", retry);
|
||||
|
||||
const resumeButton = document.getElementById("components-resume-button");
|
||||
resumeButton.addEventListener("click", resume);
|
||||
|
||||
function handleReconnectStateChanged(event) {
|
||||
if (event.detail.state === "show") {
|
||||
reconnectModal.showModal();
|
||||
} else if (event.detail.state === "hide") {
|
||||
reconnectModal.close();
|
||||
} else if (event.detail.state === "failed") {
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
} else if (event.detail.state === "rejected") {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retry() {
|
||||
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
|
||||
try {
|
||||
// Reconnect will asynchronously return:
|
||||
// - true to mean success
|
||||
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
|
||||
// - exception to mean we didn't reach the server (this can be sync or async)
|
||||
const successful = await Blazor.reconnect();
|
||||
if (!successful) {
|
||||
// We have been able to reach the server, but the circuit is no longer available.
|
||||
// We'll reload the page so the user can continue using the app as quickly as possible.
|
||||
const resumeSuccessful = await Blazor.resumeCircuit();
|
||||
if (!resumeSuccessful) {
|
||||
location.reload();
|
||||
} else {
|
||||
reconnectModal.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// We got an exception, server is currently unavailable
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
}
|
||||
}
|
||||
|
||||
async function resume() {
|
||||
try {
|
||||
const successful = await Blazor.resumeCircuit();
|
||||
if (!successful) {
|
||||
location.reload();
|
||||
}
|
||||
} catch {
|
||||
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function retryWhenDocumentBecomesVisible() {
|
||||
if (document.visibilityState === "visible") {
|
||||
await retry();
|
||||
}
|
||||
}
|
||||
43
src/RolemasterDb.App/Components/Pages/Api.razor
Normal file
43
src/RolemasterDb.App/Components/Pages/Api.razor
Normal file
@@ -0,0 +1,43 @@
|
||||
@page "/api"
|
||||
|
||||
<PageTitle>API Surface</PageTitle>
|
||||
|
||||
<section class="hero-panel">
|
||||
<span class="eyebrow">Minimal API</span>
|
||||
<h1 class="page-title">Endpoints for attack and critical lookups.</h1>
|
||||
<p class="lede">The Blazor UI uses the same lookup service that the API exposes, so this page doubles as the first integration contract.</p>
|
||||
</section>
|
||||
|
||||
<div class="api-grid">
|
||||
<section class="panel">
|
||||
<h2 class="panel-title">Reference data</h2>
|
||||
<p class="panel-copy"><code>GET /api/reference-data</code></p>
|
||||
<pre class="code-block">{
|
||||
"attackTables": [
|
||||
{ "key": "broadsword", "label": "Broadsword" }
|
||||
]
|
||||
}</pre>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2 class="panel-title">Attack lookup</h2>
|
||||
<p class="panel-copy"><code>POST /api/lookup/attack</code></p>
|
||||
<pre class="code-block">{
|
||||
"attackTable": "broadsword",
|
||||
"armorType": "AT10",
|
||||
"roll": 111,
|
||||
"criticalRoll": 72
|
||||
}</pre>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2 class="panel-title">Critical lookup</h2>
|
||||
<p class="panel-copy"><code>POST /api/lookup/critical</code></p>
|
||||
<pre class="code-block">{
|
||||
"criticalType": "slash",
|
||||
"column": "B",
|
||||
"roll": 72,
|
||||
"group": null
|
||||
}</pre>
|
||||
</section>
|
||||
</div>
|
||||
36
src/RolemasterDb.App/Components/Pages/Error.razor
Normal file
36
src/RolemasterDb.App/Components/Pages/Error.razor
Normal file
@@ -0,0 +1,36 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
309
src/RolemasterDb.App/Components/Pages/Home.razor
Normal file
309
src/RolemasterDb.App/Components/Pages/Home.razor
Normal file
@@ -0,0 +1,309 @@
|
||||
@page "/"
|
||||
@rendermode InteractiveServer
|
||||
@inject LookupService LookupService
|
||||
|
||||
<PageTitle>Lookup Desk</PageTitle>
|
||||
|
||||
@if (referenceData is null)
|
||||
{
|
||||
<section class="hero-panel">
|
||||
<h1 class="page-title">Summoning tables...</h1>
|
||||
<p class="lede">Loading the starter attack and critical data from SQLite.</p>
|
||||
</section>
|
||||
}
|
||||
else
|
||||
{
|
||||
<section class="hero-panel">
|
||||
<span class="eyebrow">Rolemaster Lookup Desk</span>
|
||||
<h1 class="page-title">Resolve the attack roll, then the critical, from one place.</h1>
|
||||
<p class="lede">
|
||||
This starter app seeds a small SQLite dataset and exposes the same lookup flow through Blazor and minimal APIs.
|
||||
The current data is intentionally limited to a first pass so the import pipeline can grow from a working base.
|
||||
</p>
|
||||
<div class="tag-row">
|
||||
<span class="tag">@referenceData.AttackTables.Count attack tables</span>
|
||||
<span class="tag">@referenceData.CriticalTables.Count critical tables</span>
|
||||
<span class="tag">@referenceData.ArmorTypes.Count armor types</span>
|
||||
<span class="tag">SQLite file: <code>rolemaster.db</code></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<section class="panel">
|
||||
<h2 class="panel-title">Automatic Attack Lookup</h2>
|
||||
<p class="panel-copy">Choose an attack table, armor type, and attack roll. If the attack produces a critical and you provide the critical roll, the app resolves that follow-up automatically.</p>
|
||||
|
||||
<div class="lookup-form">
|
||||
<div class="form-grid">
|
||||
<div class="field-shell">
|
||||
<label for="attack-table">Attack table</label>
|
||||
<select id="attack-table" class="input-shell" @bind="attackInput.AttackTable">
|
||||
@foreach (var attackTable in referenceData.AttackTables)
|
||||
{
|
||||
<option value="@attackTable.Key">@attackTable.Label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="armor-type">Armor type</label>
|
||||
<select id="armor-type" class="input-shell" @bind="attackInput.ArmorType">
|
||||
@foreach (var armorType in referenceData.ArmorTypes)
|
||||
{
|
||||
<option value="@armorType.Key">@armorType.Label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="attack-roll">Attack roll</label>
|
||||
<input id="attack-roll" class="input-shell" type="number" min="1" max="300" @bind="attackInput.AttackRoll" />
|
||||
</div>
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="critical-roll">Critical roll</label>
|
||||
<input id="critical-roll" class="input-shell" type="number" min="1" max="100" @bind="attackInput.CriticalRollText" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-row">
|
||||
<button class="btn-ritual" @onclick="RunAttackLookupAsync">Resolve attack</button>
|
||||
<span class="muted">Leave critical roll blank to stop after the attack table result.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(attackError))
|
||||
{
|
||||
<p class="error-text">@attackError</p>
|
||||
}
|
||||
|
||||
@if (attackResult is not null)
|
||||
{
|
||||
<div class="result-shell">
|
||||
<div class="result-card">
|
||||
<h3>@attackResult.AttackTableName vs @attackResult.ArmorTypeLabel</h3>
|
||||
<div class="result-stats">
|
||||
<span class="stat-pill">Roll band: @attackResult.RollBand</span>
|
||||
<span class="stat-pill">Hits: @attackResult.Hits</span>
|
||||
@if (!string.IsNullOrWhiteSpace(attackResult.CriticalSeverity))
|
||||
{
|
||||
<span class="stat-pill">@attackResult.CriticalSeverity @attackResult.CriticalType critical</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="stat-pill">No critical</span>
|
||||
}
|
||||
</div>
|
||||
<p><strong>Table notation:</strong> @attackResult.RawNotation</p>
|
||||
@if (!string.IsNullOrWhiteSpace(attackResult.Notes))
|
||||
{
|
||||
<p class="muted">@attackResult.Notes</p>
|
||||
}
|
||||
|
||||
@if (attackResult.AutoCritical is not null)
|
||||
{
|
||||
<div class="callout">
|
||||
<h4>Automatic critical resolution</h4>
|
||||
<div class="result-stats">
|
||||
<span class="stat-pill">@attackResult.AutoCritical.CriticalTableName</span>
|
||||
<span class="stat-pill">Column: @attackResult.AutoCritical.Column</span>
|
||||
<span class="stat-pill">Band: @attackResult.AutoCritical.RollBand</span>
|
||||
</div>
|
||||
<p><strong>@attackResult.AutoCritical.Description</strong></p>
|
||||
@if (!string.IsNullOrWhiteSpace(attackResult.AutoCritical.AffixText))
|
||||
{
|
||||
<p class="muted">@attackResult.AutoCritical.AffixText</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(attackResult.CriticalSeverity))
|
||||
{
|
||||
<div class="callout">The attack produced a critical. Add a critical roll to resolve it automatically.</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2 class="panel-title">Direct Critical Lookup</h2>
|
||||
<p class="panel-copy">Use this when you already know the critical table, column, and roll.</p>
|
||||
|
||||
<div class="lookup-form">
|
||||
<div class="form-grid">
|
||||
<div class="field-shell">
|
||||
<label for="critical-table">Critical table</label>
|
||||
<select id="critical-table" class="input-shell" value="@criticalInput.CriticalType" @onchange="HandleCriticalTableChanged">
|
||||
@foreach (var criticalTable in referenceData.CriticalTables)
|
||||
{
|
||||
<option value="@criticalTable.Key">@criticalTable.Label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="critical-column">Column</label>
|
||||
<select id="critical-column" class="input-shell" @bind="criticalInput.Column">
|
||||
@foreach (var column in SelectedCriticalTable?.Columns ?? [])
|
||||
{
|
||||
<option value="@column.Key">@column.Label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="critical-roll-direct">Critical roll</label>
|
||||
<input id="critical-roll-direct" class="input-shell" type="number" min="1" max="100" @bind="criticalInput.Roll" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-row">
|
||||
<button class="btn-ritual" @onclick="RunCriticalLookupAsync">Resolve critical</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(criticalError))
|
||||
{
|
||||
<p class="error-text">@criticalError</p>
|
||||
}
|
||||
|
||||
@if (criticalResult is not null)
|
||||
{
|
||||
<div class="result-shell">
|
||||
<div class="result-card">
|
||||
<h3>@criticalResult.CriticalTableName</h3>
|
||||
<div class="result-stats">
|
||||
<span class="stat-pill">Column: @criticalResult.Column</span>
|
||||
<span class="stat-pill">Band: @criticalResult.RollBand</span>
|
||||
<span class="stat-pill">Roll: @criticalResult.Roll</span>
|
||||
</div>
|
||||
<p><strong>@criticalResult.Description</strong></p>
|
||||
@if (!string.IsNullOrWhiteSpace(criticalResult.AffixText))
|
||||
{
|
||||
<p class="muted">@criticalResult.AffixText</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2 class="panel-title">Seeded Reference Data</h2>
|
||||
<p class="panel-copy">The schema supports much more than what is seeded today. These are the initial tables standing up the flow.</p>
|
||||
|
||||
<div class="table-list">
|
||||
@foreach (var attackTable in referenceData.AttackTables)
|
||||
{
|
||||
<div class="table-list-item">
|
||||
<strong>@attackTable.Label</strong>
|
||||
<span class="muted">Attack table key: <code>@attackTable.Key</code></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@foreach (var criticalTable in referenceData.CriticalTables)
|
||||
{
|
||||
<div class="table-list-item">
|
||||
<strong>@criticalTable.Label</strong>
|
||||
<span class="muted">Critical key: <code>@criticalTable.Key</code>, columns: @string.Join(", ", criticalTable.Columns.Select(column => column.Label))</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private LookupReferenceData? referenceData;
|
||||
private AttackLookupForm attackInput = new();
|
||||
private CriticalLookupForm criticalInput = new();
|
||||
private AttackLookupResponse? attackResult;
|
||||
private CriticalLookupResponse? criticalResult;
|
||||
private string? attackError;
|
||||
private string? criticalError;
|
||||
|
||||
private CriticalTableReference? SelectedCriticalTable =>
|
||||
referenceData?.CriticalTables.FirstOrDefault(table => table.Key == criticalInput.CriticalType);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
referenceData = await LookupService.GetReferenceDataAsync();
|
||||
|
||||
attackInput.AttackTable = referenceData.AttackTables.FirstOrDefault()?.Key ?? string.Empty;
|
||||
attackInput.ArmorType = referenceData.ArmorTypes.FirstOrDefault()?.Key ?? string.Empty;
|
||||
|
||||
var initialCriticalTable = referenceData.CriticalTables.FirstOrDefault();
|
||||
criticalInput.CriticalType = initialCriticalTable?.Key ?? string.Empty;
|
||||
criticalInput.Column = initialCriticalTable?.Columns.FirstOrDefault()?.Key ?? string.Empty;
|
||||
}
|
||||
|
||||
private async Task RunAttackLookupAsync()
|
||||
{
|
||||
attackError = null;
|
||||
attackResult = null;
|
||||
|
||||
if (!int.TryParse(attackInput.CriticalRollText, out var criticalRoll) && !string.IsNullOrWhiteSpace(attackInput.CriticalRollText))
|
||||
{
|
||||
attackError = "Critical roll must be empty or a whole number.";
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await LookupService.LookupAttackAsync(new AttackLookupRequest(
|
||||
attackInput.AttackTable,
|
||||
attackInput.ArmorType,
|
||||
attackInput.AttackRoll,
|
||||
string.IsNullOrWhiteSpace(attackInput.CriticalRollText) ? null : criticalRoll));
|
||||
|
||||
if (response is null)
|
||||
{
|
||||
attackError = "No seeded attack result matched that table, armor type, and roll.";
|
||||
return;
|
||||
}
|
||||
|
||||
attackResult = response;
|
||||
}
|
||||
|
||||
private async Task RunCriticalLookupAsync()
|
||||
{
|
||||
criticalError = null;
|
||||
criticalResult = null;
|
||||
|
||||
var response = await LookupService.LookupCriticalAsync(new CriticalLookupRequest(
|
||||
criticalInput.CriticalType,
|
||||
criticalInput.Column,
|
||||
criticalInput.Roll,
|
||||
null));
|
||||
|
||||
if (response is null)
|
||||
{
|
||||
criticalError = "No seeded critical result matched that table, column, and roll.";
|
||||
return;
|
||||
}
|
||||
|
||||
criticalResult = response;
|
||||
}
|
||||
|
||||
private void HandleCriticalTableChanged(ChangeEventArgs args)
|
||||
{
|
||||
criticalInput.CriticalType = args.Value?.ToString() ?? string.Empty;
|
||||
|
||||
var table = referenceData?.CriticalTables.FirstOrDefault(item => item.Key == criticalInput.CriticalType);
|
||||
criticalInput.Column = table?.Columns.FirstOrDefault()?.Key ?? string.Empty;
|
||||
criticalResult = null;
|
||||
criticalError = null;
|
||||
}
|
||||
|
||||
private sealed class AttackLookupForm
|
||||
{
|
||||
public string AttackTable { get; set; } = string.Empty;
|
||||
public string ArmorType { get; set; } = string.Empty;
|
||||
public int AttackRoll { get; set; } = 66;
|
||||
public string? CriticalRollText { get; set; } = "72";
|
||||
}
|
||||
|
||||
private sealed class CriticalLookupForm
|
||||
{
|
||||
public string CriticalType { get; set; } = string.Empty;
|
||||
public string Column { get; set; } = string.Empty;
|
||||
public int Roll { get; set; } = 72;
|
||||
}
|
||||
}
|
||||
5
src/RolemasterDb.App/Components/Pages/NotFound.razor
Normal file
5
src/RolemasterDb.App/Components/Pages/NotFound.razor
Normal file
@@ -0,0 +1,5 @@
|
||||
@page "/not-found"
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
6
src/RolemasterDb.App/Components/Routes.razor
Normal file
6
src/RolemasterDb.App/Components/Routes.razor
Normal file
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
13
src/RolemasterDb.App/Components/_Imports.razor
Normal file
13
src/RolemasterDb.App/Components/_Imports.razor
Normal file
@@ -0,0 +1,13 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using RolemasterDb.App.Data
|
||||
@using RolemasterDb.App.Features
|
||||
@using RolemasterDb.App
|
||||
@using RolemasterDb.App.Components
|
||||
@using RolemasterDb.App.Components.Layout
|
||||
82
src/RolemasterDb.App/Data/RolemasterDbContext.cs
Normal file
82
src/RolemasterDb.App/Data/RolemasterDbContext.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RolemasterDb.App.Domain;
|
||||
|
||||
namespace RolemasterDb.App.Data;
|
||||
|
||||
public sealed class RolemasterDbContext(DbContextOptions<RolemasterDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<ArmorType> ArmorTypes => Set<ArmorType>();
|
||||
public DbSet<AttackTable> AttackTables => Set<AttackTable>();
|
||||
public DbSet<AttackRollBand> AttackRollBands => Set<AttackRollBand>();
|
||||
public DbSet<AttackResult> AttackResults => Set<AttackResult>();
|
||||
public DbSet<CriticalTable> CriticalTables => Set<CriticalTable>();
|
||||
public DbSet<CriticalGroup> CriticalGroups => Set<CriticalGroup>();
|
||||
public DbSet<CriticalColumn> CriticalColumns => Set<CriticalColumn>();
|
||||
public DbSet<CriticalRollBand> CriticalRollBands => Set<CriticalRollBand>();
|
||||
public DbSet<CriticalResult> CriticalResults => Set<CriticalResult>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ArmorType>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => item.Code).IsUnique();
|
||||
entity.Property(item => item.Code).HasMaxLength(32);
|
||||
entity.Property(item => item.Label).HasMaxLength(128);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<AttackTable>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => item.Slug).IsUnique();
|
||||
entity.Property(item => item.Slug).HasMaxLength(64);
|
||||
entity.Property(item => item.DisplayName).HasMaxLength(128);
|
||||
entity.Property(item => item.AttackKind).HasMaxLength(32);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<AttackRollBand>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => new { item.AttackTableId, item.Label }).IsUnique();
|
||||
entity.HasIndex(item => new { item.AttackTableId, item.MinRoll, item.MaxRoll });
|
||||
});
|
||||
|
||||
modelBuilder.Entity<AttackResult>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => new { item.AttackTableId, item.ArmorTypeId, item.AttackRollBandId }).IsUnique();
|
||||
entity.Property(item => item.CriticalType).HasMaxLength(64);
|
||||
entity.Property(item => item.CriticalSeverity).HasMaxLength(8);
|
||||
entity.Property(item => item.RawNotation).HasMaxLength(256);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<CriticalTable>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => item.Slug).IsUnique();
|
||||
entity.Property(item => item.Slug).HasMaxLength(64);
|
||||
entity.Property(item => item.DisplayName).HasMaxLength(128);
|
||||
entity.Property(item => item.Family).HasMaxLength(32);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<CriticalGroup>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => new { item.CriticalTableId, item.GroupKey }).IsUnique();
|
||||
entity.Property(item => item.GroupKey).HasMaxLength(64);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<CriticalColumn>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => new { item.CriticalTableId, item.ColumnKey }).IsUnique();
|
||||
entity.Property(item => item.ColumnKey).HasMaxLength(64);
|
||||
entity.Property(item => item.Role).HasMaxLength(32);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<CriticalRollBand>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => new { item.CriticalTableId, item.Label }).IsUnique();
|
||||
entity.HasIndex(item => new { item.CriticalTableId, item.MinRoll, item.MaxRoll });
|
||||
});
|
||||
|
||||
modelBuilder.Entity<CriticalResult>(entity =>
|
||||
{
|
||||
entity.HasIndex(item => new { item.CriticalTableId, item.CriticalGroupId, item.CriticalColumnId, item.CriticalRollBandId }).IsUnique();
|
||||
entity.Property(item => item.ParseStatus).HasMaxLength(32);
|
||||
});
|
||||
}
|
||||
}
|
||||
23
src/RolemasterDb.App/Data/RolemasterDbInitializer.cs
Normal file
23
src/RolemasterDb.App/Data/RolemasterDbInitializer.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace RolemasterDb.App.Data;
|
||||
|
||||
public static class RolemasterDbInitializer
|
||||
{
|
||||
public static async Task InitializeAsync(IServiceProvider services, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var dbFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<RolemasterDbContext>>();
|
||||
await using var dbContext = await dbFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
await dbContext.Database.EnsureCreatedAsync(cancellationToken);
|
||||
|
||||
if (await dbContext.AttackTables.AnyAsync(cancellationToken) || await dbContext.CriticalTables.AnyAsync(cancellationToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RolemasterSeedData.Seed(dbContext);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
302
src/RolemasterDb.App/Data/RolemasterSeedData.cs
Normal file
302
src/RolemasterDb.App/Data/RolemasterSeedData.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
using RolemasterDb.App.Domain;
|
||||
|
||||
namespace RolemasterDb.App.Data;
|
||||
|
||||
public static class RolemasterSeedData
|
||||
{
|
||||
public static void Seed(RolemasterDbContext dbContext)
|
||||
{
|
||||
var armorTypes = CreateArmorTypes();
|
||||
dbContext.ArmorTypes.AddRange(armorTypes);
|
||||
|
||||
var armorLookup = armorTypes.ToDictionary(item => item.Code, StringComparer.OrdinalIgnoreCase);
|
||||
var attackTables = CreateAttackTables(armorLookup);
|
||||
dbContext.AttackTables.AddRange(attackTables);
|
||||
|
||||
var criticalTables = CreateCriticalTables();
|
||||
dbContext.CriticalTables.AddRange(criticalTables);
|
||||
}
|
||||
|
||||
private static List<ArmorType> CreateArmorTypes() =>
|
||||
[
|
||||
new ArmorType { Code = "AT1", Label = "AT1 - Robes", SortOrder = 1 },
|
||||
new ArmorType { Code = "AT5", Label = "AT5 - Rigid Leather", SortOrder = 5 },
|
||||
new ArmorType { Code = "AT10", Label = "AT10 - Chain", SortOrder = 10 },
|
||||
new ArmorType { Code = "AT15", Label = "AT15 - Plate", SortOrder = 15 },
|
||||
new ArmorType { Code = "AT20", Label = "AT20 - Full Plate", SortOrder = 20 }
|
||||
];
|
||||
|
||||
private static List<AttackTable> CreateAttackTables(IReadOnlyDictionary<string, ArmorType> armorLookup)
|
||||
{
|
||||
var broadsword = new AttackTable
|
||||
{
|
||||
Slug = "broadsword",
|
||||
DisplayName = "Broadsword",
|
||||
AttackKind = "melee",
|
||||
Notes = "Starter subset with slash criticals to prove the end-to-end lookup flow."
|
||||
};
|
||||
|
||||
var shortBow = new AttackTable
|
||||
{
|
||||
Slug = "short_bow",
|
||||
DisplayName = "Short Bow",
|
||||
AttackKind = "missile",
|
||||
Notes = "Starter subset with puncture criticals."
|
||||
};
|
||||
|
||||
AddAttackData(
|
||||
broadsword,
|
||||
armorLookup,
|
||||
"slash",
|
||||
new (string Label, int MinRoll, int? MaxRoll)[]
|
||||
{
|
||||
("01-25", 1, 25),
|
||||
("26-50", 26, 50),
|
||||
("51-75", 51, 75),
|
||||
("76-100", 76, 100),
|
||||
("101-125", 101, 125),
|
||||
("126+", 126, null)
|
||||
},
|
||||
new Dictionary<string, (int Hits, string? Severity)[]>
|
||||
{
|
||||
["AT1"] = [(0, null), (4, "A"), (10, "B"), (18, "C"), (26, "D"), (34, "E")],
|
||||
["AT5"] = [(0, null), (2, null), (8, "A"), (14, "B"), (22, "C"), (30, "D")],
|
||||
["AT10"] = [(0, null), (0, null), (4, null), (10, "A"), (18, "B"), (26, "C")],
|
||||
["AT15"] = [(0, null), (0, null), (2, null), (6, null), (12, "A"), (20, "B")],
|
||||
["AT20"] = [(0, null), (0, null), (0, null), (4, null), (8, null), (16, "A")]
|
||||
});
|
||||
|
||||
AddAttackData(
|
||||
shortBow,
|
||||
armorLookup,
|
||||
"puncture",
|
||||
new (string Label, int MinRoll, int? MaxRoll)[]
|
||||
{
|
||||
("01-25", 1, 25),
|
||||
("26-50", 26, 50),
|
||||
("51-75", 51, 75),
|
||||
("76-100", 76, 100),
|
||||
("101-125", 101, 125),
|
||||
("126+", 126, null)
|
||||
},
|
||||
new Dictionary<string, (int Hits, string? Severity)[]>
|
||||
{
|
||||
["AT1"] = [(0, null), (3, "A"), (8, "B"), (12, "C"), (18, "D"), (24, "E")],
|
||||
["AT5"] = [(0, null), (1, null), (5, "A"), (9, "B"), (14, "C"), (20, "D")],
|
||||
["AT10"] = [(0, null), (0, null), (2, null), (6, "A"), (10, "B"), (15, "C")],
|
||||
["AT15"] = [(0, null), (0, null), (1, null), (4, "A"), (8, "B"), (12, "C")],
|
||||
["AT20"] = [(0, null), (0, null), (0, null), (2, null), (5, "A"), (9, "B")]
|
||||
});
|
||||
|
||||
return [broadsword, shortBow];
|
||||
}
|
||||
|
||||
private static void AddAttackData(
|
||||
AttackTable table,
|
||||
IReadOnlyDictionary<string, ArmorType> armorLookup,
|
||||
string criticalType,
|
||||
IReadOnlyList<(string Label, int MinRoll, int? MaxRoll)> rollBands,
|
||||
IReadOnlyDictionary<string, (int Hits, string? Severity)[]> matrix)
|
||||
{
|
||||
table.RollBands = rollBands
|
||||
.Select((band, index) => new AttackRollBand
|
||||
{
|
||||
Label = band.Label,
|
||||
MinRoll = band.MinRoll,
|
||||
MaxRoll = band.MaxRoll,
|
||||
SortOrder = index + 1
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var rollBandLookup = table.RollBands.ToDictionary(item => item.SortOrder);
|
||||
|
||||
foreach (var (armorCode, results) in matrix)
|
||||
{
|
||||
for (var index = 0; index < results.Length; index++)
|
||||
{
|
||||
var result = results[index];
|
||||
table.Results.Add(new AttackResult
|
||||
{
|
||||
ArmorType = armorLookup[armorCode],
|
||||
AttackRollBand = rollBandLookup[index + 1],
|
||||
Hits = result.Hits,
|
||||
CriticalType = result.Severity is null ? null : criticalType,
|
||||
CriticalSeverity = result.Severity,
|
||||
RawNotation = BuildAttackNotation(result.Hits, result.Severity, criticalType),
|
||||
Notes = result.Severity is null
|
||||
? "No critical triggered from the seeded starter data."
|
||||
: "Critical can be resolved automatically if a critical roll is supplied."
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<CriticalTable> CreateCriticalTables()
|
||||
{
|
||||
return
|
||||
[
|
||||
CreateCriticalTable(
|
||||
slug: "slash",
|
||||
displayName: "Slash Critical",
|
||||
notes: "Starter subset derived from the critical-table schema design notes.",
|
||||
matrix: new Dictionary<string, (string Description, string Affix)[]>
|
||||
{
|
||||
["A"] =
|
||||
[
|
||||
("Glancing slash across the forearm.", "+2 hits, 1 bleed"),
|
||||
("Rib-level cut knocks the foe back a step.", "+4 hits, 1 stun, 2 bleed"),
|
||||
("Deep cut to the thigh unbalances the target.", "+6 hits, 2 stun, 3 bleed"),
|
||||
("Shoulder opened; guard collapses.", "+8 hits, 3 stun, 4 bleed"),
|
||||
("Neck line opened. The fight is over.", "Instant death")
|
||||
],
|
||||
["B"] =
|
||||
[
|
||||
("Slash cuts through the hand and weapon grip wavers.", "+4 hits, 1 stun, 2 bleed"),
|
||||
("Diagonal cut across the chest steals breath.", "+6 hits, 2 stun, 3 bleed"),
|
||||
("Hip strike drives the foe to one knee.", "+8 hits, 3 stun, 4 bleed"),
|
||||
("Sword arm carved deeply; weapon drops.", "+10 hits, 4 stun, 5 bleed"),
|
||||
("Throat opened in a full red arc.", "Instant death")
|
||||
],
|
||||
["C"] =
|
||||
[
|
||||
("Forearm split to the bone; parry ruined.", "+6 hits, 2 stun, 3 bleed"),
|
||||
("Wide cut over the ribs spins the foe.", "+8 hits, 3 stun, 4 bleed"),
|
||||
("Hamstring sliced; prone unless magically supported.", "+10 hits, 4 stun, 6 bleed"),
|
||||
("Slash rips across face and collar. Vision swims.", "+12 hits, 5 stun, 7 bleed"),
|
||||
("Head nearly severed.", "Instant death")
|
||||
],
|
||||
["D"] =
|
||||
[
|
||||
("Deep abdominal slice spills momentum and blood together.", "+8 hits, 3 stun, 5 bleed"),
|
||||
("Sword tracks from shoulder to sternum. Collapse likely.", "+10 hits, 4 stun, 6 bleed"),
|
||||
("Leg cut through muscle; foe falls hard.", "+12 hits, 5 stun, 8 bleed"),
|
||||
("Upper arm nearly removed; weapon arm useless.", "+14 hits, 6 stun, 10 bleed"),
|
||||
("Killing cut through the neck.", "Instant death")
|
||||
],
|
||||
["E"] =
|
||||
[
|
||||
("Massive slash opens chest and the foe staggers blindly.", "+10 hits, 4 stun, 6 bleed"),
|
||||
("Spine-grazing cut destroys posture and control.", "+12 hits, 5 stun, 8 bleed"),
|
||||
("Leg severed below the knee.", "+14 hits, 6 stun, 10 bleed"),
|
||||
("Torso opened from clavicle to hip.", "+16 hits, 8 stun, 12 bleed"),
|
||||
("Clean decapitation.", "Instant death")
|
||||
]
|
||||
}),
|
||||
CreateCriticalTable(
|
||||
slug: "puncture",
|
||||
displayName: "Puncture Critical",
|
||||
notes: "Starter subset seeded for missile and thrust attacks.",
|
||||
matrix: new Dictionary<string, (string Description, string Affix)[]>
|
||||
{
|
||||
["A"] =
|
||||
[
|
||||
("Point slips into the shoulder.", "+2 hits, 1 bleed"),
|
||||
("Stab bites into the upper arm.", "+4 hits, 1 stun, 2 bleed"),
|
||||
("Short thrust catches the ribs.", "+5 hits, 2 stun, 2 bleed"),
|
||||
("Point pierces the thigh. Movement slows.", "+7 hits, 2 stun, 3 bleed"),
|
||||
("Exact thrust to the throat.", "Instant death")
|
||||
],
|
||||
["B"] =
|
||||
[
|
||||
("Puncture through the palm forces a drop check.", "+4 hits, 1 stun, 2 bleed"),
|
||||
("Arrow buries in the flank.", "+6 hits, 2 stun, 3 bleed"),
|
||||
("Deep thrust into the belly doubles the foe over.", "+8 hits, 3 stun, 4 bleed"),
|
||||
("Point through the shoulder locks the arm.", "+10 hits, 4 stun, 5 bleed"),
|
||||
("Eye socket struck cleanly.", "Instant death")
|
||||
],
|
||||
["C"] =
|
||||
[
|
||||
("Stab through the bicep ruins the next parry.", "+6 hits, 2 stun, 3 bleed"),
|
||||
("Arrow punches through lung tissue.", "+8 hits, 3 stun, 5 bleed"),
|
||||
("Thrust under the ribs steals breath and footing.", "+10 hits, 4 stun, 6 bleed"),
|
||||
("Deep puncture pins the foe in place.", "+12 hits, 5 stun, 7 bleed"),
|
||||
("Heart strike.", "Instant death")
|
||||
],
|
||||
["D"] =
|
||||
[
|
||||
("Weapon point tears through the abdomen.", "+8 hits, 3 stun, 5 bleed"),
|
||||
("Arrow lodges near the spine; movement nearly gone.", "+10 hits, 4 stun, 6 bleed"),
|
||||
("Lance-like thrust through the torso.", "+12 hits, 5 stun, 8 bleed"),
|
||||
("Point enters the neck and exits behind the shoulder.", "+14 hits, 6 stun, 10 bleed"),
|
||||
("Brain pierced.", "Instant death")
|
||||
],
|
||||
["E"] =
|
||||
[
|
||||
("Massive puncture opens the chest cavity.", "+10 hits, 4 stun, 6 bleed"),
|
||||
("Shaft buries to the fletching; foe drops.", "+12 hits, 5 stun, 8 bleed"),
|
||||
("Groin-to-spine thrust destroys the body line.", "+14 hits, 6 stun, 10 bleed"),
|
||||
("Perfect impalement through the throat and neck.", "+16 hits, 8 stun, 12 bleed"),
|
||||
("Instantly fatal puncture.", "Instant death")
|
||||
]
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
private static CriticalTable CreateCriticalTable(
|
||||
string slug,
|
||||
string displayName,
|
||||
string notes,
|
||||
IReadOnlyDictionary<string, (string Description, string Affix)[]> matrix)
|
||||
{
|
||||
var table = new CriticalTable
|
||||
{
|
||||
Slug = slug,
|
||||
DisplayName = displayName,
|
||||
Family = "standard",
|
||||
SourceDocument = "Seeded starter data",
|
||||
Notes = notes
|
||||
};
|
||||
|
||||
table.Columns =
|
||||
[
|
||||
new CriticalColumn { ColumnKey = "A", Label = "A", SortOrder = 1 },
|
||||
new CriticalColumn { ColumnKey = "B", Label = "B", SortOrder = 2 },
|
||||
new CriticalColumn { ColumnKey = "C", Label = "C", SortOrder = 3 },
|
||||
new CriticalColumn { ColumnKey = "D", Label = "D", SortOrder = 4 },
|
||||
new CriticalColumn { ColumnKey = "E", Label = "E", SortOrder = 5 }
|
||||
];
|
||||
|
||||
table.RollBands =
|
||||
[
|
||||
new CriticalRollBand { Label = "01-20", MinRoll = 1, MaxRoll = 20, SortOrder = 1 },
|
||||
new CriticalRollBand { Label = "21-40", MinRoll = 21, MaxRoll = 40, SortOrder = 2 },
|
||||
new CriticalRollBand { Label = "41-60", MinRoll = 41, MaxRoll = 60, SortOrder = 3 },
|
||||
new CriticalRollBand { Label = "61-80", MinRoll = 61, MaxRoll = 80, SortOrder = 4 },
|
||||
new CriticalRollBand { Label = "81-100", MinRoll = 81, MaxRoll = 100, SortOrder = 5 }
|
||||
];
|
||||
|
||||
var columnsByKey = table.Columns.ToDictionary(item => item.ColumnKey, StringComparer.OrdinalIgnoreCase);
|
||||
var bandsByOrder = table.RollBands.ToDictionary(item => item.SortOrder);
|
||||
|
||||
foreach (var (columnKey, rows) in matrix)
|
||||
{
|
||||
for (var index = 0; index < rows.Length; index++)
|
||||
{
|
||||
var row = rows[index];
|
||||
table.Results.Add(new CriticalResult
|
||||
{
|
||||
CriticalColumn = columnsByKey[columnKey],
|
||||
CriticalRollBand = bandsByOrder[index + 1],
|
||||
DescriptionText = row.Description,
|
||||
RawAffixText = row.Affix,
|
||||
RawCellText = $"{row.Description} {row.Affix}",
|
||||
ParsedJson = "{}",
|
||||
ParseStatus = "verified"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
private static string BuildAttackNotation(int hits, string? severity, string criticalType)
|
||||
{
|
||||
return severity is null
|
||||
? $"{hits} hits"
|
||||
: $"{hits} hits, {severity.ToUpperInvariant()} {ToTitleCase(criticalType)} critical";
|
||||
}
|
||||
|
||||
private static string ToTitleCase(string value) =>
|
||||
string.Join(' ', value.Split('_', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(segment => char.ToUpperInvariant(segment[0]) + segment[1..]));
|
||||
}
|
||||
10
src/RolemasterDb.App/Domain/ArmorType.cs
Normal file
10
src/RolemasterDb.App/Domain/ArmorType.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace RolemasterDb.App.Domain;
|
||||
|
||||
public sealed class ArmorType
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public List<AttackResult> AttackResults { get; set; } = [];
|
||||
}
|
||||
17
src/RolemasterDb.App/Domain/AttackResult.cs
Normal file
17
src/RolemasterDb.App/Domain/AttackResult.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace RolemasterDb.App.Domain;
|
||||
|
||||
public sealed class AttackResult
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int AttackTableId { get; set; }
|
||||
public int ArmorTypeId { get; set; }
|
||||
public int AttackRollBandId { get; set; }
|
||||
public int Hits { get; set; }
|
||||
public string? CriticalType { get; set; }
|
||||
public string? CriticalSeverity { get; set; }
|
||||
public string RawNotation { get; set; } = string.Empty;
|
||||
public string? Notes { get; set; }
|
||||
public AttackTable AttackTable { get; set; } = null!;
|
||||
public ArmorType ArmorType { get; set; } = null!;
|
||||
public AttackRollBand AttackRollBand { get; set; } = null!;
|
||||
}
|
||||
13
src/RolemasterDb.App/Domain/AttackRollBand.cs
Normal file
13
src/RolemasterDb.App/Domain/AttackRollBand.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace RolemasterDb.App.Domain;
|
||||
|
||||
public sealed class AttackRollBand
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int AttackTableId { get; set; }
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public int MinRoll { get; set; }
|
||||
public int? MaxRoll { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public AttackTable AttackTable { get; set; } = null!;
|
||||
public List<AttackResult> Results { get; set; } = [];
|
||||
}
|
||||
12
src/RolemasterDb.App/Domain/AttackTable.cs
Normal file
12
src/RolemasterDb.App/Domain/AttackTable.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace RolemasterDb.App.Domain;
|
||||
|
||||
public sealed class AttackTable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string AttackKind { get; set; } = string.Empty;
|
||||
public string? Notes { get; set; }
|
||||
public List<AttackRollBand> RollBands { get; set; } = [];
|
||||
public List<AttackResult> Results { get; set; } = [];
|
||||
}
|
||||
13
src/RolemasterDb.App/Domain/CriticalColumn.cs
Normal file
13
src/RolemasterDb.App/Domain/CriticalColumn.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace RolemasterDb.App.Domain;
|
||||
|
||||
public sealed class CriticalColumn
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int CriticalTableId { get; set; }
|
||||
public string ColumnKey { get; set; } = string.Empty;
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public string Role { get; set; } = "severity";
|
||||
public int SortOrder { get; set; }
|
||||
public CriticalTable CriticalTable { get; set; } = null!;
|
||||
public List<CriticalResult> Results { get; set; } = [];
|
||||
}
|
||||
12
src/RolemasterDb.App/Domain/CriticalGroup.cs
Normal file
12
src/RolemasterDb.App/Domain/CriticalGroup.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace RolemasterDb.App.Domain;
|
||||
|
||||
public sealed class CriticalGroup
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int CriticalTableId { get; set; }
|
||||
public string GroupKey { get; set; } = string.Empty;
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public CriticalTable CriticalTable { get; set; } = null!;
|
||||
public List<CriticalResult> Results { get; set; } = [];
|
||||
}
|
||||
19
src/RolemasterDb.App/Domain/CriticalResult.cs
Normal file
19
src/RolemasterDb.App/Domain/CriticalResult.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace RolemasterDb.App.Domain;
|
||||
|
||||
public sealed class CriticalResult
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int CriticalTableId { get; set; }
|
||||
public int? CriticalGroupId { get; set; }
|
||||
public int CriticalColumnId { get; set; }
|
||||
public int CriticalRollBandId { get; set; }
|
||||
public string RawCellText { get; set; } = string.Empty;
|
||||
public string DescriptionText { get; set; } = string.Empty;
|
||||
public string? RawAffixText { get; set; }
|
||||
public string ParsedJson { get; set; } = "{}";
|
||||
public string ParseStatus { get; set; } = "verified";
|
||||
public CriticalTable CriticalTable { get; set; } = null!;
|
||||
public CriticalGroup? CriticalGroup { get; set; }
|
||||
public CriticalColumn CriticalColumn { get; set; } = null!;
|
||||
public CriticalRollBand CriticalRollBand { get; set; } = null!;
|
||||
}
|
||||
13
src/RolemasterDb.App/Domain/CriticalRollBand.cs
Normal file
13
src/RolemasterDb.App/Domain/CriticalRollBand.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace RolemasterDb.App.Domain;
|
||||
|
||||
public sealed class CriticalRollBand
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int CriticalTableId { get; set; }
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public int MinRoll { get; set; }
|
||||
public int? MaxRoll { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public CriticalTable CriticalTable { get; set; } = null!;
|
||||
public List<CriticalResult> Results { get; set; } = [];
|
||||
}
|
||||
15
src/RolemasterDb.App/Domain/CriticalTable.cs
Normal file
15
src/RolemasterDb.App/Domain/CriticalTable.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace RolemasterDb.App.Domain;
|
||||
|
||||
public sealed class CriticalTable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string Family { get; set; } = "standard";
|
||||
public string SourceDocument { get; set; } = string.Empty;
|
||||
public string? Notes { get; set; }
|
||||
public List<CriticalGroup> Groups { get; set; } = [];
|
||||
public List<CriticalColumn> Columns { get; set; } = [];
|
||||
public List<CriticalRollBand> RollBands { get; set; } = [];
|
||||
public List<CriticalResult> Results { get; set; } = [];
|
||||
}
|
||||
50
src/RolemasterDb.App/Features/LookupContracts.cs
Normal file
50
src/RolemasterDb.App/Features/LookupContracts.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
namespace RolemasterDb.App.Features;
|
||||
|
||||
public sealed record LookupOption(string Key, string Label);
|
||||
|
||||
public sealed record CriticalTableReference(
|
||||
string Key,
|
||||
string Label,
|
||||
IReadOnlyList<LookupOption> Columns,
|
||||
IReadOnlyList<LookupOption> Groups);
|
||||
|
||||
public sealed record LookupReferenceData(
|
||||
IReadOnlyList<LookupOption> AttackTables,
|
||||
IReadOnlyList<LookupOption> ArmorTypes,
|
||||
IReadOnlyList<CriticalTableReference> CriticalTables);
|
||||
|
||||
public sealed record AttackLookupRequest(
|
||||
string AttackTable,
|
||||
string ArmorType,
|
||||
int Roll,
|
||||
int? CriticalRoll);
|
||||
|
||||
public sealed record CriticalLookupRequest(
|
||||
string CriticalType,
|
||||
string Column,
|
||||
int Roll,
|
||||
string? Group);
|
||||
|
||||
public sealed record CriticalLookupResponse(
|
||||
string CriticalType,
|
||||
string CriticalTableName,
|
||||
string? Group,
|
||||
string Column,
|
||||
int Roll,
|
||||
string RollBand,
|
||||
string Description,
|
||||
string? AffixText);
|
||||
|
||||
public sealed record AttackLookupResponse(
|
||||
string AttackTable,
|
||||
string AttackTableName,
|
||||
string ArmorType,
|
||||
string ArmorTypeLabel,
|
||||
int Roll,
|
||||
string RollBand,
|
||||
int Hits,
|
||||
string? CriticalType,
|
||||
string? CriticalSeverity,
|
||||
string RawNotation,
|
||||
string? Notes,
|
||||
CriticalLookupResponse? AutoCritical);
|
||||
116
src/RolemasterDb.App/Features/LookupService.cs
Normal file
116
src/RolemasterDb.App/Features/LookupService.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RolemasterDb.App.Data;
|
||||
|
||||
namespace RolemasterDb.App.Features;
|
||||
|
||||
public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbContextFactory)
|
||||
{
|
||||
public async Task<LookupReferenceData> GetReferenceDataAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var attackTables = await dbContext.AttackTables
|
||||
.AsNoTracking()
|
||||
.OrderBy(item => item.DisplayName)
|
||||
.Select(item => new LookupOption(item.Slug, item.DisplayName))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var armorTypes = await dbContext.ArmorTypes
|
||||
.AsNoTracking()
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.Select(item => new LookupOption(item.Code, item.Label))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var criticalTables = await dbContext.CriticalTables
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Include(item => item.Columns)
|
||||
.Include(item => item.Groups)
|
||||
.OrderBy(item => item.DisplayName)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new LookupReferenceData(
|
||||
attackTables,
|
||||
armorTypes,
|
||||
criticalTables.Select(item => new CriticalTableReference(
|
||||
item.Slug,
|
||||
item.DisplayName,
|
||||
item.Columns.OrderBy(column => column.SortOrder).Select(column => new LookupOption(column.ColumnKey, column.Label)).ToList(),
|
||||
item.Groups.OrderBy(group => group.SortOrder).Select(group => new LookupOption(group.GroupKey, group.Label)).ToList()))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
public async Task<AttackLookupResponse?> LookupAttackAsync(AttackLookupRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var attackTable = NormalizeSlug(request.AttackTable);
|
||||
var armorType = request.ArmorType.Trim().ToUpperInvariant();
|
||||
|
||||
var attackResult = await dbContext.AttackResults
|
||||
.AsNoTracking()
|
||||
.Where(item =>
|
||||
item.AttackTable.Slug == attackTable &&
|
||||
item.ArmorType.Code == armorType &&
|
||||
request.Roll >= item.AttackRollBand.MinRoll &&
|
||||
(item.AttackRollBand.MaxRoll == null || request.Roll <= item.AttackRollBand.MaxRoll))
|
||||
.Select(item => new AttackLookupResponse(
|
||||
item.AttackTable.Slug,
|
||||
item.AttackTable.DisplayName,
|
||||
item.ArmorType.Code,
|
||||
item.ArmorType.Label,
|
||||
request.Roll,
|
||||
item.AttackRollBand.Label,
|
||||
item.Hits,
|
||||
item.CriticalType,
|
||||
item.CriticalSeverity,
|
||||
item.RawNotation,
|
||||
item.Notes,
|
||||
null))
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (attackResult is null || attackResult.CriticalType is null || attackResult.CriticalSeverity is null || request.CriticalRoll is null)
|
||||
{
|
||||
return attackResult;
|
||||
}
|
||||
|
||||
var autoCritical = await LookupCriticalAsync(
|
||||
new CriticalLookupRequest(attackResult.CriticalType, attackResult.CriticalSeverity, request.CriticalRoll.Value, null),
|
||||
cancellationToken);
|
||||
|
||||
return attackResult with { AutoCritical = autoCritical };
|
||||
}
|
||||
|
||||
public async Task<CriticalLookupResponse?> LookupCriticalAsync(CriticalLookupRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var criticalType = NormalizeSlug(request.CriticalType);
|
||||
var column = request.Column.Trim().ToUpperInvariant();
|
||||
var group = string.IsNullOrWhiteSpace(request.Group) ? null : NormalizeSlug(request.Group);
|
||||
|
||||
return await dbContext.CriticalResults
|
||||
.AsNoTracking()
|
||||
.Where(item =>
|
||||
item.CriticalTable.Slug == criticalType &&
|
||||
item.CriticalColumn.ColumnKey == column &&
|
||||
(group == null
|
||||
? item.CriticalGroupId == null
|
||||
: item.CriticalGroup != null && item.CriticalGroup.GroupKey == group) &&
|
||||
request.Roll >= item.CriticalRollBand.MinRoll &&
|
||||
(item.CriticalRollBand.MaxRoll == null || request.Roll <= item.CriticalRollBand.MaxRoll))
|
||||
.Select(item => new CriticalLookupResponse(
|
||||
item.CriticalTable.Slug,
|
||||
item.CriticalTable.DisplayName,
|
||||
item.CriticalGroup != null ? item.CriticalGroup.GroupKey : null,
|
||||
item.CriticalColumn.ColumnKey,
|
||||
request.Roll,
|
||||
item.CriticalRollBand.Label,
|
||||
item.DescriptionText,
|
||||
item.RawAffixText))
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static string NormalizeSlug(string value) =>
|
||||
value.Trim().Replace(' ', '_').ToLowerInvariant();
|
||||
}
|
||||
41
src/RolemasterDb.App/Program.cs
Normal file
41
src/RolemasterDb.App/Program.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RolemasterDb.App.Components;
|
||||
using RolemasterDb.App.Data;
|
||||
using RolemasterDb.App.Features;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var connectionString = builder.Configuration.GetConnectionString("RolemasterDb") ?? "Data Source=rolemaster.db";
|
||||
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
builder.Services.AddDbContextFactory<RolemasterDbContext>(options => options.UseSqlite(connectionString));
|
||||
builder.Services.AddScoped<LookupService>();
|
||||
|
||||
var app = builder.Build();
|
||||
await RolemasterDbInitializer.InitializeAsync(app.Services);
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
}
|
||||
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.MapStaticAssets();
|
||||
var api = app.MapGroup("/api");
|
||||
api.MapGet("/reference-data", async (LookupService lookupService, CancellationToken cancellationToken) =>
|
||||
Results.Ok(await lookupService.GetReferenceDataAsync(cancellationToken)));
|
||||
api.MapPost("/lookup/attack", async (AttackLookupRequest request, LookupService lookupService, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await lookupService.LookupAttackAsync(request, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
});
|
||||
api.MapPost("/lookup/critical", async (CriticalLookupRequest request, LookupService lookupService, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await lookupService.LookupCriticalAsync(request, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
});
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.Run();
|
||||
14
src/RolemasterDb.App/Properties/launchSettings.json
Normal file
14
src/RolemasterDb.App/Properties/launchSettings.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5184",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/RolemasterDb.App/RolemasterDb.App.csproj
Normal file
19
src/RolemasterDb.App/RolemasterDb.App.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
8
src/RolemasterDb.App/appsettings.Development.json
Normal file
8
src/RolemasterDb.App/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/RolemasterDb.App/appsettings.json
Normal file
12
src/RolemasterDb.App/appsettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"RolemasterDb": "Data Source=rolemaster.db"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
BIN
src/RolemasterDb.App/rolemaster.db
Normal file
BIN
src/RolemasterDb.App/rolemaster.db
Normal file
Binary file not shown.
259
src/RolemasterDb.App/wwwroot/app.css
Normal file
259
src/RolemasterDb.App/wwwroot/app.css
Normal file
@@ -0,0 +1,259 @@
|
||||
:root {
|
||||
--paper: #f7f1e4;
|
||||
--paper-strong: #fff9ee;
|
||||
--ink: #261a14;
|
||||
--ink-soft: #5a4c41;
|
||||
--accent: #8f5a2f;
|
||||
--accent-strong: #b8793b;
|
||||
--panel: rgba(255, 250, 240, 0.82);
|
||||
--line: rgba(111, 87, 59, 0.18);
|
||||
--shadow: 0 18px 40px rgba(41, 22, 11, 0.12);
|
||||
}
|
||||
|
||||
html, body {
|
||||
min-height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(240, 223, 185, 0.55), transparent 28%),
|
||||
linear-gradient(180deg, #eadbc0 0%, #f4ecda 38%, #e2d3b7 100%);
|
||||
color: var(--ink);
|
||||
font-family: Georgia, "Palatino Linotype", serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #6e4320;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: clamp(2.2rem, 4vw, 3.5rem);
|
||||
line-height: 0.95;
|
||||
margin: 0;
|
||||
font-family: Cambria, Georgia, serif;
|
||||
}
|
||||
|
||||
.lede {
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
gap: 1.25rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
}
|
||||
|
||||
.hero-panel,
|
||||
.panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 24px;
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
padding: 1.6rem;
|
||||
margin-bottom: 1.25rem;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 251, 243, 0.94), rgba(244, 232, 203, 0.94)),
|
||||
var(--panel);
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 1.35rem;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.panel-copy,
|
||||
.muted {
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.lookup-form {
|
||||
display: grid;
|
||||
gap: 0.95rem;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
gap: 0.95rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.field-shell {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.field-shell label {
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #75562f;
|
||||
}
|
||||
|
||||
.input-shell {
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(127, 96, 55, 0.2);
|
||||
background: rgba(255, 252, 247, 0.92);
|
||||
padding: 0.8rem 0.9rem;
|
||||
color: var(--ink);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-shell:focus {
|
||||
outline: 2px solid rgba(184, 121, 59, 0.35);
|
||||
border-color: rgba(184, 121, 59, 0.45);
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-ritual {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 0.8rem 1.15rem;
|
||||
background: linear-gradient(135deg, var(--accent-strong), var(--accent));
|
||||
color: #fff8ef;
|
||||
letter-spacing: 0.04em;
|
||||
box-shadow: 0 10px 18px rgba(143, 90, 47, 0.2);
|
||||
}
|
||||
|
||||
.btn-ritual:hover {
|
||||
background: linear-gradient(135deg, #c38a4d, #8f5a2f);
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(143, 90, 47, 0.18);
|
||||
padding: 0.4rem 0.7rem;
|
||||
background: rgba(255, 250, 242, 0.84);
|
||||
color: #5d4429;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.result-shell {
|
||||
margin-top: 1rem;
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
border-radius: 20px;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid rgba(127, 96, 55, 0.14);
|
||||
}
|
||||
|
||||
.result-card h3,
|
||||
.result-card h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
.result-stats {
|
||||
display: flex;
|
||||
gap: 0.7rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.stat-pill {
|
||||
border-radius: 999px;
|
||||
padding: 0.35rem 0.65rem;
|
||||
background: rgba(238, 223, 193, 0.65);
|
||||
color: #5b4327;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.callout {
|
||||
margin-top: 0.9rem;
|
||||
padding: 0.85rem 0.95rem;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 247, 230, 0.76);
|
||||
border: 1px solid rgba(184, 121, 59, 0.18);
|
||||
color: #5b4327;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #8d2b1e;
|
||||
}
|
||||
|
||||
.table-list {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.table-list-item {
|
||||
border-radius: 16px;
|
||||
padding: 0.9rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.54);
|
||||
border: 1px solid rgba(127, 96, 55, 0.12);
|
||||
}
|
||||
|
||||
.table-list-item strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
border-radius: 16px;
|
||||
background: #2a1d17;
|
||||
color: #f9ecd2;
|
||||
overflow-x: auto;
|
||||
font-family: Consolas, "Courier New", monospace;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.api-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: #7e2c22;
|
||||
color: #fff7ee;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.content-shell {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.hero-panel,
|
||||
.panel {
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
BIN
src/RolemasterDb.App/wwwroot/favicon.png
Normal file
BIN
src/RolemasterDb.App/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
4085
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
4085
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4084
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
4084
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
597
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
597
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
@@ -0,0 +1,597 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
vendored
Normal file
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
594
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
594
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
@@ -0,0 +1,594 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5402
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
5402
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
vendored
Normal file
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5393
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
vendored
Normal file
5393
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
12057
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.css
vendored
Normal file
12057
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
vendored
Normal file
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
12030
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
vendored
Normal file
12030
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
vendored
Normal file
6
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6314
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
vendored
Normal file
6314
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
vendored
Normal file
1
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
vendored
Normal file
7
src/RolemasterDb.App/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user