matrix-shooter-train / matrix /code /matrix_capture_game.py
peih's picture
backup matrix/code
f1caa47 verified
Raw History Blame Contribute Delete
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()