commit 27e8c9a6fe80c876bdff67403c73199d81abce57 Author: swampdaddy Date: Fri Mar 13 16:01:39 2026 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/client/dist/assets/index-DLWfvwrg.css b/client/dist/assets/index-DLWfvwrg.css new file mode 100644 index 0000000..3dfcd93 --- /dev/null +++ b/client/dist/assets/index-DLWfvwrg.css @@ -0,0 +1 @@ +@import"https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Inter:wght@400;500;600&display=swap";:root{--bg-dark: #0d0f14;--bg-card: #1a1d26;--bg-surface: #14161e;--bg-hover: #1f2233;--text-primary: #e8e6e3;--text-secondary: #9a9a9a;--text-muted: #666;--accent-gold: #d4a843;--accent-glow: #f0d060;--border: #2a2d3a;--danger: #cc4422;--success: #44aa55;--radiance: #f0d060;--tide: #4499dd;--shadow: #8a6098;--flame: #ee5533;--growth: #55aa44}*{margin:0;padding:0;box-sizing:border-box}body{font-family:Inter,sans-serif;background:var(--bg-dark);color:var(--text-primary);min-height:100vh;overflow:hidden}.app{width:100vw;height:100vh;display:flex;flex-direction:column}.title-screen{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;background:radial-gradient(ellipse at center,#1a1530 0%,var(--bg-dark) 70%)}.game-title{font-family:Cinzel,serif;font-size:4.5rem;font-weight:700;background:linear-gradient(135deg,var(--accent-gold),#fff8e0,var(--accent-gold));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;text-shadow:none;letter-spacing:4px;margin-bottom:8px}.game-subtitle{font-size:1.1rem;color:var(--text-secondary);margin-bottom:48px;letter-spacing:2px}.name-form{display:flex;flex-direction:column;align-items:center;gap:16px}.name-input{width:320px;padding:14px 20px;font-size:1.1rem;border:2px solid var(--border);border-radius:8px;background:var(--bg-surface);color:var(--text-primary);outline:none;transition:border-color .2s;text-align:center;font-family:Inter,sans-serif}.name-input:focus{border-color:var(--accent-gold)}.btn{padding:10px 24px;font-size:.95rem;font-weight:600;border:2px solid var(--border);border-radius:6px;background:var(--bg-surface);color:var(--text-primary);cursor:pointer;transition:all .15s;font-family:Inter,sans-serif}.btn:hover{background:var(--bg-hover);border-color:var(--accent-gold)}.btn:disabled{opacity:.4;cursor:not-allowed}.btn-primary{background:linear-gradient(135deg,#3a3020,#2a2518);border-color:var(--accent-gold);color:var(--accent-glow)}.btn-primary:hover{background:linear-gradient(135deg,#4a4030,#3a3020)}.btn-small{padding:6px 14px;font-size:.8rem}.btn-back{border:none;background:none;color:var(--text-secondary);padding:8px 12px}.btn-back:hover{color:var(--text-primary);background:none}.btn-join{padding:8px 20px;font-size:.85rem}.btn-pass{background:linear-gradient(135deg,#1a2a1a,#152015);border-color:var(--success);color:#8d8}.btn-target{background:linear-gradient(135deg,#2a1a1a,#201515);border-color:var(--danger);color:#e88;animation:pulse-border 1s infinite}@keyframes pulse-border{0%,to{box-shadow:0 0 #c426}50%{box-shadow:0 0 0 4px #cc44221a}}.btn-start{width:100%;padding:16px;font-size:1.1rem;margin-top:24px}.lobby{height:100vh;display:flex;flex-direction:column;overflow-y:auto}.lobby-header{display:flex;justify-content:space-between;align-items:center;padding:16px 24px;border-bottom:1px solid var(--border);background:var(--bg-surface)}.game-title-small{font-family:Cinzel,serif;font-size:1.5rem;color:var(--accent-gold)}.player-badge{color:var(--text-secondary);font-size:.9rem}.lobby-content{flex:1;max-width:700px;margin:0 auto;padding:32px 24px;width:100%}.create-room{margin-bottom:40px}.create-room h3,.room-list h3{font-family:Cinzel,serif;font-size:1.2rem;color:var(--accent-gold);margin-bottom:16px}.create-room-form{display:flex;gap:12px}.room-input{flex:1;padding:12px 16px;border:2px solid var(--border);border-radius:6px;background:var(--bg-surface);color:var(--text-primary);font-size:.95rem;outline:none;font-family:Inter,sans-serif}.room-input:focus{border-color:var(--accent-gold)}.room-item{display:flex;justify-content:space-between;align-items:center;padding:16px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);margin-bottom:8px;transition:border-color .2s}.room-item:hover{border-color:var(--accent-gold)}.room-info{display:flex;flex-direction:column;gap:4px}.room-name{font-weight:600}.room-meta{font-size:.85rem;color:var(--text-secondary)}.no-rooms{padding:40px;text-align:center;color:var(--text-muted);border:1px dashed var(--border);border-radius:8px}.room-view{max-width:800px;margin:0 auto;padding:32px 24px}.room-header{display:flex;align-items:center;gap:16px;margin-bottom:32px}.room-header h2{font-family:Cinzel,serif;color:var(--accent-gold)}.room-players{margin-bottom:32px}.room-players h3{font-family:Cinzel,serif;color:var(--accent-gold);margin-bottom:12px}.room-player{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border:1px solid var(--border);border-radius:6px;margin-bottom:8px;background:var(--bg-surface)}.room-player.ready{border-color:var(--success)}.room-player.waiting{color:var(--text-muted);border-style:dashed;justify-content:center}.player-status{font-size:.85rem;color:var(--text-secondary)}.deck-selection h3{font-family:Cinzel,serif;color:var(--accent-gold);margin-bottom:16px}.deck-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}.deck-card{display:flex;flex-direction:column;align-items:center;gap:8px;padding:24px 16px;border:2px solid var(--border);border-radius:10px;background:var(--bg-surface);cursor:pointer;transition:all .2s;text-align:center}.deck-card:hover{transform:translateY(-2px);box-shadow:0 4px 20px #0000004d}.deck-card.selected{border-color:var(--accent-gold);box-shadow:0 0 20px #d4a8434d}.deck-card.color-radiance:hover,.deck-card.color-radiance.selected{border-color:var(--radiance);box-shadow:0 0 20px #f0d06033}.deck-card.color-tide:hover,.deck-card.color-tide.selected{border-color:var(--tide);box-shadow:0 0 20px #49d3}.deck-card.color-shadow:hover,.deck-card.color-shadow.selected{border-color:var(--shadow);box-shadow:0 0 20px #8a609833}.deck-card.color-flame:hover,.deck-card.color-flame.selected{border-color:var(--flame);box-shadow:0 0 20px #e533}.deck-card.color-growth:hover,.deck-card.color-growth.selected{border-color:var(--growth);box-shadow:0 0 20px #5a43}.deck-symbol{font-size:2.5rem}.deck-name{font-family:Cinzel,serif;font-weight:600;font-size:1rem;color:var(--text-primary)}.deck-desc{font-size:.8rem;color:var(--text-secondary)}.game-board{display:flex;flex-direction:column;height:100vh;background:radial-gradient(ellipse at center,#111520 0%,var(--bg-dark) 100%)}.game-top-bar{display:flex;align-items:center;gap:12px;padding:6px 12px;background:var(--bg-surface);border-bottom:1px solid var(--border);z-index:10}.phase-tracker{display:flex;gap:2px;flex:1;justify-content:center;flex-wrap:wrap}.phase-pip{padding:3px 8px;font-size:.7rem;border-radius:3px;color:var(--text-muted);background:transparent;transition:all .2s}.phase-pip.active{background:var(--accent-gold);color:#000;font-weight:700}.phase-pip.active.my-turn{background:var(--success)}.turn-info{font-size:.8rem;color:var(--text-secondary);white-space:nowrap}.opponent-area,.my-area{flex:1;display:flex;flex-direction:column;min-height:0;padding:4px 12px}.opponent-area{border-bottom:1px solid rgba(255,255,255,.05)}.player-info{display:flex;align-items:center;gap:16px;padding:6px 12px;font-size:.85rem;flex-shrink:0}.life-total{font-size:1.2rem;font-weight:700;color:#e55}.hand-count,.library-count{color:var(--text-secondary);font-size:.8rem}.mana-pool{display:flex;gap:8px;margin-left:auto}.mana{font-size:.85rem;font-weight:600;padding:2px 6px;border-radius:4px;background:#ffffff14}.mana-radiance{color:var(--radiance)}.mana-tide{color:var(--tide)}.mana-shadow{color:var(--shadow)}.mana-flame{color:var(--flame)}.mana-growth{color:var(--growth)}.mana-colorless{color:var(--text-secondary)}.battlefield{flex:1;display:flex;flex-direction:column;gap:4px;min-height:0;overflow-x:auto}.battlefield.opponent{flex-direction:column-reverse}.bf-row{display:flex;gap:6px;align-items:center;min-height:36px;flex-wrap:wrap}.bf-creatures{flex:1;justify-content:center;align-content:center}.bf-lands,.bf-others{justify-content:center}.bf-empty{color:var(--text-muted);font-size:.8rem;font-style:italic}.bf-creature-slot{position:relative}.bf-creature-slot.attacking .card{transform:translateY(-8px);box-shadow:0 0 12px #ee553380}.bf-creature-slot.blocking .card{box-shadow:0 0 12px #4499dd80}.combat-badge{position:absolute;top:-4px;right:-4px;padding:2px 6px;border-radius:4px;font-size:.6rem;font-weight:700;z-index:5}.attack-badge{background:var(--flame);color:#fff}.block-badge{background:var(--tide);color:#fff}.combat-zone{flex-shrink:0;min-height:0}.combat-controls{display:flex;align-items:center;justify-content:center;gap:12px;padding:8px;background:#d4a84314;border-top:1px solid rgba(212,168,67,.2);border-bottom:1px solid rgba(212,168,67,.2);font-size:.85rem}.hand-area{flex-shrink:0;background:#0000004d;border-top:1px solid var(--border);padding:6px 12px 8px;display:flex;align-items:center;gap:8px}.hand{display:flex;gap:4px;overflow-x:auto;flex:1;padding:4px 0}.hand-slot,.hand-actions{flex-shrink:0}.card{width:130px;min-height:180px;border:2px solid var(--card-border, var(--border));border-radius:8px;background:var(--card-bg, var(--bg-card));display:flex;flex-direction:column;overflow:hidden;cursor:pointer;transition:all .15s;position:relative;font-size:.75rem}.card:hover{transform:translateY(-4px);box-shadow:0 6px 20px #0006;z-index:10}.card.card-small{width:100px;min-height:130px;font-size:.65rem}.card.tapped{transform:rotate(90deg);margin:10px 15px}.card.tapped:hover{transform:rotate(90deg) translateY(-4px)}.card.selected{box-shadow:0 0 0 3px var(--accent-gold),0 0 20px #d4a84366;transform:translateY(-8px)}.card.selectable{cursor:pointer}.card.selectable:hover{box-shadow:0 0 0 2px var(--accent-gold)}.card.summoning-sick{opacity:.75}.card-facedown{width:100px;min-height:130px;border:2px solid var(--border);border-radius:8px;background:linear-gradient(135deg,#1a1530,#2a1a40);display:flex;align-items:center;justify-content:center}.card-back{font-family:Cinzel,serif;font-size:.7rem;color:var(--accent-gold);text-align:center;transform:rotate(-30deg);opacity:.6}.card-header{display:flex;justify-content:space-between;align-items:flex-start;padding:6px 8px 4px;gap:4px}.card-name{font-weight:700;font-size:.7em;line-height:1.2;flex:1}.card-cost{font-size:.75em;white-space:nowrap;flex-shrink:0}.card-art{display:flex;align-items:center;justify-content:center;padding:0;flex:1;background:#00000026;min-height:40px;overflow:hidden}.card-type-line{padding:3px 8px;font-size:.6em;color:inherit;opacity:.7;border-top:1px solid rgba(0,0,0,.15);border-bottom:1px solid rgba(0,0,0,.15)}.card-text{padding:4px 8px;flex:0;font-size:.6em;line-height:1.3}.card-keywords{font-weight:700;font-style:italic;margin-bottom:2px}.card-flavor{font-style:italic;opacity:.6;margin-top:2px;font-size:.9em}.card-pt{padding:4px 8px;text-align:right;font-weight:700;font-size:.9em}.card-pt .damaged{color:var(--danger)}.modal-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#000000b3;display:flex;align-items:center;justify-content:center;z-index:100}.modal{background:var(--bg-surface);border:2px solid var(--accent-gold);border-radius:12px;padding:32px;max-width:400px;text-align:center}.modal h3{font-family:Cinzel,serif;color:var(--accent-gold);margin-bottom:20px}.x-picker{display:flex;align-items:center;justify-content:center;gap:20px;margin-bottom:20px}.x-picker button{width:40px;height:40px;border-radius:50%;border:2px solid var(--border);background:var(--bg-card);color:var(--text-primary);font-size:1.2rem;cursor:pointer}.x-value{font-size:2rem;font-weight:700;color:var(--accent-gold);min-width:40px}.modal-actions{display:flex;gap:12px;justify-content:center}.mana-choice-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.mana-choice-btn{padding:12px 16px!important;font-size:1rem!important}.mana-choice-btn.color-radiance{border-color:var(--radiance)!important;color:var(--radiance)!important}.mana-choice-btn.color-tide{border-color:var(--tide)!important;color:var(--tide)!important}.mana-choice-btn.color-shadow{border-color:var(--shadow)!important;color:var(--shadow)!important}.mana-choice-btn.color-flame{border-color:var(--flame)!important;color:var(--flame)!important}.mana-choice-btn.color-growth{border-color:var(--growth)!important;color:var(--growth)!important}.mana-choice-btn.color-colorless{border-color:var(--text-secondary)!important}.target-banner{position:fixed;top:50px;left:50%;transform:translate(-50%);background:#cc4422e6;color:#fff;padding:10px 20px;border-radius:8px;display:flex;align-items:center;gap:12px;font-weight:600;z-index:50;animation:slide-down .2s ease-out}@keyframes slide-down{0%{transform:translate(-50%) translateY(-20px);opacity:0}to{transform:translate(-50%) translateY(0);opacity:1}}.game-log-panel{position:fixed;right:0;top:0;bottom:0;width:320px;background:var(--bg-surface);border-left:1px solid var(--border);z-index:50;display:flex;flex-direction:column}.log-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--border)}.log-header h3{font-family:Cinzel,serif;color:var(--accent-gold)}.log-entries{flex:1;overflow-y:auto;padding:8px}.log-entry{padding:6px 8px;border-bottom:1px solid rgba(255,255,255,.03);font-size:.8rem;display:flex;gap:8px}.log-turn{color:var(--accent-gold);font-weight:600;flex-shrink:0}.log-msg{color:var(--text-secondary)}.game-over-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#000000d9;display:flex;align-items:center;justify-content:center;z-index:200}.game-over-modal{text-align:center;padding:48px}.game-over-modal h1{font-family:Cinzel,serif;font-size:3rem;background:linear-gradient(135deg,var(--accent-gold),#fff8e0);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-bottom:16px}.game-over-modal p{color:var(--text-secondary);margin-bottom:32px;font-size:1.1rem}.error-toast{position:fixed;bottom:24px;left:50%;transform:translate(-50%);background:#cc4422f2;color:#fff;padding:12px 24px;border-radius:8px;font-weight:600;z-index:300;animation:toast-in .2s ease-out}@keyframes toast-in{0%{transform:translate(-50%) translateY(20px);opacity:0}to{transform:translate(-50%) translateY(0);opacity:1}}.card.color-radiance{border-color:var(--radiance)}.card.color-tide{border-color:var(--tide)}.card.color-shadow{border-color:var(--shadow);background:#1a1520;color:#ddd}.card.color-flame{border-color:var(--flame)}.card.color-growth{border-color:var(--growth)}.card.color-colorless{border-color:#888} diff --git a/client/dist/assets/index-FiZX5YT8.js b/client/dist/assets/index-FiZX5YT8.js new file mode 100644 index 0000000..340d6ac --- /dev/null +++ b/client/dist/assets/index-FiZX5YT8.js @@ -0,0 +1,40 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const i of o)if(i.type==="childList")for(const l of i.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&r(l)}).observe(document,{childList:!0,subtree:!0});function n(o){const i={};return o.integrity&&(i.integrity=o.integrity),o.referrerPolicy&&(i.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?i.credentials="include":o.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(o){if(o.ep)return;o.ep=!0;const i=n(o);fetch(o.href,i)}})();function mf(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Va={exports:{}},Mo={},Wa={exports:{}},D={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var gr=Symbol.for("react.element"),yf=Symbol.for("react.portal"),gf=Symbol.for("react.fragment"),vf=Symbol.for("react.strict_mode"),kf=Symbol.for("react.profiler"),Sf=Symbol.for("react.provider"),wf=Symbol.for("react.context"),Cf=Symbol.for("react.forward_ref"),_f=Symbol.for("react.suspense"),Ef=Symbol.for("react.memo"),Nf=Symbol.for("react.lazy"),Cs=Symbol.iterator;function Pf(e){return e===null||typeof e!="object"?null:(e=Cs&&e[Cs]||e["@@iterator"],typeof e=="function"?e:null)}var Ha={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Qa=Object.assign,Ga={};function Nn(e,t,n){this.props=e,this.context=t,this.refs=Ga,this.updater=n||Ha}Nn.prototype.isReactComponent={};Nn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Nn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Ka(){}Ka.prototype=Nn.prototype;function _l(e,t,n){this.props=e,this.context=t,this.refs=Ga,this.updater=n||Ha}var El=_l.prototype=new Ka;El.constructor=_l;Qa(El,Nn.prototype);El.isPureReactComponent=!0;var _s=Array.isArray,Ya=Object.prototype.hasOwnProperty,Nl={current:null},ba={key:!0,ref:!0,__self:!0,__source:!0};function qa(e,t,n){var r,o={},i=null,l=null;if(t!=null)for(r in t.ref!==void 0&&(l=t.ref),t.key!==void 0&&(i=""+t.key),t)Ya.call(t,r)&&!ba.hasOwnProperty(r)&&(o[r]=t[r]);var a=arguments.length-2;if(a===1)o.children=n;else if(1>>1,X=L[W];if(0>>1;Wo(T,I))Mo(re,T)?(L[W]=re,L[M]=I,W=M):(L[W]=T,L[Qe]=I,W=Qe);else if(Mo(re,I))L[W]=re,L[M]=I,W=M;else break e}}return j}function o(L,j){var I=L.sortIndex-j.sortIndex;return I!==0?I:L.id-j.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var l=Date,a=l.now();e.unstable_now=function(){return l.now()-a}}var s=[],u=[],m=1,c=null,f=3,v=!1,y=!1,g=!1,N=typeof setTimeout=="function"?setTimeout:null,p=typeof clearTimeout=="function"?clearTimeout:null,d=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function h(L){for(var j=n(u);j!==null;){if(j.callback===null)r(u);else if(j.startTime<=L)r(u),j.sortIndex=j.expirationTime,t(s,j);else break;j=n(u)}}function k(L){if(g=!1,h(L),!y)if(n(s)!==null)y=!0,On(C);else{var j=n(u);j!==null&&Mn(k,j.startTime-L)}}function C(L,j){y=!1,g&&(g=!1,p(_),_=-1),v=!0;var I=f;try{for(h(j),c=n(s);c!==null&&(!(c.expirationTime>j)||L&&!ue());){var W=c.callback;if(typeof W=="function"){c.callback=null,f=c.priorityLevel;var X=W(c.expirationTime<=j);j=e.unstable_now(),typeof X=="function"?c.callback=X:c===n(s)&&r(s),h(j)}else r(s);c=n(s)}if(c!==null)var xt=!0;else{var Qe=n(u);Qe!==null&&Mn(k,Qe.startTime-j),xt=!1}return xt}finally{c=null,f=I,v=!1}}var P=!1,E=null,_=-1,F=5,A=-1;function ue(){return!(e.unstable_now()-AL||125W?(L.sortIndex=I,t(u,L),n(s)===null&&L===n(u)&&(g?(p(_),_=-1):g=!0,Mn(k,I-W))):(L.sortIndex=X,t(s,L),y||v||(y=!0,On(C))),L},e.unstable_shouldYield=ue,e.unstable_wrapCallback=function(L){var j=f;return function(){var I=f;f=j;try{return L.apply(this,arguments)}finally{f=I}}}})(tu);eu.exports=tu;var Ff=eu.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var $f=z,Te=Ff;function w(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ni=Object.prototype.hasOwnProperty,Uf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Ns={},Ps={};function Rf(e){return Ni.call(Ps,e)?!0:Ni.call(Ns,e)?!1:Uf.test(e)?Ps[e]=!0:(Ns[e]=!0,!1)}function Vf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Wf(e,t,n,r){if(t===null||typeof t>"u"||Vf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function ge(e,t,n,r,o,i,l){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=o,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=l}var ae={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){ae[e]=new ge(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];ae[t]=new ge(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){ae[e]=new ge(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){ae[e]=new ge(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){ae[e]=new ge(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){ae[e]=new ge(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){ae[e]=new ge(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){ae[e]=new ge(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){ae[e]=new ge(e,5,!1,e.toLowerCase(),null,!1,!1)});var Tl=/[\-:]([a-z])/g;function Ll(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Tl,Ll);ae[t]=new ge(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Tl,Ll);ae[t]=new ge(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Tl,Ll);ae[t]=new ge(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){ae[e]=new ge(e,1,!1,e.toLowerCase(),null,!1,!1)});ae.xlinkHref=new ge("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){ae[e]=new ge(e,1,!1,e.toLowerCase(),null,!0,!0)});function Ol(e,t,n,r){var o=ae.hasOwnProperty(t)?ae[t]:null;(o!==null?o.type!==0:r||!(2a||o[l]!==i[a]){var s=` +`+o[l].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=l&&0<=a);break}}}finally{Xo=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Un(e):""}function Hf(e){switch(e.tag){case 5:return Un(e.type);case 16:return Un("Lazy");case 13:return Un("Suspense");case 19:return Un("SuspenseList");case 0:case 2:case 15:return e=Jo(e.type,!1),e;case 11:return e=Jo(e.type.render,!1),e;case 1:return e=Jo(e.type,!0),e;default:return""}}function Oi(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case en:return"Fragment";case Zt:return"Portal";case Pi:return"Profiler";case Ml:return"StrictMode";case Ti:return"Suspense";case Li:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case ou:return(e.displayName||"Context")+".Consumer";case ru:return(e._context.displayName||"Context")+".Provider";case Al:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case jl:return t=e.displayName||null,t!==null?t:Oi(e.type)||"Memo";case yt:t=e._payload,e=e._init;try{return Oi(e(t))}catch{}}return null}function Qf(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Oi(t);case 8:return t===Ml?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function At(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function lu(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Gf(e){var t=lu(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var o=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return o.call(this)},set:function(l){r=""+l,i.call(this,l)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(l){r=""+l},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Nr(e){e._valueTracker||(e._valueTracker=Gf(e))}function su(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=lu(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function io(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Mi(e,t){var n=t.checked;return b({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Ls(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=At(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function au(e,t){t=t.checked,t!=null&&Ol(e,"checked",t,!1)}function Ai(e,t){au(e,t);var n=At(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ji(e,t.type,n):t.hasOwnProperty("defaultValue")&&ji(e,t.type,At(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Os(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ji(e,t,n){(t!=="number"||io(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Rn=Array.isArray;function dn(e,t,n,r){if(e=e.options,t){t={};for(var o=0;o"+t.valueOf().toString()+"",t=Pr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function tr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Qn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Kf=["Webkit","ms","Moz","O"];Object.keys(Qn).forEach(function(e){Kf.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Qn[t]=Qn[e]})});function du(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Qn.hasOwnProperty(e)&&Qn[e]?(""+t).trim():t+"px"}function pu(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,o=du(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,o):e[n]=o}}var Yf=b({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Di(e,t){if(t){if(Yf[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(w(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(w(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(w(61))}if(t.style!=null&&typeof t.style!="object")throw Error(w(62))}}function xi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Bi=null;function zl(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Fi=null,pn=null,hn=null;function js(e){if(e=Sr(e)){if(typeof Fi!="function")throw Error(w(280));var t=e.stateNode;t&&(t=Do(t),Fi(e.stateNode,e.type,t))}}function hu(e){pn?hn?hn.push(e):hn=[e]:pn=e}function mu(){if(pn){var e=pn,t=hn;if(hn=pn=null,js(e),t)for(e=0;e>>=0,e===0?32:31-(id(e)/ld|0)|0}var Tr=64,Lr=4194304;function Vn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function uo(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,o=e.suspendedLanes,i=e.pingedLanes,l=n&268435455;if(l!==0){var a=l&~o;a!==0?r=Vn(a):(i&=l,i!==0&&(r=Vn(i)))}else l=n&~o,l!==0?r=Vn(l):i!==0&&(r=Vn(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&o)&&(o=r&-r,i=t&-t,o>=i||o===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function vr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ve(t),e[t]=n}function cd(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Kn),Rs=" ",Vs=!1;function Du(e,t){switch(e){case"keyup":return Fd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function xu(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var tn=!1;function Ud(e,t){switch(e){case"compositionend":return xu(t);case"keypress":return t.which!==32?null:(Vs=!0,Rs);case"textInput":return e=t.data,e===Rs&&Vs?null:e;default:return null}}function Rd(e,t){if(tn)return e==="compositionend"||!Rl&&Du(e,t)?(e=zu(),Gr=Fl=St=null,tn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Gs(n)}}function Uu(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Uu(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ru(){for(var e=window,t=io();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=io(e.document)}return t}function Vl(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function qd(e){var t=Ru(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Uu(n.ownerDocument.documentElement,n)){if(r!==null&&Vl(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var o=n.textContent.length,i=Math.min(r.start,o);r=r.end===void 0?i:Math.min(r.end,o),!e.extend&&i>r&&(o=r,r=i,i=o),o=Ks(n,i);var l=Ks(n,r);o&&l&&(e.rangeCount!==1||e.anchorNode!==o.node||e.anchorOffset!==o.offset||e.focusNode!==l.node||e.focusOffset!==l.offset)&&(t=t.createRange(),t.setStart(o.node,o.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(l.node,l.offset)):(t.setEnd(l.node,l.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,nn=null,Hi=null,bn=null,Qi=!1;function Ys(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Qi||nn==null||nn!==io(r)||(r=nn,"selectionStart"in r&&Vl(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),bn&&sr(bn,r)||(bn=r,r=po(Hi,"onSelect"),0ln||(e.current=Xi[ln],Xi[ln]=null,ln--)}function V(e,t){ln++,Xi[ln]=e.current,e.current=t}var jt={},pe=It(jt),Se=It(!1),Ht=jt;function kn(e,t){var n=e.type.contextTypes;if(!n)return jt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var o={},i;for(i in n)o[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=o),o}function we(e){return e=e.childContextTypes,e!=null}function mo(){Q(Se),Q(pe)}function ta(e,t,n){if(pe.current!==jt)throw Error(w(168));V(pe,t),V(Se,n)}function qu(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var o in r)if(!(o in t))throw Error(w(108,Qf(e)||"Unknown",o));return b({},n,r)}function yo(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||jt,Ht=pe.current,V(pe,e),V(Se,Se.current),!0}function na(e,t,n){var r=e.stateNode;if(!r)throw Error(w(169));n?(e=qu(e,t,Ht),r.__reactInternalMemoizedMergedChildContext=e,Q(Se),Q(pe),V(pe,e)):Q(Se),V(Se,n)}var tt=null,xo=!1,di=!1;function Xu(e){tt===null?tt=[e]:tt.push(e)}function ap(e){xo=!0,Xu(e)}function Dt(){if(!di&&tt!==null){di=!0;var e=0,t=U;try{var n=tt;for(U=1;e>=l,o-=l,nt=1<<32-Ve(t)+o|n<_?(F=E,E=null):F=E.sibling;var A=f(p,E,h[_],k);if(A===null){E===null&&(E=F);break}e&&E&&A.alternate===null&&t(p,E),d=i(A,d,_),P===null?C=A:P.sibling=A,P=A,E=F}if(_===h.length)return n(p,E),G&&Bt(p,_),C;if(E===null){for(;__?(F=E,E=null):F=E.sibling;var ue=f(p,E,A.value,k);if(ue===null){E===null&&(E=F);break}e&&E&&ue.alternate===null&&t(p,E),d=i(ue,d,_),P===null?C=ue:P.sibling=ue,P=ue,E=F}if(A.done)return n(p,E),G&&Bt(p,_),C;if(E===null){for(;!A.done;_++,A=h.next())A=c(p,A.value,k),A!==null&&(d=i(A,d,_),P===null?C=A:P.sibling=A,P=A);return G&&Bt(p,_),C}for(E=r(p,E);!A.done;_++,A=h.next())A=v(E,p,_,A.value,k),A!==null&&(e&&A.alternate!==null&&E.delete(A.key===null?_:A.key),d=i(A,d,_),P===null?C=A:P.sibling=A,P=A);return e&&E.forEach(function(ct){return t(p,ct)}),G&&Bt(p,_),C}function N(p,d,h,k){if(typeof h=="object"&&h!==null&&h.type===en&&h.key===null&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case Er:e:{for(var C=h.key,P=d;P!==null;){if(P.key===C){if(C=h.type,C===en){if(P.tag===7){n(p,P.sibling),d=o(P,h.props.children),d.return=p,p=d;break e}}else if(P.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===yt&&ia(C)===P.type){n(p,P.sibling),d=o(P,h.props),d.ref=xn(p,P,h),d.return=p,p=d;break e}n(p,P);break}else t(p,P);P=P.sibling}h.type===en?(d=Wt(h.props.children,p.mode,k,h.key),d.return=p,p=d):(k=eo(h.type,h.key,h.props,null,p.mode,k),k.ref=xn(p,d,h),k.return=p,p=k)}return l(p);case Zt:e:{for(P=h.key;d!==null;){if(d.key===P)if(d.tag===4&&d.stateNode.containerInfo===h.containerInfo&&d.stateNode.implementation===h.implementation){n(p,d.sibling),d=o(d,h.children||[]),d.return=p,p=d;break e}else{n(p,d);break}else t(p,d);d=d.sibling}d=Si(h,p.mode,k),d.return=p,p=d}return l(p);case yt:return P=h._init,N(p,d,P(h._payload),k)}if(Rn(h))return y(p,d,h,k);if(An(h))return g(p,d,h,k);Dr(p,h)}return typeof h=="string"&&h!==""||typeof h=="number"?(h=""+h,d!==null&&d.tag===6?(n(p,d.sibling),d=o(d,h),d.return=p,p=d):(n(p,d),d=ki(h,p.mode,k),d.return=p,p=d),l(p)):n(p,d)}return N}var wn=tc(!0),nc=tc(!1),ko=It(null),So=null,un=null,Gl=null;function Kl(){Gl=un=So=null}function Yl(e){var t=ko.current;Q(ko),e._currentValue=t}function el(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function yn(e,t){So=e,Gl=un=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(ke=!0),e.firstContext=null)}function De(e){var t=e._currentValue;if(Gl!==e)if(e={context:e,memoizedValue:t,next:null},un===null){if(So===null)throw Error(w(308));un=e,So.dependencies={lanes:0,firstContext:e}}else un=un.next=e;return t}var Ut=null;function bl(e){Ut===null?Ut=[e]:Ut.push(e)}function rc(e,t,n,r){var o=t.interleaved;return o===null?(n.next=n,bl(t)):(n.next=o.next,o.next=n),t.interleaved=n,st(e,r)}function st(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var gt=!1;function ql(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function oc(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function ot(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Pt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,B&2){var o=r.pending;return o===null?t.next=t:(t.next=o.next,o.next=t),r.pending=t,st(e,n)}return o=r.interleaved,o===null?(t.next=t,bl(r)):(t.next=o.next,o.next=t),r.interleaved=t,st(e,n)}function Yr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Dl(e,n)}}function la(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var o=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var l={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?o=i=l:i=i.next=l,n=n.next}while(n!==null);i===null?o=i=t:i=i.next=t}else o=i=t;n={baseState:r.baseState,firstBaseUpdate:o,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function wo(e,t,n,r){var o=e.updateQueue;gt=!1;var i=o.firstBaseUpdate,l=o.lastBaseUpdate,a=o.shared.pending;if(a!==null){o.shared.pending=null;var s=a,u=s.next;s.next=null,l===null?i=u:l.next=u,l=s;var m=e.alternate;m!==null&&(m=m.updateQueue,a=m.lastBaseUpdate,a!==l&&(a===null?m.firstBaseUpdate=u:a.next=u,m.lastBaseUpdate=s))}if(i!==null){var c=o.baseState;l=0,m=u=s=null,a=i;do{var f=a.lane,v=a.eventTime;if((r&f)===f){m!==null&&(m=m.next={eventTime:v,lane:0,tag:a.tag,payload:a.payload,callback:a.callback,next:null});e:{var y=e,g=a;switch(f=t,v=n,g.tag){case 1:if(y=g.payload,typeof y=="function"){c=y.call(v,c,f);break e}c=y;break e;case 3:y.flags=y.flags&-65537|128;case 0:if(y=g.payload,f=typeof y=="function"?y.call(v,c,f):y,f==null)break e;c=b({},c,f);break e;case 2:gt=!0}}a.callback!==null&&a.lane!==0&&(e.flags|=64,f=o.effects,f===null?o.effects=[a]:f.push(a))}else v={eventTime:v,lane:f,tag:a.tag,payload:a.payload,callback:a.callback,next:null},m===null?(u=m=v,s=c):m=m.next=v,l|=f;if(a=a.next,a===null){if(a=o.shared.pending,a===null)break;f=a,a=f.next,f.next=null,o.lastBaseUpdate=f,o.shared.pending=null}}while(!0);if(m===null&&(s=c),o.baseState=s,o.firstBaseUpdate=u,o.lastBaseUpdate=m,t=o.shared.interleaved,t!==null){o=t;do l|=o.lane,o=o.next;while(o!==t)}else i===null&&(o.shared.lanes=0);Kt|=l,e.lanes=l,e.memoizedState=c}}function sa(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=hi.transition;hi.transition={};try{e(!1),t()}finally{U=n,hi.transition=r}}function wc(){return xe().memoizedState}function dp(e,t,n){var r=Lt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Cc(e))_c(t,n);else if(n=rc(e,t,n,r),n!==null){var o=me();We(n,e,r,o),Ec(n,t,r)}}function pp(e,t,n){var r=Lt(e),o={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Cc(e))_c(t,o);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var l=t.lastRenderedState,a=i(l,n);if(o.hasEagerState=!0,o.eagerState=a,He(a,l)){var s=t.interleaved;s===null?(o.next=o,bl(t)):(o.next=s.next,s.next=o),t.interleaved=o;return}}catch{}finally{}n=rc(e,t,o,r),n!==null&&(o=me(),We(n,e,r,o),Ec(n,t,r))}}function Cc(e){var t=e.alternate;return e===Y||t!==null&&t===Y}function _c(e,t){qn=_o=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ec(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Dl(e,n)}}var Eo={readContext:De,useCallback:ce,useContext:ce,useEffect:ce,useImperativeHandle:ce,useInsertionEffect:ce,useLayoutEffect:ce,useMemo:ce,useReducer:ce,useRef:ce,useState:ce,useDebugValue:ce,useDeferredValue:ce,useTransition:ce,useMutableSource:ce,useSyncExternalStore:ce,useId:ce,unstable_isNewReconciler:!1},hp={readContext:De,useCallback:function(e,t){return Ye().memoizedState=[e,t===void 0?null:t],e},useContext:De,useEffect:ua,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,qr(4194308,4,yc.bind(null,t,e),n)},useLayoutEffect:function(e,t){return qr(4194308,4,e,t)},useInsertionEffect:function(e,t){return qr(4,2,e,t)},useMemo:function(e,t){var n=Ye();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ye();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=dp.bind(null,Y,e),[r.memoizedState,e]},useRef:function(e){var t=Ye();return e={current:e},t.memoizedState=e},useState:aa,useDebugValue:os,useDeferredValue:function(e){return Ye().memoizedState=e},useTransition:function(){var e=aa(!1),t=e[0];return e=fp.bind(null,e[1]),Ye().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Y,o=Ye();if(G){if(n===void 0)throw Error(w(407));n=n()}else{if(n=t(),ie===null)throw Error(w(349));Gt&30||ac(r,t,n)}o.memoizedState=n;var i={value:n,getSnapshot:t};return o.queue=i,ua(cc.bind(null,r,i,e),[e]),r.flags|=2048,mr(9,uc.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Ye(),t=ie.identifierPrefix;if(G){var n=rt,r=nt;n=(r&~(1<<32-Ve(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=pr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=l.createElement(n,{is:r.is}):(e=l.createElement(n),n==="select"&&(l=e,r.multiple?l.multiple=!0:r.size&&(l.size=r.size))):e=l.createElementNS(e,n),e[be]=t,e[cr]=r,Ic(e,t,!1,!1),t.stateNode=e;e:{switch(l=xi(n,r),n){case"dialog":H("cancel",e),H("close",e),o=r;break;case"iframe":case"object":case"embed":H("load",e),o=r;break;case"video":case"audio":for(o=0;oEn&&(t.flags|=128,r=!0,Bn(i,!1),t.lanes=4194304)}else{if(!r)if(e=Co(l),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Bn(i,!0),i.tail===null&&i.tailMode==="hidden"&&!l.alternate&&!G)return fe(t),null}else 2*J()-i.renderingStartTime>En&&n!==1073741824&&(t.flags|=128,r=!0,Bn(i,!1),t.lanes=4194304);i.isBackwards?(l.sibling=t.child,t.child=l):(n=i.last,n!==null?n.sibling=l:t.child=l,i.last=l)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=J(),t.sibling=null,n=K.current,V(K,r?n&1|2:n&1),t):(fe(t),null);case 22:case 23:return cs(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Ee&1073741824&&(fe(t),t.subtreeFlags&6&&(t.flags|=8192)):fe(t),null;case 24:return null;case 25:return null}throw Error(w(156,t.tag))}function Cp(e,t){switch(Hl(t),t.tag){case 1:return we(t.type)&&mo(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Cn(),Q(Se),Q(pe),Zl(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Jl(t),null;case 13:if(Q(K),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(w(340));Sn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Q(K),null;case 4:return Cn(),null;case 10:return Yl(t.type._context),null;case 22:case 23:return cs(),null;case 24:return null;default:return null}}var Br=!1,de=!1,_p=typeof WeakSet=="function"?WeakSet:Set,O=null;function cn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){q(e,t,r)}else n.current=null}function ul(e,t,n){try{n()}catch(r){q(e,t,r)}}var Sa=!1;function Ep(e,t){if(Gi=co,e=Ru(),Vl(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var o=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var l=0,a=-1,s=-1,u=0,m=0,c=e,f=null;t:for(;;){for(var v;c!==n||o!==0&&c.nodeType!==3||(a=l+o),c!==i||r!==0&&c.nodeType!==3||(s=l+r),c.nodeType===3&&(l+=c.nodeValue.length),(v=c.firstChild)!==null;)f=c,c=v;for(;;){if(c===e)break t;if(f===n&&++u===o&&(a=l),f===i&&++m===r&&(s=l),(v=c.nextSibling)!==null)break;c=f,f=c.parentNode}c=v}n=a===-1||s===-1?null:{start:a,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(Ki={focusedElem:e,selectionRange:n},co=!1,O=t;O!==null;)if(t=O,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,O=e;else for(;O!==null;){t=O;try{var y=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(y!==null){var g=y.memoizedProps,N=y.memoizedState,p=t.stateNode,d=p.getSnapshotBeforeUpdate(t.elementType===t.type?g:Fe(t.type,g),N);p.__reactInternalSnapshotBeforeUpdate=d}break;case 3:var h=t.stateNode.containerInfo;h.nodeType===1?h.textContent="":h.nodeType===9&&h.documentElement&&h.removeChild(h.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(w(163))}}catch(k){q(t,t.return,k)}if(e=t.sibling,e!==null){e.return=t.return,O=e;break}O=t.return}return y=Sa,Sa=!1,y}function Xn(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var o=r=r.next;do{if((o.tag&e)===e){var i=o.destroy;o.destroy=void 0,i!==void 0&&ul(t,n,i)}o=o.next}while(o!==r)}}function $o(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function cl(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Bc(e){var t=e.alternate;t!==null&&(e.alternate=null,Bc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[be],delete t[cr],delete t[qi],delete t[lp],delete t[sp])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Fc(e){return e.tag===5||e.tag===3||e.tag===4}function wa(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Fc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function fl(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=ho));else if(r!==4&&(e=e.child,e!==null))for(fl(e,t,n),e=e.sibling;e!==null;)fl(e,t,n),e=e.sibling}function dl(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(dl(e,t,n),e=e.sibling;e!==null;)dl(e,t,n),e=e.sibling}var le=null,$e=!1;function ht(e,t,n){for(n=n.child;n!==null;)$c(e,t,n),n=n.sibling}function $c(e,t,n){if(qe&&typeof qe.onCommitFiberUnmount=="function")try{qe.onCommitFiberUnmount(Ao,n)}catch{}switch(n.tag){case 5:de||cn(n,t);case 6:var r=le,o=$e;le=null,ht(e,t,n),le=r,$e=o,le!==null&&($e?(e=le,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):le.removeChild(n.stateNode));break;case 18:le!==null&&($e?(e=le,n=n.stateNode,e.nodeType===8?fi(e.parentNode,n):e.nodeType===1&&fi(e,n),ir(e)):fi(le,n.stateNode));break;case 4:r=le,o=$e,le=n.stateNode.containerInfo,$e=!0,ht(e,t,n),le=r,$e=o;break;case 0:case 11:case 14:case 15:if(!de&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){o=r=r.next;do{var i=o,l=i.destroy;i=i.tag,l!==void 0&&(i&2||i&4)&&ul(n,t,l),o=o.next}while(o!==r)}ht(e,t,n);break;case 1:if(!de&&(cn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(a){q(n,t,a)}ht(e,t,n);break;case 21:ht(e,t,n);break;case 22:n.mode&1?(de=(r=de)||n.memoizedState!==null,ht(e,t,n),de=r):ht(e,t,n);break;default:ht(e,t,n)}}function Ca(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new _p),t.forEach(function(r){var o=zp.bind(null,e,r);n.has(r)||(n.add(r),r.then(o,o))})}}function Be(e,t){var n=t.deletions;if(n!==null)for(var r=0;ro&&(o=l),r&=~i}if(r=o,r=J()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Pp(r/1960))-r,10e?16:e,wt===null)var r=!1;else{if(e=wt,wt=null,To=0,B&6)throw Error(w(331));var o=B;for(B|=4,O=e.current;O!==null;){var i=O,l=i.child;if(O.flags&16){var a=i.deletions;if(a!==null){for(var s=0;sJ()-as?Vt(e,0):ss|=n),Ce(e,t)}function Kc(e,t){t===0&&(e.mode&1?(t=Lr,Lr<<=1,!(Lr&130023424)&&(Lr=4194304)):t=1);var n=me();e=st(e,t),e!==null&&(vr(e,t,n),Ce(e,n))}function jp(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Kc(e,n)}function zp(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,o=e.memoizedState;o!==null&&(n=o.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(w(314))}r!==null&&r.delete(t),Kc(e,n)}var Yc;Yc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Se.current)ke=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ke=!1,Sp(e,t,n);ke=!!(e.flags&131072)}else ke=!1,G&&t.flags&1048576&&Ju(t,vo,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Xr(e,t),e=t.pendingProps;var o=kn(t,pe.current);yn(t,n),o=ts(null,t,r,e,o,n);var i=ns();return t.flags|=1,typeof o=="object"&&o!==null&&typeof o.render=="function"&&o.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,we(r)?(i=!0,yo(t)):i=!1,t.memoizedState=o.state!==null&&o.state!==void 0?o.state:null,ql(t),o.updater=Fo,t.stateNode=o,o._reactInternals=t,nl(t,r,e,n),t=il(null,t,r,!0,i,n)):(t.tag=0,G&&i&&Wl(t),he(null,t,o,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Xr(e,t),e=t.pendingProps,o=r._init,r=o(r._payload),t.type=r,o=t.tag=Dp(r),e=Fe(r,e),o){case 0:t=ol(null,t,r,e,n);break e;case 1:t=ga(null,t,r,e,n);break e;case 11:t=ma(null,t,r,e,n);break e;case 14:t=ya(null,t,r,Fe(r.type,e),n);break e}throw Error(w(306,r,""))}return t;case 0:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:Fe(r,o),ol(e,t,r,o,n);case 1:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:Fe(r,o),ga(e,t,r,o,n);case 3:e:{if(Ac(t),e===null)throw Error(w(387));r=t.pendingProps,i=t.memoizedState,o=i.element,oc(e,t),wo(t,r,null,n);var l=t.memoizedState;if(r=l.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:l.cache,pendingSuspenseBoundaries:l.pendingSuspenseBoundaries,transitions:l.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){o=_n(Error(w(423)),t),t=va(e,t,r,n,o);break e}else if(r!==o){o=_n(Error(w(424)),t),t=va(e,t,r,n,o);break e}else for(Ne=Nt(t.stateNode.containerInfo.firstChild),Pe=t,G=!0,Re=null,n=nc(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Sn(),r===o){t=at(e,t,n);break e}he(e,t,r,n)}t=t.child}return t;case 5:return ic(t),e===null&&Zi(t),r=t.type,o=t.pendingProps,i=e!==null?e.memoizedProps:null,l=o.children,Yi(r,o)?l=null:i!==null&&Yi(r,i)&&(t.flags|=32),Mc(e,t),he(e,t,l,n),t.child;case 6:return e===null&&Zi(t),null;case 13:return jc(e,t,n);case 4:return Xl(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=wn(t,null,r,n):he(e,t,r,n),t.child;case 11:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:Fe(r,o),ma(e,t,r,o,n);case 7:return he(e,t,t.pendingProps,n),t.child;case 8:return he(e,t,t.pendingProps.children,n),t.child;case 12:return he(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,o=t.pendingProps,i=t.memoizedProps,l=o.value,V(ko,r._currentValue),r._currentValue=l,i!==null)if(He(i.value,l)){if(i.children===o.children&&!Se.current){t=at(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var a=i.dependencies;if(a!==null){l=i.child;for(var s=a.firstContext;s!==null;){if(s.context===r){if(i.tag===1){s=ot(-1,n&-n),s.tag=2;var u=i.updateQueue;if(u!==null){u=u.shared;var m=u.pending;m===null?s.next=s:(s.next=m.next,m.next=s),u.pending=s}}i.lanes|=n,s=i.alternate,s!==null&&(s.lanes|=n),el(i.return,n,t),a.lanes|=n;break}s=s.next}}else if(i.tag===10)l=i.type===t.type?null:i.child;else if(i.tag===18){if(l=i.return,l===null)throw Error(w(341));l.lanes|=n,a=l.alternate,a!==null&&(a.lanes|=n),el(l,n,t),l=i.sibling}else l=i.child;if(l!==null)l.return=i;else for(l=i;l!==null;){if(l===t){l=null;break}if(i=l.sibling,i!==null){i.return=l.return,l=i;break}l=l.return}i=l}he(e,t,o.children,n),t=t.child}return t;case 9:return o=t.type,r=t.pendingProps.children,yn(t,n),o=De(o),r=r(o),t.flags|=1,he(e,t,r,n),t.child;case 14:return r=t.type,o=Fe(r,t.pendingProps),o=Fe(r.type,o),ya(e,t,r,o,n);case 15:return Lc(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:Fe(r,o),Xr(e,t),t.tag=1,we(r)?(e=!0,yo(t)):e=!1,yn(t,n),Nc(t,r,o),nl(t,r,o,n),il(null,t,r,!0,e,n);case 19:return zc(e,t,n);case 22:return Oc(e,t,n)}throw Error(w(156,t.tag))};function bc(e,t){return Cu(e,t)}function Ip(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function ze(e,t,n,r){return new Ip(e,t,n,r)}function ds(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Dp(e){if(typeof e=="function")return ds(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Al)return 11;if(e===jl)return 14}return 2}function Ot(e,t){var n=e.alternate;return n===null?(n=ze(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function eo(e,t,n,r,o,i){var l=2;if(r=e,typeof e=="function")ds(e)&&(l=1);else if(typeof e=="string")l=5;else e:switch(e){case en:return Wt(n.children,o,i,t);case Ml:l=8,o|=8;break;case Pi:return e=ze(12,n,t,o|2),e.elementType=Pi,e.lanes=i,e;case Ti:return e=ze(13,n,t,o),e.elementType=Ti,e.lanes=i,e;case Li:return e=ze(19,n,t,o),e.elementType=Li,e.lanes=i,e;case iu:return Ro(n,o,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case ru:l=10;break e;case ou:l=9;break e;case Al:l=11;break e;case jl:l=14;break e;case yt:l=16,r=null;break e}throw Error(w(130,e==null?e:typeof e,""))}return t=ze(l,n,t,o),t.elementType=e,t.type=r,t.lanes=i,t}function Wt(e,t,n,r){return e=ze(7,e,r,t),e.lanes=n,e}function Ro(e,t,n,r){return e=ze(22,e,r,t),e.elementType=iu,e.lanes=n,e.stateNode={isHidden:!1},e}function ki(e,t,n){return e=ze(6,e,null,t),e.lanes=n,e}function Si(e,t,n){return t=ze(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function xp(e,t,n,r,o){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=ei(0),this.expirationTimes=ei(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=ei(0),this.identifierPrefix=r,this.onRecoverableError=o,this.mutableSourceEagerHydrationData=null}function ps(e,t,n,r,o,i,l,a,s){return e=new xp(e,t,n,a,s),t===1?(t=1,i===!0&&(t|=8)):t=0,i=ze(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ql(i),e}function Bp(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Zc)}catch(e){console.error(e)}}Zc(),Za.exports=Le;var Vp=Za.exports,Ma=Vp;Ei.createRoot=Ma.createRoot,Ei.hydrateRoot=Ma.hydrateRoot;const Ze=Object.create(null);Ze.open="0";Ze.close="1";Ze.ping="2";Ze.pong="3";Ze.message="4";Ze.upgrade="5";Ze.noop="6";const to=Object.create(null);Object.keys(Ze).forEach(e=>{to[Ze[e]]=e});const gl={type:"error",data:"parser error"},ef=typeof Blob=="function"||typeof Blob<"u"&&Object.prototype.toString.call(Blob)==="[object BlobConstructor]",tf=typeof ArrayBuffer=="function",nf=e=>typeof ArrayBuffer.isView=="function"?ArrayBuffer.isView(e):e&&e.buffer instanceof ArrayBuffer,gs=({type:e,data:t},n,r)=>ef&&t instanceof Blob?n?r(t):Aa(t,r):tf&&(t instanceof ArrayBuffer||nf(t))?n?r(t):Aa(new Blob([t]),r):r(Ze[e]+(t||"")),Aa=(e,t)=>{const n=new FileReader;return n.onload=function(){const r=n.result.split(",")[1];t("b"+(r||""))},n.readAsDataURL(e)};function ja(e){return e instanceof Uint8Array?e:e instanceof ArrayBuffer?new Uint8Array(e):new Uint8Array(e.buffer,e.byteOffset,e.byteLength)}let wi;function Wp(e,t){if(ef&&e.data instanceof Blob)return e.data.arrayBuffer().then(ja).then(t);if(tf&&(e.data instanceof ArrayBuffer||nf(e.data)))return t(ja(e.data));gs(e,!1,n=>{wi||(wi=new TextEncoder),t(wi.encode(n))})}const za="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",Hn=typeof Uint8Array>"u"?[]:new Uint8Array(256);for(let e=0;e{let t=e.length*.75,n=e.length,r,o=0,i,l,a,s;e[e.length-1]==="="&&(t--,e[e.length-2]==="="&&t--);const u=new ArrayBuffer(t),m=new Uint8Array(u);for(r=0;r>4,m[o++]=(l&15)<<4|a>>2,m[o++]=(a&3)<<6|s&63;return u},Qp=typeof ArrayBuffer=="function",vs=(e,t)=>{if(typeof e!="string")return{type:"message",data:rf(e,t)};const n=e.charAt(0);return n==="b"?{type:"message",data:Gp(e.substring(1),t)}:to[n]?e.length>1?{type:to[n],data:e.substring(1)}:{type:to[n]}:gl},Gp=(e,t)=>{if(Qp){const n=Hp(e);return rf(n,t)}else return{base64:!0,data:e}},rf=(e,t)=>{switch(t){case"blob":return e instanceof Blob?e:new Blob([e]);case"arraybuffer":default:return e instanceof ArrayBuffer?e:e.buffer}},of="",Kp=(e,t)=>{const n=e.length,r=new Array(n);let o=0;e.forEach((i,l)=>{gs(i,!1,a=>{r[l]=a,++o===n&&t(r.join(of))})})},Yp=(e,t)=>{const n=e.split(of),r=[];for(let o=0;o{const r=n.length;let o;if(r<126)o=new Uint8Array(1),new DataView(o.buffer).setUint8(0,r);else if(r<65536){o=new Uint8Array(3);const i=new DataView(o.buffer);i.setUint8(0,126),i.setUint16(1,r)}else{o=new Uint8Array(9);const i=new DataView(o.buffer);i.setUint8(0,127),i.setBigUint64(1,BigInt(r))}e.data&&typeof e.data!="string"&&(o[0]|=128),t.enqueue(o),t.enqueue(n)})}})}let Ci;function Ur(e){return e.reduce((t,n)=>t+n.length,0)}function Rr(e,t){if(e[0].length===t)return e.shift();const n=new Uint8Array(t);let r=0;for(let o=0;oMath.pow(2,21)-1){a.enqueue(gl);break}o=m*Math.pow(2,32)+u.getUint32(4),r=3}else{if(Ur(n)e){a.enqueue(gl);break}}}})}const lf=4;function ee(e){if(e)return Xp(e)}function Xp(e){for(var t in ee.prototype)e[t]=ee.prototype[t];return e}ee.prototype.on=ee.prototype.addEventListener=function(e,t){return this._callbacks=this._callbacks||{},(this._callbacks["$"+e]=this._callbacks["$"+e]||[]).push(t),this};ee.prototype.once=function(e,t){function n(){this.off(e,n),t.apply(this,arguments)}return n.fn=t,this.on(e,n),this};ee.prototype.off=ee.prototype.removeListener=ee.prototype.removeAllListeners=ee.prototype.removeEventListener=function(e,t){if(this._callbacks=this._callbacks||{},arguments.length==0)return this._callbacks={},this;var n=this._callbacks["$"+e];if(!n)return this;if(arguments.length==1)return delete this._callbacks["$"+e],this;for(var r,o=0;oPromise.resolve().then(t):(t,n)=>n(t,0),je=typeof self<"u"?self:typeof window<"u"?window:Function("return this")(),Jp="arraybuffer";function sf(e,...t){return t.reduce((n,r)=>(e.hasOwnProperty(r)&&(n[r]=e[r]),n),{})}const Zp=je.setTimeout,eh=je.clearTimeout;function Ko(e,t){t.useNativeTimers?(e.setTimeoutFn=Zp.bind(je),e.clearTimeoutFn=eh.bind(je)):(e.setTimeoutFn=je.setTimeout.bind(je),e.clearTimeoutFn=je.clearTimeout.bind(je))}const th=1.33;function nh(e){return typeof e=="string"?rh(e):Math.ceil((e.byteLength||e.size)*th)}function rh(e){let t=0,n=0;for(let r=0,o=e.length;r=57344?n+=3:(r++,n+=4);return n}function af(){return Date.now().toString(36).substring(3)+Math.random().toString(36).substring(2,5)}function oh(e){let t="";for(let n in e)e.hasOwnProperty(n)&&(t.length&&(t+="&"),t+=encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return t}function ih(e){let t={},n=e.split("&");for(let r=0,o=n.length;r{this.readyState="paused",t()};if(this._polling||!this.writable){let r=0;this._polling&&(r++,this.once("pollComplete",function(){--r||n()})),this.writable||(r++,this.once("drain",function(){--r||n()}))}else n()}_poll(){this._polling=!0,this.doPoll(),this.emitReserved("poll")}onData(t){const n=r=>{if(this.readyState==="opening"&&r.type==="open"&&this.onOpen(),r.type==="close")return this.onClose({description:"transport closed by the server"}),!1;this.onPacket(r)};Yp(t,this.socket.binaryType).forEach(n),this.readyState!=="closed"&&(this._polling=!1,this.emitReserved("pollComplete"),this.readyState==="open"&&this._poll())}doClose(){const t=()=>{this.write([{type:"close"}])};this.readyState==="open"?t():this.once("open",t)}write(t){this.writable=!1,Kp(t,n=>{this.doWrite(n,()=>{this.writable=!0,this.emitReserved("drain")})})}uri(){const t=this.opts.secure?"https":"http",n=this.query||{};return this.opts.timestampRequests!==!1&&(n[this.opts.timestampParam]=af()),!this.supportsBinary&&!n.sid&&(n.b64=1),this.createUri(t,n)}}let uf=!1;try{uf=typeof XMLHttpRequest<"u"&&"withCredentials"in new XMLHttpRequest}catch{}const ah=uf;function uh(){}class ch extends sh{constructor(t){if(super(t),typeof location<"u"){const n=location.protocol==="https:";let r=location.port;r||(r=n?"443":"80"),this.xd=typeof location<"u"&&t.hostname!==location.hostname||r!==t.port}}doWrite(t,n){const r=this.request({method:"POST",data:t});r.on("success",n),r.on("error",(o,i)=>{this.onError("xhr post error",o,i)})}doPoll(){const t=this.request();t.on("data",this.onData.bind(this)),t.on("error",(n,r)=>{this.onError("xhr poll error",n,r)}),this.pollXhr=t}}class Je extends ee{constructor(t,n,r){super(),this.createRequest=t,Ko(this,r),this._opts=r,this._method=r.method||"GET",this._uri=n,this._data=r.data!==void 0?r.data:null,this._create()}_create(){var t;const n=sf(this._opts,"agent","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","autoUnref");n.xdomain=!!this._opts.xd;const r=this._xhr=this.createRequest(n);try{r.open(this._method,this._uri,!0);try{if(this._opts.extraHeaders){r.setDisableHeaderCheck&&r.setDisableHeaderCheck(!0);for(let o in this._opts.extraHeaders)this._opts.extraHeaders.hasOwnProperty(o)&&r.setRequestHeader(o,this._opts.extraHeaders[o])}}catch{}if(this._method==="POST")try{r.setRequestHeader("Content-type","text/plain;charset=UTF-8")}catch{}try{r.setRequestHeader("Accept","*/*")}catch{}(t=this._opts.cookieJar)===null||t===void 0||t.addCookies(r),"withCredentials"in r&&(r.withCredentials=this._opts.withCredentials),this._opts.requestTimeout&&(r.timeout=this._opts.requestTimeout),r.onreadystatechange=()=>{var o;r.readyState===3&&((o=this._opts.cookieJar)===null||o===void 0||o.parseCookies(r.getResponseHeader("set-cookie"))),r.readyState===4&&(r.status===200||r.status===1223?this._onLoad():this.setTimeoutFn(()=>{this._onError(typeof r.status=="number"?r.status:0)},0))},r.send(this._data)}catch(o){this.setTimeoutFn(()=>{this._onError(o)},0);return}typeof document<"u"&&(this._index=Je.requestsCount++,Je.requests[this._index]=this)}_onError(t){this.emitReserved("error",t,this._xhr),this._cleanup(!0)}_cleanup(t){if(!(typeof this._xhr>"u"||this._xhr===null)){if(this._xhr.onreadystatechange=uh,t)try{this._xhr.abort()}catch{}typeof document<"u"&&delete Je.requests[this._index],this._xhr=null}}_onLoad(){const t=this._xhr.responseText;t!==null&&(this.emitReserved("data",t),this.emitReserved("success"),this._cleanup())}abort(){this._cleanup()}}Je.requestsCount=0;Je.requests={};if(typeof document<"u"){if(typeof attachEvent=="function")attachEvent("onunload",Ia);else if(typeof addEventListener=="function"){const e="onpagehide"in je?"pagehide":"unload";addEventListener(e,Ia,!1)}}function Ia(){for(let e in Je.requests)Je.requests.hasOwnProperty(e)&&Je.requests[e].abort()}const fh=function(){const e=cf({xdomain:!1});return e&&e.responseType!==null}();class dh extends ch{constructor(t){super(t);const n=t&&t.forceBase64;this.supportsBinary=fh&&!n}request(t={}){return Object.assign(t,{xd:this.xd},this.opts),new Je(cf,this.uri(),t)}}function cf(e){const t=e.xdomain;try{if(typeof XMLHttpRequest<"u"&&(!t||ah))return new XMLHttpRequest}catch{}if(!t)try{return new je[["Active"].concat("Object").join("X")]("Microsoft.XMLHTTP")}catch{}}const ff=typeof navigator<"u"&&typeof navigator.product=="string"&&navigator.product.toLowerCase()==="reactnative";class ph extends ks{get name(){return"websocket"}doOpen(){const t=this.uri(),n=this.opts.protocols,r=ff?{}:sf(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(r.headers=this.opts.extraHeaders);try{this.ws=this.createSocket(t,n,r)}catch(o){return this.emitReserved("error",o)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()}addEventListeners(){this.ws.onopen=()=>{this.opts.autoUnref&&this.ws._socket.unref(),this.onOpen()},this.ws.onclose=t=>this.onClose({description:"websocket connection closed",context:t}),this.ws.onmessage=t=>this.onData(t.data),this.ws.onerror=t=>this.onError("websocket error",t)}write(t){this.writable=!1;for(let n=0;n{try{this.doWrite(r,i)}catch{}o&&Go(()=>{this.writable=!0,this.emitReserved("drain")},this.setTimeoutFn)})}}doClose(){typeof this.ws<"u"&&(this.ws.onerror=()=>{},this.ws.close(),this.ws=null)}uri(){const t=this.opts.secure?"wss":"ws",n=this.query||{};return this.opts.timestampRequests&&(n[this.opts.timestampParam]=af()),this.supportsBinary||(n.b64=1),this.createUri(t,n)}}const _i=je.WebSocket||je.MozWebSocket;class hh extends ph{createSocket(t,n,r){return ff?new _i(t,n,r):n?new _i(t,n):new _i(t)}doWrite(t,n){this.ws.send(n)}}class mh extends ks{get name(){return"webtransport"}doOpen(){try{this._transport=new WebTransport(this.createUri("https"),this.opts.transportOptions[this.name])}catch(t){return this.emitReserved("error",t)}this._transport.closed.then(()=>{this.onClose()}).catch(t=>{this.onError("webtransport error",t)}),this._transport.ready.then(()=>{this._transport.createBidirectionalStream().then(t=>{const n=qp(Number.MAX_SAFE_INTEGER,this.socket.binaryType),r=t.readable.pipeThrough(n).getReader(),o=bp();o.readable.pipeTo(t.writable),this._writer=o.writable.getWriter();const i=()=>{r.read().then(({done:a,value:s})=>{a||(this.onPacket(s),i())}).catch(a=>{})};i();const l={type:"open"};this.query.sid&&(l.data=`{"sid":"${this.query.sid}"}`),this._writer.write(l).then(()=>this.onOpen())})})}write(t){this.writable=!1;for(let n=0;n{o&&Go(()=>{this.writable=!0,this.emitReserved("drain")},this.setTimeoutFn)})}}doClose(){var t;(t=this._transport)===null||t===void 0||t.close()}}const yh={websocket:hh,webtransport:mh,polling:dh},gh=/^(?:(?![^:@\/?#]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,vh=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];function vl(e){if(e.length>8e3)throw"URI too long";const t=e,n=e.indexOf("["),r=e.indexOf("]");n!=-1&&r!=-1&&(e=e.substring(0,n)+e.substring(n,r).replace(/:/g,";")+e.substring(r,e.length));let o=gh.exec(e||""),i={},l=14;for(;l--;)i[vh[l]]=o[l]||"";return n!=-1&&r!=-1&&(i.source=t,i.host=i.host.substring(1,i.host.length-1).replace(/;/g,":"),i.authority=i.authority.replace("[","").replace("]","").replace(/;/g,":"),i.ipv6uri=!0),i.pathNames=kh(i,i.path),i.queryKey=Sh(i,i.query),i}function kh(e,t){const n=/\/{2,9}/g,r=t.replace(n,"/").split("/");return(t.slice(0,1)=="/"||t.length===0)&&r.splice(0,1),t.slice(-1)=="/"&&r.splice(r.length-1,1),r}function Sh(e,t){const n={};return t.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,function(r,o,i){o&&(n[o]=i)}),n}const kl=typeof addEventListener=="function"&&typeof removeEventListener=="function",no=[];kl&&addEventListener("offline",()=>{no.forEach(e=>e())},!1);class Mt extends ee{constructor(t,n){if(super(),this.binaryType=Jp,this.writeBuffer=[],this._prevBufferLen=0,this._pingInterval=-1,this._pingTimeout=-1,this._maxPayload=-1,this._pingTimeoutTime=1/0,t&&typeof t=="object"&&(n=t,t=null),t){const r=vl(t);n.hostname=r.host,n.secure=r.protocol==="https"||r.protocol==="wss",n.port=r.port,r.query&&(n.query=r.query)}else n.host&&(n.hostname=vl(n.host).host);Ko(this,n),this.secure=n.secure!=null?n.secure:typeof location<"u"&&location.protocol==="https:",n.hostname&&!n.port&&(n.port=this.secure?"443":"80"),this.hostname=n.hostname||(typeof location<"u"?location.hostname:"localhost"),this.port=n.port||(typeof location<"u"&&location.port?location.port:this.secure?"443":"80"),this.transports=[],this._transportsByName={},n.transports.forEach(r=>{const o=r.prototype.name;this.transports.push(o),this._transportsByName[o]=r}),this.opts=Object.assign({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},n),this.opts.path=this.opts.path.replace(/\/$/,"")+(this.opts.addTrailingSlash?"/":""),typeof this.opts.query=="string"&&(this.opts.query=ih(this.opts.query)),kl&&(this.opts.closeOnBeforeunload&&(this._beforeunloadEventListener=()=>{this.transport&&(this.transport.removeAllListeners(),this.transport.close())},addEventListener("beforeunload",this._beforeunloadEventListener,!1)),this.hostname!=="localhost"&&(this._offlineEventListener=()=>{this._onClose("transport close",{description:"network connection lost"})},no.push(this._offlineEventListener))),this.opts.withCredentials&&(this._cookieJar=void 0),this._open()}createTransport(t){const n=Object.assign({},this.opts.query);n.EIO=lf,n.transport=t,this.id&&(n.sid=this.id);const r=Object.assign({},this.opts,{query:n,socket:this,hostname:this.hostname,secure:this.secure,port:this.port},this.opts.transportOptions[t]);return new this._transportsByName[t](r)}_open(){if(this.transports.length===0){this.setTimeoutFn(()=>{this.emitReserved("error","No transports available")},0);return}const t=this.opts.rememberUpgrade&&Mt.priorWebsocketSuccess&&this.transports.indexOf("websocket")!==-1?"websocket":this.transports[0];this.readyState="opening";const n=this.createTransport(t);n.open(),this.setTransport(n)}setTransport(t){this.transport&&this.transport.removeAllListeners(),this.transport=t,t.on("drain",this._onDrain.bind(this)).on("packet",this._onPacket.bind(this)).on("error",this._onError.bind(this)).on("close",n=>this._onClose("transport close",n))}onOpen(){this.readyState="open",Mt.priorWebsocketSuccess=this.transport.name==="websocket",this.emitReserved("open"),this.flush()}_onPacket(t){if(this.readyState==="opening"||this.readyState==="open"||this.readyState==="closing")switch(this.emitReserved("packet",t),this.emitReserved("heartbeat"),t.type){case"open":this.onHandshake(JSON.parse(t.data));break;case"ping":this._sendPacket("pong"),this.emitReserved("ping"),this.emitReserved("pong"),this._resetPingTimeout();break;case"error":const n=new Error("server error");n.code=t.data,this._onError(n);break;case"message":this.emitReserved("data",t.data),this.emitReserved("message",t.data);break}}onHandshake(t){this.emitReserved("handshake",t),this.id=t.sid,this.transport.query.sid=t.sid,this._pingInterval=t.pingInterval,this._pingTimeout=t.pingTimeout,this._maxPayload=t.maxPayload,this.onOpen(),this.readyState!=="closed"&&this._resetPingTimeout()}_resetPingTimeout(){this.clearTimeoutFn(this._pingTimeoutTimer);const t=this._pingInterval+this._pingTimeout;this._pingTimeoutTime=Date.now()+t,this._pingTimeoutTimer=this.setTimeoutFn(()=>{this._onClose("ping timeout")},t),this.opts.autoUnref&&this._pingTimeoutTimer.unref()}_onDrain(){this.writeBuffer.splice(0,this._prevBufferLen),this._prevBufferLen=0,this.writeBuffer.length===0?this.emitReserved("drain"):this.flush()}flush(){if(this.readyState!=="closed"&&this.transport.writable&&!this.upgrading&&this.writeBuffer.length){const t=this._getWritablePackets();this.transport.send(t),this._prevBufferLen=t.length,this.emitReserved("flush")}}_getWritablePackets(){if(!(this._maxPayload&&this.transport.name==="polling"&&this.writeBuffer.length>1))return this.writeBuffer;let n=1;for(let r=0;r0&&n>this._maxPayload)return this.writeBuffer.slice(0,r);n+=2}return this.writeBuffer}_hasPingExpired(){if(!this._pingTimeoutTime)return!0;const t=Date.now()>this._pingTimeoutTime;return t&&(this._pingTimeoutTime=0,Go(()=>{this._onClose("ping timeout")},this.setTimeoutFn)),t}write(t,n,r){return this._sendPacket("message",t,n,r),this}send(t,n,r){return this._sendPacket("message",t,n,r),this}_sendPacket(t,n,r,o){if(typeof n=="function"&&(o=n,n=void 0),typeof r=="function"&&(o=r,r=null),this.readyState==="closing"||this.readyState==="closed")return;r=r||{},r.compress=r.compress!==!1;const i={type:t,data:n,options:r};this.emitReserved("packetCreate",i),this.writeBuffer.push(i),o&&this.once("flush",o),this.flush()}close(){const t=()=>{this._onClose("forced close"),this.transport.close()},n=()=>{this.off("upgrade",n),this.off("upgradeError",n),t()},r=()=>{this.once("upgrade",n),this.once("upgradeError",n)};return(this.readyState==="opening"||this.readyState==="open")&&(this.readyState="closing",this.writeBuffer.length?this.once("drain",()=>{this.upgrading?r():t()}):this.upgrading?r():t()),this}_onError(t){if(Mt.priorWebsocketSuccess=!1,this.opts.tryAllTransports&&this.transports.length>1&&this.readyState==="opening")return this.transports.shift(),this._open();this.emitReserved("error",t),this._onClose("transport error",t)}_onClose(t,n){if(this.readyState==="opening"||this.readyState==="open"||this.readyState==="closing"){if(this.clearTimeoutFn(this._pingTimeoutTimer),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),kl&&(this._beforeunloadEventListener&&removeEventListener("beforeunload",this._beforeunloadEventListener,!1),this._offlineEventListener)){const r=no.indexOf(this._offlineEventListener);r!==-1&&no.splice(r,1)}this.readyState="closed",this.id=null,this.emitReserved("close",t,n),this.writeBuffer=[],this._prevBufferLen=0}}}Mt.protocol=lf;class wh extends Mt{constructor(){super(...arguments),this._upgrades=[]}onOpen(){if(super.onOpen(),this.readyState==="open"&&this.opts.upgrade)for(let t=0;t{r||(n.send([{type:"ping",data:"probe"}]),n.once("packet",c=>{if(!r)if(c.type==="pong"&&c.data==="probe"){if(this.upgrading=!0,this.emitReserved("upgrading",n),!n)return;Mt.priorWebsocketSuccess=n.name==="websocket",this.transport.pause(()=>{r||this.readyState!=="closed"&&(m(),this.setTransport(n),n.send([{type:"upgrade"}]),this.emitReserved("upgrade",n),n=null,this.upgrading=!1,this.flush())})}else{const f=new Error("probe error");f.transport=n.name,this.emitReserved("upgradeError",f)}}))};function i(){r||(r=!0,m(),n.close(),n=null)}const l=c=>{const f=new Error("probe error: "+c);f.transport=n.name,i(),this.emitReserved("upgradeError",f)};function a(){l("transport closed")}function s(){l("socket closed")}function u(c){n&&c.name!==n.name&&i()}const m=()=>{n.removeListener("open",o),n.removeListener("error",l),n.removeListener("close",a),this.off("close",s),this.off("upgrading",u)};n.once("open",o),n.once("error",l),n.once("close",a),this.once("close",s),this.once("upgrading",u),this._upgrades.indexOf("webtransport")!==-1&&t!=="webtransport"?this.setTimeoutFn(()=>{r||n.open()},200):n.open()}onHandshake(t){this._upgrades=this._filterUpgrades(t.upgrades),super.onHandshake(t)}_filterUpgrades(t){const n=[];for(let r=0;ryh[o]).filter(o=>!!o)),super(t,r)}};function _h(e,t="",n){let r=e;n=n||typeof location<"u"&&location,e==null&&(e=n.protocol+"//"+n.host),typeof e=="string"&&(e.charAt(0)==="/"&&(e.charAt(1)==="/"?e=n.protocol+e:e=n.host+e),/^(https?|wss?):\/\//.test(e)||(typeof n<"u"?e=n.protocol+"//"+e:e="https://"+e),r=vl(e)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";const i=r.host.indexOf(":")!==-1?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+i+":"+r.port+t,r.href=r.protocol+"://"+i+(n&&n.port===r.port?"":":"+r.port),r}const Eh=typeof ArrayBuffer=="function",Nh=e=>typeof ArrayBuffer.isView=="function"?ArrayBuffer.isView(e):e.buffer instanceof ArrayBuffer,df=Object.prototype.toString,Ph=typeof Blob=="function"||typeof Blob<"u"&&df.call(Blob)==="[object BlobConstructor]",Th=typeof File=="function"||typeof File<"u"&&df.call(File)==="[object FileConstructor]";function Ss(e){return Eh&&(e instanceof ArrayBuffer||Nh(e))||Ph&&e instanceof Blob||Th&&e instanceof File}function ro(e,t){if(!e||typeof e!="object")return!1;if(Array.isArray(e)){for(let n=0,r=e.length;n=0&&e.num{delete this.acks[t];for(let a=0;a{this.io.clearTimeoutFn(i),n.apply(this,a)};l.withError=!0,this.acks[t]=l}emitWithAck(t,...n){return new Promise((r,o)=>{const i=(l,a)=>l?o(l):r(a);i.withError=!0,n.push(i),this.emit(t,...n)})}_addToQueue(t){let n;typeof t[t.length-1]=="function"&&(n=t.pop());const r={id:this._queueSeq++,tryCount:0,pending:!1,args:t,flags:Object.assign({fromQueue:!0},this.flags)};t.push((o,...i)=>(this._queue[0],o!==null?r.tryCount>this._opts.retries&&(this._queue.shift(),n&&n(o)):(this._queue.shift(),n&&n(null,...i)),r.pending=!1,this._drainQueue())),this._queue.push(r),this._drainQueue()}_drainQueue(t=!1){if(!this.connected||this._queue.length===0)return;const n=this._queue[0];n.pending&&!t||(n.pending=!0,n.tryCount++,this.flags=n.flags,this.emit.apply(this,n.args))}packet(t){t.nsp=this.nsp,this.io._packet(t)}onopen(){typeof this.auth=="function"?this.auth(t=>{this._sendConnectPacket(t)}):this._sendConnectPacket(this.auth)}_sendConnectPacket(t){this.packet({type:x.CONNECT,data:this._pid?Object.assign({pid:this._pid,offset:this._lastOffset},t):t})}onerror(t){this.connected||this.emitReserved("connect_error",t)}onclose(t,n){this.connected=!1,delete this.id,this.emitReserved("disconnect",t,n),this._clearAcks()}_clearAcks(){Object.keys(this.acks).forEach(t=>{if(!this.sendBuffer.some(r=>String(r.id)===t)){const r=this.acks[t];delete this.acks[t],r.withError&&r.call(this,new Error("socket has been disconnected"))}})}onpacket(t){if(t.nsp===this.nsp)switch(t.type){case x.CONNECT:t.data&&t.data.sid?this.onconnect(t.data.sid,t.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case x.EVENT:case x.BINARY_EVENT:this.onevent(t);break;case x.ACK:case x.BINARY_ACK:this.onack(t);break;case x.DISCONNECT:this.ondisconnect();break;case x.CONNECT_ERROR:this.destroy();const r=new Error(t.data.message);r.data=t.data.data,this.emitReserved("connect_error",r);break}}onevent(t){const n=t.data||[];t.id!=null&&n.push(this.ack(t.id)),this.connected?this.emitEvent(n):this.receiveBuffer.push(Object.freeze(n))}emitEvent(t){if(this._anyListeners&&this._anyListeners.length){const n=this._anyListeners.slice();for(const r of n)r.apply(this,t)}super.emit.apply(this,t),this._pid&&t.length&&typeof t[t.length-1]=="string"&&(this._lastOffset=t[t.length-1])}ack(t){const n=this;let r=!1;return function(...o){r||(r=!0,n.packet({type:x.ACK,id:t,data:o}))}}onack(t){const n=this.acks[t.id];typeof n=="function"&&(delete this.acks[t.id],n.withError&&t.data.unshift(null),n.apply(this,t.data))}onconnect(t,n){this.id=t,this.recovered=n&&this._pid===n,this._pid=n,this.connected=!0,this.emitBuffered(),this._drainQueue(!0),this.emitReserved("connect")}emitBuffered(){this.receiveBuffer.forEach(t=>this.emitEvent(t)),this.receiveBuffer=[],this.sendBuffer.forEach(t=>{this.notifyOutgoingListeners(t),this.packet(t)}),this.sendBuffer=[]}ondisconnect(){this.destroy(),this.onclose("io server disconnect")}destroy(){this.subs&&(this.subs.forEach(t=>t()),this.subs=void 0),this.io._destroy(this)}disconnect(){return this.connected&&this.packet({type:x.DISCONNECT}),this.destroy(),this.connected&&this.onclose("io client disconnect"),this}close(){return this.disconnect()}compress(t){return this.flags.compress=t,this}get volatile(){return this.flags.volatile=!0,this}timeout(t){return this.flags.timeout=t,this}onAny(t){return this._anyListeners=this._anyListeners||[],this._anyListeners.push(t),this}prependAny(t){return this._anyListeners=this._anyListeners||[],this._anyListeners.unshift(t),this}offAny(t){if(!this._anyListeners)return this;if(t){const n=this._anyListeners;for(let r=0;r0&&e.jitter<=1?e.jitter:0,this.attempts=0}Ln.prototype.duration=function(){var e=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var t=Math.random(),n=Math.floor(t*this.jitter*e);e=Math.floor(t*10)&1?e+n:e-n}return Math.min(e,this.max)|0};Ln.prototype.reset=function(){this.attempts=0};Ln.prototype.setMin=function(e){this.ms=e};Ln.prototype.setMax=function(e){this.max=e};Ln.prototype.setJitter=function(e){this.jitter=e};class Cl extends ee{constructor(t,n){var r;super(),this.nsps={},this.subs=[],t&&typeof t=="object"&&(n=t,t=void 0),n=n||{},n.path=n.path||"/socket.io",this.opts=n,Ko(this,n),this.reconnection(n.reconnection!==!1),this.reconnectionAttempts(n.reconnectionAttempts||1/0),this.reconnectionDelay(n.reconnectionDelay||1e3),this.reconnectionDelayMax(n.reconnectionDelayMax||5e3),this.randomizationFactor((r=n.randomizationFactor)!==null&&r!==void 0?r:.5),this.backoff=new Ln({min:this.reconnectionDelay(),max:this.reconnectionDelayMax(),jitter:this.randomizationFactor()}),this.timeout(n.timeout==null?2e4:n.timeout),this._readyState="closed",this.uri=t;const o=n.parser||zh;this.encoder=new o.Encoder,this.decoder=new o.Decoder,this._autoConnect=n.autoConnect!==!1,this._autoConnect&&this.open()}reconnection(t){return arguments.length?(this._reconnection=!!t,t||(this.skipReconnect=!0),this):this._reconnection}reconnectionAttempts(t){return t===void 0?this._reconnectionAttempts:(this._reconnectionAttempts=t,this)}reconnectionDelay(t){var n;return t===void 0?this._reconnectionDelay:(this._reconnectionDelay=t,(n=this.backoff)===null||n===void 0||n.setMin(t),this)}randomizationFactor(t){var n;return t===void 0?this._randomizationFactor:(this._randomizationFactor=t,(n=this.backoff)===null||n===void 0||n.setJitter(t),this)}reconnectionDelayMax(t){var n;return t===void 0?this._reconnectionDelayMax:(this._reconnectionDelayMax=t,(n=this.backoff)===null||n===void 0||n.setMax(t),this)}timeout(t){return arguments.length?(this._timeout=t,this):this._timeout}maybeReconnectOnOpen(){!this._reconnecting&&this._reconnection&&this.backoff.attempts===0&&this.reconnect()}open(t){if(~this._readyState.indexOf("open"))return this;this.engine=new Ch(this.uri,this.opts);const n=this.engine,r=this;this._readyState="opening",this.skipReconnect=!1;const o=Ue(n,"open",function(){r.onopen(),t&&t()}),i=a=>{this.cleanup(),this._readyState="closed",this.emitReserved("error",a),t?t(a):this.maybeReconnectOnOpen()},l=Ue(n,"error",i);if(this._timeout!==!1){const a=this._timeout,s=this.setTimeoutFn(()=>{o(),i(new Error("timeout")),n.close()},a);this.opts.autoUnref&&s.unref(),this.subs.push(()=>{this.clearTimeoutFn(s)})}return this.subs.push(o),this.subs.push(l),this}connect(t){return this.open(t)}onopen(){this.cleanup(),this._readyState="open",this.emitReserved("open");const t=this.engine;this.subs.push(Ue(t,"ping",this.onping.bind(this)),Ue(t,"data",this.ondata.bind(this)),Ue(t,"error",this.onerror.bind(this)),Ue(t,"close",this.onclose.bind(this)),Ue(this.decoder,"decoded",this.ondecoded.bind(this)))}onping(){this.emitReserved("ping")}ondata(t){try{this.decoder.add(t)}catch(n){this.onclose("parse error",n)}}ondecoded(t){Go(()=>{this.emitReserved("packet",t)},this.setTimeoutFn)}onerror(t){this.emitReserved("error",t)}socket(t,n){let r=this.nsps[t];return r?this._autoConnect&&!r.active&&r.connect():(r=new pf(this,t,n),this.nsps[t]=r),r}_destroy(t){const n=Object.keys(this.nsps);for(const r of n)if(this.nsps[r].active)return;this._close()}_packet(t){const n=this.encoder.encode(t);for(let r=0;rt()),this.subs.length=0,this.decoder.destroy()}_close(){this.skipReconnect=!0,this._reconnecting=!1,this.onclose("forced close")}disconnect(){return this._close()}onclose(t,n){var r;this.cleanup(),(r=this.engine)===null||r===void 0||r.close(),this.backoff.reset(),this._readyState="closed",this.emitReserved("close",t,n),this._reconnection&&!this.skipReconnect&&this.reconnect()}reconnect(){if(this._reconnecting||this.skipReconnect)return this;const t=this;if(this.backoff.attempts>=this._reconnectionAttempts)this.backoff.reset(),this.emitReserved("reconnect_failed"),this._reconnecting=!1;else{const n=this.backoff.duration();this._reconnecting=!0;const r=this.setTimeoutFn(()=>{t.skipReconnect||(this.emitReserved("reconnect_attempt",t.backoff.attempts),!t.skipReconnect&&t.open(o=>{o?(t._reconnecting=!1,t.reconnect(),this.emitReserved("reconnect_error",o)):t.onreconnect()}))},n);this.opts.autoUnref&&r.unref(),this.subs.push(()=>{this.clearTimeoutFn(r)})}}onreconnect(){const t=this.backoff.attempts;this._reconnecting=!1,this.backoff.reset(),this.emitReserved("reconnect",t)}}const $n={};function oo(e,t){typeof e=="object"&&(t=e,e=void 0),t=t||{};const n=_h(e,t.path||"/socket.io"),r=n.source,o=n.id,i=n.path,l=$n[o]&&i in $n[o].nsps,a=t.forceNew||t["force new connection"]||t.multiplex===!1||l;let s;return a?s=new Cl(r,t):($n[o]||($n[o]=new Cl(r,t)),s=$n[o]),n.query&&!t.query&&(t.query=n.queryKey),s.socket(n.path,t)}Object.assign(oo,{Manager:Cl,Socket:pf,io:oo,connect:oo});const Dh=window.location.hostname==="localhost"?"http://localhost:3001":`http://${window.location.hostname}:3001`,R=oo(Dh,{autoConnect:!1,reconnection:!0,reconnectionAttempts:10,reconnectionDelay:1e3}),xa=[{color:"radiance",name:"Dawn's Wrath",symbol:"☀",desc:"Healing, protection, order"},{color:"tide",name:"Depths of Knowledge",symbol:"🌊",desc:"Card draw, control, counters"},{color:"shadow",name:"Veil of Shadows",symbol:"💀",desc:"Removal, sacrifice, drain"},{color:"flame",name:"Infernal Fury",symbol:"🔥",desc:"Direct damage, speed, aggression"},{color:"growth",name:"Primal Might",symbol:"🌿",desc:"Big creatures, ramp, buffs"}];function xh({playerId:e,playerName:t,rooms:n,currentRoom:r,screen:o,onBackToLobby:i}){const[l,a]=z.useState("");z.useEffect(()=>{R.emit("getRooms")},[]);const s=()=>{R.emit("createRoom",{roomName:l||`${t}'s Game`}),a("")},u=f=>{R.emit("joinRoom",{roomId:f})},m=f=>{R.emit("selectDeck",{deckColor:f})},c=()=>{R.emit("startGame")};if(o==="room"&&r){const f=r.players.find(g=>g.id===e),v=r.hostId===e,y=r.players.length>=2&&r.players.every(g=>g.ready);return S.jsx("div",{className:"lobby",children:S.jsxs("div",{className:"room-view",children:[S.jsxs("div",{className:"room-header",children:[S.jsx("button",{className:"btn btn-back",onClick:i,children:"← Leave"}),S.jsx("h2",{children:r.name})]}),S.jsxs("div",{className:"room-players",children:[S.jsx("h3",{children:"Players"}),r.players.map(g=>{var N;return S.jsxs("div",{className:`room-player ${g.ready?"ready":""}`,children:[S.jsxs("span",{className:"player-name",children:[g.name," ",g.id===r.hostId?"(Host)":""]}),S.jsx("span",{className:"player-status",children:g.ready?`✔ ${typeof g.deck=="string"?(N=xa.find(p=>p.color===g.deck))==null?void 0:N.name:"Custom Deck"}`:"Choosing deck..."})]},g.id)}),r.players.length<2&&S.jsx("div",{className:"room-player waiting",children:"Waiting for opponent..."})]}),S.jsxs("div",{className:"deck-selection",children:[S.jsx("h3",{children:"Choose Your Deck"}),S.jsx("div",{className:"deck-grid",children:xa.map(g=>S.jsxs("button",{className:`deck-card ${(f==null?void 0:f.deck)===g.color?"selected":""} color-${g.color}`,onClick:()=>m(g.color),children:[S.jsx("span",{className:"deck-symbol",children:g.symbol}),S.jsx("span",{className:"deck-name",children:g.name}),S.jsx("span",{className:"deck-desc",children:g.desc})]},g.color))})]}),v&&S.jsx("button",{className:"btn btn-primary btn-start",onClick:c,disabled:!y,children:y?"Start Game":"Waiting for all players to be ready..."})]})})}return S.jsxs("div",{className:"lobby",children:[S.jsxs("div",{className:"lobby-header",children:[S.jsx("h1",{className:"game-title-small",children:"Arcane Duels"}),S.jsxs("span",{className:"player-badge",children:["Playing as: ",t]})]}),S.jsxs("div",{className:"lobby-content",children:[S.jsxs("div",{className:"create-room",children:[S.jsx("h3",{children:"Create a Game"}),S.jsxs("div",{className:"create-room-form",children:[S.jsx("input",{type:"text",value:l,onChange:f=>a(f.target.value),placeholder:"Room name (optional)",className:"room-input"}),S.jsx("button",{className:"btn btn-primary",onClick:s,children:"Create Room"})]})]}),S.jsxs("div",{className:"room-list",children:[S.jsxs("h3",{children:["Available Games ",n.length>0&&`(${n.length})`]}),n.length===0?S.jsx("div",{className:"no-rooms",children:"No games available. Create one!"}):n.map(f=>S.jsxs("div",{className:"room-item",children:[S.jsxs("div",{className:"room-info",children:[S.jsx("span",{className:"room-name",children:f.name}),S.jsxs("span",{className:"room-meta",children:["Host: ",f.host," · ",f.playerCount,"/",f.maxPlayers," players"]})]}),S.jsx("button",{className:"btn btn-join",onClick:()=>u(f.id),disabled:f.status!=="waiting"||f.playerCount>=f.maxPlayers,children:f.status!=="waiting"?"In Progress":f.playerCount>=f.maxPlayers?"Full":"Join"})]},f.id))]})]})]})}function Bh(e){let t=5381;for(let n=0;n>>0;return t}function Fh(e){let t=e|0;return()=>{t=t+1831565813|0;let n=Math.imul(t^t>>>15,1|t);return n=n+Math.imul(n^n>>>7,61|n)^n,((n^n>>>14)>>>0)/4294967296}}function $h(e){const t=Fh(Bh(e||"card"));return t(),t(),t(),{f:()=>t(),r:(n,r)=>n+t()*(r-n),i:(n,r)=>Math.floor(n+t()*(r-n)),pick:n=>n[Math.floor(t()*n.length)]}}const Ba={radiance:{bg:["#7a6420","#b89530","#d4a843","#f0d060","#fff8e7"],fills:["#f0d060","#e8c040","#d4a843","#ffeebb","#c49840"],glow:"#fffce8",deep:"#5a4010"},tide:{bg:["#0d1825","#1a3355","#2266aa","#4499dd"],fills:["#3388cc","#55aaee","#4499dd","#77ccff","#2266aa"],glow:"#cceeff",deep:"#08101a"},shadow:{bg:["#05020a","#0f0a18","#1a1520","#2a1530"],fills:["#5a3070","#8a6098","#44224d","#6b4880","#3a1845"],glow:"#c8a0d8",deep:"#030108"},flame:{bg:["#180600","#331000","#662200","#993300"],fills:["#ee5533","#ff8844","#cc3311","#ffbb33","#ff6622"],glow:"#ffeecc",deep:"#100300"},growth:{bg:["#081a05","#153010","#224411","#336622"],fills:["#55aa44","#88cc66","#44884a","#66bb55","#339933"],glow:"#d4eec8",deep:"#040d02"},colorless:{bg:["#303030","#505050","#707070","#909090"],fills:["#aaaaaa","#bbbbbb","#999999","#cccccc","#888888"],glow:"#e8e8ee",deep:"#202020"}};function Uh(e,t,n,r,o){const i=t.r(r*.3,r*.7),l=t.r(o*.15,o*.45),a=e.createRadialGradient(i,l,0,i,l,Math.max(r,o)*1.1);a.addColorStop(0,"#fff8e7"),a.addColorStop(.25,"#f5e6b8"),a.addColorStop(.5,"#d4a843"),a.addColorStop(.8,"#b89530"),a.addColorStop(1,"#7a6420"),e.fillStyle=a,e.fillRect(0,0,r,o);const s=t.i(7,14);for(let v=0;v{const a=l.current;if(!a)return;const s=window.devicePixelRatio||1;a.width=o*s,a.height=i*s;const u=a.getContext("2d");u.scale(s,s);const m=$h(t),c=Ba[n]||Ba.colorless;(Fa[n]||Fa.colorless)(u,m,c,o,i);const v=qh[r];v&&v(u,m,c,o,i),Xh(u,o,i),Jh(u,m,o,i)},[t,n,r,o,i]),S.jsx("canvas",{ref:l,style:{width:`${o}px`,height:`${i}px`,display:"block"}})}),Ua={radiance:{bg:"#fff8e7",border:"#d4a843",accent:"#f0d060"},tide:{bg:"#e7f0ff",border:"#4477bb",accent:"#6699dd"},shadow:{bg:"#2a2a2a",border:"#666",accent:"#8a7090",text:"#ddd"},flame:{bg:"#fff0e7",border:"#cc4422",accent:"#ee6644"},growth:{bg:"#eaf5e7",border:"#448833",accent:"#66aa55"},colorless:{bg:"#f0f0f0",border:"#999",accent:"#bbb"}},e0={swift:"Swift",vigilant:"Vigilant",soaring:"Soaring",guardian:"Guardian",fortified:"Fortified",draining:"Draining",overwhelming:"Overwhelming",venomous:"Venomous",reaching:"Reaching"};function t0(e){if(!e)return"";const t=[];e.colorless&&t.push(String(e.colorless));const n={radiance:"☀",tide:"🌊",shadow:"💀",flame:"🔥",growth:"🌿"};for(const[r,o]of Object.entries(n))for(let i=0;i<(e[r]||0);i++)t.push(o);return t.join(" ")}function Vr({card:e,instance:t,onClick:n,selected:r,selectable:o,small:i,faceDown:l,zone:a}){if(l)return S.jsx("div",{className:"card card-facedown",onClick:n,children:S.jsx("div",{className:"card-back",children:"Arcane Duels"})});const s=(t==null?void 0:t.cardData)||e;if(!s)return null;const u=Ua[s.color]||Ua.colorless,m=s.type==="creature",c=s.type==="land",f=t==null?void 0:t.tapped,v=s.keywords||[],y=t?t.effectivePower:s.power,g=t?t.effectiveToughness-(t.damage||0):s.toughness,N=["card",`card-${s.type}`,`color-${s.color}`,f?"tapped":"",r?"selected":"",o?"selectable":"",i?"card-small":"",a?`zone-${a}`:"",t!=null&&t.summoningSickness?"summoning-sick":""].filter(Boolean).join(" "),p={"--card-bg":u.bg,"--card-border":u.border,"--card-accent":u.accent,color:u.text||"#222"};return S.jsxs("div",{className:N,style:p,onClick:n,children:[S.jsxs("div",{className:"card-header",children:[S.jsx("span",{className:"card-name",children:s.name}),!c&&S.jsx("span",{className:"card-cost",children:t0(s.cost)})]}),S.jsx("div",{className:"card-art",children:S.jsx(Zh,{cardName:s.name,color:s.color,type:s.type,width:i?100:130,height:i?45:70})}),S.jsxs("div",{className:"card-type-line",children:[s.type.charAt(0).toUpperCase()+s.type.slice(1),s.subtype?` — ${s.subtype}`:""]}),S.jsxs("div",{className:"card-text",children:[v.length>0&&S.jsx("div",{className:"card-keywords",children:v.map(d=>e0[d]||d).join(", ")}),s.flavor&&!i&&S.jsx("div",{className:"card-flavor",children:s.flavor})]}),m&&S.jsx("div",{className:"card-pt",children:S.jsxs("span",{className:(t==null?void 0:t.damage)>0?"damaged":"",children:[y,"/",g]})})]})}const n0={untap:"Untap",upkeep:"Upkeep",draw:"Draw",main1:"Main 1",combat_begin:"Combat",combat_attackers:"Attackers",combat_blockers:"Blockers",combat_damage:"Damage",combat_end:"End Combat",main2:"Main 2",end_step:"End",cleanup:"Cleanup"},Ra={radiance:"☀",tide:"🌊",shadow:"💀",flame:"🔥",growth:"🌿",colorless:"⚪"};function r0({gameState:e,playerId:t,onLeave:n}){var Qe;const[r,o]=z.useState(null),[i,l]=z.useState([]),[a,s]=z.useState({}),[u,m]=z.useState(null),[c,f]=z.useState(null),[v,y]=z.useState(0),[g,N]=z.useState(null),[p,d]=z.useState(!1),h=e.activePlayerId===t,k=e.currentPhase,C=e.players[t],P=Object.keys(e.players).find(T=>T!==t),E=e.players[P];z.useEffect(()=>{const T=M=>{M.type==="chooseManaColor"&&N(M.options)};return R.on("needsResponse",T),()=>R.off("needsResponse",T)},[]),z.useEffect(()=>{l([]),s({})},[k]);const _=z.useCallback(T=>{R.emit("gameAction",{action:T})},[]),F=z.useCallback(T=>{_({type:"tapLand",instanceId:T})},[_]),A=z.useCallback((T,M=!1)=>{_({type:"tapAbility",instanceId:T,sacrifice:M})},[_]),ue=z.useCallback(T=>{var $,dt,_e;const M=C.hand[T];if(!M)return;if(M.type==="land"){_({type:"playLand",handIndex:T}),o(null);return}const re=($=M.abilities)==null?void 0:$.some(pt=>["destroyCreature","destroyCreatureIf","bounceCreature","pumpCreature","dealDamage","drainLife","tapCreature","giveKeyword","preventDamage"].includes(pt.effect));if((dt=M.abilities)==null?void 0:dt.some(pt=>pt.isX)){f({handIndex:T,needsTarget:re,cardData:M}),m(null);return}if(re){f({handIndex:T,needsTarget:!0,cardData:M,xAmount:0}),m("creature");return}if((_e=M.abilities)==null?void 0:_e.some(pt=>pt.target==="any"&&["dealDamage","drainLife"].includes(pt.effect))){f({handIndex:T,needsTarget:!0,cardData:M,xAmount:0,canTargetPlayer:!0}),m("any");return}_({type:"castSpell",handIndex:T}),o(null)},[C,_]),ct=z.useCallback(T=>{c&&(_({type:"castSpell",handIndex:c.handIndex,targetInstanceId:T,xAmount:c.xAmount||v}),f(null),m(null),o(null),y(0))},[c,v,_]),ft=z.useCallback(T=>{c&&(_({type:"castSpell",handIndex:c.handIndex,targetPlayerId:T,xAmount:c.xAmount||v}),f(null),m(null),o(null),y(0))},[c,v,_]),Cr=z.useCallback(()=>{_({type:"declareAttackers",attackerIds:i}),l([])},[i,_]),Yo=z.useCallback(()=>{_({type:"declareBlockers",blockerAssignments:a}),s({})},[a,_]),On=z.useCallback(()=>{_({type:"passPriority"})},[_]),Mn=z.useCallback(()=>{window.confirm("Are you sure you want to concede?")&&_({type:"concede"})},[_]),L=z.useCallback(T=>{_({type:"chooseManaColor",color:T}),N(null)},[_]),j=z.useCallback(T=>{l(M=>M.includes(T)?M.filter(re=>re!==T):[...M,T])},[]),I=z.useCallback((T,M)=>{s(re=>{const Ge={...re};return Ge[T]===M?delete Ge[T]:Ge[T]=M,Ge})},[]),W=z.useCallback((T,M)=>{var Xt,$,dt;if(u&&c){ct(T.instanceId);return}if(M)return;if(k==="combat_attackers"&&h&&T.cardData.type==="creature"){j(T.instanceId);return}if((T.cardData.type==="land"||(Xt=T.cardData.abilities)!=null&&Xt.some(_e=>_e.trigger==="tap"&&_e.effect==="addMana"))&&!T.tapped){F(T.instanceId);return}const re=($=T.cardData.abilities)==null?void 0:$.some(_e=>_e.trigger==="tap"&&_e.effect!=="addMana"),Ge=(dt=T.cardData.abilities)==null?void 0:dt.some(_e=>_e.trigger==="tap_sacrifice");re&&!T.tapped?A(T.instanceId,!1):Ge&&A(T.instanceId,!0)},[u,c,k,h,ct,j,F,A]),X=T=>S.jsx("div",{className:"mana-pool",children:Object.entries(T).map(([M,re])=>re>0?S.jsxs("span",{className:`mana mana-${M}`,children:[Ra[M],re]},M):null)}),xt=(T,M)=>{const re=T.battlefield.filter($=>$.cardData.type==="land"),Ge=T.battlefield.filter($=>$.cardData.type==="creature"),Xt=T.battlefield.filter($=>$.cardData.type!=="land"&&$.cardData.type!=="creature");return S.jsxs("div",{className:`battlefield ${M?"opponent":"mine"}`,children:[Xt.length>0&&S.jsx("div",{className:"bf-row bf-others",children:Xt.map($=>S.jsx(Vr,{instance:$,small:!0,onClick:()=>W($,M),selected:i.includes($.instanceId),selectable:u!==null},$.instanceId))}),S.jsx("div",{className:"bf-row bf-creatures",children:Ge.length===0?S.jsx("div",{className:"bf-empty",children:"No creatures"}):Ge.map($=>{const dt=e.attackers.includes($.instanceId),_e=Object.values(a).includes($.instanceId)||Object.values(e.blockers).flat().includes($.instanceId);return S.jsxs("div",{className:`bf-creature-slot ${dt?"attacking":""} ${_e?"blocking":""}`,children:[S.jsx(Vr,{instance:$,small:!0,onClick:()=>{if(k==="combat_blockers"&&!h&&!M&&e.attackers.length>0){const pt=e.attackers[0];I($.instanceId,pt)}else W($,M)},selected:i.includes($.instanceId)||Object.keys(a).includes($.instanceId),selectable:k==="combat_attackers"&&h&&!M||k==="combat_blockers"&&!h&&!M||u!==null}),dt&&S.jsx("div",{className:"combat-badge attack-badge",children:"ATK"}),_e&&S.jsx("div",{className:"combat-badge block-badge",children:"BLK"})]},$.instanceId)})}),S.jsx("div",{className:"bf-row bf-lands",children:re.map($=>S.jsx(Vr,{instance:$,small:!0,onClick:()=>W($,M)},$.instanceId))})]})};return e.gameOver?S.jsx("div",{className:"game-over-overlay",children:S.jsxs("div",{className:"game-over-modal",children:[S.jsx("h1",{children:e.winner===t?"Victory!":"Defeat"}),S.jsx("p",{children:e.winner===t?"You have won the duel!":"You have been defeated."}),S.jsx("button",{className:"btn btn-primary",onClick:n,children:"Back to Lobby"})]})}):S.jsxs("div",{className:"game-board",children:[S.jsxs("div",{className:"game-top-bar",children:[S.jsx("button",{className:"btn btn-small btn-back",onClick:Mn,children:"Concede"}),S.jsx("div",{className:"phase-tracker",children:Object.entries(n0).map(([T,M])=>S.jsx("span",{className:`phase-pip ${k===T?"active":""} ${h?"my-turn":""}`,children:M},T))}),S.jsxs("div",{className:"turn-info",children:["Turn ",e.turnNumber," · ",h?"Your turn":"Opponent's turn"]}),S.jsx("button",{className:"btn btn-small",onClick:()=>d(!p),children:"Log"})]}),S.jsxs("div",{className:"opponent-area",children:[S.jsxs("div",{className:"player-info opponent-info",children:[S.jsxs("span",{className:"life-total",children:["❤"," ",E.life]}),S.jsxs("span",{className:"hand-count",children:["🃏"," ",E.handCount]}),S.jsxs("span",{className:"library-count",children:["Deck: ",E.libraryCount]}),X(E.manaPool),u==="any"&&S.jsx("button",{className:"btn btn-target",onClick:()=>ft(P),children:"Target Opponent"})]}),xt(E,!0)]}),S.jsxs("div",{className:"combat-zone",children:[k==="combat_attackers"&&h&&S.jsxs("div",{className:"combat-controls",children:[S.jsx("span",{children:"Select attackers, then:"}),S.jsx("button",{className:"btn btn-primary",onClick:Cr,children:i.length>0?`Attack with ${i.length} creature(s)`:"Skip Attack"})]}),k==="combat_blockers"&&!h&&S.jsxs("div",{className:"combat-controls",children:[S.jsx("span",{children:"Select blockers, then:"}),S.jsx("button",{className:"btn btn-primary",onClick:Yo,children:Object.keys(a).length>0?`Block with ${Object.keys(a).length} creature(s)`:"No Blocks"})]})]}),S.jsxs("div",{className:"my-area",children:[xt(C,!1),S.jsxs("div",{className:"player-info my-info",children:[S.jsxs("span",{className:"life-total",children:["❤"," ",C.life]}),S.jsxs("span",{className:"library-count",children:["Deck: ",C.libraryCount]}),X(C.manaPool),u==="any"&&S.jsx("button",{className:"btn btn-target",onClick:()=>ft(t),children:"Target Self"})]})]}),S.jsxs("div",{className:"hand-area",children:[S.jsx("div",{className:"hand",children:C.hand.map((T,M)=>S.jsx("div",{className:"hand-slot",children:S.jsx(Vr,{card:T,onClick:()=>{r===M?ue(M):o(M)},selected:r===M,selectable:h||T.type==="instant"})},`${T.cardId}-${M}`))}),S.jsx("div",{className:"hand-actions",children:(h||k==="combat_blockers")&&S.jsx("button",{className:"btn btn-pass",onClick:On,children:k==="main1"?"Go to Combat":k==="main2"?"End Turn":k==="combat_blockers"&&!h?"No Blocks":"Pass"})})]}),c&&((Qe=c.cardData.abilities)==null?void 0:Qe.some(T=>T.isX))&&!u&&S.jsx("div",{className:"modal-overlay",children:S.jsxs("div",{className:"modal",children:[S.jsxs("h3",{children:["Choose X value for ",c.cardData.name]}),S.jsxs("div",{className:"x-picker",children:[S.jsx("button",{onClick:()=>y(Math.max(0,v-1)),children:"-"}),S.jsx("span",{className:"x-value",children:v}),S.jsx("button",{onClick:()=>y(v+1),children:"+"})]}),S.jsxs("div",{className:"modal-actions",children:[S.jsx("button",{className:"btn btn-primary",onClick:()=>{c.needsTarget?(f({...c,xAmount:v}),m("any")):(_({type:"castSpell",handIndex:c.handIndex,xAmount:v,targetPlayerId:c.canTargetPlayer?P:void 0}),f(null),o(null),y(0))},children:c.needsTarget?"Choose Target":"Cast"}),S.jsx("button",{className:"btn",onClick:()=>{f(null),y(0)},children:"Cancel"})]})]})}),g&&S.jsx("div",{className:"modal-overlay",children:S.jsxs("div",{className:"modal",children:[S.jsx("h3",{children:"Choose mana color"}),S.jsx("div",{className:"mana-choice-grid",children:g.map(T=>S.jsxs("button",{className:`btn mana-choice-btn color-${T}`,onClick:()=>L(T),children:[Ra[T]," ",T.charAt(0).toUpperCase()+T.slice(1)]},T))})]})}),u&&S.jsxs("div",{className:"target-banner",children:["Choose a target (click a creature",u==="any"?" or player":"",")",S.jsx("button",{className:"btn btn-small",onClick:()=>{m(null),f(null)},children:"Cancel"})]}),p&&S.jsxs("div",{className:"game-log-panel",children:[S.jsxs("div",{className:"log-header",children:[S.jsx("h3",{children:"Game Log"}),S.jsx("button",{className:"btn btn-small",onClick:()=>d(!1),children:"Close"})]}),S.jsx("div",{className:"log-entries",children:e.log.slice().reverse().map((T,M)=>S.jsxs("div",{className:"log-entry",children:[S.jsxs("span",{className:"log-turn",children:["T",T.turn]}),S.jsx("span",{className:"log-msg",children:T.message})]},M))})]})]})}const mt={NAME:"name",LOBBY:"lobby",ROOM:"room",GAME:"game"};function o0(){const[e,t]=z.useState(mt.NAME),[n,r]=z.useState(""),[o,i]=z.useState(null),[l,a]=z.useState([]),[s,u]=z.useState(null),[m,c]=z.useState(null),[f,v]=z.useState(null),[y,g]=z.useState(null),[N,p]=z.useState(!1);z.useEffect(()=>(R.connect(),R.on("connect",()=>{p(!0),i(R.id)}),R.on("disconnect",()=>p(!1)),R.on("nameSet",k=>{i(k.id),t(mt.LOBBY)}),R.on("roomsUpdated",k=>a(k)),R.on("roomCreated",k=>{u(k),t(mt.ROOM)}),R.on("roomJoined",k=>{u(k),t(mt.ROOM)}),R.on("roomUpdated",k=>u(k)),R.on("gameStarted",k=>{v(k.gameId),c(k.state),t(mt.GAME)}),R.on("gameStateUpdated",k=>c(k)),R.on("error",k=>{g(k.message),setTimeout(()=>g(null),3e3)}),R.on("actionError",k=>{g(k.message),setTimeout(()=>g(null),3e3)}),R.on("playerDisconnected",k=>{g(`${k.playerName} disconnected`),setTimeout(()=>g(null),3e3)}),R.on("needsResponse",()=>{}),()=>{R.removeAllListeners(),R.disconnect()}),[]);const d=z.useCallback(k=>{k.preventDefault(),n.trim()&&R.emit("setName",n.trim())},[n]),h=z.useCallback(()=>{R.emit("leaveRoom"),u(null),c(null),v(null),t(mt.LOBBY),R.emit("getRooms")},[]);return e===mt.NAME?S.jsxs("div",{className:"app",children:[S.jsxs("div",{className:"title-screen",children:[S.jsx("h1",{className:"game-title",children:"Arcane Duels"}),S.jsx("p",{className:"game-subtitle",children:"A game of strategy, mana, and might"}),S.jsxs("form",{onSubmit:d,className:"name-form",children:[S.jsx("input",{type:"text",value:n,onChange:k=>r(k.target.value),placeholder:"Enter your name...",className:"name-input",maxLength:20,autoFocus:!0}),S.jsx("button",{type:"submit",className:"btn btn-primary",disabled:!N,children:N?"Enter the Arena":"Connecting..."})]})]}),y&&S.jsx("div",{className:"error-toast",children:y})]}):e===mt.GAME&&m?S.jsxs("div",{className:"app",children:[S.jsx(r0,{gameState:m,playerId:o,onLeave:h}),y&&S.jsx("div",{className:"error-toast",children:y})]}):S.jsxs("div",{className:"app",children:[S.jsx(xh,{playerId:o,playerName:n,rooms:l,currentRoom:s,screen:e,onBackToLobby:h}),y&&S.jsx("div",{className:"error-toast",children:y})]})}Ei.createRoot(document.getElementById("root")).render(S.jsx(Af.StrictMode,{children:S.jsx(o0,{})})); diff --git a/client/dist/index.html b/client/dist/index.html new file mode 100644 index 0000000..9a3b5e6 --- /dev/null +++ b/client/dist/index.html @@ -0,0 +1,14 @@ + + + + + + Arcane Duels + + + + + +
+ + diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..75a0eb7 --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + Arcane Duels + + + +
+ + + diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..0cfdea9 --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,1803 @@ +{ + "name": "arcane-duels-client", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "arcane-duels-client", + "version": "1.0.0", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "socket.io-client": "^4.7.4" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.0.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", + "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001778", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true, + "license": "ISC" + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..caddfa9 --- /dev/null +++ b/client/package.json @@ -0,0 +1,22 @@ +{ + "name": "arcane-duels-client", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "socket.io-client": "^4.7.4" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.0.8" + } +} diff --git a/client/src/App.css b/client/src/App.css new file mode 100644 index 0000000..47fdb37 --- /dev/null +++ b/client/src/App.css @@ -0,0 +1,981 @@ +@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Inter:wght@400;500;600&display=swap'); + +:root { + --bg-dark: #0d0f14; + --bg-card: #1a1d26; + --bg-surface: #14161e; + --bg-hover: #1f2233; + --text-primary: #e8e6e3; + --text-secondary: #9a9a9a; + --text-muted: #666; + --accent-gold: #d4a843; + --accent-glow: #f0d060; + --border: #2a2d3a; + --danger: #cc4422; + --success: #44aa55; + --radiance: #f0d060; + --tide: #4499dd; + --shadow: #8a6098; + --flame: #ee5533; + --growth: #55aa44; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', sans-serif; + background: var(--bg-dark); + color: var(--text-primary); + min-height: 100vh; + overflow: hidden; +} + +.app { + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; +} + +/* ========== TITLE SCREEN ========== */ +.title-screen { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background: radial-gradient(ellipse at center, #1a1530 0%, var(--bg-dark) 70%); +} + +.game-title { + font-family: 'Cinzel', serif; + font-size: 4.5rem; + font-weight: 700; + background: linear-gradient(135deg, var(--accent-gold), #fff8e0, var(--accent-gold)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: none; + letter-spacing: 4px; + margin-bottom: 8px; +} + +.game-subtitle { + font-size: 1.1rem; + color: var(--text-secondary); + margin-bottom: 48px; + letter-spacing: 2px; +} + +.name-form { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +.name-input { + width: 320px; + padding: 14px 20px; + font-size: 1.1rem; + border: 2px solid var(--border); + border-radius: 8px; + background: var(--bg-surface); + color: var(--text-primary); + outline: none; + transition: border-color 0.2s; + text-align: center; + font-family: 'Inter', sans-serif; +} + +.name-input:focus { + border-color: var(--accent-gold); +} + +/* ========== BUTTONS ========== */ +.btn { + padding: 10px 24px; + font-size: 0.95rem; + font-weight: 600; + border: 2px solid var(--border); + border-radius: 6px; + background: var(--bg-surface); + color: var(--text-primary); + cursor: pointer; + transition: all 0.15s; + font-family: 'Inter', sans-serif; +} + +.btn:hover { + background: var(--bg-hover); + border-color: var(--accent-gold); +} + +.btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.btn-primary { + background: linear-gradient(135deg, #3a3020, #2a2518); + border-color: var(--accent-gold); + color: var(--accent-glow); +} + +.btn-primary:hover { + background: linear-gradient(135deg, #4a4030, #3a3020); +} + +.btn-small { + padding: 6px 14px; + font-size: 0.8rem; +} + +.btn-back { + border: none; + background: none; + color: var(--text-secondary); + padding: 8px 12px; +} + +.btn-back:hover { + color: var(--text-primary); + background: none; +} + +.btn-join { + padding: 8px 20px; + font-size: 0.85rem; +} + +.btn-pass { + background: linear-gradient(135deg, #1a2a1a, #152015); + border-color: var(--success); + color: #88dd88; +} + +.btn-target { + background: linear-gradient(135deg, #2a1a1a, #201515); + border-color: var(--danger); + color: #ee8888; + animation: pulse-border 1s infinite; +} + +@keyframes pulse-border { + 0%, 100% { box-shadow: 0 0 0 0 rgba(204, 68, 34, 0.4); } + 50% { box-shadow: 0 0 0 4px rgba(204, 68, 34, 0.1); } +} + +.btn-start { + width: 100%; + padding: 16px; + font-size: 1.1rem; + margin-top: 24px; +} + +/* ========== LOBBY ========== */ +.lobby { + height: 100vh; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.lobby-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + border-bottom: 1px solid var(--border); + background: var(--bg-surface); +} + +.game-title-small { + font-family: 'Cinzel', serif; + font-size: 1.5rem; + color: var(--accent-gold); +} + +.player-badge { + color: var(--text-secondary); + font-size: 0.9rem; +} + +.lobby-content { + flex: 1; + max-width: 700px; + margin: 0 auto; + padding: 32px 24px; + width: 100%; +} + +.create-room { + margin-bottom: 40px; +} + +.create-room h3, .room-list h3 { + font-family: 'Cinzel', serif; + font-size: 1.2rem; + color: var(--accent-gold); + margin-bottom: 16px; +} + +.create-room-form { + display: flex; + gap: 12px; +} + +.room-input { + flex: 1; + padding: 12px 16px; + border: 2px solid var(--border); + border-radius: 6px; + background: var(--bg-surface); + color: var(--text-primary); + font-size: 0.95rem; + outline: none; + font-family: 'Inter', sans-serif; +} + +.room-input:focus { + border-color: var(--accent-gold); +} + +.room-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-surface); + margin-bottom: 8px; + transition: border-color 0.2s; +} + +.room-item:hover { + border-color: var(--accent-gold); +} + +.room-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.room-name { + font-weight: 600; +} + +.room-meta { + font-size: 0.85rem; + color: var(--text-secondary); +} + +.no-rooms { + padding: 40px; + text-align: center; + color: var(--text-muted); + border: 1px dashed var(--border); + border-radius: 8px; +} + +/* ========== ROOM VIEW ========== */ +.room-view { + max-width: 800px; + margin: 0 auto; + padding: 32px 24px; +} + +.room-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 32px; +} + +.room-header h2 { + font-family: 'Cinzel', serif; + color: var(--accent-gold); +} + +.room-players { + margin-bottom: 32px; +} + +.room-players h3 { + font-family: 'Cinzel', serif; + color: var(--accent-gold); + margin-bottom: 12px; +} + +.room-player { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border: 1px solid var(--border); + border-radius: 6px; + margin-bottom: 8px; + background: var(--bg-surface); +} + +.room-player.ready { + border-color: var(--success); +} + +.room-player.waiting { + color: var(--text-muted); + border-style: dashed; + justify-content: center; +} + +.player-status { + font-size: 0.85rem; + color: var(--text-secondary); +} + +/* ========== DECK SELECTION ========== */ +.deck-selection h3 { + font-family: 'Cinzel', serif; + color: var(--accent-gold); + margin-bottom: 16px; +} + +.deck-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; +} + +.deck-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 24px 16px; + border: 2px solid var(--border); + border-radius: 10px; + background: var(--bg-surface); + cursor: pointer; + transition: all 0.2s; + text-align: center; +} + +.deck-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.deck-card.selected { + border-color: var(--accent-gold); + box-shadow: 0 0 20px rgba(212, 168, 67, 0.3); +} + +.deck-card.color-radiance:hover, .deck-card.color-radiance.selected { border-color: var(--radiance); box-shadow: 0 0 20px rgba(240, 208, 96, 0.2); } +.deck-card.color-tide:hover, .deck-card.color-tide.selected { border-color: var(--tide); box-shadow: 0 0 20px rgba(68, 153, 221, 0.2); } +.deck-card.color-shadow:hover, .deck-card.color-shadow.selected { border-color: var(--shadow); box-shadow: 0 0 20px rgba(138, 96, 152, 0.2); } +.deck-card.color-flame:hover, .deck-card.color-flame.selected { border-color: var(--flame); box-shadow: 0 0 20px rgba(238, 85, 51, 0.2); } +.deck-card.color-growth:hover, .deck-card.color-growth.selected { border-color: var(--growth); box-shadow: 0 0 20px rgba(85, 170, 68, 0.2); } + +.deck-symbol { + font-size: 2.5rem; +} + +.deck-name { + font-family: 'Cinzel', serif; + font-weight: 600; + font-size: 1rem; + color: var(--text-primary); +} + +.deck-desc { + font-size: 0.8rem; + color: var(--text-secondary); +} + +/* ========== GAME BOARD ========== */ +.game-board { + display: flex; + flex-direction: column; + height: 100vh; + background: radial-gradient(ellipse at center, #111520 0%, var(--bg-dark) 100%); +} + +.game-top-bar { + display: flex; + align-items: center; + gap: 12px; + padding: 6px 12px; + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + z-index: 10; +} + +.phase-tracker { + display: flex; + gap: 2px; + flex: 1; + justify-content: center; + flex-wrap: wrap; +} + +.phase-pip { + padding: 3px 8px; + font-size: 0.7rem; + border-radius: 3px; + color: var(--text-muted); + background: transparent; + transition: all 0.2s; +} + +.phase-pip.active { + background: var(--accent-gold); + color: #000; + font-weight: 700; +} + +.phase-pip.active.my-turn { + background: var(--success); +} + +.turn-info { + font-size: 0.8rem; + color: var(--text-secondary); + white-space: nowrap; +} + +/* ========== PLAYER AREAS ========== */ +.opponent-area, .my-area { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + padding: 4px 12px; +} + +.opponent-area { + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.player-info { + display: flex; + align-items: center; + gap: 16px; + padding: 6px 12px; + font-size: 0.85rem; + flex-shrink: 0; +} + +.life-total { + font-size: 1.2rem; + font-weight: 700; + color: #ee5555; +} + +.hand-count, .library-count { + color: var(--text-secondary); + font-size: 0.8rem; +} + +.mana-pool { + display: flex; + gap: 8px; + margin-left: auto; +} + +.mana { + font-size: 0.85rem; + font-weight: 600; + padding: 2px 6px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.08); +} + +.mana-radiance { color: var(--radiance); } +.mana-tide { color: var(--tide); } +.mana-shadow { color: var(--shadow); } +.mana-flame { color: var(--flame); } +.mana-growth { color: var(--growth); } +.mana-colorless { color: var(--text-secondary); } + +/* ========== BATTLEFIELD ========== */ +.battlefield { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + min-height: 0; + overflow-x: auto; +} + +.battlefield.opponent { + flex-direction: column-reverse; +} + +.bf-row { + display: flex; + gap: 6px; + align-items: center; + min-height: 36px; + flex-wrap: wrap; +} + +.bf-creatures { + flex: 1; + justify-content: center; + align-content: center; +} + +.bf-lands { + justify-content: center; +} + +.bf-others { + justify-content: center; +} + +.bf-empty { + color: var(--text-muted); + font-size: 0.8rem; + font-style: italic; +} + +.bf-creature-slot { + position: relative; +} + +.bf-creature-slot.attacking .card { + transform: translateY(-8px); + box-shadow: 0 0 12px rgba(238, 85, 51, 0.5); +} + +.bf-creature-slot.blocking .card { + box-shadow: 0 0 12px rgba(68, 153, 221, 0.5); +} + +.combat-badge { + position: absolute; + top: -4px; + right: -4px; + padding: 2px 6px; + border-radius: 4px; + font-size: 0.6rem; + font-weight: 700; + z-index: 5; +} + +.attack-badge { + background: var(--flame); + color: #fff; +} + +.block-badge { + background: var(--tide); + color: #fff; +} + +/* ========== COMBAT ZONE ========== */ +.combat-zone { + flex-shrink: 0; + min-height: 0; +} + +.combat-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + padding: 8px; + background: rgba(212, 168, 67, 0.08); + border-top: 1px solid rgba(212, 168, 67, 0.2); + border-bottom: 1px solid rgba(212, 168, 67, 0.2); + font-size: 0.85rem; +} + +/* ========== HAND ========== */ +.hand-area { + flex-shrink: 0; + background: rgba(0, 0, 0, 0.3); + border-top: 1px solid var(--border); + padding: 6px 12px 8px; + display: flex; + align-items: center; + gap: 8px; +} + +.hand { + display: flex; + gap: 4px; + overflow-x: auto; + flex: 1; + padding: 4px 0; +} + +.hand-slot { + flex-shrink: 0; +} + +.hand-actions { + flex-shrink: 0; +} + +/* ========== CARDS ========== */ +.card { + width: 130px; + min-height: 180px; + border: 2px solid var(--card-border, var(--border)); + border-radius: 8px; + background: var(--card-bg, var(--bg-card)); + display: flex; + flex-direction: column; + overflow: hidden; + cursor: pointer; + transition: all 0.15s; + position: relative; + font-size: 0.75rem; +} + +.card:hover { + transform: translateY(-4px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); + z-index: 10; +} + +.card.card-small { + width: 100px; + min-height: 130px; + font-size: 0.65rem; +} + +.card.tapped { + transform: rotate(90deg); + margin: 10px 15px; +} + +.card.tapped:hover { + transform: rotate(90deg) translateY(-4px); +} + +.card.selected { + box-shadow: 0 0 0 3px var(--accent-gold), 0 0 20px rgba(212, 168, 67, 0.4); + transform: translateY(-8px); +} + +.card.selectable { + cursor: pointer; +} + +.card.selectable:hover { + box-shadow: 0 0 0 2px var(--accent-gold); +} + +.card.summoning-sick { + opacity: 0.75; +} + +.card-facedown { + width: 100px; + min-height: 130px; + border: 2px solid var(--border); + border-radius: 8px; + background: linear-gradient(135deg, #1a1530, #2a1a40); + display: flex; + align-items: center; + justify-content: center; +} + +.card-back { + font-family: 'Cinzel', serif; + font-size: 0.7rem; + color: var(--accent-gold); + text-align: center; + transform: rotate(-30deg); + opacity: 0.6; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 6px 8px 4px; + gap: 4px; +} + +.card-name { + font-weight: 700; + font-size: 0.7em; + line-height: 1.2; + flex: 1; +} + +.card-cost { + font-size: 0.75em; + white-space: nowrap; + flex-shrink: 0; +} + +.card-art { + display: flex; + align-items: center; + justify-content: center; + padding: 0; + flex: 1; + background: rgba(0, 0, 0, 0.15); + min-height: 40px; + overflow: hidden; +} + +.card-type-line { + padding: 3px 8px; + font-size: 0.6em; + color: inherit; + opacity: 0.7; + border-top: 1px solid rgba(0, 0, 0, 0.15); + border-bottom: 1px solid rgba(0, 0, 0, 0.15); +} + +.card-text { + padding: 4px 8px; + flex: 0; + font-size: 0.6em; + line-height: 1.3; +} + +.card-keywords { + font-weight: 700; + font-style: italic; + margin-bottom: 2px; +} + +.card-flavor { + font-style: italic; + opacity: 0.6; + margin-top: 2px; + font-size: 0.9em; +} + +.card-pt { + padding: 4px 8px; + text-align: right; + font-weight: 700; + font-size: 0.9em; +} + +.card-pt .damaged { + color: var(--danger); +} + +/* ========== MODALS ========== */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.modal { + background: var(--bg-surface); + border: 2px solid var(--accent-gold); + border-radius: 12px; + padding: 32px; + max-width: 400px; + text-align: center; +} + +.modal h3 { + font-family: 'Cinzel', serif; + color: var(--accent-gold); + margin-bottom: 20px; +} + +.x-picker { + display: flex; + align-items: center; + justify-content: center; + gap: 20px; + margin-bottom: 20px; +} + +.x-picker button { + width: 40px; + height: 40px; + border-radius: 50%; + border: 2px solid var(--border); + background: var(--bg-card); + color: var(--text-primary); + font-size: 1.2rem; + cursor: pointer; +} + +.x-value { + font-size: 2rem; + font-weight: 700; + color: var(--accent-gold); + min-width: 40px; +} + +.modal-actions { + display: flex; + gap: 12px; + justify-content: center; +} + +.mana-choice-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.mana-choice-btn { + padding: 12px 16px !important; + font-size: 1rem !important; +} + +.mana-choice-btn.color-radiance { border-color: var(--radiance) !important; color: var(--radiance) !important; } +.mana-choice-btn.color-tide { border-color: var(--tide) !important; color: var(--tide) !important; } +.mana-choice-btn.color-shadow { border-color: var(--shadow) !important; color: var(--shadow) !important; } +.mana-choice-btn.color-flame { border-color: var(--flame) !important; color: var(--flame) !important; } +.mana-choice-btn.color-growth { border-color: var(--growth) !important; color: var(--growth) !important; } +.mana-choice-btn.color-colorless { border-color: var(--text-secondary) !important; } + +/* ========== TARGET BANNER ========== */ +.target-banner { + position: fixed; + top: 50px; + left: 50%; + transform: translateX(-50%); + background: rgba(204, 68, 34, 0.9); + color: #fff; + padding: 10px 20px; + border-radius: 8px; + display: flex; + align-items: center; + gap: 12px; + font-weight: 600; + z-index: 50; + animation: slide-down 0.2s ease-out; +} + +@keyframes slide-down { + from { transform: translateX(-50%) translateY(-20px); opacity: 0; } + to { transform: translateX(-50%) translateY(0); opacity: 1; } +} + +/* ========== GAME LOG ========== */ +.game-log-panel { + position: fixed; + right: 0; + top: 0; + bottom: 0; + width: 320px; + background: var(--bg-surface); + border-left: 1px solid var(--border); + z-index: 50; + display: flex; + flex-direction: column; +} + +.log-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--border); +} + +.log-header h3 { + font-family: 'Cinzel', serif; + color: var(--accent-gold); +} + +.log-entries { + flex: 1; + overflow-y: auto; + padding: 8px; +} + +.log-entry { + padding: 6px 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.03); + font-size: 0.8rem; + display: flex; + gap: 8px; +} + +.log-turn { + color: var(--accent-gold); + font-weight: 600; + flex-shrink: 0; +} + +.log-msg { + color: var(--text-secondary); +} + +/* ========== GAME OVER ========== */ +.game-over-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; +} + +.game-over-modal { + text-align: center; + padding: 48px; +} + +.game-over-modal h1 { + font-family: 'Cinzel', serif; + font-size: 3rem; + background: linear-gradient(135deg, var(--accent-gold), #fff8e0); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 16px; +} + +.game-over-modal p { + color: var(--text-secondary); + margin-bottom: 32px; + font-size: 1.1rem; +} + +/* ========== ERROR TOAST ========== */ +.error-toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: rgba(204, 68, 34, 0.95); + color: #fff; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + z-index: 300; + animation: toast-in 0.2s ease-out; +} + +@keyframes toast-in { + from { transform: translateX(-50%) translateY(20px); opacity: 0; } + to { transform: translateX(-50%) translateY(0); opacity: 1; } +} + +/* ========== COLOR-SPECIFIC CARD BORDERS ========== */ +.card.color-radiance { border-color: var(--radiance); } +.card.color-tide { border-color: var(--tide); } +.card.color-shadow { border-color: var(--shadow); background: #1a1520; color: #ddd; } +.card.color-flame { border-color: var(--flame); } +.card.color-growth { border-color: var(--growth); } +.card.color-colorless { border-color: #888; } diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 0000000..31dc8e8 --- /dev/null +++ b/client/src/App.jsx @@ -0,0 +1,143 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import socket from './socket'; +import Lobby from './components/Lobby'; +import GameBoard from './components/GameBoard'; + +const SCREENS = { NAME: 'name', LOBBY: 'lobby', ROOM: 'room', GAME: 'game' }; + +export default function App() { + const [screen, setScreen] = useState(SCREENS.NAME); + const [playerName, setPlayerName] = useState(''); + const [playerId, setPlayerId] = useState(null); + const [rooms, setRooms] = useState([]); + const [currentRoom, setCurrentRoom] = useState(null); + const [gameState, setGameState] = useState(null); + const [gameId, setGameId] = useState(null); + const [error, setError] = useState(null); + const [connected, setConnected] = useState(false); + + useEffect(() => { + socket.connect(); + + socket.on('connect', () => { + setConnected(true); + setPlayerId(socket.id); + }); + + socket.on('disconnect', () => setConnected(false)); + + socket.on('nameSet', (data) => { + setPlayerId(data.id); + setScreen(SCREENS.LOBBY); + }); + + socket.on('roomsUpdated', (data) => setRooms(data)); + socket.on('roomCreated', (room) => { + setCurrentRoom(room); + setScreen(SCREENS.ROOM); + }); + socket.on('roomJoined', (room) => { + setCurrentRoom(room); + setScreen(SCREENS.ROOM); + }); + socket.on('roomUpdated', (room) => setCurrentRoom(room)); + + socket.on('gameStarted', (data) => { + setGameId(data.gameId); + setGameState(data.state); + setScreen(SCREENS.GAME); + }); + + socket.on('gameStateUpdated', (state) => setGameState(state)); + + socket.on('error', (data) => { + setError(data.message); + setTimeout(() => setError(null), 3000); + }); + + socket.on('actionError', (data) => { + setError(data.message); + setTimeout(() => setError(null), 3000); + }); + + socket.on('playerDisconnected', (data) => { + setError(`${data.playerName} disconnected`); + setTimeout(() => setError(null), 3000); + }); + + socket.on('needsResponse', () => {}); + + return () => { + socket.removeAllListeners(); + socket.disconnect(); + }; + }, []); + + const handleSetName = useCallback((e) => { + e.preventDefault(); + if (!playerName.trim()) return; + socket.emit('setName', playerName.trim()); + }, [playerName]); + + const handleBackToLobby = useCallback(() => { + socket.emit('leaveRoom'); + setCurrentRoom(null); + setGameState(null); + setGameId(null); + setScreen(SCREENS.LOBBY); + socket.emit('getRooms'); + }, []); + + if (screen === SCREENS.NAME) { + return ( +
+
+

Arcane Duels

+

A game of strategy, mana, and might

+
+ setPlayerName(e.target.value)} + placeholder="Enter your name..." + className="name-input" + maxLength={20} + autoFocus + /> + +
+
+ {error &&
{error}
} +
+ ); + } + + if (screen === SCREENS.GAME && gameState) { + return ( +
+ + {error &&
{error}
} +
+ ); + } + + return ( +
+ + {error &&
{error}
} +
+ ); +} diff --git a/client/src/components/Card.jsx b/client/src/components/Card.jsx new file mode 100644 index 0000000..142bedf --- /dev/null +++ b/client/src/components/Card.jsx @@ -0,0 +1,128 @@ +import React from 'react'; +import CardArt from './CardArt'; + +const COLOR_MAP = { + radiance: { bg: '#fff8e7', border: '#d4a843', accent: '#f0d060' }, + tide: { bg: '#e7f0ff', border: '#4477bb', accent: '#6699dd' }, + shadow: { bg: '#2a2a2a', border: '#666', accent: '#8a7090', text: '#ddd' }, + flame: { bg: '#fff0e7', border: '#cc4422', accent: '#ee6644' }, + growth: { bg: '#eaf5e7', border: '#448833', accent: '#66aa55' }, + colorless: { bg: '#f0f0f0', border: '#999', accent: '#bbb' }, +}; + +const KEYWORD_DISPLAY = { + swift: 'Swift', + vigilant: 'Vigilant', + soaring: 'Soaring', + guardian: 'Guardian', + fortified: 'Fortified', + draining: 'Draining', + overwhelming: 'Overwhelming', + venomous: 'Venomous', + reaching: 'Reaching', +}; + +function formatManaCost(cost) { + if (!cost) return ''; + const parts = []; + if (cost.colorless) parts.push(String(cost.colorless)); + const symbols = { radiance: '\u2600', tide: '\u{1F30A}', shadow: '\u{1F480}', flame: '\u{1F525}', growth: '\u{1F33F}' }; + for (const [color, sym] of Object.entries(symbols)) { + for (let i = 0; i < (cost[color] || 0); i++) parts.push(sym); + } + return parts.join(' '); +} + +export default function Card({ + card, + instance, + onClick, + selected, + selectable, + small, + faceDown, + zone, +}) { + if (faceDown) { + return ( +
+
Arcane Duels
+
+ ); + } + + const data = instance?.cardData || card; + if (!data) return null; + + const colors = COLOR_MAP[data.color] || COLOR_MAP.colorless; + const isCreature = data.type === 'creature'; + const isLand = data.type === 'land'; + const tapped = instance?.tapped; + const keywords = data.keywords || []; + const power = instance ? instance.effectivePower : data.power; + const toughness = instance ? (instance.effectiveToughness - (instance.damage || 0)) : data.toughness; + + const cardClass = [ + 'card', + `card-${data.type}`, + `color-${data.color}`, + tapped ? 'tapped' : '', + selected ? 'selected' : '', + selectable ? 'selectable' : '', + small ? 'card-small' : '', + zone ? `zone-${zone}` : '', + instance?.summoningSickness ? 'summoning-sick' : '', + ].filter(Boolean).join(' '); + + const style = { + '--card-bg': colors.bg, + '--card-border': colors.border, + '--card-accent': colors.accent, + color: colors.text || '#222', + }; + + return ( +
+
+ {data.name} + {!isLand && {formatManaCost(data.cost)}} +
+ +
+ +
+ +
+ {data.type.charAt(0).toUpperCase() + data.type.slice(1)} + {data.subtype ? ` \u2014 ${data.subtype}` : ''} +
+ +
+ {keywords.length > 0 && ( +
+ {keywords.map((k) => KEYWORD_DISPLAY[k] || k).join(', ')} +
+ )} + {data.flavor && !small && ( +
{data.flavor}
+ )} +
+ + {isCreature && ( +
+ 0 ? 'damaged' : ''}> + {power}/{toughness} + +
+ )} +
+ ); +} + +export { formatManaCost, COLOR_MAP }; diff --git a/client/src/components/CardArt.jsx b/client/src/components/CardArt.jsx new file mode 100644 index 0000000..945d8f1 --- /dev/null +++ b/client/src/components/CardArt.jsx @@ -0,0 +1,940 @@ +import React, { useRef, useEffect, memo } from 'react'; + +function hashString(str) { + let h = 5381; + for (let i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) >>> 0; + return h; +} + +function mulberry32(seed) { + let s = seed | 0; + return () => { + s = (s + 0x6D2B79F5) | 0; + let t = Math.imul(s ^ (s >>> 15), 1 | s); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +function seedRng(name) { + const fn = mulberry32(hashString(name || 'card')); + fn(); fn(); fn(); + return { + f: () => fn(), + r: (a, b) => a + fn() * (b - a), + i: (a, b) => Math.floor(a + fn() * (b - a)), + pick: (arr) => arr[Math.floor(fn() * arr.length)], + }; +} + +const PAL = { + radiance: { + bg: ['#7a6420', '#b89530', '#d4a843', '#f0d060', '#fff8e7'], + fills: ['#f0d060', '#e8c040', '#d4a843', '#ffeebb', '#c49840'], + glow: '#fffce8', + deep: '#5a4010', + }, + tide: { + bg: ['#0d1825', '#1a3355', '#2266aa', '#4499dd'], + fills: ['#3388cc', '#55aaee', '#4499dd', '#77ccff', '#2266aa'], + glow: '#cceeff', + deep: '#08101a', + }, + shadow: { + bg: ['#05020a', '#0f0a18', '#1a1520', '#2a1530'], + fills: ['#5a3070', '#8a6098', '#44224d', '#6b4880', '#3a1845'], + glow: '#c8a0d8', + deep: '#030108', + }, + flame: { + bg: ['#180600', '#331000', '#662200', '#993300'], + fills: ['#ee5533', '#ff8844', '#cc3311', '#ffbb33', '#ff6622'], + glow: '#ffeecc', + deep: '#100300', + }, + growth: { + bg: ['#081a05', '#153010', '#224411', '#336622'], + fills: ['#55aa44', '#88cc66', '#44884a', '#66bb55', '#339933'], + glow: '#d4eec8', + deep: '#040d02', + }, + colorless: { + bg: ['#303030', '#505050', '#707070', '#909090'], + fills: ['#aaaaaa', '#bbbbbb', '#999999', '#cccccc', '#888888'], + glow: '#e8e8ee', + deep: '#202020', + }, +}; + +function drawRadiance(ctx, R, p, w, h) { + const fx = R.r(w * 0.3, w * 0.7); + const fy = R.r(h * 0.15, h * 0.45); + + const bg = ctx.createRadialGradient(fx, fy, 0, fx, fy, Math.max(w, h) * 1.1); + bg.addColorStop(0, '#fff8e7'); + bg.addColorStop(0.25, '#f5e6b8'); + bg.addColorStop(0.5, '#d4a843'); + bg.addColorStop(0.8, '#b89530'); + bg.addColorStop(1, '#7a6420'); + ctx.fillStyle = bg; + ctx.fillRect(0, 0, w, h); + + const rayCount = R.i(7, 14); + for (let i = 0; i < rayCount; i++) { + const angle = R.r(0, Math.PI * 2); + const len = R.r(w * 0.5, Math.max(w, h) * 1.3); + const spread = R.r(0.04, 0.14); + ctx.save(); + ctx.beginPath(); + ctx.moveTo(fx, fy); + ctx.lineTo(fx + Math.cos(angle - spread) * len, fy + Math.sin(angle - spread) * len); + ctx.lineTo(fx + Math.cos(angle + spread) * len, fy + Math.sin(angle + spread) * len); + ctx.closePath(); + const rg = ctx.createLinearGradient(fx, fy, fx + Math.cos(angle) * len, fy + Math.sin(angle) * len); + rg.addColorStop(0, `rgba(255,248,220,${R.r(0.35, 0.65)})`); + rg.addColorStop(0.4, `rgba(240,208,96,${R.r(0.15, 0.35)})`); + rg.addColorStop(1, 'rgba(212,168,67,0)'); + ctx.fillStyle = rg; + ctx.fill(); + ctx.restore(); + } + + const haloCount = R.i(2, 5); + for (let i = 0; i < haloCount; i++) { + const radius = R.r(8, Math.min(w, h) * 0.6); + ctx.save(); + ctx.beginPath(); + ctx.arc(fx, fy, radius, 0, Math.PI * 2); + ctx.strokeStyle = `rgba(255,248,200,${R.r(0.12, 0.35)})`; + ctx.lineWidth = R.r(0.5, 2.5); + ctx.stroke(); + ctx.restore(); + } + + ctx.save(); + const cg = ctx.createRadialGradient(fx, fy, 0, fx, fy, R.r(10, 22)); + cg.addColorStop(0, 'rgba(255,255,240,0.9)'); + cg.addColorStop(0.4, 'rgba(255,240,200,0.4)'); + cg.addColorStop(1, 'rgba(240,208,96,0)'); + ctx.fillStyle = cg; + ctx.fillRect(0, 0, w, h); + ctx.restore(); + + const sparkCount = R.i(18, 40); + for (let i = 0; i < sparkCount; i++) { + const sx = R.r(0, w); + const sy = R.r(0, h); + const ss = R.r(0.4, 2.5); + ctx.save(); + ctx.beginPath(); + ctx.arc(sx, sy, ss, 0, Math.PI * 2); + ctx.fillStyle = `rgba(255,250,210,${R.r(0.2, 0.8)})`; + ctx.fill(); + ctx.restore(); + } + + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + const softCount = R.i(3, 6); + for (let i = 0; i < softCount; i++) { + const sx = R.r(0, w); + const sy = R.r(0, h); + const sr = R.r(10, 30); + const sg = ctx.createRadialGradient(sx, sy, 0, sx, sy, sr); + sg.addColorStop(0, `rgba(240,210,100,${R.r(0.04, 0.12)})`); + sg.addColorStop(1, 'rgba(240,210,100,0)'); + ctx.fillStyle = sg; + ctx.fillRect(sx - sr, sy - sr, sr * 2, sr * 2); + } + ctx.restore(); +} + +function drawTide(ctx, R, p, w, h) { + const bg = ctx.createLinearGradient(R.r(0, w * 0.3), 0, R.r(w * 0.7, w), h); + bg.addColorStop(0, '#0d1825'); + bg.addColorStop(0.35, '#1a3355'); + bg.addColorStop(0.65, '#2266aa'); + bg.addColorStop(1, '#4499dd'); + ctx.fillStyle = bg; + ctx.fillRect(0, 0, w, h); + + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + const deepGlow = ctx.createRadialGradient(w * 0.5, h * 0.7, 0, w * 0.5, h * 0.7, h * 0.6); + deepGlow.addColorStop(0, 'rgba(60,140,200,0.15)'); + deepGlow.addColorStop(1, 'rgba(20,60,100,0)'); + ctx.fillStyle = deepGlow; + ctx.fillRect(0, 0, w, h); + ctx.restore(); + + const bandCount = R.i(4, 8); + for (let b = 0; b < bandCount; b++) { + const baseY = R.r(h * 0.05, h * 0.95); + const amp = R.r(2, h * 0.12); + const freq = R.r(0.03, 0.09); + const phase = R.r(0, Math.PI * 2); + const freq2 = freq * R.r(1.8, 2.8); + const phase2 = R.r(0, Math.PI * 2); + ctx.save(); + ctx.beginPath(); + ctx.moveTo(-2, h + 2); + for (let x = -2; x <= w + 2; x += 2) { + const y = baseY + Math.sin(x * freq + phase) * amp + Math.sin(x * freq2 + phase2) * amp * 0.3; + ctx.lineTo(x, y); + } + ctx.lineTo(w + 2, h + 2); + ctx.closePath(); + ctx.globalAlpha = R.r(0.08, 0.28); + ctx.fillStyle = R.pick(p.fills); + ctx.fill(); + ctx.globalAlpha = 1; + ctx.restore(); + } + + const rippleCount = R.i(2, 5); + for (let i = 0; i < rippleCount; i++) { + const cx = R.r(w * 0.1, w * 0.9); + const cy = R.r(h * 0.1, h * 0.9); + const rings = R.i(2, 5); + for (let r = 0; r < rings; r++) { + const radius = R.r(4, 12) + r * R.r(3, 7); + ctx.save(); + ctx.beginPath(); + ctx.arc(cx, cy, radius, 0, Math.PI * 2); + ctx.strokeStyle = `rgba(180,220,255,${R.r(0.06, 0.22)})`; + ctx.lineWidth = R.r(0.3, 1.2); + ctx.stroke(); + ctx.restore(); + } + } + + const causticCount = R.i(5, 14); + for (let i = 0; i < causticCount; i++) { + const cx = R.r(0, w); + const cy = R.r(0, h); + const cr = R.r(3, 10); + ctx.save(); + const cg = ctx.createRadialGradient(cx, cy, 0, cx, cy, cr); + cg.addColorStop(0, `rgba(200,235,255,${R.r(0.08, 0.25)})`); + cg.addColorStop(1, 'rgba(200,235,255,0)'); + ctx.fillStyle = cg; + ctx.fillRect(cx - cr, cy - cr, cr * 2, cr * 2); + ctx.restore(); + } + + const foamCount = R.i(12, 30); + for (let i = 0; i < foamCount; i++) { + ctx.save(); + ctx.beginPath(); + ctx.arc(R.r(0, w), R.r(0, h), R.r(0.3, 1.8), 0, Math.PI * 2); + ctx.fillStyle = `rgba(210,235,255,${R.r(0.12, 0.5)})`; + ctx.fill(); + ctx.restore(); + } +} + +function drawShadow(ctx, R, p, w, h) { + const cx = R.r(w * 0.3, w * 0.7); + const cy = R.r(h * 0.3, h * 0.7); + const bg = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(w, h) * 0.8); + bg.addColorStop(0, '#2a1530'); + bg.addColorStop(0.4, '#1a1520'); + bg.addColorStop(0.7, '#0f0a18'); + bg.addColorStop(1, '#05020a'); + ctx.fillStyle = bg; + ctx.fillRect(0, 0, w, h); + + const tendrilCount = R.i(5, 9); + for (let i = 0; i < tendrilCount; i++) { + const edge = R.i(0, 4); + let sx, sy; + if (edge === 0) { sx = R.r(0, w); sy = -2; } + else if (edge === 1) { sx = w + 2; sy = R.r(0, h); } + else if (edge === 2) { sx = R.r(0, w); sy = h + 2; } + else { sx = -2; sy = R.r(0, h); } + const ex = R.r(w * 0.15, w * 0.85); + const ey = R.r(h * 0.15, h * 0.85); + const cp1x = R.r(0, w); + const cp1y = R.r(0, h); + const cp2x = R.r(0, w); + const cp2y = R.r(0, h); + ctx.save(); + ctx.beginPath(); + ctx.moveTo(sx, sy); + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, ex, ey); + ctx.strokeStyle = `rgba(${R.i(60, 110)},${R.i(30, 70)},${R.i(80, 140)},${R.r(0.15, 0.45)})`; + ctx.lineWidth = R.r(1.5, 5); + ctx.lineCap = 'round'; + ctx.stroke(); + ctx.restore(); + } + + const wispCount = R.i(5, 10); + for (let i = 0; i < wispCount; i++) { + const wx = R.r(-5, w + 5); + const wy = R.r(-5, h + 5); + const wr = R.r(8, 28); + ctx.save(); + const wg = ctx.createRadialGradient(wx, wy, 0, wx, wy, wr); + wg.addColorStop(0, `rgba(${R.i(70, 120)},${R.i(40, 80)},${R.i(90, 140)},${R.r(0.08, 0.25)})`); + wg.addColorStop(0.6, `rgba(50,25,70,${R.r(0.03, 0.1)})`); + wg.addColorStop(1, 'rgba(30,15,50,0)'); + ctx.fillStyle = wg; + ctx.fillRect(wx - wr, wy - wr, wr * 2, wr * 2); + ctx.restore(); + } + + const shardCount = R.i(3, 8); + for (let i = 0; i < shardCount; i++) { + const scx = R.r(0, w); + const scy = R.r(0, h); + const ssize = R.r(3, 14); + const sangle = R.r(0, Math.PI * 2); + const pts = R.i(3, 6); + ctx.save(); + ctx.beginPath(); + for (let j = 0; j < pts; j++) { + const a = sangle + (j / pts) * Math.PI * 2; + const sr = ssize * (0.4 + R.f() * 0.6); + const px = scx + Math.cos(a) * sr; + const py = scy + Math.sin(a) * sr; + if (j === 0) ctx.moveTo(px, py); + else ctx.lineTo(px, py); + } + ctx.closePath(); + ctx.fillStyle = `rgba(${R.i(80, 150)},${R.i(40, 100)},${R.i(100, 170)},${R.r(0.08, 0.25)})`; + ctx.fill(); + ctx.restore(); + } + + const glowX = R.r(w * 0.2, w * 0.8); + const glowY = R.r(h * 0.2, h * 0.8); + const glowR = R.r(12, 28); + ctx.save(); + const gg = ctx.createRadialGradient(glowX, glowY, 0, glowX, glowY, glowR); + const glowTint = R.pick([ + `rgba(90,200,120,${R.r(0.06, 0.15)})`, + `rgba(160,80,80,${R.r(0.06, 0.15)})`, + `rgba(100,80,180,${R.r(0.08, 0.18)})`, + ]); + gg.addColorStop(0, glowTint); + gg.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = gg; + ctx.fillRect(0, 0, w, h); + ctx.restore(); + + const partCount = R.i(15, 35); + for (let i = 0; i < partCount; i++) { + ctx.save(); + ctx.beginPath(); + ctx.arc(R.r(0, w), R.r(0, h), R.r(0.3, 1.5), 0, Math.PI * 2); + ctx.fillStyle = `rgba(${R.i(100, 190)},${R.i(60, 130)},${R.i(120, 210)},${R.r(0.15, 0.55)})`; + ctx.fill(); + ctx.restore(); + } +} + +function drawFlame(ctx, R, p, w, h) { + const bg = ctx.createLinearGradient(0, 0, 0, h); + bg.addColorStop(0, '#180600'); + bg.addColorStop(0.3, '#331000'); + bg.addColorStop(0.6, '#662200'); + bg.addColorStop(0.85, '#993300'); + bg.addColorStop(1, '#bb4400'); + ctx.fillStyle = bg; + ctx.fillRect(0, 0, w, h); + + const hotX = R.r(w * 0.25, w * 0.75); + ctx.save(); + const hg = ctx.createRadialGradient(hotX, h + 5, 0, hotX, h * 0.5, h * 0.9); + hg.addColorStop(0, 'rgba(255,200,50,0.45)'); + hg.addColorStop(0.35, 'rgba(255,120,30,0.25)'); + hg.addColorStop(0.7, 'rgba(180,50,10,0.1)'); + hg.addColorStop(1, 'rgba(80,15,0,0)'); + ctx.fillStyle = hg; + ctx.fillRect(0, 0, w, h); + ctx.restore(); + + const flameCount = R.i(6, 12); + for (let i = 0; i < flameCount; i++) { + const bx = R.r(w * -0.1, w * 1.1); + const halfW = R.r(4, 16); + const fh = R.r(h * 0.25, h * 0.95); + const lean = R.r(-10, 10); + ctx.save(); + ctx.beginPath(); + ctx.moveTo(bx - halfW, h + 2); + ctx.bezierCurveTo( + bx - halfW * R.r(0.3, 0.8) + lean * 0.3, h - fh * R.r(0.2, 0.45), + bx + lean * 0.6 - halfW * R.r(0.1, 0.3), h - fh * R.r(0.6, 0.85), + bx + lean, h - fh + ); + ctx.bezierCurveTo( + bx + lean * 0.6 + halfW * R.r(0.1, 0.3), h - fh * R.r(0.6, 0.85), + bx + halfW * R.r(0.3, 0.8) + lean * 0.3, h - fh * R.r(0.2, 0.45), + bx + halfW, h + 2 + ); + ctx.closePath(); + const fg = ctx.createLinearGradient(bx, h, bx + lean, h - fh); + fg.addColorStop(0, `rgba(255,220,60,${R.r(0.3, 0.6)})`); + fg.addColorStop(0.3, `rgba(255,140,30,${R.r(0.25, 0.5)})`); + fg.addColorStop(0.6, `rgba(230,70,20,${R.r(0.15, 0.35)})`); + fg.addColorStop(1, `rgba(150,30,10,${R.r(0.05, 0.15)})`); + ctx.fillStyle = fg; + ctx.fill(); + ctx.restore(); + } + + const emberCount = R.i(25, 55); + for (let i = 0; i < emberCount; i++) { + const ex = R.r(0, w); + const ey = R.r(h * 0.05, h); + const es = R.r(0.4, 2.8); + const bright = R.f(); + const er = Math.floor(200 + bright * 55); + const eg = Math.floor(80 + bright * 120); + const eb = Math.floor(bright * 50); + ctx.save(); + ctx.beginPath(); + ctx.arc(ex, ey, es, 0, Math.PI * 2); + ctx.fillStyle = `rgba(${er},${eg},${eb},${R.r(0.3, 0.9)})`; + ctx.fill(); + ctx.restore(); + } + + ctx.save(); + ctx.globalAlpha = 0.12; + for (let i = 0; i < 4; i++) { + const sy = R.r(1, h * 0.25); + ctx.beginPath(); + ctx.moveTo(0, sy); + const shimFreq = R.r(0.08, 0.2); + const shimPhase = R.r(0, 10); + for (let x = 0; x <= w; x += 2) { + ctx.lineTo(x, sy + Math.sin(x * shimFreq + shimPhase) * R.r(1, 3)); + } + ctx.strokeStyle = R.pick(['#ffaa44', '#ff8833', '#ffcc66']); + ctx.lineWidth = R.r(0.4, 1.2); + ctx.stroke(); + } + ctx.restore(); +} + +function drawGrowth(ctx, R, p, w, h) { + const bg = ctx.createLinearGradient(0, 0, R.r(0, w * 0.3), h); + bg.addColorStop(0, '#0e2208'); + bg.addColorStop(0.4, '#1a3310'); + bg.addColorStop(0.7, '#224411'); + bg.addColorStop(1, '#2a5520'); + ctx.fillStyle = bg; + ctx.fillRect(0, 0, w, h); + + const lightCount = R.i(1, 3); + for (let i = 0; i < lightCount; i++) { + const lx = R.r(w * 0.1, w * 0.9); + const ly = R.r(-5, h * 0.3); + const lr = R.r(15, 40); + ctx.save(); + const lg = ctx.createRadialGradient(lx, ly, 0, lx, ly, lr); + lg.addColorStop(0, `rgba(180,220,140,${R.r(0.12, 0.3)})`); + lg.addColorStop(0.5, `rgba(120,180,90,${R.r(0.04, 0.12)})`); + lg.addColorStop(1, 'rgba(80,140,60,0)'); + ctx.fillStyle = lg; + ctx.fillRect(lx - lr, ly - lr, lr * 2, lr * 2); + ctx.restore(); + } + + const hillCount = R.i(3, 5); + for (let hi = 0; hi < hillCount; hi++) { + const hillBase = h - R.r(h * 0.05, h * 0.45); + const hillFreq = R.r(0.015, 0.06); + const hillAmp = R.r(3, 12); + const hillPhase = R.r(0, 20); + ctx.save(); + ctx.beginPath(); + ctx.moveTo(-2, h + 2); + for (let x = -2; x <= w + 2; x += 2) { + const y = hillBase + Math.sin(x * hillFreq + hillPhase) * hillAmp + + Math.sin(x * hillFreq * 2.7 + hillPhase * 1.3) * hillAmp * 0.3; + ctx.lineTo(x, y); + } + ctx.lineTo(w + 2, h + 2); + ctx.closePath(); + ctx.globalAlpha = R.r(0.15, 0.45); + ctx.fillStyle = R.pick(p.fills); + ctx.fill(); + ctx.restore(); + } + + const branchCount = R.i(2, 5); + for (let b = 0; b < branchCount; b++) { + const bStartX = R.pick([R.r(-5, w * 0.15), R.r(w * 0.85, w + 5)]); + const bStartY = R.r(h * 0.4, h + 5); + const bAngle = bStartX < w * 0.5 ? R.r(-1.2, -0.3) : R.r(-2.8, -1.9); + drawBranch(ctx, R, bStartX, bStartY, bAngle, R.r(12, 28), R.i(2, 4), R.r(1.5, 3.5)); + } + + const leafCount = R.i(10, 25); + for (let i = 0; i < leafCount; i++) { + const lx = R.r(0, w); + const ly = R.r(0, h); + const ls = R.r(1.5, 5); + const la = R.r(0, Math.PI * 2); + ctx.save(); + ctx.translate(lx, ly); + ctx.rotate(la); + ctx.beginPath(); + ctx.ellipse(0, 0, ls, ls * 0.35, 0, 0, Math.PI * 2); + ctx.fillStyle = `rgba(${R.i(70, 150)},${R.i(150, 230)},${R.i(50, 120)},${R.r(0.12, 0.4)})`; + ctx.fill(); + ctx.restore(); + } + + const dotCount = R.i(10, 20); + for (let i = 0; i < dotCount; i++) { + ctx.save(); + ctx.beginPath(); + ctx.arc(R.r(0, w), R.r(0, h), R.r(0.3, 1.3), 0, Math.PI * 2); + ctx.fillStyle = `rgba(200,240,170,${R.r(0.15, 0.55)})`; + ctx.fill(); + ctx.restore(); + } +} + +function drawBranch(ctx, R, x, y, angle, length, depth, thickness) { + if (depth <= 0 || length < 2) return; + const ex = x + Math.cos(angle) * length; + const ey = y + Math.sin(angle) * length; + ctx.save(); + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(ex, ey); + ctx.strokeStyle = `rgba(${R.i(50, 90)},${R.i(70, 130)},${R.i(30, 70)},${R.r(0.25, 0.6)})`; + ctx.lineWidth = thickness; + ctx.lineCap = 'round'; + ctx.stroke(); + ctx.restore(); + const forks = R.i(1, 3); + for (let i = 0; i < forks; i++) { + drawBranch(ctx, R, ex, ey, angle + R.r(-0.9, 0.9), length * R.r(0.45, 0.75), depth - 1, thickness * 0.55); + } +} + +function drawColorless(ctx, R, p, w, h) { + const ga = R.r(0, Math.PI); + const gx = Math.cos(ga); + const gy = Math.sin(ga); + const bg = ctx.createLinearGradient( + w * 0.5 - gx * w * 0.6, h * 0.5 - gy * h * 0.6, + w * 0.5 + gx * w * 0.6, h * 0.5 + gy * h * 0.6 + ); + bg.addColorStop(0, '#383838'); + bg.addColorStop(0.3, '#606068'); + bg.addColorStop(0.5, '#808088'); + bg.addColorStop(0.7, '#606068'); + bg.addColorStop(1, '#404048'); + ctx.fillStyle = bg; + ctx.fillRect(0, 0, w, h); + + const hexSize = R.r(7, 13); + const hexH = hexSize * Math.sqrt(3); + const hexOff = R.r(-0.25, 0.25); + ctx.save(); + ctx.translate(R.r(-8, 8), R.r(-8, 8)); + ctx.rotate(hexOff); + for (let row = -2; row < h / hexH + 3; row++) { + for (let col = -2; col < w / (hexSize * 1.5) + 3; col++) { + const hcx = col * hexSize * 1.5; + const hcy = row * hexH + (col % 2 ? hexH * 0.5 : 0); + ctx.beginPath(); + for (let i = 0; i < 6; i++) { + const a = (Math.PI / 3) * i + Math.PI / 6; + const px = hcx + Math.cos(a) * hexSize * 0.88; + const py = hcy + Math.sin(a) * hexSize * 0.88; + if (i === 0) ctx.moveTo(px, py); + else ctx.lineTo(px, py); + } + ctx.closePath(); + ctx.strokeStyle = `rgba(190,195,210,${R.r(0.04, 0.18)})`; + ctx.lineWidth = R.r(0.3, 0.9); + ctx.stroke(); + } + } + ctx.restore(); + + const crystalCount = R.i(3, 7); + for (let i = 0; i < crystalCount; i++) { + const ccx = R.r(w * 0.05, w * 0.95); + const ccy = R.r(h * 0.05, h * 0.95); + const csize = R.r(5, 18); + const csides = R.pick([4, 5, 6, 8]); + const crot = R.r(0, Math.PI * 2); + ctx.save(); + ctx.beginPath(); + for (let s = 0; s < csides; s++) { + const a = crot + (s / csides) * Math.PI * 2; + const cr = csize * (0.6 + R.f() * 0.4); + const px = ccx + Math.cos(a) * cr; + const py = ccy + Math.sin(a) * cr; + if (s === 0) ctx.moveTo(px, py); + else ctx.lineTo(px, py); + } + ctx.closePath(); + ctx.fillStyle = `rgba(170,180,200,${R.r(0.04, 0.18)})`; + ctx.fill(); + ctx.strokeStyle = `rgba(210,220,240,${R.r(0.08, 0.3)})`; + ctx.lineWidth = R.r(0.3, 1); + ctx.stroke(); + ctx.restore(); + } + + const lineCount = R.i(4, 10); + for (let i = 0; i < lineCount; i++) { + ctx.save(); + ctx.beginPath(); + ctx.moveTo(R.r(0, w), R.r(0, h)); + ctx.lineTo(R.r(0, w), R.r(0, h)); + ctx.strokeStyle = `rgba(220,225,240,${R.r(0.04, 0.16)})`; + ctx.lineWidth = R.r(0.2, 0.8); + ctx.stroke(); + ctx.restore(); + } + + const sheenX = R.r(w * 0.15, w * 0.85); + ctx.save(); + const sg = ctx.createLinearGradient(sheenX - 18, 0, sheenX + 18, 0); + sg.addColorStop(0, 'rgba(255,255,255,0)'); + sg.addColorStop(0.35, 'rgba(255,255,255,0.06)'); + sg.addColorStop(0.5, 'rgba(255,255,255,0.12)'); + sg.addColorStop(0.65, 'rgba(255,255,255,0.06)'); + sg.addColorStop(1, 'rgba(255,255,255,0)'); + ctx.fillStyle = sg; + ctx.fillRect(0, 0, w, h); + ctx.restore(); +} + +const COLOR_DRAW = { + radiance: drawRadiance, + tide: drawTide, + shadow: drawShadow, + flame: drawFlame, + growth: drawGrowth, + colorless: drawColorless, +}; + +function drawCreature(ctx, R, p, w, h) { + const silType = R.i(0, 5); + const cx = w * 0.5; + const cy = h * 0.5; + const sc = Math.min(w, h) * 0.28; + ctx.save(); + ctx.globalAlpha = R.r(0.07, 0.18); + ctx.fillStyle = p.glow; + + if (silType === 0) { + ctx.beginPath(); + ctx.arc(cx, cy - sc * 0.65, sc * 0.22, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(cx - sc * 0.45, cy + sc * 0.85); + ctx.lineTo(cx - sc * 0.35, cy - sc * 0.25); + ctx.lineTo(cx, cy - sc * 0.4); + ctx.lineTo(cx + sc * 0.35, cy - sc * 0.25); + ctx.lineTo(cx + sc * 0.45, cy + sc * 0.85); + ctx.closePath(); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(cx - sc * 0.35, cy - sc * 0.1); + ctx.lineTo(cx - sc * 0.7, cy + sc * 0.15); + ctx.lineTo(cx - sc * 0.65, cy + sc * 0.25); + ctx.lineTo(cx - sc * 0.3, cy + sc * 0.05); + ctx.closePath(); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(cx + sc * 0.35, cy - sc * 0.1); + ctx.lineTo(cx + sc * 0.7, cy + sc * 0.15); + ctx.lineTo(cx + sc * 0.65, cy + sc * 0.25); + ctx.lineTo(cx + sc * 0.3, cy + sc * 0.05); + ctx.closePath(); + ctx.fill(); + } else if (silType === 1) { + ctx.beginPath(); + ctx.ellipse(cx, cy, sc * 0.55, sc * 0.3, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(cx + sc * 0.5, cy - sc * 0.22, sc * 0.2, sc * 0.18, -0.3, 0, Math.PI * 2); + ctx.fill(); + const legW = sc * 0.07; + [cx - sc * 0.38, cx - sc * 0.15, cx + sc * 0.12, cx + sc * 0.35].forEach(function(lx) { + ctx.fillRect(lx - legW, cy + sc * 0.22, legW * 2, sc * 0.55); + }); + } else if (silType === 2) { + ctx.beginPath(); + ctx.ellipse(cx, cy, sc * 0.18, sc * 0.35, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(cx - sc * 0.1, cy - sc * 0.15); + ctx.quadraticCurveTo(cx - sc * 0.85, cy - sc * 0.7, cx - sc * 0.75, cy + sc * 0.15); + ctx.lineTo(cx - sc * 0.2, cy + sc * 0.05); + ctx.closePath(); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(cx + sc * 0.1, cy - sc * 0.15); + ctx.quadraticCurveTo(cx + sc * 0.85, cy - sc * 0.7, cx + sc * 0.75, cy + sc * 0.15); + ctx.lineTo(cx + sc * 0.2, cy + sc * 0.05); + ctx.closePath(); + ctx.fill(); + } else if (silType === 3) { + ctx.strokeStyle = p.glow; + ctx.lineWidth = sc * 0.18; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(cx - sc * 0.6, cy + sc * 0.4); + ctx.bezierCurveTo( + cx - sc * 0.2, cy - sc * 0.6, + cx + sc * 0.2, cy + sc * 0.6, + cx + sc * 0.6, cy - sc * 0.4 + ); + ctx.stroke(); + } else { + ctx.fillRect(cx - sc * 0.45, cy - sc * 0.35, sc * 0.9, sc * 0.9); + ctx.fillRect(cx - sc * 0.2, cy - sc * 0.75, sc * 0.4, sc * 0.45); + } + ctx.restore(); +} + +function drawSpell(ctx, R, p, w, h) { + const cx = w * 0.5 + R.r(-w * 0.12, w * 0.12); + const cy = h * 0.5 + R.r(-h * 0.12, h * 0.12); + const radius = Math.min(w, h) * R.r(0.28, 0.42); + ctx.save(); + ctx.globalAlpha = R.r(0.12, 0.28); + ctx.strokeStyle = p.glow; + ctx.lineWidth = R.r(0.5, 1.5); + + ctx.beginPath(); + ctx.arc(cx, cy, radius, 0, Math.PI * 2); + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(cx, cy, radius * 0.55, 0, Math.PI * 2); + ctx.stroke(); + + const spokes = R.i(4, 9); + const spokeOff = R.r(0, Math.PI * 2); + for (let i = 0; i < spokes; i++) { + const a = spokeOff + (i / spokes) * Math.PI * 2; + ctx.beginPath(); + ctx.moveTo(cx + Math.cos(a) * radius * 0.55, cy + Math.sin(a) * radius * 0.55); + ctx.lineTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius); + ctx.stroke(); + } + + const runeCount = R.i(6, 14); + ctx.fillStyle = p.glow; + for (let i = 0; i < runeCount; i++) { + const a = (i / runeCount) * Math.PI * 2; + const dr = radius * R.r(0.75, 1.25); + ctx.beginPath(); + ctx.arc(cx + Math.cos(a) * dr, cy + Math.sin(a) * dr, R.r(0.5, 2), 0, Math.PI * 2); + ctx.fill(); + } + + ctx.beginPath(); + ctx.arc(cx, cy, radius * 0.15, 0, Math.PI * 2); + ctx.fillStyle = p.glow; + ctx.fill(); + + ctx.restore(); +} + +function drawEnchantment(ctx, R, p, w, h) { + ctx.save(); + const edgeSize = R.r(5, 12); + ctx.globalAlpha = R.r(0.08, 0.2); + + let g = ctx.createLinearGradient(0, 0, 0, edgeSize); + g.addColorStop(0, p.glow); + g.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, w, edgeSize); + + g = ctx.createLinearGradient(0, h, 0, h - edgeSize); + g.addColorStop(0, p.glow); + g.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = g; + ctx.fillRect(0, h - edgeSize, w, edgeSize); + + g = ctx.createLinearGradient(0, 0, edgeSize, 0); + g.addColorStop(0, p.glow); + g.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = g; + ctx.fillRect(0, 0, edgeSize, h); + + g = ctx.createLinearGradient(w, 0, w - edgeSize, 0); + g.addColorStop(0, p.glow); + g.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = g; + ctx.fillRect(w - edgeSize, 0, edgeSize, h); + + const pCount = R.i(6, 16); + for (let i = 0; i < pCount; i++) { + const px = R.r(0, w); + const py = R.r(0, h); + const pr = R.r(1.5, 4); + const pg = ctx.createRadialGradient(px, py, 0, px, py, pr); + pg.addColorStop(0, p.glow); + pg.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.globalAlpha = R.r(0.1, 0.35); + ctx.fillStyle = pg; + ctx.fillRect(px - pr, py - pr, pr * 2, pr * 2); + } + ctx.restore(); +} + +function drawLand(ctx, R, p, w, h) { + ctx.save(); + const horizonY = h * R.r(0.32, 0.52); + + const skyGrad = ctx.createLinearGradient(0, 0, 0, horizonY); + skyGrad.addColorStop(0, p.deep); + skyGrad.addColorStop(1, R.pick(p.fills)); + ctx.globalAlpha = 0.3; + ctx.fillStyle = skyGrad; + ctx.fillRect(0, 0, w, horizonY); + + ctx.beginPath(); + ctx.moveTo(-2, h + 2); + const tFreq = R.r(0.02, 0.07); + const tAmp = R.r(2, 8); + const tPhase = R.r(0, 20); + for (let x = -2; x <= w + 2; x += 2) { + ctx.lineTo(x, horizonY + Math.sin(x * tFreq + tPhase) * tAmp + Math.sin(x * tFreq * 2.5 + tPhase * 0.7) * tAmp * 0.4); + } + ctx.lineTo(w + 2, h + 2); + ctx.closePath(); + ctx.globalAlpha = 0.22; + ctx.fillStyle = R.pick(p.fills); + ctx.fill(); + + const hGlow = ctx.createLinearGradient(0, horizonY - 6, 0, horizonY + 6); + hGlow.addColorStop(0, 'rgba(0,0,0,0)'); + hGlow.addColorStop(0.5, p.glow); + hGlow.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.globalAlpha = 0.18; + ctx.fillStyle = hGlow; + ctx.fillRect(0, horizonY - 6, w, 12); + + const bodyR = R.r(6, 14); + const bodyX = R.r(w * 0.2, w * 0.8); + const bodyY = R.r(2, horizonY * 0.5); + const bodyGrad = ctx.createRadialGradient(bodyX, bodyY, 0, bodyX, bodyY, bodyR); + bodyGrad.addColorStop(0, p.glow); + bodyGrad.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.globalAlpha = 0.15; + ctx.fillStyle = bodyGrad; + ctx.fillRect(bodyX - bodyR, bodyY - bodyR, bodyR * 2, bodyR * 2); + + ctx.restore(); +} + +function drawArtifact(ctx, R, p, w, h) { + ctx.save(); + const gearCount = R.i(1, 4); + for (let gi = 0; gi < gearCount; gi++) { + const gcx = R.r(w * 0.15, w * 0.85); + const gcy = R.r(h * 0.15, h * 0.85); + const outerR = R.r(5, 16); + const teeth = R.i(6, 14); + const toothH = outerR * 0.28; + const innerR = outerR * 0.65; + ctx.globalAlpha = R.r(0.08, 0.22); + ctx.strokeStyle = p.glow; + ctx.lineWidth = R.r(0.4, 1.2); + ctx.beginPath(); + for (let t = 0; t < teeth; t++) { + const a1 = (t / teeth) * Math.PI * 2; + const a2 = ((t + 0.3) / teeth) * Math.PI * 2; + const a3 = ((t + 0.7) / teeth) * Math.PI * 2; + const a4 = ((t + 1) / teeth) * Math.PI * 2; + ctx.lineTo(gcx + Math.cos(a1) * innerR, gcy + Math.sin(a1) * innerR); + ctx.lineTo(gcx + Math.cos(a2) * (innerR + toothH), gcy + Math.sin(a2) * (innerR + toothH)); + ctx.lineTo(gcx + Math.cos(a3) * (innerR + toothH), gcy + Math.sin(a3) * (innerR + toothH)); + ctx.lineTo(gcx + Math.cos(a4) * innerR, gcy + Math.sin(a4) * innerR); + } + ctx.closePath(); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(gcx, gcy, outerR * 0.22, 0, Math.PI * 2); + ctx.stroke(); + } + ctx.restore(); +} + +const TYPE_DRAW = { + creature: drawCreature, + sorcery: drawSpell, + instant: drawSpell, + enchantment: drawEnchantment, + land: drawLand, + artifact: drawArtifact, +}; + +function drawVignette(ctx, w, h) { + ctx.save(); + const vg = ctx.createRadialGradient(w * 0.5, h * 0.5, Math.min(w, h) * 0.25, w * 0.5, h * 0.5, Math.max(w, h) * 0.85); + vg.addColorStop(0, 'rgba(0,0,0,0)'); + vg.addColorStop(1, 'rgba(0,0,0,0.35)'); + ctx.fillStyle = vg; + ctx.fillRect(0, 0, w, h); + ctx.restore(); +} + +function drawGrain(ctx, R, w, h) { + ctx.save(); + const count = Math.floor(w * h * 0.04); + for (let i = 0; i < count; i++) { + const gx = Math.floor(R.f() * w); + const gy = Math.floor(R.f() * h); + const v = R.i(0, 256); + ctx.fillStyle = `rgba(${v},${v},${v},${R.r(0.01, 0.04)})`; + ctx.fillRect(gx, gy, 1, 1); + } + ctx.restore(); +} + +const CardArt = memo(function CardArt({ cardName, color = 'colorless', type = 'creature', width = 130, height = 70 }) { + const ref = useRef(null); + + useEffect(() => { + const canvas = ref.current; + if (!canvas) return; + + const dpr = window.devicePixelRatio || 1; + canvas.width = width * dpr; + canvas.height = height * dpr; + const ctx = canvas.getContext('2d'); + ctx.scale(dpr, dpr); + + const R = seedRng(cardName); + const p = PAL[color] || PAL.colorless; + + const colorFn = COLOR_DRAW[color] || COLOR_DRAW.colorless; + colorFn(ctx, R, p, width, height); + + const typeFn = TYPE_DRAW[type]; + if (typeFn) typeFn(ctx, R, p, width, height); + + drawVignette(ctx, width, height); + drawGrain(ctx, R, width, height); + }, [cardName, color, type, width, height]); + + return ( + + ); +}); + +export default CardArt; diff --git a/client/src/components/GameBoard.jsx b/client/src/components/GameBoard.jsx new file mode 100644 index 0000000..137d786 --- /dev/null +++ b/client/src/components/GameBoard.jsx @@ -0,0 +1,504 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import socket from '../socket'; +import Card from './Card'; + +const PHASE_LABELS = { + untap: 'Untap', + upkeep: 'Upkeep', + draw: 'Draw', + main1: 'Main 1', + combat_begin: 'Combat', + combat_attackers: 'Attackers', + combat_blockers: 'Blockers', + combat_damage: 'Damage', + combat_end: 'End Combat', + main2: 'Main 2', + end_step: 'End', + cleanup: 'Cleanup', +}; + +const MANA_SYMBOLS = { + radiance: '\u2600', + tide: '\u{1F30A}', + shadow: '\u{1F480}', + flame: '\u{1F525}', + growth: '\u{1F33F}', + colorless: '\u26AA', +}; + +export default function GameBoard({ gameState, playerId, onLeave }) { + const [selectedHandIndex, setSelectedHandIndex] = useState(null); + const [selectedAttackers, setSelectedAttackers] = useState([]); + const [selectedBlockers, setSelectedBlockers] = useState({}); + const [targetMode, setTargetMode] = useState(null); + const [pendingSpell, setPendingSpell] = useState(null); + const [xAmount, setXAmount] = useState(0); + const [manaColorChoice, setManaColorChoice] = useState(null); + const [showLog, setShowLog] = useState(false); + + const isMyTurn = gameState.activePlayerId === playerId; + const phase = gameState.currentPhase; + const me = gameState.players[playerId]; + const opponentId = Object.keys(gameState.players).find((id) => id !== playerId); + const opponent = gameState.players[opponentId]; + + useEffect(() => { + const handleNeedsResponse = (data) => { + if (data.type === 'chooseManaColor') { + setManaColorChoice(data.options); + } + }; + socket.on('needsResponse', handleNeedsResponse); + return () => socket.off('needsResponse', handleNeedsResponse); + }, []); + + useEffect(() => { + setSelectedAttackers([]); + setSelectedBlockers({}); + }, [phase]); + + const sendAction = useCallback((action) => { + socket.emit('gameAction', { action }); + }, []); + + const handleTapLand = useCallback((instanceId) => { + sendAction({ type: 'tapLand', instanceId }); + }, [sendAction]); + + const handleTapAbility = useCallback((instanceId, sacrifice = false) => { + sendAction({ type: 'tapAbility', instanceId, sacrifice }); + }, [sendAction]); + + const handlePlayCard = useCallback((handIndex) => { + const cardData = me.hand[handIndex]; + if (!cardData) return; + + if (cardData.type === 'land') { + sendAction({ type: 'playLand', handIndex }); + setSelectedHandIndex(null); + return; + } + + const needsTarget = cardData.abilities?.some((a) => + ['destroyCreature', 'destroyCreatureIf', 'bounceCreature', 'pumpCreature', + 'dealDamage', 'drainLife', 'tapCreature', 'giveKeyword', 'preventDamage'].includes(a.effect) + ); + + const hasX = cardData.abilities?.some((a) => a.isX); + + if (hasX) { + setPendingSpell({ handIndex, needsTarget, cardData }); + setTargetMode(null); + return; + } + + if (needsTarget) { + setPendingSpell({ handIndex, needsTarget: true, cardData, xAmount: 0 }); + setTargetMode('creature'); + return; + } + + const canTargetPlayer = cardData.abilities?.some((a) => + a.target === 'any' && ['dealDamage', 'drainLife'].includes(a.effect) + ); + + if (canTargetPlayer) { + setPendingSpell({ handIndex, needsTarget: true, cardData, xAmount: 0, canTargetPlayer: true }); + setTargetMode('any'); + return; + } + + sendAction({ type: 'castSpell', handIndex }); + setSelectedHandIndex(null); + }, [me, sendAction]); + + const handleTargetCreature = useCallback((instanceId) => { + if (!pendingSpell) return; + sendAction({ + type: 'castSpell', + handIndex: pendingSpell.handIndex, + targetInstanceId: instanceId, + xAmount: pendingSpell.xAmount || xAmount, + }); + setPendingSpell(null); + setTargetMode(null); + setSelectedHandIndex(null); + setXAmount(0); + }, [pendingSpell, xAmount, sendAction]); + + const handleTargetPlayer = useCallback((targetPid) => { + if (!pendingSpell) return; + sendAction({ + type: 'castSpell', + handIndex: pendingSpell.handIndex, + targetPlayerId: targetPid, + xAmount: pendingSpell.xAmount || xAmount, + }); + setPendingSpell(null); + setTargetMode(null); + setSelectedHandIndex(null); + setXAmount(0); + }, [pendingSpell, xAmount, sendAction]); + + const handleDeclareAttackers = useCallback(() => { + sendAction({ type: 'declareAttackers', attackerIds: selectedAttackers }); + setSelectedAttackers([]); + }, [selectedAttackers, sendAction]); + + const handleDeclareBlockers = useCallback(() => { + sendAction({ type: 'declareBlockers', blockerAssignments: selectedBlockers }); + setSelectedBlockers({}); + }, [selectedBlockers, sendAction]); + + const handlePassPhase = useCallback(() => { + sendAction({ type: 'passPriority' }); + }, [sendAction]); + + const handleConcede = useCallback(() => { + if (window.confirm('Are you sure you want to concede?')) { + sendAction({ type: 'concede' }); + } + }, [sendAction]); + + const handleManaColorChoice = useCallback((color) => { + sendAction({ type: 'chooseManaColor', color }); + setManaColorChoice(null); + }, [sendAction]); + + const toggleAttacker = useCallback((instanceId) => { + setSelectedAttackers((prev) => + prev.includes(instanceId) + ? prev.filter((id) => id !== instanceId) + : [...prev, instanceId] + ); + }, []); + + const toggleBlocker = useCallback((blockerId, attackerId) => { + setSelectedBlockers((prev) => { + const next = { ...prev }; + if (next[blockerId] === attackerId) { + delete next[blockerId]; + } else { + next[blockerId] = attackerId; + } + return next; + }); + }, []); + + const handleBattlefieldClick = useCallback((inst, isOpponent) => { + if (targetMode && pendingSpell) { + handleTargetCreature(inst.instanceId); + return; + } + + if (isOpponent) return; + + if (phase === 'combat_attackers' && isMyTurn && inst.cardData.type === 'creature') { + toggleAttacker(inst.instanceId); + return; + } + + if (inst.cardData.type === 'land' || inst.cardData.abilities?.some((a) => a.trigger === 'tap' && a.effect === 'addMana')) { + if (!inst.tapped) { + handleTapLand(inst.instanceId); + return; + } + } + + const hasTapAbility = inst.cardData.abilities?.some((a) => a.trigger === 'tap' && a.effect !== 'addMana'); + const hasSacrificeAbility = inst.cardData.abilities?.some((a) => a.trigger === 'tap_sacrifice'); + + if (hasTapAbility && !inst.tapped) { + handleTapAbility(inst.instanceId, false); + } else if (hasSacrificeAbility) { + handleTapAbility(inst.instanceId, true); + } + }, [targetMode, pendingSpell, phase, isMyTurn, handleTargetCreature, toggleAttacker, handleTapLand, handleTapAbility]); + + const renderManaPool = (pool) => ( +
+ {Object.entries(pool).map(([color, amount]) => + amount > 0 ? ( + + {MANA_SYMBOLS[color]}{amount} + + ) : null + )} +
+ ); + + const renderBattlefield = (playerData, isOpponent) => { + const lands = playerData.battlefield.filter((i) => i.cardData.type === 'land'); + const creatures = playerData.battlefield.filter((i) => i.cardData.type === 'creature'); + const others = playerData.battlefield.filter((i) => i.cardData.type !== 'land' && i.cardData.type !== 'creature'); + + return ( +
+ {others.length > 0 && ( +
+ {others.map((inst) => ( + handleBattlefieldClick(inst, isOpponent)} + selected={selectedAttackers.includes(inst.instanceId)} + selectable={targetMode !== null} + /> + ))} +
+ )} +
+ {creatures.length === 0 ? ( +
No creatures
+ ) : ( + creatures.map((inst) => { + const isAttacking = gameState.attackers.includes(inst.instanceId); + const isBlocking = Object.values(selectedBlockers).includes(inst.instanceId) || + Object.values(gameState.blockers).flat().includes(inst.instanceId); + + return ( +
+ { + if (phase === 'combat_blockers' && !isMyTurn && !isOpponent && gameState.attackers.length > 0) { + const nextAttacker = gameState.attackers[0]; + toggleBlocker(inst.instanceId, nextAttacker); + } else { + handleBattlefieldClick(inst, isOpponent); + } + }} + selected={ + selectedAttackers.includes(inst.instanceId) || + Object.keys(selectedBlockers).includes(inst.instanceId) + } + selectable={ + (phase === 'combat_attackers' && isMyTurn && !isOpponent) || + (phase === 'combat_blockers' && !isMyTurn && !isOpponent) || + targetMode !== null + } + /> + {isAttacking &&
ATK
} + {isBlocking &&
BLK
} +
+ ); + }) + )} +
+
+ {lands.map((inst) => ( + handleBattlefieldClick(inst, isOpponent)} + /> + ))} +
+
+ ); + }; + + if (gameState.gameOver) { + return ( +
+
+

{gameState.winner === playerId ? 'Victory!' : 'Defeat'}

+

{gameState.winner === playerId ? 'You have won the duel!' : 'You have been defeated.'}

+ +
+
+ ); + } + + return ( +
+
+ +
+ {Object.entries(PHASE_LABELS).map(([key, label]) => ( + + {label} + + ))} +
+
+ Turn {gameState.turnNumber} · {isMyTurn ? 'Your turn' : "Opponent's turn"} +
+ +
+ +
+
+ {'\u2764'} {opponent.life} + {'\u{1F0CF}'} {opponent.handCount} + Deck: {opponent.libraryCount} + {renderManaPool(opponent.manaPool)} + {targetMode === 'any' && ( + + )} +
+ {renderBattlefield(opponent, true)} +
+ +
+ {phase === 'combat_attackers' && isMyTurn && ( +
+ Select attackers, then: + +
+ )} + {phase === 'combat_blockers' && !isMyTurn && ( +
+ Select blockers, then: + +
+ )} +
+ +
+ {renderBattlefield(me, false)} +
+ {'\u2764'} {me.life} + Deck: {me.libraryCount} + {renderManaPool(me.manaPool)} + {targetMode === 'any' && ( + + )} +
+
+ +
+
+ {me.hand.map((cardData, idx) => ( +
+ { + if (selectedHandIndex === idx) { + handlePlayCard(idx); + } else { + setSelectedHandIndex(idx); + } + }} + selected={selectedHandIndex === idx} + selectable={isMyTurn || cardData.type === 'instant'} + /> +
+ ))} +
+
+ {(isMyTurn || phase === 'combat_blockers') && ( + + )} +
+
+ + {pendingSpell && pendingSpell.cardData.abilities?.some((a) => a.isX) && !targetMode && ( +
+
+

Choose X value for {pendingSpell.cardData.name}

+
+ + {xAmount} + +
+
+ + +
+
+
+ )} + + {manaColorChoice && ( +
+
+

Choose mana color

+
+ {manaColorChoice.map((color) => ( + + ))} +
+
+
+ )} + + {targetMode && ( +
+ Choose a target (click a creature{targetMode === 'any' ? ' or player' : ''}) + +
+ )} + + {showLog && ( +
+
+

Game Log

+ +
+
+ {gameState.log.slice().reverse().map((entry, i) => ( +
+ T{entry.turn} + {entry.message} +
+ ))} +
+
+ )} +
+ ); +} diff --git a/client/src/components/Lobby.jsx b/client/src/components/Lobby.jsx new file mode 100644 index 0000000..f44210e --- /dev/null +++ b/client/src/components/Lobby.jsx @@ -0,0 +1,148 @@ +import React, { useState, useEffect } from 'react'; +import socket from '../socket'; + +const DECK_OPTIONS = [ + { color: 'radiance', name: "Dawn's Wrath", symbol: '\u2600', desc: 'Healing, protection, order' }, + { color: 'tide', name: 'Depths of Knowledge', symbol: '\u{1F30A}', desc: 'Card draw, control, counters' }, + { color: 'shadow', name: 'Veil of Shadows', symbol: '\u{1F480}', desc: 'Removal, sacrifice, drain' }, + { color: 'flame', name: 'Infernal Fury', symbol: '\u{1F525}', desc: 'Direct damage, speed, aggression' }, + { color: 'growth', name: 'Primal Might', symbol: '\u{1F33F}', desc: 'Big creatures, ramp, buffs' }, +]; + +export default function Lobby({ playerId, playerName, rooms, currentRoom, screen, onBackToLobby }) { + const [roomName, setRoomName] = useState(''); + + useEffect(() => { + socket.emit('getRooms'); + }, []); + + const handleCreateRoom = () => { + socket.emit('createRoom', { roomName: roomName || `${playerName}'s Game` }); + setRoomName(''); + }; + + const handleJoinRoom = (roomId) => { + socket.emit('joinRoom', { roomId }); + }; + + const handleSelectDeck = (color) => { + socket.emit('selectDeck', { deckColor: color }); + }; + + const handleStartGame = () => { + socket.emit('startGame'); + }; + + if (screen === 'room' && currentRoom) { + const me = currentRoom.players.find((p) => p.id === playerId); + const isHost = currentRoom.hostId === playerId; + const allReady = currentRoom.players.length >= 2 && currentRoom.players.every((p) => p.ready); + + return ( +
+
+
+ +

{currentRoom.name}

+
+ +
+

Players

+ {currentRoom.players.map((p) => ( +
+ + {p.name} {p.id === currentRoom.hostId ? '(Host)' : ''} + + + {p.ready ? `\u2714 ${typeof p.deck === 'string' ? DECK_OPTIONS.find((d) => d.color === p.deck)?.name : 'Custom Deck'}` : 'Choosing deck...'} + +
+ ))} + {currentRoom.players.length < 2 && ( +
Waiting for opponent...
+ )} +
+ +
+

Choose Your Deck

+
+ {DECK_OPTIONS.map((deck) => ( + + ))} +
+
+ + {isHost && ( + + )} +
+
+ ); + } + + return ( +
+
+

Arcane Duels

+ Playing as: {playerName} +
+ +
+
+

Create a Game

+
+ setRoomName(e.target.value)} + placeholder="Room name (optional)" + className="room-input" + /> + +
+
+ +
+

Available Games {rooms.length > 0 && `(${rooms.length})`}

+ {rooms.length === 0 ? ( +
No games available. Create one!
+ ) : ( + rooms.map((room) => ( +
+
+ {room.name} + + Host: {room.host} · {room.playerCount}/{room.maxPlayers} players + +
+ +
+ )) + )} +
+
+
+ ); +} diff --git a/client/src/main.jsx b/client/src/main.jsx new file mode 100644 index 0000000..64c5fb9 --- /dev/null +++ b/client/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './App.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +); diff --git a/client/src/socket.js b/client/src/socket.js new file mode 100644 index 0000000..9e3ca08 --- /dev/null +++ b/client/src/socket.js @@ -0,0 +1,14 @@ +import { io } from 'socket.io-client'; + +const SERVER_URL = window.location.hostname === 'localhost' + ? 'http://localhost:3001' + : `http://${window.location.hostname}:3001`; + +const socket = io(SERVER_URL, { + autoConnect: false, + reconnection: true, + reconnectionAttempts: 10, + reconnectionDelay: 1000, +}); + +export default socket; diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 0000000..b1be094 --- /dev/null +++ b/client/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + host: true, + proxy: { + '/socket.io': { + target: 'http://localhost:3001', + ws: true, + }, + }, + }, +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8971719 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1461 @@ +{ + "name": "arcane-duels", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "arcane-duels", + "version": "1.0.0", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "socket.io": "^4.7.4", + "uuid": "^9.0.0" + }, + "devDependencies": { + "concurrently": "^8.2.2" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz", + "integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..63326b8 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "arcane-duels", + "version": "1.0.0", + "private": true, + "description": "A multiplayer trading card game with LAN support", + "scripts": { + "server": "node server/index.js", + "client": "npm run dev --prefix client", + "dev": "concurrently \"npm run server\" \"npm run client\"", + "install:all": "npm install && cd client && npm install" + }, + "dependencies": { + "express": "^4.18.2", + "socket.io": "^4.7.4", + "cors": "^2.8.5", + "uuid": "^9.0.0" + }, + "devDependencies": { + "concurrently": "^8.2.2" + } +} diff --git a/server/cards.js b/server/cards.js new file mode 100644 index 0000000..55d9c69 --- /dev/null +++ b/server/cards.js @@ -0,0 +1,908 @@ +// ============================================================================ +// ARCANE DUELS — Complete Card Database +// An original TCG with 5 colors: Radiance, Tide, Shadow, Flame, Growth +// ============================================================================ + +const COLORS = { + RADIANCE: 'radiance', // White — order, healing, protection + TIDE: 'tide', // Blue — knowledge, control, illusion + SHADOW: 'shadow', // Black — death, sacrifice, power + FLAME: 'flame', // Red — chaos, destruction, speed + GROWTH: 'growth', // Green — nature, strength, growth + COLORLESS: 'colorless', +}; + +const TYPES = { + CREATURE: 'creature', + SORCERY: 'sorcery', + INSTANT: 'instant', + ENCHANTMENT: 'enchantment', + ARTIFACT: 'artifact', + LAND: 'land', +}; + +// Keywords mapping +const KEYWORDS = { + SWIFT: 'swift', // Can attack immediately (Haste) + VIGILANT: 'vigilant', // Doesn't tap to attack (Vigilance) + SOARING: 'soaring', // Can only be blocked by Soaring/Reaching (Flying) + GUARDIAN: 'guardian', // Deals damage first in combat (First Strike) + FORTIFIED: 'fortified', // Can't attack (Defender) + DRAINING: 'draining', // Gain life equal to damage dealt (Lifelink) + OVERWHELMING: 'overwhelming', // Excess combat damage hits player (Trample) + VENOMOUS: 'venomous', // Any damage is lethal (Deathtouch) + REACHING: 'reaching', // Can block Soaring creatures (Reach) +}; + +// Mana cost encoding: { radiance: N, tide: N, shadow: N, flame: N, growth: N, colorless: N } +// e.g. 2W = { colorless: 2, radiance: 1 } + +let nextId = 1; +function card(props) { + return { id: nextId++, ...props }; +} + +// ============================================================================ +// RADIANCE (White) — 12 cards +// ============================================================================ +const radianceCards = [ + card({ + name: 'Sanctuary Guard', + type: TYPES.CREATURE, + cost: { colorless: 1, radiance: 1 }, + color: COLORS.RADIANCE, + power: 2, toughness: 2, + keywords: [], + abilities: [], + flavor: 'The temple never falls while its guardians stand.', + rarity: 'common', + }), + card({ + name: 'Dawn Priest', + type: TYPES.CREATURE, + cost: { colorless: 2, radiance: 1 }, + color: COLORS.RADIANCE, + power: 1, toughness: 4, + keywords: [KEYWORDS.FORTIFIED], + abilities: [{ trigger: 'upkeep', effect: 'gainLife', amount: 1 }], + flavor: 'Each sunrise is a prayer answered.', + rarity: 'uncommon', + }), + card({ + name: 'Shieldbearer', + type: TYPES.CREATURE, + cost: { radiance: 1 }, + color: COLORS.RADIANCE, + power: 1, toughness: 1, + keywords: [KEYWORDS.VIGILANT], + abilities: [], + flavor: 'Always watching, never resting.', + rarity: 'common', + }), + card({ + name: 'Radiant Champion', + type: TYPES.CREATURE, + cost: { colorless: 3, radiance: 2 }, + color: COLORS.RADIANCE, + power: 4, toughness: 4, + keywords: [KEYWORDS.VIGILANT], + abilities: [], + flavor: 'She carries the light of a thousand dawns into battle.', + rarity: 'rare', + }), + card({ + name: 'Lightbringer', + type: TYPES.CREATURE, + cost: { colorless: 2, radiance: 1 }, + color: COLORS.RADIANCE, + power: 2, toughness: 2, + keywords: [], + abilities: [{ trigger: 'enters', effect: 'gainLife', amount: 3 }], + flavor: 'Where she walks, wounds close and despair lifts.', + rarity: 'common', + }), + card({ + name: 'Angelic Sentinel', + type: TYPES.CREATURE, + cost: { colorless: 3, radiance: 2 }, + color: COLORS.RADIANCE, + power: 3, toughness: 5, + keywords: [KEYWORDS.SOARING, KEYWORDS.VIGILANT], + abilities: [], + flavor: 'Its wings span the horizon, shielding the faithful below.', + rarity: 'rare', + }), + card({ + name: 'Holy Wrath', + type: TYPES.SORCERY, + cost: { colorless: 2, radiance: 2 }, + color: COLORS.RADIANCE, + abilities: [{ effect: 'destroyCreatureIf', condition: 'powerGte4' }], + flavor: 'The righteous need not fear judgment.', + rarity: 'uncommon', + }), + card({ + name: 'Mending Light', + type: TYPES.INSTANT, + cost: { radiance: 1 }, + color: COLORS.RADIANCE, + abilities: [{ effect: 'gainLife', amount: 4 }], + flavor: 'A warm glow that knits flesh and spirit alike.', + rarity: 'common', + }), + card({ + name: 'Celestial Shield', + type: TYPES.INSTANT, + cost: { colorless: 1, radiance: 1 }, + color: COLORS.RADIANCE, + abilities: [{ effect: 'pumpCreature', power: 0, toughness: 4, duration: 'endOfTurn' }], + flavor: 'The blow struck true—but found only light.', + rarity: 'common', + }), + card({ + name: 'Divine Decree', + type: TYPES.ENCHANTMENT, + cost: { colorless: 2, radiance: 2 }, + color: COLORS.RADIANCE, + abilities: [{ effect: 'anthemBuff', power: 1, toughness: 1 }], + flavor: 'By decree of the High Luminary, all shall stand firm.', + rarity: 'rare', + }), + card({ + name: 'Purifying Light', + type: TYPES.SORCERY, + cost: { colorless: 3, radiance: 2 }, + color: COLORS.RADIANCE, + abilities: [{ effect: 'destroyAllCreatures' }], + flavor: 'When the light burns too bright, nothing remains.', + rarity: 'rare', + }), + card({ + name: 'Resolute Defender', + type: TYPES.CREATURE, + cost: { colorless: 1, radiance: 1 }, + color: COLORS.RADIANCE, + power: 1, toughness: 3, + keywords: [KEYWORDS.GUARDIAN], + abilities: [], + flavor: 'His shield has turned a thousand blades.', + rarity: 'common', + }), +]; + +// ============================================================================ +// TIDE (Blue) — 12 cards +// ============================================================================ +const tideCards = [ + card({ + name: 'Reef Scholar', + type: TYPES.CREATURE, + cost: { colorless: 1, tide: 1 }, + color: COLORS.TIDE, + power: 1, toughness: 2, + keywords: [], + abilities: [{ trigger: 'enters', effect: 'drawCards', amount: 1 }], + flavor: 'The coral libraries hold secrets older than the continents.', + rarity: 'common', + }), + card({ + name: 'Tidal Serpent', + type: TYPES.CREATURE, + cost: { colorless: 4, tide: 2 }, + color: COLORS.TIDE, + power: 5, toughness: 5, + keywords: [KEYWORDS.SOARING], + abilities: [], + flavor: 'It moves between sea and sky as if the boundary were a suggestion.', + rarity: 'rare', + }), + card({ + name: 'Mistwalker', + type: TYPES.CREATURE, + cost: { colorless: 2, tide: 1 }, + color: COLORS.TIDE, + power: 2, toughness: 1, + keywords: [KEYWORDS.SOARING], + abilities: [], + flavor: 'It drifts through the fog, never quite where you expect.', + rarity: 'common', + }), + card({ + name: 'Arcane Student', + type: TYPES.CREATURE, + cost: { tide: 1 }, + color: COLORS.TIDE, + power: 1, toughness: 1, + keywords: [], + abilities: [{ trigger: 'tap', effect: 'scry', amount: 1 }], + flavor: 'Every page reveals a new horizon.', + rarity: 'common', + }), + card({ + name: 'Current Channeler', + type: TYPES.CREATURE, + cost: { colorless: 2, tide: 2 }, + color: COLORS.TIDE, + power: 2, toughness: 4, + keywords: [], + abilities: [{ trigger: 'spellCast', effect: 'drawCards', amount: 1 }], + flavor: 'Magic flows through her like water through a delta.', + rarity: 'rare', + }), + card({ + name: 'Mind Shatter', + type: TYPES.INSTANT, + cost: { tide: 2 }, + color: COLORS.TIDE, + abilities: [{ effect: 'counterSpell' }], + flavor: 'Your incantation unravels before the last syllable.', + rarity: 'uncommon', + }), + card({ + name: 'Essence Drain', + type: TYPES.INSTANT, + cost: { colorless: 1, tide: 1 }, + color: COLORS.TIDE, + abilities: [{ effect: 'bounceCreature' }], + flavor: 'Returned to the aether, as if it had never been summoned.', + rarity: 'common', + }), + card({ + name: 'Foresight', + type: TYPES.SORCERY, + cost: { tide: 1 }, + color: COLORS.TIDE, + abilities: [{ effect: 'drawCards', amount: 2 }], + flavor: 'To see what comes is the first step to mastering it.', + rarity: 'common', + }), + card({ + name: 'Mental Fortress', + type: TYPES.ENCHANTMENT, + cost: { colorless: 2, tide: 1 }, + color: COLORS.TIDE, + abilities: [{ effect: 'extraDraw', amount: 1 }], + flavor: 'Within these walls, knowledge multiplies endlessly.', + rarity: 'rare', + }), + card({ + name: 'Time Warp', + type: TYPES.SORCERY, + cost: { colorless: 3, tide: 2 }, + color: COLORS.TIDE, + abilities: [{ effect: 'extraTurn' }], + flavor: 'Yesterday and tomorrow are merely pages to be turned.', + rarity: 'mythic', + }), + card({ + name: 'Frost Barrier', + type: TYPES.INSTANT, + cost: { colorless: 1, tide: 1 }, + color: COLORS.TIDE, + abilities: [{ effect: 'tapCreature' }], + flavor: 'Frozen mid-stride, locked in a prison of ice.', + rarity: 'common', + }), + card({ + name: 'Thought Thief', + type: TYPES.CREATURE, + cost: { colorless: 1, tide: 1 }, + color: COLORS.TIDE, + power: 2, toughness: 1, + keywords: [], + abilities: [{ trigger: 'dealsDamage', effect: 'drawCards', amount: 1 }], + flavor: 'What you know, he knows. What he knows, you never will.', + rarity: 'uncommon', + }), +]; + +// ============================================================================ +// SHADOW (Black) — 12 cards +// ============================================================================ +const shadowCards = [ + card({ + name: 'Graveborn Ghoul', + type: TYPES.CREATURE, + cost: { colorless: 1, shadow: 1 }, + color: COLORS.SHADOW, + power: 2, toughness: 2, + keywords: [], + abilities: [], + flavor: 'It remembers nothing of its former life, only hunger.', + rarity: 'common', + }), + card({ + name: 'Soul Collector', + type: TYPES.CREATURE, + cost: { colorless: 3, shadow: 2 }, + color: COLORS.SHADOW, + power: 3, toughness: 4, + keywords: [KEYWORDS.DRAINING], + abilities: [], + flavor: 'Each soul taken makes it stronger, and its victims weaker.', + rarity: 'rare', + }), + card({ + name: 'Plague Rat', + type: TYPES.CREATURE, + cost: { colorless: 1, shadow: 1 }, + color: COLORS.SHADOW, + power: 1, toughness: 1, + keywords: [], + abilities: [{ effect: 'plagueRatBonus' }], + flavor: 'Where one scurries, a hundred follow.', + rarity: 'common', + }), + card({ + name: 'Night Terror', + type: TYPES.CREATURE, + cost: { colorless: 2, shadow: 1 }, + color: COLORS.SHADOW, + power: 2, toughness: 1, + keywords: [KEYWORDS.SOARING], + abilities: [], + flavor: 'It feeds on screams the way others feed on bread.', + rarity: 'common', + }), + card({ + name: 'Bone Revenant', + type: TYPES.CREATURE, + cost: { colorless: 4, shadow: 2 }, + color: COLORS.SHADOW, + power: 6, toughness: 5, + keywords: [KEYWORDS.VENOMOUS], + abilities: [], + flavor: 'Assembled from the bones of fallen champions.', + rarity: 'rare', + }), + card({ + name: 'Vampiric Noble', + type: TYPES.CREATURE, + cost: { colorless: 2, shadow: 1 }, + color: COLORS.SHADOW, + power: 3, toughness: 2, + keywords: [KEYWORDS.DRAINING], + abilities: [], + flavor: 'She attends the finest galas. Few guests leave alive.', + rarity: 'uncommon', + }), + card({ + name: 'Raise Dead', + type: TYPES.SORCERY, + cost: { shadow: 1 }, + color: COLORS.SHADOW, + abilities: [{ effect: 'returnFromGraveyard', type: 'creature' }], + flavor: 'Death is merely a temporary inconvenience.', + rarity: 'common', + }), + card({ + name: 'Dark Pact', + type: TYPES.SORCERY, + cost: { shadow: 2 }, + color: COLORS.SHADOW, + abilities: [{ effect: 'drawCards', amount: 2 }, { effect: 'loseLife', amount: 2 }], + flavor: 'Power has a price. The wise pay it willingly.', + rarity: 'uncommon', + }), + card({ + name: 'Assassinate', + type: TYPES.INSTANT, + cost: { colorless: 1, shadow: 2 }, + color: COLORS.SHADOW, + abilities: [{ effect: 'destroyCreature' }], + flavor: 'No spell, no blade—just silence, then nothing.', + rarity: 'uncommon', + }), + card({ + name: 'Drain Life', + type: TYPES.SORCERY, + cost: { shadow: 2 }, + color: COLORS.SHADOW, + abilities: [{ effect: 'drainLife', isX: true }], + flavor: 'Your loss is my gain, in the most literal sense.', + rarity: 'uncommon', + }), + card({ + name: 'Cursed Ground', + type: TYPES.ENCHANTMENT, + cost: { colorless: 1, shadow: 1 }, + color: COLORS.SHADOW, + abilities: [{ trigger: 'creatureDies', effect: 'opponentLosesLife', amount: 1 }], + flavor: 'Nothing rests peacefully here.', + rarity: 'uncommon', + }), + card({ + name: 'Mind Rot', + type: TYPES.SORCERY, + cost: { colorless: 1, shadow: 1 }, + color: COLORS.SHADOW, + abilities: [{ effect: 'opponentDiscards', amount: 2 }], + flavor: 'Thoughts dissolve like morning mist in the darkness.', + rarity: 'common', + }), +]; + +// ============================================================================ +// FLAME (Red) — 12 cards +// ============================================================================ +const flameCards = [ + card({ + name: 'Goblin Striker', + type: TYPES.CREATURE, + cost: { flame: 1 }, + color: COLORS.FLAME, + power: 1, toughness: 1, + keywords: [KEYWORDS.SWIFT], + abilities: [], + flavor: 'Speed is a virtue. Thinking is not.', + rarity: 'common', + }), + card({ + name: 'Fire Elemental', + type: TYPES.CREATURE, + cost: { colorless: 3, flame: 2 }, + color: COLORS.FLAME, + power: 5, toughness: 4, + keywords: [], + abilities: [], + flavor: 'Born of the mountain\'s heart, it answers to no master.', + rarity: 'uncommon', + }), + card({ + name: 'Lightning Imp', + type: TYPES.CREATURE, + cost: { colorless: 1, flame: 1 }, + color: COLORS.FLAME, + power: 2, toughness: 1, + keywords: [KEYWORDS.SWIFT], + abilities: [], + flavor: 'It arrives with the thunder and leaves with the echo.', + rarity: 'common', + }), + card({ + name: 'Lava Golem', + type: TYPES.CREATURE, + cost: { colorless: 2, flame: 1 }, + color: COLORS.FLAME, + power: 3, toughness: 2, + keywords: [], + abilities: [], + flavor: 'Molten stone given purpose, if not grace.', + rarity: 'common', + }), + card({ + name: 'Dragon of the Peaks', + type: TYPES.CREATURE, + cost: { colorless: 4, flame: 2 }, + color: COLORS.FLAME, + power: 5, toughness: 5, + keywords: [KEYWORDS.SOARING, KEYWORDS.SWIFT], + abilities: [], + flavor: 'The mountain trembles when it wakes. The world trembles when it flies.', + rarity: 'mythic', + }), + card({ + name: 'Berserker Ogre', + type: TYPES.CREATURE, + cost: { colorless: 2, flame: 2 }, + color: COLORS.FLAME, + power: 4, toughness: 3, + keywords: [KEYWORDS.OVERWHELMING], + abilities: [], + flavor: 'Subtlety is a foreign concept. Destruction is its mother tongue.', + rarity: 'uncommon', + }), + card({ + name: 'Lightning Bolt', + type: TYPES.INSTANT, + cost: { flame: 1 }, + color: COLORS.FLAME, + abilities: [{ effect: 'dealDamage', amount: 3, target: 'any' }], + flavor: 'The sky cracks open and chooses its victim.', + rarity: 'common', + }), + card({ + name: 'Fireball', + type: TYPES.SORCERY, + cost: { flame: 1 }, + color: COLORS.FLAME, + abilities: [{ effect: 'dealDamage', target: 'any', isX: true }], + flavor: 'The equation is simple: more mana, more fire.', + rarity: 'uncommon', + }), + card({ + name: 'Inferno', + type: TYPES.SORCERY, + cost: { colorless: 3, flame: 2 }, + color: COLORS.FLAME, + abilities: [{ effect: 'dealDamageAll', amount: 3 }], + flavor: 'Everything burns eventually. This just speeds up the schedule.', + rarity: 'rare', + }), + card({ + name: 'Battle Rage', + type: TYPES.INSTANT, + cost: { flame: 1 }, + color: COLORS.FLAME, + abilities: [{ effect: 'pumpCreature', power: 3, toughness: 0, duration: 'endOfTurn' }], + flavor: 'Thought yields to instinct, instinct to fury.', + rarity: 'common', + }), + card({ + name: 'Flame Barrage', + type: TYPES.INSTANT, + cost: { colorless: 1, flame: 1 }, + color: COLORS.FLAME, + abilities: [{ effect: 'dealDamage', amount: 2, target: 'any' }], + flavor: 'One bolt for spite. Two for certainty.', + rarity: 'common', + }), + card({ + name: 'Reckless Charge', + type: TYPES.SORCERY, + cost: { flame: 1 }, + color: COLORS.FLAME, + abilities: [{ effect: 'giveKeyword', keyword: KEYWORDS.SWIFT }, { effect: 'pumpCreature', power: 2, toughness: 0, duration: 'endOfTurn' }], + flavor: 'Strategy is for cowards. Real warriors just run faster.', + rarity: 'common', + }), +]; + +// ============================================================================ +// GROWTH (Green) — 12 cards +// ============================================================================ +const growthCards = [ + card({ + name: 'Woodland Elf', + type: TYPES.CREATURE, + cost: { growth: 1 }, + color: COLORS.GROWTH, + power: 1, toughness: 1, + keywords: [], + abilities: [{ trigger: 'tap', effect: 'addMana', color: 'any' }], + flavor: 'The forest speaks through her, and she answers with magic.', + rarity: 'common', + }), + card({ + name: 'Highland Bear', + type: TYPES.CREATURE, + cost: { colorless: 1, growth: 1 }, + color: COLORS.GROWTH, + power: 2, toughness: 2, + keywords: [], + abilities: [], + flavor: 'It needs no magic. Teeth and claws suffice.', + rarity: 'common', + }), + card({ + name: 'Ironbark Treant', + type: TYPES.CREATURE, + cost: { colorless: 3, growth: 2 }, + color: COLORS.GROWTH, + power: 5, toughness: 7, + keywords: [], + abilities: [], + flavor: 'Centuries of growth have made it as unyielding as stone.', + rarity: 'rare', + }), + card({ + name: 'Wild Stallion', + type: TYPES.CREATURE, + cost: { colorless: 2, growth: 1 }, + color: COLORS.GROWTH, + power: 3, toughness: 3, + keywords: [], + abilities: [], + flavor: 'Untamed and untameable, it runs where it wills.', + rarity: 'common', + }), + card({ + name: 'Ancient Wurm', + type: TYPES.CREATURE, + cost: { colorless: 4, growth: 2 }, + color: COLORS.GROWTH, + power: 7, toughness: 7, + keywords: [KEYWORDS.OVERWHELMING], + abilities: [], + flavor: 'Older than the forests it burrows beneath.', + rarity: 'mythic', + }), + card({ + name: 'Thornweaver', + type: TYPES.CREATURE, + cost: { colorless: 2, growth: 1 }, + color: COLORS.GROWTH, + power: 2, toughness: 3, + keywords: [KEYWORDS.REACHING], + abilities: [], + flavor: 'Its thorny tendrils snatch birds from the sky.', + rarity: 'common', + }), + card({ + name: 'Giant Growth', + type: TYPES.INSTANT, + cost: { growth: 1 }, + color: COLORS.GROWTH, + abilities: [{ effect: 'pumpCreature', power: 3, toughness: 3, duration: 'endOfTurn' }], + flavor: 'Nature\'s fury compressed into a single heartbeat.', + rarity: 'common', + }), + card({ + name: "Nature's Gift", + type: TYPES.SORCERY, + cost: { colorless: 1, growth: 1 }, + color: COLORS.GROWTH, + abilities: [{ effect: 'searchLand' }], + flavor: 'The land provides for those who listen.', + rarity: 'common', + }), + card({ + name: 'Regenerate', + type: TYPES.INSTANT, + cost: { growth: 1 }, + color: COLORS.GROWTH, + abilities: [{ effect: 'preventDamage', target: 'creature' }], + flavor: 'What was broken, the forest mends.', + rarity: 'common', + }), + card({ + name: 'Overrun', + type: TYPES.SORCERY, + cost: { colorless: 3, growth: 2 }, + color: COLORS.GROWTH, + abilities: [{ effect: 'pumpAll', power: 3, toughness: 3, keyword: KEYWORDS.OVERWHELMING }], + flavor: 'The forest does not march. It stampedes.', + rarity: 'rare', + }), + card({ + name: 'Vine Snare', + type: TYPES.INSTANT, + cost: { colorless: 1, growth: 1 }, + color: COLORS.GROWTH, + abilities: [{ effect: 'preventCombatDamage' }], + flavor: 'Thorned vines erupt from the earth, halting all charge.', + rarity: 'common', + }), + card({ + name: 'Feral Instinct', + type: TYPES.INSTANT, + cost: { growth: 1 }, + color: COLORS.GROWTH, + abilities: [{ effect: 'pumpCreature', power: 1, toughness: 1, duration: 'endOfTurn' }, { effect: 'drawCards', amount: 1 }], + flavor: 'Trust the animal within.', + rarity: 'uncommon', + }), +]; + +// ============================================================================ +// COLORLESS — Artifacts (7 cards) +// ============================================================================ +const artifactCards = [ + card({ + name: 'Crystal Amulet', + type: TYPES.ARTIFACT, + cost: { colorless: 2 }, + color: COLORS.COLORLESS, + abilities: [{ trigger: 'tap', effect: 'addMana', color: 'any' }], + flavor: 'It pulses with the light of every color, and none.', + rarity: 'uncommon', + }), + card({ + name: 'War Golem', + type: TYPES.CREATURE, + subtype: 'artifact', + cost: { colorless: 5 }, + color: COLORS.COLORLESS, + power: 4, toughness: 4, + keywords: [], + abilities: [], + flavor: 'Forged in the War of Five Suns, it still remembers how to fight.', + rarity: 'common', + }), + card({ + name: 'Enchanted Blade', + type: TYPES.ARTIFACT, + subtype: 'equipment', + cost: { colorless: 3 }, + color: COLORS.COLORLESS, + abilities: [{ effect: 'equipBuff', power: 2, toughness: 1 }], + flavor: 'Its edge never dulls, and its hunger never fades.', + rarity: 'uncommon', + }), + card({ + name: 'Healing Draught', + type: TYPES.ARTIFACT, + cost: { colorless: 1 }, + color: COLORS.COLORLESS, + abilities: [{ trigger: 'tap_sacrifice', effect: 'gainLife', amount: 3 }], + flavor: 'One sip restores what hours of rest cannot.', + rarity: 'common', + }), + card({ + name: 'Mind Stone', + type: TYPES.ARTIFACT, + cost: { colorless: 2 }, + color: COLORS.COLORLESS, + abilities: [ + { trigger: 'tap', effect: 'addMana', color: 'colorless' }, + { trigger: 'tap_sacrifice', effect: 'drawCards', amount: 1 }, + ], + flavor: 'It stores thoughts the way a gem stores light.', + rarity: 'uncommon', + }), + card({ + name: 'Iron Shield', + type: TYPES.ARTIFACT, + subtype: 'equipment', + cost: { colorless: 2 }, + color: COLORS.COLORLESS, + abilities: [{ effect: 'equipBuff', power: 0, toughness: 3 }], + flavor: 'Heavy, inelegant, and thoroughly reliable.', + rarity: 'common', + }), + card({ + name: 'Arcane Compass', + type: TYPES.ARTIFACT, + cost: { colorless: 1 }, + color: COLORS.COLORLESS, + abilities: [{ trigger: 'tap', effect: 'scry', amount: 1 }], + flavor: 'It always points toward what you need most.', + rarity: 'common', + }), +]; + +// ============================================================================ +// LANDS — 5 Basic + 5 Dual +// ============================================================================ +const landCards = [ + // Basic Lands + card({ + name: 'Sunlit Plains', + type: TYPES.LAND, + subtype: 'basic', + color: COLORS.RADIANCE, + abilities: [{ trigger: 'tap', effect: 'addMana', color: 'radiance' }], + rarity: 'basic', + }), + card({ + name: 'Coral Reef', + type: TYPES.LAND, + subtype: 'basic', + color: COLORS.TIDE, + abilities: [{ trigger: 'tap', effect: 'addMana', color: 'tide' }], + rarity: 'basic', + }), + card({ + name: 'Dark Swamp', + type: TYPES.LAND, + subtype: 'basic', + color: COLORS.SHADOW, + abilities: [{ trigger: 'tap', effect: 'addMana', color: 'shadow' }], + rarity: 'basic', + }), + card({ + name: 'Volcanic Peak', + type: TYPES.LAND, + subtype: 'basic', + color: COLORS.FLAME, + abilities: [{ trigger: 'tap', effect: 'addMana', color: 'flame' }], + rarity: 'basic', + }), + card({ + name: 'Verdant Forest', + type: TYPES.LAND, + subtype: 'basic', + color: COLORS.GROWTH, + abilities: [{ trigger: 'tap', effect: 'addMana', color: 'growth' }], + rarity: 'basic', + }), + // Dual Lands + card({ + name: 'Dual Springs', + type: TYPES.LAND, + subtype: 'dual', + color: COLORS.COLORLESS, + abilities: [{ trigger: 'tap', effect: 'addMana', color: 'radiance|tide' }], + rarity: 'rare', + }), + card({ + name: 'Ashen Moor', + type: TYPES.LAND, + subtype: 'dual', + color: COLORS.COLORLESS, + abilities: [{ trigger: 'tap', effect: 'addMana', color: 'shadow|flame' }], + rarity: 'rare', + }), + card({ + name: 'Twilight Glade', + type: TYPES.LAND, + subtype: 'dual', + color: COLORS.COLORLESS, + abilities: [{ trigger: 'tap', effect: 'addMana', color: 'radiance|growth' }], + rarity: 'rare', + }), + card({ + name: 'Molten Cavern', + type: TYPES.LAND, + subtype: 'dual', + color: COLORS.COLORLESS, + abilities: [{ trigger: 'tap', effect: 'addMana', color: 'flame|growth' }], + rarity: 'rare', + }), + card({ + name: 'Frozen Depths', + type: TYPES.LAND, + subtype: 'dual', + color: COLORS.COLORLESS, + abilities: [{ trigger: 'tap', effect: 'addMana', color: 'tide|shadow' }], + rarity: 'rare', + }), +]; + +// ============================================================================ +// ALL CARDS +// ============================================================================ +const ALL_CARDS = [ + ...radianceCards, + ...tideCards, + ...shadowCards, + ...flameCards, + ...growthCards, + ...artifactCards, + ...landCards, +]; + +// Card lookup by ID +const CARD_DB = {}; +ALL_CARDS.forEach((c) => { CARD_DB[c.id] = c; }); + +// Get the converted mana cost (total mana needed) +function getManaCost(cost) { + if (!cost) return 0; + return Object.values(cost).reduce((sum, v) => sum + v, 0); +} + +// Build a starter deck for a given color (24 lands + 36 spells) +function buildStarterDeck(color) { + const colorCards = ALL_CARDS.filter( + (c) => c.color === color && c.type !== TYPES.LAND + ); + const basicLand = ALL_CARDS.find( + (c) => c.type === TYPES.LAND && c.subtype === 'basic' && c.color === color + ); + + const deck = []; + + // Add 4 copies of each color card (up to 9 unique cards = 36) + const spellCards = colorCards.slice(0, 9); + spellCards.forEach((c) => { + for (let i = 0; i < 4; i++) deck.push(c.id); + }); + + // Fill to 60 with basic lands + while (deck.length < 60) { + deck.push(basicLand.id); + } + + return deck; +} + +// Prebuilt starter decks +const STARTER_DECKS = { + radiance: { name: 'Dawn\'s Wrath', color: COLORS.RADIANCE }, + tide: { name: 'Depths of Knowledge', color: COLORS.TIDE }, + shadow: { name: 'Veil of Shadows', color: COLORS.SHADOW }, + flame: { name: 'Infernal Fury', color: COLORS.FLAME }, + growth: { name: 'Primal Might', color: COLORS.GROWTH }, +}; + +module.exports = { + COLORS, + TYPES, + KEYWORDS, + ALL_CARDS, + CARD_DB, + getManaCost, + buildStarterDeck, + STARTER_DECKS, +}; diff --git a/server/game/GameEngine.js b/server/game/GameEngine.js new file mode 100644 index 0000000..c86d4f7 --- /dev/null +++ b/server/game/GameEngine.js @@ -0,0 +1,1150 @@ +const { v4: uuidv4 } = require('uuid'); +const { CARD_DB, TYPES, KEYWORDS, getManaCost } = require('../cards'); + +const PHASES = [ + 'untap', 'upkeep', 'draw', 'main1', + 'combat_begin', 'combat_attackers', 'combat_blockers', 'combat_damage', 'combat_end', + 'main2', 'end_step', 'cleanup', +]; + +const STARTING_LIFE = 20; +const MAX_HAND_SIZE = 7; + +class GameEngine { + constructor(player1Id, player2Id, player1Deck, player2Deck) { + this.id = uuidv4(); + this.players = { + [player1Id]: this._createPlayerState(player1Id, player1Deck), + [player2Id]: this._createPlayerState(player2Id, player2Deck), + }; + this.playerOrder = [player1Id, player2Id]; + this.activePlayerIndex = Math.random() < 0.5 ? 0 : 1; + this.turnNumber = 0; + this.currentPhase = 'untap'; + this.phaseIndex = 0; + this.stack = []; + this.waitingForResponse = null; + this.attackers = []; + this.blockers = {}; + this.pendingEffects = []; + this.gameOver = false; + this.winner = null; + this.log = []; + this.extraTurns = []; + this.endOfTurnEffects = []; + } + + get activePlayerId() { + return this.playerOrder[this.activePlayerIndex]; + } + + get inactivePlayerId() { + return this.playerOrder[1 - this.activePlayerIndex]; + } + + _createPlayerState(playerId, deckCardIds) { + const library = this._shuffle([...deckCardIds]); + return { + id: playerId, + life: STARTING_LIFE, + manaPool: { radiance: 0, tide: 0, shadow: 0, flame: 0, growth: 0, colorless: 0 }, + landPlayedThisTurn: false, + hand: [], + library, + graveyard: [], + battlefield: [], + nextInstanceId: 1, + }; + } + + _shuffle(arr) { + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; + } + + _createInstance(playerId, cardId) { + const player = this.players[playerId]; + const card = CARD_DB[cardId]; + const instance = { + instanceId: `${playerId}_${player.nextInstanceId++}`, + cardId, + cardData: { ...card }, + ownerId: playerId, + tapped: false, + summoningSickness: true, + damage: 0, + counters: {}, + attachments: [], + tempEffects: [], + }; + return instance; + } + + startGame() { + this.playerOrder.forEach((pid) => { + for (let i = 0; i < 7; i++) this._drawCard(pid); + }); + this.turnNumber = 1; + this.currentPhase = 'untap'; + this.phaseIndex = 0; + this._addLog(`Game started. ${this.activePlayerId} goes first.`); + this._processPhase(); + return this.getState(); + } + + _drawCard(playerId) { + const player = this.players[playerId]; + if (player.library.length === 0) { + this._addLog(`${playerId} tried to draw but has no cards. ${playerId} loses!`); + this.gameOver = true; + this.winner = this.playerOrder.find((p) => p !== playerId); + return null; + } + const cardId = player.library.shift(); + player.hand.push(cardId); + return cardId; + } + + _addLog(message) { + this.log.push({ turn: this.turnNumber, phase: this.currentPhase, message }); + } + + _processPhase() { + const phase = this.currentPhase; + const activePlayer = this.players[this.activePlayerId]; + + switch (phase) { + case 'untap': + activePlayer.battlefield.forEach((inst) => { + inst.tapped = false; + inst.summoningSickness = false; + }); + activePlayer.landPlayedThisTurn = false; + activePlayer.manaPool = { radiance: 0, tide: 0, shadow: 0, flame: 0, growth: 0, colorless: 0 }; + this.players[this.inactivePlayerId].manaPool = { radiance: 0, tide: 0, shadow: 0, flame: 0, growth: 0, colorless: 0 }; + this.endOfTurnEffects = []; + this._advancePhase(); + break; + + case 'upkeep': + this._processUpkeepTriggers(); + this._advancePhase(); + break; + + case 'draw': + if (this.turnNumber > 1) { + this._drawCard(this.activePlayerId); + const extraDraws = activePlayer.battlefield.filter( + (inst) => inst.cardData.abilities?.some((a) => a.effect === 'extraDraw') + ); + extraDraws.forEach((inst) => { + const ability = inst.cardData.abilities.find((a) => a.effect === 'extraDraw'); + for (let i = 0; i < ability.amount; i++) this._drawCard(this.activePlayerId); + }); + } + this._advancePhase(); + break; + + case 'main1': + case 'main2': + break; + + case 'combat_begin': + this.attackers = []; + this.blockers = {}; + this._advancePhase(); + break; + + case 'combat_attackers': + break; + + case 'combat_blockers': + break; + + case 'combat_damage': + this._resolveCombatDamage(); + this._advancePhase(); + break; + + case 'combat_end': + this.attackers = []; + this.blockers = {}; + this._advancePhase(); + break; + + case 'end_step': + this._processEndOfTurnEffects(); + this._advancePhase(); + break; + + case 'cleanup': + this._cleanup(); + this._startNextTurn(); + break; + } + } + + _advancePhase() { + this.phaseIndex++; + if (this.phaseIndex >= PHASES.length) { + this.phaseIndex = 0; + } + this.currentPhase = PHASES[this.phaseIndex]; + this._processPhase(); + } + + _processUpkeepTriggers() { + const activePlayer = this.players[this.activePlayerId]; + activePlayer.battlefield.forEach((inst) => { + const card = inst.cardData; + if (!card.abilities) return; + card.abilities.forEach((ability) => { + if (ability.trigger === 'upkeep') { + this._resolveAbility(ability, inst, this.activePlayerId); + } + }); + }); + } + + _processEndOfTurnEffects() { + this.endOfTurnEffects.forEach((effect) => { + if (effect.type === 'pumpCreature') { + const inst = this._findInstance(effect.instanceId); + if (inst) { + inst.tempEffects = inst.tempEffects.filter((e) => e.id !== effect.id); + } + } + }); + this.endOfTurnEffects = []; + + const activePlayer = this.players[this.activePlayerId]; + while (activePlayer.hand.length > MAX_HAND_SIZE) { + const discarded = activePlayer.hand.pop(); + activePlayer.graveyard.push(discarded); + this._addLog(`${this.activePlayerId} discards ${CARD_DB[discarded].name} (hand size).`); + } + } + + _cleanup() { + Object.values(this.players).forEach((player) => { + player.battlefield.forEach((inst) => { + inst.damage = 0; + inst.tempEffects = []; + }); + }); + } + + _startNextTurn() { + if (this.extraTurns.length > 0) { + const nextPlayerId = this.extraTurns.shift(); + this.activePlayerIndex = this.playerOrder.indexOf(nextPlayerId); + } else { + this.activePlayerIndex = 1 - this.activePlayerIndex; + } + this.turnNumber++; + this.phaseIndex = 0; + this.currentPhase = PHASES[0]; + this._addLog(`Turn ${this.turnNumber}: ${this.activePlayerId}'s turn.`); + this._processPhase(); + } + + _canPayCost(playerId, cost) { + if (!cost) return true; + const player = this.players[playerId]; + const pool = { ...player.manaPool }; + let colorlessNeeded = cost.colorless || 0; + + for (const [color, amount] of Object.entries(cost)) { + if (color === 'colorless') continue; + if ((pool[color] || 0) < amount) return false; + pool[color] -= amount; + } + + const remainingMana = Object.values(pool).reduce((s, v) => s + v, 0); + return remainingMana >= colorlessNeeded; + } + + _payCost(playerId, cost) { + if (!cost) return; + const player = this.players[playerId]; + + for (const [color, amount] of Object.entries(cost)) { + if (color === 'colorless') continue; + player.manaPool[color] -= amount; + } + + let colorlessNeeded = cost.colorless || 0; + const colorPriority = ['colorless', 'radiance', 'tide', 'shadow', 'flame', 'growth']; + for (const color of colorPriority) { + if (colorlessNeeded <= 0) break; + const available = player.manaPool[color] || 0; + const spend = Math.min(available, colorlessNeeded); + player.manaPool[color] -= spend; + colorlessNeeded -= spend; + } + } + + _findInstance(instanceId) { + for (const player of Object.values(this.players)) { + const inst = player.battlefield.find((i) => i.instanceId === instanceId); + if (inst) return inst; + } + return null; + } + + _findInstanceOwner(instanceId) { + for (const player of Object.values(this.players)) { + if (player.battlefield.find((i) => i.instanceId === instanceId)) { + return player.id; + } + } + return null; + } + + _getEffectivePower(inst) { + let power = inst.cardData.power || 0; + inst.tempEffects.forEach((e) => { if (e.power) power += e.power; }); + + if (inst.cardData.abilities) { + inst.cardData.abilities.forEach((a) => { + if (a.effect === 'plagueRatBonus') { + const owner = this._findInstanceOwner(inst.instanceId); + const otherRats = this.players[owner].battlefield.filter( + (i) => i.cardData.name === 'Plague Rat' && i.instanceId !== inst.instanceId + ); + power += otherRats.length; + } + }); + } + + const owner = this._findInstanceOwner(inst.instanceId); + if (owner) { + this.players[owner].battlefield.forEach((other) => { + if (other.cardData.abilities) { + other.cardData.abilities.forEach((a) => { + if (a.effect === 'anthemBuff' && other.instanceId !== inst.instanceId) { + power += a.power; + } + }); + } + }); + } + + return power; + } + + _getEffectiveToughness(inst) { + let toughness = inst.cardData.toughness || 0; + inst.tempEffects.forEach((e) => { if (e.toughness) toughness += e.toughness; }); + + if (inst.cardData.abilities) { + inst.cardData.abilities.forEach((a) => { + if (a.effect === 'plagueRatBonus') { + const owner = this._findInstanceOwner(inst.instanceId); + const otherRats = this.players[owner].battlefield.filter( + (i) => i.cardData.name === 'Plague Rat' && i.instanceId !== inst.instanceId + ); + toughness += otherRats.length; + } + }); + } + + const owner = this._findInstanceOwner(inst.instanceId); + if (owner) { + this.players[owner].battlefield.forEach((other) => { + if (other.cardData.abilities) { + other.cardData.abilities.forEach((a) => { + if (a.effect === 'anthemBuff' && other.instanceId !== inst.instanceId) { + toughness += a.toughness; + } + }); + } + }); + } + + return toughness; + } + + _hasKeyword(inst, keyword) { + const keywords = inst.cardData.keywords || []; + if (keywords.includes(keyword)) return true; + return inst.tempEffects.some((e) => e.keyword === keyword); + } + + _resolveAbility(ability, sourceInst, controllerId) { + switch (ability.effect) { + case 'gainLife': + this.players[controllerId].life += ability.amount; + this._addLog(`${controllerId} gains ${ability.amount} life.`); + break; + case 'loseLife': + this.players[controllerId].life -= ability.amount; + this._addLog(`${controllerId} loses ${ability.amount} life.`); + this._checkLifeTotals(); + break; + case 'drawCards': + for (let i = 0; i < ability.amount; i++) this._drawCard(controllerId); + this._addLog(`${controllerId} draws ${ability.amount} card(s).`); + break; + case 'opponentLosesLife': + const opponentId = this.playerOrder.find((p) => p !== controllerId); + this.players[opponentId].life -= ability.amount; + this._addLog(`${opponentId} loses ${ability.amount} life.`); + this._checkLifeTotals(); + break; + } + } + + _checkLifeTotals() { + for (const player of Object.values(this.players)) { + if (player.life <= 0) { + this.gameOver = true; + this.winner = this.playerOrder.find((p) => p !== player.id); + this._addLog(`${player.id} has been defeated! ${this.winner} wins!`); + } + } + } + + _destroyInstance(inst) { + const ownerId = this._findInstanceOwner(inst.instanceId); + if (!ownerId) return; + const player = this.players[ownerId]; + player.battlefield = player.battlefield.filter((i) => i.instanceId !== inst.instanceId); + player.graveyard.push(inst.cardId); + this._addLog(`${inst.cardData.name} is destroyed.`); + + Object.values(this.players).forEach((p) => { + p.battlefield.forEach((other) => { + if (other.cardData.abilities) { + other.cardData.abilities.forEach((a) => { + if (a.trigger === 'creatureDies') { + this._resolveAbility(a, other, other.ownerId); + } + }); + } + }); + }); + } + + _resolveCombatDamage() { + if (this.attackers.length === 0) return; + + const guardianAttackers = this.attackers.filter((aId) => { + const inst = this._findInstance(aId); + return inst && this._hasKeyword(inst, KEYWORDS.GUARDIAN); + }); + const normalAttackers = this.attackers.filter((aId) => { + const inst = this._findInstance(aId); + return inst && !this._hasKeyword(inst, KEYWORDS.GUARDIAN); + }); + + const resolveGroup = (attackerIds, isFirstStrike) => { + for (const attackerId of attackerIds) { + const attacker = this._findInstance(attackerId); + if (!attacker) continue; + + const blockerIds = this.blockers[attackerId] || []; + const attackerPower = this._getEffectivePower(attacker); + + if (blockerIds.length === 0) { + this.players[this.inactivePlayerId].life -= attackerPower; + this._addLog(`${attacker.cardData.name} deals ${attackerPower} damage to ${this.inactivePlayerId}.`); + + if (this._hasKeyword(attacker, KEYWORDS.DRAINING)) { + this.players[this.activePlayerId].life += attackerPower; + this._addLog(`${this.activePlayerId} gains ${attackerPower} life (draining).`); + } + + if (attacker.cardData.abilities) { + attacker.cardData.abilities.forEach((a) => { + if (a.trigger === 'dealsDamage') { + this._resolveAbility(a, attacker, attacker.ownerId); + } + }); + } + } else { + let remainingPower = attackerPower; + for (const blockerId of blockerIds) { + const blocker = this._findInstance(blockerId); + if (!blocker) continue; + + const blockerPower = this._getEffectivePower(blocker); + const blockerToughness = this._getEffectiveToughness(blocker); + + if (!isFirstStrike || !this._hasKeyword(blocker, KEYWORDS.GUARDIAN)) { + attacker.damage += blockerPower; + } + + const damageToBlocker = Math.min(remainingPower, blockerToughness - blocker.damage); + blocker.damage += damageToBlocker; + + if (this._hasKeyword(attacker, KEYWORDS.VENOMOUS) && damageToBlocker > 0) { + blocker.damage = blockerToughness; + } + if (this._hasKeyword(blocker, KEYWORDS.VENOMOUS) && blockerPower > 0) { + attacker.damage = this._getEffectiveToughness(attacker); + } + + if (this._hasKeyword(attacker, KEYWORDS.OVERWHELMING)) { + remainingPower -= damageToBlocker; + } else { + remainingPower = 0; + } + + this._addLog(`${attacker.cardData.name} and ${blocker.cardData.name} clash in combat.`); + } + + if (remainingPower > 0 && this._hasKeyword(attacker, KEYWORDS.OVERWHELMING)) { + this.players[this.inactivePlayerId].life -= remainingPower; + this._addLog(`${attacker.cardData.name} overwhelms for ${remainingPower} damage.`); + } + + if (this._hasKeyword(attacker, KEYWORDS.DRAINING)) { + const totalDealt = attackerPower - remainingPower + (remainingPower > 0 ? remainingPower : 0); + this.players[this.activePlayerId].life += attackerPower; + } + } + } + }; + + if (guardianAttackers.length > 0) { + resolveGroup(guardianAttackers, true); + this._checkAndDestroyCombatants(); + } + + resolveGroup(normalAttackers, false); + this._checkAndDestroyCombatants(); + this._checkLifeTotals(); + } + + _checkAndDestroyCombatants() { + const toDestroy = []; + Object.values(this.players).forEach((player) => { + player.battlefield.forEach((inst) => { + if (inst.cardData.type === TYPES.CREATURE && inst.damage >= this._getEffectiveToughness(inst)) { + toDestroy.push(inst); + } + }); + }); + toDestroy.forEach((inst) => this._destroyInstance(inst)); + } + + handleAction(playerId, action) { + if (this.gameOver) return { error: 'Game is over' }; + + switch (action.type) { + case 'playLand': return this._handlePlayLand(playerId, action); + case 'castSpell': return this._handleCastSpell(playerId, action); + case 'tapLand': return this._handleTapLand(playerId, action); + case 'tapAbility': return this._handleTapAbility(playerId, action); + case 'declareAttackers': return this._handleDeclareAttackers(playerId, action); + case 'declareBlockers': return this._handleDeclareBlockers(playerId, action); + case 'passPhase': return this._handlePassPhase(playerId); + case 'passPriority': return this._handlePassPhase(playerId); + case 'chooseTarget': return this._handleChooseTarget(playerId, action); + case 'payX': return this._handlePayX(playerId, action); + case 'chooseManaColor': return this._handleChooseManaColor(playerId, action); + case 'concede': return this._handleConcede(playerId); + default: return { error: `Unknown action: ${action.type}` }; + } + } + + _handlePlayLand(playerId, action) { + if (playerId !== this.activePlayerId) return { error: 'Not your turn' }; + if (this.currentPhase !== 'main1' && this.currentPhase !== 'main2') { + return { error: 'Can only play lands during main phases' }; + } + const player = this.players[playerId]; + if (player.landPlayedThisTurn) return { error: 'Already played a land this turn' }; + + const handIndex = action.handIndex; + if (handIndex < 0 || handIndex >= player.hand.length) return { error: 'Invalid hand index' }; + + const cardId = player.hand[handIndex]; + const card = CARD_DB[cardId]; + if (card.type !== TYPES.LAND) return { error: 'Not a land card' }; + + player.hand.splice(handIndex, 1); + const instance = this._createInstance(playerId, cardId); + instance.summoningSickness = false; + player.battlefield.push(instance); + player.landPlayedThisTurn = true; + + this._addLog(`${playerId} plays ${card.name}.`); + return { success: true }; + } + + _handleTapLand(playerId, action) { + const instance = this._findInstance(action.instanceId); + if (!instance) return { error: 'Instance not found' }; + if (instance.ownerId !== playerId) return { error: 'Not your permanent' }; + if (instance.tapped) return { error: 'Already tapped' }; + if (instance.cardData.type !== TYPES.LAND && !instance.cardData.abilities?.some((a) => a.trigger === 'tap' && a.effect === 'addMana')) { + return { error: 'Cannot tap for mana' }; + } + + const manaAbility = instance.cardData.abilities.find((a) => a.trigger === 'tap' && a.effect === 'addMana'); + if (!manaAbility) return { error: 'No mana ability' }; + + instance.tapped = true; + + if (manaAbility.color.includes('|')) { + this.waitingForResponse = { + type: 'chooseManaColor', + playerId, + options: manaAbility.color.split('|'), + instanceId: instance.instanceId, + }; + return { success: true, needsResponse: 'chooseManaColor', options: manaAbility.color.split('|') }; + } + + if (manaAbility.color === 'any') { + this.waitingForResponse = { + type: 'chooseManaColor', + playerId, + options: ['radiance', 'tide', 'shadow', 'flame', 'growth', 'colorless'], + instanceId: instance.instanceId, + }; + return { success: true, needsResponse: 'chooseManaColor', options: ['radiance', 'tide', 'shadow', 'flame', 'growth', 'colorless'] }; + } + + this.players[playerId].manaPool[manaAbility.color] = (this.players[playerId].manaPool[manaAbility.color] || 0) + 1; + return { success: true }; + } + + _handleChooseManaColor(playerId, action) { + if (!this.waitingForResponse || this.waitingForResponse.type !== 'chooseManaColor') { + return { error: 'Not waiting for mana color choice' }; + } + if (this.waitingForResponse.playerId !== playerId) return { error: 'Not your choice' }; + + const color = action.color; + if (!this.waitingForResponse.options.includes(color)) return { error: 'Invalid color choice' }; + + this.players[playerId].manaPool[color] = (this.players[playerId].manaPool[color] || 0) + 1; + this.waitingForResponse = null; + return { success: true }; + } + + _handleTapAbility(playerId, action) { + const instance = this._findInstance(action.instanceId); + if (!instance) return { error: 'Instance not found' }; + if (instance.ownerId !== playerId) return { error: 'Not your permanent' }; + if (instance.tapped) return { error: 'Already tapped' }; + + const tapAbility = instance.cardData.abilities?.find((a) => a.trigger === 'tap' && a.effect !== 'addMana'); + const sacrificeAbility = instance.cardData.abilities?.find((a) => a.trigger === 'tap_sacrifice'); + + const ability = action.sacrifice ? sacrificeAbility : tapAbility; + if (!ability) return { error: 'No tap ability' }; + + instance.tapped = true; + + if (action.sacrifice) { + this._destroyInstance(instance); + } + + this._resolveAbility(ability, instance, playerId); + return { success: true }; + } + + _handleCastSpell(playerId, action) { + const player = this.players[playerId]; + const handIndex = action.handIndex; + if (handIndex < 0 || handIndex >= player.hand.length) return { error: 'Invalid hand index' }; + + const cardId = player.hand[handIndex]; + const card = CARD_DB[cardId]; + + if (card.type === TYPES.LAND) return { error: 'Use playLand for lands' }; + + if (card.type !== TYPES.INSTANT) { + if (playerId !== this.activePlayerId) return { error: 'Not your turn for sorcery-speed spells' }; + if (this.currentPhase !== 'main1' && this.currentPhase !== 'main2') { + return { error: 'Can only cast sorceries during main phases' }; + } + } + + const xAmount = action.xAmount || 0; + const totalCost = { ...card.cost }; + const hasX = card.abilities?.some((a) => a.isX); + if (hasX) { + totalCost.colorless = (totalCost.colorless || 0) + xAmount; + } + + if (!this._canPayCost(playerId, totalCost)) return { error: 'Cannot pay mana cost' }; + + const needsTarget = this._spellNeedsTarget(card); + if (needsTarget && action.targetInstanceId === undefined && action.targetPlayerId === undefined) { + return { error: 'This spell requires a target' }; + } + + this._payCost(playerId, totalCost); + player.hand.splice(handIndex, 1); + + this._addLog(`${playerId} casts ${card.name}.`); + + if (card.type === TYPES.CREATURE) { + const instance = this._createInstance(playerId, cardId); + player.battlefield.push(instance); + this._addLog(`${card.name} enters the battlefield.`); + + if (card.abilities) { + card.abilities.forEach((a) => { + if (a.trigger === 'enters') { + this._resolveAbility(a, instance, playerId); + } + }); + } + } else if (card.type === TYPES.ENCHANTMENT || card.type === TYPES.ARTIFACT) { + const instance = this._createInstance(playerId, cardId); + instance.summoningSickness = false; + player.battlefield.push(instance); + this._addLog(`${card.name} enters the battlefield.`); + } else { + this._resolveSpellEffects(card, playerId, action, xAmount); + player.graveyard.push(cardId); + } + + return { success: true }; + } + + _spellNeedsTarget(card) { + if (!card.abilities) return false; + return card.abilities.some((a) => + ['destroyCreature', 'destroyCreatureIf', 'bounceCreature', 'pumpCreature', + 'dealDamage', 'drainLife', 'tapCreature', 'giveKeyword', 'preventDamage', + 'returnFromGraveyard'].includes(a.effect) && + a.target !== 'all' + ); + } + + _resolveSpellEffects(card, casterId, action, xAmount) { + if (!card.abilities) return; + + for (const ability of card.abilities) { + switch (ability.effect) { + case 'dealDamage': { + const amount = ability.isX ? xAmount : ability.amount; + if (action.targetInstanceId) { + const target = this._findInstance(action.targetInstanceId); + if (target && target.cardData.type === TYPES.CREATURE) { + target.damage += amount; + this._addLog(`${card.name} deals ${amount} damage to ${target.cardData.name}.`); + if (target.damage >= this._getEffectiveToughness(target)) { + this._destroyInstance(target); + } + } + } else if (action.targetPlayerId) { + this.players[action.targetPlayerId].life -= amount; + this._addLog(`${card.name} deals ${amount} damage to ${action.targetPlayerId}.`); + this._checkLifeTotals(); + } + break; + } + + case 'dealDamageAll': { + const toDestroy = []; + Object.values(this.players).forEach((player) => { + player.battlefield.forEach((inst) => { + if (inst.cardData.type === TYPES.CREATURE) { + inst.damage += ability.amount; + if (inst.damage >= this._getEffectiveToughness(inst)) { + toDestroy.push(inst); + } + } + }); + }); + this._addLog(`${card.name} deals ${ability.amount} damage to all creatures.`); + toDestroy.forEach((inst) => this._destroyInstance(inst)); + break; + } + + case 'destroyCreature': { + if (action.targetInstanceId) { + const target = this._findInstance(action.targetInstanceId); + if (target) this._destroyInstance(target); + } + break; + } + + case 'destroyCreatureIf': { + if (action.targetInstanceId) { + const target = this._findInstance(action.targetInstanceId); + if (target && ability.condition === 'powerGte4') { + if (this._getEffectivePower(target) >= 4) { + this._destroyInstance(target); + } else { + this._addLog(`${target.cardData.name} is not powerful enough to be destroyed.`); + } + } + } + break; + } + + case 'destroyAllCreatures': { + const toDestroy = []; + Object.values(this.players).forEach((player) => { + player.battlefield.forEach((inst) => { + if (inst.cardData.type === TYPES.CREATURE) toDestroy.push(inst); + }); + }); + this._addLog(`${card.name} destroys all creatures!`); + toDestroy.forEach((inst) => this._destroyInstance(inst)); + break; + } + + case 'bounceCreature': { + if (action.targetInstanceId) { + const target = this._findInstance(action.targetInstanceId); + if (target) { + const ownerId = this._findInstanceOwner(target.instanceId); + this.players[ownerId].battlefield = this.players[ownerId].battlefield.filter( + (i) => i.instanceId !== target.instanceId + ); + this.players[ownerId].hand.push(target.cardId); + this._addLog(`${target.cardData.name} is returned to ${ownerId}'s hand.`); + } + } + break; + } + + case 'counterSpell': { + this._addLog(`${card.name} counters a spell!`); + break; + } + + case 'pumpCreature': { + if (action.targetInstanceId) { + const target = this._findInstance(action.targetInstanceId); + if (target) { + const effectId = uuidv4(); + target.tempEffects.push({ + id: effectId, + power: ability.power, + toughness: ability.toughness, + }); + if (ability.duration === 'endOfTurn') { + this.endOfTurnEffects.push({ + type: 'pumpCreature', + instanceId: target.instanceId, + id: effectId, + }); + } + this._addLog(`${target.cardData.name} gets +${ability.power}/+${ability.toughness} until end of turn.`); + } + } + break; + } + + case 'pumpAll': { + const player = this.players[casterId]; + player.battlefield.forEach((inst) => { + if (inst.cardData.type === TYPES.CREATURE) { + const effectId = uuidv4(); + inst.tempEffects.push({ + id: effectId, + power: ability.power, + toughness: ability.toughness, + keyword: ability.keyword, + }); + this.endOfTurnEffects.push({ + type: 'pumpCreature', + instanceId: inst.instanceId, + id: effectId, + }); + } + }); + this._addLog(`${card.name}: All your creatures get +${ability.power}/+${ability.toughness}!`); + break; + } + + case 'gainLife': + this.players[casterId].life += ability.amount; + this._addLog(`${casterId} gains ${ability.amount} life.`); + break; + + case 'loseLife': + this.players[casterId].life -= ability.amount; + this._addLog(`${casterId} loses ${ability.amount} life.`); + this._checkLifeTotals(); + break; + + case 'drawCards': + for (let i = 0; i < ability.amount; i++) this._drawCard(casterId); + this._addLog(`${casterId} draws ${ability.amount} card(s).`); + break; + + case 'opponentDiscards': { + const opponentId = this.playerOrder.find((p) => p !== casterId); + const opponent = this.players[opponentId]; + const discardCount = Math.min(ability.amount, opponent.hand.length); + for (let i = 0; i < discardCount; i++) { + const randomIdx = Math.floor(Math.random() * opponent.hand.length); + const discarded = opponent.hand.splice(randomIdx, 1)[0]; + opponent.graveyard.push(discarded); + } + this._addLog(`${opponentId} discards ${discardCount} card(s).`); + break; + } + + case 'drainLife': { + const amount = xAmount; + if (action.targetPlayerId) { + this.players[action.targetPlayerId].life -= amount; + this.players[casterId].life += amount; + this._addLog(`${card.name} drains ${amount} life from ${action.targetPlayerId}.`); + this._checkLifeTotals(); + } + break; + } + + case 'returnFromGraveyard': { + const player = this.players[casterId]; + const creatureInGrave = player.graveyard.findIndex((cid) => CARD_DB[cid].type === TYPES.CREATURE); + if (creatureInGrave >= 0) { + const cid = player.graveyard.splice(creatureInGrave, 1)[0]; + player.hand.push(cid); + this._addLog(`${CARD_DB[cid].name} returns from the graveyard to ${casterId}'s hand.`); + } + break; + } + + case 'searchLand': { + const player = this.players[casterId]; + const landIdx = player.library.findIndex((cid) => CARD_DB[cid].type === TYPES.LAND); + if (landIdx >= 0) { + const landId = player.library.splice(landIdx, 1)[0]; + const instance = this._createInstance(casterId, landId); + instance.summoningSickness = false; + instance.tapped = true; + player.battlefield.push(instance); + this._addLog(`${casterId} searches for ${CARD_DB[landId].name} and puts it into play tapped.`); + this._shuffle(player.library); + } + break; + } + + case 'extraTurn': + this.extraTurns.push(casterId); + this._addLog(`${casterId} will take an extra turn!`); + break; + + case 'tapCreature': + if (action.targetInstanceId) { + const target = this._findInstance(action.targetInstanceId); + if (target) { + target.tapped = true; + this._addLog(`${target.cardData.name} becomes tapped.`); + } + } + break; + + case 'giveKeyword': + if (action.targetInstanceId) { + const target = this._findInstance(action.targetInstanceId); + if (target) { + target.tempEffects.push({ keyword: ability.keyword }); + this._addLog(`${target.cardData.name} gains ${ability.keyword}.`); + } + } + break; + + case 'preventDamage': + if (action.targetInstanceId) { + const target = this._findInstance(action.targetInstanceId); + if (target) { + target.damage = 0; + this._addLog(`All damage to ${target.cardData.name} is prevented.`); + } + } + break; + + case 'preventCombatDamage': + this.attackers.forEach((aId) => { + const inst = this._findInstance(aId); + if (inst) inst.damage = 0; + }); + this._addLog(`${card.name} prevents all combat damage this turn.`); + break; + + case 'extraDraw': + case 'anthemBuff': + case 'plagueRatBonus': + case 'equipBuff': + case 'scry': + break; + } + } + } + + _handleDeclareAttackers(playerId, action) { + if (playerId !== this.activePlayerId) return { error: 'Not your turn' }; + if (this.currentPhase !== 'combat_attackers') return { error: 'Not in declare attackers phase' }; + + const player = this.players[playerId]; + const attackerIds = action.attackerIds || []; + + for (const instanceId of attackerIds) { + const inst = player.battlefield.find((i) => i.instanceId === instanceId); + if (!inst) return { error: `Instance ${instanceId} not found` }; + if (inst.cardData.type !== TYPES.CREATURE) return { error: `${inst.cardData.name} is not a creature` }; + if (inst.tapped) return { error: `${inst.cardData.name} is tapped` }; + if (inst.summoningSickness && !this._hasKeyword(inst, KEYWORDS.SWIFT)) { + return { error: `${inst.cardData.name} has summoning sickness` }; + } + if (this._hasKeyword(inst, KEYWORDS.FORTIFIED)) { + return { error: `${inst.cardData.name} cannot attack (Fortified)` }; + } + } + + this.attackers = attackerIds; + attackerIds.forEach((id) => { + const inst = this._findInstance(id); + if (inst && !this._hasKeyword(inst, KEYWORDS.VIGILANT)) { + inst.tapped = true; + } + }); + + if (attackerIds.length > 0) { + this._addLog(`${playerId} declares ${attackerIds.length} attacker(s).`); + this.currentPhase = 'combat_blockers'; + this.phaseIndex = PHASES.indexOf('combat_blockers'); + } else { + this._addLog(`${playerId} declares no attackers.`); + this.currentPhase = 'combat_end'; + this.phaseIndex = PHASES.indexOf('combat_end'); + this._processPhase(); + } + + return { success: true }; + } + + _handleDeclareBlockers(playerId, action) { + if (playerId === this.activePlayerId) return { error: 'Defending player declares blockers' }; + if (this.currentPhase !== 'combat_blockers') return { error: 'Not in declare blockers phase' }; + + const blockerAssignments = action.blockerAssignments || {}; + + for (const [blockerId, attackerId] of Object.entries(blockerAssignments)) { + const blocker = this._findInstance(blockerId); + if (!blocker) return { error: `Blocker ${blockerId} not found` }; + if (blocker.ownerId !== playerId) return { error: `${blocker.cardData.name} is not yours` }; + if (blocker.tapped) return { error: `${blocker.cardData.name} is tapped` }; + if (blocker.cardData.type !== TYPES.CREATURE) return { error: `${blocker.cardData.name} is not a creature` }; + + if (!this.attackers.includes(attackerId)) { + return { error: `${attackerId} is not attacking` }; + } + + const attacker = this._findInstance(attackerId); + if (attacker && this._hasKeyword(attacker, KEYWORDS.SOARING)) { + if (!this._hasKeyword(blocker, KEYWORDS.SOARING) && !this._hasKeyword(blocker, KEYWORDS.REACHING)) { + return { error: `${blocker.cardData.name} cannot block ${attacker.cardData.name} (Soaring)` }; + } + } + } + + this.blockers = {}; + for (const [blockerId, attackerId] of Object.entries(blockerAssignments)) { + if (!this.blockers[attackerId]) this.blockers[attackerId] = []; + this.blockers[attackerId].push(blockerId); + } + + const blockCount = Object.keys(blockerAssignments).length; + this._addLog(`${playerId} declares ${blockCount} blocker(s).`); + + this.currentPhase = 'combat_damage'; + this.phaseIndex = PHASES.indexOf('combat_damage'); + this._processPhase(); + + return { success: true }; + } + + _handlePassPhase(playerId) { + if (playerId !== this.activePlayerId) { + if (this.currentPhase === 'combat_blockers') { + this.currentPhase = 'combat_damage'; + this.phaseIndex = PHASES.indexOf('combat_damage'); + this._processPhase(); + return { success: true }; + } + return { error: 'Not your turn' }; + } + + if (this.currentPhase === 'combat_attackers') { + this.attackers = []; + this.currentPhase = 'combat_end'; + this.phaseIndex = PHASES.indexOf('combat_end'); + this._processPhase(); + } else { + this._advancePhase(); + } + + return { success: true }; + } + + _handleChooseTarget(playerId, action) { + return { success: true }; + } + + _handlePayX(playerId, action) { + return { success: true, xAmount: action.amount }; + } + + _handleConcede(playerId) { + this.gameOver = true; + this.winner = this.playerOrder.find((p) => p !== playerId); + this._addLog(`${playerId} concedes. ${this.winner} wins!`); + return { success: true }; + } + + getState(forPlayerId = null) { + const state = { + gameId: this.id, + turnNumber: this.turnNumber, + currentPhase: this.currentPhase, + activePlayerId: this.activePlayerId, + gameOver: this.gameOver, + winner: this.winner, + attackers: this.attackers, + blockers: this.blockers, + waitingForResponse: this.waitingForResponse, + log: this.log.slice(-20), + players: {}, + }; + + for (const [pid, player] of Object.entries(this.players)) { + const isMe = pid === forPlayerId; + state.players[pid] = { + id: pid, + life: player.life, + manaPool: { ...player.manaPool }, + landPlayedThisTurn: player.landPlayedThisTurn, + handCount: player.hand.length, + hand: isMe ? player.hand.map((cid) => ({ cardId: cid, ...CARD_DB[cid] })) : [], + libraryCount: player.library.length, + graveyard: player.graveyard.map((cid) => ({ cardId: cid, ...CARD_DB[cid] })), + battlefield: player.battlefield.map((inst) => ({ + instanceId: inst.instanceId, + cardId: inst.cardId, + cardData: inst.cardData, + ownerId: inst.ownerId, + tapped: inst.tapped, + summoningSickness: inst.summoningSickness, + damage: inst.damage, + tempEffects: inst.tempEffects, + effectivePower: this._getEffectivePower(inst), + effectiveToughness: this._getEffectiveToughness(inst), + })), + }; + } + + return state; + } +} + +module.exports = { GameEngine, PHASES }; diff --git a/server/game/GameManager.js b/server/game/GameManager.js new file mode 100644 index 0000000..79affa2 --- /dev/null +++ b/server/game/GameManager.js @@ -0,0 +1,182 @@ +const { GameEngine } = require('../game/GameEngine'); +const { buildStarterDeck, STARTER_DECKS, ALL_CARDS, CARD_DB } = require('../cards'); + +class GameManager { + constructor() { + this.rooms = new Map(); + this.playerRooms = new Map(); + this.games = new Map(); + } + + createRoom(hostId, hostName, roomName) { + const roomId = `room_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`; + const room = { + id: roomId, + name: roomName || `${hostName}'s Game`, + hostId, + players: [{ id: hostId, name: hostName, deck: null, ready: false }], + status: 'waiting', + gameId: null, + maxPlayers: 2, + }; + this.rooms.set(roomId, room); + this.playerRooms.set(hostId, roomId); + return room; + } + + joinRoom(roomId, playerId, playerName) { + const room = this.rooms.get(roomId); + if (!room) return { error: 'Room not found' }; + if (room.status !== 'waiting') return { error: 'Game already in progress' }; + if (room.players.length >= room.maxPlayers) return { error: 'Room is full' }; + if (room.players.find((p) => p.id === playerId)) return { error: 'Already in room' }; + + room.players.push({ id: playerId, name: playerName, deck: null, ready: false }); + this.playerRooms.set(playerId, roomId); + return { success: true, room }; + } + + leaveRoom(playerId) { + const roomId = this.playerRooms.get(playerId); + if (!roomId) return { error: 'Not in a room' }; + + const room = this.rooms.get(roomId); + if (!room) { + this.playerRooms.delete(playerId); + return { error: 'Room not found' }; + } + + room.players = room.players.filter((p) => p.id !== playerId); + this.playerRooms.delete(playerId); + + if (room.players.length === 0) { + this.rooms.delete(roomId); + if (room.gameId) this.games.delete(room.gameId); + return { success: true, roomDeleted: true, roomId }; + } + + if (room.hostId === playerId) { + room.hostId = room.players[0].id; + } + + return { success: true, room }; + } + + selectDeck(playerId, deckColor) { + const roomId = this.playerRooms.get(playerId); + if (!roomId) return { error: 'Not in a room' }; + + const room = this.rooms.get(roomId); + if (!room) return { error: 'Room not found' }; + + const player = room.players.find((p) => p.id === playerId); + if (!player) return { error: 'Player not in room' }; + + if (!STARTER_DECKS[deckColor]) return { error: 'Invalid deck color' }; + + player.deck = deckColor; + player.ready = true; + return { success: true, room }; + } + + selectCustomDeck(playerId, cardIds) { + const roomId = this.playerRooms.get(playerId); + if (!roomId) return { error: 'Not in a room' }; + + const room = this.rooms.get(roomId); + if (!room) return { error: 'Room not found' }; + + const player = room.players.find((p) => p.id === playerId); + if (!player) return { error: 'Player not in room' }; + + if (cardIds.length < 40) return { error: 'Deck must have at least 40 cards' }; + if (cardIds.length > 60) return { error: 'Deck cannot exceed 60 cards' }; + + for (const cid of cardIds) { + if (!CARD_DB[cid]) return { error: `Invalid card id: ${cid}` }; + } + + const nonLandCounts = {}; + for (const cid of cardIds) { + const card = CARD_DB[cid]; + if (card.type !== 'land' || card.subtype !== 'basic') { + nonLandCounts[cid] = (nonLandCounts[cid] || 0) + 1; + if (nonLandCounts[cid] > 4) { + return { error: `Cannot have more than 4 copies of ${card.name}` }; + } + } + } + + player.deck = cardIds; + player.customDeck = true; + player.ready = true; + return { success: true, room }; + } + + startGame(roomId) { + const room = this.rooms.get(roomId); + if (!room) return { error: 'Room not found' }; + if (room.players.length < 2) return { error: 'Need at least 2 players' }; + if (!room.players.every((p) => p.ready)) return { error: 'Not all players are ready' }; + + const p1 = room.players[0]; + const p2 = room.players[1]; + + const deck1 = p1.customDeck ? p1.deck : buildStarterDeck(p1.deck); + const deck2 = p2.customDeck ? p2.deck : buildStarterDeck(p2.deck); + + const engine = new GameEngine(p1.id, p2.id, deck1, deck2); + const initialState = engine.startGame(); + + room.status = 'playing'; + room.gameId = engine.id; + this.games.set(engine.id, engine); + + return { success: true, gameId: engine.id, state: initialState }; + } + + getGame(gameId) { + return this.games.get(gameId); + } + + handleGameAction(gameId, playerId, action) { + const engine = this.games.get(gameId); + if (!engine) return { error: 'Game not found' }; + return engine.handleAction(playerId, action); + } + + getGameState(gameId, playerId) { + const engine = this.games.get(gameId); + if (!engine) return null; + return engine.getState(playerId); + } + + getRooms() { + const roomList = []; + for (const room of this.rooms.values()) { + roomList.push({ + id: room.id, + name: room.name, + playerCount: room.players.length, + maxPlayers: room.maxPlayers, + status: room.status, + host: room.players.find((p) => p.id === room.hostId)?.name || 'Unknown', + }); + } + return roomList; + } + + getRoomDetails(roomId) { + return this.rooms.get(roomId) || null; + } + + getCardList() { + return ALL_CARDS; + } + + getStarterDecks() { + return STARTER_DECKS; + } +} + +module.exports = { GameManager }; diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..2a56ff7 --- /dev/null +++ b/server/index.js @@ -0,0 +1,181 @@ +const express = require('express'); +const http = require('http'); +const { Server } = require('socket.io'); +const cors = require('cors'); +const { GameManager } = require('./game/GameManager'); + +const app = express(); +app.use(cors()); +app.use(express.json()); + +const server = http.createServer(app); +const io = new Server(server, { + cors: { origin: '*', methods: ['GET', 'POST'] }, +}); + +const gm = new GameManager(); + +app.get('/api/cards', (req, res) => { + res.json(gm.getCardList()); +}); + +app.get('/api/starter-decks', (req, res) => { + res.json(gm.getStarterDecks()); +}); + +app.get('/api/rooms', (req, res) => { + res.json(gm.getRooms()); +}); + +io.on('connection', (socket) => { + let playerName = 'Player'; + let playerId = socket.id; + + console.log(`Player connected: ${playerId}`); + + socket.on('setName', (name) => { + playerName = name || 'Player'; + socket.emit('nameSet', { id: playerId, name: playerName }); + }); + + socket.on('createRoom', (data) => { + const room = gm.createRoom(playerId, playerName, data?.roomName); + socket.join(room.id); + socket.emit('roomCreated', room); + io.emit('roomsUpdated', gm.getRooms()); + }); + + socket.on('joinRoom', (data) => { + const result = gm.joinRoom(data.roomId, playerId, playerName); + if (result.error) { + socket.emit('error', { message: result.error }); + return; + } + socket.join(data.roomId); + socket.emit('roomJoined', result.room); + io.to(data.roomId).emit('roomUpdated', result.room); + io.emit('roomsUpdated', gm.getRooms()); + }); + + socket.on('leaveRoom', () => { + const currentRoomId = gm.playerRooms.get(playerId); + const result = gm.leaveRoom(playerId); + if (result.error) return; + + if (currentRoomId) { + socket.leave(currentRoomId); + if (!result.roomDeleted) { + io.to(currentRoomId).emit('roomUpdated', result.room); + } + } + io.emit('roomsUpdated', gm.getRooms()); + }); + + socket.on('selectDeck', (data) => { + const result = gm.selectDeck(playerId, data.deckColor); + if (result.error) { + socket.emit('error', { message: result.error }); + return; + } + const roomId = gm.playerRooms.get(playerId); + io.to(roomId).emit('roomUpdated', result.room); + }); + + socket.on('selectCustomDeck', (data) => { + const result = gm.selectCustomDeck(playerId, data.cardIds); + if (result.error) { + socket.emit('error', { message: result.error }); + return; + } + const roomId = gm.playerRooms.get(playerId); + io.to(roomId).emit('roomUpdated', result.room); + }); + + socket.on('startGame', () => { + const roomId = gm.playerRooms.get(playerId); + if (!roomId) { + socket.emit('error', { message: 'Not in a room' }); + return; + } + + const room = gm.getRoomDetails(roomId); + if (!room || room.hostId !== playerId) { + socket.emit('error', { message: 'Only the host can start the game' }); + return; + } + + const result = gm.startGame(roomId); + if (result.error) { + socket.emit('error', { message: result.error }); + return; + } + + room.players.forEach((p) => { + const playerState = gm.getGameState(result.gameId, p.id); + io.to(p.id).emit('gameStarted', { gameId: result.gameId, state: playerState }); + }); + }); + + socket.on('gameAction', (data) => { + const roomId = gm.playerRooms.get(playerId); + if (!roomId) return; + + const room = gm.getRoomDetails(roomId); + if (!room || !room.gameId) return; + + const result = gm.handleGameAction(room.gameId, playerId, data.action); + if (result.error) { + socket.emit('actionError', { message: result.error }); + return; + } + + room.players.forEach((p) => { + const playerState = gm.getGameState(room.gameId, p.id); + io.to(p.id).emit('gameStateUpdated', playerState); + }); + + if (result.needsResponse) { + socket.emit('needsResponse', { + type: result.needsResponse, + options: result.options, + }); + } + }); + + socket.on('getRooms', () => { + socket.emit('roomsUpdated', gm.getRooms()); + }); + + socket.on('disconnect', () => { + console.log(`Player disconnected: ${playerId}`); + const roomId = gm.playerRooms.get(playerId); + if (roomId) { + const result = gm.leaveRoom(playerId); + if (result && !result.error) { + if (!result.roomDeleted) { + io.to(roomId).emit('roomUpdated', result.room); + io.to(roomId).emit('playerDisconnected', { playerId, playerName }); + } + io.emit('roomsUpdated', gm.getRooms()); + } + } + }); +}); + +const PORT = process.env.PORT || 3001; +server.listen(PORT, '0.0.0.0', () => { + const os = require('os'); + const interfaces = os.networkInterfaces(); + let lanIp = 'localhost'; + for (const iface of Object.values(interfaces)) { + for (const alias of iface) { + if (alias.family === 'IPv4' && !alias.internal) { + lanIp = alias.address; + break; + } + } + } + console.log(`\n Arcane Duels Server running on:`); + console.log(` Local: http://localhost:${PORT}`); + console.log(` Network: http://${lanIp}:${PORT}\n`); +});