From 03ebc3a138071eab7c6962e49bafb2b8449da015 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 19:04:12 +0000 Subject: [PATCH] Lobby: MP3 dice sound with pitch per die, DM sound settings Default dice sound is public/dice.mp3 with a small random pitch and a base pitch per die size (d4 lighter ... d100 heavier). The DM can upload an own dice sound and sounds for critical success / failure per lobby, reset them, and switch result and critical sounds on or off. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K --- README.md | 5 +- backend/app/main.py | 6 ++- backend/app/sounds.py | 104 +++++++++++++++++++++++++++++++++++++++ frontend/public/dice.mp3 | Bin 0 -> 44544 bytes frontend/src/Lobbies.jsx | 66 ++++++++++++++++++++++--- frontend/src/sound.js | 103 +++++++++++++++++++------------------- frontend/src/styles.css | 4 ++ 7 files changed, 226 insertions(+), 62 deletions(-) create mode 100644 backend/app/sounds.py create mode 100644 frontend/public/dice.mp3 diff --git a/README.md b/README.md index 4c29744..f68ed3e 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,8 @@ The lobby page is built for desktop with three columns: the **character sheet** - **Skill and attribute checks**: players click an attribute or an unlocked skill on their own sheet. The server rolls a d100 (roll under): target = attribute or skill total + the DM's difficulty + the character's personal modifier. 01-05 is always a critical success, 96-100 always a critical failure. The chat and the table show the roll, the target and by how much it succeeded or failed. Combat is not handled specially: roll the normal skill and let the DM adjust with the modifiers. - **Difficulty** (bar under the characters): the DM picks a level from the rules table (Trivial +60 ... Hellish -60) or a custom number. It applies to all checks until changed. With **Hide numbers from players** the players only see their roll and success / failure, not the target, difficulty or margin. - **Dice** (below the table): d4 ... d100 with a count (1 to 10 dice). The server rolls, the dice tumble into the middle of the table in the colour of whoever threw them, and the result goes into the chat. With a person selected as recipient in the chat dropdown it is a private roll. **DM rolls** have three modes: *Mystery* (default: players see grey dice with "?" but not the result), *Hidden* (only the DM sees it) and *Public*. -- **Sound**: dice clatter when anything is rolled (yours, other players' and DM rolls, mystery rolls too), plus a short cue for the result of a check (rising chime for a critical success, soft ping for success, low tone for failure, dark double tone for a critical failure). The sounds are generated in the browser, there are no sound files. The speaker button next to the dice mutes them, the slider sets the volume; both are remembered per browser. Browsers only allow sound after the page was clicked once, and nothing plays while the tab is in the background. +- **Sound**: rolling plays `frontend/public/dice.mp3` (the default dice sound) with a slight random pitch; every die has its own base pitch, from a little higher and lighter (d4) to a little lower and heavier (d100). Many dice layer the sound up to three times. Checks add a short cue after the dice: built-in tones for success and failure, and for a critical success or failure either a built-in tone or a sound the DM uploaded. The speaker button next to the dice mutes everything for yourself, the slider sets the volume (remembered per browser). Browsers only allow sound after the page was clicked once, and nothing plays while the tab is in the background. +- **DM sound settings** (*Lobby actions* -> *Sounds*): replace the dice sound with an own MP3 (up to 1.5 MB, per lobby) or reset it to the default, upload sounds for *Critical success* and *Critical failure* (reset = built-in tone), and switch the critical sounds and the short success / failure tones on or off for the whole lobby. *Test* plays a sound locally. - **Own dice**: with this switch on, a click on a die or a skill asks for the result of your real dice instead of rolling. The server only checks the range and marks the roll as "own dice" in the chat. - **Colours**: every player has a different colour (lobby menu -> Your colour); the DM is gold. - **Lobby menu** (top right): join code, rename, remove players, colour, switch character, leave or delete the lobby. @@ -148,6 +149,8 @@ All `/api/*` routes except `/api/auth/me`, `/api/auth/register` and `/api/auth/l | PUT | `/api/lobbies/{id}/doors/{wall}` , `/sight` | DM: open / close a door; line of sight on / off | | POST / PUT / DELETE | `/api/lobbies/{id}/tokens[/{tid}]` | Add NPC token (DM) / move (own token or DM) and edit (DM) / remove (DM) | | WebSocket | `/api/lobbies/{id}/ws` | Live notes `{t: messages / lobby / sheet / map}` for lobby members (cookie login, same origin) | +| PUT | `/api/lobbies/{id}/sounds` | DM: switch result / critical sounds on or off | +| GET / POST / DELETE | `/api/lobbies/{id}/sounds/{roll\|crit_success\|crit_failure}` | Custom lobby sound (MP3): everyone gets it / DM uploads (raw body) / DM resets | | GET / POST | `/api/lobbies/{id}/messages` | Chat and event log (`?after=` to poll) / send, optional `to` = whisper | | POST | `/api/lobbies/{id}/rolls` | Roll `{sides, count, to?, manual?, mode?}` (d4-d20, d100; up to 10 dice). `manual` = results of real dice, `mode` (DM only) = `hidden`/`mystery`/`public` | | POST | `/api/lobbies/{id}/checks` | d100 check `{kind: skill\|attr, id, manual?}` for the caller's lobby character | diff --git a/backend/app/main.py b/backend/app/main.py index 9e9994f..dafd6c7 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -16,6 +16,7 @@ from . import auth from .derive import compute from . import rules_edit from . import maps as maps_mod +from . import sounds as sounds_mod from .realtime import hub from .export import to_docx, to_pdf @@ -102,6 +103,7 @@ def db() -> sqlite3.Connection: if col not in [r["name"] for r in con.execute(f"PRAGMA table_info({table})")]: con.execute(f"ALTER TABLE {table} ADD COLUMN {col} {ddl}") maps_mod.schema(con) + sounds_mod.schema(con) for table in ("characters", "presets"): # older databases: add the owner column if "owner_id" not in [r["name"] for r in con.execute(f"PRAGMA table_info({table})")]: con.execute(f"ALTER TABLE {table} ADD COLUMN owner_id INTEGER") @@ -612,7 +614,7 @@ def read_lobby(lid: str, user: dict = Depends(current_user)): diff = {"label": "", "mod": None, "hidden": True} return {"id": lobby["id"], "name": lobby["name"], "dm": lobby["dm"], "is_dm": is_dm, "code": lobby["code"] if is_dm else None, "members": members, "difficulty": diff, - "levels": _levels(rules), "palette": [{"id": i, "hex": h} for i, h in PALETTE], "dm_color": DM_COLOR} + "sounds": sounds_mod.settings(lobby["sounds"]), "levels": _levels(rules), "palette": [{"id": i, "hex": h} for i, h in PALETTE], "dm_color": DM_COLOR} @api.put("/lobbies/{lid}/difficulty") @@ -841,6 +843,7 @@ def delete_lobby(lid: str, user: dict = Depends(current_user)): con.execute("DELETE FROM lobby_messages WHERE lobby_id=?", (lid,)) con.execute("DELETE FROM lobby_tokens WHERE lobby_id=?", (lid,)) con.execute("DELETE FROM lobbies WHERE id=?", (lid,)) + purge_sounds(lid) hub.close_lobby(lid) return {"ok": True} @@ -1018,6 +1021,7 @@ def editor_reset(section: str, confirm: bool = False, user: dict = Depends(admin maps_mod.install(api, db, current_user, _lobby_for, _dm_only, hub, _event, Path(DB_PATH).parent / "maps") +purge_sounds = sounds_mod.install(api, db, current_user, _lobby_for, _dm_only, hub, Path(DB_PATH).parent / "sounds") @app.websocket("/api/lobbies/{lid}/ws") diff --git a/backend/app/sounds.py b/backend/app/sounds.py new file mode 100644 index 0000000..0dfa870 --- /dev/null +++ b/backend/app/sounds.py @@ -0,0 +1,104 @@ +"""Lobby sounds: the DM can replace the dice sound and add sounds for critical successes / failures (MP3, per lobby).""" +import json +from pathlib import Path + +from fastapi import Body, Depends, HTTPException, Request +from fastapi.responses import Response + +MAX_SOUND = 1_500_000 # bytes per file +SLOTS = ("roll", "crit_success", "crit_failure") +FLAGS = ("result", "crit_success", "crit_failure") # result = the short tones for success / failure of a check + + +def schema(con): + if "sounds" not in [r["name"] for r in con.execute("PRAGMA table_info(lobbies)")]: + con.execute("ALTER TABLE lobbies ADD COLUMN sounds TEXT NOT NULL DEFAULT '{}'") + + +def settings(raw) -> dict: + try: + s = json.loads(raw or "{}") + except ValueError: + s = {} + return {"result": s.get("result", True), "crit_success": s.get("crit_success", True), "crit_failure": s.get("crit_failure", True), + "custom": {k: bool(s.get("custom", {}).get(k)) for k in SLOTS}, "v": int(s.get("v", 0))} + + +def install(api, db, current_user, lobby_for, dm_only, hub, store_dir: Path): + store_dir.mkdir(parents=True, exist_ok=True) + + def path(lid: str, slot: str) -> Path: + if slot not in SLOTS: + raise HTTPException(404, "Unknown sound") + return store_dir / f"{lid}_{slot}.mp3" + + def save(con, lid, s): + s["v"] += 1 + con.execute("UPDATE lobbies SET sounds=? WHERE id=?", (json.dumps(s), lid)) + + @api.put("/lobbies/{lid}/sounds") + def set_flags(lid: str, body: dict = Body(...), user: dict = Depends(current_user)): + with db() as con: + lobby = lobby_for(con, lid, user) + dm_only(lobby, user) + s = settings(lobby["sounds"]) + for k in FLAGS: + if k in body: + s[k] = bool(body[k]) + save(con, lid, s) + hub.notify(lid, "lobby") + return {"ok": True} + + @api.post("/lobbies/{lid}/sounds/{slot}") + async def upload_sound(lid: str, slot: str, request: Request, user: dict = Depends(current_user)): + """Raw MP3 bytes in the body.""" + p = path(lid, slot) + with db() as con: + dm_only(lobby_for(con, lid, user), user) + if int(request.headers.get("content-length") or 0) > MAX_SOUND: + raise HTTPException(413, f"The file is too large (at most {MAX_SOUND // 1_000_000} MB)") + chunks, size = [], 0 + async for c in request.stream(): + size += len(c) + if size > MAX_SOUND: + raise HTTPException(413, f"The file is too large (at most {MAX_SOUND // 1_000_000} MB)") + chunks.append(c) + data = b"".join(chunks) + if not (data[:3] == b"ID3" or (len(data) > 4 and data[0] == 0xFF and data[1] & 0xE0 == 0xE0)): + raise HTTPException(422, "Please upload an MP3 file") + p.write_bytes(data) + with db() as con: + lobby = lobby_for(con, lid, user) + s = settings(lobby["sounds"]) + s["custom"][slot] = True + save(con, lid, s) + hub.notify(lid, "lobby") + return {"ok": True} + + @api.delete("/lobbies/{lid}/sounds/{slot}") + def reset_sound(lid: str, slot: str, user: dict = Depends(current_user)): + p = path(lid, slot) + with db() as con: + lobby = lobby_for(con, lid, user) + dm_only(lobby, user) + s = settings(lobby["sounds"]) + s["custom"][slot] = False + save(con, lid, s) + p.unlink(missing_ok=True) + hub.notify(lid, "lobby") + return {"ok": True} + + @api.get("/lobbies/{lid}/sounds/{slot}") + def get_sound(lid: str, slot: str, v: int = 0, user: dict = Depends(current_user)): + p = path(lid, slot) + with db() as con: + lobby_for(con, lid, user) + if not p.exists(): + raise HTTPException(404, "No custom sound") + return Response(p.read_bytes(), media_type="audio/mpeg", headers={"Cache-Control": "private, max-age=86400"}) + + def purge(lid: str): + for slot in SLOTS: + path(lid, slot).unlink(missing_ok=True) + + return purge diff --git a/frontend/public/dice.mp3 b/frontend/public/dice.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e73d79d6d3925eb2562926cd4644a3892a4883a5 GIT binary patch literal 44544 zcmZ^qXpMzp&b?t%?eyd6B}GAH7md+G;7={Yg!J1xP+!@ic5t{ zq-AAhrLEvXYFL`KPK&lUHPbY<%~aF!*!?^&?)$~_f5BNkoHtxwe%J5%o&WXy--&(z zaL>xkA4%dV11?nrUD5$1r&mnm9E2Zq+oqay*HDO7i8qdb+?U86P<1_1e|jL(ncMA!+Gqd%(cR8tuxeL|TiBY$ed53X{ppFItjJJoHrkd`;F^_CePi3&1t_r?gdD#s!@z=T_zBY-s32 z-CcuYMAP6wc3Hj@_NvYI6OxW3wk-#-H-qIdTm46u93&a%>26zOU(P; zzjiqJ6T@+R*7ogrn|yBG+mTIaX=yoS{I_7~+vw|4+W7f(+3rFt`7wHIEkgN{<(%#>98rna-MD z{qdSo_q?}Z(VnRt=QmCmr?--CFkdQ+DgUIQ1|yUbkep|9>jKkqN0bOQ$)i7&wLpER z%Om8q@7XO=YnPRYSNw8;pwMUEOE{VR+_+hMXSD#jN$E+sDhZ3HhsFHte+Fa=!2G?075j>xH^?Z{M^oTz9CAe)P{) zco6kTHx%)xQi|AAy>F-QUyt@*c=P0S@SEHv^XvZD`m#0%wuJb!W1Ci}!D7ap|Jit; z_cknW(Nafp*SF}wtS^a0VLXGWW#6XN)!z+Iv(9}TTyLKBpwQEQ?Lh|pS>*8(A09m& z0snE5wfhX*y+bKd*TI#4o;gz}$ey7-d2t{m&7$P@pAjS1{yYlW=`bJ(qJwvW zs=;yCQSdLk1tUW79yHatdPm6zP4$I;6J8cCw;3ziZh_B4eV^r@1bBRi06Sgs>X4ODQSuKn|7(x&+p)usG6OY+5o zct3E4%f*5a$N9ZPf=>wiPlko9H zvM0{1B$qfB%ejJ*bk>_hIP(o|99|&;kjy9uT$vsX9vSoCK^=g70tA>!q z7JzC$=gD{RFM*CGHv*Q~3Ss?N{Qdx2xhg z$v?#~-zT$X&@pRW(!d8!edxCs2EoyF{>**`WL0S}4wd@_wV5TyPh77NK&@Bdf@ho%qIf;p ziNe+-LGFJLs`j9kyjYGRqV&Bjsd%{d+DHAe!7>2Fw@prKdIFE;#Q-*58hHb+#Z7O( zt_{NCX@S=3ipxFIl!t3}3@B9>t>KFtbDx9?LC?1Gy<`kyFG`))R-u%YsB*d1=Sc#Q zn*p6T4Cy@FJj74YY-%zR&~Nr5;k}gBW+a^qDueCe@9C8VL3$yo==wlNQ)X~tid7TQ zG`Q!$nz-WID{?<$;qw3$IDuXrYhZwPe%o#3^Q79}>d^Newg1@LCVa3BqHvp^FK!%t zE6&eiam<$f73{O#F=kB*7=H$^U~gEATJ$>@<12HuEeqftXT(@k+5l@9kE~p+sV^8U zPCxi}gnTNpKU@C%6RO$s@$=Wxo?ufzWopY3$jNsZQbdvmudgYMITaJ&3|sDyp$XF0 zUnWoLAHO}Z+Gido)f(S%Y8@0KSm7Hi34eT}^e8*pFtnctDFZX{Y!{+NAv zrKW94B+;vW+3%5Vs1?g(W_t}c||%-w#y7B9(PdTQZH@#jp2h}j#v z_varY@$LD7PQ6!of{o6W3ve?AGYfF#y8b)+OI4e%U@QK$u`w}Xzx#kiWoAIn9Rtk(M@j-ASw9Lrk;YrC(V6cYLyBg zNEd8V$Vu|)jl5D1chy6fzc^oxt6*7Bew~k*!Qr!P>k2g!a|zpzo3;40;(|x78OSir z+Z+dsLi?$enf$wbUz4mKAxLt?BjZjTjspZq*ir$}FQ8Zwh^D+ej@giZ#ZdKRp=lTs z6Cx-mwP!cMi;z?|F$@(^Nn}o!-nBVC0Iuj58h5s3+@v;3LW|@7V<;Ftr~Eo8H8AV~Z6Y0o@essGut^PE;NXk7gc{25oZF zqOEZhCv$hoV!(9=3@|NSRSQ+l7xhua1z7tu$?4L%g9F7CSE?L{4F94xd2=QY**71l z|2(Whr1oKcGp*Hb#3?yV+MCz6v?_Nl7X3DTAXjXaG{jF$k3+$W>tLQ$vyWCBEpteS z9}TTd7^`-vimZL(f3hSWfqNd;pR5A=b0=+F*0*Zw>{C;kJ)X+-G2H08@1uGS5b4ZG zVJ0v`Issqo>+T8{kf;ndMx`)|(RREGElC$Oe_b7!vG3rEy+*B zJAM}j1`5pyEN0G6?kkl1>7W48Tw;QcRD~mPuh+!|-bRf2EQzc1FBW>c&m3Nov%>qeYNN^kVS`j(uA5oOAX}f_uzD)Rceb zrUkfB+~qRWX5${#0LqFV`IH;Nq6H^mP2_Mxvo!WjJ-i3L?g#%4h(8OUFP2+_&7X7} z>b;}^nAb~-#?xko7#JtpKEJ|qJb+?z zg&A%n=KF}+#W7)@)A5MVy$McXQY%EThI;d%?X%>cYWw8+>#mcNyi9JAa#4JzN0O=* zCL-!Z&(|01uLxj?;V%St883%!9!FoC^k-CVchAEEd6G9SWdO~ZmiEkQ`C$_ius{Gn za(*bUQ;JR&>4@;9FYKA33ep=bzV%IzBK(UBkXGQ9b=WIu6tlmqie11BS9NOa@>e>? zPRVf{Km;IN7Hkda(H_3+;!+S5uwScTZ`WkY24KQrdIC(Wm%x{EJ$jwgRnJf0q^U~} z-5jzF3pCbAyrU_><0R6J5<%XQ-9Hq+E%BRgKNl9{ys-U-fOLwQ_34jm_7SG0N0GLq zwwkPR&sI``yh8Tkp_+%PP#2YvVw6G}0k;6c>vp)vc($St(`mTo6`oiC{bG-a3F_bR z5xn=>OMzZQvcK1YU%!q#o{D@a%cn>k%ZG!QgGS;wU;1miR{_Jz+=V^PZZVe!skr6* zwr#n+zX{#}E4VAIg7TuU<$hr!ZZY<;NMYX|H!L#U4%kqEaPmP|tFGHcGNq~X9;FA> z{zfsb5D5=}k-*@n<R%+~I8y8o2Et^aPmm+W!9DPHJUf%3JZRF`S(V`OMlk z=u8k3lWsXU;5NqvMH)7t^tKR*Db&{osT zLbLK`+1|FMXvpa7+^F9(`b4iMVr~RCvrAPtOFs?Rr>L|lyb+oKa*M){B7CDlRtRqW z!M_tQpJ+P&({%tY;tSFd3;mEM zonLk*WNh;EsS3s~J{cjUEL_I>6)XpJ5Q-~@xWq7*`rMQtyqWq`!j6p>hSF+xtht;v z=5DL4Ui@xzew!jJ{&xG!n04yp z$G`!k$her@V|!F{Y*d51INUKk{&?JbPyWEc(>B*Js`y{Emk z=0{h;OI7J0!lL2uuPCjpF9e<*#M2-x9Q+ zyO_Hlygu>$!G&*c`i}E!{`W`z{btiX0Du8-7)%EVQ5b_A4yB*=v&2jkbp(Zw-PBx_ z1RAR>nvPpw(*ii>p!UkbnO!zkXw6{wy@Lu!bFZe5FR())rlTq3(a)mHpny{N0RQITIvrZgmB{RM|7sxM|l6>BU8x9pD`Bn=KE@DU>Vbi{=9Vo9{ z;p!LCGU<@_3N2ZtKY{>UDEHyAi;dsV}a3`~F&@!h8<)4%Bg70f0DV{g~4z z<5Kaa`H-{7%w?Aj$ZRF)s7Xkf^*}?9^`!$1S9XqXeiQpwET!nVW|eiT1`26M027z# z>huG5Y+%m)I<6HGHsK#oQ@7tXjEG!0@o9(W{H<61XGp%)sm53S@2#)e&g$dd|6%|D zp=NWa0()t+HF>`!A8uP4TP3yx7Kuk=GLD+ z&QsVg> za*%CePL{&OnORqfhUZ@${WKX95H%W)VZ7Zj%I$MIaH!^k1!K%K(q&2lTtdQ|hs6QO+R*a*W zGVz!J5uQ!ypGvZ?Y{(I)krxNa3rMPYva8e1X?x4lj3nW7c{jH87P5x%s_aX%4+fKr zBPV-wu^0QZ?GX>kFr>-YIl%_?$UEjTBefo<{d1s}ShRj_Z$>wLJzgHz+ zh)b!B6IZw-1qY?%Cp)&*hJRk4&pVf<>-SgS)(3q&RQpJTt!REI%ZAm*qkZS8k)xsy z+fE9!bGDL_Qzd|>>b9XL(mZ~|!ioJ!bIpWh-Ps?ZCqRQB%kOw;OFm;NZy3X}SB}{3 zLUs%85BdoOdZ5x7QA+jplOxBQSUh_{>S9ncI3Vwv>tt+7W_3Y3s6^xNb_~1)3gouvf&fAZHY5i?prPSdwvDwZ^QI)x4~IBXzcl*8DiBOmQ=7Nm z!uFmAU2?~t$Jbq5r}#y?yCJRTp_}#gHRelV@626$*XCh$i+y3#c;AAfW8MX4qW`wo zXV?>g1n!(;e4rAs<`{@;gr8nep$c4%;oD!z&#Q?2=%>uALH~z=}K;Tgm=C{J3!9@^B{Lu%8 zQ3aUDg2oiQ_4B^<{7`huw$FD`HhVt1{c>*8CVvIQog|4gs9Zf4yre}#5+1-tnZP%= z;ZOtrtM;>x?)N`5`}tpW_+MMK+_-~Y>fnlF*s64{#zib6i)M~=5I}K)0IoTC5pL~# z@_OZ@o)3v(b2f)zJ6HxpJ9K99~th}=F49Oh`4DA;0aJ%0Hu86VY$9V z&!Q!B3_;(no-v8c?$SyQ(YJ`e;Pah(0*gYcB}2wbRfgD)?cNK%W^Atc{_XdFzW@9E z``7Ok7krj4gMj~(3~I?13|fc94f1g$QJ;-@EQPK@Q_zR;HFdbL%EhI@WU#i#oFtmM z!kE($H~JADX#BzdZ2q}r*8g0BP$SI4^DgLsRgvZy{?--c{b2>6+`j2u*!2Do*)%6V zJJVxDoEV0N)9zW9tAtkzRA|wNm8va64^stlgG}sQESZ|-!#}eKE1rm@zd3!RNJLN8 zBX7DaiJBWZmy|uSr><_OrKB!OR%uQuhwUvm5Ic1;G$!UEf>d`ZJX^Y zxLwl4x9%r(I9q^(4;^;qKjmgGml9V#IGEsGsBG?dV%f6VL|}^iOmvx1=j+>|Yg}(# zwOHV=dn&ekU~re8NXLVKMvg=1cNnbZ71f2Z-&ea`v2UX5NJ7E~y7c`M+12{AHG{C+ z;Ns$iL-sB6;bC^m(NC4?KaPX;&lG24K%3SQsOCej?C>gz`$*wGOa2S(+0(rN``vc` z?=6eIj*X1}41*T}hA@rnn=?^PEJ_uD$(*$=?yA$Yk%JL}t7(F1k5cQS%NKi$bP{n$ zN1U~&bazncW0@*9)7G>O87H}UuV%f8?BpD8X*Q#01Tbi;>hRA}s+?7$=qsnl#}h5+ z^_OC$S%7$S%-|Y9#`z?I1Zeay0_|WP_76it8cI1$MhlcF!W_%JhW*pGBh)<>$vpBg zk;y6^Wxp;3G_9$envisvA|8VlHxOLu0xT`m&Vd&dPJ0oL zN9hTrg9d+l(kqRaN{f{$XkWzTQ4lvs^V}2E%o@$x^DPQ^F1q;O2NUDg7ko-+r#$jr z9gTe@*g1Eerl*-6;ZaH0{dn8PKYdRPKe94>cVnf0Mv5UbQo!n~v65)B2T%S^W)2gvaM*c*A$hDqrwN~mGQ}*3p zaXHpgTp+_ih=`T<6lUu3Fbz*u04NSdzW@{}92IX7FPoezYd;cQ)z}C@H0JGGaWAze zj^<8WcX`PR;&u}#W-NQbO}Z2VJ8<^5wWO_%ii_LbR!%S+BJLh7Uf{WW9BZ=D_N}ok zuQ3_s+Ob1j(`%glG+qQ;NE<@b8|L>phPbVJMK2Ga0j_V$eWjy*#@ldTA^=&Qg zVV+H8-Wuh+TL+pj*)KiWPhwy3t^8=ps--KyhHa{!8@IKNXLgO-*r^auca`Cma(~-#xl9cd9#dy5F?^lD!x%mS0D& z4ue;s)4kNl1nEk08)DkTi#)@zIAO*%Vr*6OauF2F$NONTYScwyz2 z7HC_g)4II7+=*I-w>d}LBGKA5%5nL{x+GXls}J6&wLlMPJRHrsXNq9t$>d%fCtR_2 ztp|M@H$Q1UBF33`_;q73Y3S~OR`WUNy^T*gnJ-VtW`4oO)QLcR=AgWXiol|aH?-)M z7NO0T_lh>;>vWJbNZ5$At{|M9HuWfU`#Y>BeTy=E9(+cKv>KON+O2rH?V)?5S;t_N zecbo;h5r$oe^cGPmCWu1i?-YNgROooec;Qxz7Nj${i*f!zsG3V%rmhAHK{={8(RG+X>o(groWPFUQWTD|e{7VR{~@dl9dxCl}#IsNjSbHXW;^D^tR! zcia^pGw89KBBt}PTX=i{l>CbS8ofCt#SES3lb#wJ=5}-%r|j?RIoPNCGbmzPxIobIIxR ze{nH*#;ca@kzMXU(>aL?%uoQgm{?;{uGn(@E{xlPP4C4qf{XXGuNtf%LUYz*PhUxy zdpI)PY<=V~T8xbhSZDId;#k#0g_t~rIWzZTKjCokk*ly!Np$Xbg?a4CGkrVUfa+C$|b}NBjlxX%KRQOa7w0 zJ?topyK=WGIBBZOAAvYVeqVG~+TTS8LRZ9jhVR8<0>w|=UwWbX1#VsQ;#2x)OgoNH z+COJY34y8@CW2I4ma_{Z$|Lnfm@vKo-be@%igc;g*0d@DWBcS}JD|?@O4!~4OdWp6 zkQs$`t<(tr^5N#ho%fSOV55F$Z94lj5%;XC6~he=uBk2aJ9;QrM7?<{q|eEyG4b{x zC3^p}BG>&)6ITFvWxO%52+~=F0f(=NUb7CHdk;q3<#f$3bt=xM?TnpV-!vPxa5=cf z(ob@X!4)v#5*ZCkF-qf@iK@U%fCl=D-;sI|lwkR49QYi;qOs+2M_V^P%u@EvaUs`5 z?b|X3F6V{oa~3xeWZ!c`WQ^m8jm2LSF>O-l@3z;3w>gxS)2(F&Ls z)(p)oih}=stI~RC57F5IknKw^*ZeUm+&8|bfz&U=k(RwNMV|=5n@D?sd>nz)J0zg9 z%Nx#5Wy&OC(GUJb0PRvF+U6l-GLP%-uxJ!;HABU==uooRNtJ-sn8LVI@BcNycA~oA z&Z|zTg@Cn`PVq?Wpm9fsudLaWxWv}Og@CbzMJQb?h?d1|4Yu%kI3O-aJkz=mptvgc zKRlOlao{n=3Wf@1ecSg0WZ=^)H;Jf|nL)68Q~_!>=DE`c>(%SwRJij$I50gvEf%5p z=l_~If?TtL#3(eC)0U~foAQ*v3U!fc5VPDE71XXrW~n+26Gi8Y#mI6!brEhMtYbWI zr66A!GTJc5=+^5tkyO+>HSr*z{n~S5-}~5WJ{N}wf`RCaVMbv85#bXqFtq;T!{k?k zR>t3QUg&g1*FfA*Fz}!%cc8v}U~|TA$**qLXzp~=2D$19_{ZLNg6moRFq7oTz1AH?)&#IOYT(O9bC20cjufEOm@n$$LIc|%|E{L z|Eq5wWIqy10ES_MEhYZ6Q3ib(<3S-PDh5LoexJDP#wBdGjm#ojc6bfj2E#J8vYvaf z5A0>Po@BmvwQx3m>0&L&ygKF@RbR^fm649gD9?S*C|-&FbD_iC+b$iB?ft2T2~9DE z`9Ig85;s=X9=mTaTYE5ovLSY=z$SGqtoG8r4aZOEDEw)8ZFdzr6j`s@Q3}#=*MAEB z!G9#cVO&|@{|G=?wY4B-{l8PbN{eH?$Q<+xUSl8mB7q~)T=ikO6bh`7jH3R(-20f8 zg6eWVGwO|JNtQJ@kvDTH5V@4WMfOr#0lj$KuE03BjG=RusJl55s<@%L34BwQpDJY3 za;imHcy4Q$+ghQ>t%{=Xeh6~;a^jv4URF7ewjv~9lt#5Nhl04yZ-il9j=>ew#Z>5a z(}kp?=36{*BoZzm*jynXnnxzjRU@59Rt%9y&vl!wVGTSJ3CpQyEYLa7w4YUO>zjW2 z!AM%ubuevz1q5mD*Z{Ipcu6(kxQuZN-p88);@*C}&$Dd7F$8W{m z+?{!!(0yZW*u;C%{ixP)#H(xjMR(t24n}8=-N)bSNIuU`bF6jG-+W}M+_;n%Nb$EY z@vbNNqz&m$Hcn?9bNnDk@-x+z8*+Rc<0k_1Kv3|&$7-1w>?)P0xv~6Ruy5(gyPOEj zc#=7eyGxauDvxN&I~W+@yQNoaI7ET!OZbU(dd-C zw-K=dE=;+Fpa$wRsEIqmJUXhZ72o0i9EzAwM@+<_oV-aLp&-*NaC#Jp%t|hVO7(LMU0Z^>gD4{fu4}aN~GJQ}pWJXJxK+;H4|qH{Zo@ zY306Xu3HYVeL0%!8!dYHfR(b+I2aGscioH#K6F{8t;DEtq9qKGv9HmJqb^&nVRO2# z<+HXlAqr>BwLNrpy>8pSy_0kA?F9^u2y#_mN^_4EZZMlia;w_d0a^Du)8JJ~3s60I zKUtqvImG|L{}RB%Jd(QkI$$zM41*#N}lDdqC3fV1#w zvfKq@T<(B#%P)2X%DZVHW==qviK$|EwmGB28moO1#1$)aJir=8ajIC}KlVCw_S^O6gsjyF_^3`@ncL(=4hQvxa=K@}9Q&3c0egN5+9j$!O=&ykd0)VrsNa zT&Le>n$Ml>r};P%=w_@g)lA$HntvN^=tWwZNkm@|NA{9uisciKI{3g_D(>f0x~0EU_6LxLxKZ5S*C?KOs1S-kfX6t z_0DWk9Xz~H9jWd^FpAEHkhSdaeZv8@3Sy5Ef=Ev+)1QDQfE*BdApsebht%pw2%|@T zV&BC%#?_(pBBhD}19h)pAwWyYU26~DlpV=5Pmkrv?UI9wr*TuU;-i+s$R@oG__Mis zer~s3hfv{QQa#?J5X#i65lU{L2HppOx=Fs#TVK>mqZ;i21=dMx0;7tk$Y~6BtMC5s z*b8UogUb%rpS$vP@o;nqj4E_Tz)W)Pm<`Gl+YnM3yLv#X*%X>=eN|~itSl%B z!o!EFjxh53;Vl);zxQ*St%^z8WIBm1v=C(}Aeo!sdAx}TMCszK&33HW|Mq&qyIptUun)H$ME{+CcHO^!z5BlHTbqCTp;p`MUG3@iB{vct z{I$S8Lh#`VXk4huUl;94uZc*@Iu;RezpJb3Y!>86R`CtA;(BRWGWS%wtrmx~Z>bWk zKWbcHKcnCH^pHZpX88uS+_)y+9m2w%&OgLfSlNcK|9R?}$38v2#fRz!GGKFaaQPsz za&ncI8=0i5I(4eh@yV&SYSPdt*{ND|oj?CIt1)MmL=COg_hc zb0*p|qZ&gm%JS;K#vD8`5$^!G&JRyt&QLydavX!A3d$Dx-0$=055+@0&VOHg<6f~t zy85>CP`^%z?7v^Q?Pd9csro zo|atO^k%Xm+QeK0D^r#e)F_XnB%Z9Ae}GSZZflWFa&FDJNa$8fb4*C8*7Z44W&Eew zq;-QM89y;;?blLV-Fu83)(m`qzsSn9KoFM=euVlao9CMuZwo;@y*Jcqfkl7FICXI( zzs$FeD^;d)2_>cIRSOH}3NDARw#ALO_X%swai@s_bzHSX=2@IS?FCcQyu_k-@c;S$ z!;wUrS^d9q{OPRz!(#`aEmcg65KnFC0?NyELLIk*X44xyndv5trq5X;>E#f1>6A7A zoNGo7zU!q?PQ87#= z8n|Wz`oC4-2bQd(UNGY>lM?@gVF9<5=Y(|}F`ypb;XY(9;Z<)sf+q2=yX;`H;WxBlpt=wxX7p~a#A zxQC5SDqf6Dojh`TW9LBX!ZKXO=jYR*UU};ehNYV6GDJ92!)%wQ2ye!>nIvY=e=;n9 z+ozSaPFojbrr7RR{pFt(5pZHcg-;P+C_GPQ}CDC+?o+YkuN zY$FPbl9R(+fnvF4#xuUTH1QlTfmVdfE6h1=&1m2&LyUAq0P`76_O9reJa8fG>;oM9 z_bweBkZcOssUh<3rX2r5SeBKQE8DH5XO zTx&R@>7JZR>71k@6Pf`43xs~8!>qN4NxBF!LBn+d9BdS_b3wX5D9{o%4gjujK=Sip z9`pic1NA^c;K&~+JcL&ujWEC>`i1Vw4=?$W@%guHv&Uy#XWHKG7oN!b8UVnFA+Zu2 z0_C_msceZ1JiD*SYVIk4XctFiJ!mSQ({0@6zGkQsmW<2zHCZ&VB?)We_I#EW9KacNw>AJ!M+!~k8H9`irJU@aDw7<)JnJgLb#SaU31 zS)?vXC~BFcEeeB|2`7i)lc`R`^4zv%B2CtF%Fkwl1pXlMWLWO=d;3uE;nSo#0W+KP6%&CDBLVif z+)_ZPx;%%;Z7>*I*Y3yVBhaOQ0s$UVl?1REZn~+{5+~zX1U@Wsw8mwTIy{rKi=8L5 zCbRd^eHJlC%ZHk7M!j+C`!(+MlZEH@uG0i0xfJQbJngsp9mr9#z~Q8CFbn=c>NVVs z{t3d9RDMphYx@R7^Q`mZ?0fdK=+}AMr%MA_w(p)i^v?+y0PrBS_gROueF!dh z&lhNDh=nRQI155PC}ay^QCBI}R?I(2w*@cdwwShsP;!S4lVa%%x8Y`Pv4GYgl!=mL z$O>&*ysItsG#6^$l2|(MHgjyu@n+~YpR?H_Y}>I3=m-Bfu){+U9+B@7aQ8i*k{|va zm+FylC(;?Bw!EtCXp?b&Sy7XNzTr@rF1deuXVVzj%yo;M;E$GSm6Ky!d_;8TJI?PF ze;r5@T7g;=YB@ zQ}2~l7$nQ4w~vCY^aKd{K!YxEsBxn&F45WetCNalYDCNtoQUCu71t0Tf-24WDh?O- ztS`!PG5$%O$|Cb0Ng=+JAsSl>8K>j?+}w(FArHG1;9#YOd&ue1gH6>ooFz)1V?ft zlA{Tw8B?IlwZz2rqZ`Yw8kK+PLt9&Ze>8fz@j>4C*Ko!9=oJ^+NB;%Z-slQD?cT;W zpf<90lBGc#}*LGN6u1lF_wJ(Q_K7uras0U z=4x(An3}*^kuxpIFrw7)xV`LTQCHMZuLD)~Z;69w+qEMO{(3n?4vUw!lio72MlQ-O zvtm#GoXN0~2jFNPj1Isvit(uiW$8oC^laT%&m1bWCA}=iQ*o`%miA%b%@~x}Uw_&T zCOkd6DPUhO?=;Po$YFsD=Pc85Zg~-jipdQ%h3Si4Nkrm4%w;q(==p0D&K@QC(;Mc> z{XJ4~@@i5*hkNgg%`-a2m$IPwrQata$deWHFtLeu&t~Y+vZ#d~Z5|@7Mv{R0w}X``4m@1dx|Q->|VI{a12f-FcZE z`jhNpaS2j<#jE0oDfwu^%t92{eS|3jSw(Ed178Mb4~WMz)h3|A)5NBU>#PuI09OVw z2gH*>-3Czoga5OD`AW8|7oKqvMNlq{|*Q+++S)EIdgHA-I4%FJZ zs;k-?!hFf(D{9V7k{?3Nw5&yA)G9z$blOaBb+9*Q@?`TrToJ-6jf~&KvHn?lJHsEF zeorbIa!G6UO3K@^?v%(e!)~R6EnJO2BdhKAo*|wF!`~!{BEDHJJ%79nr30>%yrKDJAbkMJiYrHoP@)fz;*tZS!V~+ zeC+T;B2Y5ZZ{JlYg9XZigifWvn(*cN@(w8zM9!Iq> zGKSzZGcF#S31nd4;Flradpt(h&wfo=;6(v5EW@us4C2~+yWTT*)-WIoJc18dr7l`z zcwLcNpqeJb5W&teO-JaA$SXZy8+oO+_FJ6;ZmBxf3{60zApWAm9+nQNt31)tLrQ)p zI#{hG!wj~7(i)Ow;c-OriPqZpzG^(#p151RMO8cIYu}VeV!yS2;p;$4l;^VmD92Q$ z7v<__T97!r!Xv*%yU-Thj^QViiIHRV^G4&XScA)1`ebHq8zb;17`%mBo`?jw2rwBH z)Pg2mjHh;(z>qC$RV?V*a*A5Xy1B_UX9}K$Mp)f)Yhi`kK=im<9@$BOqXss{`o+ zR1R*DdaV@+m|G4rxGD$6!8iDd|aYIN-a z$CK(r0t}Lo>_wUQz0l-rgofc@Fz^zc%k_OSB(!A74@3B0_3QH8|8+Hc`fug0vlt+d zb6lF7;Z73J^%TL2C>Wg>k_j$~Zq!~6q7N`-_e6-kU-B4lV++7+1lh@vz!3rKB)247m8JXjpCv!!IdBMyW_u}sIjvKWlFH9fa zz>7lIB9x>sOqWg;j406=-7tJyh?L}m4L#tF!OAYl#D8~csk+R}K`u=NTPD0EsUEU$ zANr|3S`z+B#5)@IZ!s#Q9RsXx%JmeqX-}_O>8QGLvfaS$toh(2v>*07YDi+8%|7lk z8_nLv0*9MhDm-t-KP%Z?OZ+pfZK5*mG5z@7m)Da|Jo!B@E&EK()-ul1>iCt~A2s0b zV`X$$_$-HalrhzZS;V2`fVc%&!O0tZWii24%tPX=nO3fT26|7Yk#%8-5|mP!Zk^<7 zh>2@tqnfFBrd%HH1A;t6qoJY1PR%U>8o<{uCSiOPlMbt!A@EMA6(%r8OwS$|$^bcQ z*%knt(H|U+CR}zbfvHMs)m&8n?ZP0L2Nep!OJDM_pTMTtGcun*=TQ-!%g??R z$)?dHQes219CItadG6q-|7I>U|B2<%3)GPHjAdL6mCF_;EUR7U{ozuE<$AA0sF9E# z`dxdfSKp)&JYB3-52=YLD+-g+m_#Z5(1!xT92IRBC10rHGq<06d5n z=CPFKJJ9$01Fa~+-rGE)3Jw4o!>^mBrHii)>iNGEPcMn&Cux+rDhWV+>~1}%)tUDg#vvXw0-MxlhKvnRj8($EsLAUc$yeory;iyr8x=CK?!J(`Ol)eV z^u;R|m7}>TJ~*3-dl<-gKG%}Js@S7Zpb|phEBQsw~!!M_R-O2Z; zNpC!oyIaM&>k=$C`7Szzw|$fA$E=3VB(g$0n3$|9NiO<*_7yYILV!jXy-Wwfg2B)n z%)nCHI|JcLNINJNz_hc>V~#wP>to;mhJ+S_y}PS`nuHg}JxH2C*0{QeL?GL=#=D^u zS@eoxT8QN#^hy~+Da+(0#{q*fzfQU*iErl0*e7w81;2=MU=$&ptV$Zg6{0D*$W$SE zS=_j=M3?ZIa7lczJ#@h}f;rwe8Fqf}pvnr&fQ5XB*pM2CG6cZf;DnONd#We; zJy4Gv8iS@dU;rZn4q|8|V06i>9DTdRRYKfZMH>C}*{}pTx=b_zDIT#^!^zKXBi;04 zEfa-d*RCQTyX6kYB=eof)@)Log*ul=?hhxs;7BjMlCs2gL<5UMPW(IB>Wzjx?PP6X zt@=NFy=hoeSKBtcGlvi$34=Q^V8Rds0`3f8K(q;91PmAk6>-YIEK(3uY^`R15Fj9v zm>_BxL>#K%tkpJQh=4&s9B`yU z=eb-`7ohakP+7d4wj3Q~57z6__|Zm0JA9=+C@_Y^G9XFekl1>!Vw8H?5#h%Ujk>eq z9luso?zSkpdeG#|@z+(ag)TF1&^ZvwL6gOzjQ!Xu_UIqD&B;g=lpT2q$-^4OWu~dQ2e&u&RCo}Hdwtoc%RG@k14W6#r0;UqLiwwXp<$TdKDwyCYx08!vck3*##701#q!OxFY9w?@Z9IH>uYMwA(5atpp%GDi!^`2u_B> zmlZ!Vc1yk}26QBvBmDe`0t7v12%$1O2~R>TzPw;6H`fQ6La=gnLbehqeuoDezYM24(HZ8jloJ1 zosg4p#gp@aJTr6fOwtE;{~I4{H(7}9h2Z{H2!E#A^Sji1#rj}YswZQPXr;YQ zh`^_#>|ko#ydL>j;>u<367I9G#nQ4XQh+2&K9pu7NKy2m$a8Q$VD6%ZFVw4Gh6_kg zh|zn)_v5>JI)}PaZ?hCMduR;DqAi4GJR+7Ei?bRoKU$`DwA~VU)rD{*sLj(BU)+hY zu0`vdaQKxZOF6<()rAdnh0By;e;3y(*o4WDg;p+_*!uM=2^NXN2~~`*ZapqFyOJIt z)y;xrIMoRbIKu%192luyTdG5Z6IMM6*tRg^)8XH`?yWYPVz>2A+8RWkhKC7AJsgHL zZFjm$v;6^^vi?>2+p?pj7N#c{K*?4-s6g@)Up}jy4n9N;hUl#nPN;nT#MvZ`PI6#p z)MxyYmF+*SWc)LvaB%K;rhJlhNlNTsCMSVcyS%MIiE4;1_BQtkuERZd;j%|oq8Qu`tjf02eTZs zZ?9!+z6aasapB?W(Ps@erz*S`Z{8({>ERF{b{ix7r*`k|9sOJc2)oE+qp|A;2C>;ZS;X?s^QK>FuNJV zZk$WIGxw_%dLE!Z6??$1dn-SG{|@2vqyxoQQ;{|GE$Eac^7|G!)vh`0Xl>|$ob1ip zj+s+5KNlj2Lx1ao8pj{WTJ5~zGJDz5j{9wh7|l(_3TWicfh&2Y2j2qbh~bH`-sO{Q zJYLITWnLP5)mr>a*Z9pM17ReK{_NvkMH}49USV}ZAe@y5H5*?wcS^%LS#{#<9Nw*S zix%EId$tFHzyHY|AYjnYnW51ep|@h7#j3} z$BGgL2SY*K5y7wXIZ|(S_URDfu0_M++|(c0l_c&5J?rn+F7@+Vla{q}!?9zM)>A^$ zkE1&WhZbu%zE^vY2%0Xo!no-2Om7lL3DfzF1Q$%*$uHO*IrQNiC%Sq-(j#cAyeDB( zUf)|j8h%n2xQT0ieY2!f5M@FmZ2lvG8ffICMcFcf?LluQ)Sy4(KOSIa#k#mY0WjAB zCrX!$!+PnA7jz1U^mgQH%^F{XGrB*r-4288vCMO{N8AZ`o#^=#%nOI?BJ>2DD*~am z003CeRBRnP@^;S&cn|-G0%U@BNdx$~?-6Si4@rC1$2tD>=gk$Le12f=Ok6VP3b+E$ z*PGUFK6Z=0uXix+;?f94PEp^|7cbz4w%kzM$+zZie$W_(lbm=tMk#K6J?~DF2Zr0e zL;h7;TGg8%oe72reBTePaCRmDt5}ifv;JPsIWnpjk?;|EY(MJWoXPruUJ!)53aLZh zE#SvOWFm((D_}iBoL$UL1{`Kp$(hM(7+Eju6Q;^}VUf@e51E zJ*g3RuCltfbJp4!-8oqNz)C?VdYc@qn_%lL{Dq=el%i*hU?nUnM|_niMki4Q3B2ifGF3)6yLf2YJ0{eA+2ze2>{+= z7<(GKn$Bz^*fw^+?{zO~a<0Pq6(<SYl+DG1g8 z-sD+(?=;2$FP3#G4Y^iNcanoX)kf(FBIfkI@bbl3@mT1kt@R@k%R0NtB8$!E+?O?o z-C%uI5@O||c-WiU$O5ymZ4lz+CtDf#Tnh@OQVvNEEo*-NN4C?3RSz6PoV@Q`SQ&ie z_3XYQoBJoOop}B@aw?raZ5((pm0!B*gYMhAzu&+rIvJaOsFxqubmR7Kaj8$goF<>X zN?7yP@7>QGq|6#;J^f<$MZ;nD=|e9J zZxW;hO6|ss*2@PTz9@LM_U{udk9uNi9IkA^m8;4nZprmm)3-fn5?fAV4L`?ZbI8|V z>Fd{$X;)IubP9I$rbUn{bXRV2N?fczjZ!>C>*qjY)IbrWSnt1(wJz$2*v4GP@SxGeW*Hd^#aSg+jYh^xU!Z3A;mX?LS6P)AG0(Ea3HZ#VDyt$DG6F=MeA+ zbi<9MENzoZHnBvbeBs0rCtZgt&32{BgiiJ$39H~MhzU;CE4;s4ZdQsIc!CPmF7VaQX#K@>EamZ)JY;hjuG!>F(F2~{Y0UM3a(?uI z?&~?*LGkx;NfcmHyTOIPIIFDKLIyzsRlCYf6_fWTZ0GhXX^g|Nb~P zx^Yd+e0mZ6l>Fbx{X=IG5+&wwBMAJ8aX5~hoAWoBZ81`uhU$6v)@LJg1SP=@k zvg`Lm$XGNylL+EOwKs#x#&4gHYa7(?X(>v-i=SF8mR=gY%dvMAIsU_gi~9IocDJlRW0ChS* zOlqo!j6m1)gbR%{yFe@Cy0)@IyO=FehLj)~7)%8`Totr1X56$hZ}o~R>(SlU+|<|5 z4S;`u_S(sD6aUOJ^J#87Z~wsSH~ZIK{uA>9RF4pm%U6yGhD^gSigDkH@k_i4^YP#Ywf zV-x%5(2m(1x0}9*(|18KMf{-Wv?ZFlktA7WE{3&dg^M%uw26`}1} zgV6og2sKxC+ra*gI@nu4#o>Rab0=w*`D$Dl9*gv_Q@Dm3CmC%HVeY}>X5g;q z=Kfs&plw%bie|)wt`%nv<~*CXk5?%@bft<@x2h#*|HmWO-s&lof#>IKEIjhgIqzZu z)e9mj?wHx1K7A}@duXy+N5b}e`c&`w#j0caA8gC;Hy;4(W*D3CmhWBL51wnEV~5@n zj{foxW66HBbF*+s+qA$feLhi>V&A^6Qn<5>;k6%)s1t@cJ!=hVKhoq;b|usBpoq-v zt6%wgrhh{8PvrHzq8VT)Tr>rZSVZ5Pv{dSN*gr9V?XABksc|4rBl>2)YmLED?{3?+-!c0J2_`OKM*&ZZ|v#vu2v zPHcOw-s;`M@0VRkmT`TG>3gEY$GYZoy5I02|EG_3?wKBc!4m-?Kjk^7?2j?bBe^aFBQ=_(dzbi5)hs2uVH5_AlqK+!kH=jQ@GS!sjb! zWLy3JzJKcH`i~R=PSXsGb#Cp4sHOJDrk0aC`jn-@*;3BVR&Rm6IqUTDol3Lvrp@^7 zC%f+3FkK|8$!^*B<^AyeOqz^2Skwv@C zVivQ`-{BC#IM3xl=WYb8C9s(}7}#_mhTnbZHZ*hNUh;4vbbYqNOb~a|@I7L&Y2v+U zHm^sL*FE84ZqBqxUbQ-QhYB*Gp?S!T9f^0a5t_IF~4s8^yMY!1uBh7pU!5W)7+;y_2`v23x?3H8a%p;pYH82tp+rk zwdS5<4LMyEejz2Qm%51PYUE{&T%hkF*Y^ubBOhqJ3AGGaNU~gVTq+=a*|a*lLYA$3 z;eZs`1fXZ&I=ri;#MI>}|IoJk@|9epP8P*ZWmkS%{@=Uf%l~}+ zyBt8GOc>k~{ZyuGQH>(9eXHysP*||#@^|5u0X!JR$W?rteAS*&*Z9wP);iWi!*V~n z1E29v0Rk$w{3ri;#-EC?<^Sv5wmP#a?s!6GCNQju2#pch(b$KzLs6s4F|njCf}3Bw zX(@0Hc6!QS9dCQoSMbGo2eF+k=dh}JjvFl3(syAE{LF&K%F|QrkC*BpniR+~_G!W- z`f8c0!(7*2mh5Gvh=yK_&ZS%Z*c{*L-U%<9Mg*VFQF-5@MIf~zg&l&@qi*lpYPd#` zw5~m!oPO8SkcZB(khP_jxk&Isxy+fR0O*pJYO_V)2zt9o2v{sB8SjMF-LB#G>mDq( z)!C65oP4ZH2qZ~`+~#0VB@&?Gj8qVcHod^Dxa7_sP~u*3BXMj<+pBD$nUzX3hzEJd zf&D#c*?|>J&%O%?ZBvG(tjZvPgP8obZT2&LUR$Pmcw6SQyypAN`%lpS`SdjF^JFyf z>So;D_NE<&mS{aCjl6$zSA}#WmU{d{PT>|nJHK6F*)1}3#B6Q}xfqUlk3PutOiwNA zRbu=G8_$(zCuAM55vW;J4ntWDA*`^0;|Xr#Ar0AT9vCIfA8Acop7WHt!Y{R(T(-gD z=AaMrqc*WR-XnQ9r^+aD=p5@%5&&-sJ61`G(0O z?e~AM1#)uJZ`toYefr_k#;5N-{oCf$as6|6|6k*uzb<^Z*JXPa18>9tC;KsbX9Nqz zBPsxAmwgl;MO0qpnPf9OC>=SWu-QYF$o_80Op0AbEAX@i!S z21!Ila5$BlbIOvpI-OR^9L{10`8LN>OTWLQP2=r>r8%mRD#I{Y?Fgm-5r$F)_$Q1u zQLOZ-+d*{?Tzj~N1r?AUBG86p+AJi-`#E-#$)$jje|JRD2)A3VL`vFm47<6avs7F_ zw@#-)l))7(K&NDY2ormoV5@I`T?O-mXZy-|1<6xD-OIPflFyvpazxuQLc!^Nh@R@5 zSorUeQhtW6Y%LtHS#l7z zie#I0L8xYTfpB^4$lj9ik(c_L!ep#Ll$4QY)|N9qZD?$2%mj8SoTc{*DsJIV#c+Jz4=5#y zE~w+WBu|AM#t*cOeo3fkS%FOL+OtdITLXw@m*W$P8BclvJ zt;oaxsv12WMn-{59NUqIZhU)P`^EV9>&tSKvIT%gv77m!%TE{K;{F`+4pH;pBtck5 zARu+ku3yxOaRvPEyTN<>S`7z3eTsMbPr%w>1M>V;~d>bdMY zK`eb5i%VgLjj_Qmp|L*zS2P}NoWS-$(8(%RM;;Psq|wk-6S?OBG!rq=G42{3=$;(aAYtY1;0Hb0T-H+ zg(62V+2#~uiVCM8*Vfo*8$cPNtcs63x$57`{feo|m>o5&{cm^ZnRpog5}(flU5nw# zjsO}7Xn*J7n(YUxCBu$hjFpA3kOc{goOiqUt(~i#GCf-KpOyIg;%7^aPyhaW{*p(n zTl(26nH6CxM020*G3b|*nF0FV8bc!Au->kH=1qQEB9jlY5Cq=jdTK#ARW;8IN zHFe1AjKYWJ)A}mcZMMD z`#ZjvZxWOK1eAM6pGm~wNyZS)H;3(k8%gbi3Qq-0GP>#1+UKu$*l$|gj;x$&-*FIQLpD4sk?!Fbzj~e_$|wWyV(j?TH%P8IbQXpS88yI}z^%$&@3VSYjiY5*E@? zLm7U#cQ4R^try*CGimX!Q~MmsOQ!wA;kl{Oz0ixbiW)spJRSvNp(MR~dVCEAUP82a zJ<>5G=QPQLL?s6ueTaN;=2S(Zz$HLsf{&jj&)b=SxnsV<@)v;@vCxH7Cn&zqMiGBBbz4`qJx1CkJlaDcOrDY zKs-zHGpXyvebm+`+CZqyu4uuk2UcWNcJEw zgkY1j> zYLXx?@oMG-Uy|9LiVc>BCTBfjZut@}Ul5jcq6p<8Fn_?3vls}4=sj;Pgm+*S(*v3n zCJAo`=U^gNxtp!@UlhBL0~}{deAX;`2hotZr_Su~npFvE6beC!B9mVwt zRl&9?VI z&}s)i8sxdJ>*M_xw03mIe>dpQdjbY}95wOE(?6@ZVY(`=Ns*g|UVFQOkrrdvH=UjH zvdVS3<0cku5eFE6b6Ncl>J;cXao~E3E%&Xgvdl$OcB#^q89MRPp|k_PWggV5+)S?b zwDTidrV8ISU%qSEQb;9yzZp8#Q=2{)(&s&sAo-zga96Mp+MH@Ak7UrD6foh_G?=~S zKo|udnv?FkH;#d$fx4v6_!l8w+AVZ*zX>=jJ~9Ef`M+hD^~g&S?#qMr0u+66Rt-17 z&21K;7%C^~NUJ!6Wk(UHb{{xGC=^g>O})!T-R;ah)h2-|k}vRF0G`W}<#oQGQpu1J zY2u#+5b9#2X%YjNjN?$&Bpp6y3334zqGi1kIK57l82WQ~RoVWS+SSY4?DzVKI` z@#KS_3p|*yzZCO+OX?9ukQenH&|IK;=$`G6lMRg-HX9ZDU4?bzV&e23CHOTyZE^Iy z4ZW5rY+BsU#EurAu%IabOZ) zlS@n9pma7$uGmFP@dg3Z@?w}+P!is4Gwh*Km#n+#ITr!^`~mBk%dkh-^ppG|7h3&^ zeFv~6xG|sLZ!(FM;!doIKi0{YnY^!pxGWo)ZA$^@71#MQbQkDL&97D{^jYlqzi+gVefR%_HfN9@2y29k zP z&!mzD?6hV>cqqU`peh=8IdNqaCsOj*Us~`10o)y&bGZ4RuLsFR z7yr}a>oX3DPKuzL1CF|`geglD*6N7H4!PXXeT;fMpd|6jHcEwlnn1Y!W|~iW7zUqS z4W^5FTEdoC78##qu4C``?tKQnBehJDofMiKl%5(oQy@(La;zx3^P!xWlQGLpmm!&7 zQO>(G;??sSI}N(}MsYFao&r=I1OtVX^_LA`TBV@jo-2A_W1ZOpn!|q zA1|>^rUex}VzV)j0mJz_hm}KTznNma_=)*7ejd)c!O6;;k7toYAG>1P?|Seu_z_KdkGD^lm@WW z5-B#E2SAB1{x%ifQHLfykb4p|Xg4MMm(@2OrJtoocuvZ1{d4Y%V^w>;|6|mDfG}v; z^?4Ce>3r{G)c!r6=g;M#DqG$GdtO`CI3K^I1y-9oJ%tC@j0-T(3`l?5xmRHM{Ujw4 zo_40Js>hOIv&U-5PV}u@ZV7_7HAH@CtLM7P7>;dQE9M1AP%&-gD;Fo{`|%eMYkM1XgNt^O1J zdfVfMsLrZ&ebOle5r1eAD=H`vwC2jhMKe*|I7p8i+Q z^$_D=$dNa^2Yq|fwBEllGu=N&@*f1h^VcsvPpbTdr6vGyRj>)v-tt8I^2&rmEWH@D zl=asCQ6$Ij6n^R{XZG>PKu@}FcS&6$yE4;@mzQ^wM$}|!_c0sO=+P>bFed=a`aUcD zflkh_Pi!3EW2^L+*lmIqv&QaL|4)E>BY7~Dv$htSp3gP>J%wJQ-x-sUF#AI~qexZ~ zshJ16Y&jy--x>C#Bt^S)R>Dg#~DZ>e8Q%}wz>5IzcBrMhg z=A&Moz8Ys>x?$}uN7K?t=F+nCRdd1O^|F~`6+N0IZeV||M3U^C>xk4xIqIMBo$V2W z%|s(V+0PYrrmY5rQ1In;rFa}E!sz1SfDcUCwRKqk`;)(bmh*c1>3|wtT z0Mdt%`r(j|#dXVaL+JOh zsGIlcgC|nn*>F0r`HLFivs3er)ekj}E~gJHxNzV8{dqdB9|rVE?>tvt`N;W*O#u1HR|b2MRfLP5T-a2wHE^bRGupzXj4VM`$$)LVru_rc#Z!lUNL3(GI~_5=|z zX`vT7R+aoA?Z0bhNX|m!oS}S8$(5PseAV!6$h1Xi(~#^~j(GNO&pB4}#7vZA{>Ra{ zjC05RZz#5B&U=xmUvqymf$@r^uUs^CD0uo+$Tu#?7zA;ncSl=k`<{8QG z?Wlg~`yoNt^=9wx(V4MTA1vbaBayLB_>X^zYYFQv6J?$p%f@Hrnd(IRNYjxaqW4Ho zAI&ka2!BdO`bkQfvmrRMj-Cl6On!Hg)%h;L83vIR6&(%>7j>LCr{#b|)h>qBaB;aA-G0p~zg1{-wnR+sf$Z|d;dG>o(#kXy4hnD{ zj2UL9X-)nYHJ6g3XkLkK0WiPgxFbc(9PVrG+nGwVCnLruLRdbhJd4!Y-MvG$o2Ye0h90}k=Kt2nDHz0eX(+N*+LZt-RE)l$ut%?PV4oP(^e!{ zYg|0V9(}odtBnzQ<=)7LWW<1(45xgkyJ)z;dRgk%L5|!zQaUWsUr^+0fLeV>v9GSE z3OuW9)@duj?b?u%ncaE^qNZ*L0PlAVP@pS5Ezz!AoPHd zICAE|Z1m}Ch8JDE3JSTaQLPVEe#ZY@_0ic_wBxTUmwgXCEqU|u%$b13mzQrv>QiIQDp^(HLb z&ztH3g4z#f#aRjW%CJYoo@cTWsCgDdzGwuRVySSLnO_16wwn zzqr(*GuAB?4PiLp>ppIs-4H0nA~sf2n)+5a=0qy`RZ+An1q`@99bLeyabq3S?b%AQ zE0?VzZ>?(6_4y$exkHX~p8jVu{rvGbSH4@F?!CaR;US;^;E+2yk=T1Ev}P?YpkoPT z#Ueg6^yBa!RK%-aR??m>672eQej>aY|9sNYRro*&Wrxk_68ztso{($6&Y;F_94wOqFh_9|S%I z2@saoXXeNA3gd8e3;|F`fL*3Z%j^gei4UVdMPLuWstLX8f&gi8;d8U_QOLM(8nev; zn;nOSuR_TX${vCD0HA)TSJK=Z1G7_RCK1>u7o@-oQ3ms(6UeozXmg09L(llp3dPg0 z;$M;x$d|$QthsiOIIIUImDF99Z9?#T4)EXdlV?99RfAsjqdv#OI}`o|Goif-Q_V=E z#s$DQ!pFHMN)5n^OV$1k$hIFpvl{+8K190zn{02$5y`;0)5@r-%!54iE<~dY2MXzu zHt7eiX&$^q7Eb8p=Xk>zG*3d#oufgHg$oAsk`wJI9l5y~p+z_X4?9{4a7y7ua>u7e z#id$fh`-6KK2VuVeXU0>kVqz%&{rmAY+w&Nf5!hi<>`MEADwn6mYs*5!rr{xDLWn7 z9TCTfE$BybT+S|73%G+L6XnQ8-v?#YU@`H;e!ZehxPm#J8oI+lFCny66fg9rCXLjw z-r(0UW-ecsN)^~jOFlt9eMi_d>H@FlDXo48?7po1R{vCCa%RoZxPy36kAnR(mK4%_ zTf~~s7$<2y619eZBF8PxH9QSnN@Hi~h@BJk1e&ZH7NHLB#zSpySDk(qh&YjOx?Apm zzFVX_3BvxN^j8=s+FuYx)q$6*zC}l+oO{QLjOiGH-UY8W+XIS~7|B-&^S_;GK{##z zWd8GSj1F#^fB65rJMrk?!ebiTiL`q$ccGW=h&8j17K5wK zE^$p@yAyg`#`i&OE{K4w{oHK-2nVGHG&l$02yK8i!+YSggRD7NFHHPAs0-KXpwL1Y z7%!CZHe_`6a*`4t2`~=9K_SvyCNYCa==kDH4}Ao3Lh}K0VBOB{hlUltjrENXc2Ifk z3}vVCCk)shQrXr>;rUpJ5J2UuE|^!vS;oH?(cJ_;(MXu6t73bF`*KFcCCbKYz*!i= zlI$IMJgd~QRJBDoV=DCB?{%Q@WhH^E806^bKpkZx*!iIuT>oL9Sa;6F<-JOMT{ZTM zKlm7(Pq$~WBbGQ)!03jl$VhPWc=f_**Nxjs&3SY#M+^YK6Rmn7>8&4bo4z8)DE%!P|+R5u7T&jXce zrIFuZ^FHH+Gjt%2Z?Iq-Vaj$afbF}E^s;$!g(I=(GG`C5(brB0hdW^I{=HBNS0K=E zd57d?;k0CPM^;-#dWztJobS;7Ja+C5es*C(G}o%ots-D8-IQ4;AVHt5aD63u7)zXdvD<9>Jga$ld( z_wbQlayr4U4JR3mu>SGkyv(=FHp@@r>|tkp^v>$TDxuXb@-hZnn=!yhy&{`@g~Y>c zJ`p^ppxPN`7G0q6boL&xk4ANOKUbD8kQ-yX39E~1+9nxv_KfG5WxJ!8-Z^$idw|>3 zA|3YZ$kM=0z=&6o;b=BVs$%$goPQ~R={i!^Q^lSbPq*qif(~7SRF;<8N`LwBbpQ;8 z?wK-rEo)Qy?5|{F0BT{`tIlqLAcPfj8 zK;#?;4lv9#f_H8#fA~RWS5~3~6c%(a7`arYE&VfC?W0)cx}50!;z4aW*eN)q(GE<# zg0`-Gw?UT78{5Ol^l{)ut>#fLE?TxRh<^rZPJwm;tR-jB4>(;?mNQe$6BRh&SYDnj z@s*WxbK@ zu*ciH2@THnNT-=Vk8kn)KO7NHTiYeiJxN^@m=gP)Y#H~QjHbwgfHX8-0f@i#K$Vk6 zG?iHX?P`dM)VB#VM+XZX*~B_KA`%p&8mClg7in+|I_2J9RjZEpwaIK&=w-db2ch4H_g_Bn{}O8nhIJ_G>PEGA+N3(cm{@!n3BAQsh5e$5~U$+dHGO zQNplso!?fI!zYNg_9dvAtqnPF<>fWtz>UkQc6FOh)Hwhj z(nfv#AN(VJiIx5Cv_n?eE!GV<75_?08uPt%9 zY(C6FCO58)dO<$SEM`=A>mjW7RIl2>J83EjUySU=SYPmyi;k=65x%j=?&Wx}vn<+9 z0t09%hX9>a{brWS0a`={6Umdg-Ooz_Pzp?n3WOp7O~Rn5Onq(=rX?;8W=G}rXz{r872)B&a}!r z(fFiI)ro5>V3im{W^^sEuMwdyg_XDXbtbQkQ<+57rUCQyb}Blpw;AvA7YPnjm#jb( z~)WaoKnq5NheK$3*;^SYq%NfKt+kxbwy+1t0=0pvZXRMRD9 z2~tu-D}LP5v+m8mAD^r$^FCH;uPl|jeaP`=qaC|&=^iy`$DEXmvb^nivW%48w3(Zd zbsk%MvvW`W#O>HxdGy>Fe9~gGw5_LPE-xY<>wDmwUNCW9Pc}851j%79i)+X`VO{%F zm9cJjy2&@^Ngm%89}Zq0bxwF1T#KW$BYiD+2#PXOa@Ze;ey(TIiW;Q zZ}XFu$cIb(E#bun>@UQVq!aBp&v0yN`JkVTv(r2-8Ra2ZHN~UZ<1V-V0wF@zr6lfv zs#joPt}gy#%P<-M`@!|%aQyy-#$hNbYnf-86SWR?{>IAH7? z%x~*Yh*-n>9xGHN1k*8626)Z!V>MqqnB|q+IYlZ~OwMfWNQ$JpxP&=|tonA>4|y}y zhSI%uIAw!owpw@G4eh;7C5m=oLaXL`Q#S4Y005k*Rk1uu*5kXePxTG!l8Vacei zPW&Y-JKHIO${J{ga}OHwgbpCj!l8wb-0>^{+6oQ9x@cI4gXozK;h8yXBIJ@-D1ef> zS`i>SXg-11tVJ7M&`*>d9Ek2CYQCP^VK~`vsPE(6cg4w#h?-tJ`Al;yhB7=+-P}l! zq`eS_8T`kjvD@0FWjdp{2Z^_x0JZ$)%=?>K<*+hhl(oz~fqS8j0ONSqK`1DHGThN- zB_wzYuoP#h9LEB-F|VhUzlm+x4cEg};=WA`aL8cE~+f zK(w6}Zr2jF^gU2kbsVSlbmnC~z9I~p-YEI|^-*tOcNS#_gQ2RD9j7qtmD5tsS6aYhPXupfjq`P~NKRSj zEz6`$k}C=0kf=9HoqfD?R0bAI@^1S!J7$} z8Zy)2LQvo6g+P@|LmX5;AJ$cjU&oH>=mE}RtME`7!_|&P#%%(dcrpT*H&)cym>vMw zJ6WkVJ*lYk3yQ5RCZZBqMPURFSNp{)2;F8Wfm_(*h~#jHS_?`k7>AOj#utM1-Lh6p z$ym4Nu@Rc}LfSC^Sn)M{$Twjd*C@f$;GXU}ofVH`&p_SAadpHa;_|yC0o^1#JPG3` zfN|!PoB3_Pf?+xt$T5D;m%b%qh06)^t2M|fRSU-*Xno}kIOqv+R3;uDzLopt3xwy9 z6|6sF5nD|^u!4hIec?Qot9`3qk9SbLUj*(gADZnSLDVF|E_gjpn5dIlCRqCDtGV~3 zhlVf|ug0ZJPqDdp$RX1_ITTwxTX)OCv*tJ&`bPt9{!7i~-L0=27#@6xcf|LO$NkM4 z8Vdh)dj9SB1Zn1Skgz#7gDc6+;{WNjK5+3z=f(3wjFWr=2Q?b%#x2gT9rg2vdVm%l zfV7UGpGH<&Ya}sL&(>{du?^oejU3E;ZeRZ||yRx@ZxrGoH zgKgH~apCwlw|Tg;9_5B|Y>AW5K4~N94rjw>y(i(32#1FjbYLkZopjV^x$g$zlO~9^vmm*h9{4J*kTSn?&v%r z6s*Jn`B%coiVj5)o|mCb$DwPD!vzq-&5IQwO?Jqrl zPN=R`3Vga5aTp{y?^qI+=G_~it{!`lf2p|evT3|U5e_KZRq-PPVip=EqUyV&tWmp! z39GR@d-Z#E95<1MKmZla2{-bVEgv^2N2tar=tcEZB-h4ToSi>-(ddpn%zGh^feb~N z`JSY-iLG}o72vU9H*SnY*J+!|XQ~N&rB>lvd|p~nj6`_QSC-B3vz63c9%wIFw=46e zf6<6;?adrKm!zxaBbjrj9gkFf6??3rGBUod@Q_1`Bl{Sk58&kJ9ggc@i>;%0*1F+A z<7A#T_6u903!g*s=Jcek z{G`I4SW=X9lNwp)k{aw2e@I zqPrv0-Z3o2`+7PWCo`+zla^>S-ll}X)`<5dt*CNz^IQqda^OsF7y2BMAH!2VR7`ts zhOOAGscECb90^zK1=ehRws&jvS1(PVp&-v8u)hnaR)Zkb`#T=L9 zE+#ntFxUPcT?JJYWw(yrZC20{oS zgb*M>!~`$~1gs1oAoc?!%mKro9S}8v5C+jkKpTf{GZ6v=34<6EH4H{@MA2@vZ4FZd z49X;pii&~=>SL>@Ej@AK`{Dct=hJ!bwQ8;UP@i^P>#lq6+WU9?Mm|9-+q~Qla`GaO zjNUOh)$>*VpJ5lS2|s?9pJ6ua`6T9Tjz%&}+2fI!H7g|TVfBO?+O&Ll3>7|03bt-! zHp85^fpOjI_M7dhiwgdw2~yR2hXWoEH+(_!K{ClMtGJrtUaUlhL+*y5&X`%yY{=X^ zX|;VAIMjqjIe$YJr$HPrUf?iI+$AL=`{peU!|lvbJy6V)R+p_p)A(3#& z?yiVs!k#;`1Uy#Zl$^2&)yNm)D=`ktwq2sOsJO})nZz<#z%sj4N}zs83~b@so5y6{T@SKu{4|N^eY;#6HtQ;G6I20mlOJoq>ffXU9e!@GMb z=&2k7rpz>;1?L8IG4DT)RKUri)E~bGNtG-_F#RXjZFwLq-w%Y|B$9^Dz_H$r4;#** zj;hg`$TGaSx!e{{35Eh<)lfGuYKIqh1zV*2{%j^d z!zJ+Z-uWq>;>}H1VYt`?sK`I}abzydiakE{)GTT+frd2TF=_1z^-)jfJv&TmEfJZ6tuJE2+SX3r>L4-AcFu)qS>^;f`m zO;JL#Z5nvPah#?>GScQ01<^e@`ak-Yy-SLHYyDkC?^Npvh-v??f*A3H_|A*#mgT9} za_(zb$BaItg1+G`v4=c6D-T7@J>9^0AJ{u?l-F;}8R_PTCkG`~h5XJT=-?{Hk5)Ut zd>i1B+ODoMOXIHbe?SHZ=p;D)OGdY$fNd3pJq-7d?@filD2-0|Z!l&!3AUszLOZv&qSn$~X{Ew*JI$j16s?TliVPGu5Ezm= z<-MJT;i6Y~sR?e%lG>fGr;unHS*xI;N`g`q7HG!Wbx`)h_F?@H_@EdeJ%nzsXe&ZP zapdBGayq($5@X(q3mUgj0YPR00o6+d2jki zgNm`-n}sJI2VrYvD=Oo`-y9MM{Kw!{l!<-D^i67VQh*=Cd09mpmr;rLza>iI&g31} zFP5@S+LZKPAq-Vk4mdUj3(!+rVzHCM3FvE@ag|v)`qMem+~aumg~34PJ$%em3mNEj zfS00;PE+^Mdzszha`DUqbMzqmwYdtumS*YZVM3T`{PcpLOXZTVJ>;5IMr^pr#`jSu z7l$nTH$6T{CP@XSs2(%K`mKk^hM-FmUhQtg{Nl;B z)x>POtmA@z2~34V&J9nDIsWP!VJuCFoc$53yD{l3rE7b7LRFfSz6K*?>W@;ie_vk; zS~c_-G|mV0M#s)f4O*i=u^-|Go8k8dIt~1|_WirpQHaqGUAP6BQ(o3$Qvn$V$Z_88H9ONgQ05d@O;j?UZ6Kjg>uF@ggXbtS5UGK;bPcf6 zbjTF9Om-x;=FX`#m+8$zd74Dt{I|v&V#M_>98!l10vpuQf6C7UM1_u`vbWJ+RVFkf z+WCB>wA*3{w1a3EPV7K$SuyI^xt(r{l2OUu_`7L`-DO9{IiD|qZ~QvFre#Xb){_jc zl!q-aM48bR?mr@)zyc-n`e!0Cw{G&Xz^?9yIidwa*hR=TD>>0`@UgCY9LE3ox%07{ z)a>ArlQ6`-wHm!x_&l!M7#0)h$ZpH0(;5+!OfrEVknJiWKy$;*Ux~7xhapDtvJKC0 z_-!WZwae%ITv+Q#s8TVvTv0O?ugX$;x9yCa?ZWP0$)dxmJW!#&mx5 zk()ab_rSfxjzD-;ROAYY9lp}fwE{U2`bd|8Hs92hk`VC}z)agkADW{2g|qqjRsV&zkN@j0DH52Nzs}0%`9g^=9aUdJ$2=;V*4=%Dt!$&t4mzRFk z|0h|S4d;z@{`TGh_Y)RxckI=GdwE%&srt5R&wh8;Rc7UR>Aw6vJzQT@=pMp1i!)T| zCU{V>!gl4cP^WaU0@{^zUlFh4UwWCsPdhyuE#3h&^w5Qcl&A|CxuT%0qOQ(ohP&|| znQ1XS9%XGxo?*D$@0<&KaKKzM9`%+s%vugh-QGeqHT308=-tVk*l8>n#H_J*1oipg ziZh4F{L9kISp5`_)Gspz@vsQwYI35_QU^$Td{10JWfU+Z*A%*IgXyrubvN~m{ zIsWHHfECC>0%SXS-iyp;9MaLIb4hN$M2;>%F zuLh2nCFjOB6u+U|iy*%Kb63%W;&8-%x4H)xgx&tKB?;SsjQ*u4Qv@irzk(&h{HZ;cGG3v3m`$MeJSDZ+Nkze{?D0?H+QSObK$X_!NkW;$qu^0A}bRoHdb8 z6jKOYL@&X$0)+8eji%WsqKsZMIY^VHbwl@2y&p&bj(*fqfI7 z9~eD~LNvuGVfzl#koi&Ze#807Um0VgEEr>7HPnm|)V?~~8BGfs@vN4G(XfXduu_5WUa>8fI3O@3BU0PShggtqOuD-$@zRiGz`j_708Yv-nIdewpu_d3jOjYIlYG zKYpR<-I_|P$XzqjgEAOlxNmDqI3Jzoz9A_nesc%}$bkVt%b3{X{RK}A#Yk3V-HRxf>yvs&K+lznk?QrN&0E;rWMRhG~eY39`93WABYUAsde0pEEw!wuhWsn2ZNs~}!}PWfqf-G6;|V&7jYN-IypG?vfY2dM`Fzl0VC z9+Slttb4GKxY@@NJ0*Z*l52*uWjA9IYx)K_b_62^V@FJtC%{ofcQ{J?qhK+hdsQXu z4#pVj5j*W;+ zj(Lfl+_P4$Kiv7qu<6NG2Q?JuWRA;{OS$()%|L z>+w#`9~cO_r+wB8MeH<5|XqL(2-~_R{jgzg$L}71K^2GfQ+f1K#F7I|3(apZH zbC({gRzh|LDZFp;=SYhiKIlg}66%TtbuQPyJnqIy8+6NfDd&Ltp~0+V z_}7g4wDtuu5Oqhd89GsSC`v4;oEsm~sg~Uy>Ykw)ACmkA=em0mzUu#r$H0 zi;g@F#zjn5t4KX#SbK~Zb3F@ntZ6pUy~A;PEV1&?rD|vXAi+f#-pf(4HM>ctT-hO= zup&oL0U|Au7r$*(%!Eo)v2NiHH?N|JpPkZ_zyIvy5OSd;a`oae>y|zu7pq6>-Ky)4nTnrFg`?kzINk#b;*%uu2Vj`jc>gtFqP6sPbc=V4GejRZ9 z)wXkoQcQL`<8`j_p?@(v)qR0-b8G7CIoF?#|GrPu9FTPF$YDGX*YKf)xSv{ba`DYk z-4RoR!XAwuBu+kclYlQ;RAk~bEOUVr2-f&x8Wb(t2@w)nQDG9HI`aJhtm>{1dk1=G zA9ns9w6v`{mGwbYy4I%6)MUnx^ok@19}9!(U_-{cI9 zBSi@$P$!;-my5L#tP~wM+NKFQHH1Clb$wMi-=i2}jVNO3aff*pQE-S0y@Hx@e8eoC z^C@xDEJq^HOK!t#rNo9j_`?sM?Fk&Ve?)*EbuR;y>0QYNyK{OauMfbZ>z{L>r>*}x zip54eGr@ugZ1)^1q)$+pwjSgMg^^Mk)YTRuY0FBuc*E=}Vqv72wjK+KCr@5Z>pDZ~ z+Wm_%Kp?ZS{Hp&xNqb4nhl}6E7}73p6}-(+V+jBDl?8P2HTIwNaswEims(Rm**-rd z+NqsI&q&ks#Fs0`M_qn284D>#!y#)N-QxMW*_Q!706N3uKDxFm-@KAO2%H^m-B<^> zH&>jv4mmWkO1tIF{5oScIH^=66?)=?U9+m}tlNmm582j;D~gP17DKUkFKZh13r|gXfr$VV*F#R>5tIsl z9`j+@u6L2awbD;fBUc5FqhdV(0^khNZ5VcTk&I8dEXoYr-={Vp(#}-pq@KU61a!ws z4JWK;@yYKL?@x5EL<*O!mPh$Vr3objO3L155o6#8o0U9T23{jbd}>I z%AOlx6$Og5M)jv`$*63ElI{JycNl*p^L#jjm+q!u13T`ll3{+h9abaUhn?;5O-s-* z2eRhEM&K6l0aG%QuSTWMy9G$PuN}$dS?lq-694L>gokw+?TuN2v;8%uI4#|gMpL(4 zi`J%Ev>dK$v8g_x0j`xTBlr=Et?H_1GL0iC#LcnLy{yE`)-KT0sUtrhie$d$u@gk+$1`3u6Ai>66O?wXy=v-L*rUe&h ztrRdCGkc|+dgG9-nwE}Mh(ivaf)5-Rx{vnU-AsrJ5X2PK6e~-R3T`k-kqPJ1!{3h+ zv&^O8R(S7-Vr@-z0!VA4u6PmGT2QqGJPhG)GUQWOiSwOf-3l1Ev_LOEW%2?w@%d$W)NDifJJ{&lqXT|!gnBQRqJ(sc1v24%E zbrDb&e%S0A2Cv;$Q%jFd?FQa`1V6yL*FtmYJL#|$;3+Vj(CwBG25z>TM#nkPen7Bn z-@O9=>zsfkZ(?)^d%MzZ9zzq|){!}HPA|l{!)tW6F~`!Qa@BQxc80}c^?YI8zqF!l zB8#B9tbp$}X@IGH`o*?;VR}~GJf0TqX93=A9u1XSrDg@>GRMPgU2ri`^}gFZ2OHAX zpW_sU)Boi3(5k;#R&MBL&L4;%q+O=TS%yV?-OL-}cJxXjwN@aA(*}zut2BX^1>i9M zXsQcWjDZ8hl&}zzHrghO!p=0A0}7g4zm%tRN*&u=GV#!ra9;GA0O~X8@stbD>%S5{#SIS<-} zeV+acd6|c`Uc2BhP+Xq%<8+J-(+mBvVU-Sd?QKWQaNRoknd`qFT$+7OZ%xTMPn27^ zonP}CiMkzr1nd-NCZ1(qInx=wvrXI}^@?2yg#&;)T8=niApK@VR?Xd(x3eGFMi^x2 zx7%gkvR2>2Dml0FCkC$OSh;A%HfmzSiGdxDB!^CFOoq-)PP^Z$x16M1tDVuf#Wk;4 zmN?(;dhASa8T8m-UBzjeK0;hp-6aC6zrNzwSxED7!d}P)t7BQaR`=z85t8NjhA+fL z{NS+LT^@Wpsnc~s8na3wm95+8qf79F0rmuazp9!5cC0Kgz*(|8pHrv9koHAop8ydx zh|R`&@EfUb&^7?;9~`t*;yD1$ldm}%Cp?`qI=+AD*6(W? zP4HZq=6zCasbLNCk6PaNanbi1+~b?$_rIFVX_?|TUVOAVfUk~IS%+?SHs$+oe_onL zO1kbO|LWS)dP-%r`(FNh>#g39;7~0VAPs-qiT)6yoLv6G7>`98IXL5up1Gd6LKQ8Z z+cjxt^fp|%%~@sif@AwKHyN2B&D|Q9(f5mQQ*-~WX^TgkEcE&i!t=Y;<`~ueNZ*E! zcXqJWfn83zD$lz6CdyRYO`P_$zmlL^=X$i|$Lz+WLjuaxKn+*r`Cs~8q|>)PetLKz z<63T4qiHG*-Gg90P~I^xa$P%h)3z0{S9Z9ZA!h!W?do4y9CqEgPU!^l7>$XB(6m?l z_ocpyZYmnSdNydU;Ly7e55c5F5n4OVfuv&DNNOQ@qK_9KI&El5pR9v+422PnQ<(;JJhUuDx{! z${AYo@1~3U&MOjM<9r`eU4xoGGi(}JQH>dNk<}<{=vUe=b`1BCE}Dx=0#v`={BG>^ zOUxS5@GIEbi-$hf2LubpzRM2@-3>(nuh8Z6_`cL1 zOvpS|^7g)BkHVlQLUIESMh>U>!d961=1RnXYP6LORwvlm=K#VX>@ckmI1D-0BRog` z;;GCu(>YPorD@?}tSu3MW6)RvFsBl#naBXHAyrN`Ss^xtf7SmU$m+^GQ~sM(wBKv; z!P^~vBphqG9}5xl@XA#*sfW}qLilp%0zIEJ6vwI^<%S7JYxZaz_B~nu^7~r9&uVX` zP)-J-Tc1hN6W3n+@W@@VBl~Zip`bj+XfvR{&Dn4uvnM2ZW^3uvIukYU{K3Op8}E?< zPMkWuv`Xm*@GL;t>1YuS_j)D*6zCqLh+A$yJu$jJ&%!q&21n}2zF{ru~# zx(jy}v$=5ma$tj-kMC@%c(w?7r#0{E7P4%q6E4C)pC+B({C792w95`{s^bn>IcNw2 z*y0$JYecJ*5ByT1$64e(2Wv( zSF?rW0*qp9EL@~5Qb)_1q^HsBG-jE0thn3V(pcKUJTtVn$E@tA&3O9rX9}@`nxT0w z7Ha)37l)l#UbhP+9O^$lr0YRT)H(u|5ZIHJd##a0vGqnG6{kUT3GUpZ0dd4(BHP&=F`(h>Ucxmh$q)Vfw;Zxuf$Q) zv14gzQ2kZ^d{E%j+iY#6aeTESsS(rkj{!*)V9=mR*_Y` zz=)njif;(3ibUC+9)gddVK8~3=th_>Om>KmccNGdeoP&g{Bc<4U^-mGA#kU^6LMh3@8F0ypVG)>$lL6>`pxk2JlcRWRSc?cc_`qF`wJT!f?i?b#-ircpKovZ543{65NI5lPk|C_+3xplo_STQ9B@5}rl|a)8RA6rPMZJJW19s! zoYP44b64JPX)@Vpc{(w&hK9@;W^y<+-~NzPB6Cy>4tMY3b-U0y?{QguGtz6V9*8$t z4Nf0)bia+y?%+0aH(CL`46}AeGjai&V?N=H?lqUOt=gk|!p1ym>jVnbz8u?Z zM;8);50jW*zVdQY&tp1lGNeUb;Do{;|F~K-e&|$7bc48hI1|u6Byr zy!nzb(qfUxPcxa8!u#4lAjBTOyxcK*6Q1DRVbP7|;v`7Pven!IHLLi#`n5eD`R6IG zIk1N7;mB;Xte|Tk-6&-;{nFvRB#9OBE@orvPi4pbY~DoNzp>5X9ch+Z^t|UtYw78c zpWO5&C*_P&{JbDRYG(Xh>G+@b8i>9?F9M$Nwup_QvW}SL}8d zF#s@47d(_l=6WYX9M%P9p88C1+6Zkm`-4+-592z(K%oKzq(0FZ)lKmH**Px0ASzw= zHhf!#SyW|B%}O*s*TmoYVPnV5R_Ybvq<_}==$gJ8^U5mm)X9#fw-@4D54Y}cSq1ud zRkeM)xPFtVy)@&@Y|&Nkyt`XBcv)VbsJ$|k2oFp5wmEq8FOv6I)_CkDWtYvL-oNcq zYwIp{xv4)gM>IZ3iIHf{nO7`(!pmS) setOwn(e.target.checked)} style={{ width: 'auto' }} /> Own dice - - {snd && { setVol(+e.target.value); sound.setVolume(+e.target.value) }} onPointerUp={() => playRoll(1)} />} + + {snd && { setVol(+e.target.value); sound.setVolume(+e.target.value) }} onPointerUp={() => playRoll(20, 1, undefined, sfx)} />} {isDm && !to && ( @@ -498,6 +498,49 @@ function CharTab({ cid, rules, onRoll, tick }) { } // lobby settings: join code, rename, colour, character switch, members, leave / delete +// DM: replace the dice sound, add sounds for critical results, switch cues on or off +function SoundSettings({ lobby, id, act, sfx }) { + const sn = lobby.sounds + const [err, setErr] = useState('') + const upload = async (slot, e) => { + const f = e.target.files?.[0] + e.target.value = '' + if (!f) return + setErr('') + const r = await fetch(`/api/lobbies/${id}/sounds/${slot}`, { method: 'POST', headers: { 'Content-Type': 'audio/mpeg' }, body: f }).catch(() => null) + if (!r || !r.ok) return setErr(r ? ((await r.json().catch(() => ({}))).detail || 'Upload failed') : 'Upload failed') + act() + } + const flag = (k, v) => act(`/api/lobbies/${id}/sounds`, 'PUT', { [k]: v }) + const row = (slot, label, defaultText, flagKey) => ( +
+
+ {flagKey && } + {!flagKey && {label}} +
+
+ {sn.custom[slot] ? 'Your file' : defaultText} + + + + {sn.custom[slot] && } + +
+
+ ) + return ( +
+

Sounds

+

For everyone in this lobby. MP3 up to 1.5 MB; players can still mute the sounds for themselves.

+ {row('roll', 'Dice sound', 'Default sound', null)} + + {row('crit_success', 'Critical success', 'Built-in tone', 'crit_success')} + {row('crit_failure', 'Critical failure', 'Built-in tone', 'crit_failure')} + {err &&

{err}

} +
+ ) +} + function Settings({ lobby, id, mine, act, setInfo, onClose, inline }) { const [rename, setRename] = useState('') const [pick, setPick] = useState('') @@ -604,10 +647,17 @@ function LobbyView({ id, rules, user }) { return () => clearInterval(t) }, [load, loadMap, up]) + const sfx = useRef({}) + const sn = lobby?.sounds + sfx.current = sn ? { roll: sn.custom.roll ? `/api/lobbies/${id}/sounds/roll?v=${sn.v}` : DEFAULT_ROLL, + crit_success: sn.custom.crit_success ? `/api/lobbies/${id}/sounds/crit_success?v=${sn.v}` : null, + crit_failure: sn.custom.crit_failure ? `/api/lobbies/${id}/sounds/crit_failure?v=${sn.v}` : null, + result: sn.result, crit_success_on: sn.crit_success, crit_failure_on: sn.crit_failure } : { roll: DEFAULT_ROLL } + useEffect(() => { preload(sfx.current) }, [sn?.v]) // eslint-disable-line const onRoll = useCallback((m) => { const key = `${m.id}` - if (m.kind === 'check') playRoll(1, m.check.outcome) - else if (m.kind === 'roll') playRoll(m.roll.n) + if (m.kind === 'check') playRoll(100, 1, m.check.outcome, sfx.current) + else if (m.kind === 'roll') playRoll(m.roll.sides, m.roll.n, undefined, sfx.current) setTray((t) => [...t.slice(-2), { ...m, key }]) setTimeout(() => setTray((t) => t.filter((x) => x.key !== key)), 20000) }, []) @@ -695,7 +745,7 @@ function LobbyView({ id, rules, user }) { {(!lobby.is_dm || leftTab === 'sheet') && } {lobby.is_dm && leftTab === 'lobby' - ? <> + ? <> : cur ? :

{lobby.is_dm ? 'Pick a character in the bar to look at its sheet.' : 'You have no character in this lobby.'}

} @@ -737,7 +787,7 @@ function LobbyView({ id, rules, user }) { {pending && { setPending(null); setMErr('') }} err={mErr} />} {!pending && mErr &&

{mErr}

} - +