← BibliotecaKitmormon/team-deathmatch
Duelo por equipos
Clonar
fogon pieces add team-deathmatchgit clone https://git.fogon.gg/mormon/team-deathmatch.gitfogon new team-deathmatch --from team-deathmatchhttps://fogon.gg/piece/49/duelo-por-equiposDuelo por equipos
La base de un juego de disparos por equipos, lista para ponerle un mapa y una cara propios: dos equipos, bots que completan los lugares vacíos, vida que vuelve sola, bajas, reaparición con un momento de protección, marcador, reloj y rondas que empiezan una atrás de la otra. La sala decide todo lo que importa; la página camina, apunta y dibuja.

En la demo: un patio de cajas de 48 × 30 m, cuatro contra cuatro, a 30 bajas o cinco minutos. Entrás y ya estás jugando; los lugares que no ocupa una persona los juegan bots. WASD camina, el mouse mira, clic dispara (o F), espacio salta (a las cajas naranjas se sube) y Tab muestra la tabla. En el teléfono: el pulgar izquierdo camina, el derecho mira, y hay botones de fuego y salto. Con ?room=otra se abre otra partida, y una sala cuyo nombre empieza con quick (?room=quick-1) juega rondas cortas, a 5 bajas, para ver una ronda entera sin esperar.
Qué trae
Tres carpetas, y las tres se llevan:
| Carpeta | Qué hay |
|---|---|
shared/deathmatch | Lo que saben la sala y la página. map.ts: el mapa como datos (MapDef: una lista de cajas, dónde entra cada equipo y dónde es el medio), walkGrid (por dónde se puede caminar, en celdas de medio metro) y rayBoxes (hasta dónde llega una bala). nav.ts: createNav(map) da el camino de un punto a otro (A\*, prefiere los pasillos anchos), walkLine, sees, rayDist y spots. state.ts: el estado (Fighter, Match, game), las reglas que son números (RULES) y los mensajes. |
server/deathmatch | Las reglas. match.ts: deathmatch(map, rules?, hooks?), todo lo que una sala necesita para correr la partida. bots.ts: los bots (buscan a alguien del otro equipo que puedan ver, tardan un instante en reaccionar, disparan en ráfagas mientras se mueven de costado, y si no ven a nadie van adonde vieron al último o salen a buscar). |
client/deathmatch | Lo que se ve y se toca. walk.ts: Walker, caminar en primera persona entre las cajas (resbala contra las paredes, salta, se para arriba de las cosas). aim.ts: castShot, qué encuentra primero un disparo: una caja o alguien, y si fue en la cabeza. figure.ts: Figure, los demás como figuras de cajas del color de su equipo, y makeWeapon. scene.ts: buildMap (el mapa dibujado como lo que es, cajas con una grilla de un metro) y Tracers (la línea que deja un disparo). sound.ts: Sounds, los sonidos hechos con código. touch.ts: touchControls, los controles del teléfono. |
map.ts, nav.ts, walk.ts y aim.ts no dependen de ningún motor: son números. Lo de three.js está en figure.ts y scene.ts.
Lo demás es la demo: yard.ts (su mapa), server/room.ts (cinco líneas) y client/main.ts (el teclado, el mouse y los números de la pantalla).
Cómo se usa en un juego
1. El mapa. Cajas [minX, minY, minZ, maxX, maxY, maxZ] en metros, con y hacia arriba. De esa lista salen las colisiones, la grilla por la que caminan los bots y lo que frena una bala.
// arena.ts
import { block, type MapDef, turned } from "./shared/deathmatch/map";
const half = [
block(-12, -6, -11, 6, 3), // de (x0, z0) a (x1, z1), 3 m de alto
block(-5, 2, -3.5, 3.5, 0.9), // bajo: sirve de cobertura y se le salta encima
];
export const ARENA: MapDef = {
bounds: { x0: -20, z0: -12, x1: 20, z1: 12 },
boxes: [...half, ...turned(half)], // la otra mitad, girada: los dos lados iguales
bases: [[-19, -10, -16, 10], [16, -10, 19, 10]],
mid: [-8, -10, 8, 10],
};
2. La sala. El estado del kit y sus reglas, con los números que quieras cambiar:
// server/room.ts
import { defineRoom } from "@fogon/server";
import { game } from "../shared/deathmatch/state";
import { deathmatch } from "./deathmatch/match";
import { ARENA } from "../arena";
export default defineRoom({
...game,
...deathmatch(ARENA, { teamSize: 3, scoreLimit: 20, roundSeconds: 240 }, {
onRoundEnd(room, winner) { room.log("ganó el equipo", winner); },
}),
});
Las reglas (RULES en state.ts trae las de fábrica): teamSize, bots, scoreLimit, roundSeconds, overSeconds, respawnMs, safeMs, bodyDamage, headDamage, fireGapMs, maxRange, checkSight, healAfterMs y healPerSecond. En vez de un objeto se puede pasar una función (room) => reglas, para elegirlas por sala. hooks: onKill(room, by, who, head) y onRoundEnd(room, winner), por si querés una tabla de posiciones o logros. Qué tan buenos son los bots está en BOT, en bots.ts. Si tu juego necesita más mensajes, sumalos al lado: messages: { ...dm.messages, taunt(room, player) { … } }.
3. La página. Entra a la sala, mueve a su luchador y le avisa a la sala a quién le pegó:
import { join } from "@fogon/client";
import { BODY, game, Status } from "../shared/deathmatch/state";
import { castShot } from "./deathmatch/aim";
import { Figure } from "./deathmatch/figure";
import { buildMap, Tracers } from "./deathmatch/scene";
import { Walker } from "./deathmatch/walk";
import { ARENA } from "../arena";
const room = await join(game, { name, room: "main" });
const walker = new Walker(ARENA);
buildMap(scene, ARENA); // o tu propio dibujo, que entre en las mismas cajas
room.on("spawn", (d) => walker.place(d.x, d.z, d.yaw));
// En cada cuadro (dt en segundos):
walker.step({ forward: 1, right: 0, jump: false }, dt);
room.move({ x: walker.x, y: walker.y, z: walker.z, yaw: walker.yaw, pitch: walker.pitch });
camera.position.set(walker.x, walker.y + BODY.eye, walker.z);
camera.rotation.set(walker.pitch, walker.yaw, 0); // con camera.rotation.order = "YXZ"
// Al disparar: contra lo que se ve (los otros, donde se los dibuja).
const targets = [];
for (const [id, e] of room.state.fighters)
if (id !== room.selfId && e.v.team !== myTeam && e.v.status === Status.playing) targets.push({ id, ...room.view("fighters", id)! });
const shot = castShot(ARENA.boxes, targets, walker.x, walker.y + BODY.eye, walker.z, ...walker.aim());
room.send("shot", [...from, ...shot.end]); // para que los demás vean la línea
if (shot.hit) room.send("hit", { to: shot.hit.id, head: shot.hit.head });
Lo que manda la sala: "spawn" { x, z, yaw } (volviste, acá), "hurt" { amount, hp, from } (te pegaron, desde ahí), "kill" { by, who, head } y "shot" [desde…, hasta…, id] (el disparo de otro). El resto se lee del estado: room.state.match (phase, left, a, b, limit, round) y cada fighter (hp, status, team, kills, deaths, name, bot). client/main.ts muestra todo junto: el marcador, las bajas, de dónde viene el daño, el cartel de fin de ronda y la tabla.
Cambiarle la cara
- Otro mapa: solo los datos. Lo que se dibuja puede ser cualquier cosa mientras entre en las cajas: una pared con ventanas en vez de una caja lisa, un auto en vez de un bloque. La pieza Mercado del desierto de esta biblioteca trae un pueblo entero con sus cajas (
townMap(), en sushared/market/layout.ts, que no necesita three.js), y sirve tal cual:deathmatch(townMap())en la sala,buildTown(market)en la página en vez debuildMap. - Otros personajes:
Figurees un punto de partida. Cualquier modelo sirve si se lo pone donde diceroom.view("fighters", id)y mide más o menos lo queBODY(1,8 m de alto, 0,7 de ancho): con esas medidas se calculan los golpes. - Otra arma: la cadencia y la dispersión están en
client/main.ts(FIRE_MS,SPREAD); el daño, en las reglas.fireGapMstiene que ser un poco menor que el tiempo entre dos disparos, o la sala rechaza golpes buenos.
Límites
- La sala le cree a la página a quién le pegó. Controla lo que puede: que los dos estén en juego, que sean de equipos distintos, que no dispare más rápido que el arma, que no esté demasiado lejos y, con
checkSight: true, que no haya una caja en el medio (más estricto: con retraso de red, algún golpe bueno al doblar una esquina se pierde). Alcanza para jugar entre amigos; no frena a quien modifique su página. - El movimiento es de cada página (la sala solo frena lo imposiblemente rápido): no revisa que nadie atraviese cajas.
- Los bots caminan por el piso: no saltan ni se suben a nada, y un mapa con pisos de distintas alturas (rampas, escaleras, un primer piso) necesita otra forma de buscar caminos. La grilla es de medio metro: un pasillo de menos de 1,5 m de ancho les cuesta.
- Un mapa es un rectángulo con cajas derechas (sin girar). Probado hasta 80 × 60 m; las posiciones llegan a ±327 m y la búsqueda de caminos se rinde después de 12.000 celdas, así que en uno mucho más grande los bots se pierden.
- Hasta 8 personas por sala con las reglas de fábrica (
teamSize× 2). - No hay munición ni recarga, ni armas distintas, ni granadas: es la base.
tools/probe.ts es un jugador sin pantalla para probar las reglas de la sala: con fogon dev corriendo, bun tools/probe.ts ws://localhost:5300/ws quick-test.
Licencia
MIT. Hecho con three.js (MIT).
Elegí un archivo.
| Versión | Cuándo | Peso | Commit |
|---|---|---|---|
| v1 en la demo | 12 de nov de 2026 | 923 KB | 75dee4a |
Todavía nadie publicó una versión hecha a partir de esta pieza.
Armá el pedido para tu agente
Elegí qué querés hacer, copiá el pedido y pegáselo a tu IA (Claude, ChatGPT, Codex…). Ella hace el resto.