Skip to content

Commit

Permalink
feat(Vision): Add new vision-blocking mode: Behind
Browse files Browse the repository at this point in the history
  • Loading branch information
Kruptein authored Dec 15, 2023
2 parents c82e7a7 + 8f9e85d commit c97c9bc
Show file tree
Hide file tree
Showing 41 changed files with 514 additions and 193 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ tech changes will usually be stripped from release notes for the public

### Changed

- Vision blocking shapes will now ignore themselves if they are closed
- e.g. a tree trunk will be visible, but what's behind the tree trunk will remain hidden
- Open polygons will behave as they have in the past
- [tech] ModalStack now supports dynamically inserted

### Fixed
Expand Down
7 changes: 6 additions & 1 deletion client/src/apiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { AuraId } from "./game/systems/auras/models";
import type { CharacterId } from "./game/systems/characters/models";
import type { ClientId } from "./game/systems/client/models";
import type { PlayerId } from "./game/systems/players/models";
import type { VisionBlock } from "./game/systems/properties/types";
import type { TrackerId } from "./game/systems/trackers/models";

export type ApiShape = ApiAssetRectShape | ApiRectShape | ApiCircleShape | ApiCircularTokenShape | ApiPolygonShape | ApiTextShape | ApiLineShape | ApiToggleCompositeShape
Expand Down Expand Up @@ -149,7 +150,7 @@ export interface ApiCoreShape {
name_visible: boolean;
fill_colour: string;
stroke_colour: string;
vision_obstruction: boolean;
vision_obstruction: VisionBlock;
movement_obstruction: boolean;
is_token: boolean;
annotation: string;
Expand Down Expand Up @@ -659,6 +660,10 @@ export interface ShapeSetDoorToggleModeValue {
shape: GlobalId;
value: "movement" | "vision" | "both";
}
export interface ShapeSetIntegerValue {
shape: GlobalId;
value: number;
}
export interface ShapeSetOptionalStringValue {
shape: GlobalId;
value: string | null;
Expand Down
97 changes: 97 additions & 0 deletions client/src/core/components/ToggleGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<script setup lang="ts" generic="T, MS extends boolean">
import { computed } from "vue";
const props = withDefaults(
defineProps<{
// data
options: readonly { label: string; value: T }[];
modelValue: MS extends true ? T[] : T;
// styles
id?: string;
activeColor?: string;
color?: string;
disabled?: boolean;
multiSelect: MS;
}>(),
{
id: undefined,
color: "rgba(236, 242, 255, 0.25)",
activeColor: "rgba(236, 242, 255, 1)",
disabled: false,
},
);
const emit = defineEmits<(e: "update:modelValue", s: MS extends true ? T[] : T) => void>();
const data = computed(() => (props.multiSelect ? props.modelValue : [props.modelValue]) as T[]);
function toggle(option: T): void {
if (props.disabled) return;
let data: MS extends true ? T[] : T;
if (props.multiSelect) {
const arr = props.modelValue as T[];
if (arr.includes(option)) {
data = arr.filter((d) => d !== option) as MS extends true ? T[] : T;
} else {
data = [...arr, option] as MS extends true ? T[] : T;
}
} else {
// idk eslint and typescript are not agreeing with eachother
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
data = option as MS extends true ? T[] : T;
}
emit("update:modelValue", data);
}
</script>

<template>
<div :id="id" class="toggle-group" :class="{ disabled }">
<div
v-for="option of options"
:key="option.label"
:class="{ selected: data.includes(option.value) }"
@click="toggle(option.value)"
>
{{ option.label }}
</div>
</div>
</template>

<style scoped lang="scss">
.toggle-group {
display: flex;
align-items: stretch;
flex-wrap: wrap;
width: fit-content;
overflow: hidden;
border-radius: 1rem;
background-color: v-bind("color");
border: solid 2px v-bind("activeColor");
> div {
padding: 0.5rem 0.7rem;
display: flex;
align-items: center;
&:hover {
cursor: pointer;
background-color: v-bind("activeColor");
}
&.selected {
background-color: v-bind("activeColor");
}
}
&.disabled > {
div:hover {
cursor: not-allowed;
}
:not(.selected):hover {
background-color: inherit;
}
}
}
</style>
9 changes: 7 additions & 2 deletions client/src/game/api/emits/shape/options.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { ShapeSetBooleanValue, ShapeSetOptionalStringValue, ShapeSetStringValue } from "../../../../apiTypes";
import type {
ShapeSetBooleanValue,
ShapeSetIntegerValue,
ShapeSetOptionalStringValue,
ShapeSetStringValue,
} from "../../../../apiTypes";
import { wrapSocket } from "../../helpers";

export const sendShapeSetInvisible = wrapSocket<ShapeSetBooleanValue>("Shape.Options.Invisible.Set");
export const sendShapeSetDefeated = wrapSocket<ShapeSetBooleanValue>("Shape.Options.Defeated.Set");
export const sendShapeSetLocked = wrapSocket<ShapeSetBooleanValue>("Shape.Options.Locked.Set");
export const sendShapeSetIsToken = wrapSocket<ShapeSetBooleanValue>("Shape.Options.Token.Set");
export const sendShapeSetBlocksMovement = wrapSocket<ShapeSetBooleanValue>("Shape.Options.MovementBlock.Set");
export const sendShapeSetBlocksVision = wrapSocket<ShapeSetBooleanValue>("Shape.Options.VisionBlock.Set");
export const sendShapeSetBlocksVision = wrapSocket<ShapeSetIntegerValue>("Shape.Options.VisionBlock.Set");
export const sendShapeSetNameVisible = wrapSocket<ShapeSetBooleanValue>("Shape.Options.NameVisible.Set");
export const sendShapeSetShowBadge = wrapSocket<ShapeSetBooleanValue>("Shape.Options.ShowBadge.Set");

Expand Down
9 changes: 7 additions & 2 deletions client/src/game/api/events/shape/options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { ShapeSetBooleanValue, ShapeSetOptionalStringValue, ShapeSetStringValue } from "../../../../apiTypes";
import type {
ShapeSetBooleanValue,
ShapeSetIntegerValue,
ShapeSetOptionalStringValue,
ShapeSetStringValue,
} from "../../../../apiTypes";
import { UI_SYNC } from "../../../../core/models/types";
import type { Sync } from "../../../../core/models/types";
import { getLocalId, getShape } from "../../../id";
Expand Down Expand Up @@ -64,7 +69,7 @@ socket.on(

socket.on(
"Shape.Options.VisionBlock.Set",
wrapSystemCall<ShapeSetBooleanValue>(propertiesSystem.setBlocksVision.bind(propertiesSystem)),
wrapSystemCall<ShapeSetIntegerValue>(propertiesSystem.setBlocksVision.bind(propertiesSystem)),
);

socket.on(
Expand Down
9 changes: 7 additions & 2 deletions client/src/game/interfaces/shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface IShape extends SimpleShape {

get visionPolygon(): Path2D;
_visionBbox: BoundingRect | undefined;
_lightBlockingNeighbours: LocalId[];

// POSITION

Expand All @@ -81,8 +82,12 @@ export interface IShape extends SimpleShape {

// DRAWING

draw: (ctx: CanvasRenderingContext2D, customScale?: { center: GlobalPoint; width: number; height: number }) => void;
drawPost: (ctx: CanvasRenderingContext2D) => void;
draw: (
ctx: CanvasRenderingContext2D,
lightRevealRender: boolean,
customScale?: { center: GlobalPoint; width: number; height: number },
) => void;
drawPost: (ctx: CanvasRenderingContext2D, lightRevealRender: boolean) => void;
drawSelection: (ctx: CanvasRenderingContext2D) => void;

// VISION
Expand Down
8 changes: 7 additions & 1 deletion client/src/game/layers/variants/fowLighting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ export class FowLightingLayer extends FowLayer {
this.vCtx.globalCompositeOperation = "source-over";
this.vCtx.fillStyle = "rgba(0, 0, 0, 1)";
this.vCtx.fill(shape.visionPolygon);
for (const sh of shape._lightBlockingNeighbours) {
const hitShape = getShape(sh);
if (hitShape) {
hitShape.draw(this.vCtx, true);
}
}
if (auraDim > 0 && !hasGameboard) {
// Fill the light aura with a radial dropoff towards the outside.
const gradient = this.vCtx.createRadialGradient(
Expand Down Expand Up @@ -178,7 +184,7 @@ export class FowLightingLayer extends FowLayer {
else if (preShape.globalCompositeOperation === "destination-out")
preShape.globalCompositeOperation = "source-over";
}
preShape.draw(this.ctx);
preShape.draw(this.ctx, false);
preShape.globalCompositeOperation = ogComposite;
this.isEmpty = false;
}
Expand Down
6 changes: 6 additions & 0 deletions client/src/game/layers/variants/fowVision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export class FowVisionLayer extends FowLayer {
}

this.ctx.fill(token.visionPolygon);
for (const sh of token._lightBlockingNeighbours) {
const hitShape = getShape(sh);
if (hitShape) {
hitShape.draw(this.ctx, true);
}
}

// Out of Bounds check
if (token._visionBbox?.visibleInCanvas({ w: this.width, h: this.height }) ?? false) {
Expand Down
4 changes: 2 additions & 2 deletions client/src/game/layers/variants/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ export class Layer implements ILayer {
if (props.isInvisible && !accessSystem.hasAccessTo(shape.id, true, { vision: true })) continue;
if (labelSystem.isFiltered(shape.id)) continue;

shape.draw(ctx);
shape.draw(ctx, false);
}
}

Expand Down Expand Up @@ -465,7 +465,7 @@ export class Layer implements ILayer {
const modifiedRay = new Ray(g2l(ray.get(min)), ray.direction);
drawTear(modifiedRay, { fillColour: playerSettingsState.raw.rulerColour.value });
target = ray.getPointAtDistance(l2gz(68), min);
shape.draw(ctx, { center: target, width: 60, height: 60 });
shape.draw(ctx, false, { center: target, width: 60, height: 60 });
positionSystem.setTokenDirection(token, g2l(target));
found = true;
}
Expand Down
6 changes: 4 additions & 2 deletions client/src/game/operations/movement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { clientSystem } from "../systems/client";
import { gameState } from "../systems/game/state";
import { teleportZoneSystem } from "../systems/logic/tp";
import { getProperties } from "../systems/properties/state";
import { VisionBlock } from "../systems/properties/types";
import { selectedSystem } from "../systems/selected";
import { locationSettingsState } from "../systems/settings/location/state";
import { initiativeStore } from "../ui/initiative/state";
Expand Down Expand Up @@ -45,7 +46,7 @@ export async function moveShapes(shapes: readonly IShape[], delta: Vector, tempo
shape: shape.id,
});
}
if (props.blocksVision) {
if (props.blocksVision !== VisionBlock.No) {
recalculateVision = true;
visionState.deleteFromTriangulation({
target: TriangulationTarget.VISION,
Expand All @@ -66,7 +67,8 @@ export async function moveShapes(shapes: readonly IShape[], delta: Vector, tempo

if (props.blocksMovement && !temporary)
visionState.addToTriangulation({ target: TriangulationTarget.MOVEMENT, shape: shape.id });
if (props.blocksVision) visionState.addToTriangulation({ target: TriangulationTarget.VISION, shape: shape.id });
if (props.blocksVision !== VisionBlock.No)
visionState.addToTriangulation({ target: TriangulationTarget.VISION, shape: shape.id });

// todo: Fix again
// if (sel.refPoint.x % gridSize !== 0 || sel.refPoint.y % gridSize !== 0) sel.snapToGrid();
Expand Down
6 changes: 4 additions & 2 deletions client/src/game/operations/resize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { GlobalPoint } from "../../core/geometry";
import { sendShapeSizeUpdate } from "../api/emits/shape/core";
import type { IShape } from "../interfaces/shape";
import { getProperties } from "../systems/properties/state";
import { VisionBlock } from "../systems/properties/types";
import { TriangulationTarget, visionState } from "../vision/state";

export function resizeShape(
Expand All @@ -23,7 +24,7 @@ export function resizeShape(
shape: shape.id,
});
}
if (props.blocksVision) {
if (props.blocksVision !== VisionBlock.No) {
recalculateVision = true;
visionState.deleteFromTriangulation({
target: TriangulationTarget.VISION,
Expand All @@ -36,7 +37,8 @@ export function resizeShape(
// todo: think about calling deleteIntersectVertex directly on the corner point
if (props.blocksMovement && !temporary)
visionState.addToTriangulation({ target: TriangulationTarget.MOVEMENT, shape: shape.id });
if (props.blocksVision) visionState.addToTriangulation({ target: TriangulationTarget.VISION, shape: shape.id });
if (props.blocksVision !== VisionBlock.No)
visionState.addToTriangulation({ target: TriangulationTarget.VISION, shape: shape.id });

if (!shape.preventSync) sendShapeSizeUpdate({ shape, temporary });

Expand Down
6 changes: 4 additions & 2 deletions client/src/game/operations/rotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { GlobalPoint } from "../../core/geometry";
import { sendShapePositionUpdate } from "../api/emits/shape/core";
import type { IShape } from "../interfaces/shape";
import { getProperties } from "../systems/properties/state";
import { VisionBlock } from "../systems/properties/types";
import { TriangulationTarget, visionState } from "../vision/state";

export function rotateShapes(
Expand All @@ -24,7 +25,7 @@ export function rotateShapes(
shape: shape.id,
});
}
if (props.blocksVision) {
if (props.blocksVision !== VisionBlock.No) {
recalculateVision = true;
visionState.deleteFromTriangulation({
target: TriangulationTarget.VISION,
Expand All @@ -36,7 +37,8 @@ export function rotateShapes(

if (props.blocksMovement && !temporary)
visionState.addToTriangulation({ target: TriangulationTarget.MOVEMENT, shape: shape.id });
if (props.blocksVision) visionState.addToTriangulation({ target: TriangulationTarget.VISION, shape: shape.id });
if (props.blocksVision !== VisionBlock.No)
visionState.addToTriangulation({ target: TriangulationTarget.VISION, shape: shape.id });

if (!shape.preventSync) sendShapePositionUpdate([shape], temporary);
}
Expand Down
Loading

0 comments on commit c97c9bc

Please sign in to comment.