fogón

← LibraryKitmormon/team-deathmatch

Duelo por equipos

m@mormonKit, MIT
v12 days ago
Clone
FOGONfogon pieces add team-deathmatch
GITgit clone https://git.fogon.gg/mormon/team-deathmatch.git
REMIXfogon new team-deathmatch --from team-deathmatch
ZIPteam-deathmatch.zipDownload
Controls

Duelo 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 primera persona: te están pegando desde adelante

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:

CarpetaQué hay
shared/deathmatchLo 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/deathmatchLas 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/deathmatchLo 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 su shared/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 de buildMap.
  • Otros personajes: Figure es un punto de partida. Cualquier modelo sirve si se lo pone donde dice room.view("fighters", id) y mide más o menos lo que BODY (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. fireGapMs tiene 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).