Download matrix/code/matrix_capture_game.py from peih/matrix-shooter-train: direct link, hf CLI and curl.
- Browser
- Download file 9.62 kB
-
https://huggingface.co/peih/matrix-shooter-train/resolve/main/matrix/code/matrix_capture_game.py
- Command line
-
hf download hf://peih/matrix-shooter-train/matrix/code/matrix_capture_game.py
-
curl -L -o matrix_capture_game.py https://huggingface.co/peih/matrix-shooter-train/resolve/main/matrix/code/matrix_capture_game.py
9.62 kB
| """Make a capture copy of matrix-shooter-js for paired graybox / styled data. | |
| usage: matrix_capture_game.py PACKAGE_DIR OUT_DIR | |
| PACKAGE_DIR is the unpacked delivery (start.py, games/, .local/). OUT_DIR gets | |
| the same tree with game.mjs patched; the package itself is never modified. | |
| The patched game, when the page sets window.__CAPTURE before loading: | |
| * does not run its own requestAnimationFrame loop; window.__cap.step() | |
| advances it by exactly 1/24 s, so pages fed the same inputs stay in lockstep | |
| (the simulation has no randomness of its own; respawns use a seeded PRNG); | |
| * takes the camera from window.__camOverride when set, so every style is | |
| rendered from the graybox page's camera (styles normally pull the camera | |
| further back and widen the field of view); | |
| * fires bullets from a style-independent point beside the actor, since each | |
| style's gun model puts its muzzle elsewhere and hits would otherwise diverge; | |
| * runs team battles: allies (player look) and enemies, each bot hunting the | |
| nearest living foe, bullets hurting the other team only, the dead respawning | |
| around the player, and the player refilled instead of dying. | |
| """ | |
| import argparse | |
| import shutil | |
| from pathlib import Path | |
| GAME = 'games/matrix-shooter-js/game.mjs' | |
| BOT_STEP = r'''function respawnBot(bot){for(let i=0;i<8;i++){const a=capRandom()*Math.PI*2,r=8+capRandom()*17,p=world.nav.nearest(player.p.x+Math.cos(a)*r,player.p.z+Math.sin(a)*r,6);if(p){bot.p=p;bot.alive=true;bot.health=100;bot.root.visible=true;bot.path=[];bot.think=0;return;}}} | |
| function botStep(bot,dt){ | |
| if(!bot.alive){bot.respawn-=dt;if(bot.respawn<=2)respawnBot(bot);return;} | |
| let foe=null,distance=1e9;for(const a of [player,...bots])if(a&&a.alive&&a!==bot&&a.team!==bot.team){const d=Math.hypot(a.p.x-bot.p.x,a.p.z-bot.p.z);if(d<distance){distance=d;foe=a;}} | |
| if(!foe){animateActor(bot,dt,false);return;} | |
| const dx=foe.p.x-bot.p.x,dz=foe.p.z-bot.p.z;bot.root.rotation.y=Math.atan2(dx,dz); | |
| bot.think-=dt;if(bot.think<=0){bot.think=.9+(bots.indexOf(bot)%4)*.15; | |
| bot.path=distance>bot.engage+1?world.nav.path(bot.p,foe.p,3500):[];bot.pathAt=0; | |
| } | |
| const target=bot.path[bot.pathAt];let moving=false; | |
| if(target&&distance>bot.engage){const x=target.x-bot.p.x,z=target.z-bot.p.z,d=Math.hypot(x,z); | |
| if(d<.2)bot.pathAt++;else{const before=bot.p;bot.p=world.nav.move(bot.p,x/d*dt*2.6,z/d*dt*2.6);moving=Math.hypot(bot.p.x-before.x,bot.p.z-before.z)>.001;}} | |
| else if(distance<=bot.engage){const s=Math.sin(elapsed*bot.strafeRate+bot.strafePhase),before=bot.p;bot.p=world.nav.move(bot.p,dz/distance*s*dt*1.6,-dx/distance*s*dt*1.6);moving=Math.hypot(bot.p.x-before.x,bot.p.z-before.z)>.001;} | |
| bot.fireAt-=dt;if(bot.fireAt<=0&&distance<35){bot.fireAt=.8+capRandom()*.9; | |
| const origin=new THREE.Vector3(bot.p.x,bot.p.y+1.2,-bot.p.z),point=new THREE.Vector3(foe.p.x,foe.p.y+1.1+(foe.height||0),-foe.p.z),dir=point.clone().sub(origin).normalize(); | |
| const wall=nearestWall(origin,dir,distance);if(!wall||wall.distance>distance-.6){bot.root.position.set(bot.p.x,bot.p.y,bot.p.z);const from=muzzle(bot,0);audio.play('shot',{weapon:0,style:currentSkin(),position:from,level:.45});launch(from,point.sub(from).normalize(),0,bot.team?'enemy':'ally');} | |
| } | |
| animateActor(bot,dt,moving);''' | |
| HOOK = r''' | |
| // ---- capture hook (matrix_capture_game.py) ---- | |
| if(window.__CAPTURE){ | |
| const pages=[]; | |
| window.__cap={ | |
| ready:()=>!loading&&!!world&&!!player, | |
| points:()=>(world.config.points??[]).map(p=>({id:p.id,name:p.name,x:p.x,z:p.z,yaw:p.yaw??null,pitch:p.pitch??null})), | |
| blocks(size){const b=world.config.bounds??{min:[-78,0,-78],max:[78,0,78]},out=[];for(let x=b.min[0]+size/2;x<b.max[0];x+=size)for(let z=b.min[2]+size/2;z<b.max[2];z+=size){const p=world.nav.nearest(x,z,size/2);if(p&&Math.abs(p.x-x)<=size/2&&Math.abs(p.z-z)<=size/2)out.push([p.x,p.z]);}return out;}, | |
| nearest:(x,z,r)=>world.nav.nearest(x,z,r??4), | |
| setup(cfg){ | |
| capSeed=cfg.seed>>>0||1; | |
| canvas.style.cssText='position:fixed;left:0;top:0;width:'+cfg.width+'px;height:'+cfg.height+'px;z-index:2147483647';resize(); | |
| resetActors();for(const b of bots)removeActor(b);bots=[];player.team=0; | |
| const p=world.nav.nearest(cfg.player.x,cfg.player.z,6);if(!p)throw Error('no walkable cell near player start'); | |
| player.p=p;yaw=cfg.player.yaw;pitch=cfg.player.pitch;player.root.rotation.y=yaw; | |
| for(const [team,list] of [[0,cfg.allies],[1,cfg.enemies]])for(const [x,z] of list){const q=world.nav.nearest(x,z,6);if(!q)continue; | |
| const b=actor(q,team===1);b.enemy=true;b.team=team;b.fireAt=.4+capRandom()*1.2;b.engage=cfg.engage[0]+capRandom()*(cfg.engage[1]-cfg.engage[0]); | |
| b.strafeRate=.7+capRandom()*.9;b.strafePhase=capRandom()*6.283;bots.push(b);} | |
| started=true;paused=false;overview=false;$('#overlay').hidden=true;window.__camOverride=null; | |
| animateActor(player,0,false);for(const b of bots)animateActor(b,0,false);updateCamera(true);render(); | |
| return this.state(); | |
| }, | |
| state(){return {elapsed,yaw,pitch,weapon,ammo:ammo[weapon],reloading,shots,hits,kills,bullets:ballistics.items.length,grenades:grenades.items.length, | |
| player:{x:player.p.x,y:player.p.y,z:player.p.z,h:player.height,health:player.health}, | |
| actors:bots.map(b=>({team:b.team,alive:b.alive,x:b.p.x,y:b.p.y,z:b.p.z,health:b.health}))};}, | |
| camera(){return {p:camera.position.toArray(),q:camera.quaternion.toArray(),fov:camera.fov,v:player.root.visible,calls:renderer.info.render.calls,near:camera.near, | |
| s:cameraState?{distance:cameraState.distance,desiredDistance:cameraState.desiredDistance,obstructed:cameraState.obstructed}:null};}, | |
| shot(){render();return canvas.toDataURL('image/png');}, | |
| step(input){ | |
| for(const k of input.up)window.dispatchEvent(new KeyboardEvent('keyup',{key:k})); | |
| for(const k of input.down)window.dispatchEvent(new KeyboardEvent('keydown',{key:k})); | |
| yaw=input.yaw;pitch=input.pitch;window.__camOverride=input.cam||null; | |
| step(1/24);render(); | |
| const out={state:this.state(),camera:this.camera()}; | |
| if(input.capture)out.image=canvas.toDataURL('image/png'); | |
| return out; | |
| }, | |
| }; | |
| } | |
| ''' | |
| PATCHES = [ | |
| # Own loop off in capture mode. | |
| ('requestAnimationFrame(frame);\ncanvas.addEventListener', 'if(!window.__CAPTURE)requestAnimationFrame(frame);\ncanvas.addEventListener'), | |
| # Seeded PRNG for respawns and bot timing. | |
| ('let player=null,bots=[]', 'let capSeed=1;function capRandom(){capSeed=capSeed+0x6D2B79F5|0;let t=Math.imul(capSeed^capSeed>>>15,1|capSeed);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;}\nlet player=null,bots=[]'), | |
| # Camera from the graybox page. | |
| ('function updateCamera(instant=false){if(!player||!world)return;', | |
| 'function updateCamera(instant=false){if(!player||!world)return;' | |
| 'if(window.__camOverride&&!overview){const o=window.__camOverride;aimVectors();camera.position.fromArray(o.p);camera.quaternion.fromArray(o.q);' | |
| 'camera.fov=o.fov;camera.updateProjectionMatrix();camera.updateMatrixWorld();player.root.visible=o.v;cameraState=o.s;return;}'), | |
| # Style-independent bullet origin. | |
| ('a.flash.visible=true;return point;\n}', | |
| 'a.flash.visible=true;return window.__CAPTURE?a.root.localToWorld(new THREE.Vector3(.35,1.2,.9)):point;\n}'), | |
| # Bullets hurt the other team. | |
| ("for(const a of p.owner==='player'?bots:[player]){if(!a.alive)continue;", | |
| "for(const a of p.owner==='enemy'?[player,...bots.filter(b=>b.team===0)]:bots.filter(b=>b.team===1)){if(!a.alive)continue;"), | |
| ("if(p.owner!=='player'||hit.kind!=='actor')return;\n hits++;audio.play('hit',{style:currentSkin()});hitTime=.18;const target=hit.target;target.health-=p.damage;\n if(target.health<=0){", | |
| "if(hit.kind!=='actor')return;const target=hit.target;\n if(p.owner==='player'){hits++;audio.play('hit',{style:currentSkin()});hitTime=.18;}target.health-=p.damage;\n " | |
| "if(target===player){if(player.health<=0)player.health=100;return;}\n if(target.health<=0){"), | |
| # Background pages must not pause themselves or drop held keys. | |
| ("window.addEventListener('blur',()=>{release();if(started)pause(true);});", | |
| "if(!window.__CAPTURE)window.addEventListener('blur',()=>{release();if(started)pause(true);});"), | |
| ("document.addEventListener('visibilitychange',()=>{if(document.hidden&&started)pause(true);});", | |
| "if(!window.__CAPTURE)document.addEventListener('visibilitychange',()=>{if(document.hidden&&started)pause(true);});"), | |
| # The player's hitscan and grenades only look at enemies. | |
| ('for(const bot of bots){if(!bot.alive)continue;targetBox.min', 'for(const bot of bots.filter(b=>b.team===1)){if(!bot.alive)continue;targetBox.min'), | |
| ('for(const bot of bots){if(!bot.alive)continue;\n const target={x:bot.p.x', 'for(const bot of bots.filter(b=>b.team===1)){if(!bot.alive)continue;\n const target={x:bot.p.x'), | |
| ] | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument('package', type=Path) | |
| parser.add_argument('out', type=Path) | |
| args = parser.parse_args() | |
| if args.out.exists(): | |
| shutil.rmtree(args.out) | |
| shutil.copytree(args.package, args.out) | |
| game = args.out / GAME | |
| text = game.read_text() | |
| start = text.index('function botStep(bot,dt){') | |
| end = text.index(' animateActor(bot,dt,moving);', start) + len(' animateActor(bot,dt,moving);') | |
| text = text[:start] + BOT_STEP + text[end:] | |
| for old, new in PATCHES: | |
| if text.count(old) != 1: | |
| raise SystemExit(f'patch target found {text.count(old)} times: {old[:70]!r}') | |
| text = text.replace(old, new) | |
| game.write_text(text + HOOK) | |
| print('capture game at', args.out) | |
| if __name__ == '__main__': | |
| main() | |