2019-07-18 21:39:23 +08:00
|
|
|
import { autorun, runInAction } from "mobx";
|
2019-07-18 01:37:48 +08:00
|
|
|
import { Intersection, Mesh, MeshLambertMaterial, Plane, Raycaster, Vector2, Vector3 } from "three";
|
|
|
|
import { Vec3 } from "../data_formats/vector";
|
2019-07-18 21:39:23 +08:00
|
|
|
import { QuestEntity, QuestNpc, QuestObject, Section } from "../domain";
|
2019-07-18 01:37:48 +08:00
|
|
|
import { quest_editor_store } from "../stores/QuestEditorStore";
|
2019-07-18 21:39:23 +08:00
|
|
|
import { AreaUserData } from "./conversion/areas";
|
2019-07-18 01:37:48 +08:00
|
|
|
import {
|
2019-07-18 03:59:41 +08:00
|
|
|
EntityUserData,
|
2019-07-18 01:37:48 +08:00
|
|
|
NPC_COLOR,
|
2019-07-18 03:59:41 +08:00
|
|
|
NPC_HIGHLIGHTED_COLOR,
|
2019-07-18 01:37:48 +08:00
|
|
|
NPC_SELECTED_COLOR,
|
|
|
|
OBJECT_COLOR,
|
2019-07-18 03:59:41 +08:00
|
|
|
OBJECT_HIGHLIGHTED_COLOR,
|
2019-07-18 01:37:48 +08:00
|
|
|
OBJECT_SELECTED_COLOR,
|
|
|
|
} from "./conversion/entities";
|
|
|
|
import { QuestRenderer } from "./QuestRenderer";
|
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
type Selection = {
|
2019-07-18 01:37:48 +08:00
|
|
|
entity: QuestEntity;
|
2019-07-18 21:39:23 +08:00
|
|
|
mesh: Mesh;
|
|
|
|
};
|
|
|
|
|
|
|
|
type Pick = {
|
|
|
|
initial_position: Vec3;
|
2019-07-18 01:37:48 +08:00
|
|
|
grab_offset: Vector3;
|
|
|
|
drag_adjust: Vector3;
|
|
|
|
drag_y: number;
|
|
|
|
};
|
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
type PickResult = Pick & {
|
|
|
|
entity: QuestEntity;
|
|
|
|
mesh: Mesh;
|
|
|
|
};
|
|
|
|
|
2019-07-18 03:59:41 +08:00
|
|
|
enum ColorType {
|
|
|
|
Normal,
|
|
|
|
Highlighted,
|
|
|
|
Selected,
|
|
|
|
}
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
export class QuestEntityControls {
|
2019-07-18 01:37:48 +08:00
|
|
|
private raycaster = new Raycaster();
|
2019-07-18 21:39:23 +08:00
|
|
|
private selected?: Selection;
|
|
|
|
private highlighted?: Selection;
|
|
|
|
/**
|
|
|
|
* Iff defined, the user is transforming the selected entity.
|
|
|
|
*/
|
|
|
|
private pick?: Pick;
|
2019-07-18 03:59:41 +08:00
|
|
|
private last_pointer_position = new Vector2(0, 0);
|
|
|
|
private moved_since_last_mouse_down = false;
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
constructor(private renderer: QuestRenderer) {
|
|
|
|
autorun(() => {
|
|
|
|
const entity = quest_editor_store.selected_entity;
|
|
|
|
|
|
|
|
if (!this.selected || this.selected.entity !== entity) {
|
|
|
|
this.stop_transforming();
|
|
|
|
|
|
|
|
if (entity) {
|
|
|
|
// Mesh might not be loaded yet.
|
|
|
|
this.try_highlight_selected();
|
|
|
|
} else {
|
|
|
|
this.deselect();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Highlights the selected entity if its mesh has been loaded.
|
|
|
|
*/
|
|
|
|
try_highlight_selected = () => {
|
|
|
|
const entity = quest_editor_store.selected_entity!;
|
|
|
|
const mesh = this.renderer.get_entity_mesh(entity);
|
|
|
|
|
|
|
|
if (mesh) {
|
|
|
|
this.select({ entity, mesh });
|
|
|
|
}
|
|
|
|
};
|
2019-07-18 01:37:48 +08:00
|
|
|
|
|
|
|
on_mouse_down = (e: MouseEvent) => {
|
2019-07-18 03:59:41 +08:00
|
|
|
this.process_event(e);
|
2019-07-18 21:39:23 +08:00
|
|
|
this.stop_transforming();
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 03:59:41 +08:00
|
|
|
const new_pick = this.pick_entity(this.renderer.pointer_pos_to_device_coords(e));
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 03:59:41 +08:00
|
|
|
if (new_pick) {
|
|
|
|
// Disable camera controls while the user is transforming an entity.
|
2019-07-18 21:39:23 +08:00
|
|
|
this.renderer.controls.enabled = false;
|
|
|
|
this.pick = new_pick;
|
2019-07-18 03:59:41 +08:00
|
|
|
this.select(new_pick);
|
2019-07-18 21:39:23 +08:00
|
|
|
} else {
|
|
|
|
this.renderer.controls.enabled = true;
|
|
|
|
this.pick = undefined;
|
2019-07-18 01:37:48 +08:00
|
|
|
}
|
|
|
|
|
2019-07-18 03:59:41 +08:00
|
|
|
this.renderer.schedule_render();
|
|
|
|
};
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 03:59:41 +08:00
|
|
|
on_mouse_up = (e: MouseEvent) => {
|
|
|
|
this.process_event(e);
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
if (!this.moved_since_last_mouse_down && !this.pick) {
|
|
|
|
// If the user clicks on nothing, deselect the currently selected entity.
|
2019-07-18 03:59:41 +08:00
|
|
|
this.deselect();
|
2019-07-18 01:37:48 +08:00
|
|
|
}
|
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
this.stop_transforming();
|
2019-07-18 03:59:41 +08:00
|
|
|
// Enable camera controls again after transforming an entity.
|
2019-07-18 21:39:23 +08:00
|
|
|
this.renderer.controls.enabled = true;
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 03:59:41 +08:00
|
|
|
this.renderer.schedule_render();
|
2019-07-18 01:37:48 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
on_mouse_move = (e: MouseEvent) => {
|
2019-07-18 03:59:41 +08:00
|
|
|
this.process_event(e);
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 03:59:41 +08:00
|
|
|
const pointer_device_pos = this.renderer.pointer_pos_to_device_coords(e);
|
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
if (this.selected && this.pick) {
|
|
|
|
// User is tranforming selected entity.
|
2019-07-18 01:37:48 +08:00
|
|
|
if (e.buttons === 1) {
|
2019-07-18 21:39:23 +08:00
|
|
|
// User is dragging selected entity.
|
2019-07-18 01:37:48 +08:00
|
|
|
if (e.shiftKey) {
|
|
|
|
// Vertical movement.
|
2019-07-18 21:39:23 +08:00
|
|
|
this.translate_vertically(this.selected, this.pick, pointer_device_pos);
|
2019-07-18 01:37:48 +08:00
|
|
|
} else {
|
|
|
|
// Horizontal movement accross terrain.
|
2019-07-18 21:39:23 +08:00
|
|
|
this.translate_horizontally(this.selected, this.pick, pointer_device_pos);
|
2019-07-18 01:37:48 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.renderer.schedule_render();
|
|
|
|
} else {
|
|
|
|
// User is hovering.
|
2019-07-18 03:59:41 +08:00
|
|
|
const new_pick = this.pick_entity(pointer_device_pos);
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 03:59:41 +08:00
|
|
|
if (this.highlight(new_pick)) {
|
2019-07-18 01:37:48 +08:00
|
|
|
this.renderer.schedule_render();
|
|
|
|
}
|
2019-07-18 03:59:41 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private process_event(e: MouseEvent): void {
|
|
|
|
if (e.type === "mousedown") {
|
|
|
|
this.moved_since_last_mouse_down = false;
|
|
|
|
} else {
|
|
|
|
if (
|
|
|
|
e.offsetX !== this.last_pointer_position.x ||
|
|
|
|
e.offsetY !== this.last_pointer_position.y
|
|
|
|
) {
|
|
|
|
this.moved_since_last_mouse_down = true;
|
|
|
|
}
|
|
|
|
}
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 03:59:41 +08:00
|
|
|
this.last_pointer_position.set(e.offsetX, e.offsetY);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @returns true if a render is required.
|
|
|
|
*/
|
2019-07-18 21:39:23 +08:00
|
|
|
private highlight(selection?: Selection): boolean {
|
2019-07-18 03:59:41 +08:00
|
|
|
let render_required = false;
|
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
if (!this.selected || !selection_equals(selection, this.selected)) {
|
|
|
|
if (!selection_equals(selection, this.highlighted)) {
|
|
|
|
if (this.highlighted) {
|
|
|
|
set_color(this.highlighted, ColorType.Normal);
|
|
|
|
this.highlighted = undefined;
|
|
|
|
}
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
if (selection) {
|
|
|
|
set_color(selection, ColorType.Highlighted);
|
2019-07-18 01:37:48 +08:00
|
|
|
}
|
|
|
|
|
2019-07-18 03:59:41 +08:00
|
|
|
render_required = true;
|
2019-07-18 01:37:48 +08:00
|
|
|
}
|
2019-07-18 03:59:41 +08:00
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
this.highlighted = selection;
|
2019-07-18 01:37:48 +08:00
|
|
|
}
|
2019-07-18 03:59:41 +08:00
|
|
|
|
|
|
|
return render_required;
|
|
|
|
}
|
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
private select(selection: Selection): void {
|
|
|
|
if (selection_equals(selection, this.highlighted)) {
|
2019-07-18 03:59:41 +08:00
|
|
|
this.highlighted = undefined;
|
|
|
|
}
|
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
if (!selection_equals(selection, this.selected)) {
|
2019-07-18 03:59:41 +08:00
|
|
|
if (this.selected) {
|
|
|
|
set_color(this.selected, ColorType.Normal);
|
|
|
|
}
|
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
set_color(selection, ColorType.Selected);
|
2019-07-18 03:59:41 +08:00
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
this.selected = selection;
|
|
|
|
quest_editor_store.set_selected_entity(selection.entity);
|
|
|
|
} else {
|
|
|
|
this.selected = selection;
|
|
|
|
}
|
2019-07-18 03:59:41 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
private deselect(): void {
|
|
|
|
if (this.selected) {
|
|
|
|
set_color(this.selected, ColorType.Normal);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.selected = undefined;
|
2019-07-18 21:39:23 +08:00
|
|
|
quest_editor_store.set_selected_entity(undefined);
|
2019-07-18 03:59:41 +08:00
|
|
|
}
|
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
private translate_vertically(
|
|
|
|
selection: Selection,
|
|
|
|
pick: Pick,
|
|
|
|
pointer_position: Vector2
|
|
|
|
): void {
|
2019-07-18 03:59:41 +08:00
|
|
|
// We intersect with a plane that's oriented toward the camera and that's coplanar with the point where the entity was grabbed.
|
|
|
|
this.raycaster.setFromCamera(pointer_position, this.renderer.camera);
|
|
|
|
const ray = this.raycaster.ray;
|
|
|
|
|
|
|
|
const negative_world_dir = this.renderer.camera.getWorldDirection(new Vector3()).negate();
|
|
|
|
const plane = new Plane().setFromNormalAndCoplanarPoint(
|
|
|
|
new Vector3(negative_world_dir.x, 0, negative_world_dir.z).normalize(),
|
2019-07-18 21:39:23 +08:00
|
|
|
selection.mesh.position.sub(pick.grab_offset)
|
2019-07-18 03:59:41 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
const intersection_point = new Vector3();
|
|
|
|
|
|
|
|
if (ray.intersectPlane(plane, intersection_point)) {
|
|
|
|
const y = intersection_point.y + pick.grab_offset.y;
|
2019-07-18 21:39:23 +08:00
|
|
|
const y_delta = y - selection.entity.position.y;
|
2019-07-18 03:59:41 +08:00
|
|
|
pick.drag_y += y_delta;
|
|
|
|
pick.drag_adjust.y -= y_delta;
|
2019-07-18 21:39:23 +08:00
|
|
|
selection.entity.position = new Vec3(
|
|
|
|
selection.entity.position.x,
|
|
|
|
y,
|
|
|
|
selection.entity.position.z
|
|
|
|
);
|
2019-07-18 03:59:41 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
private translate_horizontally(
|
|
|
|
selection: Selection,
|
|
|
|
pick: Pick,
|
|
|
|
pointer_position: Vector2
|
|
|
|
): void {
|
2019-07-18 03:59:41 +08:00
|
|
|
// Cast ray adjusted for dragging entities.
|
|
|
|
const { intersection, section } = this.pick_terrain(pointer_position, pick);
|
|
|
|
|
|
|
|
if (intersection) {
|
|
|
|
runInAction(() => {
|
2019-07-18 21:39:23 +08:00
|
|
|
selection.entity.position = new Vec3(
|
2019-07-18 03:59:41 +08:00
|
|
|
intersection.point.x,
|
|
|
|
intersection.point.y + pick.drag_y,
|
|
|
|
intersection.point.z
|
|
|
|
);
|
2019-07-18 21:39:23 +08:00
|
|
|
selection.entity.section = section;
|
2019-07-18 03:59:41 +08:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// If the cursor is not over any terrain, we translate the entity accross the horizontal plane in which the entity's origin lies.
|
|
|
|
this.raycaster.setFromCamera(pointer_position, this.renderer.camera);
|
|
|
|
const ray = this.raycaster.ray;
|
|
|
|
// ray.origin.add(data.dragAdjust);
|
|
|
|
const plane = new Plane(
|
|
|
|
new Vector3(0, 1, 0),
|
2019-07-18 21:39:23 +08:00
|
|
|
-selection.entity.position.y + pick.grab_offset.y
|
2019-07-18 03:59:41 +08:00
|
|
|
);
|
|
|
|
const intersection_point = new Vector3();
|
|
|
|
|
|
|
|
if (ray.intersectPlane(plane, intersection_point)) {
|
2019-07-18 21:39:23 +08:00
|
|
|
selection.entity.position = new Vec3(
|
2019-07-18 03:59:41 +08:00
|
|
|
intersection_point.x + pick.grab_offset.x,
|
2019-07-18 21:39:23 +08:00
|
|
|
selection.entity.position.y,
|
2019-07-18 03:59:41 +08:00
|
|
|
intersection_point.z + pick.grab_offset.z
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
private stop_transforming = () => {
|
|
|
|
if (this.moved_since_last_mouse_down && this.selected && this.pick) {
|
|
|
|
const entity = this.selected.entity;
|
|
|
|
const initial_position = this.pick.initial_position;
|
|
|
|
const new_position = entity.position;
|
|
|
|
const entity_type =
|
|
|
|
entity instanceof QuestNpc ? entity.type.name : (entity as QuestObject).type.name;
|
|
|
|
|
|
|
|
quest_editor_store.undo_stack.push_action(
|
|
|
|
`Move ${entity_type}`,
|
|
|
|
() => {
|
|
|
|
entity.position = initial_position;
|
|
|
|
quest_editor_store.set_selected_entity(entity);
|
|
|
|
},
|
|
|
|
() => {
|
|
|
|
entity.position = new_position;
|
|
|
|
quest_editor_store.set_selected_entity(entity);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.pick = undefined;
|
|
|
|
};
|
|
|
|
|
2019-07-18 01:37:48 +08:00
|
|
|
/**
|
2019-07-18 03:59:41 +08:00
|
|
|
* @param pointer_position pointer coordinates in normalized device space
|
2019-07-18 01:37:48 +08:00
|
|
|
*/
|
2019-07-18 21:39:23 +08:00
|
|
|
private pick_entity(pointer_position: Vector2): PickResult | undefined {
|
2019-07-18 01:37:48 +08:00
|
|
|
// Find the nearest object and NPC under the pointer.
|
2019-07-18 03:59:41 +08:00
|
|
|
this.raycaster.setFromCamera(pointer_position, this.renderer.camera);
|
2019-07-18 21:39:23 +08:00
|
|
|
const [intersection] = this.raycaster.intersectObjects(
|
|
|
|
this.renderer.entity_models.children
|
2019-07-18 01:37:48 +08:00
|
|
|
);
|
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
if (!intersection) {
|
2019-07-18 03:59:41 +08:00
|
|
|
return undefined;
|
2019-07-18 01:37:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
const entity = (intersection.object.userData as EntityUserData).entity;
|
|
|
|
// Vector that points from the grabbing point to the model's origin.
|
|
|
|
const grab_offset = intersection.object.position.clone().sub(intersection.point);
|
|
|
|
// Vector that points from the grabbing point to the terrain point directly under the model's origin.
|
|
|
|
const drag_adjust = grab_offset.clone();
|
|
|
|
// Distance to terrain.
|
|
|
|
let drag_y = 0;
|
|
|
|
|
|
|
|
// Find vertical distance to terrain.
|
|
|
|
this.raycaster.set(intersection.object.position, new Vector3(0, -1, 0));
|
|
|
|
const [terrain] = this.raycaster.intersectObjects(
|
|
|
|
this.renderer.collision_geometry.children,
|
|
|
|
true
|
|
|
|
);
|
|
|
|
|
|
|
|
if (terrain) {
|
|
|
|
drag_adjust.sub(new Vector3(0, terrain.distance, 0));
|
|
|
|
drag_y += terrain.distance;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2019-07-18 21:39:23 +08:00
|
|
|
mesh: intersection.object as Mesh,
|
2019-07-18 01:37:48 +08:00
|
|
|
entity,
|
2019-07-18 21:39:23 +08:00
|
|
|
initial_position: entity.position,
|
2019-07-18 01:37:48 +08:00
|
|
|
grab_offset,
|
|
|
|
drag_adjust,
|
|
|
|
drag_y,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param pointer_pos - pointer coordinates in normalized device space
|
|
|
|
*/
|
|
|
|
private pick_terrain(
|
|
|
|
pointer_pos: Vector2,
|
2019-07-18 03:59:41 +08:00
|
|
|
data: Pick
|
2019-07-18 01:37:48 +08:00
|
|
|
): {
|
|
|
|
intersection?: Intersection;
|
|
|
|
section?: Section;
|
|
|
|
} {
|
|
|
|
this.raycaster.setFromCamera(pointer_pos, this.renderer.camera);
|
|
|
|
this.raycaster.ray.origin.add(data.drag_adjust);
|
|
|
|
const terrains = this.raycaster.intersectObjects(
|
|
|
|
this.renderer.collision_geometry.children,
|
|
|
|
true
|
|
|
|
);
|
|
|
|
|
|
|
|
// Don't allow entities to be placed on very steep terrain.
|
|
|
|
// E.g. walls.
|
|
|
|
// TODO: make use of the flags field in the collision data.
|
|
|
|
for (const terrain of terrains) {
|
|
|
|
if (terrain.face!.normal.y > 0.75) {
|
|
|
|
// Find section ID.
|
|
|
|
this.raycaster.set(terrain.point.clone().setY(1000), new Vector3(0, -1, 0));
|
|
|
|
const render_terrains = this.raycaster
|
|
|
|
.intersectObjects(this.renderer.render_geometry.children, true)
|
2019-07-18 03:59:41 +08:00
|
|
|
.filter(rt => (rt.object.userData as AreaUserData).section.id >= 0);
|
2019-07-18 01:37:48 +08:00
|
|
|
|
|
|
|
return {
|
|
|
|
intersection: terrain,
|
2019-07-18 03:59:41 +08:00
|
|
|
section:
|
|
|
|
render_terrains[0] &&
|
|
|
|
(render_terrains[0].object.userData as AreaUserData).section,
|
2019-07-18 01:37:48 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {};
|
|
|
|
}
|
2019-07-18 03:59:41 +08:00
|
|
|
}
|
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
function set_color({ entity, mesh }: Selection, type: ColorType): void {
|
|
|
|
const color = get_color(entity, type);
|
2019-07-18 01:37:48 +08:00
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
if (mesh) {
|
|
|
|
for (const material of mesh.material as MeshLambertMaterial[]) {
|
|
|
|
if (type === ColorType.Normal && material.map) {
|
|
|
|
material.color.set(0xffffff);
|
|
|
|
} else {
|
|
|
|
material.color.set(color);
|
|
|
|
}
|
2019-07-18 01:37:48 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-07-18 03:59:41 +08:00
|
|
|
|
2019-07-18 21:39:23 +08:00
|
|
|
function selection_equals(a?: Selection, b?: Selection): boolean {
|
2019-07-18 03:59:41 +08:00
|
|
|
return a && b ? a.entity === b.entity : a === b;
|
|
|
|
}
|
|
|
|
|
|
|
|
function get_color(entity: QuestEntity, type: ColorType): number {
|
|
|
|
const is_npc = entity instanceof QuestNpc;
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
default:
|
|
|
|
case ColorType.Normal:
|
|
|
|
return is_npc ? NPC_COLOR : OBJECT_COLOR;
|
|
|
|
case ColorType.Highlighted:
|
|
|
|
return is_npc ? NPC_HIGHLIGHTED_COLOR : OBJECT_HIGHLIGHTED_COLOR;
|
|
|
|
case ColorType.Selected:
|
|
|
|
return is_npc ? NPC_SELECTED_COLOR : OBJECT_SELECTED_COLOR;
|
|
|
|
}
|
|
|
|
}
|