heal beam
All checks were successful
CI / checks (push) Successful in 59s

This commit is contained in:
Robin Appelman 2025-06-28 23:03:20 +02:00
commit d33a8df45d
5 changed files with 99 additions and 17 deletions

View file

@ -186,6 +186,7 @@ export enum MedigunType {
export interface MedicState { export interface MedicState {
charge: number, charge: number,
medigun: MedigunType medigun: MedigunType
target: number,
} }
export interface SpyState { export interface SpyState {
@ -350,7 +351,8 @@ function unpackPlayer(bytes: Uint8Array, base: number, world: WorldBoundaries, i
case Class.Medic: case Class.Medic:
class_data = { class_data = {
charge: class_bits[0], charge: class_bits[0],
medigun: class_bits[1], medigun: (class_bits[1] >> 6),
target: class_bits[1] & 31,
} }
break; break;
case Class.Spy: case Class.Spy:

View file

@ -5,6 +5,8 @@ import {findMapAlias} from './MapBoundries';
import {PlayerState, Header, WorldBoundaries, BuildingState, ProjectileState, CartState} from "./Data/Parser"; import {PlayerState, Header, WorldBoundaries, BuildingState, ProjectileState, CartState} from "./Data/Parser";
import {Show} from "solid-js"; import {Show} from "solid-js";
import {Projectile} from "./Render/Cart"; import {Projectile} from "./Render/Cart";
import {HealBeam} from "./Render/HealBeam";
import {medics} from "./Render/PlayerSpec";
export interface MapRenderProps { export interface MapRenderProps {
header: Header; header: Header;
@ -28,6 +30,7 @@ export function MapRender(props: MapRenderProps) {
const mapAlias = findMapAlias(props.header.map); const mapAlias = findMapAlias(props.header.map);
const image = `${map_root}images/${mapAlias}.webp`; const image = `${map_root}images/${mapAlias}.webp`;
const background = `url(${image})`; const background = `url(${image})`;
const medicPlayers = () => medics(props.players);
return ( return (
<svg class="map-background" width={props.size.width} height={props.size.height} <svg class="map-background" width={props.size.width} height={props.size.height}
@ -65,10 +68,18 @@ export function MapRender(props: MapRenderProps) {
</filter> </filter>
</defs> </defs>
<For each={medicPlayers()}>{(player) =>
<Show when={player.health}>
<HealBeam player={player} mapBoundary={props.world} targetSize={props.size} scale={props.scale}
players={props.players}
/>
</Show>
}</For>
<For each={props.players}>{(player) => <For each={props.players}>{(player) =>
<Show when={player.health}> <Show when={player.health}>
<PlayerDot player={player} mapBoundary={props.world} targetSize={props.size} scale={props.scale} <PlayerDot player={player} mapBoundary={props.world} targetSize={props.size} scale={props.scale}
onHover={props.onHover} onHover={props.onHover}
players={props.players}
highlighted={props.highlighted === player.info.userId} highlighted={props.highlighted === player.info.userId}
/> />
</Show> </Show>

View file

@ -0,0 +1,67 @@
import {Class, MedicState, PlayerState, WorldBoundaries} from "../Data/Parser";
import {Show} from "solid-js";
export interface HealBeamProp {
player: PlayerState;
mapBoundary: WorldBoundaries;
targetSize: {
width: number;
height: number;
};
scale: number;
players: PlayerState[];
}
export function HealBeam(props: HealBeamProp) {
const worldWidth = props.mapBoundary.boundary_max.x - props.mapBoundary.boundary_min.x;
const worldHeight = props.mapBoundary.boundary_max.y - props.mapBoundary.boundary_min.y;
const scaleX = (x) => (x - props.mapBoundary.boundary_min.x) / worldWidth * props.targetSize.width;
const scaleY = (y) => (worldHeight - (y - props.mapBoundary.boundary_min.y)) / worldHeight * props.targetSize.height;
const scaledX = () => scaleX(props.player.position.x);
const scaledY = () => scaleY(props.player.position.y);
const targetPosition = () => {
if (props.player.playerClass !== Class.Medic) {
return null;
}
const target = (props.player.class_data as MedicState).target;
if (target === 0) {
return null;
}
for (let player of props.players) {
if (player.info.entityId === target) {
return {
x: scaleX(player.position.x),
y: scaleY(player.position.y),
};
}
}
return null;
};
const distance = () => Math.sqrt(Math.pow(Math.abs(scaledX() - targetPosition().x), 2) + Math.pow(Math.abs(scaledY() - targetPosition().y), 2));
const aimHandle = () => {
let angle = (270 - props.player.angle) / 360 * (Math.PI * 2);
return {
x: scaledX() - Math.sin(angle) * (distance() / 2),
y: scaledY() + Math.cos(angle) * (distance() / 2)
}
}
const aimStart = () => {
let angle = (270 - props.player.angle) / 360 * (Math.PI * 2);
return {
x: scaledX() - Math.sin(angle) * (16 / props.scale),
y: scaledY() + Math.cos(angle) * (16 / props.scale)
}
}
const path = () => `M ${aimStart().x} ${aimStart().y} C ${aimHandle().x} ${aimHandle().y} ${aimHandle().x} ${aimHandle().y} ${targetPosition().x} ${targetPosition().y}`;
return <g>
<Show when={targetPosition()}>
<path
d={path()}
fill="transparent" stroke="white" stroke-width={1.5 / props.scale}
/>
</Show>
</g>
}

View file

@ -10,6 +10,7 @@ export interface PlayerProp {
scale: number; scale: number;
onHover: (userId: number) => void; onHover: (userId: number) => void;
highlighted: boolean; highlighted: boolean;
players: PlayerState[];
} }
const healthMap = { const healthMap = {
@ -65,21 +66,22 @@ export function Player(props: PlayerProp) {
const teamHatch = () => (props.player.team === Team.Red) ? `url(#diagonalHatchRed)` : `url(#diagonalHatchBlue)` const teamHatch = () => (props.player.team === Team.Red) ? `url(#diagonalHatchRed)` : `url(#diagonalHatchBlue)`
const fill = () => props.player.cloaked ? teamHatch() : teamColor(); const fill = () => props.player.cloaked ? teamHatch() : teamColor();
return <g return (
onmouseover={() => props.onHover(props.player.info.userId)} <g
onmouseout={() => props.onHover(0)} onmouseover={() => props.onHover(props.player.info.userId)}
transform={transform()}> onmouseout={() => props.onHover(0)}
<polygon points="-6,14 0, 16 6,14 0,24" fill="white" transform={transform()}>
opacity={imageOpacity()} <polygon points="-6,14 0, 16 6,14 0,24" fill="white"
transform={rotate()}/> opacity={imageOpacity()}
<circle r={16} stroke-width={props.highlighted ? 4 : 1.5} stroke="white" transform={rotate()}/>
stroke-dasharray={circleStrokeStyle()} <circle r={16} stroke-width={props.highlighted ? 4 : 1.5} stroke="white"
fill={fill()} stroke-dasharray={circleStrokeStyle()}
opacity={alpha()} fill={fill()}
filter={filter()} opacity={alpha()}
/> filter={filter()}
{getClassImage(props.player, imageOpacity())} />
</g> {getClassImage(props.player, imageOpacity())}
</g>)
} }
function getClassImage(player: PlayerState, imageOpacity: number) { function getClassImage(player: PlayerState, imageOpacity: number) {

View file

@ -66,7 +66,7 @@ function filterPlayers(players: PlayerState[], team: number): PlayerState[] {
return filtered; return filtered;
} }
function medics(players: PlayerState[]): PlayerState[] { export function medics(players: PlayerState[]): PlayerState[] {
return players.filter(player => player.playerClass === Class.Medic); return players.filter(player => player.playerClass === Class.Medic);
} }