UNPKG

@javelin/devtool

Version:

Developer tool for Javelin ECS.

182 lines (165 loc) 11.6 kB
import{protocol as e,createMessageProducer as t,serializeWorldModel as n,createMessageHandler as l}from"@javelin/net";import o,{createContext as r,forwardRef as a,useImperativeHandle as c,useRef as i,useReducer as s,useContext as m,useState as d,useEffect as p,useCallback as u,createRef as f,createElement as y}from"react";import{render as g,unmountComponentAtNode as h}from"react-dom";import{useParams as E,Link as x,Switch as w,Route as v,MemoryRouter as b,useHistory as k,useLocation as C}from"react-router-dom";import S,{createGlobalStyle as I}from"styled-components";import M from"immer";import{mutableEmpty as z,number as j,string as $,$worldStorageKey as _,WorldOpType as O}from"@javelin/ecs";var P;function A(e,t){return M(e,e=>{switch(t.type){case P.Info:{const n={...t.payload},l=e.messages[e.messages.length-1];if(!l||!l.context||n.context&&n.context.id===l.context.id||(l.start=-1/0),n.context){const t=e.messages.findIndex(e=>e.context&&e.context.id===n.context.id&&n.time-(e.start||0)<=n.context.duration);if(t>-1){const l=e.messages[t];return n.count=(l.count||0)+1,n.start=l.start,void(e.messages[t]=n)}n.count=0,n.start=n.time}e.messages.push(n),e.messages=e.messages.slice(e.messages.length+1-e.limit)}}})}!function(e){e.Info="log/info"}(P||(P={}));const F={limit:0,messages:[]};const H=r({messages:[],limit:0,info(){}}),L=a((function(e,t){const n=function(e=100){const t=i(0),[n,l]=s(A,{...F,limit:e});return{info(e,n){l({type:P.Info,payload:{id:++t.current,time:+performance.now().toFixed(2),text:e,context:n}})},...n}}(e.limit);return c(t,()=>n),o.createElement(H.Provider,{value:n},e.children)}));function N(){return m(H)}const D=r({worlds:[],sendMessage:()=>{}}),T=()=>m(D);function J(e){const t={worlds:e.worlds,sendMessage:(t,n)=>e.onMessage(t,n)};return o.createElement(D.Provider,{value:t},e.children)}function q(e,t){var n;return null===(n=t.find(t=>e===t.type))||void 0===n?void 0:n.name}const B=S.div` display: flex; `,G=S.div` display: flex; flex: 1; flex-direction: column; justify-content: center; align-items: center; `,K=S.button` display: flex; `,Q=S.select` flex: 2; `,R=S.ul` list-style-type: none; margin: 0 0 8px 0; padding: 0; `,U=S.ul` list-style-type: none; margin: 0; padding: 0; `,V=S.li` margin: 8px 0; `;function W(e,t){return M(e,e=>{switch(t.type){case"add_components":e.components.push(...t.payload);break;case"remove_components":e.components=e.components.filter(e=>!t.payload.includes(e.type));break;case"set":{const n=e.components.findIndex(e=>e.type===t.payload.type);e.components[n][t.payload.key]=t.payload.value;break}case"reset":z(e.components)}})}function X(e,t){return"number"===e?Number(t):t}function Y(){const{world:t}=E(),{worlds:n,sendMessage:l}=T(),r=n.find(e=>e.name===t),[a,c]=s(W,{components:[]}),i=N(),m=u(()=>{l(r,e.spawn(a.components.map(e=>({...e})))),g([]),x([]),i.info("Spawned "+JSON.stringify(a.components)),c({type:"reset",payload:r})},[a,r]);function f(e){const{schema:t}=r.model.find(t=>t.type===e);return Object.entries(t).reduce((e,[t,n])=>(e[t]="number"===n?j.create():$.create(),e),{type:e})}p(()=>{c({type:"reset",payload:r})},[r]);const[y,g]=d([]),[h,x]=d([]);function w(e,t){const n=r.model.find(t=>t.type===e.type);for(const e in n.schema)if(t===e)return n.schema[e];throw new Error(`Field ${t} does not exist on component with type ${q(e.type,r.model)}`)}return o.createElement("div",null,o.createElement("h4",null,"Spawn"),o.createElement("p",null,"Create new entities by moving component types into the box on the right side."),o.createElement(B,null,o.createElement(Q,{multiple:!0,value:y.map(String),onChange:function(e){const t=Array.from(e.target.selectedOptions).map(e=>Number(e.value));g(t)}},r.model.filter(e=>void 0===a.components.find(t=>t.type===e.type)).map(e=>o.createElement("option",{key:e.type,value:e.type},e.name))),o.createElement(G,null,o.createElement(K,{onClick:function(){c({type:"add_components",payload:y.map(f)}),g([]),x([...h,...y])}},">"),o.createElement(K,{onClick:function(){c({type:"remove_components",payload:h}),x([])}},"<")),o.createElement(Q,{multiple:!0,value:h.map(String),onChange:function(e){const t=Array.from(e.target.selectedOptions).map(e=>Number(e.value));x(t)}},a.components.map(e=>o.createElement("option",{key:e.type,value:e.type},q(e.type,r.model))))),o.createElement(R,null,h.map(e=>a.components.find(t=>t.type===e)).map(e=>o.createElement("li",{key:e.type},o.createElement("h4",null,q(e.type,r.model)),o.createElement("fieldset",null,o.createElement(U,null,Object.entries(e).filter(([e])=>"type"!==e).map(([t,n])=>{return o.createElement(V,{key:t},o.createElement("label",{htmlFor:t},t),o.createElement("input",{id:t,name:t,type:(l=w(e,t),"number"===l?"number":"text"),value:String(n),onChange:n=>c({type:"set",payload:{type:e.type,key:t,value:X(w(e,t),n.target.value)}})}));var l})))))),o.createElement("button",{onClick:m},"Spawn"))}function Z(){const{world:e}=E(),{worlds:t}=T(),n=t.find(t=>t.name===e).world,l=n[_].archetypes.length,r=n[_].archetypes.reduce((e,t)=>e+t.entities.length,0);return o.createElement("div",null,o.createElement("h4",null,"Details"),o.createElement("dl",null,o.createElement("dt",null,"Archetypes"),o.createElement("dd",null,l),o.createElement("dt",null,"Entities"),o.createElement("dd",null,r)),o.createElement("h4",null,"Actions"),o.createElement("ul",null,o.createElement("li",null,o.createElement(x,{to:`/${e}/spawn`},"Spawn")),o.createElement("li",null,o.createElement(x,{to:`/${e}/inspect`},"Inspect"))))}const ee=S.ul` display: flex; flex-wrap: wrap; list-style-type: none; margin: 0; padding: 0; `,te=S.section` margin-bottom: 1em; `,ne=S.h4` font-family: "SF Mono", "Consolas", monospace; font-weight: normal; text-decoration: underline; `,le=S.li` background-color: ${e=>e.selected?"#6688ae":"transparent"}; border-radius: 3px; border: ${e=>"1px solid "+(e.selected?"#6688ae":"#ccc")}; box-sizing: border-box; color: ${e=>e.selected?"#fafafa":"inherit"}; cursor: pointer; flex: 0 0 30px; margin: 2px; padding: 4px; text-align: center; &:hover { background-color: ${e=>e.selected?"#6688ae":"#ccc"}; } `;function oe(e,t){return M(e,e=>{switch(t.type){case"select_entity":e.selected.push(t.payload);break;case"deselect_entity":e.selected.splice(e.selected.indexOf(t.payload),1);break;case"reset":z(e.selected)}})}function re(){const{world:t}=E(),{worlds:n,sendMessage:l}=T(),r=n.find(e=>e.name===t),[a,c]=s(oe,{selected:[]}),i=r.world[_].archetypes.map(e=>{const t=e.entities.slice(0,50);return 0===t.length?null:o.createElement(te,{key:e.layout.join(",")},o.createElement(ne,null,e.layout.map(e=>q(e,null==r?void 0:r.model)).join(" & ")),o.createElement("span",null,t.length," of ",e.entities.length),o.createElement(ee,null,t.map(e=>{const t=a.selected.includes(e);return o.createElement(le,{key:e,selected:t,onClick:()=>c({type:t?"deselect_entity":"select_entity",payload:e})},o.createElement("span",null,e))})))});return o.createElement("div",null,o.createElement("h4",null,"Archetypes"),o.createElement("p",null,"Inspect your game world and modify or delete existing entitites."),i,o.createElement("button",{onClick:()=>{l(r,e.ops(a.selected.map(e=>[O.Destroy,e]),!0)),c({type:"reset"})}},"Delete"))}function ae(){return o.createElement(w,null,o.createElement(v,{path:"/:world/inspect",component:re}),o.createElement(v,{path:"/:world/spawn",component:Y}),o.createElement(v,{path:"/:world",component:Z}))}const ce=S.ul` font-family: "SF Mono", "Consolas", monospace; font-size: 10px; list-style-type: none; margin: 0; padding: 0; `;function ie(e){const{messages:t}=N();return o.createElement(ce,null,t.map(e=>o.createElement("li",{key:e.id},e.time,(e.count||0)>0&&`(${e.count})`,": ",e.text)))}const se=S.div` display: flex; flex: 1; flex-direction: column; overflow: scroll; `,me=S.div` background-color: #fefefe; padding: 8px; display: flex; flex-direction: row; `,de=S.div` padding: 8px; `,pe=S.h3` margin: 0; `,ue=a((e,t)=>o.createElement(se,{ref:t},e.title&&o.createElement(me,null,o.createElement(pe,null,e.title)),o.createElement(de,null,e.children))),fe=I` a { text-decoration: none; color: #6688ae; &:hover { color: #4e677e; } } dl { display: inline-grid; grid-template-columns: auto auto; margin: 0; dt { padding: 4px; text-align: right; word-break: break-word; text-decoration: underline; } dd { padding: 4px; margin: 0; } } fieldset { border: none; margin: 0; padding: 0; } select { > option { padding: 4px 2px; } } label { margin-right: 8px; } button { cursor: pointer; } h1, h2, h3, h4, h5, h6 { margin: 1em 0; } h1 { font-size: 2em; } h2 { font-size: 1.6em; } h3 { font-size: 1.2em; } h4, h5, h6 { font-size: 1em; } input, select, textarea, button { font-size: 12px; } `,ye=S.div` background: #efefef; font-family: Helvetica, sans-serif; font-size: 12px; color: #222; display: flex; flex-direction: column; flex: 1; height: 100%; overflow-y: auto; overflow-x: hidden; *, *:before, *:after { box-sizing: border-box; } `,ge=(S.div` padding: 8px; flex: 1; overflow: auto; `,S.select` margin-right: 8px; align-self: flex-end; `);function he(){const{worlds:e}=T(),{push:t}=k(),{pathname:n}=C(),l=u(n=>t("/"+e[n.target.selectedIndex].name),[]),r=n.match(/^\/(\w+)\/?/),a=r?r[1]:"";return o.createElement(ge,{value:a,onChange:l},e.map(e=>o.createElement("option",{key:e.name,value:e.name},e.name)))}const Ee=S(x)` &:before { content: "/"; } `,xe=S.nav` flex: 1; `;function we(){const{pathname:e}=C(),t=e.replace(/^\//,"").split("/").reduce((e,t)=>(e.currentPath+=t+"/",e.links.push(o.createElement(Ee,{key:e.currentPath,to:"/"+e.currentPath},t)),e),{currentPath:"",links:[]});return o.createElement(xe,null,t.links)}function ve(){const{worlds:e}=T(),t=function(e){const t=i(null),[n,l]=d(!0),{current:o}=t;return p(()=>{if(!o)return;const e=()=>l(o.scrollHeight-o.scrollTop===o.clientHeight);return o.addEventListener("scroll",e),()=>o.removeEventListener("scroll",e)},[o]),p(()=>{o&&n&&o.scrollTo({top:o.scrollHeight})},[n,e,o]),t}(N().messages),n="/"+e[0].name;return o.createElement(b,{initialEntries:[n]},o.createElement(fe,null),o.createElement(ye,null,o.createElement(me,null,o.createElement(x,{to:n},"javelin"),o.createElement(we,null),o.createElement(he,null)),o.createElement(ue,null,o.createElement(ae,null)),o.createElement(ue,{title:"Log",ref:t},o.createElement(ie,null))))}function be(e){return t({components:e.componentTypes.map(e=>({type:e,priority:1})),updateInterval:1e3,updateSize:1e3,isLocal:!0})}function ke(e){const t=new Map,o=Object.entries(e.worlds).map(([e,t])=>"remote"in t?{name:e,world:t.world,model:n(t.world),remote:!0}:{name:e,world:t,model:n(t),remote:!1});let r,a;for(const{world:e,remote:n}of o)if(!n){const n=l();e.addSystem(n.system),t.set(e,n)}return{mount:function(n){const l=f(),c=(n,l)=>{const{world:r}=o.find(e=>e===n);if(n.remote)e.onMessage(r,l);else{t.get(r).push(l)}},i=()=>g(y(L,{ref:l},y(J,{worlds:o,onMessage:c},y(ve,null))),n);return i(),r=n,a=setInterval(i,500),{log:l.current}},unmount:function(){clearInterval(a),h(r)},setModel:function(e,t){const n=o.find(t=>t.world===e);if(void 0===n)throw new Error("Cannot set model: world not registered with devtool");n.model=t}}}export{ke as createDevtool,be as createDevtoolMessageProducer};