ELITE BASEBALL LEAGUE
GENESIS • CLOSED ALPHA • ALPHA HISTORY MAY RESET AT HARD LAUNCH
Not signed in

Welcome to the EBL

Create Player Account

Login

Password Recovery

EBL NEWSROOM

The stories, results, rivalries, and moments shaping the Elite Baseball League.

EBL COMMUNITY

The history the league creates together.

Rivalry Board

EBL Record Book

PRIVATE MESSAGES

Talk directly with coaches, teammates, and other EBL members.

Contacts

Conversation

Franchise Operations

Manage contracts, batting order, field alignment, rotation, bullpen roles, and defensive tactics.

League

EBL Analytics

Award Races

Genesis races update as the season develops. Statistical titles are earned on the field; major awards use performance-based rankings.

GameCast Center

Completed games become watchable pitch-by-pitch archives. Live reveal mode is the foundation for community game nights.

EBL Rules & Policies

Commissioner / Admin

Reset Test Account

Moderation Queue

async function api(url,opt={}){let r=await fetch(url,{credentials:'same-origin',headers:{'Content-Type':'application/json'},...opt});let j=await r.json();if(!r.ok)throw j;return j} let ME=null,PLAYER=null,LEAGUE=null,SCHEDULE=[],GG=null,gi=0,timer=null,D=null; const HA=['CON','POW','VIS','DISC','TIM','SPD','FLD','ARM','ACC','REAC'], PA=['STA','H9','K9','BB9','HR9','PCLT','CTRL','VEL','BRK','FLD','ARM','ACC','REAC']; const cost=v=>v<25?1:v<50?2:v<70?3:v<85?5:v<95?8:12; document.querySelectorAll('.navbtn').forEach(b=>b.onclick=()=>showPage(b.dataset.page,b)); function showPage(id,b){document.querySelectorAll('.page,.navbtn').forEach(x=>x.classList.remove('active'));document.querySelector('#'+id).classList.add('active');b.classList.add('active')} function switchPane(sel,id,b){let root=b.closest('.card');root.querySelectorAll(sel+',.subtab').forEach(x=>x.classList.remove('active'));root.querySelector('#'+id).classList.add('active');b.classList.add('active')} function leaguePane(id,b){switchPane('.subpane',id,b)} function analyticsPane(id,b){switchPane('.subpane',id,b)} function awardPane(id,b){switchPane('.subpane',id,b)} async function recoverAccount(){ try{ let j=await api('/api/account/recover',{method:'POST',body:JSON.stringify({username:recUser.value,recovery_code:recCode.value,new_password:recPw.value})}); recMsg.innerHTML=`Password reset. Save your NEW recovery code somewhere safe: ${j.new_recovery_code}`; }catch(e){recMsg.textContent=e.error||'Recovery failed'} } async function requestReset(){ try{await api('/api/account/request-password-reset',{method:'POST',body:JSON.stringify({email:resetEmail.value})});recMsg.textContent='If that verified email exists, a reset link has been sent.'}catch(e){recMsg.textContent='Request could not be processed.'} } async function register(){try{let j=await api('/api/register',{method:'POST',body:JSON.stringify({username:ru.value,password:rp.value,email:re.value})});ME=j.user;if(j.recovery_code)alert('SAVE THIS RECOVERY CODE:\n'+j.recovery_code+'\n\nIt is required to reset your password in closed alpha.');await refresh()}catch(e){loginMsg.textContent=e.error||'Registration failed'}} async function login(){try{let j=await api('/api/login',{method:'POST',body:JSON.stringify({username:lu.value,password:lp.value})});ME=j.user;await refresh()}catch(e){loginMsg.textContent=e.error||'Login failed'}} async function logout(){await api('/api/logout',{method:'POST'});location.reload()} async function refresh(){ let me=await api('/api/me');ME=me.user; if(!ME){accountTag.textContent='Not signed in';return} accountTag.innerHTML=`${ME.username} • ${ME.role}   `; loginCard.classList.add('hidden');dashboard.classList.remove('hidden');adminNav.classList.toggle('hidden',ME.role!=='COMMISSIONER');coachNav.classList.toggle('hidden',!['COACH','COMMISSIONER'].includes(ME.role)); await Promise.all([loadLeague(),loadSchedule(),loadAnalytics(),loadAwards(),loadPlayer(),loadNews(),loadReadiness(),loadCommunity(),loadDMContacts()]);if(['COACH','COMMISSIONER'].includes(ME.role))await loadCoachHub(); await Promise.all([loadChat('EBL'),loadChat('TEAM')]); renderHome(); } function teamName(fid){let t=LEAGUE?.teams?.find(x=>x.id===fid);return t?t.name:fid} async function loadPlayer(){try{let j=await api('/api/my-player');PLAYER=j.player;renderPlayer()}catch{PLAYER=null}} function initials(n){return (n||'EB').split(/\s+/).slice(0,2).map(x=>x[0]).join('').toUpperCase()} function fmt3(v){return Number(v||0).toFixed(3).replace(/^0/,'')} function statBox(k,v){return `
${k}${v}
`} function hitterStats(p){ let s=p.season||{},ab=s.AB||0,pa=s.PA||0,h=s.H||0,bb=s.BB||0,tb=(s['1B']||0)+2*(s['2B']||0)+3*(s['3B']||0)+4*(s.HR||0); let avg=ab?h/ab:0,obp=pa?(h+bb)/pa:0,slg=ab?tb/ab:0; return [{id:'bat',title:'BATTING',stats:[['G',s.G||0],['PA',pa],['AB',ab],['H',h],['AVG',fmt3(avg)],['OBP',fmt3(obp)],['SLG',fmt3(slg)],['HR',s.HR||0],['2B',s['2B']||0],['3B',s['3B']||0],['BB',bb],['SO',s.SO||0],['R',s.R||0],['RBI',s.RBI||0]]}, {id:'fld',title:'FIELDING',stats:[['G',s.FG||0],['PO',s.PO||0],['A',s.A||0],['E',s.E||0],['DP',s.DP||0],['FLD%',s.FLD_PCT||'1.000'],['OAA',s.OAA||0]]}, {id:'run',title:'BASERUNNING',stats:[['SB',s.SB||0],['CS',s.CS||0],['SB%',(s.SB||s.CS)?((s.SB||0)/((s.SB||0)+(s.CS||0))*100).toFixed(1)+'%':'0.0%'],['XBT',s.XBT||0],['OOB',s.OOB||0],['BsR',s.BSR||'0.0']]}]} function pitcherStats(p){let s=p.season||{},outs=s.OUTS||0,ip=(outs/3).toFixed(1),era=outs?(s.ER||0)*27/outs:0,whip=outs?((s.H||0)+(s.BB||0))/(outs/3):0;return [{id:'pit',title:'PITCHING',stats:[['G',s.G||0],['GS',s.GS||0],['IP',ip],['W',s.W||0],['L',s.L||0],['SV',s.SV||0],['H',s.H||0],['ER',s.ER||0],['BB',s.BB||0],['SO',s.SO||0],['ERA',era.toFixed(2)],['WHIP',whip.toFixed(2)]]},{id:'pfld',title:'FIELDING',stats:[['PO',s.PO||0],['A',s.A||0],['E',s.E||0],['DP',s.DP||0],['FLD%',s.FLD_PCT||'1.000']]}]} function statTabs(defs){return `
${defs.map((d,i)=>``).join('')}
${defs.map((d,i)=>`
${d.stats.map(x=>statBox(x[0],x[1])).join('')}
`).join('')}`} function playerStat(id,b){let card=b.closest('.card');card.querySelectorAll('.statPane,.statTabs .subtab').forEach(x=>x.classList.remove('active'));card.querySelector('#'+id).classList.add('active');b.classList.add('active')} function renderPlayer(){ if(!PLAYER){playerMain.innerHTML='

No EBL player yet

Build your first Genesis player below.

';createCard.classList.remove('hidden');setCreatorStep(1);return} createCard.classList.add('hidden');let p=PLAYER,team=p.team?.name||'Free Agent',defs=p.type==='P'?pitcherStats(p):hitterStats(p); let f=FACE_PRESETS[(p.face_id||1)-1]||FACE_PRESETS[0]; let profileAvatar=`
`; playerMain.innerHTML=`
${profileAvatar}

${p.name}

${p.primary_pos} • Bats ${p.bats} • Throws ${p.throws} • ${team}
AVAILABLE XP${(+p.xp_wallet).toFixed(3)}
STATUS${p.status}
CONTRACT${p.contract?`${p.contract.salary}/G • ${p.contract.years_remaining} yr`:'—'}
LEGACYGenesis Career

Statistics

${statTabs(defs)}

Development

${Object.entries(p.attributes).map(([k,v])=>`
${k}
${v}
`).join('')}

Legacy

Career Seasons1
Awards0
Championships0
MilestonesGenesis Begins

CPU Market

${p.status==='FREE_AGENT'?``:''}${(p.offers||[]).map(o=>`
${teamName(o.franchise_id)}
${o.bonus} XP signing bonus • ${o.salary} XP/game • ${o.years} year(s)
`).join('')}`; } let CREATOR={name:'',position:'SS',bats:'R',throws:'R',face_id:1,hair_id:1,pool:50,attributes:{}}; const FACE_PRESETS=[ {skin:'#f0c4a6',hair:'#4a2d20',label:'Face 1'},{skin:'#dca37c',hair:'#2a1b16',label:'Face 2'}, {skin:'#c98d66',hair:'#4b3022',label:'Face 3'},{skin:'#b97855',hair:'#261b17',label:'Face 4'}, {skin:'#f1c7a4',hair:'#7a4a24',label:'Face 5'},{skin:'#8e5b43',hair:'#1e1714',label:'Face 6'}, {skin:'#6f4937',hair:'#161212',label:'Face 7'},{skin:'#d7a078',hair:'#3d281d',label:'Face 8'}, {skin:'#a86d4f',hair:'#231914',label:'Face 9'},{skin:'#e1b08b',hair:'#5b3622',label:'Face 10'} ]; const HAIR_NAMES=['Classic Crop','Short Fade','Textured Top','Loose Waves','Side Part','Buzz Cut','Medium Flow','Slick Back','Shaved','Curly/Afro']; function setCreatorStep(n){ document.querySelectorAll('.creatorStep').forEach((x,i)=>{x.classList.toggle('active',i===n-1);x.classList.toggle('done',i
`; } function saveBasic(){CREATOR.name=cname.value.trim()||'My Rookie';CREATOR.position=cpos.value;CREATOR.bats=cbats.value;CREATOR.throws=cthrows.value;setCreatorStep(2)} function creatorAppearance(){ creatorStage.innerHTML=`

FACE

${FACE_PRESETS.map((f,i)=>`
${f.label}
`).join('')}

HAIR STYLE

${HAIR_NAMES.map((n,i)=>`
${n}
`).join('')}

PREVIEW

${avatarMarkup()}
`; } function avatarMarkup(){ let f=FACE_PRESETS[CREATOR.face_id-1];return `
`; } function pickFace(id){CREATOR.face_id=id;creatorAppearance()} function pickHair(id){CREATOR.hair_id=id;creatorAppearance()} function beginAttributes(){ let pitcher=['SP','RP'].includes(CREATOR.position),names=pitcher?PA:HA; if(!CREATOR.attributes || Object.keys(CREATOR.attributes).join(',')!==names.join(',')){CREATOR.attributes=Object.fromEntries(names.map(x=>[x,0]));CREATOR.pool=50} setCreatorStep(3); } function creatorAttributes(){ D={...CREATOR,pool:CREATOR.pool,attributes:CREATOR.attributes}; const pitcher=['SP','RP'].includes(CREATOR.position); const groups=pitcher ? [{id:'buildPitch',title:'PITCHING',attrs:['STA','H9','K9','BB9','HR9','PCLT','CTRL','VEL','BRK']},{id:'buildField',title:'FIELDING',attrs:['FLD','ARM','ACC','REAC']}] : [{id:'buildHit',title:'HITTING',attrs:['CON','POW','VIS','DISC','TIM']},{id:'buildField',title:'FIELDING',attrs:['FLD','ARM','ACC','REAC']},{id:'buildRun',title:'BASERUNNING',attrs:['SPD']}]; creatorStage.innerHTML=`
STARTING POOL50 XP
REMAINING${CREATOR.pool} XP
SPENT${50-CREATOR.pool} XP
${groups.map((g,i)=>``).join('')}
${groups.map((g,i)=>`
${g.attrs.filter(a=>a in CREATOR.attributes).map(a=>creatorAttr(a)).join('')}
`).join('')}
`; } function creatorAttr(k){let v=CREATOR.attributes[k];return `
${k}
${v}
`} function addCreatorAttr(k){if(CREATOR.pool>0){CREATOR.attributes[k]++;CREATOR.pool--;creatorAttributes()}} function builderTab(id,b){let card=b.closest('.card')||document;card.querySelectorAll('.statPane,.statTabs .subtab').forEach(x=>x.classList.remove('active'));document.querySelector('#'+id).classList.add('active');b.classList.add('active')} function attributeReview(){if(CREATOR.pool!==0)return alert('Spend all 50 XP first.');setCreatorStep(4)} function creatorReview(){ creatorStage.innerHTML=`
${initials(CREATOR.name)}

${CREATOR.name}

${CREATOR.position} • Bats ${CREATOR.bats} • Throws ${CREATOR.throws}

50-XP Build

${Object.entries(CREATOR.attributes).map(([k,v])=>`
${k}${v}
`).join('')}

Appearance

${avatarMarkup()}

Face ${CREATOR.face_id} • ${HAIR_NAMES[CREATOR.hair_id-1]}

`; } async function finishCreator(){ try{ let payload={name:CREATOR.name,position:CREATOR.position,bats:CREATOR.bats,throws:CREATOR.throws,face_id:CREATOR.face_id,hair_id:CREATOR.hair_id,attributes:CREATOR.attributes}; await api('/api/player/create',{method:'POST',body:JSON.stringify(payload)}); await loadPlayer();renderHome(); }catch(e){alert(e.error||'Could not create player')} } async function spend(a){if(!confirm(`Spend ${cost(PLAYER.attributes[a])} XP to increase ${a}?`))return;try{await api('/api/player/spend-xp',{method:'POST',body:JSON.stringify({attribute:a})});await loadPlayer();renderHome()}catch(e){alert(e.error)}} async function market(){try{await api('/api/player/request-cpu-market',{method:'POST'});await loadPlayer()}catch(e){alert(e.error)}} async function respond(id,action){try{await api('/api/player/respond-offer',{method:'POST',body:JSON.stringify({offer_id:id,action})});await loadPlayer();await loadLeague();renderHome()}catch(e){alert(e.error)}} async function loadLeague(){let j=await api('/api/league');LEAGUE=j;renderStandings()} function renderStandings(){if(!LEAGUE)return;let html=`
Season ${LEAGUE.season} • League Day ${LEAGUE.day}
`;for(let d of LEAGUE.divisions){let teams=LEAGUE.teams.filter(t=>t.division===d).sort((a,b)=>b.wins-a.wins||((b.runs_for-b.runs_against)-(a.runs_for-a.runs_against)));let lead=teams[0]?.wins||0;html+=`

${d} Division

${teams.map(t=>{let g=t.wins+t.losses,pct=g?t.wins/g:0,gb=((lead-t.wins)+(t.losses-(teams[0]?.losses||0)))/2;return ``}).join('')}
TeamWLPCTGBRSRADIFF
${t.name}${t.wins}${t.losses}${pct.toFixed(3).replace(/^0/,'')}${gb<=0?'—':gb.toFixed(1)}${t.runs_for}${t.runs_against}${t.runs_for-t.runs_against>=0?'+':''}${t.runs_for-t.runs_against}
`}standingsPane.innerHTML=html} async function loadSchedule(){let j=await api('/api/schedule');SCHEDULE=j.games;renderSchedule();renderTicker();renderGamecastList()} function renderSchedule(){let day=LEAGUE?.day||0,filterTeam=PLAYER?.franchise_id;let upcoming=SCHEDULE.filter(g=>g.league_day>=Math.max(1,day) && g.league_day<=day+7);schedulePane.innerHTML=`
${scheduleRows(upcoming,'ALL')}
`} function scheduleRows(games,mode){let arr=mode==='MINE'&&PLAYER?.franchise_id?games.filter(g=>g.away_id===PLAYER.franchise_id||g.home_id===PLAYER.franchise_id):games;return arr.map(g=>`
Day ${g.league_day} • ${teamName(g.away_id)} @ ${teamName(g.home_id)}${g.status==='FINAL'?`${g.away_runs}-${g.home_runs} • WATCH`:'UPCOMING'}
`).join('')||'

No games in this window.

'} function scheduleFilter(mode,b){b.parentElement.querySelectorAll('.subtab').forEach(x=>x.classList.remove('active'));b.classList.add('active');let day=LEAGUE?.day||0;document.querySelector('#scheduleRows').innerHTML=scheduleRows(SCHEDULE.filter(g=>g.league_day>=Math.max(1,day)&&g.league_day<=day+7),mode)} function renderTicker(){let day=LEAGUE?.day||0,arr=SCHEDULE.filter(g=>g.league_day===Math.max(1,day)||g.league_day===day+1).slice(0,15);let items=arr.map(g=>`${g.status==='FINAL'?'FINAL':'NEXT'} ${teamName(g.away_id)} ${g.status==='FINAL'?g.away_runs:'@'} ${teamName(g.home_id)} ${g.status==='FINAL'?g.home_runs:''}`).join('');tickerTrack.innerHTML=items+items} function renderGamecastList(){let finals=SCHEDULE.filter(g=>g.status==='FINAL').slice(-30).reverse();gamecastList.innerHTML=finals.length?finals.map(g=>`
${teamName(g.away_id)} ${g.away_runs} — ${g.home_runs} ${teamName(g.home_id)} • WATCH GAMECAST
`).join(''):'

No final games yet. Genesis Opening Day will populate GameCast.

'} async function loadAnalytics(){let j=await api('/api/analytics'),L=j.league;aLeague.innerHTML=`
${[['Games',L.games],['Runs/Game',L.runs_per_game.toFixed(2)],['AVG',fmt3(L.avg)],['K%',(L.k_pct*100).toFixed(1)+'%'],['BB%',(L.bb_pct*100).toFixed(1)+'%'],['HR%',(L.hr_pct*100).toFixed(1)+'%']].map(x=>statBox(x[0],x[1])).join('')}

XP Economy

${Object.entries(j.xp).map(([k,v])=>``).join('')}
SourceTotal XPEvents
${k}${v.total.toFixed(3)}${v.events}
`;aBat.innerHTML=`
${j.leaders.ops.map((x,i)=>``).join('')}
#PlayerTeamPosOPSHRXP
${i+1}${x.name}${teamName(x.team)}${x.pos}${x.ops.toFixed(3)}${x.hr}${x.xp.toFixed(3)}
`;aPitch.innerHTML=`
${j.leaders.pitching.map((x,i)=>``).join('')}
#PitcherTeamERAWHIPSOXP
${i+1}${x.name}${teamName(x.team)}${x.era.toFixed(2)}${x.whip.toFixed(2)}${x.so}${x.xp.toFixed(3)}
`} async function loadAwards(){let j=await api('/api/awards');mvpPane.innerHTML=raceList(j.mvp,x=>`${x.pos} • ${teamName(x.team)} • OPS ${x.ops.toFixed(3)} • ${x.hr} HR`);batTitle.innerHTML=raceList(j.batting,x=>`${teamName(x.team)} • AVG ${x.avg.toFixed(3)} • OBP ${x.obp.toFixed(3)}`);pitchTitle.innerHTML=raceList(j.pitching,x=>`${teamName(x.team)} • ERA ${x.era.toFixed(2)} • WHIP ${x.whip.toFixed(2)} • ${x.so} K`);sbTitle.innerHTML=raceList(j.stolen_bases,x=>`${teamName(x.team)} • ${x.sb} SB • OBP ${x.obp.toFixed(3)}`);fieldTitle.innerHTML=Object.entries(j.fielding).map(([pos,rows])=>`

${pos}

${raceList(rows,x=>`${teamName(x.team)} • Genesis fielding race`)}`).join('')} function raceList(rows,detail){return rows.length?rows.map((x,i)=>`
${i+1}
${x.name}
${detail(x)}
${i<3?'🏆':''}
`).join(''):'

Race begins on Opening Day.

'} function renderHome(){ if(!PLAYER){homePlayer.innerHTML='

Create Your Player

Your Genesis career starts with 50 XP and a blank history.

';homeTeam.innerHTML='

No Team Yet

Create a player, enter the CPU market, and sign your first EBL contract.

';nextGame.innerHTML='

Next Game

Your schedule appears after signing.

';return} let p=PLAYER,t=p.team;homePlayer.innerHTML=`
${initials(p.name)}
MY PLAYER

${p.name}

${p.primary_pos} • ${(+p.xp_wallet).toFixed(3)} XP available
`; homeTeam.innerHTML=t?`MY TEAM

${t.name}

${t.wins}-${t.losses}
${t.division} Division
`:`MY TEAM

Free Agent

Enter the CPU market from the Player tab.

`; let ng=p.franchise_id?SCHEDULE.find(g=>g.status==='SCHEDULED'&&(g.away_id===p.franchise_id||g.home_id===p.franchise_id)&&g.league_day>=(LEAGUE?.day||0)):null; nextGame.innerHTML=ng?`NEXT GAME • DAY ${ng.league_day}

${teamName(ng.away_id)} @ ${teamName(ng.home_id)}

${ng.away_id===p.franchise_id?'Road game':'Home game'}

`:`

Next Game

${p.franchise_id?'No upcoming game found.':'Sign a contract to join the schedule.'}

`; } async function loadChat(ch){try{let j=await api('/api/chat/'+ch);let box=ch==='EBL'?eblChat:teamChat;box.innerHTML=j.messages.map(m=>`
${m.username} ${m.created_at}
${escapeHtml(m.message)}
`).join('');box.scrollTop=box.scrollHeight}catch{}} function escapeHtml(x){return x.replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))} async function sendChat(ch){let input=ch==='EBL'?eblMsg:teamMsg,msg=input.value.trim();if(!msg)return;try{await api('/api/chat/send',{method:'POST',body:JSON.stringify({channel:ch,message:msg})});input.value='';loadChat(ch)}catch(e){alert(e.error)}} let NEWS=[]; let DM_CONTACTS=[],DM_ACTIVE=null; async function loadDMContacts(){ try{ let j=await api('/api/dm/contacts');DM_CONTACTS=j.contacts||[]; dmContacts.innerHTML=DM_CONTACTS.length?DM_CONTACTS.map(c=>`
${c.username}
${c.player_name||c.role}${c.team_name?' • '+c.team_name:''}
`).join(''):'

No other users yet.

'; }catch(e){} } async function openDM(id){ DM_ACTIVE=id;let c=DM_CONTACTS.find(x=>x.id===id);dmTitle.textContent=c?`Conversation with ${c.username}`:'Conversation'; await loadDMContacts(); let j=await api('/api/dm/thread/'+id); dmThread.innerHTML=j.messages.map(m=>`
${m.sender_name} • ${m.created_at}
${escapeHtml(m.message)}
`).join(''); dmThread.scrollTop=dmThread.scrollHeight; } async function sendDM(){ if(!DM_ACTIVE)return alert('Choose a contact first.'); let msg=dmMsg.value.trim();if(!msg)return; try{await api('/api/dm/send',{method:'POST',body:JSON.stringify({recipient_user_id:DM_ACTIVE,message:msg})});dmMsg.value='';await openDM(DM_ACTIVE)}catch(e){alert(e.error)} } async function loadCommunity(){ try{ let [rv,rc]=await Promise.all([api('/api/rivalries'),api('/api/records')]); rivalryBoard.innerHTML=rv.rivalries.length?rv.rivalries.map(r=>`
${r.team_a_name} vs ${r.team_b_name}
${r.games} meetings • ${r.a_wins}-${r.b_wins} • ${r.one_run_games} one-run games
Intensity ${Math.round(r.intensity)}/100
`).join(''):'

No rivalries yet. They have to be earned.

'; recordBook.innerHTML=rc.records.length?rc.records.map(r=>`
${r.record_label}
${r.holder_name} — ${r.record_value}
${r.detail||''} • Day ${r.league_day}
`).join(''):'

Genesis begins with a blank record book.

'; }catch(e){} } async function loadReadiness(){ try{ let r=await api('/api/league/readiness'),pct=r.total?Math.round(r.filled/r.total*100):0; readinessBar.querySelector('span').style.width=pct+'%'; readinessText.innerHTML=`${r.filled}/${r.total} roster slots filled • ${r.human} human • ${r.cpu} CPU • Phase: ${r.phase}`; positionReadiness.innerHTML=r.positions.map(p=>`
${p.position_group}${p.filled}/${p.total}${p.human} human
`).join(''); }catch(e){} } async function loadNews(){ try{ let j=await api('/api/news');NEWS=j.news||[];renderNews('ALL');renderHomeNews(); }catch(e){ if(newsFeed)newsFeed.innerHTML='

The newsroom is waiting for the first EBL games to create history.

'; } } function renderNews(filter){ if(!window.newsFeed)return; let rows=filter==='ALL'?NEWS:NEWS.filter(n=>n.category===filter); newsFeed.innerHTML=rows.length?rows.map(n=>`
${n.category.replaceAll('_',' ')}LEAGUE DAY ${n.league_day}
${n.headline}
${n.body}
`).join(''):'

No stories in this section yet. Genesis history is still being written.

'; } function newsFilter(f,b){document.querySelectorAll('#news .subtab').forEach(x=>x.classList.remove('active'));b.classList.add('active');renderNews(f)} function renderHomeNews(){ if(!window.homeNews)return; let rows=NEWS.slice(0,5); homeNews.innerHTML=rows.length?rows.map(n=>`
${n.category.replaceAll('_',' ')} • DAY ${n.league_day}
${n.headline}
`).join(''):'

No headlines yet. The first pitch of Genesis will start the story.

'; } function goPage(id){let b=document.querySelector(`.navbtn[data-page="${id}"]`);if(b)b.click()} function uniformOptions(selected){return ['WHITE','NAVY','RED','GRAY','BLACK','CREAM'].map(x=>``).join('')} function teamLogoMarkup(style,p,s,a,name){ const shapes=['◆','⬟','★','⚾','A','E','EBL','▲','●','✦']; return `
${shapes[(style-1)%shapes.length]}
${name}
`; } function updateBrandPreview(){ if(!window.brandPreview)return; brandPreview.innerHTML=teamLogoMarkup(+brandLogo.value,brandPrimary.value,brandSecondary.value,brandAccent.value,brandName.value||'EBL TEAM'); } async function saveBranding(){ try{ await api('/api/coach/branding',{method:'POST',body:JSON.stringify({display_name:brandName.value,logo_style:+brandLogo.value,primary_color:brandPrimary.value,secondary_color:brandSecondary.value,accent_color:brandAccent.value,uniform_home:brandHome.value,uniform_away:brandAway.value})}); alert('Team identity saved.');await loadLeague();await loadCoachHub(); }catch(e){alert(e.error)} } async function loadCoachHub(){ try{ let j=await api('/api/coach/team'); if(!j.team){coachHub.innerHTML='

No franchise assigned.

';return} let H=j.roster.filter(x=>x.type==='H'),P=j.roster.filter(x=>x.type==='P'),SP=P.filter(x=>x.primary_pos==='SP'); let lineup=j.lineup||[],rot=j.rotation||[],bp=j.strategy?.bullpen||{},def=j.strategy?.defense||{}; coachHub.innerHTML=`
FRANCHISE${j.team.name}
RECORD${j.team.wins}-${j.team.losses}
XP BUDGET${(j.team.xp_budget-j.team.xp_spent).toFixed(1)}
OPEN OFFERS${j.offers.length}

Team Identity

Team Name
Logo Style
Primary
Secondary
Accent
Home Uniform
Away Uniform

Preview

Batting Order & Fielding Positions

${[0,1,2,3,4,5,6,7,8].map(i=>lineupRowHTML(i,lineup[i],H)).join('')}

Starting Rotation

${[0,1,2,3,4].map(i=>`
SP ${i+1}
`).join('')}

Bullpen Roles

Closer
Setup 1
Setup 2
Middle Relief
Long Relief
Emergency

Position Depth Chart

Rank backups for each position. The game engine can use these for substitutions and injuries later.

${['C','1B','2B','3B','SS','LF','CF','RF','DH'].map(pos=>depthRow(pos,H,j.strategy?.bench?.[pos]||[])).join('')}

Substitution Preferences

Pinch Hit Order
Pinch Run Order
Def. Replacements
Backup Catcher
Late Def. Inning
Pinch Hit Use
Steal Aggression
Bunt Aggression

Defensive Strategy

${shiftRow('Default','defDefault',def.default_shift||'STANDARD')} ${shiftRow('vs LHB','defL',def.vs_lhb||'STANDARD')} ${shiftRow('vs RHB','defR',def.vs_rhb||'STANDARD')}
Corners In
Infield In

Free Agents

Open Offers

${j.offers.length?j.offers.map(o=>`
${o.name} • ${o.primary_pos}
${o.bonus} XP bonus • ${o.salary}/G • ${o.years} yr
`).join(''):'

No open offers.

'}
`; if(window.brandPreview){updateBrandPreview();['brandName','brandLogo','brandPrimary','brandSecondary','brandAccent'].forEach(id=>document.querySelector('#'+id)?.addEventListener('input',updateBrandPreview));} }catch(e){coachHub.innerHTML=`

${e.error||'Could not load coach interface'}

`} } function coachPane(id,b){switchPane('.subpane',id,b)} function playerOptions(arr,selected,multi=[]){return arr.map(x=>``).join('')} function lineupRowHTML(i,id,H){ const posList=['C','1B','2B','3B','SS','LF','CF','RF','DH']; return `
${i+1}
`; } function shiftRow(label,id,val){let opts=['STANDARD','PULL','OPPO','NO_DOUBLES','BUNT_DEFENSE','INFIELD_IN'];return `
${label}
`} async function saveCoachLineup(){let ids=[...Array(9)].map((_,i)=>+document.querySelector('#bat'+i).value);try{await api('/api/coach/set-lineup',{method:'POST',body:JSON.stringify({batting_order:ids})});alert('Batting order saved.')}catch(e){alert(e.error)}} function multiVals(id){return [...document.querySelector('#'+id).selectedOptions].map(x=>+x.value)} async function savePitchingStaff(){ let rotation=[...Array(5)].map((_,i)=>+document.querySelector('#rot'+i).value); try{ await api('/api/coach/set-rotation',{method:'POST',body:JSON.stringify({rotation})}); let bullpen={CL:valOrNull('bpCL'),SU1:valOrNull('bpSU1'),SU2:valOrNull('bpSU2'),MR:multiVals('bpMR'),LR:multiVals('bpLR'),EMERGENCY:multiVals('bpEmergency')}; let j=await api('/api/coach/team');await api('/api/coach/set-strategy',{method:'POST',body:JSON.stringify({bullpen,defense:currentDefense(),bench:j.strategy.bench||{},substitutions:j.strategy.substitutions||{}})}); alert('Pitching staff saved.'); }catch(e){alert(e.error)} } function valOrNull(id){let v=document.querySelector('#'+id).value;return v?+v:null} function currentDefense(){return {default_shift:defDefault.value,vs_lhb:defL.value,vs_rhb:defR.value,corners_in:cornersIn.checked,infield_in:infieldIn.checked}} async function saveDefense(){try{let j=await api('/api/coach/team');await api('/api/coach/set-strategy',{method:'POST',body:JSON.stringify({bullpen:j.strategy.bullpen,defense:currentDefense(),bench:j.strategy.bench||{},substitutions:j.strategy.substitutions||{}})});alert('Defensive plan saved.')}catch(e){alert(e.error)}} function depthRow(pos,H,selected){ return `
${pos}
`; } function depthId(pos){return 'depth_'+pos.replace('1','one').replace('2','two').replace('3','three')} async function saveDepthSubs(){ try{ let j=await api('/api/coach/team'); let bench={}; ['C','1B','2B','3B','SS','LF','CF','RF','DH'].forEach(pos=>bench[pos]=multiVals(depthId(pos))); let substitutions={ pinch_hit:multiVals('phOrder'), pinch_run:multiVals('prOrder'), def_replacement:multiVals('drOrder'), catcher_backup:valOrNull('backupC'), late_inning_defense_inning:+lateDefInning.value, pinch_hit_threshold:phThresh.value, steal_aggression:stealAgg.value, bunt_aggression:buntAgg.value }; await api('/api/coach/set-strategy',{method:'POST',body:JSON.stringify({bullpen:j.strategy.bullpen,defense:j.strategy.defense,bench,substitutions})}); alert('Depth chart and substitution preferences saved.'); }catch(e){alert(e.error||'Could not save depth chart')} } async function loadCoachFreeAgents(){try{let j=await api('/api/coach/free-agents');coachFAs.innerHTML=j.players.slice(0,75).map(p=>`
${p.name} • ${p.primary_pos}
${Object.entries(p.attributes).slice(0,6).map(([k,v])=>`${k} ${v}`).join(' • ')}
`).join('')||'

No free agents.

'}catch(e){alert(e.error)}} async function coachOffer(pid){let bonus=+prompt('Signing bonus XP (0-25)','10'),salary=+prompt('Salary XP/game (.25,.30,.35,.40,.45)','.35'),years=+prompt('Years (1-3)','2');try{await api('/api/coach/offer',{method:'POST',body:JSON.stringify({player_id:pid,bonus,salary,years})});await loadCoachHub();alert('Offer sent.')}catch(e){alert(e.error)}} async function cancelOffer(id){try{await api('/api/coach/cancel-offer',{method:'POST',body:JSON.stringify({offer_id:id})});await loadCoachHub()}catch(e){alert(e.error)}} async function simDay(){try{let j=await api('/api/commish/sim-day',{method:'POST'});simout.textContent=`League Day ${j.day} complete — ${j.results.length} games`;await refresh()}catch(e){simout.textContent=e.error}} async function resetTestAccount(){ const target=document.getElementById('resetTestTarget').value.trim(); const out=document.getElementById('resetTestOut'); if(!target){ out.textContent='Enter a username or email.'; return; } if(!confirm(`Reset test account "${target}"? This will delete that test account and its player.`)){ return; } out.textContent='Resetting account...'; try{ const j=await api('/api/commish/reset-test-account',{ method:'POST', body:JSON.stringify({target}) }); out.textContent=`Reset complete: ${j.username}. Removed ${j.removed_players || 0} player(s) and restored ${j.restored_slots || 0} roster slot(s).`; document.getElementById('resetTestTarget').value=''; await refresh(); }catch(e){ out.textContent='Reset failed: '+(e.error || e.message || 'Unknown error'); } } function policyPane(id,b){switchPane('.subpane',id,b)} async function loadPolicies(){ communityRules.innerHTML=`

Community Rules

Keep EBL competitive, welcoming, and baseball-focused.

  1. No harassment, threats, hate speech, sexual content, or targeted abuse.
  2. No spam, scams, impersonation, or attempts to manipulate other users.
  3. No cheating, exploits, multi-account collusion, or intentionally corrupting league systems.
  4. Respect team and league moderation decisions. Use reports instead of escalating conflicts.
  5. Private messages are not a loophole around community rules.
  6. Franchise names, logos, chat, and player names must be appropriate for a general audience.

Violations may lead to warnings, mutes, suspensions, account removal, or competitive penalties.

`; termsPolicy.innerHTML=`

Beta Terms

EBL is currently a beta service. Features, balance, league formats, virtual progression, and Genesis records may change or reset before the hard launch.

Users must provide accurate account information, protect their credentials, avoid exploiting the service, and follow community rules. EBL may suspend accounts or pause league activity to protect competitive integrity and service safety.

Virtual XP, awards, franchise rights, and other in-game items have no cash value unless a future published policy explicitly says otherwise.

These beta terms are a product draft and should receive legal review before commercial/public launch.

`; privacyPolicy.innerHTML=`

Privacy Summary

EBL stores account credentials in hashed form, verified email addresses, login/session records, game activity, league statistics, chat/private messages, moderation reports, and technical security logs needed to operate the service.

Email addresses are used for account verification, password recovery, security notices, and service communications. EBL should not sell personal information.

Users can report abuse and block other users. Moderators may review reported community content and private messages when specifically reported or when required for safety/service integrity.

A jurisdiction-specific privacy policy and retention/deletion process should receive legal review before commercial/public launch.

`; } async function loadReports(){ try{let j=await api('/api/commish/reports');reportQueue.innerHTML=j.reports.length?j.reports.map(r=>`
#${r.id} ${r.reason} • ${r.status}
${r.reporter} reported ${r.reported||'content'} • ${r.created_at}
${escapeHtml(r.detail||'')} ${r.status==='OPEN'?`
`:''}
`).join(''):'

No reports.

'}catch(e){alert(e.error)} } async function moderateUser(uid,action){if(!uid)return;let reason=prompt('Reason:',action);if(reason===null)return;try{await api('/api/commish/moderate',{method:'POST',body:JSON.stringify({user_id:uid,action,minutes:1440,reason})});await loadReports()}catch(e){alert(e.error)}} async function resolveReport(id){let resolution=prompt('Resolution:','Reviewed');if(resolution===null)return;await api('/api/commish/resolve-report',{method:'POST',body:JSON.stringify({report_id:id,resolution})});loadReports()} async function backupNow(){try{let j=await api('/api/commish/backup-now',{method:'POST'});alert('Backup created: '+j.path)}catch(e){alert(e.error)}} async function loadLab(){let j=await api('/api/simulation-lab');labBody.innerHTML=`

Developer Simulation Lab

Seed ${j.seed}; ${j.pa_per_test.toLocaleString()} PA/test

${Object.entries(j.baseline).map(([k,v])=>statBox(k,typeof v==='number'?v.toFixed(3):v)).join('')}
`} async function watch(id){let j=await api('/api/game/'+id);GG=j.game;gi=0;clearInterval(timer);renderGameShell();gc.showModal();timer=setInterval(step,450)} function renderGameShell(){gcontent.innerHTML=`
${GG.id}

${teamName(GG.away_id)} ${GG.away_runs} — ${GG.home_runs} ${teamName(GG.home_id)}

GameCast

Play-by-Play

`} function speed(ms){clearInterval(timer);timer=setInterval(step,ms)}function replay(){gi=0;renderGameShell();speed(450)}function instant(){clearInterval(timer);while(gi=GG.events.length){clearInterval(timer);return}let e=GG.events[gi++],d=document.createElement('div');d.className='play';d.textContent=describe(e);feed.prepend(d);if(e.type==='PA_START')match.textContent=`${e.batter} vs ${e.pitcher}`;if(e.type==='PITCH'){pt.textContent=`${e.pitch_type} ${e.velocity} MPH — ${e.call}`;let q=document.createElement('div');q.className='dot';q.style.left=(e.px*100)+'%';q.style.top=((1-e.pz)*100)+'%';zone.appendChild(q)}if(e.type==='BALL_IN_PLAY'){metric.textContent=`EV ${e.exit_velocity} • LA ${e.launch_angle}° • ${e.contact_quality}`;ball.style.left=(50+e.spray_angle*.7)+'%';ball.style.top='20%'}} function describe(e){switch(e.type){case'GAME_START':return`${e.away_name} at ${e.home_name}`;case'PA_START':return`${e.half} ${e.inning}: ${e.batter} vs ${e.pitcher}`;case'PITCH':return`${e.pitch_type} ${e.velocity} MPH — ${e.call}`;case'BALL_IN_PLAY':return`${e.result}: ${e.exit_velocity} MPH, ${e.launch_angle}°`;case'PITCHING_CHANGE':return`Pitching change: ${teamName(e.team)} brings in ${e.role}`;case'PINCH_HITTER':return`Pinch hitter enters for player ${e.replaced_id}`;case'PINCH_RUNNER':return`Pinch runner enters for player ${e.replaced_id}`;case'DEFENSIVE_SHIFT':return`${teamName(e.team)} uses ${e.mode.replaceAll('_',' ')} alignment`;case'STEAL_ATTEMPT':return`Steal attempt — ${e.success?'SAFE':'OUT'}`;case'BUNT_ATTEMPT':return`Bunt attempt — ${e.success?'successful':'out recorded'}`;case'DEFENSIVE_REPLACEMENT_WINDOW':return`${teamName(e.team)} goes to late-inning defensive replacements`;case'OUT':return`${e.out_type} — ${e.outs} out(s)`;case'RUN':return`Run scores! ${e.score[0]}-${e.score[1]}`;case'PA_END':return`PA: ${e.result}`;case'INNING_END':return`End ${e.half} ${e.inning}`;case'GAME_END':return`FINAL ${e.final_score[0]}-${e.final_score[1]}`;default:return e.type}} loadPolicies(); refresh(); setInterval(()=>{if(ME){loadChat('EBL');loadChat('TEAM')}},15000);