Projekt přejmenován. Neko nastaven na výchozí pozici

This commit is contained in:
Perry 2026-03-22 18:31:05 +01:00
parent 1a27dd6fab
commit ceac37920b
104 changed files with 873 additions and 208 deletions

View file

@ -0,0 +1,36 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-mgcb": {
"version": "3.8.4",
"commands": [
"mgcb"
]
},
"dotnet-mgcb-editor": {
"version": "3.8.4",
"commands": [
"mgcb-editor"
]
},
"dotnet-mgcb-editor-linux": {
"version": "3.8.4",
"commands": [
"mgcb-editor-linux"
]
},
"dotnet-mgcb-editor-windows": {
"version": "3.8.4",
"commands": [
"mgcb-editor-windows"
]
},
"dotnet-mgcb-editor-mac": {
"version": "3.8.4",
"commands": [
"mgcb-editor-mac"
]
}
}
}

14
ONDClient/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,14 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "C#: FNAF_Clone Debug",
"type": "dotnet",
"request": "launch",
"projectPath": "${workspaceFolder}/FNAF_Clone.csproj"
}
],
}

18
ONDClient/CameraSystem.cs Normal file
View file

@ -0,0 +1,18 @@
namespace ONDClient;
public class CameraSystem {
public bool Enabled { get; private set; }
public int CurrentCamera { get; private set; }
public void FlipUp() {
Enabled = true;
}
public void FlipDown() {
Enabled = false;
}
public void SetCamera(int camera) {
}
}

129
ONDClient/Client.cs Normal file
View file

@ -0,0 +1,129 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using ONDClient.GUI;
using GlobalClassLib;
using LiteNetLib;
using LiteNetLib.Utils;
using ONDClient.Map;
using PacketLib;
namespace ONDClient;
public class Client {
public enum ConnectionState {
IDLE,
CONNECTING,
CONNECTED,
ACCEPTED,
GAME_STARTING,
GAME_IN_PROGRESS,
SERVER_NOT_FOUND,
SERVER_FULL,
ERROR
}
public static ConnectionState State { get; private set; } = ConnectionState.IDLE;
private static EventBasedNetListener listener = new();
private static NetManager client;
private static NetPeer server;
private static NetDataWriter writer;
private static NetPacketProcessor processor;
public static ClientPlayer Player { get; } = new();
public static ClientPlayer Opponent { get; } = new();
public static ClientPlayer GetPlayer(int pid) => Player.state.pid == pid ? Player : Opponent;
public static void Init() {
writer = new NetDataWriter();
processor = new NetPacketProcessor();
NestedTypeManager.AutoRegister(processor);
processor.SubscribeReusable<JoinAcceptPacket>(OnJoinAccept);
processor.SubscribeReusable<UpdatePlayerPacket>(OnPlayerUpdate);
processor.SubscribeReusable<MapInitPacket>(OnMapInit);
processor.SubscribeReusable<OpponentInitPacket>(packet => { // TODO: move this to a method
Opponent.state = packet.state;
Opponent.username = packet.username;
State = ConnectionState.GAME_STARTING;
});
client = new NetManager(listener){
AutoRecycle = true
};
listener.NetworkReceiveEvent += (peer, reader, channel, method) => {
OnNetworkReceive(peer, reader, method);
};
listener.PeerConnectedEvent += peer => {
Console.WriteLine("Connected to Server");
server = peer;
State = ConnectionState.CONNECTED;
SendPacket(new JoinPacket {username = Player.username == "" ? "Anonymous" : Player.username}, DeliveryMethod.ReliableOrdered);
};
}
public static void Connect(string endPoint, int port) {
State = ConnectionState.CONNECTING;
client.Start();
Console.WriteLine($"Connecting to server @ {endPoint}:{port}");
new Thread(() => client.Connect(endPoint, port, "")).Start(); // TODO: figure out how keys work
}
public static void SendPacket<T>(T packet, DeliveryMethod deliveryMethod) where T : class, new() {
if (server != null) {
writer.Reset();
processor.Write(writer, packet);
server.Send(writer, deliveryMethod);
}
}
public static void SendCommands(PlayerCommand[] pCommands) {
SendPacket(new PlayerCommandPacket{commands = pCommands}, DeliveryMethod.ReliableOrdered);
}
private static void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) {
processor.ReadAllPackets(reader, peer);
}
private static void OnJoinAccept(JoinAcceptPacket packet) {
Console.WriteLine($"Accepted by server, pid: {packet.state.pid}");
State = ConnectionState.ACCEPTED;
Player.state = packet.state;
}
#nullable enable
private static void OnMapInit(MapInitPacket packet) {
ClientMapManager.InitMap(packet.Connectors, packet.YourTiles, packet.OpponentTiles, packet.LitTiles, packet.UpsideDown);
}
public static void Update() {
client.PollEvents();
}
public static void OnPlayerUpdate(UpdatePlayerPacket packet) { // TODO: move this to a separate class
EventProcessor.Evaluate(packet.events);
//Player.state = Player.state.pid == 0 ? packet.stateP1 : packet.stateP2;
}
public static void Disconnect() {
if (client == null || client.ConnectedPeersCount == 0) return;
client.DisconnectAll();
client.Stop();
State = ConnectionState.IDLE;
}
public static void StartServer() {
Process.Start("ONDServer");
// new Thread(() => Server.Start(9012)).Start();
}
}

24
ONDClient/ClientEnemy.cs Normal file
View file

@ -0,0 +1,24 @@
using GlobalClassLib;
using MonoGameLibrary.Graphics;
using ONDClient.GUI;
using ONDClient.Map;
namespace ONDClient;
public class ClientEnemy : GlobalEnemy<MapTileProjection, TileConnectorProjection> {
public ClientEnemy(int typeId, string name, int id, EnemyUIElement sprite, int difficulty, MapTileProjection location, JumpscareUIElement jumpscareSprite) : base(id) {
Name = name;
TypeId = typeId;
Sprite = sprite;
Location = location;
JumpscareSprite = jumpscareSprite;
}
// public TextureRegion Sprite { get; set; }
public UIElement Sprite { get; set; }
public override string Name{ get; }
public override int TypeId{ get; }
public JumpscareUIElement JumpscareSprite { get; set; }
}

View file

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using GlobalClassLib;
using Microsoft.Xna.Framework;
using MonoGameLibrary.Graphics;
using ONDClient.GUI;
using ONDClient.Map;
namespace ONDClient;
public static class ClientEnemyManager {
private static Dictionary<int, ClientEnemy> enemies = new();
private static Point cameraCorner = new(64, 64);
private static Action defaultAfterJumpscare = UIManager.ShowDeathScreen;
public static void AddEnemy(ClientEnemy enemy) {
enemies.Add(enemy.Id, enemy);
UIManager.AddEnemySprite(enemy.Id, enemy.Sprite, enemy.JumpscareSprite);
}
public static void AddEnemyByTemplate(EnemyType type, int id, int difficulty, MapTileProjection location) {
switch (type){
case EnemyType.LURK:
AddEnemy(new ClientEnemy(
(int)type,
"Lurk",
id,
new EnemyUIElement(UIManager.EnemyAtlas["lurk-lit"], UIManager.EnemyAtlas["lurk-unlit"], cameraCorner),
difficulty,
location,
new JumpscareUIElement(UIManager.EnemyAtlas["lurk-lit"], new(0, 0), 3, 2, 2, 0.1f, afterStop:defaultAfterJumpscare)
));
break;
case EnemyType.NEKO:
AddEnemy(new ClientEnemy(
(int)type,
"Neko",
id,
new EnemyUIElement(UIManager.EnemyAtlas["neko-lit"], UIManager.EnemyAtlas["neko-unlit"], cameraCorner, 1),
difficulty,
location,
new JumpscareUIElement(UIManager.EnemyAtlas["neko-lit"], new(0, -30), 3, 2, 2, 0.1f, afterStop:defaultAfterJumpscare)
));
break;
case EnemyType.SPOT:
EnemyUIElement element =
new EnemyUIElement([UIManager.EnemyAtlas["spot-awake-lit"], UIManager.EnemyAtlas["spot-asleep-lit"]],[UIManager.EnemyAtlas["spot-awake-unlit"], UIManager.EnemyAtlas["spot-asleep-unlit"]], cameraCorner, 2);
element.SetTexture(true, 1);
AddEnemy(new ClientEnemy(
(int)type,
"Spot",
id,
element,
difficulty,
location,
new JumpscareUIElement(UIManager.EnemyAtlas["spot-awake-lit"], new(0, 0), 3, 2, 2, 0.1f, afterStop:defaultAfterJumpscare)
));
break;
case EnemyType.DASH:
AddEnemy(new ClientEnemy(
(int)type,
"Dash",
id,
new EnemyUIElement(UIManager.EnemyAtlas["dash-lit"], UIManager.EnemyAtlas["dash-unlit"], cameraCorner, 3),
difficulty,
location,
new JumpscareUIElement(UIManager.EnemyAtlas["dash-lit"], new(0, 0), 3, 2, 2, 0.1f, afterStop:defaultAfterJumpscare)
));
break;
case EnemyType.MARE:
AddEnemy(new ClientEnemy(
(int)type,
"Mare",
id,
new EnemyUIElement(UIManager.EnemyAtlas["mare-lit"], UIManager.EnemyAtlas["mare-unlit"], cameraCorner),
difficulty,
location,
new JumpscareUIElement(UIManager.EnemyAtlas["mare-lit"], new(0, 0), 3, 2, 2, 0.1f, afterStop:defaultAfterJumpscare)
));
break;
}
}
public static void Move(int id, MapTileProjection tile) {
enemies[id].Location = tile;
}
public static ClientEnemy Get(int id) => enemies[id];
public static ClientEnemy[] GetByLocation(MapTileProjection tile) {
List<ClientEnemy> output = new();
foreach (var e in enemies.Values){
if (e.Location == tile){
output.Add(e);
}
}
return output.ToArray();
}
public static void ClearEnemies() {
enemies.Clear();
}
}

View file

@ -0,0 +1,8 @@
using PacketLib;
namespace ONDClient;
public class ClientPlayer {
public PlayerState state;
public string username;
}

View file

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GlobalClassLib;
using Microsoft.Xna.Framework.Input;
using MonoGameLibrary.Input;
using ONDClient.GUI;
using ONDClient.Map;
using PacketLib;
namespace ONDClient;
public class CommandManager {
private static (string label, Keys key, Action action)[] keybinds = [
("Toggle camera", Keys.Space, SendToggleCamera),
("Toggle left door", Keys.A, ToggleDoorLeft),
("Toggle centre door", Keys.W, ToggleDoorCentre),
("Toggle right door", Keys.D, ToggleDoorRight),
("Toggle back door", Keys.S, ToggleDoorBack),
("Toggle light", Keys.F, ToggleLight)
];
private static InputListenerHook allControlsHook{ get; } = new(true);
public static void InitInputListeners() {
Array.ForEach(keybinds, tuple => InputManager.AddListener(tuple.label, tuple.key, () => tuple.action(), InputTiming.PRESS, allControlsHook));
}
public static void AllowGameControls(bool state) {
allControlsHook.Enabled = state;
}
private static void SendToggleCamera() {
Client.Player.state.monitorUp = !Client.Player.state.monitorUp;
UIManager.ChangeMonitorState(Client.Player.state.monitorUp);
Client.SendCommands([PlayerCommand.SET_MONITOR(Client.Player.state.monitorUp)]);
SoundManager.PlayMonitorFlip();
}
private static void ToggleDoorLeft() => SendToggleDoor(Direction.EAST);
private static void ToggleDoorCentre() => SendToggleDoor(Direction.NORTH);
private static void ToggleDoorRight() => SendToggleDoor(Direction.WEST);
private static void ToggleDoorBack() => SendToggleDoor(Direction.SOUTH);
private static Dictionary<Direction, TileConnectorProjection> currentDoorBinds = new();
private static void ToggleLight() => SendToggleLight(Client.Player.state.camera, !ClientMapManager.Get(Client.Player.state.camera).Lit);
private static void SendToggleDoor(Direction direction) {
if (Screen.CurrentScreen.Label == UIManager.ScreenTypes.CAMERAS){
SendToggleRemoteDoor(direction);
return;
}
if (direction == Direction.SOUTH) return;
Client.Player.state.doorStates[(int)direction] = !Client.Player.state.doorStates[(int)direction];
UIManager.ChangeDoorState(direction, Client.Player.state.doorStates[(int)direction]);
SoundManager.PlayDoor(Client.Player.state.doorStates[(int)direction]);
Client.SendCommands([PlayerCommand.SET_DOOR_OFFICE(direction, Client.Player.state.doorStates[(int)direction])]);
}
private static void SendToggleRemoteDoor(Direction direction) {
if (!currentDoorBinds.TryGetValue(direction, out var connector)) return;
if (connector.Owner != Client.Player) return;
connector.Blocked = !connector.Blocked;
Client.SendCommands([PlayerCommand.SET_DOOR_REMOTE(connector.Id, connector.Blocked)]);
SoundManager.PlayDoorRemote(connector.Blocked);
UIManager.ChangeRemoteDoorState(connector.Id, connector.Blocked);
// Console.WriteLine("Changed door state to: " + (connector.Blocked ? "blocked" : "open") + " for door " + connector);
// add UIManager toggle door
}
public static void SendChangeCamera(int id) {
if (id == Client.Player.state.officeTileId || id == Client.Opponent.state.officeTileId) return;
Client.Player.state.camera = id;
UIManager.ChangeCamera(id);
SoundManager.PlayCameraSwitch();
Client.SendCommands([PlayerCommand.SWITCH_CAM(id)]);
MapTileProjection tile = ClientMapManager.Get(id);
// add UIManager switch camera
currentDoorBinds.Clear();
foreach (var c in tile.GetAllConnectors().Where(c => c.Type == ConnectorType.DOOR_REMOTE)){
Direction dir = c.GetDirection(tile);
if (dir == Direction.NONE) return;
currentDoorBinds.Add(dir, c);
}
}
private static void SendToggleLight(int id, bool state) {
if(!Client.Player.state.monitorUp || ClientMapManager.Get(id).Owner != Client.Player) return;
ClientMapManager.Get(id).Lit = state;
UIManager.UpdateCameras([id]);
SoundManager.PlayLight(state);
Client.SendCommands([PlayerCommand.SET_LIGHT(id, state)]);
}
}

View file

@ -0,0 +1,223 @@
#----------------------------- Global Properties ----------------------------#
/outputDir:bin/$(Platform)
/intermediateDir:obj/$(Platform)
/platform:DesktopGL
/config:
/profile:Reach
/compress:False
#-------------------------------- References --------------------------------#
#---------------------------------- Content ---------------------------------#
#begin images/enemies-definition.xml
/copy:images/enemies-definition.xml
#begin images/monitor-definition.xml
/copy:images/monitor-definition.xml
#begin images/office-definition.xml
/copy:images/office-definition.xml
#begin images/rooms-definition.xml
/copy:images/rooms-definition.xml
#begin images/SpriteSheet_enemies.png
/importer:TextureImporter
/processor:TextureProcessor
/processorParam:ColorKeyColor=255,0,255,255
/processorParam:ColorKeyEnabled=True
/processorParam:GenerateMipmaps=False
/processorParam:PremultiplyAlpha=True
/processorParam:ResizeToPowerOfTwo=False
/processorParam:MakeSquare=False
/processorParam:TextureFormat=Color
/build:images/SpriteSheet_enemies.png
#begin images/SpriteSheet_monitor.png
/importer:TextureImporter
/processor:TextureProcessor
/processorParam:ColorKeyColor=255,0,255,255
/processorParam:ColorKeyEnabled=True
/processorParam:GenerateMipmaps=False
/processorParam:PremultiplyAlpha=True
/processorParam:ResizeToPowerOfTwo=False
/processorParam:MakeSquare=False
/processorParam:TextureFormat=Color
/build:images/SpriteSheet_monitor.png
#begin images/SpriteSheet_office.png
/importer:TextureImporter
/processor:TextureProcessor
/processorParam:ColorKeyColor=255,0,255,255
/processorParam:ColorKeyEnabled=True
/processorParam:GenerateMipmaps=False
/processorParam:PremultiplyAlpha=True
/processorParam:ResizeToPowerOfTwo=False
/processorParam:MakeSquare=False
/processorParam:TextureFormat=Color
/build:images/SpriteSheet_office.png
#begin images/SpriteSheet_rooms.png
/importer:TextureImporter
/processor:TextureProcessor
/processorParam:ColorKeyColor=255,0,255,255
/processorParam:ColorKeyEnabled=True
/processorParam:GenerateMipmaps=False
/processorParam:PremultiplyAlpha=True
/processorParam:ResizeToPowerOfTwo=False
/processorParam:MakeSquare=False
/processorParam:TextureFormat=Color
/build:images/SpriteSheet_rooms.png;images/SpriteSheet_map.png
#begin images/SpriteSheet_testBlocks.png
/importer:TextureImporter
/processor:TextureProcessor
/processorParam:ColorKeyColor=255,0,255,255
/processorParam:ColorKeyEnabled=True
/processorParam:GenerateMipmaps=False
/processorParam:PremultiplyAlpha=True
/processorParam:ResizeToPowerOfTwo=False
/processorParam:MakeSquare=False
/processorParam:TextureFormat=Color
/build:images/SpriteSheet_testBlocks.png
#begin images/testBlocks-definition.xml
/copy:images/testBlocks-definition.xml
#begin ponderosa.spritefont
/importer:FontDescriptionImporter
/processor:FontDescriptionProcessor
/processorParam:PremultiplyAlpha=True
/processorParam:TextureFormat=Compressed
/build:ponderosa.spritefont
#begin sounds/ambience.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/ambience.wav
#begin sounds/bell.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/bell.wav
#begin sounds/camera-switch.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/camera-switch.wav
#begin sounds/dash-move.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/dash-move.wav
#begin sounds/dash-sounds-raw.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/dash-sounds-raw.wav
#begin sounds/door-close-remote.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/door-close-remote.wav
#begin sounds/door-close.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/door-close.wav
#begin sounds/door-open-remote.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/door-open-remote.wav
#begin sounds/door-open.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/door-open.wav
#begin sounds/jumpscare.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/jumpscare.wav
#begin sounds/light-off.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/light-off.wav
#begin sounds/light-on.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/light-on.wav
#begin sounds/mare-move.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/mare-move.wav
#begin sounds/monitor-flip.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/monitor-flip.wav
#begin sounds/neko-angry.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/neko-angry.wav
#begin sounds/neko-move1.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/neko-move1.wav
#begin sounds/neko-purr.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/neko-purr.wav
#begin sounds/powerout.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/powerout.wav
#begin sounds/spot-activate.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/spot-activate.wav
#begin sounds/spot-move.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/spot-move.wav
#begin sounds/vent-walk.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
/build:sounds/vent-walk.wav

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<TextureAtlas>
<Texture>images/SpriteSheet_enemies</Texture>
<Regions>
<Region name="lurk-lit" x="0" y="0" width="240" height="240"/>
<Region name="lurk-unlit" x="240" y="0" width="240" height="240"/>
<Region name="neko-lit" x="0" y="240" width="240" height="240"/>
<Region name="neko-unlit" x="240" y="240" width="240" height="240"/>
<Region name="spot-awake-lit" x="0" y="480" width="240" height="240"/>
<Region name="spot-awake-unlit" x="240" y="480" width="240" height="240"/>
<Region name="spot-asleep-lit" x="0" y="720" width="240" height="240"/>
<Region name="spot-asleep-unlit" x="240" y="720" width="240" height="240"/>
<Region name="dash-lit" x="0" y="960" width="240" height="240"/>\
<Region name="dash-unlit" x="240" y="960" width="240" height="240"/>
<Region name="mare-lit" x="0" y="1200" width="240" height="240"/>
<Region name="mare-unlit" x="240" y="1200" width="240" height="240"/>
</Regions>
</TextureAtlas>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<TextureAtlas>
<Texture>images/SpriteSheet_monitor</Texture>
<Regions>
<Region name="screen" x="0" y="0" width="640" height="360"/>
<Region name="view-frame" x="0" y="360" width="244" height="251"/>
<Region name="map-frame" x="244" y="360" width="164" height="171"/>
<Region name="map" x="408" y="360" width="164" height="171"/>
<Region name="light-button" x="244" y="531" width="36" height="42"/>
<Region name="door-remote-open" x="280" y="531" width="32" height="33"/>
<Region name="door-remote-closed" x="312" y="531" width="32" height="33"/>
<Region name="door-office-p1-left-open" x="344" y="531" width="32" height="33"/>
<Region name="door-office-p1-left-closed" x="376" y="531" width="32" height="33"/>
<Region name="door-office-p1-centre-open" x="408" y="531" width="32" height="33"/>
<Region name="door-office-p1-centre-closed" x="440" y="531" width="32" height="33"/>
<Region name="door-office-p1-right-open" x="472" y="531" width="32" height="33"/>
<Region name="door-office-p1-right-closed" x="504" y="531" width="32" height="33"/>
<Region name="door-office-p2-right-open" x="344" y="564" width="32" height="33"/>
<Region name="door-office-p2-right-closed" x="376" y="564" width="32" height="33"/>
<Region name="door-office-p2-centre-open" x="408" y="564" width="32" height="33"/>
<Region name="door-office-p2-centre-closed" x="440" y="564" width="32" height="33"/>
<Region name="door-office-p2-left-open" x="472" y="564" width="32" height="33"/>
<Region name="door-office-p2-left-closed" x="504" y="564" width="32" height="33"/>
<Region name="eye-opponent-open" x="572" y="360" width="32" height="14"/>
<Region name="eye-opponent-open" x="604" y="360" width="32" height="14"/>
<Region name="eye-player-open" x="572" y="374" width="32" height="14"/>
<Region name="eye-small-opponent-open" x="572" y="402" width="32" height="9"/>
<Region name="eye-small-opponent-closed" x="604" y="402" width="32" height="9"/>
<Region name="eye-small-player" x="572" y="411" width="32" height="9"/>
<Region name="map-light-indicator" x="280" y="564" width="32" height="32"/>
</Regions>
</TextureAtlas>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<TextureAtlas>
<Texture>images/SpriteSheet_office</Texture>
<Regions>
<Region name="left-closed" x="0" y="0" width="200" height="360"/>
<Region name="centre-closed" x="200" y="0" width="240" height="360"/>
<Region name="right-closed" x="440" y="0" width="200" height="360"/>
<Region name="left-open" x="0" y="360" width="200" height="360"/>
<Region name="centre-open" x="200" y="360" width="240" height="360"/>
<Region name="right-open" x="440" y="360" width="200" height="360"/>
</Regions>
</TextureAtlas>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<TextureAtlas>
<Texture>images/SpriteSheet_map</Texture>
<Regions>
<Region name="room4" x="0" y="0" width="240" height="240"/>
<Region name="room3" x="0" y="240" width="240" height="240"/>
<Region name="room2" x="0" y="480" width="240" height="240"/>
<Region name="room1" x="0" y="720" width="240" height="240"/>
<Region name="room0" x="0" y="960" width="240" height="240"/>
<Region name="room9" x="240" y="0" width="240" height="240"/>
<Region name="room8" x="240" y="240" width="240" height="240"/>
<Region name="room7" x="240" y="480" width="240" height="240"/>
<Region name="room6" x="240" y="720" width="240" height="240"/>
<Region name="room5" x="240" y="960" width="240" height="240"/>
<Region name="room14" x="480" y="0" width="240" height="240"/>
<Region name="room13" x="480" y="240" width="240" height="240"/>
<Region name="room12" x="480" y="480" width="240" height="240"/>
<Region name="room11" x="480" y="720" width="240" height="240"/>
<Region name="room10" x="480" y="960" width="240" height="240"/>
<Region name="room19" x="720" y="0" width="240" height="240"/>
<Region name="room18" x="720" y="240" width="240" height="240"/>
<Region name="room17" x="720" y="480" width="240" height="240"/>
<Region name="room16" x="720" y="720" width="240" height="240"/>
<Region name="room15" x="720" y="960" width="240" height="240"/>
<Region name="room24" x="960" y="0" width="240" height="240"/>
<Region name="room23" x="960" y="240" width="240" height="240"/>
<Region name="room22" x="960" y="480" width="240" height="240"/>
<Region name="room21" x="960" y="720" width="240" height="240"/>
<Region name="room20" x="960" y="960" width="240" height="240"/>
</Regions>
</TextureAtlas>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<TextureAtlas count="4">
<Texture>images/SpriteSheet_testBlocks</Texture>
<Regions>
<Region id = "0" name="R128" x="0" y="0" width="128" height="128"/>
<Region id = "1" name="B128" x="128" y="0" width="128" height="128"/>
<Region id = "2" name="R64" x="0" y="128" width="64" height="64"/>
<Region id = "3" name="B64" x="64" y="128" width="64" height="64"/>
</Regions>
</TextureAtlas>

Binary file not shown.

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file contains an xml description of a font, and will be read by the XNA
Framework Content Pipeline. Follow the comments to customize the appearance
of the font in your game, and to change the characters which are available to draw
with.
-->
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<!--
Modify this string to change the font that will be imported.
-->
<FontName>Ponderosa</FontName>
<!--
Size is a float value, measured in points. Modify this value to change
the size of the font.
-->
<Size>40</Size>
<!--
Spacing is a float value, measured in pixels. Modify this value to change
the amount of spacing in between characters.
-->
<Spacing>0</Spacing>
<!--
UseKerning controls the layout of the font. If this value is true, kerning information
will be used when placing characters.
-->
<UseKerning>true</UseKerning>
<!--
Style controls the style of the font. Valid entries are "Regular", "Bold", "Italic",
and "Bold, Italic", and are case sensitive.
-->
<Style>Regular</Style>
<!--
If you uncomment this line, the default character will be substituted if you draw
or measure text that contains characters which were not included in the font.
-->
<!-- <DefaultCharacter>*</DefaultCharacter> -->
<!--
CharacterRegions control what letters are available in the font. Every
character from Start to End will be built and made available for drawing. The
default range is from 32, (ASCII space), to 126, ('~'), covering the basic Latin
character set. The characters are ordered according to the Unicode standard.
See the documentation for more information.
-->
<CharacterRegions>
<CharacterRegion>
<Start>&#32;</Start>
<End>&#126;</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

187
ONDClient/EventProcessor.cs Normal file
View file

@ -0,0 +1,187 @@
using System;
using System.Linq;
using GlobalClassLib;
using MonoGameLibrary.Input;
using ONDClient.GUI;
using ONDClient.Map;
using PacketLib;
namespace ONDClient;
public class EventProcessor {
public static void Evaluate(GameEvent[] events) {
foreach (var e in events){
switch (e.ID){
case 0: // join
Console.WriteLine("E: Player joined");
break;
case 1: // leave
Console.WriteLine("E: Player left");
break;
case 2: // switch cam
if (Client.Player.state.pid != e.Args[0]){
UIManager.ChangeCameraOpponent(e.Args[1]);
break;
}
if (Client.Player.state.camera != e.Args[1]) Console.WriteLine("!!! DESYNC: CAMERA STATE");
Console.WriteLine($"E: player {e.Args[0]} switched to camera {e.Args[1]}");
break;
case 3: // toggle cam
Console.WriteLine($"E: Player {e.Args[0]} toggled monitor {(e.Args[1] == 0 ? "off" : "on")}");
if (e.Args[0] != Client.Player.state.pid){
UIManager.ChangeMonitorStateOpponent(e.Args[1] == 1);
break;
}
if (Client.Player.state.monitorUp != (e.Args[1] == 1)) Console.WriteLine("!!! DESYNC: MONITOR STATE");
// UIManager.ChangeMonitorState(e.Args[1] == 1);
break;
case 4: // toggle door
Console.WriteLine($"E: Player {e.Args[0]} {(e.Args[2] == 1 ? "closed" : "opened")} door {e.Args[1]}");
if (e.Args[0] == Client.Player.state.pid){
if (Client.Player.state.doorStates[e.Args[1]] != (e.Args[2] == 1)) Console.WriteLine("!!! DESYNC: DOOR STATE");
break;
}
Client.Opponent.state.doorStates[e.Args[1]] = e.Args[2] == 1;
UIManager.ChangeDoorStateOpponent((Direction)e.Args[1], e.Args[2] == 1);
break;
case 5: // toggle remote door
Console.WriteLine($"E: Player {e.Args[0]} {(e.Args[3] == 1 ? "closed" : "opened")} door {e.Args[1]}-{e.Args[2]}");
if (e.Args[0] == Client.Player.state.pid){
if (ClientMapManager.GetConnector((e.Args[1], e.Args[2])).Blocked != (e.Args[3] == 1)) Console.WriteLine("!!! DESYNC: REMOTE DOOR STATE");
break;
}
ClientMapManager.GetConnector((e.Args[1], e.Args[2])).Blocked = e.Args[3] == 1;
UIManager.ChangeRemoteDoorState((e.Args[1], e.Args[2]), e.Args[3] == 1);
break;
case 6: // spawn
Console.WriteLine($"E: Spawned enemy {e.Args[0]} at {e.Args[3]}");
ClientEnemyManager.AddEnemyByTemplate((EnemyType)e.Args[0], e.Args[1], e.Args[2], ClientMapManager.Get(e.Args[3]));
UIManager.UpdateCameras([e.Args[3]]);
break;
case 7: // movement
Console.WriteLine($"E: Enemy {e.Args[0]} moved to {e.Args[1]}");
int oldPos = ClientEnemyManager.Get(e.Args[0]).Location!.Id;
ClientEnemyManager.Move(e.Args[0], ClientMapManager.Get(e.Args[1]));
UIManager.UpdateCameras([oldPos, e.Args[1]]);
SoundManager.PlayEnemyMove(ClientEnemyManager.Get(e.Args[0]));
break;
case 8: // attack
Console.WriteLine($"E: Enemy {e.Args[0]} attacked player {e.Args[1]}"); // TODO: add an arg to indicate lethality
if (e.Args[1] == Client.Player.state.pid) {
UIManager.Jumpscare(ClientEnemyManager.Get(e.Args[0]));
SoundManager.PlayJumpscare();
}
break;
case 9: // reset
Console.WriteLine($"E: Enemy {e.Args[0]} reset to {e.Args[1]}");
int preResetPos = ClientEnemyManager.Get(e.Args[0]).Location!.Id;
ClientEnemyManager.Move(e.Args[0], ClientMapManager.Get(e.Args[1]));
UIManager.UpdateCameras([preResetPos, e.Args[1]]);
SoundManager.PlayEnemyReset(ClientEnemyManager.Get(e.Args[0]));
break;
case 10:
Console.WriteLine($"E: Spot:{e.Args[0]} turned {(e.Args[1] == 1 ? "on" : " off")}");
ClientEnemyManager.Get(e.Args[0]).Sprite.SetTexture(e.Args[1] == 1 ? 0 : 1);
SoundManager.PlaySpotActivate();
break;
case 11:
Console.WriteLine($"E: Player {e.Args[0]} won");
if(Client.Player.state.pid == e.Args[0]) UIManager.ShowVictoryScreen();
Client.Disconnect();
ClientEnemyManager.ClearEnemies();
break;
case 12: // game start
Console.WriteLine("E: Game started");
UIManager.DisplayGameUI();
UIManager.StartTimer();
SoundManager.StartAmbience();
break;
case 13:
// Console.WriteLine($"E: power tick {e.Args[0]}: {e.Args[1]}");
if (e.Args[0] == Client.Player.state.pid){
Client.Player.state.power = e.Args[1];
}
else{
Client.Opponent.state.power = e.Args[1];
}
break;
case 14: // powerout
Console.WriteLine($"E: Player {e.Args[0]} powered out");
ClientMapManager.GetAllConnectors().Where(c =>
(c.Type == ConnectorType.DOOR_REMOTE || c.Type == ConnectorType.DOOR_OFFICE) &&
c.Owner == Client.GetPlayer(e.Args[0])).ToList().ForEach(c =>
{
c.Blocked = false;
if(c.Type == ConnectorType.DOOR_REMOTE)
UIManager.ChangeRemoteDoorState(c.Id, false);
});
foreach (var tile in ClientMapManager.GetAllTiles()){
tile.Lit = false;
}
if (e.Args[0] == Client.Player.state.pid){
UIManager.ChangeDoorState(Direction.EAST, false);
UIManager.ChangeDoorState(Direction.NORTH, false);
UIManager.ChangeDoorState(Direction.WEST, false);
CommandManager.AllowGameControls(false);
UIManager.ChangeMonitorState(false);
SoundManager.PlayPowerOut();
}
else{
UIManager.ChangeDoorStateOpponent(Direction.EAST, false);
UIManager.ChangeDoorStateOpponent(Direction.NORTH, false);
UIManager.ChangeDoorStateOpponent(Direction.WEST, false);
UIManager.ChangeMonitorStateOpponent(false);
}
break;
case 15: // light
bool lightState = e.Args[2] == 1;
Console.WriteLine($"E: Player {e.Args[0]} {(lightState ? "lit": "unlit")} tile {e.Args[1]}");
if (e.Args[0] == Client.Player.state.pid){
if (ClientMapManager.Get(e.Args[1]).Lit != lightState) Console.WriteLine("!!! DESYNC: LIGHT STATE");
break;
}
ClientMapManager.Get(e.Args[1]).Lit = lightState;
UIManager.UpdateCameras([e.Args[1]]);
break;
case 16: // neko anger
SoundManager.PlayNekoAnger();
break;
case 17:
SoundManager.PlayVentWalk();
break;
case 18:
SoundManager.PlayBell();
break;
}
}
}
}

View file

@ -0,0 +1,33 @@
using System.Linq;
using Microsoft.Xna.Framework;
using MonoGameLibrary.Graphics;
namespace ONDClient.GUI;
public class EnemyUIElement : UIElement {
private int unlitTexturesId;
private bool currentlyLit = true;
public EnemyUIElement(TextureRegion litTexture, TextureRegion unlitTexture, Point position, int drawPriority = 0) : base([litTexture, unlitTexture], position, drawPriority) {
unlitTexturesId = 1;
}
public EnemyUIElement(TextureRegion[] litTextures, TextureRegion[] unlitTextures, Point position, int drawPriority = 0) : base(litTextures.Concat(unlitTextures).ToArray(), position, drawPriority) {
unlitTexturesId = litTextures.Length;
}
public void SetTexture(bool lit, int id) {
currentlyLit = lit;
base.SetTexture(lit ? id : id + unlitTexturesId);
}
public override void SetTexture(int id) {
base.SetTexture(currentlyLit ? id : id + unlitTexturesId);
}
public void SetTexture(bool lit) {
if(lit == currentlyLit) return;
currentlyLit = lit;
SetTexture(lit ? currentTextureId - unlitTexturesId : currentTextureId);
}
}

View file

@ -0,0 +1,59 @@
using System;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using MonoGameLibrary.Graphics;
namespace ONDClient.GUI;
public class JumpscareUIElement : UIElement {
private int twitchHorizontal;
private int twitchVertical;
private Point positionDefault;
private Random random;
private float defaultScaleMultiplier;
private float twitchScale;
private bool playing = false;
private Stopwatch stopwatch = new();
private int duration;
public JumpscareUIElement(TextureRegion texture, Point positionDefault, int twitchHorizontal, int twitchVertical, float defaultScaleMultiplier, float twitchScale, int durationMs = 2000, Action afterStop = null) : base(texture, positionDefault) {
this.twitchHorizontal = twitchHorizontal;
this.twitchVertical = twitchVertical;
this.positionDefault = positionDefault;
random = new Random();
this.defaultScaleMultiplier = defaultScaleMultiplier;
ScaleMultiplier = defaultScaleMultiplier;
this.twitchScale = twitchScale;
duration = durationMs;
Active = false;
Visible = false;
AfterStop = afterStop;
}
// public JumpscareUIElement(UIElement element) : base(element.GetTextures(), element.Bounds.Item1) {}
public void Play() {
playing = true;
Active = true;
Visible = true;
stopwatch.Start();
}
public override void Update() {
if (!playing) return;
SetPosition(new(positionDefault.X + random.Next(-twitchHorizontal, twitchHorizontal), positionDefault.Y + random.Next(-twitchVertical, twitchVertical)));
if (stopwatch.ElapsedMilliseconds >= duration){
playing = false;
Active = false;
Visible = false;
if (AfterStop != null){
AfterStop();
}
}
// ScaleMultiplier = defaultScaleMultiplier + (float)(random.NextDouble() * twitchScale * new[]{-1, 1}[random.Next(2)]);
}
private Action AfterStop;
}

View file

@ -0,0 +1,37 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace ONDClient.GUI;
public class LoadingUIElement : TextUIElement {
private string expectedEndpoint;
private Client.ConnectionState lastState = Client.ConnectionState.IDLE;
public LoadingUIElement(Point corner1, SpriteFont font, string expectedEndpoint, Alignment alignment = Alignment.CENTER, bool autoBounds = true) : base(corner1, font, alignment, autoBounds) {
this.expectedEndpoint = expectedEndpoint;
Active = true;
// Color = Color.LightGray;
}
public override void Update() {
if(lastState == Client.State) return;
lastState = Client.State;
switch (Client.State){
case Client.ConnectionState.CONNECTING:
Text = "Connecting to " + expectedEndpoint;
break;
case Client.ConnectionState.CONNECTED:
Text = "Connected to " + expectedEndpoint;
break;
case Client.ConnectionState.ACCEPTED:
Text = "Waiting for opponent...";
break;
case Client.ConnectionState.GAME_STARTING:
Text = "Opponent: " + Client.Opponent.username;
Color = Color.White;
// ScaleMultiplier = 1.5f;
break;
}
}
}

View file

@ -0,0 +1,48 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGameLibrary.Graphics;
namespace ONDClient.GUI;
public class MenuInputField : UIElement {
private TextUIElement labelElement;
private TextBoxUIElement textBoxElement;
public MenuInputField(SpriteFont font, Point position, string label, string defaultValue = "") : base(position, position) {
labelElement =
new TextUIElement(position, font){Text = label, Color = Color.Gray};
textBoxElement =
new TextBoxUIElement(font,
new(labelElement.Bounds.Item1.X + (int)labelElement.Measure().X, labelElement.Bounds.Item1.Y),
new(640, labelElement.Bounds.Item2.Y + (int)labelElement.Measure().Y));
textBoxElement.OnFocused = () => {
textBoxElement.Color = Color.LightGreen;
labelElement.Color = Color.DarkGreen;
};
textBoxElement.OnUnfocused = () => {
textBoxElement.Color = Color.White;
labelElement.Color = Color.Gray;
};
Bounds = (labelElement.Bounds.Item1, textBoxElement.Bounds.Item2);
Pressable = true;
OnMousePress = textBoxElement.OnMousePress;
textBoxElement.Text = defaultValue;
}
public string Text{
get => textBoxElement.Text;
set => textBoxElement.Text = value;
}
public override void Draw(SpriteBatch spriteBatch) {
labelElement.Draw(spriteBatch);
textBoxElement.Draw(spriteBatch);
}
public override void Update() {
base.Update();
labelElement.Update();
textBoxElement.Update();
}
}

View file

@ -0,0 +1,9 @@
using Microsoft.Xna.Framework;
namespace ONDClient.GUI;
public static class PointExtensions {
public static Point MultiplyByScalar(this Point point, int scalar) => new(point.X * scalar, point.Y * scalar);
public static Point MultiplyByScalar(this Point point, float scalar) => new((int)(point.X * scalar), (int)(point.Y * scalar));
}

View file

@ -0,0 +1,26 @@
using GlobalClassLib;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace ONDClient.GUI;
public class PowerIndicator : TextUIElement {
private string label;
private ClientPlayer player;
private int lastPowerValue;
public PowerIndicator(Point corner1, SpriteFont font, ClientPlayer player, string label, Alignment alignment = Alignment.LEFT) : base(corner1, font, alignment, autoBounds:true) {
this.player = player;
this.label = label;
lastPowerValue = player.state.power;
Text = GetText();
}
public override void Update() {
if (player.state.power == lastPowerValue) return;
lastPowerValue = player.state.power;
Text = GetText();
}
private string GetText() => $"{label}{(int)(((float)player.state.power / Power.MAX_POWER_VALUE) * 100)}";
}

155
ONDClient/GUI/Screen.cs Normal file
View file

@ -0,0 +1,155 @@
using System.Collections.Generic;
using Microsoft.VisualBasic.CompilerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGameLibrary.Input;
namespace ONDClient.GUI;
public class Screen {
public static Dictionary<string, Screen> Screens = new();
public static Screen CurrentScreen{ get; private set; } = Empty;
public static Screen OverlayScreen{ get; private set; } = Empty;
public static void AddScreens(Screen[] screens) {
foreach (var screen in screens){
Screens.Add(screen.Label, screen);
}
}
public static void AddScreen(string id, Screen screen, bool activate = false) {
Screens.Add(id, screen);
if (activate) SetScreen(id);
}
public static void SetScreen(string id) {
if (CurrentScreen.temporary){
Screens.Remove(CurrentScreen.Label);
}
CurrentScreen.Active = false;
CurrentScreen = Screens[id];
CurrentScreen.Active = true;
}
public static void RemoveScreen(string id) {
Screens.Remove(id);
}
public static void UpdateAll() {
foreach (var screen in Screens.Values){
if (!screen.Active) continue;
screen.Update();
}
}
public static Screen Empty => new(""){temporary = true};
public static void SetOverlayScreen(string id) {
if (OverlayScreen.temporary){
Screens.Remove(OverlayScreen.Label);
}
OverlayScreen.Active = false;
OverlayScreen = Screens[id];
OverlayScreen.Active = true;
}
public static void DisableOverlay() {
OverlayScreen = Empty;
}
public static void DrawCurrentAndOverlay(SpriteBatch spriteBatch) {
CurrentScreen.Draw(spriteBatch);
OverlayScreen.Draw(spriteBatch);
}
public string Label{ get; }
private Dictionary<string, UIElement> elements = new();
private List<UIElement> elementsInDrawOrder = new();
public bool Active { get; private set; } = false;
private InputListenerHook mouseInputHook = new(true);
private bool temporary = false;
private List<string> temporaryElements = new();
public Screen(string label) {
Label = label;
InputManager.AddListener(InputManager.MouseButton.LEFT, () => ProcessMouseInput(InputManager.MouseState), InputTiming.PRESS, mouseInputHook);
}
public Screen(string label, Dictionary<string, UIElement> elements) {
this.elements = elements;
Label = label;
}
public UIElement this[string id] => elements[id];
public UIElement TryGetElement(string id) => elements.TryGetValue(id, out var val) ? val : null;
public UIElement AddElement(string id, UIElement element, bool temporary = false) {
elements.Add(id, element);
int insertIndex = elementsInDrawOrder.FindLastIndex(e => e.DrawPriority == element.DrawPriority);
if (insertIndex == -1){
elementsInDrawOrder.Add(element);
}
else{
elementsInDrawOrder.Insert(insertIndex + 1, element);
}
if (temporary){
temporaryElements.Add(id);
}
return element;
}
public void RemoveElement(string id) {
if (!elements.ContainsKey(id)) return;
elements.Remove(id, out var element);
elementsInDrawOrder.RemoveAll(e => e == element);
}
public void RemoveTemporary() {
temporaryElements.ForEach(RemoveElement);
temporaryElements.Clear();
}
public void SetActive(bool active) {
Active = active;
if (Active == active) return;
foreach (var keyValuePair in elements){
keyValuePair.Value.Active = Active;
}
}
private void ProcessMouseInput(MouseState mouseState) {
if (!Active){
return;
}
foreach (var element in elements.Values){
if (!element.Pressable) continue;
if (element.IsWithinBounds(mouseState.Position)){
element.OnMousePress(); // TODO: differentiate between press, hold and release events
}
}
}
public void Update() {
foreach (var element in elements.Values){
if (!element.Active) continue;
element.Update();
}
}
public void Draw(SpriteBatch spriteBatch) {
foreach (var val in elementsInDrawOrder){
val.Draw(spriteBatch);
}
}
}

View file

@ -0,0 +1,42 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGameLibrary;
using MonoGameLibrary.Graphics;
using MonoGameLibrary.Input;
namespace ONDClient.GUI;
public class TextBoxUIElement : TextUIElement {
public bool Focused{ get; set; } = false;
public Action OnFocused{ get; set; } = () => { };
public Action OnUnfocused{ get; set; } = () => { };
public TextBoxUIElement(SpriteFont font, Point corner1, Point corner2) : base(corner1, corner2, font) {
Core.Instance.Window.TextInput += TextInputHandler;
InputManager.AddListener(InputManager.MouseButton.LEFT, () => {
if (!IsWithinBounds(InputManager.MouseState.Position)){
Focused = false;
OnUnfocused();
}
},
InputTiming.PRESS, new InputListenerHook(true));
Pressable = true;
OnMousePress = () => {
Focused = true;
OnFocused();
};
}
public void TextInputHandler(object sender, TextInputEventArgs e) {
if (!Focused) return;
if (e.Character == '\b') {
if (Text.Length > 0) Text = Text[..^1];
return;
}
if(Font.Characters.Contains(e.Character))
Text += e.Character;
}
}

View file

@ -0,0 +1,81 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGameLibrary.Graphics;
namespace ONDClient.GUI;
public class TextUIElement : UIElement {
public SpriteFont Font { get; set; }
public Alignment CurrentAlignment { get; set; }
public string Text{
get;
set{
field = value;
if(AutoBounds)
Bounds = (Bounds.Item1, Bounds.Item1 + new Point((int)Measure().X, (int)Measure().Y));
}
} = "";
public Color Color{ get; set; } = Color.White;
public bool AutoBounds{ get; protected set; } = false;
private Vector2 origin;
private const float UNIVERSAL_TEXT_SCALE_MULTIPLIER = 0.3f;
public TextUIElement(Point corner1, SpriteFont font, Alignment alignment = Alignment.LEFT, bool autoBounds = true) : base(corner1, corner1) {
Font = font;
AutoBounds = autoBounds;
Align(alignment);
}
public TextUIElement(Point corner1, Point corner2, SpriteFont font, Alignment alignment = Alignment.LEFT) : base(corner1, corner2) {
Font = font;
Align(alignment);
}
public override void Draw(SpriteBatch spriteBatch) {
base.Draw(spriteBatch);
align();
spriteBatch.DrawString(Font, Text, screenSpaceBounds.Item1.ToVector2(), Color, 0, origin, pixelScaleMultiplier * UNIVERSAL_TEXT_SCALE_MULTIPLIER, SpriteEffects.None, 0);
}
public void Align(Alignment alignment) {
CurrentAlignment = alignment;
switch (alignment){
case Alignment.LEFT:
AlignLeft();
break;
case Alignment.RIGHT:
AlignRight();
break;
case Alignment.CENTER:
AlignCenter();
break;
}
}
protected override void UpdateBounds() {
Point inSpaceOrigin = Bounds.Item1 + origin.ToPoint();
_bounds = ((Bounds.Item1 - inSpaceOrigin).MultiplyByScalar(ScaleMultiplier) + inSpaceOrigin, (Bounds.Item2 - Bounds.Item1).MultiplyByScalar(ScaleMultiplier) + inSpaceOrigin);
screenSpaceBounds = (Bounds.Item1.MultiplyByScalar(pixelScaleMultiplier), Bounds.Item2.MultiplyByScalar(pixelScaleMultiplier));
Align(CurrentAlignment);
}
public Vector2 Measure() => Font.MeasureString(Text) * UNIVERSAL_TEXT_SCALE_MULTIPLIER * ScaleMultiplier;
private void AlignLeft() {
align = () => origin = Vector2.Zero;
}
private void AlignRight() {
align = () => origin = Font.MeasureString(Text) * ScaleMultiplier;
}
private void AlignCenter() {
align = () => origin = new(Font.MeasureString(Text).X * ScaleMultiplier / 2, 0);
}
private Action align;
public enum Alignment {
LEFT, RIGHT, CENTER
}
}

View file

@ -0,0 +1,31 @@
using System;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace ONDClient.GUI;
public class TimerUIElement : TextUIElement{
private Stopwatch stopwatch = new();
public TimerUIElement(Point corner1, SpriteFont font) : base(corner1, font) {
Text = "00:00.000";
Bounds = (corner1, corner1 + new Point((int)Measure().X, (int)Measure().Y));
}
public override void Update() {
if (stopwatch.IsRunning){
Text = stopwatch.Elapsed.ToString("mm\\:ss\\.fff");
// Text = stopwatch.ElapsedMilliseconds.ToString();
}
}
public void Start() {
stopwatch.Restart();
}
public void Stop() {
stopwatch.Stop();
}
}

108
ONDClient/GUI/UIElement.cs Normal file
View file

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGameLibrary;
using MonoGameLibrary.Graphics;
using MonoGameLibrary.Input;
namespace ONDClient.GUI;
public class UIElement {
public bool Active { get; set; } = true;
public bool Pressable { get; set; } = false;
public bool Visible { get; set; } = true;
public int DrawPriority { get; } = 0;
protected (Point, Point) _bounds;
public (Point, Point) Bounds{
get => _bounds;
protected set { _bounds = value; UpdateBounds(); }
}
protected (Point, Point) screenSpaceBounds;
public List<TextureRegion> Textures = new();
protected int currentTextureId = 0;
private float _scaleMultiplier = 1;
public float ScaleMultiplier{
get{
return _scaleMultiplier;
}
set{
_scaleMultiplier = value;
LoadPixelScaleMultiplier();
}
}
protected int pixelScaleMultiplier = 1;
private void LoadPixelScaleMultiplier() {
pixelScaleMultiplier = (int)(UIManager.GlobalPixelMultiplier * _scaleMultiplier); // TODO: move GlobalPixelMultiplier somewhere where it would make sense
UpdateBounds();
}
protected virtual void UpdateBounds() {
_bounds = (Bounds.Item1, (Bounds.Item2 - Bounds.Item1).MultiplyByScalar(ScaleMultiplier) + Bounds.Item1);
screenSpaceBounds = (Bounds.Item1.MultiplyByScalar(pixelScaleMultiplier), Bounds.Item2.MultiplyByScalar(pixelScaleMultiplier));
}
public UIElement(TextureRegion texture, Point position, int drawPriority = 0) {
Textures.Add(texture);
Bounds = (position, position + new Point(texture.Width, texture.Height));
DrawPriority = drawPriority;
LoadPixelScaleMultiplier();
}
public UIElement(TextureRegion[] textures, Point position, int drawPriority = 0) {
this.Textures.AddRange(textures);
Bounds = (position, position + new Point(textures[0].Width, textures[0].Height));
DrawPriority = drawPriority;
LoadPixelScaleMultiplier();
}
public UIElement(Point corner1, Point corner2) {
Bounds = (corner1, corner2);
Visible = false;
LoadPixelScaleMultiplier();
}
public virtual void SetTexture(int textureId) {
if (textureId >= Textures.Count){
Console.WriteLine($"WARNING: TEXTURE {textureId} OUT OF BOUNDS");
return;
}
currentTextureId = textureId;
}
public virtual void Update() { }
public bool IsWithinBounds(Point pos) {
return pos.X >= Math.Min(screenSpaceBounds.Item1.X, screenSpaceBounds.Item2.X) && pos.X <= Math.Max(screenSpaceBounds.Item1.X, screenSpaceBounds.Item2.X) &&
pos.Y >= Math.Min(screenSpaceBounds.Item1.Y, screenSpaceBounds.Item2.Y) && pos.Y <= Math.Max(screenSpaceBounds.Item1.Y, screenSpaceBounds.Item2.Y);
}
public Action OnMousePress{ get; set; }
// public virtual void OnMousePress() { }
public virtual void OnMouseRelease() { }
public virtual void OnMouseHold() { }
public virtual void Draw(SpriteBatch spriteBatch) {
if (!Visible || !Active){
return;
}
Textures[currentTextureId].Draw(spriteBatch, screenSpaceBounds.Item1.ToVector2(), Color.White, 0, Vector2.Zero, pixelScaleMultiplier, SpriteEffects.None, 0);
// texture.Draw(spriteBatch, bounds.Item1.ToVector2(), Color.White);
}
public void SetPosition(Point position) {
Bounds = (position, position + new Point(Textures[0].Width, Textures[0].Height));
}
public UIElement Clone() {
return new UIElement(Textures.ToArray(), Bounds.Item1);
}
public TextureRegion[] GetTextures() => Textures.ToArray();
}

348
ONDClient/GUI/UIManager.cs Normal file
View file

@ -0,0 +1,348 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GlobalClassLib;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGameLibrary;
using MonoGameLibrary.Graphics;
using MonoGameLibrary.Input;
using ONDClient.Map;
namespace ONDClient.GUI;
public class UIManager {
public static class ScreenTypes {
public const string OFFICE = "office";
public const string CAMERAS = "monitor";
public const string OVERLAY = "overlay";
public const string WIN = "win";
public const string LOSE = "lose";
public const string MENU = "menu";
public const string LOADING = "loading";
}
private static Screen officeScreen = new(ScreenTypes.OFFICE);
private static Screen monitorScreen = new(ScreenTypes.CAMERAS);
private static Screen overlayScreen = new(ScreenTypes.OVERLAY);
private static Screen winScreen = new(ScreenTypes.WIN);
private static Screen loseScreen = new(ScreenTypes.LOSE);
private static Screen menuScreen = new(ScreenTypes.MENU);
private static Screen loadingScreen = new(ScreenTypes.LOADING);
private static TextureAtlas testAtlas;
public static TextureAtlas OfficeAtlas{ get; private set; }
public static TextureAtlas MonitorAtlas{ get; private set; }
public static TextureAtlas EnemyAtlas{ get; private set; }
public static TextureAtlas RoomAtlas{ get; private set; }
public static SpriteFont PixelMonoFont{ get; private set; }
public static int GlobalPixelMultiplier{ get; private set; }
// private Dictionary<(int, int), UIElement> doorElements = new();
private static Dictionary<int, UIElement> enemyElements = new();
private static TimerUIElement timerElement;
private static UIElement cameraView;
private static Dictionary<int, UIElement> lightIndicators = new();
private static InputListenerHook monitorSwitchHook;
private static bool fullBright = false; // Debug
public static void LoadAssets() {
testAtlas = TextureAtlas.FromFile(Core.content, "images/testBlocks-definition.xml");
OfficeAtlas = TextureAtlas.FromFile(Core.content, "images/office-definition.xml");
MonitorAtlas = TextureAtlas.FromFile(Core.content, "images/monitor-definition.xml");
EnemyAtlas = TextureAtlas.FromFile(Core.content, "images/enemies-definition.xml");
RoomAtlas = TextureAtlas.FromFile(Core.content, "images/rooms-definition.xml");
PixelMonoFont = Core.content.Load<SpriteFont>("ponderosa");
}
public static void InitUI() {
GlobalPixelMultiplier = Core.graphicsDevice.Viewport.Height / 360;
Screen.AddScreens([officeScreen, monitorScreen, overlayScreen, winScreen, loseScreen, menuScreen, loadingScreen]);
// Screen.SetScreen(ScreenTypes.OFFICE);
// Screen.SetOverlayScreen(ScreenTypes.OVERLAY);
officeScreen.AddElement("office_left", new UIElement([OfficeAtlas["left-open"], OfficeAtlas["left-closed"]], Point.Zero));
officeScreen.AddElement("office_centre", new UIElement([OfficeAtlas["centre-open"], OfficeAtlas["centre-closed"]], new Point(200, 0)));
officeScreen.AddElement("office_right", new UIElement([OfficeAtlas["right-open"], OfficeAtlas["right-closed"]], new Point(440, 0)));
// officeScreen.AddElement("test",
// new UIElement(testAtlas[0], Point.Zero)
// {Pressable = true, OnMousePress = () => Console.WriteLine("Pressed!")}
// );
monitorScreen.AddElement("screen", new UIElement(MonitorAtlas["screen"], Point.Zero));
monitorScreen.AddElement("view-frame", new UIElement(MonitorAtlas["view-frame"], new Point(62, 55)));
monitorScreen.AddElement("map-frame", new UIElement(MonitorAtlas["map-frame"], new Point(334, 135)));
monitorScreen.AddElement("map", new UIElement(MonitorAtlas["map"], new Point(334, 135)));
List<TextureRegion> rooms = new();
for (int i = 0; i < ClientMapManager.MAP_SIDE_LENGTH * ClientMapManager.MAP_SIDE_LENGTH; i++){
rooms.Add(RoomAtlas["room" + i]);
}
cameraView = new UIElement(rooms.ToArray(), new(64, 64));
monitorScreen.AddElement("camera-view", cameraView);
monitorScreen.AddElement("p1-office-door-left", new UIElement([MonitorAtlas["door-office-p1-left-open"], MonitorAtlas["door-office-p1-left-closed"]], new Point(400, 272)));
monitorScreen.AddElement("p1-office-door-centre", new UIElement([MonitorAtlas["door-office-p1-centre-open"], MonitorAtlas["door-office-p1-centre-closed"]], new Point(400, 272)));
monitorScreen.AddElement("p1-office-door-right", new UIElement([MonitorAtlas["door-office-p1-right-open"], MonitorAtlas["door-office-p1-right-closed"]], new Point(400, 272)));
monitorScreen.AddElement("p2-office-door-right", new UIElement([MonitorAtlas["door-office-p2-right-open"], MonitorAtlas["door-office-p2-right-closed"]], new Point(400, 144)));
monitorScreen.AddElement("p2-office-door-centre", new UIElement([MonitorAtlas["door-office-p2-centre-open"], MonitorAtlas["door-office-p2-centre-closed"]], new Point(400, 144)));
monitorScreen.AddElement("p2-office-door-left", new UIElement([MonitorAtlas["door-office-p2-left-open"], MonitorAtlas["door-office-p2-left-closed"]], new Point(400, 144)));
// main menu
timerElement = new(new(0, 0), PixelMonoFont);
overlayScreen.AddElement("timer", timerElement);
officeScreen.AddElement("power-p1-office", new PowerIndicator(new(timerElement.Bounds.Item1.X, timerElement.Bounds.Item2.Y + 5), PixelMonoFont, Client.Player, "POWER: "));
TextUIElement powerLabel = (TextUIElement)
monitorScreen.AddElement("power-label", new TextUIElement(new(510, 150), PixelMonoFont){Text = "POWER:"});
TextUIElement powerP1 = (TextUIElement)
monitorScreen.AddElement("power-p2", new PowerIndicator(new(powerLabel.Bounds.Item1.X + 10, powerLabel.Bounds.Item2.Y + 10), PixelMonoFont, Client.Opponent, ""){Color = new Color(220, 10, 10, 255)});
monitorScreen.AddElement("power-p1", new PowerIndicator( new (powerP1.Bounds.Item1.X, powerP1.Bounds.Item2.Y + 5), PixelMonoFont, Client.Player, ""){Color = new Color(15, 190, 247, 255)});
winScreen.AddElement("win-text", new TextUIElement(new(320, 180), PixelMonoFont, TextUIElement.Alignment.CENTER){Text = "YOU WIN", Color = Color.Green});
loseScreen.AddElement("lose-text", new TextUIElement(new(320, 180), PixelMonoFont, TextUIElement.Alignment.CENTER){Text = "YOU LOSE", Color = Color.Red});
MenuInputField usernameField = (MenuInputField)menuScreen.AddElement("username-field", new MenuInputField(PixelMonoFont, new(20, 20), "USERNAME: "));
MenuInputField field = (MenuInputField)menuScreen.AddElement("server-ip-field", new MenuInputField(PixelMonoFont, new(usernameField.Bounds.Item1.X, usernameField.Bounds.Item2.Y + 20), "SERVER IP: ", "127.0.0.1"));
UIElement connectButton = menuScreen.AddElement("server-ip-submit", new TextUIElement(new Point(field.Bounds.Item1.X, field.Bounds.Item2.Y), PixelMonoFont)
{
Text = "CONNECT",
Pressable = true,
OnMousePress = () => {
// string[] input = serverIpTextBox.Text.Split(":");
// if(input.Length != 2 || !int.TryParse(input[0], out var port)) return;
// Client.Connect(input[0], port);
Client.Player.username = usernameField.Text;
Client.Connect(field.Text, 9012);
Screen.SetScreen(ScreenTypes.LOADING);
}
});
menuScreen.AddElement("host-button",
new TextUIElement(new(connectButton.Bounds.Item1.X, connectButton.Bounds.Item2.Y + 30), PixelMonoFont) {Text = "HOST", Pressable = true,
OnMousePress = () => {
Client.StartServer();
Client.Player.username = usernameField.Text;
Client.Connect("127.0.0.1", 9012);
Screen.SetScreen(ScreenTypes.LOADING);
}});
loadingScreen.AddElement("loading-text", new LoadingUIElement(new(320, 180), PixelMonoFont, field.Text));
}
public static void DisplayMainMenu() {
ResetUI();
Screen.SetScreen(ScreenTypes.MENU);
CommandManager.AllowGameControls(false);
// if(Client.Player.username != null)
// ((MenuInputField)menuScreen["username-field"]).Text = Client.Player.username;
}
public static void SpawnMapElements(TileConnectorProjection[] doors) {
for (int i = 0; i < 5; i++){ // NOTE: this loop does y in reverse, y labels are inverted to match server
for (int j = 0; j < 5; j++){
int id = ClientMapManager.CoordsToId(i, 4 - j);
if (Client.Player.state.officeTileId == id || Client.Opponent.state.officeTileId == id) continue; // TODO: remove the other check for office
Point point1 = new Point(336 + (32 * i), 144 + (32 * j));
Point point2 = new Point(367 + (32 * i), 175 + (32 * j));
monitorScreen.AddElement(
$"room{id}", new UIElement(point1, point2)
{Pressable = true, OnMousePress = (() => CommandManager.SendChangeCamera(id))},
true);
lightIndicators.Add(id,
monitorScreen.AddElement(
$"light{id}", new UIElement(MonitorAtlas["map-light-indicator"], point1)
{Visible = false},
true));
//
// if (doorPositions.ContainsKey((i, j))){
// monitorScreen.AddElement("door"+doorPositions[(i, j)], new UIElement([monitorAtlas[5], monitorAtlas[6]], point1));
// }
}
}
monitorScreen.AddElement("eye-player", new UIElement(MonitorAtlas["eye-small-player"], monitorScreen["room"+Client.Player.state.camera].Bounds.Item1), true);
monitorScreen.AddElement("eye-opponent", new UIElement([MonitorAtlas["eye-small-opponent-closed"], MonitorAtlas["eye-small-opponent-open"]], monitorScreen["room"+Client.Opponent.state.camera].Bounds.Item1), true);
foreach (var door in doors){
if(door.Type != ConnectorType.DOOR_REMOTE) continue;
(int x, int y) dpos = (Math.Abs(door.Tiles.tile1.GridPosition.x - door.Tiles.tile2.GridPosition.x), Math.Abs(door.Tiles.tile1.GridPosition.y - door.Tiles.tile2.GridPosition.y));
if (dpos.y == 1){
int targetId = door.Tiles.tile1.GridPosition.y > door.Tiles.tile2.GridPosition.y ? door.Tiles.tile1.Id : door.Tiles.tile2.Id;
UIElement tile = monitorScreen["room"+targetId];
monitorScreen.AddElement("door"+Math.Max(door.Tiles.tile1.Id, door.Tiles.tile2.Id)+"-"+Math.Min(door.Tiles.tile1.Id, door.Tiles.tile2.Id), new UIElement([MonitorAtlas["door-remote-open"], MonitorAtlas["door-remote-closed"]], tile.Bounds.Item1), true);
}
}
}
public static void DisplayGameUI() {
Screen.SetScreen(ScreenTypes.OFFICE);
Screen.SetOverlayScreen(ScreenTypes.OVERLAY);
CommandManager.AllowGameControls(true);
UpdateCameras([Client.Player.state.camera]); // in case there is an enemy on the default camera
cameraView.SetTexture(Client.Player.state.camera);
}
public static void StartTimer() {
timerElement.Start();
}
public static void AddEnemySprite(int id, UIElement sprite, UIElement jumpscareSprite = null) {
monitorScreen.AddElement($"enemy{id}", sprite, true);
if (jumpscareSprite != null){
officeScreen.AddElement($"enemy{id}-jumpscare", jumpscareSprite, true);
jumpscareSprite.Active = false;
jumpscareSprite.Visible = false;
}
enemyElements.Add(id, sprite);
sprite.Visible = false;
}
public static void ChangeDoorState(Direction dir, bool state) {
int stateInt = state ? 1 : 0;
switch ((int)dir){
case 0:
officeScreen["office_left"].SetTexture(stateInt);
monitorScreen["p1-office-door-left"].SetTexture(stateInt);
break;
case 1:
officeScreen["office_centre"].SetTexture(stateInt);
monitorScreen["p1-office-door-centre"].SetTexture(stateInt);
break;
case 2:
officeScreen["office_right"].SetTexture(stateInt);
monitorScreen["p1-office-door-right"].SetTexture(stateInt);
break;
}
}
public static void ChangeDoorStateOpponent(Direction dir, bool state) { // TODO: overload to avoid excessive casting
int stateInt = state ? 1 : 0;
switch ((int)dir){
case 0:
monitorScreen["p2-office-door-left"].SetTexture(stateInt);
break;
case 1:
monitorScreen["p2-office-door-centre"].SetTexture(stateInt);
break;
case 2:
monitorScreen["p2-office-door-right"].SetTexture(stateInt);
break;
}
}
public static void ChangeRemoteDoorState((int, int) id, bool state) {
monitorScreen["door"+Math.Max(id.Item1, id.Item2)+"-"+Math.Min(id.Item1, id.Item2)].SetTexture(state ? 1 : 0);
}
public static void ChangeMonitorState(bool state) {
Screen.SetScreen(state ? ScreenTypes.CAMERAS : ScreenTypes.OFFICE);
UpdateCameras([Client.Player.state.camera]);
}
public static void ChangeMonitorStateOpponent(bool state) {
monitorScreen["eye-opponent"].SetTexture(state ? 1 : 0);
}
public static void ChangeCamera(int id) {
monitorScreen["eye-player"].SetPosition(monitorScreen["room"+id].Bounds.Item1);
cameraView.SetTexture(id);
UpdateCameras([id]);
}
public static void UpdateCameras(int[] camIds) {
foreach (var id in camIds){
MapTileProjection tile = ClientMapManager.Get(id);
if(tile.Owner == null || tile.Id == Client.Player.state.officeTileId || tile.Id == Client.Opponent.state.officeTileId) continue;
lightIndicators[id].Visible = tile.Lit;
}
if (camIds.Contains(Client.Player.state.camera)){
bool lit = ClientMapManager.Get(Client.Player.state.camera).Lit || fullBright;
cameraView.Visible = lit;
enemyElements.Values.Where(e => e.Visible).ToList().ForEach(e => e.Visible = false);
ClientEnemy[] enemies = ClientEnemyManager.GetByLocation(ClientMapManager.Get(Client.Player.state.camera));
foreach (var enemy in enemies){
enemyElements.TryGetValue(enemy.Id, out var element);
if (element == null) continue;
EnemyUIElement enemyElement = (EnemyUIElement)element;
enemyElement.Visible = true;
enemyElement.SetTexture(lit);
}
if (!lit && Client.Player.state.monitorUp && enemies.Any(e => e.TypeId == (int)EnemyType.NEKO)){
SoundManager.StartNekoPurr();
}
else{
SoundManager.StopNekoPurr();
}
}
}
public static void ChangeCameraOpponent(int id) {
monitorScreen["eye-opponent"].SetPosition(monitorScreen["room"+id].Bounds.Item1);
}
public static void Jumpscare(ClientEnemy enemy) {
Screen.SetScreen(ScreenTypes.OFFICE);
enemy.JumpscareSprite.Play();
timerElement.Stop();
CommandManager.AllowGameControls(false);
// UIElement jumpscareElement = enemy.Sprite.Clone();
// jumpscareElement.ScaleMultiplier = 2;
// jumpscareElement.SetPosition(new Point(0, 0));
// officeScreen.AddElement("jumpscare", jumpscareElement);
}
public static void ShowVictoryScreen() {
Screen.SetScreen(ScreenTypes.WIN);
Screen.DisableOverlay();
CommandManager.AllowGameControls(false);
SoundManager.StopAmbience();
InputManager.AddListener(Keys.Space, DisplayMainMenu, InputTiming.PRESS, new InputListenerHook(true, true));
}
public static void ShowDeathScreen() {
Screen.SetScreen(ScreenTypes.LOSE);
Screen.DisableOverlay();
CommandManager.AllowGameControls(false);
SoundManager.StopAmbience();
InputManager.AddListener(Keys.Space, DisplayMainMenu, InputTiming.PRESS, new InputListenerHook(true, true));
}
public static void ResetUI() {
foreach (Screen screen in Screen.Screens.Values){
screen.RemoveTemporary();
}
lightIndicators.Clear();
enemyElements.Clear();
timerElement.Stop();
}
// private static Point GetRoomUIPos((int x, int y) pos) {
// return new Point(336 + (32 * pos.x), 144 + (32 * pos.y));
// }
}

69
ONDClient/GameMain.cs Normal file
View file

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGameLibrary;
using MonoGameLibrary.Graphics;
using MonoGameLibrary.Input;
using ONDClient.GUI;
namespace ONDClient;
public class GameMain() : Core("fnafkooo", 1280, 720, false) {
// private GraphicsDeviceManager _graphics;
// private SpriteBatch _spriteBatch;
protected override void Initialize() {
Exiting += (_, _) => {
Client.Disconnect();
};
// Client.Connect("127.0.0.1", 9012);
CommandManager.InitInputListeners();
// InputManager.AddListener(InputManager.MouseButton.LEFT, (() => Console.WriteLine("LMB pressed at: " + InputManager.MouseState.Position)), InputTiming.PRESS, new InputListenerHook(true));
base.Initialize();
Client.Init();
UIManager.InitUI();
UIManager.DisplayMainMenu();
}
protected override void LoadContent() {
UIManager.LoadAssets();
SoundManager.LoadSounds();
// spriteBatch = new SpriteBatch(GraphicsDevice);
// font = Content.Load<SpriteFont>("font");
}
protected override void Update(GameTime gameTime) {
if (Keyboard.GetState().IsKeyDown(Keys.Escape)){
Exit();
}
InputManager.NextInputCycle();
Screen.UpdateAll();
if(Client.State == Client.ConnectionState.IDLE) return;
Client.Update();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime) {
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(samplerState:SamplerState.PointClamp);
Screen.DrawCurrentAndOverlay(spriteBatch);
// Screen.CurrentScreen.Draw(spriteBatch);
// spriteBatch.DrawString(font, "Elapsed time: " + gameTime.TotalGameTime.Milliseconds, new Vector2(10, 10), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}

BIN
ONDClient/Icon.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
ONDClient/Icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View file

@ -0,0 +1,109 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.JavaScript;
using GlobalClassLib;
using ONDClient.GUI;
using PacketLib;
namespace ONDClient.Map;
public class ClientMapManager {
private static MapTileProjection[,] map = new MapTileProjection[5, 5];
public static MapTileProjection Get((int x, int y) coords) => map[coords.x, coords.y];
public static MapTileProjection Get(int tileId) => Get(IdToCoords(tileId));
private static bool inverted;
public static void InitMap(int[] connectors, int[] yourTiles, int[] opponentTiles, int[] litTiles, bool upsideDown ) {
(int id1, int id2, ConnectorType type, ClientPlayer? owner)[] connectorsData = new (int, int , ConnectorType, ClientPlayer?)[connectors.Length / 4];
for (int i = 0; i < connectors.Length / 4; i++){
connectorsData[i] = (connectors[i * 4], connectors[i * 4 + 1], (ConnectorType)connectors[i * 4 + 2], connectors[i * 4 + 3] == -1 ? null : Client.GetPlayer(connectors[i * 4 + 3]));
}
// ClientMapManager.InitMap(upsideDown);
inverted = upsideDown;
IdToCoords = upsideDown ? _IdToCoordsInverse : _IdToCoords;
CoordsToId = upsideDown ? _CoordsToIdInverse : _CoordsToId;
for (int i = 0; i < 5; i++){
for (int j = 0; j < 2; j++){
map[i, j] = new MapTileProjection(CoordsToId(i, j));
}
map[i, 2] = new MapTileProjection(CoordsToId(i, 2));
for (int j = 3; j < 5; j++){
map[i, j] = new MapTileProjection(CoordsToId(i, j));
}
}
foreach (var tileId in yourTiles){
Get(tileId).Owner = Client.Player;
}
foreach (var tileId in opponentTiles){
Get(tileId).Owner = Client.Opponent;
}
foreach (var tileId in litTiles){
Get(tileId).Lit = true;
}
TileConnectorProjection[] connectorProjections = connectorsData.Select(c => new TileConnectorProjection(Get(c.id1), Get(c.id2), c.type){Owner = c.owner}).ToArray();
InitConnectors(connectorProjections);
UIManager.SpawnMapElements(connectorProjections.Where(c => c.Type == ConnectorType.DOOR_REMOTE).ToArray());
}
public static void InitConnectors(TileConnectorProjection[] connectors) {
foreach (var con in connectors){
// (int x, int y) coords1 = MapTileProjection.IdToCoords(con.Tiles.tile1.Id);
// (int x, int y) coords2 = MapTileProjection.IdToCoords(con.Tiles.tile2.Id);
map[con.Tiles.tile1.GridPosition.x, con.Tiles.tile1.GridPosition.y].AddConnector(con);
}
}
public static TileConnectorProjection GetConnector((int, int) id) => Get(id.Item1).GetConnector(id.Item2);
public static TileConnectorProjection[] GetConnectors(int tileId) => Get(IdToCoords(tileId)).GetAllConnectors();
public static TileConnectorProjection[] GetAllConnectors() {
List<TileConnectorProjection> connectors = new();
for (int i = 0; i < 5; i++){
for (int j = 0; j < 5; j++){
MapTileProjection tile = map[i, j];
Array.ForEach(tile.GetAllConnectors(), c => {
if(tile.Id < c.OtherTile(tile).Id)
connectors.Add(c);
});
}
}
return connectors.ToArray();
}
public const int MAP_SIDE_LENGTH = 5; // map grid height
public static Func<int, int, int> CoordsToId{ get; private set; }
public static Func<int, (int x, int y)> IdToCoords{ get; private set; }
private static Func<int, (int x, int y)> _IdToCoords = id => (id / MAP_SIDE_LENGTH, id % MAP_SIDE_LENGTH);
private static Func<int, (int x, int y)> _IdToCoordsInverse = id => (MAP_SIDE_LENGTH - 1 - (id / MAP_SIDE_LENGTH), MAP_SIDE_LENGTH - 1 - (id % MAP_SIDE_LENGTH));
private static Func<int, int, int> _CoordsToId = (x, y) => x * MAP_SIDE_LENGTH + y;
private static Func<int, int, int> _CoordsToIdInverse = (x, y) => (MAP_SIDE_LENGTH - 1 - x) * MAP_SIDE_LENGTH + (MAP_SIDE_LENGTH - 1 - y);
public static MapTileProjection[] GetAllTiles() {
List<MapTileProjection> tiles = new();
foreach (var tile in map){
tiles.Add(tile);
}
return tiles.ToArray();
}
}

View file

@ -0,0 +1,11 @@
using GlobalClassLib;
namespace ONDClient.Map;
public class MapTileProjection : GlobalMapTile<TileConnectorProjection, MapTileProjection> {
public ClientPlayer? Owner { get; set; }
public MapTileProjection(int id) : base(id, ClientMapManager.IdToCoords(id)) {
Lit = false;
}
}

View file

@ -0,0 +1,14 @@
using GlobalClassLib;
namespace ONDClient.Map;
public class TileConnectorProjection : GlobalTileConnector<MapTileProjection, TileConnectorProjection> {
public ClientPlayer? Owner { get; set; }
public TileConnectorProjection(MapTileProjection tile1, MapTileProjection tile2, ConnectorType type) : base(tile1, tile2, type) {
}
public TileConnectorProjection(MapTileProjection tile2, ConnectorType type) : base(tile2, type) {
}
public override TileConnectorProjection Clone() => new(Tiles.tile1, Tiles.tile2, Type){Owner = Owner};
}

View file

@ -0,0 +1,63 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<RollForward>Major</RollForward>
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
<LangVersion>14</LangVersion>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Icon.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<OutputPath>../bin/Client/</OutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<OutputPath>../bin/Debug/Client/</OutputPath>
</PropertyGroup>
<ItemGroup>
<None Remove="Icon.ico"/>
<None Remove="Icon.bmp"/>
<None Remove="Input\**" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Icon.ico">
<LogicalName>Icon.ico</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Icon.bmp">
<LogicalName>Icon.bmp</LogicalName>
</EmbeddedResource>
<EmbeddedResource Remove="Input\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="LiteNetLib" Version="1.3.1" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.*"/>
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.*"/>
</ItemGroup>
<ItemGroup>
<Folder Include="link\" />
</ItemGroup>
<ItemGroup>
<Reference Include="MonoGameLibrary">
<HintPath>link\MonoGameLibrary.dll</HintPath>
</Reference>
<!-- <Reference Include="FNAF_Server">-->
<!-- <HintPath>link\FNAF_Server.dll</HintPath>-->
<!-- </Reference>-->
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GlobalClassLib\GlobalClassLib.csproj" />
<ProjectReference Include="..\PacketLib\PacketLib.csproj" />
<!-- <ProjectReference Include="..\FNAF_Server\FNAF_Server.csproj" />-->
</ItemGroup>
<ItemGroup>
<Compile Remove="Input\**" />
</ItemGroup>
<Target Name="RestoreDotnetTools" BeforeTargets="CollectPackageReferences">
<Message Text="Restoring dotnet tools (this might take a while depending on your internet speed and should only happen upon building your project for the first time, or after upgrading MonoGame, or clearing your nuget cache)" Importance="High"/>
<Exec Command="dotnet tool restore"/>
</Target>
</Project>

5
ONDClient/Program.cs Normal file
View file

@ -0,0 +1,5 @@
using System;
using var game = new ONDClient.GameMain();
Environment.CurrentDirectory = AppDomain.CurrentDomain.BaseDirectory;
game.Run();

168
ONDClient/SoundManager.cs Normal file
View file

@ -0,0 +1,168 @@
using System;
using GlobalClassLib;
using Microsoft.Xna.Framework.Audio;
using MonoGameLibrary;
namespace ONDClient;
public static class SoundManager {
private static SoundEffect ambience;
private static SoundEffect ventWalk;
private static SoundEffect camSwitch;
private static SoundEffect lightOff;
private static SoundEffect lightOn;
private static SoundEffect doorClose;
private static SoundEffect doorCloseRemote;
private static SoundEffect doorOpen;
private static SoundEffect doorOpenRemote;
private static SoundEffect monitorFlip;
private static SoundEffect powerOut;
private static SoundEffect mareMove;
private static SoundEffect dashMove;
private static SoundEffect spotMove;
private static SoundEffect spotActivate;
private static SoundEffect nekoPurr;
private static SoundEffect jumpscare;
private static SoundEffect nekoAnger;
private static SoundEffect nekoMove;
private static SoundEffect bell;
private static SoundEffectInstance ambienceInstance;
private static SoundEffectInstance nekoPurrInstance;
private static Random random = new();
public static void LoadSounds() {
ambience = Core.content.Load<SoundEffect>("sounds/ambience");
ventWalk = Core.content.Load<SoundEffect>("sounds/vent-walk");
camSwitch = Core.content.Load<SoundEffect>("sounds/camera-switch");
lightOff = Core.content.Load<SoundEffect>("sounds/light-off");
lightOn = Core.content.Load<SoundEffect>("sounds/light-on");
doorClose = Core.content.Load<SoundEffect>("sounds/door-close");
doorCloseRemote = Core.content.Load<SoundEffect>("sounds/door-close-remote");
doorOpen = Core.content.Load<SoundEffect>("sounds/door-open");
doorOpenRemote = Core.content.Load<SoundEffect>("sounds/door-open-remote");
monitorFlip = Core.content.Load<SoundEffect>("sounds/monitor-flip");
powerOut = Core.content.Load<SoundEffect>("sounds/powerout");
mareMove = Core.content.Load<SoundEffect>("sounds/mare-move");
dashMove = Core.content.Load<SoundEffect>("sounds/dash-move");
spotMove = Core.content.Load<SoundEffect>("sounds/spot-move");
spotActivate = Core.content.Load<SoundEffect>("sounds/spot-activate");
nekoPurr = Core.content.Load<SoundEffect>("sounds/neko-purr");
jumpscare = Core.content.Load<SoundEffect>("sounds/jumpscare");
nekoAnger = Core.content.Load<SoundEffect>("sounds/neko-angry");
nekoMove = Core.content.Load<SoundEffect>("sounds/neko-move1");
bell = Core.content.Load<SoundEffect>("sounds/bell");
nekoPurrInstance = nekoPurr.CreateInstance();
ambienceInstance = ambience.CreateInstance();
}
public static void StartAmbience() {
ambienceInstance = ambience.CreateInstance();
ambienceInstance.IsLooped = true;
ambienceInstance.Volume = 0.75f;
ambienceInstance.Play();
}
public static void StopAmbience() {
ambienceInstance.Stop();
}
public static void PlayDoor(bool doorState) {
if (doorState){
doorClose.Play();
}
else{
doorOpen.Play();
}
}
public static void PlayDoorRemote(bool doorState) {
if (doorState){
doorCloseRemote.Play();
}
else{
doorOpenRemote.Play();
}
}
public static void PlayLight(bool lightState) {
if (lightState){
lightOn.Play();
}
else{
lightOff.Play();
}
}
public static void PlayJumpscare() => jumpscare.Play();
public static void PlayPowerOut() => powerOut.Play();
public static void PlayNekoMove() => nekoMove.Play();
public static void PlayNekoAnger() => nekoAnger.Play();
public static void PlaySpotActivate() => spotActivate.Play();
public static void PlaySpotMove() => spotMove.Play();
public static void PlayDashMove() => dashMove.Play();
public static void PlayMareMove() => mareMove.Play();
public static void PlayMonitorFlip() => monitorFlip.Play();
public static void PlayCameraSwitch() => camSwitch.Play();
public static void PlayVentWalk() => ventWalk.Play();
public static void PlayBell() => bell.Play();
public static void StartNekoPurr() {
nekoPurrInstance = nekoPurr.CreateInstance();
nekoPurrInstance.IsLooped = true;
nekoPurrInstance.Play();
}
public static void StopNekoPurr() {
nekoPurrInstance.Stop();
}
private static SoundEffectInstance GetRandomisedPitchInstance(SoundEffect effect) {
SoundEffectInstance instance = effect.CreateInstance();
instance.Pitch = (float)(random.NextDouble() - 0.5) / 5;
instance.Play();
return instance;
}
public static void PlayEnemyMove(ClientEnemy enemy) {
switch ((EnemyType)enemy.TypeId){
case EnemyType.NEKO:
PlayNekoMove();
break;
case EnemyType.SPOT:
PlaySpotMove();
break;
case EnemyType.MARE:
PlayMareMove();
break;
}
}
public static void PlayEnemyReset(ClientEnemy enemy) {
switch ((EnemyType)enemy.TypeId){
case EnemyType.NEKO:
PlayNekoMove();
break;
case EnemyType.SPOT:
PlaySpotMove();
break;
case EnemyType.MARE:
PlayMareMove();
break;
case EnemyType.DASH:
PlayDashMove();
break;
}
}
}

43
ONDClient/app.manifest Normal file
View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="FNAF_Clone"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on and is
is designed to work with. Uncomment the appropriate elements and Windows will
automatically selected the most compatible environment. -->
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View file

@ -0,0 +1 @@
../../MonoGameLibrary/MonoGameLibrary/bin/Release/net9.0/publish/MonoGameLibrary.dll