A Python API for fetching Wuthering Waves data from prydwen.gg and wuthering.gg, built with Playwright.
Activate virtual environment
python3 -m venv .env
source .env/bin/activate
pip install playwright playwright-stealth
playwright install
git clone https://github.com/theonuverse/sol3data.gitOr optionally if you use Termux, you can use this Dockerfile together with PRoot Distro to automatically set the environment up.
FROM debian:13-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN \
apt-get update && apt-get upgrade -y && \
apt-get install python3 python3-pip git -y && \
pip install playwright playwright-stealth --break-system-packages && \
playwright install-deps && \
playwright install chromium
WORKDIR /root
RUN git clone https://github.com/theonuverse/sol3data.git
CMD ["/bin/bash"]Then build it, install it and run it.
pd build -t sol3data:latest --install-as sol3data --no-cache .
pd sh sol3datafrom sol3data import Characters
# Explicit lifecycle: no context manager is required.
chars = Characters()
char = chars.get("iuno")
# fetch character details
print(char.name)
print(char.introduction)
print(char.updates.review)
print(char.updates.build_calculations)
print(char.updates.profile)
print(char.role.all) # dict of role(s), e.g. {1: "Main DPS"}
# The rest of the API is organized by page section:
skill = char.kit.skills.active.basic_attack
print(skill.name, skill.description)
print(char.review.pros.all)
print(char.build.weapon_recommendations.all.name)
print(char.gameplay.rotations)
print(char.gameplay.synergies)
mode = char.calculations.get("Default")
print(mode.damage_profile.raw_values)
print(mode.damage_profile.percentages)
print(char.snapshot_json())
# Always close explicitly in scripts.
chars.close()character.updates mirrors the source page's Update Tracker:
print(character.updates.review) # Last review update
print(character.updates.build_calculations) # Last major build/calcs update
print(character.updates.profile) # Last profile update
print(character.updates.last_updated) # Page's overall last-updated dateThe context-manager form is still useful when cleanup must happen even if scraping raises an exception:
from sol3data import Characters
with Characters() as chars:
print(chars.get("iuno").name)Characters inherits from the public BrowserSession, so its lifecycle options
are available directly:
from sol3data import Characters
chars = Characters(
headless=False, # open a visible browser window
slow_mo=250, # slow Playwright actions by 250 ms
pause_on_open=True, # open Playwright Inspector and wait for Resume
trace_path="artifacts/trace.zip",
)
print(chars.get("iuno").name)
chars.close()Gameplay content follows the same lazy property style as the other character sections:
gameplay = character.gameplay
print(gameplay.how_to_play)
print(gameplay.echo_timing)
print(gameplay.rotations) # clean text by named rotation
print(gameplay.rotation_guides) # steps and notes for each rotation
print(gameplay.additional_tips)
print(gameplay.synergies.dps) # main DPS recommendations, role-aware
print(gameplay.synergies.sub_dps) # sub-DPS / secondary damage dealers
print(gameplay.synergies.supports) # healers and supports
print(gameplay.team_compositions) # example teams keyed from 1Characters without a particular guide section return an empty string or mapping.
The calculation API has one canonical access rule: named data uses .get(name)
and sequence data uses .get(sequence). Discovery uses .names; iteration uses
the collection itself. The old duplicate aliases were removed so the same value
does not have multiple competing access paths.
All public collection .get(...) methods have concrete generic return types, so
editor and REPL completion continues through chained calls:
character.calculations.modes.get("Electro Flare").sequence_damage.get(0).damage
character.calculations.modes.get("Electro Flare").calculation_information.get("Rover (Electro)").weaponCalculation modes expose their visible names for discovery:
print(character.calculations.mode_names)
for mode in character.calculations:
print(mode.name)
print(character.calculations.additional_information)
for teammate in mode.calculation_information:
print(teammate.character, teammate.weapon, teammate.echo_set, teammate.main_echo)
print(mode.rotation_time)
print(mode.team_build.all)
print(mode.damage_profile.raw_values)
print(mode.damage_profile.percentages)
for scenario in mode.scenarios.values():
print(scenario.name)
print(scenario.rotation_time)
print(scenario.get(0))
print(scenario.get(6))For characters with only one calculation mode, use
character.calculations.mode directly. For multi-mode characters, use
character.calculations.modes.get("Unison") or
character.calculations.get("Electro Flare"). Both forms use the same
case-insensitive string lookup.
Use .get(...) for individual entries rather than indexing:
mode = character.calculations.modes.get("Unison")
scenario = mode.get(1)
heavy_raw = scenario.damage_profile.get("Heavy")
heavy_share = scenario.damage_profile.get_percentage("Heavy")
build_entry = mode.team_build.get(1)The damage profile exposes only attack types that contribute a non-zero value in the site's profile, together with each one's raw total and percentage of the profile total:
profile = mode.damage_profile
print(profile.raw_values) # {"Basic": "69,828", "Heavy": "2,052,386", ...}
print(profile.percentages) # {"Basic": "2.61%", "Heavy": "76.52%", ...}
print(profile.total)The percentages answer questions such as “what share of this rotation is Heavy damage?” They are calculated from the displayed table totals, not from the character's in-game ATK stat. The visual pie chart itself is not reimplemented, since its table is the stable machine-readable representation.
pause_on_open=True uses Playwright's Inspector. It opens a visible browser
and an inspector window, pauses after navigation, and waits for you to press
Resume. This is useful for watching selectors and manually inspecting the
page. It requires headless=False; it is separate from devtools=True, which
opens Chromium DevTools without pausing execution.
Characters— character pagesname,introduction,rolekit— Kit tabskills— active / passive / concerto skills with multipliersresonance_chain— all 6 sequence nodesupgrade_materials— material requirements (ascension, skill upgrades, etc.)
review— Review tabpros&consfull_reviewratings— tier list and value tier list, plusavailable_rolesto check which roles have ratings
build— Build tabweapon_recommendations— recommended weapons, usage %, and write-upsecho_recommendations— recommended echo sets, rank, and write-upsecho_stats— recommended main stats per cost slot, plus substat priorityendgame_stats— recommended endgame stat targetsskill_priority— skill priority order per role
gameplay— Gameplay and Teams tab- normalized how-to-play text, Echo timing, rotation guides, and tips
- role-aware
dps,sub_dps, andsupportssynergy groups - example team compositions
calculations— Calculations tab- named calculation mode discovery and lookup
- team build entries, scenarios, raw damage profiles, and percentages
- S0-S6 sequence damage access through
.get(...)or named properties
snapshot()/snapshot_json()— complete JSON-safe character data for local AI agents, with requested sections and source metadata- shared normalization for names, slugs, mode lookup, and role names
- session page caching and explicit cache invalidation with
characters.clear_cache()
- Weapons
- Echoes
- Teams
- probably more idk
Charactersstarts lazily on the first request. Use.close()in scripts, or awithblock for automatic cleanup.- One
Characterssession reuses its browser and cached character pages. Run all reads inside one session and close it once at the end; awithblock does this automatically. - Gameplay, teams, and calculations are loaded asynchronously by the source
site. The API waits for those panels to finish loading before returning
parsed data, including when they are requested through
snapshot(). - Calculation explanations and results are separated:
character.calculations.additional_informationis the site's global explanation of DMG/DPS simulations.mode.calculation_informationis the calculation buff team. Iterate it to inspect each character, weapon, Echo set, and main Echo.mode.rotation_timegives the primary rotation duration.mode.sequence_damage.get(0)through.get(6)returns S0-S6 damage, DPS, and relative sequence percentage.
mode = character.calculations.modes.get("Unison")
s0 = mode.sequence_damage.get(0)
print(s0.damage, s0.dps, s0.percentage)
print(mode.rotation_time)
for teammate in mode.calculation_information:
print(teammate.character, teammate.weapon, teammate.echo_set, teammate.main_echo)character.snapshot()returns a JSON-safe dictionary. Select sections withcharacter.snapshot(("identity", "gameplay", "calculations")), or usecharacter.snapshot_json()for direct model input.- Use
collection.find(...)when an item is optional and should returnNone; usecollection.get(...)when absence should raise a descriptive error. - Individual collection entries should be accessed through their standardized
.get(...)methods. For example, usecalculations.get("Unison"),mode.get(1), anddamage_profile.get("Heavy"). - Properties like
.allreturn dictionaries populated with elements instead of requiring loops. - Sections that don't exist for a given character (e.g. a role/rating a character doesn't have) raise
ValueErrorrather than returningNone— wrap intry/exceptwhen scraping many characters generically. - I've also added docstrings everywhere, so your IDE should give you nice hints while coding!