diff --git a/README.md b/README.md
index edb0512..a8dd1fc 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Jeu coop 3 joueurs en ligne, jouable dans le navigateur. Horde survival isométrique : on défend un portail (le Soulgate) contre 3 vagues d'ennemis avec 2 boss à la fin.
-Projet de fin d'année DSP CNAM Paris.
+Projet de fin d'année DSP — CNAM Paris.
## Stack
diff --git a/client/assets/discord.svg b/client/assets/discord.svg
new file mode 100644
index 0000000..c03e8e1
--- /dev/null
+++ b/client/assets/discord.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/client/assets/fond_soulgate.png b/client/assets/fond_soulgate.png
new file mode 100644
index 0000000..9fbfab5
Binary files /dev/null and b/client/assets/fond_soulgate.png differ
diff --git a/client/assets/maps/Isometric/barrel_E.png b/client/assets/maps/Isometric/barrel_E.png
new file mode 100644
index 0000000..d14ea5c
Binary files /dev/null and b/client/assets/maps/Isometric/barrel_E.png differ
diff --git a/client/assets/maps/Isometric/barrel_N.png b/client/assets/maps/Isometric/barrel_N.png
new file mode 100644
index 0000000..f310f5e
Binary files /dev/null and b/client/assets/maps/Isometric/barrel_N.png differ
diff --git a/client/assets/maps/Isometric/barrel_S.png b/client/assets/maps/Isometric/barrel_S.png
new file mode 100644
index 0000000..0d45933
Binary files /dev/null and b/client/assets/maps/Isometric/barrel_S.png differ
diff --git a/client/assets/maps/Isometric/barrel_W.png b/client/assets/maps/Isometric/barrel_W.png
new file mode 100644
index 0000000..685b279
Binary files /dev/null and b/client/assets/maps/Isometric/barrel_W.png differ
diff --git a/client/assets/maps/Isometric/barrelsStacked_E.png b/client/assets/maps/Isometric/barrelsStacked_E.png
new file mode 100644
index 0000000..915a4c8
Binary files /dev/null and b/client/assets/maps/Isometric/barrelsStacked_E.png differ
diff --git a/client/assets/maps/Isometric/barrelsStacked_N.png b/client/assets/maps/Isometric/barrelsStacked_N.png
new file mode 100644
index 0000000..57f5404
Binary files /dev/null and b/client/assets/maps/Isometric/barrelsStacked_N.png differ
diff --git a/client/assets/maps/Isometric/barrelsStacked_S.png b/client/assets/maps/Isometric/barrelsStacked_S.png
new file mode 100644
index 0000000..33b6cde
Binary files /dev/null and b/client/assets/maps/Isometric/barrelsStacked_S.png differ
diff --git a/client/assets/maps/Isometric/barrelsStacked_W.png b/client/assets/maps/Isometric/barrelsStacked_W.png
new file mode 100644
index 0000000..d8586f8
Binary files /dev/null and b/client/assets/maps/Isometric/barrelsStacked_W.png differ
diff --git a/client/assets/maps/Isometric/barrels_E.png b/client/assets/maps/Isometric/barrels_E.png
new file mode 100644
index 0000000..bca9fdb
Binary files /dev/null and b/client/assets/maps/Isometric/barrels_E.png differ
diff --git a/client/assets/maps/Isometric/barrels_N.png b/client/assets/maps/Isometric/barrels_N.png
new file mode 100644
index 0000000..a7fadaf
Binary files /dev/null and b/client/assets/maps/Isometric/barrels_N.png differ
diff --git a/client/assets/maps/Isometric/barrels_S.png b/client/assets/maps/Isometric/barrels_S.png
new file mode 100644
index 0000000..94c6dd9
Binary files /dev/null and b/client/assets/maps/Isometric/barrels_S.png differ
diff --git a/client/assets/maps/Isometric/barrels_W.png b/client/assets/maps/Isometric/barrels_W.png
new file mode 100644
index 0000000..cdd5edc
Binary files /dev/null and b/client/assets/maps/Isometric/barrels_W.png differ
diff --git a/client/assets/maps/Isometric/bridgeBroken_E.png b/client/assets/maps/Isometric/bridgeBroken_E.png
new file mode 100644
index 0000000..5b6ae2f
Binary files /dev/null and b/client/assets/maps/Isometric/bridgeBroken_E.png differ
diff --git a/client/assets/maps/Isometric/bridgeBroken_N.png b/client/assets/maps/Isometric/bridgeBroken_N.png
new file mode 100644
index 0000000..60bd95e
Binary files /dev/null and b/client/assets/maps/Isometric/bridgeBroken_N.png differ
diff --git a/client/assets/maps/Isometric/bridgeBroken_S.png b/client/assets/maps/Isometric/bridgeBroken_S.png
new file mode 100644
index 0000000..897a5bd
Binary files /dev/null and b/client/assets/maps/Isometric/bridgeBroken_S.png differ
diff --git a/client/assets/maps/Isometric/bridgeBroken_W.png b/client/assets/maps/Isometric/bridgeBroken_W.png
new file mode 100644
index 0000000..2afb84c
Binary files /dev/null and b/client/assets/maps/Isometric/bridgeBroken_W.png differ
diff --git a/client/assets/maps/Isometric/bridge_E.png b/client/assets/maps/Isometric/bridge_E.png
new file mode 100644
index 0000000..73babd2
Binary files /dev/null and b/client/assets/maps/Isometric/bridge_E.png differ
diff --git a/client/assets/maps/Isometric/bridge_N.png b/client/assets/maps/Isometric/bridge_N.png
new file mode 100644
index 0000000..99dc06d
Binary files /dev/null and b/client/assets/maps/Isometric/bridge_N.png differ
diff --git a/client/assets/maps/Isometric/bridge_S.png b/client/assets/maps/Isometric/bridge_S.png
new file mode 100644
index 0000000..8768264
Binary files /dev/null and b/client/assets/maps/Isometric/bridge_S.png differ
diff --git a/client/assets/maps/Isometric/bridge_W.png b/client/assets/maps/Isometric/bridge_W.png
new file mode 100644
index 0000000..ec3b5d6
Binary files /dev/null and b/client/assets/maps/Isometric/bridge_W.png differ
diff --git a/client/assets/maps/Isometric/chair_E.png b/client/assets/maps/Isometric/chair_E.png
new file mode 100644
index 0000000..f019c8f
Binary files /dev/null and b/client/assets/maps/Isometric/chair_E.png differ
diff --git a/client/assets/maps/Isometric/chair_N.png b/client/assets/maps/Isometric/chair_N.png
new file mode 100644
index 0000000..403c48e
Binary files /dev/null and b/client/assets/maps/Isometric/chair_N.png differ
diff --git a/client/assets/maps/Isometric/chair_S.png b/client/assets/maps/Isometric/chair_S.png
new file mode 100644
index 0000000..871207f
Binary files /dev/null and b/client/assets/maps/Isometric/chair_S.png differ
diff --git a/client/assets/maps/Isometric/chair_W.png b/client/assets/maps/Isometric/chair_W.png
new file mode 100644
index 0000000..b96f221
Binary files /dev/null and b/client/assets/maps/Isometric/chair_W.png differ
diff --git a/client/assets/maps/Isometric/chestClosed_E.png b/client/assets/maps/Isometric/chestClosed_E.png
new file mode 100644
index 0000000..ca60ced
Binary files /dev/null and b/client/assets/maps/Isometric/chestClosed_E.png differ
diff --git a/client/assets/maps/Isometric/chestClosed_N.png b/client/assets/maps/Isometric/chestClosed_N.png
new file mode 100644
index 0000000..74ab1ba
Binary files /dev/null and b/client/assets/maps/Isometric/chestClosed_N.png differ
diff --git a/client/assets/maps/Isometric/chestClosed_S.png b/client/assets/maps/Isometric/chestClosed_S.png
new file mode 100644
index 0000000..7f4eddf
Binary files /dev/null and b/client/assets/maps/Isometric/chestClosed_S.png differ
diff --git a/client/assets/maps/Isometric/chestClosed_W.png b/client/assets/maps/Isometric/chestClosed_W.png
new file mode 100644
index 0000000..36902a4
Binary files /dev/null and b/client/assets/maps/Isometric/chestClosed_W.png differ
diff --git a/client/assets/maps/Isometric/chestOpen_E.png b/client/assets/maps/Isometric/chestOpen_E.png
new file mode 100644
index 0000000..9e79846
Binary files /dev/null and b/client/assets/maps/Isometric/chestOpen_E.png differ
diff --git a/client/assets/maps/Isometric/chestOpen_N.png b/client/assets/maps/Isometric/chestOpen_N.png
new file mode 100644
index 0000000..6e3da75
Binary files /dev/null and b/client/assets/maps/Isometric/chestOpen_N.png differ
diff --git a/client/assets/maps/Isometric/chestOpen_S.png b/client/assets/maps/Isometric/chestOpen_S.png
new file mode 100644
index 0000000..55661fb
Binary files /dev/null and b/client/assets/maps/Isometric/chestOpen_S.png differ
diff --git a/client/assets/maps/Isometric/chestOpen_W.png b/client/assets/maps/Isometric/chestOpen_W.png
new file mode 100644
index 0000000..13a66eb
Binary files /dev/null and b/client/assets/maps/Isometric/chestOpen_W.png differ
diff --git a/client/assets/maps/Isometric/dirtTiles_E.png b/client/assets/maps/Isometric/dirtTiles_E.png
new file mode 100644
index 0000000..a880528
Binary files /dev/null and b/client/assets/maps/Isometric/dirtTiles_E.png differ
diff --git a/client/assets/maps/Isometric/dirtTiles_N.png b/client/assets/maps/Isometric/dirtTiles_N.png
new file mode 100644
index 0000000..f76016d
Binary files /dev/null and b/client/assets/maps/Isometric/dirtTiles_N.png differ
diff --git a/client/assets/maps/Isometric/dirtTiles_S.png b/client/assets/maps/Isometric/dirtTiles_S.png
new file mode 100644
index 0000000..2bdbeb8
Binary files /dev/null and b/client/assets/maps/Isometric/dirtTiles_S.png differ
diff --git a/client/assets/maps/Isometric/dirtTiles_W.png b/client/assets/maps/Isometric/dirtTiles_W.png
new file mode 100644
index 0000000..9075736
Binary files /dev/null and b/client/assets/maps/Isometric/dirtTiles_W.png differ
diff --git a/client/assets/maps/Isometric/dirt_E.png b/client/assets/maps/Isometric/dirt_E.png
new file mode 100644
index 0000000..e121ed1
Binary files /dev/null and b/client/assets/maps/Isometric/dirt_E.png differ
diff --git a/client/assets/maps/Isometric/dirt_N.png b/client/assets/maps/Isometric/dirt_N.png
new file mode 100644
index 0000000..e3f78a7
Binary files /dev/null and b/client/assets/maps/Isometric/dirt_N.png differ
diff --git a/client/assets/maps/Isometric/dirt_S.png b/client/assets/maps/Isometric/dirt_S.png
new file mode 100644
index 0000000..1744c2f
Binary files /dev/null and b/client/assets/maps/Isometric/dirt_S.png differ
diff --git a/client/assets/maps/Isometric/dirt_W.png b/client/assets/maps/Isometric/dirt_W.png
new file mode 100644
index 0000000..cd50587
Binary files /dev/null and b/client/assets/maps/Isometric/dirt_W.png differ
diff --git a/client/assets/maps/Isometric/planksBroken_E.png b/client/assets/maps/Isometric/planksBroken_E.png
new file mode 100644
index 0000000..826ee82
Binary files /dev/null and b/client/assets/maps/Isometric/planksBroken_E.png differ
diff --git a/client/assets/maps/Isometric/planksBroken_N.png b/client/assets/maps/Isometric/planksBroken_N.png
new file mode 100644
index 0000000..732312c
Binary files /dev/null and b/client/assets/maps/Isometric/planksBroken_N.png differ
diff --git a/client/assets/maps/Isometric/planksBroken_S.png b/client/assets/maps/Isometric/planksBroken_S.png
new file mode 100644
index 0000000..911605c
Binary files /dev/null and b/client/assets/maps/Isometric/planksBroken_S.png differ
diff --git a/client/assets/maps/Isometric/planksBroken_W.png b/client/assets/maps/Isometric/planksBroken_W.png
new file mode 100644
index 0000000..a5d143d
Binary files /dev/null and b/client/assets/maps/Isometric/planksBroken_W.png differ
diff --git a/client/assets/maps/Isometric/planksHole_E.png b/client/assets/maps/Isometric/planksHole_E.png
new file mode 100644
index 0000000..67f35df
Binary files /dev/null and b/client/assets/maps/Isometric/planksHole_E.png differ
diff --git a/client/assets/maps/Isometric/planksHole_N.png b/client/assets/maps/Isometric/planksHole_N.png
new file mode 100644
index 0000000..1d92f40
Binary files /dev/null and b/client/assets/maps/Isometric/planksHole_N.png differ
diff --git a/client/assets/maps/Isometric/planksHole_S.png b/client/assets/maps/Isometric/planksHole_S.png
new file mode 100644
index 0000000..c757e71
Binary files /dev/null and b/client/assets/maps/Isometric/planksHole_S.png differ
diff --git a/client/assets/maps/Isometric/planksHole_W.png b/client/assets/maps/Isometric/planksHole_W.png
new file mode 100644
index 0000000..367201f
Binary files /dev/null and b/client/assets/maps/Isometric/planksHole_W.png differ
diff --git a/client/assets/maps/Isometric/planks_E.png b/client/assets/maps/Isometric/planks_E.png
new file mode 100644
index 0000000..7d51caa
Binary files /dev/null and b/client/assets/maps/Isometric/planks_E.png differ
diff --git a/client/assets/maps/Isometric/planks_N.png b/client/assets/maps/Isometric/planks_N.png
new file mode 100644
index 0000000..5072b43
Binary files /dev/null and b/client/assets/maps/Isometric/planks_N.png differ
diff --git a/client/assets/maps/Isometric/planks_S.png b/client/assets/maps/Isometric/planks_S.png
new file mode 100644
index 0000000..ac0f55e
Binary files /dev/null and b/client/assets/maps/Isometric/planks_S.png differ
diff --git a/client/assets/maps/Isometric/planks_W.png b/client/assets/maps/Isometric/planks_W.png
new file mode 100644
index 0000000..1c7b91d
Binary files /dev/null and b/client/assets/maps/Isometric/planks_W.png differ
diff --git a/client/assets/maps/Isometric/stairsAged_E.png b/client/assets/maps/Isometric/stairsAged_E.png
new file mode 100644
index 0000000..b04fdc0
Binary files /dev/null and b/client/assets/maps/Isometric/stairsAged_E.png differ
diff --git a/client/assets/maps/Isometric/stairsAged_N.png b/client/assets/maps/Isometric/stairsAged_N.png
new file mode 100644
index 0000000..55d2a6f
Binary files /dev/null and b/client/assets/maps/Isometric/stairsAged_N.png differ
diff --git a/client/assets/maps/Isometric/stairsAged_S.png b/client/assets/maps/Isometric/stairsAged_S.png
new file mode 100644
index 0000000..2f92129
Binary files /dev/null and b/client/assets/maps/Isometric/stairsAged_S.png differ
diff --git a/client/assets/maps/Isometric/stairsAged_W.png b/client/assets/maps/Isometric/stairsAged_W.png
new file mode 100644
index 0000000..976cbb8
Binary files /dev/null and b/client/assets/maps/Isometric/stairsAged_W.png differ
diff --git a/client/assets/maps/Isometric/stairsCorner_E.png b/client/assets/maps/Isometric/stairsCorner_E.png
new file mode 100644
index 0000000..38d3b23
Binary files /dev/null and b/client/assets/maps/Isometric/stairsCorner_E.png differ
diff --git a/client/assets/maps/Isometric/stairsCorner_N.png b/client/assets/maps/Isometric/stairsCorner_N.png
new file mode 100644
index 0000000..2d4c1d1
Binary files /dev/null and b/client/assets/maps/Isometric/stairsCorner_N.png differ
diff --git a/client/assets/maps/Isometric/stairsCorner_S.png b/client/assets/maps/Isometric/stairsCorner_S.png
new file mode 100644
index 0000000..6e00823
Binary files /dev/null and b/client/assets/maps/Isometric/stairsCorner_S.png differ
diff --git a/client/assets/maps/Isometric/stairsCorner_W.png b/client/assets/maps/Isometric/stairsCorner_W.png
new file mode 100644
index 0000000..8dfd8d4
Binary files /dev/null and b/client/assets/maps/Isometric/stairsCorner_W.png differ
diff --git a/client/assets/maps/Isometric/stairsSpiral_E.png b/client/assets/maps/Isometric/stairsSpiral_E.png
new file mode 100644
index 0000000..a83961b
Binary files /dev/null and b/client/assets/maps/Isometric/stairsSpiral_E.png differ
diff --git a/client/assets/maps/Isometric/stairsSpiral_N.png b/client/assets/maps/Isometric/stairsSpiral_N.png
new file mode 100644
index 0000000..353fff3
Binary files /dev/null and b/client/assets/maps/Isometric/stairsSpiral_N.png differ
diff --git a/client/assets/maps/Isometric/stairsSpiral_S.png b/client/assets/maps/Isometric/stairsSpiral_S.png
new file mode 100644
index 0000000..ab6e69c
Binary files /dev/null and b/client/assets/maps/Isometric/stairsSpiral_S.png differ
diff --git a/client/assets/maps/Isometric/stairsSpiral_W.png b/client/assets/maps/Isometric/stairsSpiral_W.png
new file mode 100644
index 0000000..d02324d
Binary files /dev/null and b/client/assets/maps/Isometric/stairsSpiral_W.png differ
diff --git a/client/assets/maps/Isometric/stairs_E.png b/client/assets/maps/Isometric/stairs_E.png
new file mode 100644
index 0000000..d2a75a8
Binary files /dev/null and b/client/assets/maps/Isometric/stairs_E.png differ
diff --git a/client/assets/maps/Isometric/stairs_N.png b/client/assets/maps/Isometric/stairs_N.png
new file mode 100644
index 0000000..cc72f45
Binary files /dev/null and b/client/assets/maps/Isometric/stairs_N.png differ
diff --git a/client/assets/maps/Isometric/stairs_S.png b/client/assets/maps/Isometric/stairs_S.png
new file mode 100644
index 0000000..b964acd
Binary files /dev/null and b/client/assets/maps/Isometric/stairs_S.png differ
diff --git a/client/assets/maps/Isometric/stairs_W.png b/client/assets/maps/Isometric/stairs_W.png
new file mode 100644
index 0000000..df4b6f4
Binary files /dev/null and b/client/assets/maps/Isometric/stairs_W.png differ
diff --git a/client/assets/maps/Isometric/stoneColumnWood_E.png b/client/assets/maps/Isometric/stoneColumnWood_E.png
new file mode 100644
index 0000000..f47031a
Binary files /dev/null and b/client/assets/maps/Isometric/stoneColumnWood_E.png differ
diff --git a/client/assets/maps/Isometric/stoneColumnWood_N.png b/client/assets/maps/Isometric/stoneColumnWood_N.png
new file mode 100644
index 0000000..f3da58f
Binary files /dev/null and b/client/assets/maps/Isometric/stoneColumnWood_N.png differ
diff --git a/client/assets/maps/Isometric/stoneColumnWood_S.png b/client/assets/maps/Isometric/stoneColumnWood_S.png
new file mode 100644
index 0000000..3013017
Binary files /dev/null and b/client/assets/maps/Isometric/stoneColumnWood_S.png differ
diff --git a/client/assets/maps/Isometric/stoneColumnWood_W.png b/client/assets/maps/Isometric/stoneColumnWood_W.png
new file mode 100644
index 0000000..a0eed17
Binary files /dev/null and b/client/assets/maps/Isometric/stoneColumnWood_W.png differ
diff --git a/client/assets/maps/Isometric/stoneColumn_E.png b/client/assets/maps/Isometric/stoneColumn_E.png
new file mode 100644
index 0000000..d0d7dc9
Binary files /dev/null and b/client/assets/maps/Isometric/stoneColumn_E.png differ
diff --git a/client/assets/maps/Isometric/stoneColumn_N.png b/client/assets/maps/Isometric/stoneColumn_N.png
new file mode 100644
index 0000000..c153f57
Binary files /dev/null and b/client/assets/maps/Isometric/stoneColumn_N.png differ
diff --git a/client/assets/maps/Isometric/stoneColumn_S.png b/client/assets/maps/Isometric/stoneColumn_S.png
new file mode 100644
index 0000000..9f4b175
Binary files /dev/null and b/client/assets/maps/Isometric/stoneColumn_S.png differ
diff --git a/client/assets/maps/Isometric/stoneColumn_W.png b/client/assets/maps/Isometric/stoneColumn_W.png
new file mode 100644
index 0000000..6279b95
Binary files /dev/null and b/client/assets/maps/Isometric/stoneColumn_W.png differ
diff --git a/client/assets/maps/Isometric/stoneCorner_E.png b/client/assets/maps/Isometric/stoneCorner_E.png
new file mode 100644
index 0000000..9dc99de
Binary files /dev/null and b/client/assets/maps/Isometric/stoneCorner_E.png differ
diff --git a/client/assets/maps/Isometric/stoneCorner_N.png b/client/assets/maps/Isometric/stoneCorner_N.png
new file mode 100644
index 0000000..c4f6d67
Binary files /dev/null and b/client/assets/maps/Isometric/stoneCorner_N.png differ
diff --git a/client/assets/maps/Isometric/stoneCorner_S.png b/client/assets/maps/Isometric/stoneCorner_S.png
new file mode 100644
index 0000000..960ade4
Binary files /dev/null and b/client/assets/maps/Isometric/stoneCorner_S.png differ
diff --git a/client/assets/maps/Isometric/stoneCorner_W.png b/client/assets/maps/Isometric/stoneCorner_W.png
new file mode 100644
index 0000000..42294d9
Binary files /dev/null and b/client/assets/maps/Isometric/stoneCorner_W.png differ
diff --git a/client/assets/maps/Isometric/stoneInset_E.png b/client/assets/maps/Isometric/stoneInset_E.png
new file mode 100644
index 0000000..d3ec462
Binary files /dev/null and b/client/assets/maps/Isometric/stoneInset_E.png differ
diff --git a/client/assets/maps/Isometric/stoneInset_N.png b/client/assets/maps/Isometric/stoneInset_N.png
new file mode 100644
index 0000000..58aa8af
Binary files /dev/null and b/client/assets/maps/Isometric/stoneInset_N.png differ
diff --git a/client/assets/maps/Isometric/stoneInset_S.png b/client/assets/maps/Isometric/stoneInset_S.png
new file mode 100644
index 0000000..50dfe1f
Binary files /dev/null and b/client/assets/maps/Isometric/stoneInset_S.png differ
diff --git a/client/assets/maps/Isometric/stoneInset_W.png b/client/assets/maps/Isometric/stoneInset_W.png
new file mode 100644
index 0000000..dc2d12a
Binary files /dev/null and b/client/assets/maps/Isometric/stoneInset_W.png differ
diff --git a/client/assets/maps/Isometric/stoneLeft_E.png b/client/assets/maps/Isometric/stoneLeft_E.png
new file mode 100644
index 0000000..097a611
Binary files /dev/null and b/client/assets/maps/Isometric/stoneLeft_E.png differ
diff --git a/client/assets/maps/Isometric/stoneLeft_N.png b/client/assets/maps/Isometric/stoneLeft_N.png
new file mode 100644
index 0000000..e72b5d6
Binary files /dev/null and b/client/assets/maps/Isometric/stoneLeft_N.png differ
diff --git a/client/assets/maps/Isometric/stoneLeft_S.png b/client/assets/maps/Isometric/stoneLeft_S.png
new file mode 100644
index 0000000..a8a1e7b
Binary files /dev/null and b/client/assets/maps/Isometric/stoneLeft_S.png differ
diff --git a/client/assets/maps/Isometric/stoneLeft_W.png b/client/assets/maps/Isometric/stoneLeft_W.png
new file mode 100644
index 0000000..66b3ed2
Binary files /dev/null and b/client/assets/maps/Isometric/stoneLeft_W.png differ
diff --git a/client/assets/maps/Isometric/stoneMissingTiles_E.png b/client/assets/maps/Isometric/stoneMissingTiles_E.png
new file mode 100644
index 0000000..1d34564
Binary files /dev/null and b/client/assets/maps/Isometric/stoneMissingTiles_E.png differ
diff --git a/client/assets/maps/Isometric/stoneMissingTiles_N.png b/client/assets/maps/Isometric/stoneMissingTiles_N.png
new file mode 100644
index 0000000..b28bc4f
Binary files /dev/null and b/client/assets/maps/Isometric/stoneMissingTiles_N.png differ
diff --git a/client/assets/maps/Isometric/stoneMissingTiles_S.png b/client/assets/maps/Isometric/stoneMissingTiles_S.png
new file mode 100644
index 0000000..062a4cb
Binary files /dev/null and b/client/assets/maps/Isometric/stoneMissingTiles_S.png differ
diff --git a/client/assets/maps/Isometric/stoneMissingTiles_W.png b/client/assets/maps/Isometric/stoneMissingTiles_W.png
new file mode 100644
index 0000000..9c91607
Binary files /dev/null and b/client/assets/maps/Isometric/stoneMissingTiles_W.png differ
diff --git a/client/assets/maps/Isometric/stoneRight_E.png b/client/assets/maps/Isometric/stoneRight_E.png
new file mode 100644
index 0000000..b168ec0
Binary files /dev/null and b/client/assets/maps/Isometric/stoneRight_E.png differ
diff --git a/client/assets/maps/Isometric/stoneRight_N.png b/client/assets/maps/Isometric/stoneRight_N.png
new file mode 100644
index 0000000..7db312f
Binary files /dev/null and b/client/assets/maps/Isometric/stoneRight_N.png differ
diff --git a/client/assets/maps/Isometric/stoneRight_S.png b/client/assets/maps/Isometric/stoneRight_S.png
new file mode 100644
index 0000000..a77e1f7
Binary files /dev/null and b/client/assets/maps/Isometric/stoneRight_S.png differ
diff --git a/client/assets/maps/Isometric/stoneRight_W.png b/client/assets/maps/Isometric/stoneRight_W.png
new file mode 100644
index 0000000..a5658bc
Binary files /dev/null and b/client/assets/maps/Isometric/stoneRight_W.png differ
diff --git a/client/assets/maps/Isometric/stoneSideUneven_E.png b/client/assets/maps/Isometric/stoneSideUneven_E.png
new file mode 100644
index 0000000..85748f4
Binary files /dev/null and b/client/assets/maps/Isometric/stoneSideUneven_E.png differ
diff --git a/client/assets/maps/Isometric/stoneSideUneven_N.png b/client/assets/maps/Isometric/stoneSideUneven_N.png
new file mode 100644
index 0000000..9219756
Binary files /dev/null and b/client/assets/maps/Isometric/stoneSideUneven_N.png differ
diff --git a/client/assets/maps/Isometric/stoneSideUneven_S.png b/client/assets/maps/Isometric/stoneSideUneven_S.png
new file mode 100644
index 0000000..0c0a8b7
Binary files /dev/null and b/client/assets/maps/Isometric/stoneSideUneven_S.png differ
diff --git a/client/assets/maps/Isometric/stoneSideUneven_W.png b/client/assets/maps/Isometric/stoneSideUneven_W.png
new file mode 100644
index 0000000..d4d0381
Binary files /dev/null and b/client/assets/maps/Isometric/stoneSideUneven_W.png differ
diff --git a/client/assets/maps/Isometric/stoneSide_E.png b/client/assets/maps/Isometric/stoneSide_E.png
new file mode 100644
index 0000000..9c5f4c2
Binary files /dev/null and b/client/assets/maps/Isometric/stoneSide_E.png differ
diff --git a/client/assets/maps/Isometric/stoneSide_N.png b/client/assets/maps/Isometric/stoneSide_N.png
new file mode 100644
index 0000000..c97b823
Binary files /dev/null and b/client/assets/maps/Isometric/stoneSide_N.png differ
diff --git a/client/assets/maps/Isometric/stoneSide_S.png b/client/assets/maps/Isometric/stoneSide_S.png
new file mode 100644
index 0000000..541f30c
Binary files /dev/null and b/client/assets/maps/Isometric/stoneSide_S.png differ
diff --git a/client/assets/maps/Isometric/stoneSide_W.png b/client/assets/maps/Isometric/stoneSide_W.png
new file mode 100644
index 0000000..5583d02
Binary files /dev/null and b/client/assets/maps/Isometric/stoneSide_W.png differ
diff --git a/client/assets/maps/Isometric/stoneStep_E.png b/client/assets/maps/Isometric/stoneStep_E.png
new file mode 100644
index 0000000..551a2ad
Binary files /dev/null and b/client/assets/maps/Isometric/stoneStep_E.png differ
diff --git a/client/assets/maps/Isometric/stoneStep_N.png b/client/assets/maps/Isometric/stoneStep_N.png
new file mode 100644
index 0000000..1ec600f
Binary files /dev/null and b/client/assets/maps/Isometric/stoneStep_N.png differ
diff --git a/client/assets/maps/Isometric/stoneStep_S.png b/client/assets/maps/Isometric/stoneStep_S.png
new file mode 100644
index 0000000..591541f
Binary files /dev/null and b/client/assets/maps/Isometric/stoneStep_S.png differ
diff --git a/client/assets/maps/Isometric/stoneStep_W.png b/client/assets/maps/Isometric/stoneStep_W.png
new file mode 100644
index 0000000..7108df8
Binary files /dev/null and b/client/assets/maps/Isometric/stoneStep_W.png differ
diff --git a/client/assets/maps/Isometric/stoneSteps_E.png b/client/assets/maps/Isometric/stoneSteps_E.png
new file mode 100644
index 0000000..c63eb3c
Binary files /dev/null and b/client/assets/maps/Isometric/stoneSteps_E.png differ
diff --git a/client/assets/maps/Isometric/stoneSteps_N.png b/client/assets/maps/Isometric/stoneSteps_N.png
new file mode 100644
index 0000000..c953187
Binary files /dev/null and b/client/assets/maps/Isometric/stoneSteps_N.png differ
diff --git a/client/assets/maps/Isometric/stoneSteps_S.png b/client/assets/maps/Isometric/stoneSteps_S.png
new file mode 100644
index 0000000..4f1c32e
Binary files /dev/null and b/client/assets/maps/Isometric/stoneSteps_S.png differ
diff --git a/client/assets/maps/Isometric/stoneSteps_W.png b/client/assets/maps/Isometric/stoneSteps_W.png
new file mode 100644
index 0000000..c2c0338
Binary files /dev/null and b/client/assets/maps/Isometric/stoneSteps_W.png differ
diff --git a/client/assets/maps/Isometric/stoneTile_E.png b/client/assets/maps/Isometric/stoneTile_E.png
new file mode 100644
index 0000000..6f00fe6
Binary files /dev/null and b/client/assets/maps/Isometric/stoneTile_E.png differ
diff --git a/client/assets/maps/Isometric/stoneTile_N.png b/client/assets/maps/Isometric/stoneTile_N.png
new file mode 100644
index 0000000..27af0c8
Binary files /dev/null and b/client/assets/maps/Isometric/stoneTile_N.png differ
diff --git a/client/assets/maps/Isometric/stoneTile_S.png b/client/assets/maps/Isometric/stoneTile_S.png
new file mode 100644
index 0000000..4ca4505
Binary files /dev/null and b/client/assets/maps/Isometric/stoneTile_S.png differ
diff --git a/client/assets/maps/Isometric/stoneTile_W.png b/client/assets/maps/Isometric/stoneTile_W.png
new file mode 100644
index 0000000..41e3250
Binary files /dev/null and b/client/assets/maps/Isometric/stoneTile_W.png differ
diff --git a/client/assets/maps/Isometric/stoneUneven_E.png b/client/assets/maps/Isometric/stoneUneven_E.png
new file mode 100644
index 0000000..c9eafb4
Binary files /dev/null and b/client/assets/maps/Isometric/stoneUneven_E.png differ
diff --git a/client/assets/maps/Isometric/stoneUneven_N.png b/client/assets/maps/Isometric/stoneUneven_N.png
new file mode 100644
index 0000000..4b884bd
Binary files /dev/null and b/client/assets/maps/Isometric/stoneUneven_N.png differ
diff --git a/client/assets/maps/Isometric/stoneUneven_S.png b/client/assets/maps/Isometric/stoneUneven_S.png
new file mode 100644
index 0000000..c56dbf1
Binary files /dev/null and b/client/assets/maps/Isometric/stoneUneven_S.png differ
diff --git a/client/assets/maps/Isometric/stoneUneven_W.png b/client/assets/maps/Isometric/stoneUneven_W.png
new file mode 100644
index 0000000..319537b
Binary files /dev/null and b/client/assets/maps/Isometric/stoneUneven_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallAgedLeft_E.png b/client/assets/maps/Isometric/stoneWallAgedLeft_E.png
new file mode 100644
index 0000000..010a3db
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallAgedLeft_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallAgedLeft_N.png b/client/assets/maps/Isometric/stoneWallAgedLeft_N.png
new file mode 100644
index 0000000..39e48d3
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallAgedLeft_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallAgedLeft_S.png b/client/assets/maps/Isometric/stoneWallAgedLeft_S.png
new file mode 100644
index 0000000..61079e9
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallAgedLeft_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallAgedLeft_W.png b/client/assets/maps/Isometric/stoneWallAgedLeft_W.png
new file mode 100644
index 0000000..0feafcb
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallAgedLeft_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallAgedRight_E.png b/client/assets/maps/Isometric/stoneWallAgedRight_E.png
new file mode 100644
index 0000000..6cd5c4c
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallAgedRight_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallAgedRight_N.png b/client/assets/maps/Isometric/stoneWallAgedRight_N.png
new file mode 100644
index 0000000..c7b6621
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallAgedRight_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallAgedRight_S.png b/client/assets/maps/Isometric/stoneWallAgedRight_S.png
new file mode 100644
index 0000000..fbf61ab
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallAgedRight_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallAgedRight_W.png b/client/assets/maps/Isometric/stoneWallAgedRight_W.png
new file mode 100644
index 0000000..08ad0c2
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallAgedRight_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallAged_E.png b/client/assets/maps/Isometric/stoneWallAged_E.png
new file mode 100644
index 0000000..c850826
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallAged_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallAged_N.png b/client/assets/maps/Isometric/stoneWallAged_N.png
new file mode 100644
index 0000000..fbbf0d6
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallAged_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallAged_S.png b/client/assets/maps/Isometric/stoneWallAged_S.png
new file mode 100644
index 0000000..8bc2c04
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallAged_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallAged_W.png b/client/assets/maps/Isometric/stoneWallAged_W.png
new file mode 100644
index 0000000..9cb6b50
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallAged_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallArchway_E.png b/client/assets/maps/Isometric/stoneWallArchway_E.png
new file mode 100644
index 0000000..9c670df
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallArchway_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallArchway_N.png b/client/assets/maps/Isometric/stoneWallArchway_N.png
new file mode 100644
index 0000000..b2fa8e4
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallArchway_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallArchway_S.png b/client/assets/maps/Isometric/stoneWallArchway_S.png
new file mode 100644
index 0000000..7819e8b
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallArchway_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallArchway_W.png b/client/assets/maps/Isometric/stoneWallArchway_W.png
new file mode 100644
index 0000000..d4871fd
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallArchway_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallBrokenLeft_E.png b/client/assets/maps/Isometric/stoneWallBrokenLeft_E.png
new file mode 100644
index 0000000..2d575a4
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallBrokenLeft_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallBrokenLeft_N.png b/client/assets/maps/Isometric/stoneWallBrokenLeft_N.png
new file mode 100644
index 0000000..c09e525
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallBrokenLeft_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallBrokenLeft_S.png b/client/assets/maps/Isometric/stoneWallBrokenLeft_S.png
new file mode 100644
index 0000000..f726c75
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallBrokenLeft_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallBrokenLeft_W.png b/client/assets/maps/Isometric/stoneWallBrokenLeft_W.png
new file mode 100644
index 0000000..2023ef9
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallBrokenLeft_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallBrokenRight_E.png b/client/assets/maps/Isometric/stoneWallBrokenRight_E.png
new file mode 100644
index 0000000..fe68156
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallBrokenRight_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallBrokenRight_N.png b/client/assets/maps/Isometric/stoneWallBrokenRight_N.png
new file mode 100644
index 0000000..20dfd51
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallBrokenRight_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallBrokenRight_S.png b/client/assets/maps/Isometric/stoneWallBrokenRight_S.png
new file mode 100644
index 0000000..c2c7b3b
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallBrokenRight_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallBrokenRight_W.png b/client/assets/maps/Isometric/stoneWallBrokenRight_W.png
new file mode 100644
index 0000000..c7d79fb
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallBrokenRight_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallBroken_E.png b/client/assets/maps/Isometric/stoneWallBroken_E.png
new file mode 100644
index 0000000..abd63f6
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallBroken_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallBroken_N.png b/client/assets/maps/Isometric/stoneWallBroken_N.png
new file mode 100644
index 0000000..0758cb7
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallBroken_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallBroken_S.png b/client/assets/maps/Isometric/stoneWallBroken_S.png
new file mode 100644
index 0000000..8785cf4
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallBroken_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallBroken_W.png b/client/assets/maps/Isometric/stoneWallBroken_W.png
new file mode 100644
index 0000000..f514520
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallBroken_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallColumnIn_E.png b/client/assets/maps/Isometric/stoneWallColumnIn_E.png
new file mode 100644
index 0000000..fc39750
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallColumnIn_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallColumnIn_N.png b/client/assets/maps/Isometric/stoneWallColumnIn_N.png
new file mode 100644
index 0000000..a7b3091
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallColumnIn_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallColumnIn_S.png b/client/assets/maps/Isometric/stoneWallColumnIn_S.png
new file mode 100644
index 0000000..df88ba4
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallColumnIn_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallColumnIn_W.png b/client/assets/maps/Isometric/stoneWallColumnIn_W.png
new file mode 100644
index 0000000..38d3ff3
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallColumnIn_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallColumn_E.png b/client/assets/maps/Isometric/stoneWallColumn_E.png
new file mode 100644
index 0000000..a63d8e8
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallColumn_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallColumn_N.png b/client/assets/maps/Isometric/stoneWallColumn_N.png
new file mode 100644
index 0000000..660c7c2
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallColumn_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallColumn_S.png b/client/assets/maps/Isometric/stoneWallColumn_S.png
new file mode 100644
index 0000000..38705e4
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallColumn_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallColumn_W.png b/client/assets/maps/Isometric/stoneWallColumn_W.png
new file mode 100644
index 0000000..c6386a6
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallColumn_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallCorner_E.png b/client/assets/maps/Isometric/stoneWallCorner_E.png
new file mode 100644
index 0000000..e7ef38f
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallCorner_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallCorner_N.png b/client/assets/maps/Isometric/stoneWallCorner_N.png
new file mode 100644
index 0000000..7f7dffd
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallCorner_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallCorner_S.png b/client/assets/maps/Isometric/stoneWallCorner_S.png
new file mode 100644
index 0000000..defb582
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallCorner_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallCorner_W.png b/client/assets/maps/Isometric/stoneWallCorner_W.png
new file mode 100644
index 0000000..0964303
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallCorner_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoorBars_E.png b/client/assets/maps/Isometric/stoneWallDoorBars_E.png
new file mode 100644
index 0000000..aa1db6f
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoorBars_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoorBars_N.png b/client/assets/maps/Isometric/stoneWallDoorBars_N.png
new file mode 100644
index 0000000..6abc5b0
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoorBars_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoorBars_S.png b/client/assets/maps/Isometric/stoneWallDoorBars_S.png
new file mode 100644
index 0000000..dbacc81
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoorBars_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoorBars_W.png b/client/assets/maps/Isometric/stoneWallDoorBars_W.png
new file mode 100644
index 0000000..4cdb117
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoorBars_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoorClosed_E.png b/client/assets/maps/Isometric/stoneWallDoorClosed_E.png
new file mode 100644
index 0000000..0972124
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoorClosed_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoorClosed_N.png b/client/assets/maps/Isometric/stoneWallDoorClosed_N.png
new file mode 100644
index 0000000..4cdfdf3
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoorClosed_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoorClosed_S.png b/client/assets/maps/Isometric/stoneWallDoorClosed_S.png
new file mode 100644
index 0000000..3c972df
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoorClosed_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoorClosed_W.png b/client/assets/maps/Isometric/stoneWallDoorClosed_W.png
new file mode 100644
index 0000000..fdf9a76
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoorClosed_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoorOpen_E.png b/client/assets/maps/Isometric/stoneWallDoorOpen_E.png
new file mode 100644
index 0000000..d38ce46
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoorOpen_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoorOpen_N.png b/client/assets/maps/Isometric/stoneWallDoorOpen_N.png
new file mode 100644
index 0000000..a5140dc
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoorOpen_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoorOpen_S.png b/client/assets/maps/Isometric/stoneWallDoorOpen_S.png
new file mode 100644
index 0000000..e5c7fdc
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoorOpen_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoorOpen_W.png b/client/assets/maps/Isometric/stoneWallDoorOpen_W.png
new file mode 100644
index 0000000..22819c2
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoorOpen_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoor_E.png b/client/assets/maps/Isometric/stoneWallDoor_E.png
new file mode 100644
index 0000000..f7a3861
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoor_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoor_N.png b/client/assets/maps/Isometric/stoneWallDoor_N.png
new file mode 100644
index 0000000..3aa18a6
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoor_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoor_S.png b/client/assets/maps/Isometric/stoneWallDoor_S.png
new file mode 100644
index 0000000..0753aaa
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoor_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallDoor_W.png b/client/assets/maps/Isometric/stoneWallDoor_W.png
new file mode 100644
index 0000000..1109347
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallDoor_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGateBars_E.png b/client/assets/maps/Isometric/stoneWallGateBars_E.png
new file mode 100644
index 0000000..1f277e8
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGateBars_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGateBars_N.png b/client/assets/maps/Isometric/stoneWallGateBars_N.png
new file mode 100644
index 0000000..0c3969a
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGateBars_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGateBars_S.png b/client/assets/maps/Isometric/stoneWallGateBars_S.png
new file mode 100644
index 0000000..edc9e49
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGateBars_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGateBars_W.png b/client/assets/maps/Isometric/stoneWallGateBars_W.png
new file mode 100644
index 0000000..f55adb2
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGateBars_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGateClosed_E.png b/client/assets/maps/Isometric/stoneWallGateClosed_E.png
new file mode 100644
index 0000000..3e765ed
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGateClosed_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGateClosed_N.png b/client/assets/maps/Isometric/stoneWallGateClosed_N.png
new file mode 100644
index 0000000..0229c48
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGateClosed_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGateClosed_S.png b/client/assets/maps/Isometric/stoneWallGateClosed_S.png
new file mode 100644
index 0000000..bd29711
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGateClosed_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGateClosed_W.png b/client/assets/maps/Isometric/stoneWallGateClosed_W.png
new file mode 100644
index 0000000..977f511
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGateClosed_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGateOpen_E.png b/client/assets/maps/Isometric/stoneWallGateOpen_E.png
new file mode 100644
index 0000000..22621cd
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGateOpen_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGateOpen_N.png b/client/assets/maps/Isometric/stoneWallGateOpen_N.png
new file mode 100644
index 0000000..6a56a0e
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGateOpen_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGateOpen_S.png b/client/assets/maps/Isometric/stoneWallGateOpen_S.png
new file mode 100644
index 0000000..f4cfe28
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGateOpen_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGateOpen_W.png b/client/assets/maps/Isometric/stoneWallGateOpen_W.png
new file mode 100644
index 0000000..67d7b61
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGateOpen_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGate_E.png b/client/assets/maps/Isometric/stoneWallGate_E.png
new file mode 100644
index 0000000..1f2fe88
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGate_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGate_N.png b/client/assets/maps/Isometric/stoneWallGate_N.png
new file mode 100644
index 0000000..3707af5
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGate_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGate_S.png b/client/assets/maps/Isometric/stoneWallGate_S.png
new file mode 100644
index 0000000..61865ba
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGate_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallGate_W.png b/client/assets/maps/Isometric/stoneWallGate_W.png
new file mode 100644
index 0000000..7effe39
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallGate_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallHalf_E.png b/client/assets/maps/Isometric/stoneWallHalf_E.png
new file mode 100644
index 0000000..292bcae
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallHalf_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallHalf_N.png b/client/assets/maps/Isometric/stoneWallHalf_N.png
new file mode 100644
index 0000000..3f1906f
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallHalf_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallHalf_S.png b/client/assets/maps/Isometric/stoneWallHalf_S.png
new file mode 100644
index 0000000..45f86de
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallHalf_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallHalf_W.png b/client/assets/maps/Isometric/stoneWallHalf_W.png
new file mode 100644
index 0000000..4de6b33
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallHalf_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallHole_E.png b/client/assets/maps/Isometric/stoneWallHole_E.png
new file mode 100644
index 0000000..872b087
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallHole_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallHole_N.png b/client/assets/maps/Isometric/stoneWallHole_N.png
new file mode 100644
index 0000000..9bd3eab
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallHole_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallHole_S.png b/client/assets/maps/Isometric/stoneWallHole_S.png
new file mode 100644
index 0000000..daebafd
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallHole_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallHole_W.png b/client/assets/maps/Isometric/stoneWallHole_W.png
new file mode 100644
index 0000000..79b2c82
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallHole_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallRoundBroken_E.png b/client/assets/maps/Isometric/stoneWallRoundBroken_E.png
new file mode 100644
index 0000000..a8dea57
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallRoundBroken_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallRoundBroken_N.png b/client/assets/maps/Isometric/stoneWallRoundBroken_N.png
new file mode 100644
index 0000000..2113cc6
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallRoundBroken_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallRoundBroken_S.png b/client/assets/maps/Isometric/stoneWallRoundBroken_S.png
new file mode 100644
index 0000000..cea849a
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallRoundBroken_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallRoundBroken_W.png b/client/assets/maps/Isometric/stoneWallRoundBroken_W.png
new file mode 100644
index 0000000..4badbb5
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallRoundBroken_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallRoundWindow_E.png b/client/assets/maps/Isometric/stoneWallRoundWindow_E.png
new file mode 100644
index 0000000..62f26ab
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallRoundWindow_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallRoundWindow_N.png b/client/assets/maps/Isometric/stoneWallRoundWindow_N.png
new file mode 100644
index 0000000..85dbc48
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallRoundWindow_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallRoundWindow_S.png b/client/assets/maps/Isometric/stoneWallRoundWindow_S.png
new file mode 100644
index 0000000..899e820
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallRoundWindow_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallRoundWindow_W.png b/client/assets/maps/Isometric/stoneWallRoundWindow_W.png
new file mode 100644
index 0000000..3e8e63d
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallRoundWindow_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallRound_E.png b/client/assets/maps/Isometric/stoneWallRound_E.png
new file mode 100644
index 0000000..38054f3
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallRound_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallRound_N.png b/client/assets/maps/Isometric/stoneWallRound_N.png
new file mode 100644
index 0000000..af4b965
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallRound_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallRound_S.png b/client/assets/maps/Isometric/stoneWallRound_S.png
new file mode 100644
index 0000000..c5791ab
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallRound_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallRound_W.png b/client/assets/maps/Isometric/stoneWallRound_W.png
new file mode 100644
index 0000000..a181d9a
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallRound_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallStructure_E.png b/client/assets/maps/Isometric/stoneWallStructure_E.png
new file mode 100644
index 0000000..aea286b
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallStructure_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallStructure_N.png b/client/assets/maps/Isometric/stoneWallStructure_N.png
new file mode 100644
index 0000000..01c5823
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallStructure_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallStructure_S.png b/client/assets/maps/Isometric/stoneWallStructure_S.png
new file mode 100644
index 0000000..a931456
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallStructure_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallStructure_W.png b/client/assets/maps/Isometric/stoneWallStructure_W.png
new file mode 100644
index 0000000..bec9c18
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallStructure_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallTop_E.png b/client/assets/maps/Isometric/stoneWallTop_E.png
new file mode 100644
index 0000000..e0a0d55
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallTop_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallTop_N.png b/client/assets/maps/Isometric/stoneWallTop_N.png
new file mode 100644
index 0000000..867e32d
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallTop_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallTop_S.png b/client/assets/maps/Isometric/stoneWallTop_S.png
new file mode 100644
index 0000000..5d5193e
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallTop_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallTop_W.png b/client/assets/maps/Isometric/stoneWallTop_W.png
new file mode 100644
index 0000000..fff0071
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallTop_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallWindowBars_E.png b/client/assets/maps/Isometric/stoneWallWindowBars_E.png
new file mode 100644
index 0000000..b04c3a9
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallWindowBars_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallWindowBars_N.png b/client/assets/maps/Isometric/stoneWallWindowBars_N.png
new file mode 100644
index 0000000..e7a6a80
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallWindowBars_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallWindowBars_S.png b/client/assets/maps/Isometric/stoneWallWindowBars_S.png
new file mode 100644
index 0000000..a4c6cc6
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallWindowBars_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallWindowBars_W.png b/client/assets/maps/Isometric/stoneWallWindowBars_W.png
new file mode 100644
index 0000000..f500d29
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallWindowBars_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWallWindow_E.png b/client/assets/maps/Isometric/stoneWallWindow_E.png
new file mode 100644
index 0000000..b633115
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallWindow_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWallWindow_N.png b/client/assets/maps/Isometric/stoneWallWindow_N.png
new file mode 100644
index 0000000..0a767ae
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallWindow_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWallWindow_S.png b/client/assets/maps/Isometric/stoneWallWindow_S.png
new file mode 100644
index 0000000..90e36fa
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallWindow_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWallWindow_W.png b/client/assets/maps/Isometric/stoneWallWindow_W.png
new file mode 100644
index 0000000..0aa4e40
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWallWindow_W.png differ
diff --git a/client/assets/maps/Isometric/stoneWall_E.png b/client/assets/maps/Isometric/stoneWall_E.png
new file mode 100644
index 0000000..fae9460
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWall_E.png differ
diff --git a/client/assets/maps/Isometric/stoneWall_N.png b/client/assets/maps/Isometric/stoneWall_N.png
new file mode 100644
index 0000000..4c3c669
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWall_N.png differ
diff --git a/client/assets/maps/Isometric/stoneWall_S.png b/client/assets/maps/Isometric/stoneWall_S.png
new file mode 100644
index 0000000..4ba7ecb
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWall_S.png differ
diff --git a/client/assets/maps/Isometric/stoneWall_W.png b/client/assets/maps/Isometric/stoneWall_W.png
new file mode 100644
index 0000000..664b658
Binary files /dev/null and b/client/assets/maps/Isometric/stoneWall_W.png differ
diff --git a/client/assets/maps/Isometric/stone_E.png b/client/assets/maps/Isometric/stone_E.png
new file mode 100644
index 0000000..e68410b
Binary files /dev/null and b/client/assets/maps/Isometric/stone_E.png differ
diff --git a/client/assets/maps/Isometric/stone_N.png b/client/assets/maps/Isometric/stone_N.png
new file mode 100644
index 0000000..304b265
Binary files /dev/null and b/client/assets/maps/Isometric/stone_N.png differ
diff --git a/client/assets/maps/Isometric/stone_S.png b/client/assets/maps/Isometric/stone_S.png
new file mode 100644
index 0000000..7901978
Binary files /dev/null and b/client/assets/maps/Isometric/stone_S.png differ
diff --git a/client/assets/maps/Isometric/stone_W.png b/client/assets/maps/Isometric/stone_W.png
new file mode 100644
index 0000000..f8fe5df
Binary files /dev/null and b/client/assets/maps/Isometric/stone_W.png differ
diff --git a/client/assets/maps/Isometric/tableChairsBroken_E.png b/client/assets/maps/Isometric/tableChairsBroken_E.png
new file mode 100644
index 0000000..48ae4e9
Binary files /dev/null and b/client/assets/maps/Isometric/tableChairsBroken_E.png differ
diff --git a/client/assets/maps/Isometric/tableChairsBroken_N.png b/client/assets/maps/Isometric/tableChairsBroken_N.png
new file mode 100644
index 0000000..0664956
Binary files /dev/null and b/client/assets/maps/Isometric/tableChairsBroken_N.png differ
diff --git a/client/assets/maps/Isometric/tableChairsBroken_S.png b/client/assets/maps/Isometric/tableChairsBroken_S.png
new file mode 100644
index 0000000..c463def
Binary files /dev/null and b/client/assets/maps/Isometric/tableChairsBroken_S.png differ
diff --git a/client/assets/maps/Isometric/tableChairsBroken_W.png b/client/assets/maps/Isometric/tableChairsBroken_W.png
new file mode 100644
index 0000000..940834c
Binary files /dev/null and b/client/assets/maps/Isometric/tableChairsBroken_W.png differ
diff --git a/client/assets/maps/Isometric/tableRoundChairs_E.png b/client/assets/maps/Isometric/tableRoundChairs_E.png
new file mode 100644
index 0000000..e49ad6f
Binary files /dev/null and b/client/assets/maps/Isometric/tableRoundChairs_E.png differ
diff --git a/client/assets/maps/Isometric/tableRoundChairs_N.png b/client/assets/maps/Isometric/tableRoundChairs_N.png
new file mode 100644
index 0000000..c30b935
Binary files /dev/null and b/client/assets/maps/Isometric/tableRoundChairs_N.png differ
diff --git a/client/assets/maps/Isometric/tableRoundChairs_S.png b/client/assets/maps/Isometric/tableRoundChairs_S.png
new file mode 100644
index 0000000..c9a7dca
Binary files /dev/null and b/client/assets/maps/Isometric/tableRoundChairs_S.png differ
diff --git a/client/assets/maps/Isometric/tableRoundChairs_W.png b/client/assets/maps/Isometric/tableRoundChairs_W.png
new file mode 100644
index 0000000..8974b47
Binary files /dev/null and b/client/assets/maps/Isometric/tableRoundChairs_W.png differ
diff --git a/client/assets/maps/Isometric/tableRoundItemsChairs_E.png b/client/assets/maps/Isometric/tableRoundItemsChairs_E.png
new file mode 100644
index 0000000..79f9c42
Binary files /dev/null and b/client/assets/maps/Isometric/tableRoundItemsChairs_E.png differ
diff --git a/client/assets/maps/Isometric/tableRoundItemsChairs_N.png b/client/assets/maps/Isometric/tableRoundItemsChairs_N.png
new file mode 100644
index 0000000..f30ac1e
Binary files /dev/null and b/client/assets/maps/Isometric/tableRoundItemsChairs_N.png differ
diff --git a/client/assets/maps/Isometric/tableRoundItemsChairs_S.png b/client/assets/maps/Isometric/tableRoundItemsChairs_S.png
new file mode 100644
index 0000000..16dd219
Binary files /dev/null and b/client/assets/maps/Isometric/tableRoundItemsChairs_S.png differ
diff --git a/client/assets/maps/Isometric/tableRoundItemsChairs_W.png b/client/assets/maps/Isometric/tableRoundItemsChairs_W.png
new file mode 100644
index 0000000..87d604f
Binary files /dev/null and b/client/assets/maps/Isometric/tableRoundItemsChairs_W.png differ
diff --git a/client/assets/maps/Isometric/tableRound_E.png b/client/assets/maps/Isometric/tableRound_E.png
new file mode 100644
index 0000000..bca77f3
Binary files /dev/null and b/client/assets/maps/Isometric/tableRound_E.png differ
diff --git a/client/assets/maps/Isometric/tableRound_N.png b/client/assets/maps/Isometric/tableRound_N.png
new file mode 100644
index 0000000..beca065
Binary files /dev/null and b/client/assets/maps/Isometric/tableRound_N.png differ
diff --git a/client/assets/maps/Isometric/tableRound_S.png b/client/assets/maps/Isometric/tableRound_S.png
new file mode 100644
index 0000000..4139d56
Binary files /dev/null and b/client/assets/maps/Isometric/tableRound_S.png differ
diff --git a/client/assets/maps/Isometric/tableRound_W.png b/client/assets/maps/Isometric/tableRound_W.png
new file mode 100644
index 0000000..127cfbd
Binary files /dev/null and b/client/assets/maps/Isometric/tableRound_W.png differ
diff --git a/client/assets/maps/Isometric/tableShortChairs_E.png b/client/assets/maps/Isometric/tableShortChairs_E.png
new file mode 100644
index 0000000..22574ab
Binary files /dev/null and b/client/assets/maps/Isometric/tableShortChairs_E.png differ
diff --git a/client/assets/maps/Isometric/tableShortChairs_N.png b/client/assets/maps/Isometric/tableShortChairs_N.png
new file mode 100644
index 0000000..50af37f
Binary files /dev/null and b/client/assets/maps/Isometric/tableShortChairs_N.png differ
diff --git a/client/assets/maps/Isometric/tableShortChairs_S.png b/client/assets/maps/Isometric/tableShortChairs_S.png
new file mode 100644
index 0000000..8eb7db1
Binary files /dev/null and b/client/assets/maps/Isometric/tableShortChairs_S.png differ
diff --git a/client/assets/maps/Isometric/tableShortChairs_W.png b/client/assets/maps/Isometric/tableShortChairs_W.png
new file mode 100644
index 0000000..0b1f07d
Binary files /dev/null and b/client/assets/maps/Isometric/tableShortChairs_W.png differ
diff --git a/client/assets/maps/Isometric/tableShort_E.png b/client/assets/maps/Isometric/tableShort_E.png
new file mode 100644
index 0000000..0ce49c3
Binary files /dev/null and b/client/assets/maps/Isometric/tableShort_E.png differ
diff --git a/client/assets/maps/Isometric/tableShort_N.png b/client/assets/maps/Isometric/tableShort_N.png
new file mode 100644
index 0000000..8203b90
Binary files /dev/null and b/client/assets/maps/Isometric/tableShort_N.png differ
diff --git a/client/assets/maps/Isometric/tableShort_S.png b/client/assets/maps/Isometric/tableShort_S.png
new file mode 100644
index 0000000..aa0fc81
Binary files /dev/null and b/client/assets/maps/Isometric/tableShort_S.png differ
diff --git a/client/assets/maps/Isometric/tableShort_W.png b/client/assets/maps/Isometric/tableShort_W.png
new file mode 100644
index 0000000..5b676cb
Binary files /dev/null and b/client/assets/maps/Isometric/tableShort_W.png differ
diff --git a/client/assets/maps/Isometric/woodenCrate_E.png b/client/assets/maps/Isometric/woodenCrate_E.png
new file mode 100644
index 0000000..6df1422
Binary files /dev/null and b/client/assets/maps/Isometric/woodenCrate_E.png differ
diff --git a/client/assets/maps/Isometric/woodenCrate_N.png b/client/assets/maps/Isometric/woodenCrate_N.png
new file mode 100644
index 0000000..7913549
Binary files /dev/null and b/client/assets/maps/Isometric/woodenCrate_N.png differ
diff --git a/client/assets/maps/Isometric/woodenCrate_S.png b/client/assets/maps/Isometric/woodenCrate_S.png
new file mode 100644
index 0000000..5a6196d
Binary files /dev/null and b/client/assets/maps/Isometric/woodenCrate_S.png differ
diff --git a/client/assets/maps/Isometric/woodenCrate_W.png b/client/assets/maps/Isometric/woodenCrate_W.png
new file mode 100644
index 0000000..032fc89
Binary files /dev/null and b/client/assets/maps/Isometric/woodenCrate_W.png differ
diff --git a/client/assets/maps/Isometric/woodenCrates_E.png b/client/assets/maps/Isometric/woodenCrates_E.png
new file mode 100644
index 0000000..5e5f52d
Binary files /dev/null and b/client/assets/maps/Isometric/woodenCrates_E.png differ
diff --git a/client/assets/maps/Isometric/woodenCrates_N.png b/client/assets/maps/Isometric/woodenCrates_N.png
new file mode 100644
index 0000000..69b6276
Binary files /dev/null and b/client/assets/maps/Isometric/woodenCrates_N.png differ
diff --git a/client/assets/maps/Isometric/woodenCrates_S.png b/client/assets/maps/Isometric/woodenCrates_S.png
new file mode 100644
index 0000000..83dd5bb
Binary files /dev/null and b/client/assets/maps/Isometric/woodenCrates_S.png differ
diff --git a/client/assets/maps/Isometric/woodenCrates_W.png b/client/assets/maps/Isometric/woodenCrates_W.png
new file mode 100644
index 0000000..8853473
Binary files /dev/null and b/client/assets/maps/Isometric/woodenCrates_W.png differ
diff --git a/client/assets/maps/Isometric/woodenPile_E.png b/client/assets/maps/Isometric/woodenPile_E.png
new file mode 100644
index 0000000..d247cd6
Binary files /dev/null and b/client/assets/maps/Isometric/woodenPile_E.png differ
diff --git a/client/assets/maps/Isometric/woodenPile_N.png b/client/assets/maps/Isometric/woodenPile_N.png
new file mode 100644
index 0000000..8bfd822
Binary files /dev/null and b/client/assets/maps/Isometric/woodenPile_N.png differ
diff --git a/client/assets/maps/Isometric/woodenPile_S.png b/client/assets/maps/Isometric/woodenPile_S.png
new file mode 100644
index 0000000..e897bbc
Binary files /dev/null and b/client/assets/maps/Isometric/woodenPile_S.png differ
diff --git a/client/assets/maps/Isometric/woodenPile_W.png b/client/assets/maps/Isometric/woodenPile_W.png
new file mode 100644
index 0000000..e59476c
Binary files /dev/null and b/client/assets/maps/Isometric/woodenPile_W.png differ
diff --git a/client/assets/maps/Isometric/woodenSupportBeams_E.png b/client/assets/maps/Isometric/woodenSupportBeams_E.png
new file mode 100644
index 0000000..ed85ad2
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupportBeams_E.png differ
diff --git a/client/assets/maps/Isometric/woodenSupportBeams_N.png b/client/assets/maps/Isometric/woodenSupportBeams_N.png
new file mode 100644
index 0000000..48c47d5
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupportBeams_N.png differ
diff --git a/client/assets/maps/Isometric/woodenSupportBeams_S.png b/client/assets/maps/Isometric/woodenSupportBeams_S.png
new file mode 100644
index 0000000..5caaf18
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupportBeams_S.png differ
diff --git a/client/assets/maps/Isometric/woodenSupportBeams_W.png b/client/assets/maps/Isometric/woodenSupportBeams_W.png
new file mode 100644
index 0000000..ad40211
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupportBeams_W.png differ
diff --git a/client/assets/maps/Isometric/woodenSupportsBeam_E.png b/client/assets/maps/Isometric/woodenSupportsBeam_E.png
new file mode 100644
index 0000000..d39b269
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupportsBeam_E.png differ
diff --git a/client/assets/maps/Isometric/woodenSupportsBeam_N.png b/client/assets/maps/Isometric/woodenSupportsBeam_N.png
new file mode 100644
index 0000000..3898b1b
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupportsBeam_N.png differ
diff --git a/client/assets/maps/Isometric/woodenSupportsBeam_S.png b/client/assets/maps/Isometric/woodenSupportsBeam_S.png
new file mode 100644
index 0000000..3041e51
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupportsBeam_S.png differ
diff --git a/client/assets/maps/Isometric/woodenSupportsBeam_W.png b/client/assets/maps/Isometric/woodenSupportsBeam_W.png
new file mode 100644
index 0000000..3350676
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupportsBeam_W.png differ
diff --git a/client/assets/maps/Isometric/woodenSupportsBlock_E.png b/client/assets/maps/Isometric/woodenSupportsBlock_E.png
new file mode 100644
index 0000000..af3a67f
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupportsBlock_E.png differ
diff --git a/client/assets/maps/Isometric/woodenSupportsBlock_N.png b/client/assets/maps/Isometric/woodenSupportsBlock_N.png
new file mode 100644
index 0000000..b3efe14
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupportsBlock_N.png differ
diff --git a/client/assets/maps/Isometric/woodenSupportsBlock_S.png b/client/assets/maps/Isometric/woodenSupportsBlock_S.png
new file mode 100644
index 0000000..049b61d
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupportsBlock_S.png differ
diff --git a/client/assets/maps/Isometric/woodenSupportsBlock_W.png b/client/assets/maps/Isometric/woodenSupportsBlock_W.png
new file mode 100644
index 0000000..ec64837
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupportsBlock_W.png differ
diff --git a/client/assets/maps/Isometric/woodenSupports_E.png b/client/assets/maps/Isometric/woodenSupports_E.png
new file mode 100644
index 0000000..e62f88f
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupports_E.png differ
diff --git a/client/assets/maps/Isometric/woodenSupports_N.png b/client/assets/maps/Isometric/woodenSupports_N.png
new file mode 100644
index 0000000..978e2fd
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupports_N.png differ
diff --git a/client/assets/maps/Isometric/woodenSupports_S.png b/client/assets/maps/Isometric/woodenSupports_S.png
new file mode 100644
index 0000000..404feba
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupports_S.png differ
diff --git a/client/assets/maps/Isometric/woodenSupports_W.png b/client/assets/maps/Isometric/woodenSupports_W.png
new file mode 100644
index 0000000..80c2031
Binary files /dev/null and b/client/assets/maps/Isometric/woodenSupports_W.png differ
diff --git a/client/assets/maps/arrena.tmj b/client/assets/maps/arrena.tmj
new file mode 100644
index 0000000..5fb76bc
--- /dev/null
+++ b/client/assets/maps/arrena.tmj
@@ -0,0 +1,179 @@
+{ "compressionlevel":-1,
+ "height":35,
+ "infinite":false,
+ "layers":[
+ {
+ "data":[15, 2, 2, 27, 27, 27, 27, 27, 27, 20, 20, 20, 20, 27, 2, 19, 27, 27, 27, 27, 27, 27, 27, 27, 2, 39, 39, 2, 19, 19, 19, 2, 2, 2, 16,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 19, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 45, 2, 45, 2, 2, 2, 2, 20, 20, 20, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 29, 2, 2, 2, 48, 48, 2, 2, 2, 42, 2, 2, 2, 2, 42, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 20, 2, 2, 2, 28,
+ 29, 2, 2, 48, 2, 2, 2, 2, 2, 48, 48, 48, 2, 2, 2, 2, 2, 2, 2, 2, 19, 2, 2, 2, 2, 2, 2, 20, 2, 2, 20, 20, 2, 2, 28,
+ 29, 2, 2, 48, 2, 48, 48, 2, 2, 2, 2, 48, 2, 2, 2, 2, 2, 2, 45, 2, 2, 2, 2, 19, 2, 2, 2, 20, 2, 19, 2, 2, 2, 2, 28,
+ 2, 2, 41, 2, 2, 48, 48, 2, 42, 2, 48, 48, 2, 20, 2, 2, 2, 45, 2, 2, 2, 2, 11, 11, 11, 2, 2, 20, 2, 2, 2, 2, 2, 2, 28,
+ 33, 2, 2, 48, 2, 2, 2, 2, 2, 2, 2, 48, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 11, 11, 11, 2, 2, 20, 2, 2, 2, 2, 2, 2, 2,
+ 33, 2, 2, 48, 2, 2, 2, 2, 2, 2, 48, 48, 2, 2, 2, 2, 41, 2, 2, 2, 2, 2, 11, 11, 11, 2, 2, 20, 2, 2, 2, 2, 2, 2, 28,
+ 33, 2, 2, 2, 2, 2, 20, 20, 2, 2, 48, 48, 2, 41, 2, 2, 2, 2, 2, 2, 41, 2, 2, 2, 2, 2, 2, 2, 2, 2, 45, 2, 2, 2, 28,
+ 33, 2, 2, 2, 2, 2, 2, 2, 2, 42, 2, 2, 2, 41, 2, 20, 2, 2, 41, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 45, 45, 2, 2, 2,
+ 2, 41, 2, 48, 48, 2, 2, 2, 2, 42, 2, 41, 2, 2, 2, 2, 2, 2, 41, 2, 2, 2, 2, 20, 2, 2, 20, 20, 2, 2, 2, 45, 2, 2, 2,
+ 29, 41, 2, 2, 2, 2, 2, 48, 48, 2, 2, 2, 2, 2, 2, 2, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 20, 2, 2, 2, 45, 2, 2, 28,
+ 2, 2, 2, 2, 2, 20, 2, 2, 2, 2, 2, 2, 2, 48, 2, 2, 9, 9, 2, 2, 2, 2, 2, 2, 45, 45, 2, 2, 2, 2, 2, 45, 2, 20, 2,
+ 2, 41, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 45, 2, 2, 2, 45, 2, 2, 36,
+ 41, 41, 2, 2, 2, 2, 2, 42, 2, 2, 2, 41, 48, 2, 2, 2, 20, 20, 2, 2, 2, 41, 45, 45, 2, 45, 2, 45, 2, 2, 2, 45, 2, 2, 36,
+ 2, 2, 2, 2, 2, 2, 2, 42, 2, 2, 2, 2, 2, 48, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 45, 2, 2, 2, 2, 2, 2, 2, 2, 36,
+ 29, 2, 2, 2, 2, 20, 2, 2, 2, 2, 48, 48, 2, 2, 2, 2, 2, 2, 2, 2, 2, 41, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 40,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 20, 2, 2, 2, 2, 2, 41, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 40,
+ 33, 2, 41, 2, 2, 2, 2, 2, 2, 48, 2, 2, 2, 2, 20, 2, 2, 2, 2, 20, 2, 2, 2, 2, 2, 2, 2, 20, 2, 2, 2, 2, 2, 2, 40,
+ 33, 2, 2, 2, 2, 2, 2, 2, 2, 2, 48, 48, 2, 2, 2, 2, 2, 2, 2, 20, 20, 2, 2, 2, 45, 2, 2, 2, 2, 2, 45, 45, 2, 2, 40,
+ 33, 2, 2, 45, 2, 2, 2, 20, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 20, 2, 2, 2, 45, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 33, 2, 2, 45, 45, 2, 2, 2, 2, 2, 2, 2, 2, 48, 48, 2, 48, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 45, 2, 2, 2, 2, 14,
+ 33, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 48, 2, 2, 2, 20, 2, 2, 2, 20, 2, 45, 45, 2, 2, 2, 2, 2,
+ 33, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 20, 2, 2, 2, 2, 2, 2, 48, 2, 2, 2, 2, 2, 2, 2, 2, 45, 45, 2, 2, 2, 2, 2, 2,
+ 33, 2, 2, 2, 42, 2, 2, 45, 45, 2, 2, 2, 2, 2, 2, 2, 2, 41, 2, 2, 2, 2, 2, 2, 2, 2, 45, 45, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 42, 42, 2, 2, 45, 2, 2, 2, 2, 2, 2, 2, 41, 2, 2, 2, 2, 2, 45, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 29, 2, 2, 2, 2, 42, 42, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 20, 20, 2, 2, 2, 2,
+ 29, 2, 2, 2, 2, 2, 42, 42, 2, 2, 2, 2, 2, 41, 2, 2, 41, 2, 2, 2, 2, 41, 2, 2, 2, 2, 2, 2, 20, 20, 20, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 42, 42, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 48, 2, 2, 2, 20, 2, 2, 2, 2, 2, 23,
+ 2, 2, 2, 2, 2, 2, 2, 2, 42, 42, 42, 2, 2, 2, 2, 2, 2, 2, 2, 41, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 23,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 11, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 42, 2, 2, 2, 2, 2, 2, 2, 2, 2, 23,
+ 7, 2, 2, 34, 2, 2, 38, 38, 2, 2, 2, 2, 2, 2, 2, 2, 38, 38, 38, 38, 38, 38, 42, 42, 42, 2, 2, 2, 2, 22, 22, 22, 20, 20, 5],
+ "height":35,
+ "id":3,
+ "name":"Floor",
+ "opacity":1,
+ "type":"tilelayer",
+ "visible":true,
+ "width":35,
+ "x":0,
+ "y":0
+ },
+ {
+ "data":[137, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 55, 139,
+ 145, 0, 67, 0, 67, 67, 0, 0, 67, 0, 0, 67, 0, 0, 67, 0, 67, 0, 0, 67, 0, 0, 67, 67, 67, 0, 67, 67, 67, 0, 0, 67, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 65, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 0, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 148,
+ 145, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 148,
+ 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148,
+ 145, 0, 0, 67, 67, 0, 67, 67, 0, 0, 67, 67, 0, 0, 67, 67, 67, 0, 67, 0, 0, 67, 67, 0, 0, 0, 67, 67, 0, 67, 0, 0, 67, 0, 148,
+ 134, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 146, 136],
+ "height":35,
+ "id":1,
+ "name":"Foundations",
+ "offsetx":0,
+ "offsety":32,
+ "opacity":1,
+ "type":"tilelayer",
+ "visible":true,
+ "width":35,
+ "x":0,
+ "y":0
+ },
+ {
+ "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 265, 0, 0, 163, 0, 0, 0, 264, 264, 0, 0, 0, 163, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 265, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 265, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 271, 0, 0, 0, 0, 0, 0, 0, 0, 0, 264, 264, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 272, 0, 0, 0, 271, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 272, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 271, 271, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 271, 271, 0, 0, 0, 0, 0, 0, 264, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 271, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 271, 271, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 163, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 0, 0,
+ 0, 0, 0, 163, 0, 271, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 271, 0, 0, 264, 0, 0, 170, 0, 0,
+ 0, 0, 0, 163, 0, 0, 0, 0, 0, 0, 0, 0, 271, 0, 0, 0, 0, 0, 0, 0, 0, 271, 0, 0, 0, 0, 271, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 163, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 271, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 271, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 264, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 264, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 264, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 264, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 163, 0, 0, 271, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 271, 0, 0, 0, 0, 0, 0, 0, 0, 264, 0, 0, 0, 0,
+ 0, 0, 264, 0, 0, 163, 0, 0, 0, 0, 0, 0, 271, 271, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 264, 264, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 271, 271, 0, 0, 0, 0, 0, 0, 264, 0, 0, 264, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 272, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 272, 0, 0, 0, 264, 0, 0, 163, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 163, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 264, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 163, 0, 0, 0, 272, 0, 0, 163, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 163, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 163, 163, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ "height":35,
+ "id":4,
+ "name":"Props",
+ "opacity":1,
+ "type":"tilelayer",
+ "visible":true,
+ "width":35,
+ "x":0,
+ "y":0
+ },
+ {
+ "draworder":"topdown",
+ "id":5,
+ "name":"Triggers",
+ "objects":[],
+ "opacity":1,
+ "type":"objectgroup",
+ "visible":true,
+ "x":0,
+ "y":0
+ }],
+ "nextlayerid":6,
+ "nextobjectid":1,
+ "orientation":"isometric",
+ "renderorder":"right-down",
+ "tiledversion":"1.12.1",
+ "tileheight":128,
+ "tilesets":[
+ {
+ "firstgid":1,
+ "source":"floor.tsx"
+ },
+ {
+ "firstgid":49,
+ "source":"wall.tsx"
+ },
+ {
+ "firstgid":161,
+ "source":"props.tsx"
+ }],
+ "tilewidth":256,
+ "type":"map",
+ "version":"1.10",
+ "width":35
+}
\ No newline at end of file
diff --git a/client/assets/maps/floor.tsx b/client/assets/maps/floor.tsx
new file mode 100644
index 0000000..c25b50b
--- /dev/null
+++ b/client/assets/maps/floor.tsx
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/assets/maps/props.tsx b/client/assets/maps/props.tsx
new file mode 100644
index 0000000..6acadb4
--- /dev/null
+++ b/client/assets/maps/props.tsx
@@ -0,0 +1,388 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/assets/maps/wall.tsx b/client/assets/maps/wall.tsx
new file mode 100644
index 0000000..f2e3095
--- /dev/null
+++ b/client/assets/maps/wall.tsx
@@ -0,0 +1,340 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/assets/sky.jpg b/client/assets/sky.jpg
new file mode 100644
index 0000000..9a392e6
Binary files /dev/null and b/client/assets/sky.jpg differ
diff --git a/client/assets/soulgate.png b/client/assets/soulgate.png
new file mode 100644
index 0000000..d481b4d
Binary files /dev/null and b/client/assets/soulgate.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/east/frame_000.png b/client/assets/sprites/aldric/animations/attack/east/frame_000.png
new file mode 100644
index 0000000..9d2ace8
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/east/frame_000.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/east/frame_001.png b/client/assets/sprites/aldric/animations/attack/east/frame_001.png
new file mode 100644
index 0000000..9ce4242
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/east/frame_001.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/east/frame_002.png b/client/assets/sprites/aldric/animations/attack/east/frame_002.png
new file mode 100644
index 0000000..7371b01
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/east/frame_002.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/east/frame_003.png b/client/assets/sprites/aldric/animations/attack/east/frame_003.png
new file mode 100644
index 0000000..f8356f6
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/east/frame_003.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/east/frame_004.png b/client/assets/sprites/aldric/animations/attack/east/frame_004.png
new file mode 100644
index 0000000..a918325
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/east/frame_004.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/east/frame_005.png b/client/assets/sprites/aldric/animations/attack/east/frame_005.png
new file mode 100644
index 0000000..50e80ce
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/east/frame_005.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/east/frame_006.png b/client/assets/sprites/aldric/animations/attack/east/frame_006.png
new file mode 100644
index 0000000..d2712ec
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/east/frame_006.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/north/frame_000.png b/client/assets/sprites/aldric/animations/attack/north/frame_000.png
new file mode 100644
index 0000000..944e3a5
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/north/frame_000.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/north/frame_001.png b/client/assets/sprites/aldric/animations/attack/north/frame_001.png
new file mode 100644
index 0000000..5ff030e
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/north/frame_001.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/north/frame_002.png b/client/assets/sprites/aldric/animations/attack/north/frame_002.png
new file mode 100644
index 0000000..25e5939
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/north/frame_002.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/north/frame_003.png b/client/assets/sprites/aldric/animations/attack/north/frame_003.png
new file mode 100644
index 0000000..dd70a7f
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/north/frame_003.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/north/frame_004.png b/client/assets/sprites/aldric/animations/attack/north/frame_004.png
new file mode 100644
index 0000000..e5438fe
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/north/frame_004.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/north/frame_005.png b/client/assets/sprites/aldric/animations/attack/north/frame_005.png
new file mode 100644
index 0000000..a3f4bdc
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/north/frame_005.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/north/frame_006.png b/client/assets/sprites/aldric/animations/attack/north/frame_006.png
new file mode 100644
index 0000000..7cc8919
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/north/frame_006.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/south/frame_000.png b/client/assets/sprites/aldric/animations/attack/south/frame_000.png
new file mode 100644
index 0000000..f972a1a
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/south/frame_000.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/south/frame_001.png b/client/assets/sprites/aldric/animations/attack/south/frame_001.png
new file mode 100644
index 0000000..d531d1e
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/south/frame_001.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/south/frame_002.png b/client/assets/sprites/aldric/animations/attack/south/frame_002.png
new file mode 100644
index 0000000..55acae0
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/south/frame_002.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/south/frame_003.png b/client/assets/sprites/aldric/animations/attack/south/frame_003.png
new file mode 100644
index 0000000..22d4d19
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/south/frame_003.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/south/frame_004.png b/client/assets/sprites/aldric/animations/attack/south/frame_004.png
new file mode 100644
index 0000000..81d7419
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/south/frame_004.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/south/frame_005.png b/client/assets/sprites/aldric/animations/attack/south/frame_005.png
new file mode 100644
index 0000000..d0a85a5
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/south/frame_005.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/south/frame_006.png b/client/assets/sprites/aldric/animations/attack/south/frame_006.png
new file mode 100644
index 0000000..3a45ac6
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/south/frame_006.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/west/frame_000.png b/client/assets/sprites/aldric/animations/attack/west/frame_000.png
new file mode 100644
index 0000000..1a30670
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/west/frame_000.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/west/frame_001.png b/client/assets/sprites/aldric/animations/attack/west/frame_001.png
new file mode 100644
index 0000000..1bfdb63
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/west/frame_001.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/west/frame_002.png b/client/assets/sprites/aldric/animations/attack/west/frame_002.png
new file mode 100644
index 0000000..cc77449
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/west/frame_002.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/west/frame_003.png b/client/assets/sprites/aldric/animations/attack/west/frame_003.png
new file mode 100644
index 0000000..ff886a4
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/west/frame_003.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/west/frame_004.png b/client/assets/sprites/aldric/animations/attack/west/frame_004.png
new file mode 100644
index 0000000..1115f25
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/west/frame_004.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/west/frame_005.png b/client/assets/sprites/aldric/animations/attack/west/frame_005.png
new file mode 100644
index 0000000..6c6b9ee
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/west/frame_005.png differ
diff --git a/client/assets/sprites/aldric/animations/attack/west/frame_006.png b/client/assets/sprites/aldric/animations/attack/west/frame_006.png
new file mode 100644
index 0000000..e679c35
Binary files /dev/null and b/client/assets/sprites/aldric/animations/attack/west/frame_006.png differ
diff --git a/client/assets/sprites/aldric/animations/fly/south/frame_000.png b/client/assets/sprites/aldric/animations/fly/south/frame_000.png
new file mode 100644
index 0000000..f972a1a
Binary files /dev/null and b/client/assets/sprites/aldric/animations/fly/south/frame_000.png differ
diff --git a/client/assets/sprites/aldric/animations/fly/south/frame_001.png b/client/assets/sprites/aldric/animations/fly/south/frame_001.png
new file mode 100644
index 0000000..42c5582
Binary files /dev/null and b/client/assets/sprites/aldric/animations/fly/south/frame_001.png differ
diff --git a/client/assets/sprites/aldric/animations/fly/south/frame_002.png b/client/assets/sprites/aldric/animations/fly/south/frame_002.png
new file mode 100644
index 0000000..dd8b3a9
Binary files /dev/null and b/client/assets/sprites/aldric/animations/fly/south/frame_002.png differ
diff --git a/client/assets/sprites/aldric/animations/fly/south/frame_003.png b/client/assets/sprites/aldric/animations/fly/south/frame_003.png
new file mode 100644
index 0000000..6aead8f
Binary files /dev/null and b/client/assets/sprites/aldric/animations/fly/south/frame_003.png differ
diff --git a/client/assets/sprites/aldric/animations/fly/south/frame_004.png b/client/assets/sprites/aldric/animations/fly/south/frame_004.png
new file mode 100644
index 0000000..15ea54f
Binary files /dev/null and b/client/assets/sprites/aldric/animations/fly/south/frame_004.png differ
diff --git a/client/assets/sprites/aldric/animations/fly/south/frame_005.png b/client/assets/sprites/aldric/animations/fly/south/frame_005.png
new file mode 100644
index 0000000..19a2fb6
Binary files /dev/null and b/client/assets/sprites/aldric/animations/fly/south/frame_005.png differ
diff --git a/client/assets/sprites/aldric/animations/fly/south/frame_006.png b/client/assets/sprites/aldric/animations/fly/south/frame_006.png
new file mode 100644
index 0000000..0dba274
Binary files /dev/null and b/client/assets/sprites/aldric/animations/fly/south/frame_006.png differ
diff --git a/client/assets/sprites/aldric/animations/fly/south/frame_007.png b/client/assets/sprites/aldric/animations/fly/south/frame_007.png
new file mode 100644
index 0000000..d40b282
Binary files /dev/null and b/client/assets/sprites/aldric/animations/fly/south/frame_007.png differ
diff --git a/client/assets/sprites/aldric/animations/fly/south/frame_008.png b/client/assets/sprites/aldric/animations/fly/south/frame_008.png
new file mode 100644
index 0000000..3876d20
Binary files /dev/null and b/client/assets/sprites/aldric/animations/fly/south/frame_008.png differ
diff --git a/client/assets/sprites/aldric/animations/skill1/south/frame_000.png b/client/assets/sprites/aldric/animations/skill1/south/frame_000.png
new file mode 100644
index 0000000..f972a1a
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill1/south/frame_000.png differ
diff --git a/client/assets/sprites/aldric/animations/skill1/south/frame_001.png b/client/assets/sprites/aldric/animations/skill1/south/frame_001.png
new file mode 100644
index 0000000..286b5d9
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill1/south/frame_001.png differ
diff --git a/client/assets/sprites/aldric/animations/skill1/south/frame_002.png b/client/assets/sprites/aldric/animations/skill1/south/frame_002.png
new file mode 100644
index 0000000..20e9539
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill1/south/frame_002.png differ
diff --git a/client/assets/sprites/aldric/animations/skill1/south/frame_003.png b/client/assets/sprites/aldric/animations/skill1/south/frame_003.png
new file mode 100644
index 0000000..83e2676
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill1/south/frame_003.png differ
diff --git a/client/assets/sprites/aldric/animations/skill1/south/frame_004.png b/client/assets/sprites/aldric/animations/skill1/south/frame_004.png
new file mode 100644
index 0000000..73796ac
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill1/south/frame_004.png differ
diff --git a/client/assets/sprites/aldric/animations/skill1/south/frame_005.png b/client/assets/sprites/aldric/animations/skill1/south/frame_005.png
new file mode 100644
index 0000000..cb09c6d
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill1/south/frame_005.png differ
diff --git a/client/assets/sprites/aldric/animations/skill1/south/frame_006.png b/client/assets/sprites/aldric/animations/skill1/south/frame_006.png
new file mode 100644
index 0000000..7584780
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill1/south/frame_006.png differ
diff --git a/client/assets/sprites/aldric/animations/skill1/south/frame_007.png b/client/assets/sprites/aldric/animations/skill1/south/frame_007.png
new file mode 100644
index 0000000..5e9cce5
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill1/south/frame_007.png differ
diff --git a/client/assets/sprites/aldric/animations/skill1/south/frame_008.png b/client/assets/sprites/aldric/animations/skill1/south/frame_008.png
new file mode 100644
index 0000000..a57184c
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill1/south/frame_008.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_000.png b/client/assets/sprites/aldric/animations/skill2/south/frame_000.png
new file mode 100644
index 0000000..f972a1a
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_000.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_001.png b/client/assets/sprites/aldric/animations/skill2/south/frame_001.png
new file mode 100644
index 0000000..e1d8277
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_001.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_002.png b/client/assets/sprites/aldric/animations/skill2/south/frame_002.png
new file mode 100644
index 0000000..0b05842
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_002.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_003.png b/client/assets/sprites/aldric/animations/skill2/south/frame_003.png
new file mode 100644
index 0000000..1ced6ef
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_003.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_004.png b/client/assets/sprites/aldric/animations/skill2/south/frame_004.png
new file mode 100644
index 0000000..7df4f17
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_004.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_005.png b/client/assets/sprites/aldric/animations/skill2/south/frame_005.png
new file mode 100644
index 0000000..481b9d9
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_005.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_006.png b/client/assets/sprites/aldric/animations/skill2/south/frame_006.png
new file mode 100644
index 0000000..deeed4b
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_006.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_007.png b/client/assets/sprites/aldric/animations/skill2/south/frame_007.png
new file mode 100644
index 0000000..ae60c9b
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_007.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_008.png b/client/assets/sprites/aldric/animations/skill2/south/frame_008.png
new file mode 100644
index 0000000..8abb06b
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_008.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_009.png b/client/assets/sprites/aldric/animations/skill2/south/frame_009.png
new file mode 100644
index 0000000..3e9b51c
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_009.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_010.png b/client/assets/sprites/aldric/animations/skill2/south/frame_010.png
new file mode 100644
index 0000000..8ef3f5a
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_010.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_011.png b/client/assets/sprites/aldric/animations/skill2/south/frame_011.png
new file mode 100644
index 0000000..087e339
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_011.png differ
diff --git a/client/assets/sprites/aldric/animations/skill2/south/frame_012.png b/client/assets/sprites/aldric/animations/skill2/south/frame_012.png
new file mode 100644
index 0000000..521b475
Binary files /dev/null and b/client/assets/sprites/aldric/animations/skill2/south/frame_012.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/east/frame_000.png b/client/assets/sprites/aldric/animations/walking/east/frame_000.png
new file mode 100644
index 0000000..2312445
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/east/frame_000.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/east/frame_001.png b/client/assets/sprites/aldric/animations/walking/east/frame_001.png
new file mode 100644
index 0000000..c11ac0a
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/east/frame_001.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/east/frame_002.png b/client/assets/sprites/aldric/animations/walking/east/frame_002.png
new file mode 100644
index 0000000..ab047cc
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/east/frame_002.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/east/frame_003.png b/client/assets/sprites/aldric/animations/walking/east/frame_003.png
new file mode 100644
index 0000000..759c5ad
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/east/frame_003.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/east/frame_004.png b/client/assets/sprites/aldric/animations/walking/east/frame_004.png
new file mode 100644
index 0000000..bc02b88
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/east/frame_004.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/east/frame_005.png b/client/assets/sprites/aldric/animations/walking/east/frame_005.png
new file mode 100644
index 0000000..c3fd17a
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/east/frame_005.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/north/frame_000.png b/client/assets/sprites/aldric/animations/walking/north/frame_000.png
new file mode 100644
index 0000000..6d944cd
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/north/frame_000.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/north/frame_001.png b/client/assets/sprites/aldric/animations/walking/north/frame_001.png
new file mode 100644
index 0000000..52ea8df
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/north/frame_001.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/north/frame_002.png b/client/assets/sprites/aldric/animations/walking/north/frame_002.png
new file mode 100644
index 0000000..80051c9
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/north/frame_002.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/north/frame_003.png b/client/assets/sprites/aldric/animations/walking/north/frame_003.png
new file mode 100644
index 0000000..f5373fb
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/north/frame_003.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/north/frame_004.png b/client/assets/sprites/aldric/animations/walking/north/frame_004.png
new file mode 100644
index 0000000..62d5c81
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/north/frame_004.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/north/frame_005.png b/client/assets/sprites/aldric/animations/walking/north/frame_005.png
new file mode 100644
index 0000000..1c02a0e
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/north/frame_005.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/south/frame_000.png b/client/assets/sprites/aldric/animations/walking/south/frame_000.png
new file mode 100644
index 0000000..1864534
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/south/frame_000.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/south/frame_001.png b/client/assets/sprites/aldric/animations/walking/south/frame_001.png
new file mode 100644
index 0000000..73f87cc
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/south/frame_001.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/south/frame_002.png b/client/assets/sprites/aldric/animations/walking/south/frame_002.png
new file mode 100644
index 0000000..a1b527e
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/south/frame_002.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/south/frame_003.png b/client/assets/sprites/aldric/animations/walking/south/frame_003.png
new file mode 100644
index 0000000..8cd666e
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/south/frame_003.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/south/frame_004.png b/client/assets/sprites/aldric/animations/walking/south/frame_004.png
new file mode 100644
index 0000000..f6b909b
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/south/frame_004.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/south/frame_005.png b/client/assets/sprites/aldric/animations/walking/south/frame_005.png
new file mode 100644
index 0000000..594db1f
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/south/frame_005.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/west/frame_000.png b/client/assets/sprites/aldric/animations/walking/west/frame_000.png
new file mode 100644
index 0000000..799274f
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/west/frame_000.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/west/frame_001.png b/client/assets/sprites/aldric/animations/walking/west/frame_001.png
new file mode 100644
index 0000000..c5b52a3
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/west/frame_001.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/west/frame_002.png b/client/assets/sprites/aldric/animations/walking/west/frame_002.png
new file mode 100644
index 0000000..9a9cddc
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/west/frame_002.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/west/frame_003.png b/client/assets/sprites/aldric/animations/walking/west/frame_003.png
new file mode 100644
index 0000000..d3b0bb1
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/west/frame_003.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/west/frame_004.png b/client/assets/sprites/aldric/animations/walking/west/frame_004.png
new file mode 100644
index 0000000..dc0c1ba
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/west/frame_004.png differ
diff --git a/client/assets/sprites/aldric/animations/walking/west/frame_005.png b/client/assets/sprites/aldric/animations/walking/west/frame_005.png
new file mode 100644
index 0000000..0e6e4bb
Binary files /dev/null and b/client/assets/sprites/aldric/animations/walking/west/frame_005.png differ
diff --git a/client/assets/sprites/aldric/metadata.json b/client/assets/sprites/aldric/metadata.json
new file mode 100644
index 0000000..d7bb0b5
--- /dev/null
+++ b/client/assets/sprites/aldric/metadata.json
@@ -0,0 +1,142 @@
+{
+ "character": {
+ "id": "d328cf6f-2822-40d0-bf98-9c91e8ec3f11",
+ "name": "Male human, slim and upright build, slightly shorter than Kael. Long flowing robe split at the legs, ivory and deep teal with gold trim along the collar and cuffs. Over the robe, a lightweight breastplate of\n polished bone-white metal engraved with glowing teal runes. Long silver-white hair tied back loosely with a few strands framing the face, eyes glowing with a soft teal light. Two large ethereal wings made of\n translucent teal energy trails, folded at rest on his back. He carries a tall ornate staff topped with a crystal orb radiating a faint glow. Warm light brown skin.",
+ "prompt": "Male human, slim and upright build, slightly shorter than Kael. Long flowing robe split at the legs, ivory and deep teal with gold trim along the collar and cuffs. Over the robe, a lightweight breastplate of\n polished bone-white metal engraved with glowing teal runes. Long silver-white hair tied back loosely with a few strands framing the face, eyes glowing with a soft teal light. Two large ethereal wings made of\n translucent teal energy trails, folded at rest on his back. He carries a tall ornate staff topped with a crystal orb radiating a faint glow. Warm light brown skin.",
+ "size": {
+ "width": 68,
+ "height": 68
+ },
+ "template_id": "mannequin",
+ "directions": 4,
+ "view": "low top-down",
+ "created_at": "2026-04-13T14:45:11.934975+00:00"
+ },
+ "frames": {
+ "rotations": {
+ "south": "rotations/south.png",
+ "west": "rotations/west.png",
+ "east": "rotations/east.png",
+ "north": "rotations/north.png"
+ },
+ "animations": {
+ "animation-b8b5cbee": {
+ "north": [
+ "animations/animation-b8b5cbee/north/frame_000.png",
+ "animations/animation-b8b5cbee/north/frame_001.png",
+ "animations/animation-b8b5cbee/north/frame_002.png",
+ "animations/animation-b8b5cbee/north/frame_003.png",
+ "animations/animation-b8b5cbee/north/frame_004.png",
+ "animations/animation-b8b5cbee/north/frame_005.png"
+ ],
+ "west": [
+ "animations/animation-b8b5cbee/west/frame_000.png",
+ "animations/animation-b8b5cbee/west/frame_001.png",
+ "animations/animation-b8b5cbee/west/frame_002.png",
+ "animations/animation-b8b5cbee/west/frame_003.png",
+ "animations/animation-b8b5cbee/west/frame_004.png",
+ "animations/animation-b8b5cbee/west/frame_005.png"
+ ],
+ "east": [
+ "animations/animation-b8b5cbee/east/frame_000.png",
+ "animations/animation-b8b5cbee/east/frame_001.png",
+ "animations/animation-b8b5cbee/east/frame_002.png",
+ "animations/animation-b8b5cbee/east/frame_003.png",
+ "animations/animation-b8b5cbee/east/frame_004.png",
+ "animations/animation-b8b5cbee/east/frame_005.png"
+ ],
+ "south": [
+ "animations/animation-b8b5cbee/south/frame_000.png",
+ "animations/animation-b8b5cbee/south/frame_001.png",
+ "animations/animation-b8b5cbee/south/frame_002.png",
+ "animations/animation-b8b5cbee/south/frame_003.png",
+ "animations/animation-b8b5cbee/south/frame_004.png",
+ "animations/animation-b8b5cbee/south/frame_005.png"
+ ]
+ },
+ "He_thrusts_the_staff_forward_toward_the_target_the-f52393a5": {
+ "east": [
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/east/frame_000.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/east/frame_001.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/east/frame_002.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/east/frame_003.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/east/frame_004.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/east/frame_005.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/east/frame_006.png"
+ ],
+ "south": [
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/south/frame_000.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/south/frame_001.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/south/frame_002.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/south/frame_003.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/south/frame_004.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/south/frame_005.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/south/frame_006.png"
+ ],
+ "west": [
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/west/frame_000.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/west/frame_001.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/west/frame_002.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/west/frame_003.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/west/frame_004.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/west/frame_005.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/west/frame_006.png"
+ ],
+ "north": [
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/north/frame_000.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/north/frame_001.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/north/frame_002.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/north/frame_003.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/north/frame_004.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/north/frame_005.png",
+ "animations/He_thrusts_the_staff_forward_toward_the_target_the-f52393a5/north/frame_006.png"
+ ]
+ },
+ "He_rises_slightly_off_the_ground_his_ethereal_wing-4d8cca83": {
+ "south": [
+ "animations/He_rises_slightly_off_the_ground_his_ethereal_wing-4d8cca83/south/frame_000.png",
+ "animations/He_rises_slightly_off_the_ground_his_ethereal_wing-4d8cca83/south/frame_001.png",
+ "animations/He_rises_slightly_off_the_ground_his_ethereal_wing-4d8cca83/south/frame_002.png",
+ "animations/He_rises_slightly_off_the_ground_his_ethereal_wing-4d8cca83/south/frame_003.png",
+ "animations/He_rises_slightly_off_the_ground_his_ethereal_wing-4d8cca83/south/frame_004.png",
+ "animations/He_rises_slightly_off_the_ground_his_ethereal_wing-4d8cca83/south/frame_005.png",
+ "animations/He_rises_slightly_off_the_ground_his_ethereal_wing-4d8cca83/south/frame_006.png",
+ "animations/He_rises_slightly_off_the_ground_his_ethereal_wing-4d8cca83/south/frame_007.png",
+ "animations/He_rises_slightly_off_the_ground_his_ethereal_wing-4d8cca83/south/frame_008.png"
+ ]
+ },
+ "He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f": {
+ "south": [
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_000.png",
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_001.png",
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_002.png",
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_003.png",
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_004.png",
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_005.png",
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_006.png",
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_007.png",
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_008.png",
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_009.png",
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_010.png",
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_011.png",
+ "animations/He_drops_to_one_knee_and_drives_the_tip_of_the_sta-69376f5f/south/frame_012.png"
+ ]
+ },
+ "He_raises_the_staff_above_his_head_with_both_hands-46f23803": {
+ "south": [
+ "animations/He_raises_the_staff_above_his_head_with_both_hands-46f23803/south/frame_000.png",
+ "animations/He_raises_the_staff_above_his_head_with_both_hands-46f23803/south/frame_001.png",
+ "animations/He_raises_the_staff_above_his_head_with_both_hands-46f23803/south/frame_002.png",
+ "animations/He_raises_the_staff_above_his_head_with_both_hands-46f23803/south/frame_003.png",
+ "animations/He_raises_the_staff_above_his_head_with_both_hands-46f23803/south/frame_004.png",
+ "animations/He_raises_the_staff_above_his_head_with_both_hands-46f23803/south/frame_005.png",
+ "animations/He_raises_the_staff_above_his_head_with_both_hands-46f23803/south/frame_006.png",
+ "animations/He_raises_the_staff_above_his_head_with_both_hands-46f23803/south/frame_007.png",
+ "animations/He_raises_the_staff_above_his_head_with_both_hands-46f23803/south/frame_008.png"
+ ]
+ }
+ }
+ },
+ "export_version": "2.0",
+ "export_date": "2026-04-13T15:12:48.178349"
+}
\ No newline at end of file
diff --git a/client/assets/sprites/aldric/rotations/east.png b/client/assets/sprites/aldric/rotations/east.png
new file mode 100644
index 0000000..49441d1
Binary files /dev/null and b/client/assets/sprites/aldric/rotations/east.png differ
diff --git a/client/assets/sprites/aldric/rotations/north.png b/client/assets/sprites/aldric/rotations/north.png
new file mode 100644
index 0000000..3b5284f
Binary files /dev/null and b/client/assets/sprites/aldric/rotations/north.png differ
diff --git a/client/assets/sprites/aldric/rotations/south.png b/client/assets/sprites/aldric/rotations/south.png
new file mode 100644
index 0000000..3d07e49
Binary files /dev/null and b/client/assets/sprites/aldric/rotations/south.png differ
diff --git a/client/assets/sprites/aldric/rotations/west.png b/client/assets/sprites/aldric/rotations/west.png
new file mode 100644
index 0000000..7a1b18f
Binary files /dev/null and b/client/assets/sprites/aldric/rotations/west.png differ
diff --git a/client/assets/sprites/colosse/animations/running/east/frame_000.png b/client/assets/sprites/colosse/animations/running/east/frame_000.png
new file mode 100644
index 0000000..3d67f11
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/east/frame_000.png differ
diff --git a/client/assets/sprites/colosse/animations/running/east/frame_001.png b/client/assets/sprites/colosse/animations/running/east/frame_001.png
new file mode 100644
index 0000000..4f4093f
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/east/frame_001.png differ
diff --git a/client/assets/sprites/colosse/animations/running/east/frame_002.png b/client/assets/sprites/colosse/animations/running/east/frame_002.png
new file mode 100644
index 0000000..80e4893
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/east/frame_002.png differ
diff --git a/client/assets/sprites/colosse/animations/running/east/frame_003.png b/client/assets/sprites/colosse/animations/running/east/frame_003.png
new file mode 100644
index 0000000..b54fa60
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/east/frame_003.png differ
diff --git a/client/assets/sprites/colosse/animations/running/east/frame_004.png b/client/assets/sprites/colosse/animations/running/east/frame_004.png
new file mode 100644
index 0000000..479eafa
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/east/frame_004.png differ
diff --git a/client/assets/sprites/colosse/animations/running/east/frame_005.png b/client/assets/sprites/colosse/animations/running/east/frame_005.png
new file mode 100644
index 0000000..063b385
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/east/frame_005.png differ
diff --git a/client/assets/sprites/colosse/animations/running/north/frame_000.png b/client/assets/sprites/colosse/animations/running/north/frame_000.png
new file mode 100644
index 0000000..5be60c8
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/north/frame_000.png differ
diff --git a/client/assets/sprites/colosse/animations/running/north/frame_001.png b/client/assets/sprites/colosse/animations/running/north/frame_001.png
new file mode 100644
index 0000000..574563e
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/north/frame_001.png differ
diff --git a/client/assets/sprites/colosse/animations/running/north/frame_002.png b/client/assets/sprites/colosse/animations/running/north/frame_002.png
new file mode 100644
index 0000000..a3714db
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/north/frame_002.png differ
diff --git a/client/assets/sprites/colosse/animations/running/north/frame_003.png b/client/assets/sprites/colosse/animations/running/north/frame_003.png
new file mode 100644
index 0000000..6f2a13c
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/north/frame_003.png differ
diff --git a/client/assets/sprites/colosse/animations/running/north/frame_004.png b/client/assets/sprites/colosse/animations/running/north/frame_004.png
new file mode 100644
index 0000000..9116a6d
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/north/frame_004.png differ
diff --git a/client/assets/sprites/colosse/animations/running/north/frame_005.png b/client/assets/sprites/colosse/animations/running/north/frame_005.png
new file mode 100644
index 0000000..e8a5565
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/north/frame_005.png differ
diff --git a/client/assets/sprites/colosse/animations/running/south/frame_000.png b/client/assets/sprites/colosse/animations/running/south/frame_000.png
new file mode 100644
index 0000000..2059d00
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/south/frame_000.png differ
diff --git a/client/assets/sprites/colosse/animations/running/south/frame_001.png b/client/assets/sprites/colosse/animations/running/south/frame_001.png
new file mode 100644
index 0000000..870f7ab
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/south/frame_001.png differ
diff --git a/client/assets/sprites/colosse/animations/running/south/frame_002.png b/client/assets/sprites/colosse/animations/running/south/frame_002.png
new file mode 100644
index 0000000..0cef474
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/south/frame_002.png differ
diff --git a/client/assets/sprites/colosse/animations/running/south/frame_003.png b/client/assets/sprites/colosse/animations/running/south/frame_003.png
new file mode 100644
index 0000000..7e48f4d
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/south/frame_003.png differ
diff --git a/client/assets/sprites/colosse/animations/running/south/frame_004.png b/client/assets/sprites/colosse/animations/running/south/frame_004.png
new file mode 100644
index 0000000..dd1e42e
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/south/frame_004.png differ
diff --git a/client/assets/sprites/colosse/animations/running/south/frame_005.png b/client/assets/sprites/colosse/animations/running/south/frame_005.png
new file mode 100644
index 0000000..78c4f96
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/south/frame_005.png differ
diff --git a/client/assets/sprites/colosse/animations/running/west/frame_000.png b/client/assets/sprites/colosse/animations/running/west/frame_000.png
new file mode 100644
index 0000000..dc85b7f
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/west/frame_000.png differ
diff --git a/client/assets/sprites/colosse/animations/running/west/frame_001.png b/client/assets/sprites/colosse/animations/running/west/frame_001.png
new file mode 100644
index 0000000..7246970
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/west/frame_001.png differ
diff --git a/client/assets/sprites/colosse/animations/running/west/frame_002.png b/client/assets/sprites/colosse/animations/running/west/frame_002.png
new file mode 100644
index 0000000..e408e63
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/west/frame_002.png differ
diff --git a/client/assets/sprites/colosse/animations/running/west/frame_003.png b/client/assets/sprites/colosse/animations/running/west/frame_003.png
new file mode 100644
index 0000000..9a83708
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/west/frame_003.png differ
diff --git a/client/assets/sprites/colosse/animations/running/west/frame_004.png b/client/assets/sprites/colosse/animations/running/west/frame_004.png
new file mode 100644
index 0000000..906bb9a
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/west/frame_004.png differ
diff --git a/client/assets/sprites/colosse/animations/running/west/frame_005.png b/client/assets/sprites/colosse/animations/running/west/frame_005.png
new file mode 100644
index 0000000..756d8a8
Binary files /dev/null and b/client/assets/sprites/colosse/animations/running/west/frame_005.png differ
diff --git a/client/assets/sprites/colosse/metadata.json b/client/assets/sprites/colosse/metadata.json
new file mode 100644
index 0000000..fbedbe1
--- /dev/null
+++ b/client/assets/sprites/colosse/metadata.json
@@ -0,0 +1,61 @@
+{
+ "character": {
+ "id": "64d33d78-9732-4d70-bbd2-9c091bdb6751",
+ "name": "Not humanoid, more like a massive corrupted stone titan. Enormous broad body with no neck, head fused directly into the chest. Arms too long, dragging near the ground. Body made of cracked black\n rock with glowing orange lava visible through the fissures. No visible face, just a smooth dark surface with two deep orange glowing cracks as eyes. Massive shoulder width, hunched forward under its own\n weight.",
+ "prompt": "Not humanoid, more like a massive corrupted stone titan. Enormous broad body with no neck, head fused directly into the chest. Arms too long, dragging near the ground. Body made of cracked black\n rock with glowing orange lava visible through the fissures. No visible face, just a smooth dark surface with two deep orange glowing cracks as eyes. Massive shoulder width, hunched forward under its own\n weight.",
+ "size": {
+ "width": 92,
+ "height": 92
+ },
+ "template_id": "mannequin",
+ "directions": 4,
+ "view": "low top-down",
+ "created_at": "2026-04-21T15:45:21.678943+00:00"
+ },
+ "frames": {
+ "rotations": {
+ "south": "rotations/south.png",
+ "west": "rotations/west.png",
+ "east": "rotations/east.png",
+ "north": "rotations/north.png"
+ },
+ "animations": {
+ "Running-040f2cf4": {
+ "south": [
+ "animations/Running-040f2cf4/south/frame_000.png",
+ "animations/Running-040f2cf4/south/frame_001.png",
+ "animations/Running-040f2cf4/south/frame_002.png",
+ "animations/Running-040f2cf4/south/frame_003.png",
+ "animations/Running-040f2cf4/south/frame_004.png",
+ "animations/Running-040f2cf4/south/frame_005.png"
+ ],
+ "north": [
+ "animations/Running-040f2cf4/north/frame_000.png",
+ "animations/Running-040f2cf4/north/frame_001.png",
+ "animations/Running-040f2cf4/north/frame_002.png",
+ "animations/Running-040f2cf4/north/frame_003.png",
+ "animations/Running-040f2cf4/north/frame_004.png",
+ "animations/Running-040f2cf4/north/frame_005.png"
+ ],
+ "west": [
+ "animations/Running-040f2cf4/west/frame_000.png",
+ "animations/Running-040f2cf4/west/frame_001.png",
+ "animations/Running-040f2cf4/west/frame_002.png",
+ "animations/Running-040f2cf4/west/frame_003.png",
+ "animations/Running-040f2cf4/west/frame_004.png",
+ "animations/Running-040f2cf4/west/frame_005.png"
+ ],
+ "east": [
+ "animations/Running-040f2cf4/east/frame_000.png",
+ "animations/Running-040f2cf4/east/frame_001.png",
+ "animations/Running-040f2cf4/east/frame_002.png",
+ "animations/Running-040f2cf4/east/frame_003.png",
+ "animations/Running-040f2cf4/east/frame_004.png",
+ "animations/Running-040f2cf4/east/frame_005.png"
+ ]
+ }
+ }
+ },
+ "export_version": "2.0",
+ "export_date": "2026-04-21T15:50:51.833482"
+}
\ No newline at end of file
diff --git a/client/assets/sprites/colosse/rotations/east.png b/client/assets/sprites/colosse/rotations/east.png
new file mode 100644
index 0000000..179e281
Binary files /dev/null and b/client/assets/sprites/colosse/rotations/east.png differ
diff --git a/client/assets/sprites/colosse/rotations/north.png b/client/assets/sprites/colosse/rotations/north.png
new file mode 100644
index 0000000..4975eb5
Binary files /dev/null and b/client/assets/sprites/colosse/rotations/north.png differ
diff --git a/client/assets/sprites/colosse/rotations/south.png b/client/assets/sprites/colosse/rotations/south.png
new file mode 100644
index 0000000..d555703
Binary files /dev/null and b/client/assets/sprites/colosse/rotations/south.png differ
diff --git a/client/assets/sprites/colosse/rotations/west.png b/client/assets/sprites/colosse/rotations/west.png
new file mode 100644
index 0000000..4d97629
Binary files /dev/null and b/client/assets/sprites/colosse/rotations/west.png differ
diff --git a/client/assets/sprites/eclat/animations/running/east/frame_000.png b/client/assets/sprites/eclat/animations/running/east/frame_000.png
new file mode 100644
index 0000000..b9c80ec
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/east/frame_000.png differ
diff --git a/client/assets/sprites/eclat/animations/running/east/frame_001.png b/client/assets/sprites/eclat/animations/running/east/frame_001.png
new file mode 100644
index 0000000..65690d8
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/east/frame_001.png differ
diff --git a/client/assets/sprites/eclat/animations/running/east/frame_002.png b/client/assets/sprites/eclat/animations/running/east/frame_002.png
new file mode 100644
index 0000000..96e6d3c
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/east/frame_002.png differ
diff --git a/client/assets/sprites/eclat/animations/running/east/frame_003.png b/client/assets/sprites/eclat/animations/running/east/frame_003.png
new file mode 100644
index 0000000..a8c1199
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/east/frame_003.png differ
diff --git a/client/assets/sprites/eclat/animations/running/east/frame_004.png b/client/assets/sprites/eclat/animations/running/east/frame_004.png
new file mode 100644
index 0000000..a09c895
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/east/frame_004.png differ
diff --git a/client/assets/sprites/eclat/animations/running/east/frame_005.png b/client/assets/sprites/eclat/animations/running/east/frame_005.png
new file mode 100644
index 0000000..68ceaf8
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/east/frame_005.png differ
diff --git a/client/assets/sprites/eclat/animations/running/east/frame_006.png b/client/assets/sprites/eclat/animations/running/east/frame_006.png
new file mode 100644
index 0000000..ccbb237
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/east/frame_006.png differ
diff --git a/client/assets/sprites/eclat/animations/running/east/frame_007.png b/client/assets/sprites/eclat/animations/running/east/frame_007.png
new file mode 100644
index 0000000..36b7140
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/east/frame_007.png differ
diff --git a/client/assets/sprites/eclat/animations/running/north/frame_000.png b/client/assets/sprites/eclat/animations/running/north/frame_000.png
new file mode 100644
index 0000000..d071fc1
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/north/frame_000.png differ
diff --git a/client/assets/sprites/eclat/animations/running/north/frame_001.png b/client/assets/sprites/eclat/animations/running/north/frame_001.png
new file mode 100644
index 0000000..761be03
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/north/frame_001.png differ
diff --git a/client/assets/sprites/eclat/animations/running/north/frame_002.png b/client/assets/sprites/eclat/animations/running/north/frame_002.png
new file mode 100644
index 0000000..4a40493
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/north/frame_002.png differ
diff --git a/client/assets/sprites/eclat/animations/running/north/frame_003.png b/client/assets/sprites/eclat/animations/running/north/frame_003.png
new file mode 100644
index 0000000..8f4f0db
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/north/frame_003.png differ
diff --git a/client/assets/sprites/eclat/animations/running/north/frame_004.png b/client/assets/sprites/eclat/animations/running/north/frame_004.png
new file mode 100644
index 0000000..69d2324
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/north/frame_004.png differ
diff --git a/client/assets/sprites/eclat/animations/running/north/frame_005.png b/client/assets/sprites/eclat/animations/running/north/frame_005.png
new file mode 100644
index 0000000..e1ea18d
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/north/frame_005.png differ
diff --git a/client/assets/sprites/eclat/animations/running/north/frame_006.png b/client/assets/sprites/eclat/animations/running/north/frame_006.png
new file mode 100644
index 0000000..491d2fe
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/north/frame_006.png differ
diff --git a/client/assets/sprites/eclat/animations/running/north/frame_007.png b/client/assets/sprites/eclat/animations/running/north/frame_007.png
new file mode 100644
index 0000000..0ce46a1
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/north/frame_007.png differ
diff --git a/client/assets/sprites/eclat/animations/running/south/frame_000.png b/client/assets/sprites/eclat/animations/running/south/frame_000.png
new file mode 100644
index 0000000..cce4f05
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/south/frame_000.png differ
diff --git a/client/assets/sprites/eclat/animations/running/south/frame_001.png b/client/assets/sprites/eclat/animations/running/south/frame_001.png
new file mode 100644
index 0000000..af07f72
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/south/frame_001.png differ
diff --git a/client/assets/sprites/eclat/animations/running/south/frame_002.png b/client/assets/sprites/eclat/animations/running/south/frame_002.png
new file mode 100644
index 0000000..d1d10d9
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/south/frame_002.png differ
diff --git a/client/assets/sprites/eclat/animations/running/south/frame_003.png b/client/assets/sprites/eclat/animations/running/south/frame_003.png
new file mode 100644
index 0000000..24129c8
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/south/frame_003.png differ
diff --git a/client/assets/sprites/eclat/animations/running/south/frame_004.png b/client/assets/sprites/eclat/animations/running/south/frame_004.png
new file mode 100644
index 0000000..0b8b7fe
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/south/frame_004.png differ
diff --git a/client/assets/sprites/eclat/animations/running/south/frame_005.png b/client/assets/sprites/eclat/animations/running/south/frame_005.png
new file mode 100644
index 0000000..cd640ee
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/south/frame_005.png differ
diff --git a/client/assets/sprites/eclat/animations/running/south/frame_006.png b/client/assets/sprites/eclat/animations/running/south/frame_006.png
new file mode 100644
index 0000000..2c77d93
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/south/frame_006.png differ
diff --git a/client/assets/sprites/eclat/animations/running/south/frame_007.png b/client/assets/sprites/eclat/animations/running/south/frame_007.png
new file mode 100644
index 0000000..c0a6483
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/south/frame_007.png differ
diff --git a/client/assets/sprites/eclat/animations/running/west/frame_000.png b/client/assets/sprites/eclat/animations/running/west/frame_000.png
new file mode 100644
index 0000000..f6d1827
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/west/frame_000.png differ
diff --git a/client/assets/sprites/eclat/animations/running/west/frame_001.png b/client/assets/sprites/eclat/animations/running/west/frame_001.png
new file mode 100644
index 0000000..5322799
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/west/frame_001.png differ
diff --git a/client/assets/sprites/eclat/animations/running/west/frame_002.png b/client/assets/sprites/eclat/animations/running/west/frame_002.png
new file mode 100644
index 0000000..13a8c66
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/west/frame_002.png differ
diff --git a/client/assets/sprites/eclat/animations/running/west/frame_003.png b/client/assets/sprites/eclat/animations/running/west/frame_003.png
new file mode 100644
index 0000000..9ba1360
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/west/frame_003.png differ
diff --git a/client/assets/sprites/eclat/animations/running/west/frame_004.png b/client/assets/sprites/eclat/animations/running/west/frame_004.png
new file mode 100644
index 0000000..555cf3d
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/west/frame_004.png differ
diff --git a/client/assets/sprites/eclat/animations/running/west/frame_005.png b/client/assets/sprites/eclat/animations/running/west/frame_005.png
new file mode 100644
index 0000000..b717a80
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/west/frame_005.png differ
diff --git a/client/assets/sprites/eclat/animations/running/west/frame_006.png b/client/assets/sprites/eclat/animations/running/west/frame_006.png
new file mode 100644
index 0000000..b131bd7
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/west/frame_006.png differ
diff --git a/client/assets/sprites/eclat/animations/running/west/frame_007.png b/client/assets/sprites/eclat/animations/running/west/frame_007.png
new file mode 100644
index 0000000..8596565
Binary files /dev/null and b/client/assets/sprites/eclat/animations/running/west/frame_007.png differ
diff --git a/client/assets/sprites/eclat/metadata.json b/client/assets/sprites/eclat/metadata.json
new file mode 100644
index 0000000..70b95c3
--- /dev/null
+++ b/client/assets/sprites/eclat/metadata.json
@@ -0,0 +1,69 @@
+{
+ "character": {
+ "id": "c11eab6a-2440-4279-a082-be809bd2d2f2",
+ "name": "Not humanoid at all. A small fragment of corrupted soul energy, shaped like a jagged broken crystal shard. No limbs, no face, no body \u2014 just a glowing yellow-white core surrounded by sharp broken\n edges that spin and flicker as it moves. Trails a faint light behind it",
+ "prompt": "Not humanoid at all. A small fragment of corrupted soul energy, shaped like a jagged broken crystal shard. No limbs, no face, no body \u2014 just a glowing yellow-white core surrounded by sharp broken\n edges that spin and flicker as it moves. Trails a faint light behind it",
+ "size": {
+ "width": 36,
+ "height": 36
+ },
+ "template_id": "mannequin",
+ "directions": 4,
+ "view": "low top-down",
+ "created_at": "2026-04-21T15:39:29.335016+00:00"
+ },
+ "frames": {
+ "rotations": {
+ "south": "rotations/south.png",
+ "west": "rotations/west.png",
+ "east": "rotations/east.png",
+ "north": "rotations/north.png"
+ },
+ "animations": {
+ "Running-c63e1f30": {
+ "south": [
+ "animations/Running-c63e1f30/south/frame_000.png",
+ "animations/Running-c63e1f30/south/frame_001.png",
+ "animations/Running-c63e1f30/south/frame_002.png",
+ "animations/Running-c63e1f30/south/frame_003.png",
+ "animations/Running-c63e1f30/south/frame_004.png",
+ "animations/Running-c63e1f30/south/frame_005.png",
+ "animations/Running-c63e1f30/south/frame_006.png",
+ "animations/Running-c63e1f30/south/frame_007.png"
+ ],
+ "west": [
+ "animations/Running-c63e1f30/west/frame_000.png",
+ "animations/Running-c63e1f30/west/frame_001.png",
+ "animations/Running-c63e1f30/west/frame_002.png",
+ "animations/Running-c63e1f30/west/frame_003.png",
+ "animations/Running-c63e1f30/west/frame_004.png",
+ "animations/Running-c63e1f30/west/frame_005.png",
+ "animations/Running-c63e1f30/west/frame_006.png",
+ "animations/Running-c63e1f30/west/frame_007.png"
+ ],
+ "north": [
+ "animations/Running-c63e1f30/north/frame_000.png",
+ "animations/Running-c63e1f30/north/frame_001.png",
+ "animations/Running-c63e1f30/north/frame_002.png",
+ "animations/Running-c63e1f30/north/frame_003.png",
+ "animations/Running-c63e1f30/north/frame_004.png",
+ "animations/Running-c63e1f30/north/frame_005.png",
+ "animations/Running-c63e1f30/north/frame_006.png",
+ "animations/Running-c63e1f30/north/frame_007.png"
+ ],
+ "east": [
+ "animations/Running-c63e1f30/east/frame_000.png",
+ "animations/Running-c63e1f30/east/frame_001.png",
+ "animations/Running-c63e1f30/east/frame_002.png",
+ "animations/Running-c63e1f30/east/frame_003.png",
+ "animations/Running-c63e1f30/east/frame_004.png",
+ "animations/Running-c63e1f30/east/frame_005.png",
+ "animations/Running-c63e1f30/east/frame_006.png",
+ "animations/Running-c63e1f30/east/frame_007.png"
+ ]
+ }
+ }
+ },
+ "export_version": "2.0",
+ "export_date": "2026-04-21T15:44:02.029717"
+}
\ No newline at end of file
diff --git a/client/assets/sprites/eclat/rotations/east.png b/client/assets/sprites/eclat/rotations/east.png
new file mode 100644
index 0000000..e9f8ebf
Binary files /dev/null and b/client/assets/sprites/eclat/rotations/east.png differ
diff --git a/client/assets/sprites/eclat/rotations/north.png b/client/assets/sprites/eclat/rotations/north.png
new file mode 100644
index 0000000..1ba0ac9
Binary files /dev/null and b/client/assets/sprites/eclat/rotations/north.png differ
diff --git a/client/assets/sprites/eclat/rotations/south.png b/client/assets/sprites/eclat/rotations/south.png
new file mode 100644
index 0000000..7e5f59d
Binary files /dev/null and b/client/assets/sprites/eclat/rotations/south.png differ
diff --git a/client/assets/sprites/eclat/rotations/west.png b/client/assets/sprites/eclat/rotations/west.png
new file mode 100644
index 0000000..870e7d9
Binary files /dev/null and b/client/assets/sprites/eclat/rotations/west.png differ
diff --git a/client/assets/sprites/fracture/animations/running/east/frame_000.png b/client/assets/sprites/fracture/animations/running/east/frame_000.png
new file mode 100644
index 0000000..462ba96
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/east/frame_000.png differ
diff --git a/client/assets/sprites/fracture/animations/running/east/frame_001.png b/client/assets/sprites/fracture/animations/running/east/frame_001.png
new file mode 100644
index 0000000..e5624e3
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/east/frame_001.png differ
diff --git a/client/assets/sprites/fracture/animations/running/east/frame_002.png b/client/assets/sprites/fracture/animations/running/east/frame_002.png
new file mode 100644
index 0000000..d83505b
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/east/frame_002.png differ
diff --git a/client/assets/sprites/fracture/animations/running/east/frame_003.png b/client/assets/sprites/fracture/animations/running/east/frame_003.png
new file mode 100644
index 0000000..4e51e31
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/east/frame_003.png differ
diff --git a/client/assets/sprites/fracture/animations/running/north/frame_000.png b/client/assets/sprites/fracture/animations/running/north/frame_000.png
new file mode 100644
index 0000000..6526fec
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/north/frame_000.png differ
diff --git a/client/assets/sprites/fracture/animations/running/north/frame_001.png b/client/assets/sprites/fracture/animations/running/north/frame_001.png
new file mode 100644
index 0000000..3123756
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/north/frame_001.png differ
diff --git a/client/assets/sprites/fracture/animations/running/north/frame_002.png b/client/assets/sprites/fracture/animations/running/north/frame_002.png
new file mode 100644
index 0000000..f165026
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/north/frame_002.png differ
diff --git a/client/assets/sprites/fracture/animations/running/north/frame_003.png b/client/assets/sprites/fracture/animations/running/north/frame_003.png
new file mode 100644
index 0000000..d668f18
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/north/frame_003.png differ
diff --git a/client/assets/sprites/fracture/animations/running/south/frame_000.png b/client/assets/sprites/fracture/animations/running/south/frame_000.png
new file mode 100644
index 0000000..b10070b
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/south/frame_000.png differ
diff --git a/client/assets/sprites/fracture/animations/running/south/frame_001.png b/client/assets/sprites/fracture/animations/running/south/frame_001.png
new file mode 100644
index 0000000..e0877df
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/south/frame_001.png differ
diff --git a/client/assets/sprites/fracture/animations/running/south/frame_002.png b/client/assets/sprites/fracture/animations/running/south/frame_002.png
new file mode 100644
index 0000000..b626718
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/south/frame_002.png differ
diff --git a/client/assets/sprites/fracture/animations/running/south/frame_003.png b/client/assets/sprites/fracture/animations/running/south/frame_003.png
new file mode 100644
index 0000000..9d0a5c5
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/south/frame_003.png differ
diff --git a/client/assets/sprites/fracture/animations/running/west/frame_000.png b/client/assets/sprites/fracture/animations/running/west/frame_000.png
new file mode 100644
index 0000000..ba1309d
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/west/frame_000.png differ
diff --git a/client/assets/sprites/fracture/animations/running/west/frame_001.png b/client/assets/sprites/fracture/animations/running/west/frame_001.png
new file mode 100644
index 0000000..0282446
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/west/frame_001.png differ
diff --git a/client/assets/sprites/fracture/animations/running/west/frame_002.png b/client/assets/sprites/fracture/animations/running/west/frame_002.png
new file mode 100644
index 0000000..531ab31
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/west/frame_002.png differ
diff --git a/client/assets/sprites/fracture/animations/running/west/frame_003.png b/client/assets/sprites/fracture/animations/running/west/frame_003.png
new file mode 100644
index 0000000..fd463c4
Binary files /dev/null and b/client/assets/sprites/fracture/animations/running/west/frame_003.png differ
diff --git a/client/assets/sprites/fracture/metadata.json b/client/assets/sprites/fracture/metadata.json
new file mode 100644
index 0000000..2aa4e44
--- /dev/null
+++ b/client/assets/sprites/fracture/metadata.json
@@ -0,0 +1,53 @@
+{
+ "character": {
+ "id": "8b4f8abd-9061-486d-9a33-b9ee926a2b4b",
+ "name": "A corrupted humanoid soul, hunched posture with arms slightly too long. Body made of cracked dark stone with glowing red fissures running across the torso and limbs, like magma visible through broken rock. No\n visible face, just a smooth cracked surface with two dim red glowing eyes. Tattered remnants of dark cloth hanging around the waist. Medium build, roughly human-sized.",
+ "prompt": "A corrupted humanoid soul, hunched posture with arms slightly too long. Body made of cracked dark stone with glowing red fissures running across the torso and limbs, like magma visible through broken rock. No\n visible face, just a smooth cracked surface with two dim red glowing eyes. Tattered remnants of dark cloth hanging around the waist. Medium build, roughly human-sized.",
+ "size": {
+ "width": 68,
+ "height": 68
+ },
+ "template_id": "mannequin",
+ "directions": 4,
+ "view": "low top-down",
+ "created_at": "2026-04-16T12:56:49.807063+00:00"
+ },
+ "frames": {
+ "rotations": {
+ "south": "rotations/south.png",
+ "west": "rotations/west.png",
+ "east": "rotations/east.png",
+ "north": "rotations/north.png"
+ },
+ "animations": {
+ "Running-8ac5d8e0": {
+ "west": [
+ "animations/Running-8ac5d8e0/west/frame_000.png",
+ "animations/Running-8ac5d8e0/west/frame_001.png",
+ "animations/Running-8ac5d8e0/west/frame_002.png",
+ "animations/Running-8ac5d8e0/west/frame_003.png"
+ ],
+ "north": [
+ "animations/Running-8ac5d8e0/north/frame_000.png",
+ "animations/Running-8ac5d8e0/north/frame_001.png",
+ "animations/Running-8ac5d8e0/north/frame_002.png",
+ "animations/Running-8ac5d8e0/north/frame_003.png"
+ ],
+ "south": [
+ "animations/Running-8ac5d8e0/south/frame_000.png",
+ "animations/Running-8ac5d8e0/south/frame_001.png",
+ "animations/Running-8ac5d8e0/south/frame_002.png",
+ "animations/Running-8ac5d8e0/south/frame_003.png"
+ ],
+ "east": [
+ "animations/Running-8ac5d8e0/east/frame_000.png",
+ "animations/Running-8ac5d8e0/east/frame_001.png",
+ "animations/Running-8ac5d8e0/east/frame_002.png",
+ "animations/Running-8ac5d8e0/east/frame_003.png"
+ ]
+ }
+ }
+ },
+ "export_version": "2.0",
+ "export_date": "2026-04-16T13:00:54.235142"
+}
\ No newline at end of file
diff --git a/client/assets/sprites/fracture/rotations/east.png b/client/assets/sprites/fracture/rotations/east.png
new file mode 100644
index 0000000..cfa77b1
Binary files /dev/null and b/client/assets/sprites/fracture/rotations/east.png differ
diff --git a/client/assets/sprites/fracture/rotations/north.png b/client/assets/sprites/fracture/rotations/north.png
new file mode 100644
index 0000000..f4c14b9
Binary files /dev/null and b/client/assets/sprites/fracture/rotations/north.png differ
diff --git a/client/assets/sprites/fracture/rotations/south.png b/client/assets/sprites/fracture/rotations/south.png
new file mode 100644
index 0000000..c13f20e
Binary files /dev/null and b/client/assets/sprites/fracture/rotations/south.png differ
diff --git a/client/assets/sprites/fracture/rotations/west.png b/client/assets/sprites/fracture/rotations/west.png
new file mode 100644
index 0000000..27e2fe7
Binary files /dev/null and b/client/assets/sprites/fracture/rotations/west.png differ
diff --git a/client/assets/sprites/kael/animations/attack/east/frame_000.png b/client/assets/sprites/kael/animations/attack/east/frame_000.png
new file mode 100644
index 0000000..0867fa4
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/east/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/attack/east/frame_001.png b/client/assets/sprites/kael/animations/attack/east/frame_001.png
new file mode 100644
index 0000000..754f368
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/east/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/attack/east/frame_002.png b/client/assets/sprites/kael/animations/attack/east/frame_002.png
new file mode 100644
index 0000000..150794d
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/east/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/attack/east/frame_003.png b/client/assets/sprites/kael/animations/attack/east/frame_003.png
new file mode 100644
index 0000000..5cfea95
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/east/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/attack/east/frame_004.png b/client/assets/sprites/kael/animations/attack/east/frame_004.png
new file mode 100644
index 0000000..7441871
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/east/frame_004.png differ
diff --git a/client/assets/sprites/kael/animations/attack/north/frame_000.png b/client/assets/sprites/kael/animations/attack/north/frame_000.png
new file mode 100644
index 0000000..836a3a1
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/north/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/attack/north/frame_001.png b/client/assets/sprites/kael/animations/attack/north/frame_001.png
new file mode 100644
index 0000000..fb5f8a2
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/north/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/attack/north/frame_002.png b/client/assets/sprites/kael/animations/attack/north/frame_002.png
new file mode 100644
index 0000000..d896875
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/north/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/attack/north/frame_003.png b/client/assets/sprites/kael/animations/attack/north/frame_003.png
new file mode 100644
index 0000000..90fcbd2
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/north/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/attack/north/frame_004.png b/client/assets/sprites/kael/animations/attack/north/frame_004.png
new file mode 100644
index 0000000..9939634
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/north/frame_004.png differ
diff --git a/client/assets/sprites/kael/animations/attack/south/frame_000.png b/client/assets/sprites/kael/animations/attack/south/frame_000.png
new file mode 100644
index 0000000..4028172
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/south/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/attack/south/frame_001.png b/client/assets/sprites/kael/animations/attack/south/frame_001.png
new file mode 100644
index 0000000..6c4117f
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/south/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/attack/south/frame_002.png b/client/assets/sprites/kael/animations/attack/south/frame_002.png
new file mode 100644
index 0000000..9d92cb8
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/south/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/attack/south/frame_003.png b/client/assets/sprites/kael/animations/attack/south/frame_003.png
new file mode 100644
index 0000000..dcdd34c
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/south/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/attack/south/frame_004.png b/client/assets/sprites/kael/animations/attack/south/frame_004.png
new file mode 100644
index 0000000..1efcd62
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/south/frame_004.png differ
diff --git a/client/assets/sprites/kael/animations/attack/west/frame_000.png b/client/assets/sprites/kael/animations/attack/west/frame_000.png
new file mode 100644
index 0000000..91f13ad
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/west/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/attack/west/frame_001.png b/client/assets/sprites/kael/animations/attack/west/frame_001.png
new file mode 100644
index 0000000..a8e126e
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/west/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/attack/west/frame_002.png b/client/assets/sprites/kael/animations/attack/west/frame_002.png
new file mode 100644
index 0000000..75f0932
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/west/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/attack/west/frame_003.png b/client/assets/sprites/kael/animations/attack/west/frame_003.png
new file mode 100644
index 0000000..1620a2d
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/west/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/attack/west/frame_004.png b/client/assets/sprites/kael/animations/attack/west/frame_004.png
new file mode 100644
index 0000000..eaf8646
Binary files /dev/null and b/client/assets/sprites/kael/animations/attack/west/frame_004.png differ
diff --git a/client/assets/sprites/kael/animations/dash/east/frame_000.png b/client/assets/sprites/kael/animations/dash/east/frame_000.png
new file mode 100644
index 0000000..0867fa4
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/east/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/dash/east/frame_001.png b/client/assets/sprites/kael/animations/dash/east/frame_001.png
new file mode 100644
index 0000000..9c3ead1
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/east/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/dash/east/frame_002.png b/client/assets/sprites/kael/animations/dash/east/frame_002.png
new file mode 100644
index 0000000..26183b6
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/east/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/dash/east/frame_003.png b/client/assets/sprites/kael/animations/dash/east/frame_003.png
new file mode 100644
index 0000000..8b39163
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/east/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/dash/east/frame_004.png b/client/assets/sprites/kael/animations/dash/east/frame_004.png
new file mode 100644
index 0000000..c8caeb8
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/east/frame_004.png differ
diff --git a/client/assets/sprites/kael/animations/dash/north/frame_000.png b/client/assets/sprites/kael/animations/dash/north/frame_000.png
new file mode 100644
index 0000000..836a3a1
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/north/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/dash/north/frame_001.png b/client/assets/sprites/kael/animations/dash/north/frame_001.png
new file mode 100644
index 0000000..86194a2
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/north/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/dash/north/frame_002.png b/client/assets/sprites/kael/animations/dash/north/frame_002.png
new file mode 100644
index 0000000..e8083a7
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/north/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/dash/north/frame_003.png b/client/assets/sprites/kael/animations/dash/north/frame_003.png
new file mode 100644
index 0000000..01a0592
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/north/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/dash/north/frame_004.png b/client/assets/sprites/kael/animations/dash/north/frame_004.png
new file mode 100644
index 0000000..9b1daff
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/north/frame_004.png differ
diff --git a/client/assets/sprites/kael/animations/dash/south/frame_000.png b/client/assets/sprites/kael/animations/dash/south/frame_000.png
new file mode 100644
index 0000000..4028172
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/south/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/dash/south/frame_001.png b/client/assets/sprites/kael/animations/dash/south/frame_001.png
new file mode 100644
index 0000000..86e625f
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/south/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/dash/south/frame_002.png b/client/assets/sprites/kael/animations/dash/south/frame_002.png
new file mode 100644
index 0000000..f7623cd
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/south/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/dash/south/frame_003.png b/client/assets/sprites/kael/animations/dash/south/frame_003.png
new file mode 100644
index 0000000..4c2aef1
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/south/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/dash/south/frame_004.png b/client/assets/sprites/kael/animations/dash/south/frame_004.png
new file mode 100644
index 0000000..dd21e6b
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/south/frame_004.png differ
diff --git a/client/assets/sprites/kael/animations/dash/west/frame_000.png b/client/assets/sprites/kael/animations/dash/west/frame_000.png
new file mode 100644
index 0000000..91f13ad
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/west/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/dash/west/frame_001.png b/client/assets/sprites/kael/animations/dash/west/frame_001.png
new file mode 100644
index 0000000..239605f
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/west/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/dash/west/frame_002.png b/client/assets/sprites/kael/animations/dash/west/frame_002.png
new file mode 100644
index 0000000..243bcf6
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/west/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/dash/west/frame_003.png b/client/assets/sprites/kael/animations/dash/west/frame_003.png
new file mode 100644
index 0000000..129e8c0
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/west/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/dash/west/frame_004.png b/client/assets/sprites/kael/animations/dash/west/frame_004.png
new file mode 100644
index 0000000..e7430bd
Binary files /dev/null and b/client/assets/sprites/kael/animations/dash/west/frame_004.png differ
diff --git a/client/assets/sprites/kael/animations/running/east/frame_000.png b/client/assets/sprites/kael/animations/running/east/frame_000.png
new file mode 100644
index 0000000..48df494
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/east/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/running/east/frame_001.png b/client/assets/sprites/kael/animations/running/east/frame_001.png
new file mode 100644
index 0000000..4f77083
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/east/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/running/east/frame_002.png b/client/assets/sprites/kael/animations/running/east/frame_002.png
new file mode 100644
index 0000000..912daa2
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/east/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/running/east/frame_003.png b/client/assets/sprites/kael/animations/running/east/frame_003.png
new file mode 100644
index 0000000..d4cda5d
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/east/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/running/frame_000.png b/client/assets/sprites/kael/animations/running/frame_000.png
new file mode 100644
index 0000000..dcfe455
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/running/frame_001.png b/client/assets/sprites/kael/animations/running/frame_001.png
new file mode 100644
index 0000000..6b7ba4b
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/running/frame_002.png b/client/assets/sprites/kael/animations/running/frame_002.png
new file mode 100644
index 0000000..a93c028
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/running/frame_003.png b/client/assets/sprites/kael/animations/running/frame_003.png
new file mode 100644
index 0000000..32dd68f
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/running/north/frame_000.png b/client/assets/sprites/kael/animations/running/north/frame_000.png
new file mode 100644
index 0000000..92b5759
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/north/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/running/north/frame_001.png b/client/assets/sprites/kael/animations/running/north/frame_001.png
new file mode 100644
index 0000000..b291539
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/north/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/running/north/frame_002.png b/client/assets/sprites/kael/animations/running/north/frame_002.png
new file mode 100644
index 0000000..9f076b5
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/north/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/running/north/frame_003.png b/client/assets/sprites/kael/animations/running/north/frame_003.png
new file mode 100644
index 0000000..8c3788c
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/north/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/running/south/frame_000.png b/client/assets/sprites/kael/animations/running/south/frame_000.png
new file mode 100644
index 0000000..dcfe455
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/south/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/running/south/frame_001.png b/client/assets/sprites/kael/animations/running/south/frame_001.png
new file mode 100644
index 0000000..6b7ba4b
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/south/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/running/south/frame_002.png b/client/assets/sprites/kael/animations/running/south/frame_002.png
new file mode 100644
index 0000000..a93c028
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/south/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/running/south/frame_003.png b/client/assets/sprites/kael/animations/running/south/frame_003.png
new file mode 100644
index 0000000..32dd68f
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/south/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/running/west/frame_000.png b/client/assets/sprites/kael/animations/running/west/frame_000.png
new file mode 100644
index 0000000..f760bf1
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/west/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/running/west/frame_001.png b/client/assets/sprites/kael/animations/running/west/frame_001.png
new file mode 100644
index 0000000..d4a0d37
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/west/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/running/west/frame_002.png b/client/assets/sprites/kael/animations/running/west/frame_002.png
new file mode 100644
index 0000000..d1a9899
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/west/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/running/west/frame_003.png b/client/assets/sprites/kael/animations/running/west/frame_003.png
new file mode 100644
index 0000000..91a8f25
Binary files /dev/null and b/client/assets/sprites/kael/animations/running/west/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/skill2/south/frame_000.png b/client/assets/sprites/kael/animations/skill2/south/frame_000.png
new file mode 100644
index 0000000..4028172
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill2/south/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/skill2/south/frame_001.png b/client/assets/sprites/kael/animations/skill2/south/frame_001.png
new file mode 100644
index 0000000..e1595f3
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill2/south/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/skill2/south/frame_002.png b/client/assets/sprites/kael/animations/skill2/south/frame_002.png
new file mode 100644
index 0000000..ebcefd7
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill2/south/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/skill2/south/frame_003.png b/client/assets/sprites/kael/animations/skill2/south/frame_003.png
new file mode 100644
index 0000000..f8de4f5
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill2/south/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/skill2/south/frame_004.png b/client/assets/sprites/kael/animations/skill2/south/frame_004.png
new file mode 100644
index 0000000..fe0e9ec
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill2/south/frame_004.png differ
diff --git a/client/assets/sprites/kael/animations/skill2/south/frame_005.png b/client/assets/sprites/kael/animations/skill2/south/frame_005.png
new file mode 100644
index 0000000..2d80ccf
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill2/south/frame_005.png differ
diff --git a/client/assets/sprites/kael/animations/skill2/south/frame_006.png b/client/assets/sprites/kael/animations/skill2/south/frame_006.png
new file mode 100644
index 0000000..9f2191a
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill2/south/frame_006.png differ
diff --git a/client/assets/sprites/kael/animations/skill2/south/frame_007.png b/client/assets/sprites/kael/animations/skill2/south/frame_007.png
new file mode 100644
index 0000000..0c147a3
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill2/south/frame_007.png differ
diff --git a/client/assets/sprites/kael/animations/skill2/south/frame_008.png b/client/assets/sprites/kael/animations/skill2/south/frame_008.png
new file mode 100644
index 0000000..39c7536
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill2/south/frame_008.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_000.png b/client/assets/sprites/kael/animations/skill3/south/frame_000.png
new file mode 100644
index 0000000..4028172
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_000.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_001.png b/client/assets/sprites/kael/animations/skill3/south/frame_001.png
new file mode 100644
index 0000000..0c20994
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_001.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_002.png b/client/assets/sprites/kael/animations/skill3/south/frame_002.png
new file mode 100644
index 0000000..222a6f7
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_002.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_003.png b/client/assets/sprites/kael/animations/skill3/south/frame_003.png
new file mode 100644
index 0000000..59e54fe
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_003.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_004.png b/client/assets/sprites/kael/animations/skill3/south/frame_004.png
new file mode 100644
index 0000000..afc863b
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_004.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_005.png b/client/assets/sprites/kael/animations/skill3/south/frame_005.png
new file mode 100644
index 0000000..00c223d
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_005.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_006.png b/client/assets/sprites/kael/animations/skill3/south/frame_006.png
new file mode 100644
index 0000000..2c27057
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_006.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_007.png b/client/assets/sprites/kael/animations/skill3/south/frame_007.png
new file mode 100644
index 0000000..55ce974
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_007.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_008.png b/client/assets/sprites/kael/animations/skill3/south/frame_008.png
new file mode 100644
index 0000000..e8b85f7
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_008.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_009.png b/client/assets/sprites/kael/animations/skill3/south/frame_009.png
new file mode 100644
index 0000000..aac5ffa
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_009.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_010.png b/client/assets/sprites/kael/animations/skill3/south/frame_010.png
new file mode 100644
index 0000000..8b0ef73
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_010.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_011.png b/client/assets/sprites/kael/animations/skill3/south/frame_011.png
new file mode 100644
index 0000000..a8b2d84
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_011.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_012.png b/client/assets/sprites/kael/animations/skill3/south/frame_012.png
new file mode 100644
index 0000000..1c990f6
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_012.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_013.png b/client/assets/sprites/kael/animations/skill3/south/frame_013.png
new file mode 100644
index 0000000..86d72dc
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_013.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_014.png b/client/assets/sprites/kael/animations/skill3/south/frame_014.png
new file mode 100644
index 0000000..318e586
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_014.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_015.png b/client/assets/sprites/kael/animations/skill3/south/frame_015.png
new file mode 100644
index 0000000..43c1fed
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_015.png differ
diff --git a/client/assets/sprites/kael/animations/skill3/south/frame_016.png b/client/assets/sprites/kael/animations/skill3/south/frame_016.png
new file mode 100644
index 0000000..1dc90fe
Binary files /dev/null and b/client/assets/sprites/kael/animations/skill3/south/frame_016.png differ
diff --git a/client/assets/sprites/kael/rotations/east.png b/client/assets/sprites/kael/rotations/east.png
new file mode 100644
index 0000000..a52afb8
Binary files /dev/null and b/client/assets/sprites/kael/rotations/east.png differ
diff --git a/client/assets/sprites/kael/rotations/north.png b/client/assets/sprites/kael/rotations/north.png
new file mode 100644
index 0000000..1069e10
Binary files /dev/null and b/client/assets/sprites/kael/rotations/north.png differ
diff --git a/client/assets/sprites/kael/rotations/south.png b/client/assets/sprites/kael/rotations/south.png
new file mode 100644
index 0000000..60d1bab
Binary files /dev/null and b/client/assets/sprites/kael/rotations/south.png differ
diff --git a/client/assets/sprites/kael/rotations/west.png b/client/assets/sprites/kael/rotations/west.png
new file mode 100644
index 0000000..69d75fd
Binary files /dev/null and b/client/assets/sprites/kael/rotations/west.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_000.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_000.png
new file mode 100644
index 0000000..981896e
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_000.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_001.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_001.png
new file mode 100644
index 0000000..cd7e34f
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_001.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_002.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_002.png
new file mode 100644
index 0000000..5a90b93
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_002.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_003.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_003.png
new file mode 100644
index 0000000..f825659
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_003.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_004.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_004.png
new file mode 100644
index 0000000..28ebee7
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_004.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_005.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_005.png
new file mode 100644
index 0000000..55fd9c0
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_005.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_006.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_006.png
new file mode 100644
index 0000000..01868b7
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_006.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_007.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_007.png
new file mode 100644
index 0000000..9ba80b7
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_007.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_008.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_008.png
new file mode 100644
index 0000000..bfef0bb
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_008.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_000.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_000.png
new file mode 100644
index 0000000..0ba1e88
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_000.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_001.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_001.png
new file mode 100644
index 0000000..72e0ecf
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_001.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_002.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_002.png
new file mode 100644
index 0000000..3dfe865
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_002.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_003.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_003.png
new file mode 100644
index 0000000..c58e3cf
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_003.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_004.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_004.png
new file mode 100644
index 0000000..fe83a5e
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_004.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_005.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_005.png
new file mode 100644
index 0000000..8c84a38
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_005.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_006.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_006.png
new file mode 100644
index 0000000..c1ddc03
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_006.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_007.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_007.png
new file mode 100644
index 0000000..909389b
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_007.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_008.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_008.png
new file mode 100644
index 0000000..32ead17
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_008.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_000.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_000.png
new file mode 100644
index 0000000..bd75e50
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_000.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_001.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_001.png
new file mode 100644
index 0000000..5ca7dd7
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_001.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_002.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_002.png
new file mode 100644
index 0000000..9510a1b
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_002.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_003.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_003.png
new file mode 100644
index 0000000..835d518
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_003.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_004.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_004.png
new file mode 100644
index 0000000..531b904
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_004.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_005.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_005.png
new file mode 100644
index 0000000..d027f4c
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_005.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_006.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_006.png
new file mode 100644
index 0000000..59ea921
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_006.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_007.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_007.png
new file mode 100644
index 0000000..8fc7dfe
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_007.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_008.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_008.png
new file mode 100644
index 0000000..b61272b
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_008.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_000.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_000.png
new file mode 100644
index 0000000..0fea246
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_000.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_001.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_001.png
new file mode 100644
index 0000000..4b82a72
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_001.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_002.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_002.png
new file mode 100644
index 0000000..5651fe3
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_002.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_003.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_003.png
new file mode 100644
index 0000000..9728fa6
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_003.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_004.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_004.png
new file mode 100644
index 0000000..ddaab56
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_004.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_005.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_005.png
new file mode 100644
index 0000000..141bfaa
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_005.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_006.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_006.png
new file mode 100644
index 0000000..8a26fc8
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_006.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_007.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_007.png
new file mode 100644
index 0000000..f7b2786
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_007.png differ
diff --git a/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_008.png b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_008.png
new file mode 100644
index 0000000..16e72a9
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_008.png differ
diff --git a/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_000.png b/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_000.png
new file mode 100644
index 0000000..0cd8441
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_000.png differ
diff --git a/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_001.png b/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_001.png
new file mode 100644
index 0000000..bf292ac
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_001.png differ
diff --git a/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_002.png b/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_002.png
new file mode 100644
index 0000000..f6360ed
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_002.png differ
diff --git a/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_003.png b/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_003.png
new file mode 100644
index 0000000..c62bfa9
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_003.png differ
diff --git a/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_004.png b/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_004.png
new file mode 100644
index 0000000..596f7ec
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_004.png differ
diff --git a/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_005.png b/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_005.png
new file mode 100644
index 0000000..b6df468
Binary files /dev/null and b/client/assets/sprites/morveth/animations/Running-de5993f1/south/frame_005.png differ
diff --git a/client/assets/sprites/morveth/metadata.json b/client/assets/sprites/morveth/metadata.json
new file mode 100644
index 0000000..b814a9f
--- /dev/null
+++ b/client/assets/sprites/morveth/metadata.json
@@ -0,0 +1,83 @@
+{
+ "character": {
+ "id": "7e6c7399-64e8-4630-9e8c-cffb48cc33a9",
+ "name": "Morveth is an ancient and colossal corrupted warlord, far larger and heavier than Vexaris. Massive hunched body, twice as wide as it is tall, built like a fortress. The torso is encased in thick black\n obsidian plate armor, cracked and fused directly into the flesh beneath \u2014 there is no separation between armor and body. Eight massive tentacle-like appendages emerge from the back and shoulders, each tipped\n with a jagged hooked blade. Three eyes arranged vertically on the face, all glowing deep crimson. No mouth visible, just smooth cracked black stone where the lower face should be. The arms are huge and\n disproportionately long, dragging slightly at rest. The legs are short, thick pillars of dark rock. Eight gemstones embedded across the chest, alternating between deep red and sickly green, pulsing slowly.\n Dark purple smoke seeps from every joint and crack in the armor.",
+ "prompt": "Morveth is an ancient and colossal corrupted warlord, far larger and heavier than Vexaris. Massive hunched body, twice as wide as it is tall, built like a fortress. The torso is encased in thick black\n obsidian plate armor, cracked and fused directly into the flesh beneath \u2014 there is no separation between armor and body. Eight massive tentacle-like appendages emerge from the back and shoulders, each tipped\n with a jagged hooked blade. Three eyes arranged vertically on the face, all glowing deep crimson. No mouth visible, just smooth cracked black stone where the lower face should be. The arms are huge and\n disproportionately long, dragging slightly at rest. The legs are short, thick pillars of dark rock. Eight gemstones embedded across the chest, alternating between deep red and sickly green, pulsing slowly.\n Dark purple smoke seeps from every joint and crack in the armor.",
+ "size": {
+ "width": 92,
+ "height": 92
+ },
+ "template_id": "mannequin",
+ "directions": 4,
+ "view": "low top-down",
+ "created_at": "2026-04-29T10:33:37.739007+00:00"
+ },
+ "frames": {
+ "rotations": {
+ "south": "rotations/south.png",
+ "west": "rotations/west.png",
+ "east": "rotations/east.png",
+ "north": "rotations/north.png"
+ },
+ "animations": {
+ "Running-de5993f1": {
+ "south": [
+ "animations/Running-de5993f1/south/frame_000.png",
+ "animations/Running-de5993f1/south/frame_001.png",
+ "animations/Running-de5993f1/south/frame_002.png",
+ "animations/Running-de5993f1/south/frame_003.png",
+ "animations/Running-de5993f1/south/frame_004.png",
+ "animations/Running-de5993f1/south/frame_005.png"
+ ]
+ },
+ "Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c": {
+ "east": [
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_000.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_001.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_002.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_003.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_004.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_005.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_006.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_007.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/east/frame_008.png"
+ ],
+ "south": [
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_000.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_001.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_002.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_003.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_004.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_005.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_006.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_007.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/south/frame_008.png"
+ ],
+ "west": [
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_000.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_001.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_002.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_003.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_004.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_005.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_006.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_007.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/west/frame_008.png"
+ ],
+ "north": [
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_000.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_001.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_002.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_003.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_004.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_005.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_006.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_007.png",
+ "animations/Morveth_raises_one_enormous_arm_high_pauses_for_a-ccb9856c/north/frame_008.png"
+ ]
+ }
+ }
+ },
+ "export_version": "2.0",
+ "export_date": "2026-05-03T15:13:12.276413"
+}
\ No newline at end of file
diff --git a/client/assets/sprites/morveth/rotations/east.png b/client/assets/sprites/morveth/rotations/east.png
new file mode 100644
index 0000000..4f6248b
Binary files /dev/null and b/client/assets/sprites/morveth/rotations/east.png differ
diff --git a/client/assets/sprites/morveth/rotations/north.png b/client/assets/sprites/morveth/rotations/north.png
new file mode 100644
index 0000000..88f98bb
Binary files /dev/null and b/client/assets/sprites/morveth/rotations/north.png differ
diff --git a/client/assets/sprites/morveth/rotations/south.png b/client/assets/sprites/morveth/rotations/south.png
new file mode 100644
index 0000000..ea96c5b
Binary files /dev/null and b/client/assets/sprites/morveth/rotations/south.png differ
diff --git a/client/assets/sprites/morveth/rotations/west.png b/client/assets/sprites/morveth/rotations/west.png
new file mode 100644
index 0000000..0f404e5
Binary files /dev/null and b/client/assets/sprites/morveth/rotations/west.png differ
diff --git a/client/assets/sprites/rampant/animations/running/east/frame_000.png b/client/assets/sprites/rampant/animations/running/east/frame_000.png
new file mode 100644
index 0000000..0174e1f
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/east/frame_000.png differ
diff --git a/client/assets/sprites/rampant/animations/running/east/frame_001.png b/client/assets/sprites/rampant/animations/running/east/frame_001.png
new file mode 100644
index 0000000..ff18b22
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/east/frame_001.png differ
diff --git a/client/assets/sprites/rampant/animations/running/east/frame_002.png b/client/assets/sprites/rampant/animations/running/east/frame_002.png
new file mode 100644
index 0000000..a11106e
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/east/frame_002.png differ
diff --git a/client/assets/sprites/rampant/animations/running/east/frame_003.png b/client/assets/sprites/rampant/animations/running/east/frame_003.png
new file mode 100644
index 0000000..2f22617
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/east/frame_003.png differ
diff --git a/client/assets/sprites/rampant/animations/running/east/frame_004.png b/client/assets/sprites/rampant/animations/running/east/frame_004.png
new file mode 100644
index 0000000..b43d2b0
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/east/frame_004.png differ
diff --git a/client/assets/sprites/rampant/animations/running/east/frame_005.png b/client/assets/sprites/rampant/animations/running/east/frame_005.png
new file mode 100644
index 0000000..92becab
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/east/frame_005.png differ
diff --git a/client/assets/sprites/rampant/animations/running/north/frame_000.png b/client/assets/sprites/rampant/animations/running/north/frame_000.png
new file mode 100644
index 0000000..043c3ec
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/north/frame_000.png differ
diff --git a/client/assets/sprites/rampant/animations/running/north/frame_001.png b/client/assets/sprites/rampant/animations/running/north/frame_001.png
new file mode 100644
index 0000000..35b672c
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/north/frame_001.png differ
diff --git a/client/assets/sprites/rampant/animations/running/north/frame_002.png b/client/assets/sprites/rampant/animations/running/north/frame_002.png
new file mode 100644
index 0000000..7125a1a
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/north/frame_002.png differ
diff --git a/client/assets/sprites/rampant/animations/running/north/frame_003.png b/client/assets/sprites/rampant/animations/running/north/frame_003.png
new file mode 100644
index 0000000..ddbf692
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/north/frame_003.png differ
diff --git a/client/assets/sprites/rampant/animations/running/north/frame_004.png b/client/assets/sprites/rampant/animations/running/north/frame_004.png
new file mode 100644
index 0000000..203b836
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/north/frame_004.png differ
diff --git a/client/assets/sprites/rampant/animations/running/north/frame_005.png b/client/assets/sprites/rampant/animations/running/north/frame_005.png
new file mode 100644
index 0000000..3468f69
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/north/frame_005.png differ
diff --git a/client/assets/sprites/rampant/animations/running/south/frame_000.png b/client/assets/sprites/rampant/animations/running/south/frame_000.png
new file mode 100644
index 0000000..60447c8
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/south/frame_000.png differ
diff --git a/client/assets/sprites/rampant/animations/running/south/frame_001.png b/client/assets/sprites/rampant/animations/running/south/frame_001.png
new file mode 100644
index 0000000..8dbe4b6
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/south/frame_001.png differ
diff --git a/client/assets/sprites/rampant/animations/running/south/frame_002.png b/client/assets/sprites/rampant/animations/running/south/frame_002.png
new file mode 100644
index 0000000..802cd4c
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/south/frame_002.png differ
diff --git a/client/assets/sprites/rampant/animations/running/south/frame_003.png b/client/assets/sprites/rampant/animations/running/south/frame_003.png
new file mode 100644
index 0000000..7fbf55a
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/south/frame_003.png differ
diff --git a/client/assets/sprites/rampant/animations/running/south/frame_004.png b/client/assets/sprites/rampant/animations/running/south/frame_004.png
new file mode 100644
index 0000000..bdc3a22
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/south/frame_004.png differ
diff --git a/client/assets/sprites/rampant/animations/running/south/frame_005.png b/client/assets/sprites/rampant/animations/running/south/frame_005.png
new file mode 100644
index 0000000..8c0acdd
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/south/frame_005.png differ
diff --git a/client/assets/sprites/rampant/animations/running/west/frame_000.png b/client/assets/sprites/rampant/animations/running/west/frame_000.png
new file mode 100644
index 0000000..b7fc11d
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/west/frame_000.png differ
diff --git a/client/assets/sprites/rampant/animations/running/west/frame_001.png b/client/assets/sprites/rampant/animations/running/west/frame_001.png
new file mode 100644
index 0000000..a482480
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/west/frame_001.png differ
diff --git a/client/assets/sprites/rampant/animations/running/west/frame_002.png b/client/assets/sprites/rampant/animations/running/west/frame_002.png
new file mode 100644
index 0000000..23365ce
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/west/frame_002.png differ
diff --git a/client/assets/sprites/rampant/animations/running/west/frame_003.png b/client/assets/sprites/rampant/animations/running/west/frame_003.png
new file mode 100644
index 0000000..f780602
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/west/frame_003.png differ
diff --git a/client/assets/sprites/rampant/animations/running/west/frame_004.png b/client/assets/sprites/rampant/animations/running/west/frame_004.png
new file mode 100644
index 0000000..232f35a
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/west/frame_004.png differ
diff --git a/client/assets/sprites/rampant/animations/running/west/frame_005.png b/client/assets/sprites/rampant/animations/running/west/frame_005.png
new file mode 100644
index 0000000..ded7657
Binary files /dev/null and b/client/assets/sprites/rampant/animations/running/west/frame_005.png differ
diff --git a/client/assets/sprites/rampant/metadata.json b/client/assets/sprites/rampant/metadata.json
new file mode 100644
index 0000000..2b01838
--- /dev/null
+++ b/client/assets/sprites/rampant/metadata.json
@@ -0,0 +1,61 @@
+{
+ "character": {
+ "id": "fbd59661-83c7-4ee0-9758-7220de825133",
+ "name": "A corrupted soul turned feral predator, small and low to the ground, almost crawling. Hunched humanoid body with elongated limbs built for lunging forward. Translucent grey-smoke skin with glowing orange\n veins pulsing across the torso and limbs. No clothing, just the bare corrupted form. Wide open jaw with jagged uneven teeth, two bright orange glowing eyes. Smaller and more compact than the Fracture enemy.\n",
+ "prompt": "A corrupted soul turned feral predator, small and low to the ground, almost crawling. Hunched humanoid body with elongated limbs built for lunging forward. Translucent grey-smoke skin with glowing orange\n veins pulsing across the torso and limbs. No clothing, just the bare corrupted form. Wide open jaw with jagged uneven teeth, two bright orange glowing eyes. Smaller and more compact than the Fracture enemy.\n",
+ "size": {
+ "width": 48,
+ "height": 48
+ },
+ "template_id": "mannequin",
+ "directions": 4,
+ "view": "low top-down",
+ "created_at": "2026-04-17T13:58:19.307396+00:00"
+ },
+ "frames": {
+ "rotations": {
+ "south": "rotations/south.png",
+ "west": "rotations/west.png",
+ "east": "rotations/east.png",
+ "north": "rotations/north.png"
+ },
+ "animations": {
+ "Running-835ba967": {
+ "south": [
+ "animations/Running-835ba967/south/frame_000.png",
+ "animations/Running-835ba967/south/frame_001.png",
+ "animations/Running-835ba967/south/frame_002.png",
+ "animations/Running-835ba967/south/frame_003.png",
+ "animations/Running-835ba967/south/frame_004.png",
+ "animations/Running-835ba967/south/frame_005.png"
+ ],
+ "north": [
+ "animations/Running-835ba967/north/frame_000.png",
+ "animations/Running-835ba967/north/frame_001.png",
+ "animations/Running-835ba967/north/frame_002.png",
+ "animations/Running-835ba967/north/frame_003.png",
+ "animations/Running-835ba967/north/frame_004.png",
+ "animations/Running-835ba967/north/frame_005.png"
+ ],
+ "east": [
+ "animations/Running-835ba967/east/frame_000.png",
+ "animations/Running-835ba967/east/frame_001.png",
+ "animations/Running-835ba967/east/frame_002.png",
+ "animations/Running-835ba967/east/frame_003.png",
+ "animations/Running-835ba967/east/frame_004.png",
+ "animations/Running-835ba967/east/frame_005.png"
+ ],
+ "west": [
+ "animations/Running-835ba967/west/frame_000.png",
+ "animations/Running-835ba967/west/frame_001.png",
+ "animations/Running-835ba967/west/frame_002.png",
+ "animations/Running-835ba967/west/frame_003.png",
+ "animations/Running-835ba967/west/frame_004.png",
+ "animations/Running-835ba967/west/frame_005.png"
+ ]
+ }
+ }
+ },
+ "export_version": "2.0",
+ "export_date": "2026-04-17T14:06:15.363084"
+}
\ No newline at end of file
diff --git a/client/assets/sprites/rampant/rotations/east.png b/client/assets/sprites/rampant/rotations/east.png
new file mode 100644
index 0000000..f4fa13f
Binary files /dev/null and b/client/assets/sprites/rampant/rotations/east.png differ
diff --git a/client/assets/sprites/rampant/rotations/north.png b/client/assets/sprites/rampant/rotations/north.png
new file mode 100644
index 0000000..ff1fa23
Binary files /dev/null and b/client/assets/sprites/rampant/rotations/north.png differ
diff --git a/client/assets/sprites/rampant/rotations/south.png b/client/assets/sprites/rampant/rotations/south.png
new file mode 100644
index 0000000..170af49
Binary files /dev/null and b/client/assets/sprites/rampant/rotations/south.png differ
diff --git a/client/assets/sprites/rampant/rotations/west.png b/client/assets/sprites/rampant/rotations/west.png
new file mode 100644
index 0000000..cd9db58
Binary files /dev/null and b/client/assets/sprites/rampant/rotations/west.png differ
diff --git a/client/assets/sprites/seris/animations/attack/east/frame_000.png b/client/assets/sprites/seris/animations/attack/east/frame_000.png
new file mode 100644
index 0000000..12a4a2d
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/east/frame_000.png differ
diff --git a/client/assets/sprites/seris/animations/attack/east/frame_001.png b/client/assets/sprites/seris/animations/attack/east/frame_001.png
new file mode 100644
index 0000000..87ced98
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/east/frame_001.png differ
diff --git a/client/assets/sprites/seris/animations/attack/east/frame_002.png b/client/assets/sprites/seris/animations/attack/east/frame_002.png
new file mode 100644
index 0000000..aab6d40
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/east/frame_002.png differ
diff --git a/client/assets/sprites/seris/animations/attack/east/frame_003.png b/client/assets/sprites/seris/animations/attack/east/frame_003.png
new file mode 100644
index 0000000..76f6cca
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/east/frame_003.png differ
diff --git a/client/assets/sprites/seris/animations/attack/east/frame_004.png b/client/assets/sprites/seris/animations/attack/east/frame_004.png
new file mode 100644
index 0000000..4a9cc5b
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/east/frame_004.png differ
diff --git a/client/assets/sprites/seris/animations/attack/east/frame_005.png b/client/assets/sprites/seris/animations/attack/east/frame_005.png
new file mode 100644
index 0000000..f4a5901
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/east/frame_005.png differ
diff --git a/client/assets/sprites/seris/animations/attack/east/frame_006.png b/client/assets/sprites/seris/animations/attack/east/frame_006.png
new file mode 100644
index 0000000..f6aa8cd
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/east/frame_006.png differ
diff --git a/client/assets/sprites/seris/animations/attack/north/frame_000.png b/client/assets/sprites/seris/animations/attack/north/frame_000.png
new file mode 100644
index 0000000..bb09e34
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/north/frame_000.png differ
diff --git a/client/assets/sprites/seris/animations/attack/north/frame_001.png b/client/assets/sprites/seris/animations/attack/north/frame_001.png
new file mode 100644
index 0000000..a2fb37a
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/north/frame_001.png differ
diff --git a/client/assets/sprites/seris/animations/attack/north/frame_002.png b/client/assets/sprites/seris/animations/attack/north/frame_002.png
new file mode 100644
index 0000000..9e578ab
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/north/frame_002.png differ
diff --git a/client/assets/sprites/seris/animations/attack/north/frame_003.png b/client/assets/sprites/seris/animations/attack/north/frame_003.png
new file mode 100644
index 0000000..9eba388
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/north/frame_003.png differ
diff --git a/client/assets/sprites/seris/animations/attack/north/frame_004.png b/client/assets/sprites/seris/animations/attack/north/frame_004.png
new file mode 100644
index 0000000..98d5f6c
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/north/frame_004.png differ
diff --git a/client/assets/sprites/seris/animations/attack/north/frame_005.png b/client/assets/sprites/seris/animations/attack/north/frame_005.png
new file mode 100644
index 0000000..36f60cd
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/north/frame_005.png differ
diff --git a/client/assets/sprites/seris/animations/attack/north/frame_006.png b/client/assets/sprites/seris/animations/attack/north/frame_006.png
new file mode 100644
index 0000000..9729b6c
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/north/frame_006.png differ
diff --git a/client/assets/sprites/seris/animations/attack/south/frame_000.png b/client/assets/sprites/seris/animations/attack/south/frame_000.png
new file mode 100644
index 0000000..56dd8e9
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/south/frame_000.png differ
diff --git a/client/assets/sprites/seris/animations/attack/south/frame_001.png b/client/assets/sprites/seris/animations/attack/south/frame_001.png
new file mode 100644
index 0000000..6540b82
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/south/frame_001.png differ
diff --git a/client/assets/sprites/seris/animations/attack/south/frame_002.png b/client/assets/sprites/seris/animations/attack/south/frame_002.png
new file mode 100644
index 0000000..d78f70f
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/south/frame_002.png differ
diff --git a/client/assets/sprites/seris/animations/attack/south/frame_003.png b/client/assets/sprites/seris/animations/attack/south/frame_003.png
new file mode 100644
index 0000000..a9455d7
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/south/frame_003.png differ
diff --git a/client/assets/sprites/seris/animations/attack/south/frame_004.png b/client/assets/sprites/seris/animations/attack/south/frame_004.png
new file mode 100644
index 0000000..7f90cb9
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/south/frame_004.png differ
diff --git a/client/assets/sprites/seris/animations/attack/south/frame_005.png b/client/assets/sprites/seris/animations/attack/south/frame_005.png
new file mode 100644
index 0000000..71348ed
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/south/frame_005.png differ
diff --git a/client/assets/sprites/seris/animations/attack/south/frame_006.png b/client/assets/sprites/seris/animations/attack/south/frame_006.png
new file mode 100644
index 0000000..af4616a
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/south/frame_006.png differ
diff --git a/client/assets/sprites/seris/animations/attack/west/frame_000.png b/client/assets/sprites/seris/animations/attack/west/frame_000.png
new file mode 100644
index 0000000..25c0f94
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/west/frame_000.png differ
diff --git a/client/assets/sprites/seris/animations/attack/west/frame_001.png b/client/assets/sprites/seris/animations/attack/west/frame_001.png
new file mode 100644
index 0000000..573cc0e
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/west/frame_001.png differ
diff --git a/client/assets/sprites/seris/animations/attack/west/frame_002.png b/client/assets/sprites/seris/animations/attack/west/frame_002.png
new file mode 100644
index 0000000..cc6c5c8
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/west/frame_002.png differ
diff --git a/client/assets/sprites/seris/animations/attack/west/frame_003.png b/client/assets/sprites/seris/animations/attack/west/frame_003.png
new file mode 100644
index 0000000..de369bf
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/west/frame_003.png differ
diff --git a/client/assets/sprites/seris/animations/attack/west/frame_004.png b/client/assets/sprites/seris/animations/attack/west/frame_004.png
new file mode 100644
index 0000000..c38c576
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/west/frame_004.png differ
diff --git a/client/assets/sprites/seris/animations/attack/west/frame_005.png b/client/assets/sprites/seris/animations/attack/west/frame_005.png
new file mode 100644
index 0000000..fe2224a
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/west/frame_005.png differ
diff --git a/client/assets/sprites/seris/animations/attack/west/frame_006.png b/client/assets/sprites/seris/animations/attack/west/frame_006.png
new file mode 100644
index 0000000..1ee375c
Binary files /dev/null and b/client/assets/sprites/seris/animations/attack/west/frame_006.png differ
diff --git a/client/assets/sprites/seris/animations/running/east/frame_000.png b/client/assets/sprites/seris/animations/running/east/frame_000.png
new file mode 100644
index 0000000..b3fd6ad
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/east/frame_000.png differ
diff --git a/client/assets/sprites/seris/animations/running/east/frame_001.png b/client/assets/sprites/seris/animations/running/east/frame_001.png
new file mode 100644
index 0000000..c11e6d5
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/east/frame_001.png differ
diff --git a/client/assets/sprites/seris/animations/running/east/frame_002.png b/client/assets/sprites/seris/animations/running/east/frame_002.png
new file mode 100644
index 0000000..be4a09b
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/east/frame_002.png differ
diff --git a/client/assets/sprites/seris/animations/running/east/frame_003.png b/client/assets/sprites/seris/animations/running/east/frame_003.png
new file mode 100644
index 0000000..d91587f
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/east/frame_003.png differ
diff --git a/client/assets/sprites/seris/animations/running/east/frame_004.png b/client/assets/sprites/seris/animations/running/east/frame_004.png
new file mode 100644
index 0000000..28dd5cc
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/east/frame_004.png differ
diff --git a/client/assets/sprites/seris/animations/running/east/frame_005.png b/client/assets/sprites/seris/animations/running/east/frame_005.png
new file mode 100644
index 0000000..88fbdb4
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/east/frame_005.png differ
diff --git a/client/assets/sprites/seris/animations/running/north/frame_000.png b/client/assets/sprites/seris/animations/running/north/frame_000.png
new file mode 100644
index 0000000..ee3bcce
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/north/frame_000.png differ
diff --git a/client/assets/sprites/seris/animations/running/north/frame_001.png b/client/assets/sprites/seris/animations/running/north/frame_001.png
new file mode 100644
index 0000000..57dc573
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/north/frame_001.png differ
diff --git a/client/assets/sprites/seris/animations/running/north/frame_002.png b/client/assets/sprites/seris/animations/running/north/frame_002.png
new file mode 100644
index 0000000..5cd1770
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/north/frame_002.png differ
diff --git a/client/assets/sprites/seris/animations/running/north/frame_003.png b/client/assets/sprites/seris/animations/running/north/frame_003.png
new file mode 100644
index 0000000..b743946
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/north/frame_003.png differ
diff --git a/client/assets/sprites/seris/animations/running/north/frame_004.png b/client/assets/sprites/seris/animations/running/north/frame_004.png
new file mode 100644
index 0000000..780442a
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/north/frame_004.png differ
diff --git a/client/assets/sprites/seris/animations/running/north/frame_005.png b/client/assets/sprites/seris/animations/running/north/frame_005.png
new file mode 100644
index 0000000..d6d4443
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/north/frame_005.png differ
diff --git a/client/assets/sprites/seris/animations/running/south/frame_000.png b/client/assets/sprites/seris/animations/running/south/frame_000.png
new file mode 100644
index 0000000..64ed095
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/south/frame_000.png differ
diff --git a/client/assets/sprites/seris/animations/running/south/frame_001.png b/client/assets/sprites/seris/animations/running/south/frame_001.png
new file mode 100644
index 0000000..1265866
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/south/frame_001.png differ
diff --git a/client/assets/sprites/seris/animations/running/south/frame_002.png b/client/assets/sprites/seris/animations/running/south/frame_002.png
new file mode 100644
index 0000000..9899f01
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/south/frame_002.png differ
diff --git a/client/assets/sprites/seris/animations/running/south/frame_003.png b/client/assets/sprites/seris/animations/running/south/frame_003.png
new file mode 100644
index 0000000..3f7a338
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/south/frame_003.png differ
diff --git a/client/assets/sprites/seris/animations/running/south/frame_004.png b/client/assets/sprites/seris/animations/running/south/frame_004.png
new file mode 100644
index 0000000..5eeb273
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/south/frame_004.png differ
diff --git a/client/assets/sprites/seris/animations/running/south/frame_005.png b/client/assets/sprites/seris/animations/running/south/frame_005.png
new file mode 100644
index 0000000..1022e0f
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/south/frame_005.png differ
diff --git a/client/assets/sprites/seris/animations/running/west/frame_000.png b/client/assets/sprites/seris/animations/running/west/frame_000.png
new file mode 100644
index 0000000..dbba088
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/west/frame_000.png differ
diff --git a/client/assets/sprites/seris/animations/running/west/frame_001.png b/client/assets/sprites/seris/animations/running/west/frame_001.png
new file mode 100644
index 0000000..bfce1ef
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/west/frame_001.png differ
diff --git a/client/assets/sprites/seris/animations/running/west/frame_002.png b/client/assets/sprites/seris/animations/running/west/frame_002.png
new file mode 100644
index 0000000..a7e54f9
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/west/frame_002.png differ
diff --git a/client/assets/sprites/seris/animations/running/west/frame_003.png b/client/assets/sprites/seris/animations/running/west/frame_003.png
new file mode 100644
index 0000000..9ab2587
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/west/frame_003.png differ
diff --git a/client/assets/sprites/seris/animations/running/west/frame_004.png b/client/assets/sprites/seris/animations/running/west/frame_004.png
new file mode 100644
index 0000000..6a1d139
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/west/frame_004.png differ
diff --git a/client/assets/sprites/seris/animations/running/west/frame_005.png b/client/assets/sprites/seris/animations/running/west/frame_005.png
new file mode 100644
index 0000000..7a2f67f
Binary files /dev/null and b/client/assets/sprites/seris/animations/running/west/frame_005.png differ
diff --git a/client/assets/sprites/seris/animations/skill1/south/frame_000.png b/client/assets/sprites/seris/animations/skill1/south/frame_000.png
new file mode 100644
index 0000000..56dd8e9
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill1/south/frame_000.png differ
diff --git a/client/assets/sprites/seris/animations/skill1/south/frame_001.png b/client/assets/sprites/seris/animations/skill1/south/frame_001.png
new file mode 100644
index 0000000..335993a
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill1/south/frame_001.png differ
diff --git a/client/assets/sprites/seris/animations/skill1/south/frame_002.png b/client/assets/sprites/seris/animations/skill1/south/frame_002.png
new file mode 100644
index 0000000..0814b7d
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill1/south/frame_002.png differ
diff --git a/client/assets/sprites/seris/animations/skill1/south/frame_003.png b/client/assets/sprites/seris/animations/skill1/south/frame_003.png
new file mode 100644
index 0000000..34937e2
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill1/south/frame_003.png differ
diff --git a/client/assets/sprites/seris/animations/skill1/south/frame_004.png b/client/assets/sprites/seris/animations/skill1/south/frame_004.png
new file mode 100644
index 0000000..3a81179
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill1/south/frame_004.png differ
diff --git a/client/assets/sprites/seris/animations/skill1/south/frame_005.png b/client/assets/sprites/seris/animations/skill1/south/frame_005.png
new file mode 100644
index 0000000..0d6a4af
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill1/south/frame_005.png differ
diff --git a/client/assets/sprites/seris/animations/skill1/south/frame_006.png b/client/assets/sprites/seris/animations/skill1/south/frame_006.png
new file mode 100644
index 0000000..7042f7b
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill1/south/frame_006.png differ
diff --git a/client/assets/sprites/seris/animations/skill1/south/frame_007.png b/client/assets/sprites/seris/animations/skill1/south/frame_007.png
new file mode 100644
index 0000000..f35222e
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill1/south/frame_007.png differ
diff --git a/client/assets/sprites/seris/animations/skill1/south/frame_008.png b/client/assets/sprites/seris/animations/skill1/south/frame_008.png
new file mode 100644
index 0000000..169dcd4
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill1/south/frame_008.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_000.png b/client/assets/sprites/seris/animations/skill2/south/frame_000.png
new file mode 100644
index 0000000..56dd8e9
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_000.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_001.png b/client/assets/sprites/seris/animations/skill2/south/frame_001.png
new file mode 100644
index 0000000..8c0e19a
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_001.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_002.png b/client/assets/sprites/seris/animations/skill2/south/frame_002.png
new file mode 100644
index 0000000..829360e
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_002.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_003.png b/client/assets/sprites/seris/animations/skill2/south/frame_003.png
new file mode 100644
index 0000000..e161c89
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_003.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_004.png b/client/assets/sprites/seris/animations/skill2/south/frame_004.png
new file mode 100644
index 0000000..6498e7b
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_004.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_005.png b/client/assets/sprites/seris/animations/skill2/south/frame_005.png
new file mode 100644
index 0000000..294ad28
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_005.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_006.png b/client/assets/sprites/seris/animations/skill2/south/frame_006.png
new file mode 100644
index 0000000..bdadac1
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_006.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_007.png b/client/assets/sprites/seris/animations/skill2/south/frame_007.png
new file mode 100644
index 0000000..04baece
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_007.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_008.png b/client/assets/sprites/seris/animations/skill2/south/frame_008.png
new file mode 100644
index 0000000..b251ee9
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_008.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_009.png b/client/assets/sprites/seris/animations/skill2/south/frame_009.png
new file mode 100644
index 0000000..5c89c4c
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_009.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_010.png b/client/assets/sprites/seris/animations/skill2/south/frame_010.png
new file mode 100644
index 0000000..b111c20
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_010.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_011.png b/client/assets/sprites/seris/animations/skill2/south/frame_011.png
new file mode 100644
index 0000000..5566df7
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_011.png differ
diff --git a/client/assets/sprites/seris/animations/skill2/south/frame_012.png b/client/assets/sprites/seris/animations/skill2/south/frame_012.png
new file mode 100644
index 0000000..bf8b34d
Binary files /dev/null and b/client/assets/sprites/seris/animations/skill2/south/frame_012.png differ
diff --git a/client/assets/sprites/seris/animations/teleport/south/frame_000.png b/client/assets/sprites/seris/animations/teleport/south/frame_000.png
new file mode 100644
index 0000000..56dd8e9
Binary files /dev/null and b/client/assets/sprites/seris/animations/teleport/south/frame_000.png differ
diff --git a/client/assets/sprites/seris/animations/teleport/south/frame_001.png b/client/assets/sprites/seris/animations/teleport/south/frame_001.png
new file mode 100644
index 0000000..7eb25e2
Binary files /dev/null and b/client/assets/sprites/seris/animations/teleport/south/frame_001.png differ
diff --git a/client/assets/sprites/seris/animations/teleport/south/frame_002.png b/client/assets/sprites/seris/animations/teleport/south/frame_002.png
new file mode 100644
index 0000000..4949bc6
Binary files /dev/null and b/client/assets/sprites/seris/animations/teleport/south/frame_002.png differ
diff --git a/client/assets/sprites/seris/animations/teleport/south/frame_003.png b/client/assets/sprites/seris/animations/teleport/south/frame_003.png
new file mode 100644
index 0000000..1da269f
Binary files /dev/null and b/client/assets/sprites/seris/animations/teleport/south/frame_003.png differ
diff --git a/client/assets/sprites/seris/animations/teleport/south/frame_004.png b/client/assets/sprites/seris/animations/teleport/south/frame_004.png
new file mode 100644
index 0000000..5122382
Binary files /dev/null and b/client/assets/sprites/seris/animations/teleport/south/frame_004.png differ
diff --git a/client/assets/sprites/seris/rotations/east.png b/client/assets/sprites/seris/rotations/east.png
new file mode 100644
index 0000000..646c09b
Binary files /dev/null and b/client/assets/sprites/seris/rotations/east.png differ
diff --git a/client/assets/sprites/seris/rotations/north.png b/client/assets/sprites/seris/rotations/north.png
new file mode 100644
index 0000000..03210e7
Binary files /dev/null and b/client/assets/sprites/seris/rotations/north.png differ
diff --git a/client/assets/sprites/seris/rotations/south.png b/client/assets/sprites/seris/rotations/south.png
new file mode 100644
index 0000000..5dc2153
Binary files /dev/null and b/client/assets/sprites/seris/rotations/south.png differ
diff --git a/client/assets/sprites/seris/rotations/west.png b/client/assets/sprites/seris/rotations/west.png
new file mode 100644
index 0000000..bf5dd76
Binary files /dev/null and b/client/assets/sprites/seris/rotations/west.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/east/frame_000.png b/client/assets/sprites/vexaris/animations/attack/east/frame_000.png
new file mode 100644
index 0000000..32d1c43
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/east/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/east/frame_001.png b/client/assets/sprites/vexaris/animations/attack/east/frame_001.png
new file mode 100644
index 0000000..ac11862
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/east/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/east/frame_002.png b/client/assets/sprites/vexaris/animations/attack/east/frame_002.png
new file mode 100644
index 0000000..8ac78c3
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/east/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/east/frame_003.png b/client/assets/sprites/vexaris/animations/attack/east/frame_003.png
new file mode 100644
index 0000000..e5cf23e
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/east/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/east/frame_004.png b/client/assets/sprites/vexaris/animations/attack/east/frame_004.png
new file mode 100644
index 0000000..79bcb3a
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/east/frame_004.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/east/frame_005.png b/client/assets/sprites/vexaris/animations/attack/east/frame_005.png
new file mode 100644
index 0000000..e65c47f
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/east/frame_005.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/east/frame_006.png b/client/assets/sprites/vexaris/animations/attack/east/frame_006.png
new file mode 100644
index 0000000..f2ca0d7
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/east/frame_006.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/north/frame_000.png b/client/assets/sprites/vexaris/animations/attack/north/frame_000.png
new file mode 100644
index 0000000..66a3241
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/north/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/north/frame_001.png b/client/assets/sprites/vexaris/animations/attack/north/frame_001.png
new file mode 100644
index 0000000..5ea84e0
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/north/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/north/frame_002.png b/client/assets/sprites/vexaris/animations/attack/north/frame_002.png
new file mode 100644
index 0000000..36b53e4
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/north/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/north/frame_003.png b/client/assets/sprites/vexaris/animations/attack/north/frame_003.png
new file mode 100644
index 0000000..84e9ff6
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/north/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/north/frame_004.png b/client/assets/sprites/vexaris/animations/attack/north/frame_004.png
new file mode 100644
index 0000000..50c3d76
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/north/frame_004.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/north/frame_005.png b/client/assets/sprites/vexaris/animations/attack/north/frame_005.png
new file mode 100644
index 0000000..ed8e491
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/north/frame_005.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/north/frame_006.png b/client/assets/sprites/vexaris/animations/attack/north/frame_006.png
new file mode 100644
index 0000000..801e74f
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/north/frame_006.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/south/frame_000.png b/client/assets/sprites/vexaris/animations/attack/south/frame_000.png
new file mode 100644
index 0000000..1539db5
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/south/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/south/frame_001.png b/client/assets/sprites/vexaris/animations/attack/south/frame_001.png
new file mode 100644
index 0000000..11b62fe
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/south/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/south/frame_002.png b/client/assets/sprites/vexaris/animations/attack/south/frame_002.png
new file mode 100644
index 0000000..a998c83
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/south/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/south/frame_003.png b/client/assets/sprites/vexaris/animations/attack/south/frame_003.png
new file mode 100644
index 0000000..9815ab0
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/south/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/south/frame_004.png b/client/assets/sprites/vexaris/animations/attack/south/frame_004.png
new file mode 100644
index 0000000..962b1c7
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/south/frame_004.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/south/frame_005.png b/client/assets/sprites/vexaris/animations/attack/south/frame_005.png
new file mode 100644
index 0000000..895d2b1
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/south/frame_005.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/south/frame_006.png b/client/assets/sprites/vexaris/animations/attack/south/frame_006.png
new file mode 100644
index 0000000..f24f56e
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/south/frame_006.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/west/frame_000.png b/client/assets/sprites/vexaris/animations/attack/west/frame_000.png
new file mode 100644
index 0000000..6ecf192
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/west/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/west/frame_001.png b/client/assets/sprites/vexaris/animations/attack/west/frame_001.png
new file mode 100644
index 0000000..81b3bc2
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/west/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/west/frame_002.png b/client/assets/sprites/vexaris/animations/attack/west/frame_002.png
new file mode 100644
index 0000000..95114c6
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/west/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/west/frame_003.png b/client/assets/sprites/vexaris/animations/attack/west/frame_003.png
new file mode 100644
index 0000000..cbdda85
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/west/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/west/frame_004.png b/client/assets/sprites/vexaris/animations/attack/west/frame_004.png
new file mode 100644
index 0000000..14130b4
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/west/frame_004.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/west/frame_005.png b/client/assets/sprites/vexaris/animations/attack/west/frame_005.png
new file mode 100644
index 0000000..2291cec
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/west/frame_005.png differ
diff --git a/client/assets/sprites/vexaris/animations/attack/west/frame_006.png b/client/assets/sprites/vexaris/animations/attack/west/frame_006.png
new file mode 100644
index 0000000..fb755b7
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/attack/west/frame_006.png differ
diff --git a/client/assets/sprites/vexaris/animations/burst/south/frame_000.png b/client/assets/sprites/vexaris/animations/burst/south/frame_000.png
new file mode 100644
index 0000000..1539db5
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/burst/south/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/burst/south/frame_001.png b/client/assets/sprites/vexaris/animations/burst/south/frame_001.png
new file mode 100644
index 0000000..b31a542
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/burst/south/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/burst/south/frame_002.png b/client/assets/sprites/vexaris/animations/burst/south/frame_002.png
new file mode 100644
index 0000000..1b2b5c0
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/burst/south/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/burst/south/frame_003.png b/client/assets/sprites/vexaris/animations/burst/south/frame_003.png
new file mode 100644
index 0000000..7d1870d
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/burst/south/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/animations/burst/south/frame_004.png b/client/assets/sprites/vexaris/animations/burst/south/frame_004.png
new file mode 100644
index 0000000..dc5a528
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/burst/south/frame_004.png differ
diff --git a/client/assets/sprites/vexaris/animations/burst/south/frame_005.png b/client/assets/sprites/vexaris/animations/burst/south/frame_005.png
new file mode 100644
index 0000000..1ca28c5
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/burst/south/frame_005.png differ
diff --git a/client/assets/sprites/vexaris/animations/burst/south/frame_006.png b/client/assets/sprites/vexaris/animations/burst/south/frame_006.png
new file mode 100644
index 0000000..25c24e7
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/burst/south/frame_006.png differ
diff --git a/client/assets/sprites/vexaris/animations/burst/south/frame_007.png b/client/assets/sprites/vexaris/animations/burst/south/frame_007.png
new file mode 100644
index 0000000..408e186
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/burst/south/frame_007.png differ
diff --git a/client/assets/sprites/vexaris/animations/burst/south/frame_008.png b/client/assets/sprites/vexaris/animations/burst/south/frame_008.png
new file mode 100644
index 0000000..31d941e
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/burst/south/frame_008.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/east/frame_000.png b/client/assets/sprites/vexaris/animations/charge/east/frame_000.png
new file mode 100644
index 0000000..32d1c43
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/east/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/east/frame_001.png b/client/assets/sprites/vexaris/animations/charge/east/frame_001.png
new file mode 100644
index 0000000..564a5c6
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/east/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/east/frame_002.png b/client/assets/sprites/vexaris/animations/charge/east/frame_002.png
new file mode 100644
index 0000000..a4193b1
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/east/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/east/frame_003.png b/client/assets/sprites/vexaris/animations/charge/east/frame_003.png
new file mode 100644
index 0000000..b8c0882
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/east/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/east/frame_004.png b/client/assets/sprites/vexaris/animations/charge/east/frame_004.png
new file mode 100644
index 0000000..00e6d78
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/east/frame_004.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/east/frame_005.png b/client/assets/sprites/vexaris/animations/charge/east/frame_005.png
new file mode 100644
index 0000000..38c97d9
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/east/frame_005.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/east/frame_006.png b/client/assets/sprites/vexaris/animations/charge/east/frame_006.png
new file mode 100644
index 0000000..c44e9cc
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/east/frame_006.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/north/frame_000.png b/client/assets/sprites/vexaris/animations/charge/north/frame_000.png
new file mode 100644
index 0000000..66a3241
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/north/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/north/frame_001.png b/client/assets/sprites/vexaris/animations/charge/north/frame_001.png
new file mode 100644
index 0000000..7e6d9fe
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/north/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/north/frame_002.png b/client/assets/sprites/vexaris/animations/charge/north/frame_002.png
new file mode 100644
index 0000000..1cfdeb1
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/north/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/north/frame_003.png b/client/assets/sprites/vexaris/animations/charge/north/frame_003.png
new file mode 100644
index 0000000..2e65faf
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/north/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/north/frame_004.png b/client/assets/sprites/vexaris/animations/charge/north/frame_004.png
new file mode 100644
index 0000000..dd3c3da
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/north/frame_004.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/north/frame_005.png b/client/assets/sprites/vexaris/animations/charge/north/frame_005.png
new file mode 100644
index 0000000..0e6cc43
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/north/frame_005.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/north/frame_006.png b/client/assets/sprites/vexaris/animations/charge/north/frame_006.png
new file mode 100644
index 0000000..ccba9d0
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/north/frame_006.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/south/frame_000.png b/client/assets/sprites/vexaris/animations/charge/south/frame_000.png
new file mode 100644
index 0000000..1539db5
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/south/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/south/frame_001.png b/client/assets/sprites/vexaris/animations/charge/south/frame_001.png
new file mode 100644
index 0000000..c6dff64
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/south/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/south/frame_002.png b/client/assets/sprites/vexaris/animations/charge/south/frame_002.png
new file mode 100644
index 0000000..a55f440
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/south/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/south/frame_003.png b/client/assets/sprites/vexaris/animations/charge/south/frame_003.png
new file mode 100644
index 0000000..ead2fb1
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/south/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/south/frame_004.png b/client/assets/sprites/vexaris/animations/charge/south/frame_004.png
new file mode 100644
index 0000000..bf64464
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/south/frame_004.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/south/frame_005.png b/client/assets/sprites/vexaris/animations/charge/south/frame_005.png
new file mode 100644
index 0000000..8a0fda9
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/south/frame_005.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/south/frame_006.png b/client/assets/sprites/vexaris/animations/charge/south/frame_006.png
new file mode 100644
index 0000000..2de2926
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/south/frame_006.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/west/frame_000.png b/client/assets/sprites/vexaris/animations/charge/west/frame_000.png
new file mode 100644
index 0000000..6ecf192
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/west/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/west/frame_001.png b/client/assets/sprites/vexaris/animations/charge/west/frame_001.png
new file mode 100644
index 0000000..228a8bb
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/west/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/west/frame_002.png b/client/assets/sprites/vexaris/animations/charge/west/frame_002.png
new file mode 100644
index 0000000..8c1fb9e
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/west/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/west/frame_003.png b/client/assets/sprites/vexaris/animations/charge/west/frame_003.png
new file mode 100644
index 0000000..baadf9d
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/west/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/west/frame_004.png b/client/assets/sprites/vexaris/animations/charge/west/frame_004.png
new file mode 100644
index 0000000..7efed30
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/west/frame_004.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/west/frame_005.png b/client/assets/sprites/vexaris/animations/charge/west/frame_005.png
new file mode 100644
index 0000000..be82933
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/west/frame_005.png differ
diff --git a/client/assets/sprites/vexaris/animations/charge/west/frame_006.png b/client/assets/sprites/vexaris/animations/charge/west/frame_006.png
new file mode 100644
index 0000000..2d4e962
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/charge/west/frame_006.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/east/frame_000.png b/client/assets/sprites/vexaris/animations/running/east/frame_000.png
new file mode 100644
index 0000000..894d84f
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/east/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/east/frame_001.png b/client/assets/sprites/vexaris/animations/running/east/frame_001.png
new file mode 100644
index 0000000..3c8e280
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/east/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/east/frame_002.png b/client/assets/sprites/vexaris/animations/running/east/frame_002.png
new file mode 100644
index 0000000..e89a9f9
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/east/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/east/frame_003.png b/client/assets/sprites/vexaris/animations/running/east/frame_003.png
new file mode 100644
index 0000000..da35ea9
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/east/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/north/frame_000.png b/client/assets/sprites/vexaris/animations/running/north/frame_000.png
new file mode 100644
index 0000000..ed604c7
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/north/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/north/frame_001.png b/client/assets/sprites/vexaris/animations/running/north/frame_001.png
new file mode 100644
index 0000000..bdfa17d
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/north/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/north/frame_002.png b/client/assets/sprites/vexaris/animations/running/north/frame_002.png
new file mode 100644
index 0000000..3a56b5f
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/north/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/north/frame_003.png b/client/assets/sprites/vexaris/animations/running/north/frame_003.png
new file mode 100644
index 0000000..a47d8b8
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/north/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/south/frame_000.png b/client/assets/sprites/vexaris/animations/running/south/frame_000.png
new file mode 100644
index 0000000..615f9a6
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/south/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/south/frame_001.png b/client/assets/sprites/vexaris/animations/running/south/frame_001.png
new file mode 100644
index 0000000..62c2e0c
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/south/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/south/frame_002.png b/client/assets/sprites/vexaris/animations/running/south/frame_002.png
new file mode 100644
index 0000000..167fa9a
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/south/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/south/frame_003.png b/client/assets/sprites/vexaris/animations/running/south/frame_003.png
new file mode 100644
index 0000000..9f95609
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/south/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/west/frame_000.png b/client/assets/sprites/vexaris/animations/running/west/frame_000.png
new file mode 100644
index 0000000..89cb1c5
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/west/frame_000.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/west/frame_001.png b/client/assets/sprites/vexaris/animations/running/west/frame_001.png
new file mode 100644
index 0000000..8c73a23
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/west/frame_001.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/west/frame_002.png b/client/assets/sprites/vexaris/animations/running/west/frame_002.png
new file mode 100644
index 0000000..4df5aa6
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/west/frame_002.png differ
diff --git a/client/assets/sprites/vexaris/animations/running/west/frame_003.png b/client/assets/sprites/vexaris/animations/running/west/frame_003.png
new file mode 100644
index 0000000..2f37fa9
Binary files /dev/null and b/client/assets/sprites/vexaris/animations/running/west/frame_003.png differ
diff --git a/client/assets/sprites/vexaris/rotations/east.png b/client/assets/sprites/vexaris/rotations/east.png
new file mode 100644
index 0000000..8b3b50e
Binary files /dev/null and b/client/assets/sprites/vexaris/rotations/east.png differ
diff --git a/client/assets/sprites/vexaris/rotations/north.png b/client/assets/sprites/vexaris/rotations/north.png
new file mode 100644
index 0000000..b42fd7f
Binary files /dev/null and b/client/assets/sprites/vexaris/rotations/north.png differ
diff --git a/client/assets/sprites/vexaris/rotations/south.png b/client/assets/sprites/vexaris/rotations/south.png
new file mode 100644
index 0000000..f0606ef
Binary files /dev/null and b/client/assets/sprites/vexaris/rotations/south.png differ
diff --git a/client/assets/sprites/vexaris/rotations/west.png b/client/assets/sprites/vexaris/rotations/west.png
new file mode 100644
index 0000000..e516547
Binary files /dev/null and b/client/assets/sprites/vexaris/rotations/west.png differ
diff --git a/client/assets/twitter.png b/client/assets/twitter.png
new file mode 100644
index 0000000..84d3485
Binary files /dev/null and b/client/assets/twitter.png differ
diff --git a/client/css/style.css b/client/css/style.css
new file mode 100644
index 0000000..b2ff14d
--- /dev/null
+++ b/client/css/style.css
@@ -0,0 +1,794 @@
+/* Reset */
+* { margin: 0; padding: 0; box-sizing: border-box; }
+
+:root {
+ --bg: #0d0a07;
+ --panel: #1a1208;
+ --panel-alt: #221a0c;
+ --border-hi: #7a5c2a;
+ --border-lo: #1a0e04;
+ --border-mid: #3a2810;
+ --accent: #d4a843;
+ --accent-dim: #8a6a30;
+ --text: #e8d5a3;
+ --text-dim: #7a6040;
+ --text-faint: #3a2c18;
+ --btn-bg: #3a2810;
+ --btn-hover: #4a3818;
+ --btn-active: #251a08;
+ --input-bg: #0f0a05;
+ --red: #8a2a1a;
+ --red-hi: #cc3322;
+ --green: #2a5a2a;
+ --blue: #1a2a5a;
+}
+
+body {
+ background: var(--bg) url('../assets/sky.jpg') center / cover no-repeat fixed;
+ overflow: hidden;
+ font-family: 'Press Start 2P', 'Courier New', monospace;
+}
+
+#game-canvas {
+ display: block;
+ position: fixed;
+ inset: 0;
+ width: 100vw;
+ height: 100vh;
+}
+
+.hidden { display: none !important; }
+
+/* Minecraft border mixin */
+.mc-border {
+ border: 3px solid;
+ border-color: var(--border-hi) var(--border-lo) var(--border-lo) var(--border-hi);
+}
+
+/* BUTTONS — Desert Minecraft style */
+
+.mc-btn {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 9px;
+ background: var(--btn-bg);
+ border-top: 3px solid var(--border-hi);
+ border-left: 3px solid var(--border-hi);
+ border-bottom: 3px solid var(--border-lo);
+ border-right: 3px solid var(--border-lo);
+ color: var(--text);
+ padding: 12px 24px;
+ cursor: pointer;
+ text-align: center;
+ letter-spacing: 1px;
+ width: 100%;
+ text-shadow: 1px 1px 0 #000;
+ transition: background 0.05s;
+ image-rendering: pixelated;
+}
+.mc-btn:hover:not(:disabled) {
+ background: var(--btn-hover);
+ border-top-color: #9a7c3a;
+ border-left-color: #9a7c3a;
+}
+.mc-btn:active:not(:disabled) {
+ background: var(--btn-active);
+ border-top-color: var(--border-lo);
+ border-left-color: var(--border-lo);
+ border-bottom-color: var(--border-hi);
+ border-right-color: var(--border-hi);
+ padding-top: 13px;
+ padding-bottom: 11px;
+}
+.mc-btn:disabled {
+ background: #1a1208;
+ border-top-color: #2a1e0c;
+ border-left-color: #2a1e0c;
+ border-bottom-color: #0a0804;
+ border-right-color: #0a0804;
+ color: var(--text-faint);
+ cursor: not-allowed;
+ text-shadow: none;
+}
+
+.mc-btn-sm {
+ width: auto;
+ padding: 8px 14px;
+ font-size: 8px;
+}
+
+.mc-btn-cancel {
+ color: #cc7755;
+}
+.mc-btn-cancel:hover:not(:disabled) {
+ background: #3a1808;
+ border-top-color: #8a4422;
+ border-left-color: #8a4422;
+}
+
+.mc-btn-gold {
+ background: #3a2a08;
+ border-top-color: var(--accent);
+ border-left-color: var(--accent);
+ color: var(--accent);
+}
+.mc-btn-gold:hover:not(:disabled) {
+ background: #4a3810;
+ border-top-color: #f0cc60;
+ border-left-color: #f0cc60;
+ color: #f0d860;
+}
+
+/* Inputs */
+
+.mc-input {
+ background: var(--input-bg);
+ border-top: 2px solid #1a1208;
+ border-left: 2px solid #1a1208;
+ border-bottom: 2px solid var(--border-hi);
+ border-right: 2px solid var(--border-hi);
+ color: var(--text);
+ padding: 10px 12px;
+ font-family: 'Press Start 2P', monospace;
+ font-size: 9px;
+ outline: none;
+ width: 100%;
+ transition: border-color 0.1s;
+}
+.mc-input::placeholder { color: var(--text-faint); }
+.mc-input:focus {
+ border-top-color: #0a0804;
+ border-left-color: #0a0804;
+ border-bottom-color: var(--accent);
+ border-right-color: var(--accent);
+}
+.mc-input-code {
+ width: 120px;
+ text-align: center;
+ text-transform: uppercase;
+ letter-spacing: 4px;
+ font-size: 11px;
+}
+
+/* HOME SCREEN */
+
+#home-screen {
+ position: fixed;
+ inset: 0;
+ z-index: 10;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg) url('../assets/fond_soulgate.png') center / cover no-repeat;
+}
+
+/* Top-right gear */
+.home-top-bar {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+}
+
+.icon-btn {
+ background: var(--btn-hover);
+ border-top: 2px solid var(--border-hi);
+ border-left: 2px solid var(--border-hi);
+ border-bottom: 2px solid var(--border-lo);
+ border-right: 2px solid var(--border-lo);
+ color: var(--accent);
+ font-size: 18px;
+ width: 40px;
+ height: 40px;
+ cursor: pointer;
+ font-family: inherit;
+ line-height: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Bottom-left social icons */
+.home-bottom-bar {
+ position: absolute;
+ bottom: 16px;
+ left: 16px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.social-link {
+ width: 36px;
+ height: 36px;
+ background: var(--btn-hover);
+ border-top: 2px solid var(--border-hi);
+ border-left: 2px solid var(--border-hi);
+ border-bottom: 2px solid var(--border-lo);
+ border-right: 2px solid var(--border-lo);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-decoration: none;
+ cursor: pointer;
+ padding: 0;
+ flex-shrink: 0;
+}
+.social-link span {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 11px;
+ color: var(--accent);
+ line-height: 1;
+}
+.social-icon {
+ width: 20px;
+ height: 20px;
+ display: block;
+ object-fit: contain;
+}
+.social-icon-invert {
+ filter: invert(1) brightness(1.1);
+}
+
+.version-label {
+ font-size: 7px;
+ color: var(--text-faint);
+ letter-spacing: 1px;
+ margin-left: 8px;
+ line-height: 1;
+}
+
+/* Center panel */
+.home-center {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 360px;
+}
+
+.home-panel {
+ width: 100%;
+ background: rgba(26, 18, 8, 0.92);
+ border-top: 3px solid var(--border-hi);
+ border-left: 3px solid var(--border-hi);
+ border-bottom: 3px solid var(--border-lo);
+ border-right: 3px solid var(--border-lo);
+ padding: 32px 28px 24px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0;
+}
+
+/* Title */
+.home-title {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 28px;
+ color: var(--accent);
+ letter-spacing: 4px;
+ text-align: center;
+ margin-bottom: 8px;
+ text-shadow: 2px 2px 0 #3a2000;
+}
+
+.home-subtitle {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--accent-dim);
+ letter-spacing: 3px;
+ text-align: center;
+ margin-bottom: 28px;
+ opacity: 0.7;
+}
+
+/* Divider */
+.home-divider {
+ width: 100%;
+ height: 1px;
+ background: linear-gradient(to right, transparent, var(--border-hi), transparent);
+ margin-bottom: 20px;
+}
+
+.home-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+ width: 100%;
+ margin-bottom: 8px;
+}
+
+#home-username {
+ flex-direction: row;
+ gap: 6px;
+ margin-bottom: 16px;
+}
+
+#join-row {
+ flex-direction: row;
+ gap: 6px;
+}
+
+/* Status */
+.home-status {
+ font-size: 7px;
+ color: var(--text-faint);
+ letter-spacing: 1px;
+ margin-top: 14px;
+ text-align: center;
+}
+.home-status.ok { color: #6a9a50; }
+.home-status.err { color: var(--red-hi); }
+
+/* LOBBY SCREEN */
+
+#lobby-screen {
+ position: fixed;
+ inset: 0;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg) url('../assets/fond_soulgate.png') center / cover no-repeat;
+}
+
+#loading-screen {
+ position: fixed;
+ inset: 0;
+ z-index: 20;
+ background: var(--bg) url('../assets/fond_soulgate.png') center / cover no-repeat;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.loading-text {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 18px;
+ color: var(--accent);
+ letter-spacing: 4px;
+ background: rgba(13, 10, 7, 0.85);
+ padding: 18px 32px;
+ border-top: 3px solid var(--border-hi);
+ border-left: 3px solid var(--border-hi);
+ border-bottom: 3px solid var(--border-lo);
+ border-right: 3px solid var(--border-lo);
+ animation: loading-pulse 1.4s ease-in-out infinite;
+}
+@keyframes loading-pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+.lobby-panel {
+ background: rgba(26, 18, 8, 0.94);
+ border-top: 3px solid var(--border-hi);
+ border-left: 3px solid var(--border-hi);
+ border-bottom: 3px solid var(--border-lo);
+ border-right: 3px solid var(--border-lo);
+ padding: 24px 28px 20px;
+ width: 440px;
+ color: var(--text);
+}
+
+.lobby-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 20px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--border-mid);
+}
+
+.lobby-code {
+ font-size: 8px;
+ color: var(--accent-dim);
+ letter-spacing: 1px;
+}
+.lobby-code strong {
+ color: var(--accent);
+ letter-spacing: 5px;
+ font-size: 11px;
+}
+
+.lobby-section-title {
+ font-size: 8px;
+ color: var(--accent-dim);
+ letter-spacing: 2px;
+ margin-bottom: 10px;
+ text-align: center;
+}
+
+/* Class cards */
+.class-btns {
+ display: flex;
+ gap: 6px;
+ margin-bottom: 14px;
+}
+
+.class-btn {
+ flex: 1;
+ background: var(--panel);
+ border-top: 2px solid var(--border-hi);
+ border-left: 2px solid var(--border-hi);
+ border-bottom: 2px solid var(--border-lo);
+ border-right: 2px solid var(--border-lo);
+ padding: 12px 6px;
+ text-align: center;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ font-family: 'Press Start 2P', monospace;
+ transition: background 0.05s;
+}
+.class-btn:hover:not(:disabled) {
+ background: var(--btn-hover);
+ border-top-color: #9a7c3a;
+ border-left-color: #9a7c3a;
+}
+
+.class-name { font-size: 9px; color: var(--text); }
+.class-role { font-size: 6px; color: var(--text-dim); }
+
+.class-btn.active[data-class="kael"] {
+ background: rgba(80, 20, 10, 0.8);
+ border-top-color: #cc4433;
+ border-left-color: #cc4433;
+}
+.class-btn.active[data-class="kael"] .class-name { color: #ff7766; }
+.class-btn.active[data-class="kael"] .class-role { color: #883322; }
+
+.class-btn.active[data-class="seris"] {
+ background: rgba(10, 20, 60, 0.8);
+ border-top-color: #4477cc;
+ border-left-color: #4477cc;
+}
+.class-btn.active[data-class="seris"] .class-name { color: #7799ff; }
+.class-btn.active[data-class="seris"] .class-role { color: #334488; }
+
+.class-btn.active[data-class="aldric"] {
+ background: rgba(10, 40, 15, 0.8);
+ border-top-color: #44aa66;
+ border-left-color: #44aa66;
+}
+.class-btn.active[data-class="aldric"] .class-name { color: #77cc99; }
+.class-btn.active[data-class="aldric"] .class-role { color: #336644; }
+
+/* Players list */
+.players-lobby {
+ font-size: 8px;
+ color: var(--text-dim);
+ min-height: 18px;
+ line-height: 2.2;
+ letter-spacing: 0.5px;
+ margin-bottom: 12px;
+ text-align: center;
+ padding: 8px 0;
+ border-top: 1px solid var(--border-mid);
+ border-bottom: 1px solid var(--border-mid);
+}
+
+.lobby-actions {
+ display: flex;
+ gap: 6px;
+ margin-top: 12px;
+}
+
+.msg {
+ margin-top: 8px;
+ font-size: 7px;
+ color: var(--accent-dim);
+ min-height: 12px;
+ letter-spacing: 0.5px;
+ text-align: center;
+}
+.msg.err { color: var(--red-hi); }
+
+/* IN-GAME: Barre de vie boss */
+
+#boss-bar {
+ position: fixed;
+ bottom: 40px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 15;
+ min-width: 320px;
+}
+.boss-bar-box {
+ background: rgba(10, 5, 20, 0.88);
+ border: 1px solid #6a22aa;
+ padding: 8px 16px 10px;
+ font-family: 'Courier New', monospace;
+ text-align: center;
+}
+.boss-bar-name { font-size: 11px; letter-spacing: 3px; color: #bb88ff; margin-bottom: 6px; }
+.boss-bar-track { background: #1a0a2a; height: 10px; width: 100%; margin-bottom: 4px; border: 1px solid #3a1a5a; }
+.boss-bar-fill { height: 100%; width: 100%; background: linear-gradient(to right, #7722cc, #aa44ff); transition: width 0.1s linear; }
+.boss-bar-hp { font-size: 10px; color: #8855bb; }
+
+/* IN-GAME: Panneau d'upgrade */
+
+#upgrade-panel {
+ position: fixed;
+ inset: 0;
+ z-index: 15;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(8, 5, 2, 0.78); /* backdrop : intercepte les clics, focus sur le menu */
+}
+.upgrade-box {
+ background: #12121e;
+ border: 1px solid #4a2a8a;
+ padding: 20px 28px;
+ min-width: 280px;
+ color: #c8c8d8;
+ font-size: 13px;
+ font-family: 'Courier New', monospace;
+}
+.upgrade-title { font-size: 14px; color: #9977cc; letter-spacing: 2px; margin-bottom: 10px; text-align: center; }
+.upgrade-souls { text-align: center; margin-bottom: 14px; font-size: 12px; color: #aaa; }
+.upgrade-souls strong { color: #f0c040; }
+.upgrade-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
+.upgrade-info { flex: 1; }
+.upgrade-name { font-size: 12px; color: #ddd; }
+.upgrade-desc { font-size: 10px; color: #666; margin-top: 2px; }
+.upgrade-stacks { font-size: 10px; color: #7755aa; white-space: nowrap; }
+.upgrade-buy { padding: 4px 10px; font-size: 11px; white-space: nowrap; color: #f0c040; border-color: #8a6a10; background: #0a0a18; border: 1px solid #8a6a10; cursor: pointer; font-family: 'Courier New', monospace; }
+.upgrade-buy:disabled { color: #555; border-color: #333; cursor: not-allowed; }
+
+/* IN-GAME: Bouton plein ecran */
+
+.fullscreen-btn {
+ position: fixed;
+ top: 8px;
+ right: 8px;
+ z-index: 20;
+ padding: 4px 8px;
+ font-size: 18px;
+ line-height: 1;
+ opacity: 0.35;
+ border: 1px solid #333;
+ background: transparent;
+ color: #aaa;
+ cursor: pointer;
+ font-family: inherit;
+}
+.fullscreen-btn:hover { opacity: 0.85; }
+
+/* HUD joueur (bas-gauche) */
+
+#player-hud {
+ position: fixed;
+ bottom: 16px;
+ left: 16px;
+ z-index: 15;
+ background: rgba(8, 8, 20, 0.82);
+ border: 1px solid #2a2a4a;
+ padding: 10px 12px;
+ font-family: 'Courier New', monospace;
+ font-size: 11px;
+ color: #c8c8d8;
+ min-width: 220px;
+}
+#hud-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 7px; }
+#hud-class-name { font-size: 12px; letter-spacing: 2px; color: #9988ff; }
+#hud-souls { color: #f0c040; font-size: 10px; }
+
+#hud-hp-wrap { position: relative; height: 14px; background: #1a0808; border: 1px solid #3a1a1a; margin-bottom: 8px; overflow: hidden; }
+#hud-hp-fill { position: absolute; inset: 0; background: linear-gradient(to right, #882222, #cc3333); transition: width 0.1s linear; width: 100%; }
+#hud-hp-text { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #eee; z-index: 1; }
+
+#hud-skills { display: flex; gap: 5px; margin-bottom: 6px; }
+.hud-skill { position: relative; width: 36px; height: 36px; background: #0e0e1e; border: 1px solid #333; overflow: hidden; flex-shrink: 0; }
+.hud-skill .cd-fill { position: absolute; bottom: 0; left: 0; width: 100%; height: 0%; background: rgba(100, 60, 220, 0.45); transition: height 0.1s linear; }
+.hud-skill .key { position: absolute; bottom: 2px; left: 0; right: 0; text-align: center; font-size: 10px; color: #888; z-index: 1; }
+.hud-skill.ready { border-color: #8866ff; }
+.hud-skill.locked { opacity: 0.5; }
+.hud-skill.divine-ready { border-color: #f0c040; box-shadow: 0 0 6px rgba(240, 192, 64, 0.5); }
+#hud-divine-uses { position: absolute; top: 2px; right: 3px; font-size: 8px; color: #f0c040; z-index: 1; letter-spacing: -1px; }
+
+#hud-buffs { display: flex; flex-wrap: wrap; gap: 4px; min-height: 4px; }
+.buff-tag { font-size: 9px; padding: 1px 5px; border: 1px solid; font-family: 'Courier New', monospace; }
+.buff-tag.invulnerable { color: #f0c040; border-color: #8a6a10; background: rgba(240,192,64,0.12); }
+.buff-tag.casting { color: #44ffff; border-color: #228888; background: rgba(68,255,255,0.10); }
+.buff-tag.flying { color: #88ccff; border-color: #336688; background: rgba(136,204,255,0.10); }
+.buff-tag.intangible { color: #cc88ff; border-color: #553388; background: rgba(204,136,255,0.10); }
+.buff-tag.combat_buff { color: #ff8844; border-color: #883322; background: rgba(255,136,68,0.10); }
+.buff-tag.damage_mult { color: #ff4444; border-color: #882222; background: rgba(255,68,68,0.10); }
+
+/* IN-GAME: Barre Soulgate (haut-centre) */
+
+#soulgate-hud {
+ position: fixed; top: 14px; left: 50%; transform: translateX(-50%); z-index: 15;
+ display: flex; align-items: center; gap: 8px;
+ background: rgba(8, 8, 20, 0.82); border: 1px solid #2a2a4a; padding: 6px 12px;
+ font-family: 'Courier New', monospace; font-size: 10px; color: #c8c8d8;
+}
+.sg-label { letter-spacing: 2px; color: #9977cc; font-size: 10px; white-space: nowrap; }
+#sg-track { width: 180px; height: 8px; background: #1a0a2a; border: 1px solid #2a1a4a; overflow: hidden; }
+#sg-fill { height: 100%; width: 100%; background: linear-gradient(to right, #5522aa, #9944ff); transition: width 0.1s linear; }
+#sg-hp-text { white-space: nowrap; color: #8855bb; font-size: 10px; }
+
+/* IN-GAME: Infos vague (haut-droite) */
+
+#wave-hud {
+ position: fixed; top: 14px; right: 16px; z-index: 15;
+ background: rgba(8, 8, 20, 0.82); border: 1px solid #2a2a4a; padding: 8px 12px;
+ font-family: 'Courier New', monospace; text-align: right;
+}
+#wave-label { font-size: 11px; color: #9977cc; letter-spacing: 1px; margin-bottom: 3px; }
+#wave-state-label { font-size: 10px; color: #c8c8d8; margin-bottom: 2px; }
+#wave-enemies { font-size: 10px; color: #f0c040; }
+
+/* END GAME OVERLAY */
+
+#endgame-overlay {
+ position: fixed; inset: 0;
+ background: rgba(8, 5, 2, 0.88);
+ display: flex; align-items: center; justify-content: center;
+ z-index: 100;
+}
+
+#endgame-box {
+ background: rgba(26, 18, 8, 0.97);
+ border-top: 3px solid var(--border-hi);
+ border-left: 3px solid var(--border-hi);
+ border-bottom: 3px solid var(--border-lo);
+ border-right: 3px solid var(--border-lo);
+ padding: 32px 40px;
+ text-align: center;
+ font-family: 'Press Start 2P', monospace;
+ max-height: 90vh;
+ overflow-y: auto;
+ max-width: 520px;
+ width: 92vw;
+}
+
+#endgame-title {
+ font-size: 28px;
+ letter-spacing: 4px;
+ margin-bottom: 10px;
+}
+#endgame-title.victory { color: var(--accent); text-shadow: 2px 2px 0 #3a2000; }
+#endgame-title.defeat { color: #cc3322; text-shadow: 2px 2px 0 #2a0808; }
+
+#endgame-sub {
+ font-size: 8px;
+ color: var(--text-dim);
+ margin-bottom: 12px;
+}
+
+.endgame-stat {
+ font-size: 9px;
+ color: var(--text);
+ margin-bottom: 5px;
+}
+
+/* Leaderboard form inside endgame */
+#endgame-form { margin-top: 16px; text-align: left; }
+
+.lb-host-only {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ line-height: 1.6;
+ color: var(--accent);
+ text-align: center;
+ padding: 14px 8px;
+ background: var(--panel);
+ border-top: 2px solid var(--border-hi);
+ border-left: 2px solid var(--border-hi);
+ border-bottom: 2px solid var(--border-lo);
+ border-right: 2px solid var(--border-lo);
+ margin-bottom: 8px;
+}
+
+.lb-field { margin-bottom: 10px; }
+.lb-field label {
+ display: block;
+ font-size: 7px;
+ color: var(--accent-dim);
+ margin-bottom: 4px;
+ letter-spacing: 0.5px;
+}
+.lb-field input {
+ width: 100%;
+ background: var(--input-bg);
+ border-top: 2px solid #1a1208;
+ border-left: 2px solid #1a1208;
+ border-bottom: 2px solid var(--border-hi);
+ border-right: 2px solid var(--border-hi);
+ color: var(--text);
+ padding: 6px 8px;
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ outline: none;
+}
+.lb-field input.taken {
+ border-bottom-color: var(--red-hi);
+ border-right-color: var(--red-hi);
+}
+.lb-discord-error {
+ display: block;
+ font-size: 7px;
+ margin-top: 3px;
+ min-height: 11px;
+ color: #6a9a50;
+}
+.lb-discord-error.error { color: var(--red-hi); }
+#lb-submit-btn { width: 100%; margin-top: 12px; }
+
+/* LEADERBOARD SCREEN */
+
+#leaderboard-screen {
+ position: fixed; inset: 0; z-index: 50;
+ background: var(--bg) url('../assets/fond_soulgate.png') center / cover no-repeat;
+ display: flex; align-items: center; justify-content: center;
+}
+
+.lb-panel {
+ width: min(860px, 95vw);
+ max-height: 88vh;
+ display: flex;
+ flex-direction: column;
+ background: rgba(26, 18, 8, 0.97);
+ border-top: 3px solid var(--border-hi);
+ border-left: 3px solid var(--border-hi);
+ border-bottom: 3px solid var(--border-lo);
+ border-right: 3px solid var(--border-lo);
+ padding: 24px 28px;
+}
+
+.lb-header {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ margin-bottom: 18px;
+ padding-bottom: 14px;
+ border-bottom: 1px solid var(--border-mid);
+}
+
+.lb-title {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 14px;
+ color: var(--accent);
+ letter-spacing: 4px;
+ flex: 1;
+ text-align: center;
+ text-shadow: 2px 2px 0 #3a2000;
+}
+
+.lb-table-wrap { overflow-y: auto; flex: 1; }
+
+.lb-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+}
+.lb-table thead {
+ position: sticky;
+ top: 0;
+ background: var(--panel);
+}
+.lb-table th {
+ padding: 10px 12px;
+ color: var(--accent-dim);
+ border-bottom: 1px solid var(--border-mid);
+ text-align: left;
+ font-size: 7px;
+ letter-spacing: 1px;
+}
+.lb-table td {
+ padding: 10px 12px;
+ border-bottom: 1px solid var(--border-mid);
+ color: var(--text);
+ font-size: 8px;
+}
+.lb-table tr:nth-child(even) td { background: rgba(58, 40, 16, 0.18); }
+.lb-table tr:hover td { background: rgba(212, 168, 67, 0.06); }
+.lb-table tr.lb-first td { color: var(--accent); }
+
+.lb-rank { color: var(--accent-dim); width: 36px; }
+.lb-team { color: var(--text); }
+.lb-score { color: var(--accent); }
+.lb-players { color: #8ab4d4; font-size: 7px; }
\ No newline at end of file
diff --git a/client/debug.html b/client/debug.html
new file mode 100644
index 0000000..81bacfc
--- /dev/null
+++ b/client/debug.html
@@ -0,0 +1,277 @@
+
+
+
+
+ SOULGATE — Debug
+
+
+
+
+
+ SOULGATE — Debug
+ Connexion...
+
+
+
+
+
+ Connexion
+
+
+
+
+
+
+
+
+
+
+ Lobby
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Classe
+
+
+
+
+
+
+
+
+
+
+
+
+ Joueurs
+ En attente...
+
+
+
+ État du jeu en attente de la partie...
+
+
+
+
+
+ Joueurs
+ En attente de la partie...
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..adc1530
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,184 @@
+
+
+
+
+
+ SOULGATE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
SOULGATE
+
GUARDIANS OF SOULS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Connecting...
+
+
+
+
+

+

+
by Ylies Amara
+
+
+
+
+
+
+
+
Choose your class
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
VEXARIS
+
+
600 / 600
+
+
+
+
+
+
PREPARATION — 20s
+
Ames : 0
+
+
+
+
+
+
+
+
SOULGATE
+
+
100 / 100
+
+
+
+
Vague 1 / 3
+
Combat
+
Ennemis : 0
+
+
+
+
+
+
+
+
+
+ | # |
+ Équipe |
+ Score |
+ Temps |
+ Vagues |
+ Joueurs |
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/js/bindings.js b/client/js/bindings.js
new file mode 100644
index 0000000..74db5af
--- /dev/null
+++ b/client/js/bindings.js
@@ -0,0 +1,88 @@
+// bindings.js : tous les listeners DOM du menu et du lobby
+
+import { send } from './network.js';
+
+
+export function bindEvents(callbacks) {
+ const $ = id => document.getElementById(id);
+
+ // pseudo
+ $('username-btn').addEventListener('click', () => {
+ const v = $('username-input').value.trim();
+ if (v) {
+ callbacks.setUsername(v);
+ send('set_username', { username: v });
+ }
+ });
+
+ $('username-input').addEventListener('keydown', e => {
+ if (e.key === 'Enter') $('username-btn').click();
+ });
+
+ // creer une partie
+ $('create-game-btn').addEventListener('click', () => send('create_lobby'));
+
+ // rejoindre une partie
+ $('join-game-btn').addEventListener('click', () => {
+ $('home-buttons').classList.add('hidden');
+ $('join-row').classList.remove('hidden');
+ $('code-input').focus();
+ });
+
+ $('join-confirm-btn').addEventListener('click', () => {
+ const code = $('code-input').value.trim().toUpperCase();
+ if (code) send('join_lobby', { code });
+ });
+
+ $('code-input').addEventListener('keydown', e => {
+ if (e.key === 'Enter') $('join-confirm-btn').click();
+ });
+
+ $('join-cancel-btn').addEventListener('click', () => {
+ $('join-row').classList.add('hidden');
+ $('home-buttons').classList.remove('hidden');
+ });
+
+ // retour au menu
+ $('lobby-back-btn').addEventListener('click', () => {
+ send('leave_lobby');
+ callbacks.switchToHome();
+ });
+
+ // selection de classe
+ document.querySelectorAll('.class-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ document.querySelectorAll('.class-btn').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ send('select_class', { class: btn.dataset.class });
+ $('ready-btn').disabled = false;
+ });
+ });
+
+ // pret
+ $('ready-btn').addEventListener('click', () => {
+ send('ready');
+ $('ready-btn').disabled = true;
+ });
+
+ // demarrer (host)
+ $('start-btn').addEventListener('click', () => send('start_game'));
+
+ // plein ecran
+ $('fullscreen-btn')?.addEventListener('click', () => {
+ if (!document.fullscreenElement) {
+ document.documentElement.requestFullscreen();
+ } else {
+ document.exitFullscreen();
+ }
+ });
+
+ // settings (todo)
+ $('settings-btn').addEventListener('click', () => {
+ callbacks.notify('Settings coming soon');
+ });
+
+ // leaderboard
+ $('leaderboard-btn').addEventListener('click', () => callbacks.showLeaderboard?.());
+ $('lb-back-btn').addEventListener('click', () => callbacks.hideLeaderboard?.());
+}
diff --git a/client/js/constants.js b/client/js/constants.js
new file mode 100644
index 0000000..6bab136
--- /dev/null
+++ b/client/js/constants.js
@@ -0,0 +1,75 @@
+// constants.js : miroir cote client de server/constants.py
+// si tu modifies une valeur ici, modifier aussi dans constants.py
+
+export const ARENA_WIDTH = 35;
+export const ARENA_HEIGHT = 35;
+
+export const SCALE = 16;
+
+// rendu iso : ratio 2:1 classique
+export const TILE_WIDTH = 64;
+export const TILE_HEIGHT = 32;
+
+export const SOULGATE_X = 0;
+export const SOULGATE_Y = -15;
+export const PLAYER_SPAWN_X = 0;
+export const PLAYER_SPAWN_Y = -10;
+
+export const TICK_RATE = 20;
+export const TICK_DURATION = 0.05;
+
+export const SOULGATE_MAX_HP = 100;
+
+export const CLASS_COLORS = {
+ kael: 0xe06060,
+ seris: 0x6080e0,
+ aldric: 0x60c080,
+};
+
+export const PREPARATION_DURATION = 20;
+
+export const UPGRADE_CATALOG = {
+ damage_up: { name: "Frappe +", desc: "+25% dégâts projectile", cost: 30, max: 2 },
+ cooldown_down: { name: "Cadence +", desc: "-20% cooldown d'attaque", cost: 25, max: 2 },
+ hp_up: { name: "Vitalité +", desc: "+30 PV max", cost: 35, max: 2 },
+ speed_up: { name: "Vitesse +", desc: "+15% vitesse de déplacement", cost: 20, max: 3 },
+};
+
+// cd des skills 1/2 par classe
+export const ABILITY_COOLDOWNS = {
+ kael: [7.0, 12.0],
+ seris: [6.0, 10.0],
+ aldric: [8.0, 15.0],
+};
+
+export const DISPLACEMENT_COOLDOWNS = { kael: 9.0, seris: 4.0, aldric: 14.0 };
+
+// hitboxes (miroir constants.py, sert au debug visuel)
+export const PLAYER_HITBOX_RADIUS = 0.5;
+export const ENEMY_HITBOX_RADIUS = 0.5;
+export const PROJECTILE_HIT_RADIUS = 0.4;
+export const SOULGATE_HITBOX_RADIUS = 1.0;
+
+// rayons d'attaque Kael
+export const KAEL_MELEE_RADIUS = 3.0;
+export const KAEL_SLAM_RADIUS = 3.5;
+export const KAEL_SLAM_TICKS = 8;
+export const KAEL_STORM_RADIUS = 5.5;
+export const KAEL_STORM_TICKS = 20;
+
+// Seris
+export const SERIS_VOID_RADIUS = 4.0;
+export const SERIS_VOID_TICKS = 10;
+
+// Aldric
+export const ALDRIC_HEAL_RADIUS = 6.0;
+export const ALDRIC_PULSE_RADIUS = 5.0;
+export const ALDRIC_PULSE_TICKS = 8;
+
+export const DIVINE_POST_USE_COOLDOWN = 60.0;
+
+// score
+export const SCORE_PER_KILL = 10;
+export const SCORE_PER_SOUL = 2;
+export const SCORE_VICTORY = 1000;
+export const SCORE_PER_WAVE = 200;
diff --git a/client/js/debug.js b/client/js/debug.js
new file mode 100644
index 0000000..5c1361d
--- /dev/null
+++ b/client/js/debug.js
@@ -0,0 +1,273 @@
+// Debug client — gestion WebSocket, lobby et affichage live du game state
+
+const WS_URL = `ws://${location.hostname}:8000/ws`;
+
+let ws = null;
+let isHost = false;
+let inLobby = false;
+let gameStarted = false;
+let myConnId = null; // identifié via lobby_created / lobby_joined
+let lastTickTime = null;
+let lastTick = null;
+
+
+// Connexion
+
+function connect() {
+ ws = new WebSocket(WS_URL);
+ ws.onopen = () => setStatus("Connecté", "ok");
+ ws.onclose = () => {
+ setStatus("Déconnecté", "error");
+ gameStarted = false;
+ inLobby = false;
+ };
+ ws.onerror = () => setStatus("Erreur WebSocket", "error");
+ ws.onmessage = (e) => {
+ try {
+ handleMessage(JSON.parse(e.data));
+ } catch {
+ log("⚠ message non-JSON reçu");
+ }
+ };
+}
+
+function send(type, data = {}) {
+ if (ws?.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify({ type, ...data }));
+ }
+}
+
+
+// Dispatch des messages
+
+function handleMessage(msg) {
+ if (msg.type === "game_state") {
+ updateGameState(msg);
+ return; // pas de log à 20 Hz
+ }
+ if (msg.type === "error") {
+ log(`⚠ ${msg.code}: ${msg.message}`, "err");
+ return;
+ }
+ log(`← ${msg.type}: ${JSON.stringify(msg).slice(0, 100)}`);
+
+ switch (msg.type) {
+ case "username_set": onUsernameSet(msg); break;
+ case "lobby_created": onLobbyCreated(msg); break;
+ case "lobby_joined": onLobbyJoined(msg); break;
+ case "player_joined": onPlayerJoined(msg); break;
+ case "player_left": onPlayerLeft(msg); break;
+ case "class_selected": onClassSelected(msg); break;
+ case "player_ready": onPlayerReady(msg); break;
+ case "game_starting": onGameStarting(msg); break;
+ }
+}
+
+
+// Handlers lobby
+
+function onUsernameSet(msg) {
+ $("create-btn").disabled = false;
+ $("join-btn").disabled = false;
+ log(`Pseudo défini : ${msg.username}`, "ok");
+}
+
+function onLobbyCreated(msg) {
+ isHost = true;
+ inLobby = true;
+ $("code-input").value = msg.code;
+ $("class-kael").disabled = false;
+ $("class-seris").disabled = false;
+ $("class-aldric").disabled = false;
+ $("start-btn").disabled = false;
+ log(`Lobby créé — code : ${msg.code}`, "ok");
+ renderLobbyPlayers([{ username: $("username-input").value, player_class: null, ready: false, host: true }]);
+}
+
+function onLobbyJoined(msg) {
+ inLobby = true;
+ $("code-input").value = msg.code;
+ $("class-kael").disabled = false;
+ $("class-seris").disabled = false;
+ $("class-aldric").disabled = false;
+ renderLobbyPlayers(msg.players);
+}
+
+function onPlayerJoined(msg) {
+ refreshLobbyFromEvent("player_joined", msg.player);
+}
+
+function onPlayerLeft(msg) {
+ refreshLobbyFromEvent("player_left", { id: msg.player_id });
+}
+
+function onClassSelected(msg) {
+ // met à jour le bouton de classe si c'est nous
+ if (msg.player_id === myConnId || !myConnId) {
+ ["kael", "seris", "aldric"].forEach(c => {
+ $(`class-${c}`).classList.toggle("active", c === msg.class);
+ });
+ }
+}
+
+function onPlayerReady(msg) {
+ if (msg.player_id === myConnId || !myConnId) {
+ $("ready-btn").classList.add("active");
+ $("ready-btn").textContent = "✓ Prêt";
+ }
+}
+
+function onGameStarting(msg) {
+ gameStarted = true;
+ log(`▶ Partie démarrée ! (countdown: ${msg.countdown}s)`, "ok");
+ $("streaming").textContent = "● streaming 20 Hz";
+ $("streaming").style.color = "#4caf50";
+}
+
+
+// Game state
+
+function updateGameState(msg) {
+ // Tick + estimation du tick rate
+ const now = performance.now();
+ if (lastTick !== null && lastTickTime !== null) {
+ const dt = now - lastTickTime;
+ const rate = (1000 / dt).toFixed(0);
+ $("tick-rate").textContent = `~${rate}/s`;
+ }
+ lastTick = msg.tick;
+ lastTickTime = now;
+ $("tick").textContent = msg.tick;
+
+ // Soulgate
+ const sg = msg.soulgate;
+ const sgPct = Math.round(sg.hp / sg.max_hp * 100);
+ $("sg-hp").textContent = `${sg.hp} / ${sg.max_hp}`;
+ $("sg-pct").textContent = `${sgPct}%`;
+ $("sg-bar").style.width = `${sgPct}%`;
+
+ // Vague
+ const w = msg.wave;
+ const badge = $("wave-info");
+ badge.innerHTML = `Vague ${w.number} — ${w.state}${w.enemies_remaining > 0 ? ` (${w.enemies_remaining} ennemis)` : ""}`;
+
+ // Joueurs
+ const container = $("players-list");
+ container.innerHTML = msg.players.map(p => {
+ const pct = p.alive ? Math.round(p.hp / p.max_hp * 100) : 0;
+ return `
+
+
+ ${p.username}
+ ${p.class}
+
+
+ ${p.hp} / ${p.max_hp} HP
+ ${p.alive ? pct + "%" : "mort"}
+
+
+
`;
+ }).join("");
+}
+
+
+// Helpers UI
+
+function setStatus(text, cls = "") {
+ const el = $("status");
+ el.textContent = text;
+ el.className = "status " + cls;
+}
+
+function log(text, cls = "") {
+ const pre = $("log");
+ const ts = new Date().toLocaleTimeString("fr");
+ const line = document.createElement("span");
+ line.className = cls;
+ line.textContent = `[${ts}] ${text}\n`;
+ pre.appendChild(line);
+ pre.scrollTop = pre.scrollHeight;
+}
+
+function $(id) { return document.getElementById(id); }
+
+let _lobbyPlayers = [];
+
+function renderLobbyPlayers(players) {
+ _lobbyPlayers = players;
+ const container = $("lobby-players");
+ if (!players.length) {
+ container.innerHTML = `Aucun joueur`;
+ return;
+ }
+ container.innerHTML = players.map(p => `
+
+ ${p.username ?? p.id ?? "?"}
+ ${p.class ?? p.player_class ?? "—"}
+ ${p.ready ? `✓` : ""}
+ ${p.host ? `[chef]` : ""}
+
+ `).join("");
+}
+
+function refreshLobbyFromEvent(eventType, data) {
+ // re-render best-effort sans refetch complet
+ if (eventType === "player_joined") {
+ _lobbyPlayers.push(data);
+ } else if (eventType === "player_left") {
+ _lobbyPlayers = _lobbyPlayers.filter(p => p.id !== data.id);
+ }
+ renderLobbyPlayers(_lobbyPlayers);
+}
+
+
+// Event listeners
+
+$("username-btn").addEventListener("click", () => {
+ const v = $("username-input").value.trim();
+ if (v) send("set_username", { username: v });
+});
+
+$("username-input").addEventListener("keydown", e => {
+ if (e.key === "Enter") $("username-btn").click();
+});
+
+$("create-btn").addEventListener("click", () => send("create_lobby"));
+
+$("join-btn").addEventListener("click", () => {
+ const code = $("code-input").value.trim().toUpperCase();
+ if (code) send("join_lobby", { code });
+});
+
+$("code-input").addEventListener("keydown", e => {
+ if (e.key === "Enter") $("join-btn").click();
+});
+
+["kael", "seris", "aldric"].forEach(cls => {
+ $(`class-${cls}`).addEventListener("click", () => {
+ send("select_class", { class: cls });
+ });
+});
+
+$("ready-btn").addEventListener("click", () => {
+ $("ready-btn").disabled = true;
+ send("ready");
+});
+
+$("start-btn").addEventListener("click", () => send("start_game"));
+
+// Activer Ready dès qu'une classe est sélectionnée
+["kael", "seris", "aldric"].forEach(cls => {
+ $(`class-${cls}`).addEventListener("click", () => {
+ $("ready-btn").disabled = false;
+ $("ready-btn").classList.remove("active");
+ $("ready-btn").textContent = "Prêt";
+ });
+});
+
+
+// Init
+
+connect();
diff --git a/client/js/hud.js b/client/js/hud.js
new file mode 100644
index 0000000..dba19ec
--- /dev/null
+++ b/client/js/hud.js
@@ -0,0 +1,218 @@
+// hud.js : maj des elements html du hud (hp, cd, vagues, upgrades, end game)
+
+import { UPGRADE_CATALOG, ABILITY_COOLDOWNS, DISPLACEMENT_COOLDOWNS, SCORE_PER_KILL, SCORE_PER_SOUL, SCORE_VICTORY, SCORE_PER_WAVE } from './constants.js';
+import { showEndGameForm } from './leaderboard.js';
+import { send } from './network.js';
+import { setInputBlocked } from './input.js';
+
+
+const BUFF_LABELS = {
+ invulnerable: 'Invulnerable',
+ casting: 'Incantation',
+ flying: 'Vol',
+ intangible: 'Intangible',
+ combat_buff: 'Buff combat',
+ damage_mult: 'Degats x3',
+};
+
+const WAVE_STATES = {
+ combat: 'Combat',
+ preparation: 'Preparation',
+ boss: 'Boss',
+ victory: 'Victoire',
+};
+
+
+export function updatePlayerHud(local) {
+ document.getElementById('hud-class-name').textContent = (local.class ?? '').toUpperCase();
+ document.getElementById('hud-souls').textContent = 'Ames : ' + (local.souls ?? 0);
+
+ const hpPct = local.max_hp > 0 ? Math.max(0, local.hp / local.max_hp * 100) : 0;
+ document.getElementById('hud-hp-fill').style.width = hpPct + '%';
+ document.getElementById('hud-hp-text').textContent = local.hp + ' / ' + local.max_hp;
+
+ _updateSkillCooldowns(local);
+ _updateBuffTags(local);
+}
+
+
+function _updateSkillCooldowns(local) {
+ const cds = local.cooldowns ?? {};
+ const abMaxes = ABILITY_COOLDOWNS[local.class] ?? [8, 12];
+
+ [['hud-s1', 0], ['hud-s2', 1]].forEach(([id, i]) => {
+ const el = document.getElementById(id);
+ const cd = cds['ability_' + (i + 1)] ?? 0;
+ const maxCd = abMaxes[i];
+ const fill = cd <= 0 ? 100 : (1 - cd / maxCd) * 100;
+ el.querySelector('.cd-fill').style.height = fill + '%';
+ el.classList.toggle('ready', cd <= 0);
+ el.classList.remove('locked');
+ });
+
+ const elE = document.getElementById('hud-se');
+ const cdE = cds['displacement'] ?? 0;
+ const maxCdE = DISPLACEMENT_COOLDOWNS[local.class] ?? 6;
+ const fillE = cdE <= 0 ? 100 : (1 - cdE / maxCdE) * 100;
+ elE.querySelector('.cd-fill').style.height = fillE + '%';
+ elE.classList.toggle('ready', cdE <= 0);
+ elE.classList.remove('locked', 'divine-ready');
+}
+
+
+function _updateBuffTags(local) {
+ const buffsEl = document.getElementById('hud-buffs');
+ buffsEl.innerHTML = '';
+
+ for (const b of (local.buffs ?? [])) {
+ const label = BUFF_LABELS[b.type];
+ if (!label) continue;
+
+ const tag = document.createElement('span');
+ tag.className = 'buff-tag ' + b.type;
+ tag.textContent = label;
+ buffsEl.appendChild(tag);
+ }
+}
+
+
+export function updateSoulgateBar(sg) {
+ if (!sg) return;
+ const pct = sg.max_hp > 0 ? Math.max(0, sg.hp / sg.max_hp * 100) : 0;
+ document.getElementById('sg-fill').style.width = pct + '%';
+ document.getElementById('sg-hp-text').textContent = sg.hp + ' / ' + sg.max_hp;
+}
+
+
+export function updateWaveInfo(wave) {
+ if (!wave) return;
+ document.getElementById('wave-label').textContent = 'Vague ' + wave.number + ' / 3';
+ document.getElementById('wave-state-label').textContent = WAVE_STATES[wave.state] ?? wave.state;
+ document.getElementById('wave-enemies').textContent = 'Ennemis : ' + (wave.enemies_remaining ?? 0);
+}
+
+
+export function updateBossBar(wave) {
+ const isBoss = wave.state === 'boss' && wave.boss_max_hp > 0;
+ document.getElementById('boss-bar').classList.toggle('hidden', !isBoss);
+
+ if (isBoss) {
+ document.getElementById('boss-bar-name').textContent = wave.boss_name.toUpperCase();
+ const pct = Math.max(0, wave.boss_hp / wave.boss_max_hp * 100);
+ document.getElementById('boss-bar-fill').style.width = pct + '%';
+ document.getElementById('boss-bar-hp').textContent = wave.boss_hp + ' / ' + wave.boss_max_hp;
+ }
+}
+
+
+export function updateUpgradePanel(wave, local) {
+ const inPrep = wave.state === 'preparation';
+ document.getElementById('upgrade-panel').classList.toggle('hidden', !inPrep);
+ // pendant la prep on bloque les inputs pour focus sur le menu
+ setInputBlocked(inPrep || gameOver);
+
+ if (inPrep) {
+ document.getElementById('prep-timer').textContent = Math.ceil(wave.prep_timer);
+ const souls = local?.souls ?? 0;
+ document.getElementById('soul-count').textContent = souls;
+ _updateUpgradeList(souls, local?.upgrades ?? {});
+ }
+}
+
+
+let _upgradeListBuilt = false;
+let _upgradeClickBound = false;
+
+function _updateUpgradeList(souls, upgrades) {
+ // on construit la liste 1 seule fois et on met juste a jour les stacks/disabled apres
+ // sinon on detruit les boutons pendant qu'on clique dessus
+ const list = document.getElementById('upgrade-list');
+
+ if (!_upgradeListBuilt) {
+ list.innerHTML = '';
+ for (const [id, spec] of Object.entries(UPGRADE_CATALOG)) {
+ const item = document.createElement('div');
+ item.className = 'upgrade-item';
+ item.dataset.id = id;
+ item.innerHTML = `
+
+
${spec.name}
+
${spec.desc}
+
+
+ `;
+ list.appendChild(item);
+ }
+ _upgradeListBuilt = true;
+ }
+
+ if (!_upgradeClickBound) {
+ // Délégation : 1 seul listener sur la liste, ne meurt jamais avec les boutons
+ list.addEventListener('click', (e) => {
+ const btn = e.target.closest('.upgrade-buy');
+ if (!btn || btn.disabled) return;
+ send('player_upgrade', { upgrade_id: btn.dataset.id });
+ });
+ _upgradeClickBound = true;
+ }
+
+ for (const [id, spec] of Object.entries(UPGRADE_CATALOG)) {
+ const item = list.querySelector(`.upgrade-item[data-id="${id}"]`);
+ if (!item) continue;
+ const stacks = upgrades[id] ?? 0;
+ const maxed = stacks >= spec.max;
+ item.querySelector('.upgrade-stacks').textContent =
+ '\u25cf'.repeat(stacks) + '\u25cb'.repeat(spec.max - stacks);
+ const btn = item.querySelector('.upgrade-buy');
+ btn.disabled = maxed || souls < spec.cost;
+ btn.textContent = maxed ? 'MAX' : spec.cost + ' ames';
+ }
+}
+
+export function resetUpgradeList() {
+ _upgradeListBuilt = false;
+ _upgradeClickBound = false;
+}
+
+
+let gameOver = false;
+
+export function resetGameOver() { gameOver = false; }
+
+export function checkGameEnd(msg, gameStartTime, isHost = false, lobbyCode = '', submitterId = '') {
+ if (gameOver) return;
+
+ const victory = msg.wave?.state === 'victory';
+ const defeat = msg.soulgate?.hp === 0;
+ if (!victory && !defeat) return;
+
+ gameOver = true;
+ setInputBlocked(true);
+
+ const timeSecs = gameStartTime ? Math.floor((Date.now() - gameStartTime) / 1000) : 0;
+ const wavesCompleted = msg.wave?.number ?? 1;
+ const players = msg.players ?? [];
+
+ const totalKills = players.reduce((s, p) => s + (p.enemies_killed ?? 0), 0);
+ const totalSouls = players.reduce((s, p) => s + (p.souls ?? 0), 0);
+ const score = totalKills * SCORE_PER_KILL
+ + totalSouls * SCORE_PER_SOUL
+ + (victory ? SCORE_VICTORY : 0)
+ + wavesCompleted * SCORE_PER_WAVE;
+
+ showEndGameForm({
+ victory,
+ score,
+ timeSecs,
+ wavesCompleted,
+ isHost,
+ lobbyCode,
+ submitterId,
+ players: players.map(p => ({
+ username: p.username,
+ class: p.class,
+ souls: p.souls ?? 0,
+ enemies_killed: p.enemies_killed ?? 0,
+ })),
+ });
+}
diff --git a/client/js/input.js b/client/js/input.js
new file mode 100644
index 0000000..2e10b74
--- /dev/null
+++ b/client/js/input.js
@@ -0,0 +1,110 @@
+// input.js : capture clavier + souris, envoi au serveur
+
+import { send } from './network.js';
+
+const moveKeys = { up: false, down: false, left: false, right: false };
+
+let lastDx = 0, lastDy = 0;
+let inputBlocked = false;
+
+export function setInputBlocked(blocked) {
+ if (blocked && !inputBlocked) {
+ // on coupe le mouvement cote serveur si on bougeait, sinon le perso continue d avancer
+ if (lastDx !== 0 || lastDy !== 0) {
+ lastDx = 0; lastDy = 0;
+ send('input', { dx: 0, dy: 0 });
+ }
+ moveKeys.up = moveKeys.down = moveKeys.left = moveKeys.right = false;
+ }
+ inputBlocked = blocked;
+}
+
+export function isInputBlocked() { return inputBlocked; }
+
+
+const MOVE_KEY_MAP = {
+ 'z': 'up', 'ArrowUp': 'up',
+ 's': 'down', 'ArrowDown': 'down',
+ 'q': 'left', 'ArrowLeft': 'left',
+ 'd': 'right', 'ArrowRight': 'right',
+};
+
+// AZERTY : on utilise e.code (Digit1) plutot que e.key (qui donne '&', 'é')
+const ABILITY_CODE_MAP = { 'Digit1': 1, 'Digit2': 2 };
+
+
+function computeDirection() {
+ let dx = 0, dy = 0;
+ if (moveKeys.left) dx -= 1;
+ if (moveKeys.right) dx += 1;
+ if (moveKeys.up) dy -= 1;
+ if (moveKeys.down) dy += 1;
+
+ // normaliser sinon la diagonale est plus rapide que les axes
+ if (dx !== 0 && dy !== 0) {
+ const inv = 1 / Math.SQRT2;
+ dx *= inv;
+ dy *= inv;
+ }
+
+ return { dx, dy };
+}
+
+
+function onKeyDown(e, getTarget) {
+ if (inputBlocked) return;
+
+ const moveAction = MOVE_KEY_MAP[e.key];
+ if (moveAction) {
+ e.preventDefault();
+ if (moveKeys[moveAction]) return;
+ moveKeys[moveAction] = true;
+
+ const { dx, dy } = computeDirection();
+ if (dx !== lastDx || dy !== lastDy) {
+ lastDx = dx; lastDy = dy;
+ send('input', { dx, dy });
+ }
+ return;
+ }
+
+ const abilityId = ABILITY_CODE_MAP[e.code];
+ if (abilityId) {
+ e.preventDefault();
+ const { wx, wy } = getTarget();
+ send('ability', { id: abilityId, tx: wx, ty: wy });
+ return;
+ }
+
+ if (e.key === 'e') {
+ e.preventDefault();
+ const { wx, wy } = getTarget();
+ send('displacement', { tx: wx, ty: wy });
+ return;
+ }
+}
+
+
+function onKeyUp(e) {
+ if (inputBlocked) return;
+
+ const moveAction = MOVE_KEY_MAP[e.key];
+ if (!moveAction) return;
+
+ e.preventDefault();
+ if (!moveKeys[moveAction]) return;
+
+ moveKeys[moveAction] = false;
+ const { dx, dy } = computeDirection();
+
+ if (dx !== lastDx || dy !== lastDy) {
+ lastDx = dx; lastDy = dy;
+ send('input', { dx, dy });
+ }
+}
+
+
+export function startInputTracking(getTarget) {
+ document.addEventListener('keydown', e => onKeyDown(e, getTarget));
+ document.addEventListener('keyup', e => onKeyUp(e));
+}
diff --git a/client/js/leaderboard.js b/client/js/leaderboard.js
new file mode 100644
index 0000000..f7ea1e5
--- /dev/null
+++ b/client/js/leaderboard.js
@@ -0,0 +1,247 @@
+// leaderboard.js — Affichage du classement et soumission d'un résultat
+
+const API = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
+ ? 'http://localhost:8000'
+ : `http://${window.location.hostname}:8000`;
+
+const CLASS_ICONS = { kael: '⚔️', seris: '🗡️', aldric: '🌿' };
+
+// Leaderboard screen
+
+export async function showLeaderboard() {
+ document.getElementById('home-screen').classList.add('hidden');
+ const screen = document.getElementById('leaderboard-screen');
+ screen.classList.remove('hidden');
+ await _loadAndRender();
+}
+
+export function hideLeaderboard() {
+ document.getElementById('leaderboard-screen').classList.add('hidden');
+ document.getElementById('home-screen').classList.remove('hidden');
+}
+
+async function _loadAndRender() {
+ const tbody = document.getElementById('lb-tbody');
+ tbody.innerHTML = '| Chargement... |
';
+ try {
+ const res = await fetch(`${API}/leaderboard`);
+ const rows = await res.json();
+ _renderTable(rows);
+ } catch {
+ tbody.innerHTML = '| Impossible de charger le classement. |
';
+ }
+}
+
+function _renderTable(rows) {
+ const tbody = document.getElementById('lb-tbody');
+ if (!rows.length) {
+ tbody.innerHTML = '| Aucune partie enregistrée. |
';
+ return;
+ }
+ tbody.innerHTML = rows.map((r, i) => {
+ const time = _formatTime(r.time_seconds);
+ const vic = r.victory ? '✓' : '✗';
+ const discords = [
+ r.p1_discord ? `@${r.p1_discord}` : (r.p1_username || ''),
+ r.p2_discord ? `@${r.p2_discord}` : (r.p2_username || ''),
+ r.p3_discord ? `@${r.p3_discord}` : (r.p3_username || ''),
+ ].filter(Boolean).join(' · ');
+ return `
+ | #${i + 1} |
+ ${_esc(r.team_name)} |
+ ${r.score.toLocaleString()} |
+ ${time} |
+ ${vic} ${r.waves_completed}/3 |
+ ${_esc(discords)} |
+
`;
+ }).join('');
+}
+
+function _formatTime(s) {
+ const m = Math.floor(s / 60);
+ const sec = String(s % 60).padStart(2, '0');
+ return `${m}m${sec}s`;
+}
+
+function _esc(s) {
+ return String(s ?? '').replace(/&/g, '&').replace(//g, '>');
+}
+
+
+// End-game form
+
+export function showEndGameForm(data) {
+ // data = { victory, score, timeSecs, wavesCompleted, players: [{username, class, souls, enemies_killed}] }
+ const overlay = document.getElementById('endgame-overlay');
+ const title = document.getElementById('endgame-title');
+ const sub = document.getElementById('endgame-sub');
+
+ overlay.classList.remove('hidden');
+ title.textContent = data.victory ? 'VICTOIRE !' : 'DÉFAITE';
+ title.className = data.victory ? 'victory' : 'defeat';
+ sub.textContent = data.victory
+ ? 'Le Soulgate est sauvegardé.'
+ : 'Le Soulgate a été détruit.';
+
+ document.getElementById('endgame-score').textContent = `Score : ${data.score.toLocaleString()} pts`;
+ document.getElementById('endgame-time').textContent = `Temps : ${_formatTime(data.timeSecs)}`;
+ document.getElementById('endgame-waves').textContent = `Vagues : ${data.wavesCompleted}/3`;
+
+ _buildDiscordForm(data);
+}
+
+function _buildDiscordForm(data) {
+ const form = document.getElementById('endgame-form');
+ form.innerHTML = '';
+
+ // Defaite : pas de soumission au leaderboard, juste rejouer
+ if (!data.victory) {
+ const info = document.createElement('div');
+ info.className = 'lb-host-only';
+ info.textContent = "Défaite : le score n'est pas enregistré dans le leaderboard.";
+ form.appendChild(info);
+
+ const replayBtn = document.createElement('button');
+ replayBtn.className = 'mc-btn';
+ replayBtn.textContent = 'Rejouer';
+ replayBtn.style.marginTop = '8px';
+ replayBtn.addEventListener('click', () => location.reload());
+ form.appendChild(replayBtn);
+ return;
+ }
+
+ // Seul le chef d'équipe peut soumettre le score → les autres voient un message + Rejouer
+ if (!data.isHost) {
+ const info = document.createElement('div');
+ info.className = 'lb-host-only';
+ info.textContent = "Seul le chef d'équipe peut enregistrer le score dans le leaderboard.";
+ form.appendChild(info);
+
+ const replayBtn = document.createElement('button');
+ replayBtn.className = 'mc-btn';
+ replayBtn.textContent = 'Rejouer';
+ replayBtn.style.marginTop = '8px';
+ replayBtn.addEventListener('click', () => location.reload());
+ form.appendChild(replayBtn);
+ return;
+ }
+
+ // Champ nom d'équipe
+ const teamRow = document.createElement('div');
+ teamRow.className = 'lb-field';
+ teamRow.innerHTML = ``;
+ form.appendChild(teamRow);
+
+ // Un champ Discord par joueur
+ data.players.forEach((p, i) => {
+ const icon = CLASS_ICONS[p.class] ?? '';
+ const row = document.createElement('div');
+ row.className = 'lb-field';
+ row.innerHTML = `
+
+
+ `;
+ form.appendChild(row);
+ });
+
+ // Bouton soumettre
+ const submitBtn = document.createElement('button');
+ submitBtn.id = 'lb-submit-btn';
+ submitBtn.className = 'mc-btn mc-btn-gold';
+ submitBtn.textContent = 'Enregistrer dans le leaderboard';
+ form.appendChild(submitBtn);
+
+ // Bouton rejouer
+ const replayBtn = document.createElement('button');
+ replayBtn.className = 'mc-btn';
+ replayBtn.textContent = 'Rejouer';
+ replayBtn.style.marginTop = '8px';
+ replayBtn.addEventListener('click', () => location.reload());
+ form.appendChild(replayBtn);
+
+ // Vérification Discord à la saisie
+ form.querySelectorAll('.lb-discord-input').forEach(input => {
+ let _debounce;
+ input.addEventListener('input', () => {
+ clearTimeout(_debounce);
+ _debounce = setTimeout(() => _checkDiscord(input), 500);
+ });
+ });
+
+ submitBtn.addEventListener('click', () => _submitForm(data, form, submitBtn));
+}
+
+async function _checkDiscord(input) {
+ const tag = input.value.trim().replace(/^@/, '');
+ const errEl = input.parentElement.querySelector('.lb-discord-error');
+ if (!tag) { errEl.textContent = ''; input.classList.remove('taken'); return; }
+
+ try {
+ const res = await fetch(`${API}/leaderboard/check-discord/${encodeURIComponent(tag)}`);
+ const json = await res.json();
+ if (json.taken) {
+ errEl.textContent = '❌ Ce Discord est déjà dans le leaderboard — choisis-en un autre.';
+ input.classList.add('taken');
+ } else {
+ errEl.textContent = '✓';
+ input.classList.remove('taken');
+ }
+ } catch {
+ errEl.textContent = '';
+ }
+}
+
+async function _submitForm(data, form, btn) {
+ // Bloquer si un Discord est déjà pris
+ if (form.querySelector('.lb-discord-input.taken')) {
+ alert('Un ou plusieurs Discord sont déjà utilisés dans le leaderboard.');
+ return;
+ }
+
+ const teamName = document.getElementById('lb-team-name').value.trim();
+ if (!teamName) { alert("Entre un nom d'équipe."); return; }
+
+ const discordInputs = [...form.querySelectorAll('.lb-discord-input')];
+ const players = data.players;
+
+ const payload = {
+ team_name: teamName,
+ score: data.score,
+ time_seconds: data.timeSecs,
+ waves_completed: data.wavesCompleted,
+ victory: data.victory,
+ lobby_code: data.lobbyCode ?? '', // enforcement host-only côté serveur
+ submitter_id: data.submitterId ?? '',
+ p1_username: players[0]?.username ?? '', p1_class: players[0]?.class ?? '',
+ p1_discord: (discordInputs[0]?.value ?? '').trim().replace(/^@/, ''),
+ p2_username: players[1]?.username ?? '', p2_class: players[1]?.class ?? '',
+ p2_discord: (discordInputs[1]?.value ?? '').trim().replace(/^@/, ''),
+ p3_username: players[2]?.username ?? '', p3_class: players[2]?.class ?? '',
+ p3_discord: (discordInputs[2]?.value ?? '').trim().replace(/^@/, ''),
+ };
+
+ btn.disabled = true;
+ btn.textContent = 'Envoi...';
+
+ try {
+ const res = await fetch(`${API}/leaderboard/submit`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ });
+ const json = await res.json();
+ if (!res.ok) {
+ alert(json.detail ?? 'Erreur lors de l\'envoi.');
+ btn.disabled = false;
+ btn.textContent = 'Enregistrer dans le leaderboard';
+ return;
+ }
+ btn.textContent = `✓ Enregistré ! Vous êtes #${json.rank} au classement.`;
+ btn.style.background = '#2a7';
+ } catch {
+ alert('Erreur réseau — impossible de contacter le serveur.');
+ btn.disabled = false;
+ btn.textContent = 'Enregistrer dans le leaderboard';
+ }
+}
diff --git a/client/js/main.js b/client/js/main.js
new file mode 100644
index 0000000..a0a1b70
--- /dev/null
+++ b/client/js/main.js
@@ -0,0 +1,230 @@
+// main.js — point d'entree client, init Pixi et handlers reseau
+
+import { Application, Container } from 'pixi.js';
+import { connect, send, on } from './network.js';
+import { showLeaderboard, hideLeaderboard } from './leaderboard.js';
+import { updateIsoLayout, drawStaticArena, updatePlayers, updateProjectiles, updateEnemies, updateAoeZones, updateCamera, screenToWorld } from './renderer.js';
+import { loadArenaMap } from './renderArena.js';
+import { startInputTracking, isInputBlocked } from './input.js';
+import { updatePlayerHud, updateSoulgateBar, updateWaveInfo, updateBossBar, updateUpgradePanel, checkGameEnd } from './hud.js';
+import { bindEvents } from './bindings.js';
+import { loadKaelAssets, loadSerisAssets, loadAldricAssets } from './renderPlayers.js';
+import { loadFractureAssets, loadRampantAssets, loadColosseAssets, loadEclatAssets, loadVexarisAssets } from './renderEnemies.js';
+
+let localUsername = null;
+let localId = null;
+let isHost = false;
+let lobbyCode = '';
+let camX = 0, camY = 0;
+let gameStartTime = null;
+
+
+async function main() {
+ const app = new Application();
+ await app.init({
+ canvas: document.getElementById('game-canvas'),
+ resizeTo: window,
+ backgroundAlpha: 0,
+ antialias: true,
+ resolution: window.devicePixelRatio || 1,
+ autoDensity: true,
+ });
+
+ let worldContainer = null;
+ let layerArena = null;
+ let layerAoe = null;
+ let layerEnemies = null;
+ let layerEntities = null;
+ let layerProjectiles = null;
+
+ const playerPool = {};
+ const projPool = {};
+ const enemyPool = {};
+ const aoePool = {};
+
+ async function initGameWorld() {
+ await loadArenaMap();
+ await loadKaelAssets();
+ await loadSerisAssets();
+ await loadAldricAssets();
+ await loadFractureAssets();
+ await loadRampantAssets();
+ await loadColosseAssets();
+ await loadEclatAssets();
+ await loadVexarisAssets();
+
+ worldContainer = new Container();
+ layerArena = new Container();
+ layerAoe = new Container();
+ layerEnemies = new Container();
+ layerEntities = new Container();
+ layerProjectiles = new Container();
+
+ // ordre d'affichage : arene tout en bas, projectiles tout en haut
+ worldContainer.addChild(layerArena, layerAoe, layerEnemies, layerEntities, layerProjectiles);
+ app.stage.addChild(worldContainer);
+
+ updateIsoLayout(app.screen.width, app.screen.height);
+
+ layerArena.removeChildren();
+ drawStaticArena(layerArena);
+
+ updateCamera(worldContainer, app.screen.width, app.screen.height, camX, camY);
+
+ app.renderer.on('resize', (w, h) => {
+ updateIsoLayout(w, h);
+ layerArena.removeChildren();
+ drawStaticArena(layerArena);
+ updateCamera(worldContainer, w, h, camX, camY);
+ });
+ }
+
+ connect();
+
+ on('_open', () => setStatus('ok', 'Connected'));
+ on('_close', () => setStatus('err', 'Disconnected'));
+
+ on('username_set', (msg) => {
+ if (msg.my_id) localId = msg.my_id;
+ document.getElementById('home-buttons').classList.remove('hidden');
+ document.getElementById('home-username').classList.add('hidden');
+ });
+
+ on('lobby_created', (msg) => {
+ isHost = true;
+ lobbyCode = msg.code;
+ document.getElementById('code-display').textContent = msg.code;
+ document.getElementById('start-btn').classList.remove('hidden');
+ switchToLobby();
+ notify('Lobby created — code: ' + msg.code);
+ });
+
+ on('lobby_joined', (msg) => {
+ lobbyCode = msg.code;
+ switchToLobby();
+ document.getElementById('code-display').textContent = msg.code;
+ notify('Joined lobby ' + msg.code);
+ });
+
+ on('player_joined', (msg) => notify(msg.player.username + ' joined'));
+
+ on('player_ready', () => {
+ const btn = document.getElementById('ready-btn');
+ btn.textContent = 'READY!';
+ btn.classList.add('mc-btn-gold');
+ btn.disabled = true;
+ });
+
+ on('game_starting', async () => {
+ gameStartTime = Date.now();
+ document.getElementById('home-screen').classList.add('hidden');
+ document.getElementById('lobby-screen').classList.add('hidden');
+ document.getElementById('loading-screen').classList.remove('hidden');
+
+ await initGameWorld();
+
+ document.getElementById('loading-screen').classList.add('hidden');
+
+ ['player-hud', 'soulgate-hud', 'wave-hud', 'fullscreen-btn'].forEach(id =>
+ document.getElementById(id).classList.remove('hidden')
+ );
+
+ const getTarget = () => screenToWorld(mouseX, mouseY, worldContainer.x, worldContainer.y);
+ startInputTracking(getTarget);
+
+ app.canvas.addEventListener('click', (e) => {
+ if (isInputBlocked()) return;
+ const { wx, wy } = screenToWorld(e.clientX, e.clientY, worldContainer.x, worldContainer.y);
+ send('attack', { tx: wx, ty: wy });
+ });
+
+ app.canvas.addEventListener('contextmenu', e => e.preventDefault());
+ });
+
+ on('game_state', (msg) => {
+ if (!worldContainer) return;
+
+ updatePlayers(layerEntities, playerPool, msg.players);
+ updateEnemies(layerEnemies, enemyPool, msg.enemies);
+ updateProjectiles(layerProjectiles, projPool, msg.projectiles);
+ updateAoeZones(layerAoe, aoePool, msg.aoe_zones ?? []);
+
+ // retrouver le joueur local : par id si dispo (fiable meme avec pseudos en doublon), sinon par username
+ const local = localId
+ ? msg.players.find(p => p.id === localId)
+ : localUsername
+ ? msg.players.find(p => p.username === localUsername)
+ : null;
+
+ if (local) {
+ camX = local.x;
+ camY = local.y;
+ updateCamera(worldContainer, app.screen.width, app.screen.height, camX, camY);
+ updatePlayerHud(local);
+ }
+
+ updateSoulgateBar(msg.soulgate);
+ updateWaveInfo(msg.wave);
+ updateBossBar(msg.wave);
+ updateUpgradePanel(msg.wave, local);
+ checkGameEnd(msg, gameStartTime, isHost, lobbyCode, localId);
+ });
+
+ on('error', (msg) => notify(msg.message, true));
+
+ bindEvents({
+ setUsername: (v) => { localUsername = v; },
+ switchToHome,
+ notify,
+ showLeaderboard,
+ hideLeaderboard,
+ });
+}
+
+
+let mouseX = 0, mouseY = 0;
+document.addEventListener('mousemove', e => {
+ mouseX = e.clientX;
+ mouseY = e.clientY;
+});
+
+
+function switchToLobby() {
+ document.getElementById('home-screen').classList.add('hidden');
+ document.getElementById('lobby-screen').classList.remove('hidden');
+}
+
+function switchToHome() {
+ isHost = false;
+ lobbyCode = '';
+ document.getElementById('lobby-screen').classList.add('hidden');
+ document.getElementById('home-screen').classList.remove('hidden');
+ document.getElementById('home-buttons').classList.remove('hidden');
+ document.getElementById('home-username').classList.add('hidden');
+ document.getElementById('join-row').classList.add('hidden');
+ document.getElementById('code-input').value = '';
+ document.getElementById('code-display').textContent = '------';
+ const readyBtn = document.getElementById('ready-btn');
+ readyBtn.disabled = true;
+ readyBtn.textContent = 'Ready';
+ readyBtn.classList.remove('mc-btn-gold');
+ document.getElementById('start-btn').classList.add('hidden');
+ document.querySelectorAll('.class-btn').forEach(b => b.classList.remove('active'));
+}
+
+
+function setStatus(cls, text) {
+ const el = document.getElementById('home-status');
+ el.className = 'home-status ' + cls;
+ el.textContent = text;
+}
+
+function notify(text, isError = false) {
+ const el = document.getElementById('overlay-msg');
+ if (!el) return;
+ el.textContent = text;
+ el.className = 'msg' + (isError ? ' err' : '');
+}
+
+
+main().catch(console.error);
diff --git a/client/js/network.js b/client/js/network.js
new file mode 100644
index 0000000..0e327ca
--- /dev/null
+++ b/client/js/network.js
@@ -0,0 +1,37 @@
+// network.js : websocket client (ouverture, send, listeners par type)
+
+const WS_URL = `ws://${location.hostname}:8000/ws`;
+
+let _ws = null;
+const _handlers = {};
+
+
+export function connect() {
+ _ws = new WebSocket(WS_URL);
+ _ws.onopen = () => _emit({ type: '_open' });
+ _ws.onclose = () => _emit({ type: '_close' });
+ _ws.onmessage = ({ data }) => {
+ try {
+ _emit(JSON.parse(data));
+ } catch {
+ console.warn('SOULGATE — message non-JSON reçu');
+ }
+ };
+}
+
+
+export function send(type, data = {}) {
+ if (_ws?.readyState === WebSocket.OPEN) {
+ _ws.send(JSON.stringify({ type, ...data }));
+ }
+}
+
+
+export function on(type, fn) {
+ _handlers[type] = fn;
+}
+
+
+function _emit(msg) {
+ _handlers[msg.type]?.(msg);
+}
diff --git a/client/js/renderArena.js b/client/js/renderArena.js
new file mode 100644
index 0000000..2c608fa
--- /dev/null
+++ b/client/js/renderArena.js
@@ -0,0 +1,39 @@
+// renderArena.js : dessin de l'arene a partir de la map Tiled + sprite Soulgate
+
+import { Sprite, Assets } from 'pixi.js';
+import { loadTiledMap, drawTiledMap } from './tiledLoader.js';
+import { iso, getTw, getTh } from './renderer.js';
+import { SOULGATE_X, SOULGATE_Y } from './constants.js';
+
+let _mapData = null;
+
+export async function loadArenaMap() {
+ if (_mapData) return;
+ _mapData = await loadTiledMap('assets/maps/arrena.tmj');
+ await Assets.load('assets/soulgate.png');
+}
+
+export function drawStaticArena(layer) {
+ if (!_mapData) {
+ console.warn('arena map not loaded yet');
+ return;
+ }
+ drawTiledMap(layer, _mapData);
+ _drawSoulgate(layer);
+}
+
+function _drawSoulgate(layer) {
+ const tex = Assets.get('assets/soulgate.png');
+ if (!tex) return;
+ const tw = getTw();
+ const th = getTh();
+
+ const sprite = new Sprite(tex);
+ sprite.anchor.set(0.5, 1.0);
+ const p = iso(SOULGATE_X, SOULGATE_Y);
+ sprite.x = p.x;
+ sprite.y = p.y + th / 2;
+ // taille : ~1.5 unite monde de large (source 128px), assez present sans ecraser
+ sprite.scale.set((tw * 1.5) / 128);
+ layer.addChild(sprite);
+}
diff --git a/client/js/renderEnemies.js b/client/js/renderEnemies.js
new file mode 100644
index 0000000..384b73e
--- /dev/null
+++ b/client/js/renderEnemies.js
@@ -0,0 +1,616 @@
+// renderEnemies.js : rendu des ennemis (sprites + barre de vie + ombre)
+// pool de sprites reutilises (pas de recreate par frame)
+
+import { Container, Graphics, Assets, Sprite } from 'pixi.js';
+import { iso, getTh, getTw } from './renderer.js';
+import { ENEMY_HITBOX_RADIUS } from './constants.js';
+
+let _debugHitboxes = false;
+export function setDebugHitboxes(v) { _debugHitboxes = v; }
+
+// Assets Fracture
+const _ft = {};
+let _fractureReady = false;
+
+export async function loadFractureAssets() {
+ if (_fractureReady) return;
+ const b = '../assets/sprites/fracture/';
+
+ function anim(prefix, dirs, n, folder) {
+ const out = [];
+ for (const d of dirs) {
+ for (let i = 0; i < n; i++) {
+ const pad = String(i).padStart(3, '0');
+ out.push([`${prefix}_${d}_${i}`, `${b}animations/${folder}/${d}/frame_${pad}.png`]);
+ }
+ }
+ return out;
+ }
+
+ const D4 = ['south', 'north', 'east', 'west'];
+ const entries = [
+ ['f_south', b + 'rotations/south.png'],
+ ['f_north', b + 'rotations/north.png'],
+ ['f_east', b + 'rotations/east.png'],
+ ['f_west', b + 'rotations/west.png'],
+ ...anim('frun', D4, 4, 'running'),
+ ];
+
+ for (const [key, path] of entries) {
+ const tex = await Assets.load(path);
+ tex.source.scaleMode = 'nearest';
+ _ft[key] = tex;
+ }
+ _fractureReady = true;
+}
+
+// Assets Rampant
+const _rt = {};
+let _rampantReady = false;
+
+export async function loadRampantAssets() {
+ if (_rampantReady) return;
+ const b = '../assets/sprites/rampant/';
+
+ function anim(prefix, dirs, n, folder) {
+ const out = [];
+ for (const d of dirs) {
+ for (let i = 0; i < n; i++) {
+ const pad = String(i).padStart(3, '0');
+ out.push([`${prefix}_${d}_${i}`, `${b}animations/${folder}/${d}/frame_${pad}.png`]);
+ }
+ }
+ return out;
+ }
+
+ const D4 = ['south', 'north', 'east', 'west'];
+ const entries = [
+ ['r_south', b + 'rotations/south.png'],
+ ['r_north', b + 'rotations/north.png'],
+ ['r_east', b + 'rotations/east.png'],
+ ['r_west', b + 'rotations/west.png'],
+ ...anim('rrun', D4, 6, 'running'),
+ ];
+
+ for (const [key, path] of entries) {
+ const tex = await Assets.load(path);
+ tex.source.scaleMode = 'nearest';
+ _rt[key] = tex;
+ }
+ _rampantReady = true;
+}
+
+// Assets Colosse
+const _ct = {};
+let _colosseReady = false;
+
+export async function loadColosseAssets() {
+ if (_colosseReady) return;
+ const b = '../assets/sprites/colosse/';
+ function anim(prefix, dirs, n, folder) {
+ const out = [];
+ for (const d of dirs) {
+ for (let i = 0; i < n; i++) {
+ const pad = String(i).padStart(3, '0');
+ out.push([`${prefix}_${d}_${i}`, `${b}animations/${folder}/${d}/frame_${pad}.png`]);
+ }
+ }
+ return out;
+ }
+ const D4 = ['south', 'north', 'east', 'west'];
+ const entries = [
+ ['c_south', b + 'rotations/south.png'],
+ ['c_north', b + 'rotations/north.png'],
+ ['c_east', b + 'rotations/east.png'],
+ ['c_west', b + 'rotations/west.png'],
+ ...anim('crun', D4, 6, 'running'),
+ ];
+ for (const [key, path] of entries) {
+ const tex = await Assets.load(path);
+ tex.source.scaleMode = 'nearest';
+ _ct[key] = tex;
+ }
+ _colosseReady = true;
+}
+
+// Assets Éclat
+const _et = {};
+let _eclatReady = false;
+
+export async function loadEclatAssets() {
+ if (_eclatReady) return;
+ const b = '../assets/sprites/eclat/';
+ function anim(prefix, dirs, n, folder) {
+ const out = [];
+ for (const d of dirs) {
+ for (let i = 0; i < n; i++) {
+ const pad = String(i).padStart(3, '0');
+ out.push([`${prefix}_${d}_${i}`, `${b}animations/${folder}/${d}/frame_${pad}.png`]);
+ }
+ }
+ return out;
+ }
+ const D4 = ['south', 'north', 'east', 'west'];
+ const entries = [
+ ['e_south', b + 'rotations/south.png'],
+ ['e_north', b + 'rotations/north.png'],
+ ['e_east', b + 'rotations/east.png'],
+ ['e_west', b + 'rotations/west.png'],
+ ...anim('erun', D4, 8, 'running'),
+ ];
+ for (const [key, path] of entries) {
+ const tex = await Assets.load(path);
+ tex.source.scaleMode = 'nearest';
+ _et[key] = tex;
+ }
+ _eclatReady = true;
+}
+
+// Assets Vexaris
+const _vt = {};
+let _vexarisReady = false;
+
+export async function loadVexarisAssets() {
+ if (_vexarisReady) return;
+ const b = '../assets/sprites/vexaris/';
+ function anim(prefix, dirs, n, folder) {
+ const out = [];
+ for (const d of dirs) {
+ for (let i = 0; i < n; i++) {
+ const pad = String(i).padStart(3, '0');
+ out.push([`${prefix}_${d}_${i}`, `${b}animations/${folder}/${d}/frame_${pad}.png`]);
+ }
+ }
+ return out;
+ }
+ const D4 = ['south', 'north', 'east', 'west'];
+ const entries = [
+ ['v_south', b + 'rotations/south.png'],
+ ['v_north', b + 'rotations/north.png'],
+ ['v_east', b + 'rotations/east.png'],
+ ['v_west', b + 'rotations/west.png'],
+ ...anim('vrun', D4, 4, 'running'),
+ ...anim('vatk', D4, 7, 'attack'),
+ ...anim('vchg', D4, 7, 'charge'),
+ ...anim('vbst', ['south'], 9, 'burst'),
+ ];
+ for (const [key, path] of entries) {
+ const tex = await Assets.load(path);
+ tex.source.scaleMode = 'nearest';
+ _vt[key] = tex;
+ }
+ _vexarisReady = true;
+}
+
+// Animation state par ennemi (position précédente + état animation)
+const _ePrevPos = {}; // id → { x, y }
+const _eAnimState = {}; // id → { facing, frame, timer, action }
+
+const F_RUN_FPS = 8; const F_RUN_TICK = 1 / F_RUN_FPS;
+const R_RUN_FPS = 12; const R_RUN_TICK = 1 / R_RUN_FPS;
+const C_RUN_FPS = 5; const C_RUN_TICK = 1 / C_RUN_FPS; // colosse très lent
+const E_RUN_FPS = 16; const E_RUN_TICK = 1 / E_RUN_FPS; // éclat ultrarapide
+const V_RUN_FPS = 8; const V_RUN_TICK = 1 / V_RUN_FPS; // running 4 frames
+const V_ATK_FPS = 10; const V_ATK_TICK = 1 / V_ATK_FPS; // attack 7 frames
+const V_CHG_FPS = 14; const V_CHG_TICK = 1 / V_CHG_FPS; // charge 7 frames
+const V_BST_FPS = 8; const V_BST_TICK = 1 / V_BST_FPS; // burst 9 frames south
+const TICK_DT = 0.05;
+// iso() = projection monde → pixels isométriques
+// getTh() = pixels par unité monde (pour les tailles proportionnelles)
+
+
+// Couleur de chaque type d'ennemi (valeur hexadécimale RGB)
+const ENEMY_COLORS = {
+ fracture: 0xcc3333, // rouge foncé — ennemi de base qui fonce sur le Soulgate
+ rampant: 0xff6600, // orange — attaque les joueurs
+ colosse: 0x882200, // brun-rouge — lent et résistant
+ eclat: 0xffcc00, // jaune — petit et rapide
+ vexaris: 0xaa22ff, // violet — boss vague 2
+ general: 0xcc6600, // orange foncé — sous-boss invoqué par Morveth
+ morveth: 0x550088, // violet très foncé — boss final vague 3
+};
+
+// Tailles des ennemis selon le type
+// min/max = limites en pixels (pour les petits et grands écrans)
+// scale = facteur multiplicateur de th (pixels par unité monde)
+const ENEMY_SIZES = {
+ morveth: { min: 14, max: 55, scale: 0.65 }, // boss final = très grand
+ vexaris: { min: 12, max: 45, scale: 0.50 }, // boss vague 2 = grand
+ general: { min: 8, max: 30, scale: 0.32 }, // sous-boss = moyen
+};
+const DEFAULT_SIZE = { min: 6, max: 25, scale: 0.22 };
+// Taille par défaut pour les ennemis normaux (fracture, rampant, colosse, eclat)
+
+
+export function updateEnemies(layer, pool, enemies) {
+ // Met à jour les sprites de tous les ennemis depuis les données serveur
+ // layer = Container PixiJS du layer "ennemis"
+ // pool = { id → Container } — sprites existants
+ // enemies = tableau d'EnemyState reçus dans msg.enemies
+
+ // 1. Supprimer les sprites des ennemis morts (plus dans la liste serveur)
+ const ids = new Set(enemies.map(e => e.id));
+ // ids = Set des IDs encore vivants ce tick
+
+ for (const [id, container] of Object.entries(pool)) {
+ if (!ids.has(id)) {
+ layer.removeChild(container);
+ delete pool[id];
+ delete _ePrevPos[id];
+ delete _eAnimState[id];
+ }
+ }
+
+ // 2. Créer ou mettre à jour chaque ennemi
+ for (const e of enemies) {
+ // e = objet ennemi : { id, type, x, y, hp, max_hp, is_boss, frozen }
+
+ if (!pool[e.id]) {
+ pool[e.id] = _createEnemyGraphic(e.type);
+ layer.addChild(pool[e.id]);
+ _ePrevPos[e.id] = { x: e.x, y: e.y };
+ _eAnimState[e.id] = { facing: 'south', frame: 0, timer: 0 };
+ }
+
+ const ctr = pool[e.id];
+ const pos = iso(e.x, e.y);
+ const prev = _ePrevPos[e.id];
+ const anim = _eAnimState[e.id];
+
+ // Détecter direction depuis le déplacement
+ const ddx = e.x - prev.x;
+ const ddy = e.y - prev.y;
+ const moving = Math.abs(ddx) > 0.01 || Math.abs(ddy) > 0.01;
+ if (moving) anim.facing = _enemyDir(ddx, ddy) ?? anim.facing;
+ _ePrevPos[e.id] = { x: e.x, y: e.y };
+
+ ctr.x = pos.x;
+ ctr.y = pos.y;
+
+ // Sprite Fracture
+ if (e.type === 'fracture' && _fractureReady && ctr._sprite) {
+ anim.timer += TICK_DT;
+ if (anim.timer >= F_RUN_TICK) {
+ anim.timer -= F_RUN_TICK;
+ anim.frame = (anim.frame + 1) % 4;
+ }
+ ctr._sprite.texture = moving
+ ? _ft[`frun_${anim.facing}_${anim.frame}`]
+ : _ft[`f_${anim.facing}`];
+ }
+
+ // Sprite Rampant
+ if (e.type === 'rampant' && _rampantReady && ctr._sprite) {
+ anim.timer += TICK_DT;
+ if (anim.timer >= R_RUN_TICK) {
+ anim.timer -= R_RUN_TICK;
+ anim.frame = (anim.frame + 1) % 6;
+ }
+ ctr._sprite.texture = moving
+ ? _rt[`rrun_${anim.facing}_${anim.frame}`]
+ : _rt[`r_${anim.facing}`];
+ }
+
+ // Sprite Colosse
+ if (e.type === 'colosse' && _colosseReady && ctr._sprite) {
+ anim.timer += TICK_DT;
+ if (anim.timer >= C_RUN_TICK) {
+ anim.timer -= C_RUN_TICK;
+ anim.frame = (anim.frame + 1) % 6;
+ }
+ ctr._sprite.texture = moving
+ ? _ct[`crun_${anim.facing}_${anim.frame}`]
+ : _ct[`c_${anim.facing}`];
+ }
+
+ // Sprite Éclat
+ if (e.type === 'eclat' && _eclatReady && ctr._sprite) {
+ anim.timer += TICK_DT;
+ if (anim.timer >= E_RUN_TICK) {
+ anim.timer -= E_RUN_TICK;
+ anim.frame = (anim.frame + 1) % 8;
+ }
+ ctr._sprite.texture = moving
+ ? _et[`erun_${anim.facing}_${anim.frame}`]
+ : _et[`e_${anim.facing}`];
+ }
+
+ // Sprite Vexaris
+ if (e.type === 'vexaris' && _vexarisReady && ctr._sprite) {
+ const prevAction = anim.action ?? 'idle';
+ let action;
+ if (e.is_charging) {
+ action = 'charge';
+ } else if (moving) {
+ action = 'run';
+ } else if (anim.action === 'burst' && anim.frame < 8) {
+ action = 'burst'; // laisser le burst se terminer
+ } else {
+ action = 'attack';
+ }
+ if (action !== prevAction) { anim.frame = 0; anim.timer = 0; }
+ anim.action = action;
+ anim.timer += TICK_DT;
+
+ if (action === 'charge') {
+ if (anim.timer >= V_CHG_TICK) { anim.timer -= V_CHG_TICK; anim.frame = (anim.frame + 1) % 7; }
+ ctr._sprite.texture = _vt[`vchg_${anim.facing}_${anim.frame}`];
+ } else if (action === 'run') {
+ if (anim.timer >= V_RUN_TICK) { anim.timer -= V_RUN_TICK; anim.frame = (anim.frame + 1) % 4; }
+ ctr._sprite.texture = _vt[`vrun_${anim.facing}_${anim.frame}`];
+ } else if (action === 'burst') {
+ if (anim.timer >= V_BST_TICK) { anim.timer -= V_BST_TICK; anim.frame = Math.min(8, anim.frame + 1); }
+ ctr._sprite.texture = _vt[`vbst_south_${anim.frame}`];
+ } else {
+ if (anim.timer >= V_ATK_TICK) { anim.timer -= V_ATK_TICK; anim.frame = (anim.frame + 1) % 7; }
+ ctr._sprite.texture = _vt[`vatk_${anim.facing}_${anim.frame}`];
+ }
+ }
+
+ ctr.tint = e.frozen ? 0x88bbff : 0xffffff;
+ // Si gelé (sort divin Seris) → teinte bleue
+ // Sinon → blanc = pas de teinte (couleur de base)
+ // e.frozen = bool envoyé par le serveur (frozen_timer > 0)
+
+ _updateHealthBar(ctr, e.hp, e.max_hp);
+
+ // Debug : hitbox de collision
+ if (ctr._hboxG) {
+ const hg = ctr._hboxG;
+ hg.clear();
+ if (_debugHitboxes) {
+ const rx = ENEMY_HITBOX_RADIUS * getTw() / 2;
+ const ry = ENEMY_HITBOX_RADIUS * getTh() / 2;
+ hg.ellipse(0, 0, rx, ry).stroke({ color: 0xff4400, width: 1.5, alpha: 0.9 });
+ }
+ }
+ }
+}
+
+
+function _updateHealthBar(ctr, hp, maxHp) {
+ // Met à jour la barre de vie d'un ennemi
+ // ctr = le Container de l'ennemi (contient barFg en tant que propriété custom)
+ // hp = points de vie actuels
+ // maxHp = points de vie max
+
+ const ratio = maxHp > 0 ? Math.max(0, hp / maxHp) : 0;
+ // ratio = pourcentage de vie restante (0.0 à 1.0)
+ // Math.max(0, ...) = éviter les valeurs négatives si hp < 0 (sécurité)
+ // maxHp > 0 ? ... : 0 = éviter la division par zéro
+
+ const barFg = ctr._barFg;
+ // _barFg = la Graphics de la barre de vie (stockée comme propriété custom sur le Container)
+ // Le _ devant = convention "propriété interne"
+
+ if (!barFg) return; // sécurité si le Container n'a pas été initialisé correctement
+
+ barFg.clear(); // effacer le dessin précédent de la barre
+
+ if (ratio > 0) {
+ // Redessiner la barre avec la nouvelle largeur proportionnelle au ratio
+ barFg.rect(ctr._barX, ctr._barY, ctr._barW * ratio, ctr._barH)
+ .fill({ color: 0xff3333 });
+ // ctr._barW * ratio = largeur totale × ratio HP → barre qui rétrécit
+ // 0xff3333 = rouge
+ }
+ // Si ratio = 0 → on ne dessine rien (ennemi presque mort)
+}
+
+
+function _enemyDir(dx, dy) {
+ if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01) return null;
+ if (Math.abs(dx) >= Math.abs(dy)) return dx > 0 ? 'east' : 'west';
+ return dy > 0 ? 'south' : 'north';
+}
+
+
+function _createEnemyGraphic(type) {
+ // Crée le Container PixiJS pour un ennemi (appelé une seule fois par ennemi)
+ // type = string : "fracture", "rampant", "colosse", "eclat", "vexaris", "morveth", "general"
+
+ const th = getTh(); // pixels par unité monde (pour les tailles proportionnelles)
+ const container = new Container();
+
+ // Calculer le rayon selon le type
+ const sz = ENEMY_SIZES[type] ?? DEFAULT_SIZE;
+ // ENEMY_SIZES[type] = taille spécifique si boss/général, sinon DEFAULT_SIZE
+ // ?? = operateur nullish coalescing : si null ou undefined → utiliser DEFAULT_SIZE
+
+ const r = Math.max(sz.min, Math.min(sz.max, th * sz.scale));
+ // th * sz.scale = taille proportionnelle au zoom
+ // Math.max(sz.min, Math.min(sz.max, ...)) = clamp entre min et max
+
+ const color = ENEMY_COLORS[type] ?? 0xff0000;
+
+ // Sprite Rampant
+ // Canvas 48×48, char bbox y=[8..41], anchor y=0.75 → anchor canvas=36
+ // Char top au-dessus de l'anchor = 36-8 = 28 canvas pixels
+ // Scale = (th*1.4)/48 → char top à screen y = -28*(th*1.4/48) = -th*0.817
+ if (type === 'rampant' && _rampantReady) {
+ const shadow = new Graphics();
+ shadow.ellipse(0, r * 0.4, r * 0.5, r * 0.12).fill({ color: 0x000000, alpha: 0.3 });
+ const scale = (th * 1.4) / 48;
+ const sprite = new Sprite(_rt.r_south);
+ sprite.anchor.set(0.5, 0.75);
+ sprite.scale.set(scale);
+
+ const barW = th * 0.7;
+ const barH = Math.max(2, th * 0.035);
+ const barY = -(th * 0.82 + barH + 8); // 8px marge au-dessus de la tête
+ const barBg = new Graphics();
+ barBg.rect(-barW / 2, barY, barW, barH).fill({ color: 0x220000 });
+ const barFg = new Graphics();
+ barFg.rect(-barW / 2, barY, barW, barH).fill({ color: 0xff3333 });
+ const hboxG = new Graphics();
+ container.addChild(shadow, sprite, barBg, barFg, hboxG);
+ container._sprite = sprite;
+ container._barFg = barFg;
+ container._barX = -barW / 2;
+ container._barY = barY;
+ container._barW = barW;
+ container._barH = barH;
+ container._hboxG = hboxG;
+ return container;
+ }
+
+ // Sprite Fracture
+ // Canvas 68×68, char bbox y=[10..58], anchor y=0.75 → anchor canvas=51
+ // Char top au-dessus de l'anchor = 51-10 = 41 canvas pixels
+ // Scale = (th*1.2)/48 → char top à screen y = -41*(th*1.2/48) = -th*1.025
+ if (type === 'fracture' && _fractureReady) {
+ const shadow = new Graphics();
+ shadow.ellipse(0, r * 0.5, r * 0.6, r * 0.15).fill({ color: 0x000000, alpha: 0.3 });
+ const scale = (th * 1.2) / 48;
+ const sprite = new Sprite(_ft.f_south);
+ sprite.anchor.set(0.5, 0.75);
+ sprite.scale.set(scale);
+
+ const barW = th * 0.8;
+ const barH = Math.max(2, th * 0.04);
+ const barY = -(th * 1.025 + barH + 10); // 10px marge au-dessus de la tête
+ const barBg = new Graphics();
+ barBg.rect(-barW / 2, barY, barW, barH).fill({ color: 0x220000 });
+ const barFg = new Graphics();
+ barFg.rect(-barW / 2, barY, barW, barH).fill({ color: 0xff3333 });
+ const hboxG = new Graphics();
+ container.addChild(shadow, sprite, barBg, barFg, hboxG);
+ container._sprite = sprite;
+ container._barFg = barFg;
+ container._barX = -barW / 2;
+ container._barY = barY;
+ container._barW = barW;
+ container._barH = barH;
+ container._hboxG = hboxG;
+ return container;
+ }
+
+ // Sprite Colosse
+ // Canvas 92×92, char top à y=13, anchor y=0.75 → anchor=69, dist=56px
+ // Scale = (th*2.0)/92 → char_top = -56*(th*2.0/92) = -th*1.217
+ if (type === 'colosse' && _colosseReady) {
+ const shadow = new Graphics();
+ shadow.ellipse(0, r * 0.6, r * 0.9, r * 0.22).fill({ color: 0x000000, alpha: 0.35 });
+ const scale = (th * 2.0) / 92;
+ const sprite = new Sprite(_ct.c_south);
+ sprite.anchor.set(0.5, 0.75);
+ sprite.scale.set(scale);
+ const barW = th * 1.2;
+ const barH = Math.max(2, th * 0.05);
+ const barY = -(th * 1.22 + barH + 10);
+ const barBg = new Graphics();
+ barBg.rect(-barW / 2, barY, barW, barH).fill({ color: 0x220000 });
+ const barFg = new Graphics();
+ barFg.rect(-barW / 2, barY, barW, barH).fill({ color: 0xff3333 });
+ const hboxG = new Graphics();
+ container.addChild(shadow, sprite, barBg, barFg, hboxG);
+ container._sprite = sprite;
+ container._barFg = barFg;
+ container._barX = -barW / 2;
+ container._barY = barY;
+ container._barW = barW;
+ container._barH = barH;
+ container._hboxG = hboxG;
+ return container;
+ }
+
+ // Sprite Vexaris
+ // Canvas 92×92, char top à y≈12, anchor y=0.75 → anchor=69, dist=57px
+ // Scale = (th*2.2)/92 → char_top = -57*(th*2.2/92) = -th*1.363
+ if (type === 'vexaris' && _vexarisReady) {
+ const shadow = new Graphics();
+ shadow.ellipse(0, r * 0.7, r * 1.1, r * 0.28).fill({ color: 0x330033, alpha: 0.45 });
+ const scale = (th * 2.2) / 92;
+ const sprite = new Sprite(_vt.v_south);
+ sprite.anchor.set(0.5, 0.75);
+ sprite.scale.set(scale);
+ const barW = th * 1.6;
+ const barH = Math.max(3, th * 0.06);
+ const barY = -(th * 1.36 + barH + 12);
+ const barBg = new Graphics();
+ barBg.rect(-barW / 2, barY, barW, barH).fill({ color: 0x220022 });
+ const barFg = new Graphics();
+ barFg.rect(-barW / 2, barY, barW, barH).fill({ color: 0xaa22ff });
+ const hboxG = new Graphics();
+ container.addChild(shadow, sprite, barBg, barFg, hboxG);
+ container._sprite = sprite;
+ container._barFg = barFg;
+ container._barX = -barW / 2;
+ container._barY = barY;
+ container._barW = barW;
+ container._barH = barH;
+ container._hboxG = hboxG;
+ return container;
+ }
+
+ // Sprite Éclat
+ // Canvas 36×36, char top à y=4, anchor y=0.75 → anchor=27, dist=23px
+ // Scale = (th*1.0)/36 → char_top = -23*(th/36) = -th*0.639
+ if (type === 'eclat' && _eclatReady) {
+ const shadow = new Graphics();
+ shadow.ellipse(0, r * 0.3, r * 0.4, r * 0.10).fill({ color: 0x000000, alpha: 0.25 });
+ const scale = (th * 1.0) / 36;
+ const sprite = new Sprite(_et.e_south);
+ sprite.anchor.set(0.5, 0.75);
+ sprite.scale.set(scale);
+ const barW = th * 0.5;
+ const barH = Math.max(2, th * 0.03);
+ const barY = -(th * 0.64 + barH + 6);
+ const barBg = new Graphics();
+ barBg.rect(-barW / 2, barY, barW, barH).fill({ color: 0x220000 });
+ const barFg = new Graphics();
+ barFg.rect(-barW / 2, barY, barW, barH).fill({ color: 0xff3333 });
+ const hboxG = new Graphics();
+ container.addChild(shadow, sprite, barBg, barFg, hboxG);
+ container._sprite = sprite;
+ container._barFg = barFg;
+ container._barX = -barW / 2;
+ container._barY = barY;
+ container._barW = barW;
+ container._barH = barH;
+ container._hboxG = hboxG;
+ return container;
+ }
+
+ const body = new Graphics();
+
+ // Ombre au sol
+ body.ellipse(0, r * 0.5, r * 0.7, r * 0.18).fill({ color: 0x000000, alpha: 0.25 });
+ // Ellipse aplatie légèrement en dessous du centre (r*0.5) → simuler une ombre
+
+ // Cercle principal de l'ennemi
+ body.circle(0, 0, r).fill({ color });
+ // Contour blanc subtil
+ body.circle(0, 0, r).stroke({ color: 0xffffff, width: 1, alpha: 0.2 });
+
+ // Barre de vie
+ const barW = r * 2.4; // largeur totale de la barre = légèrement plus large que le cercle
+ const barH = Math.max(2, th * 0.04); // hauteur de la barre (au moins 2px)
+ const barY = -(r * 1.1 + barH + 3); // position Y = au-dessus du cercle (négatif = vers le haut)
+
+ const barBg = new Graphics();
+ // Fond de la barre (rouge très foncé = barre vide)
+ barBg.rect(-barW / 2, barY, barW, barH).fill({ color: 0x220000 });
+ // rect(x, y, largeur, hauteur) — centré horizontalement (-barW/2)
+
+ const barFg = new Graphics();
+ // Remplissage de la barre (rouge vif = vie restante)
+ // Dessiné à 100% au départ, puis mis à jour par _updateHealthBar()
+ barFg.rect(-barW / 2, barY, barW, barH).fill({ color: 0xff3333 });
+
+ const hboxG = new Graphics();
+
+ container.addChild(body, barBg, barFg, hboxG);
+ // Ordre d'ajout = ordre d'affichage (barFg par-dessus barBg par-dessus body)
+
+ // Stocker les infos de la barre sur le Container pour les réutiliser dans _updateHealthBar
+ // On les préfixe avec _ pour indiquer que c'est de l'état interne
+ container._barFg = barFg; // référence au Graphics à redessiner
+ container._barX = -barW / 2; // X de départ de la barre
+ container._barY = barY; // Y de la barre
+ container._barW = barW; // largeur TOTALE (× ratio HP = largeur réelle)
+ container._barH = barH; // hauteur de la barre
+ container._hboxG = hboxG; // Graphics de la hitbox debug
+
+ return container;
+}
diff --git a/client/js/renderPlayers.js b/client/js/renderPlayers.js
new file mode 100644
index 0000000..cce9050
--- /dev/null
+++ b/client/js/renderPlayers.js
@@ -0,0 +1,735 @@
+// renderPlayers.js — Rendu des joueurs (sprites Kael ou cercles colorés)
+
+import { Container, Graphics, Text, Assets, Sprite } from 'pixi.js';
+import { CLASS_COLORS, PLAYER_HITBOX_RADIUS, KAEL_MELEE_RADIUS, KAEL_SLAM_RADIUS, KAEL_STORM_RADIUS,
+ DISPLACEMENT_COOLDOWNS } from './constants.js';
+import { iso, getTh, getTw } from './renderer.js';
+
+let _debugAttackRange = false;
+export function setDebugAttackRange(v) { _debugAttackRange = v; }
+
+let _debugHitboxes = false;
+export function setDebugHitboxes(v) { _debugHitboxes = v; }
+
+const BUFF_TINTS = {
+ invulnerable: 0xffcc44,
+ casting: 0x44ffff,
+ flying: 0x88ccff,
+ intangible: 0xcc88ff,
+};
+
+// Assets Kael
+const _kt = {};
+let _kaelReady = false;
+
+// Assets Seris
+const _st = {};
+let _serisReady = false;
+
+// Assets Aldric
+const _at = {};
+let _aldricReady = false;
+
+export async function loadKaelAssets() {
+ if (_kaelReady) return;
+ const b = '../assets/sprites/kael/';
+
+ // Génère les entrées [clé, chemin] pour une animation
+ // prefix = 'run', 'atk', 'dash', 'sk2', 'sk3'
+ // dirs = tableau de directions (ex: ['south','north','east','west'])
+ // n = nombre de frames
+ // folder = dossier dans animations/
+ function anim(prefix, dirs, n, folder) {
+ const out = [];
+ for (const d of dirs) {
+ for (let i = 0; i < n; i++) {
+ const pad = String(i).padStart(3, '0');
+ out.push([`${prefix}_${d}_${i}`, `${b}animations/${folder}/${d}/frame_${pad}.png`]);
+ }
+ }
+ return out;
+ }
+
+ const D4 = ['south', 'north', 'east', 'west'];
+ const D1 = ['south'];
+
+ const entries = [
+ // Rotations statiques (idle)
+ ['south', b + 'rotations/south.png'],
+ ['north', b + 'rotations/north.png'],
+ ['east', b + 'rotations/east.png'],
+ ['west', b + 'rotations/west.png'],
+ // Animations
+ ...anim('run', D4, 4, 'running'), // running 4 dirs × 4 frames
+ ...anim('atk', D4, 5, 'attack'), // attaque 4 dirs × 5 frames
+ ...anim('dash', D4, 5, 'dash'), // dash 4 dirs × 5 frames
+ ...anim('sk2', D1, 9, 'skill2'), // frappe lourde south × 9 frames
+ ...anim('sk3', D1,17, 'skill3'), // tempête south × 17 frames
+ ];
+
+ for (const [key, path] of entries) {
+ const tex = await Assets.load(path);
+ tex.source.scaleMode = 'nearest';
+ _kt[key] = tex;
+ }
+ _kaelReady = true;
+}
+
+export async function loadSerisAssets() {
+ if (_serisReady) return;
+ const b = '../assets/sprites/seris/';
+
+ function anim(prefix, dirs, n, folder) {
+ const out = [];
+ for (const d of dirs) {
+ for (let i = 0; i < n; i++) {
+ const pad = String(i).padStart(3, '0');
+ out.push([`${prefix}_${d}_${i}`, `${b}animations/${folder}/${d}/frame_${pad}.png`]);
+ }
+ }
+ return out;
+ }
+
+ const D4 = ['south', 'north', 'east', 'west'];
+ const D1 = ['south'];
+
+ const entries = [
+ ['s_south', b + 'rotations/south.png'],
+ ['s_north', b + 'rotations/north.png'],
+ ['s_east', b + 'rotations/east.png'],
+ ['s_west', b + 'rotations/west.png'],
+ ...anim('srun', D4, 6, 'running'), // course 4 dirs × 6 frames
+ ...anim('satk', D4, 7, 'attack'), // attaque 4 dirs × 7 frames
+ ...anim('stele', D1, 5, 'teleport'), // téléport south × 5 frames
+ ...anim('ssk1', D1, 9, 'skill1'), // éventail south × 9 frames
+ ...anim('ssk2', D1,13, 'skill2'), // vide south × 13 frames
+ ];
+
+ for (const [key, path] of entries) {
+ const tex = await Assets.load(path);
+ tex.source.scaleMode = 'nearest';
+ _st[key] = tex;
+ }
+ _serisReady = true;
+}
+
+
+export async function loadAldricAssets() {
+ if (_aldricReady) return;
+ const b = '../assets/sprites/aldric/';
+
+ function anim(prefix, dirs, n, folder) {
+ const out = [];
+ for (const d of dirs) {
+ for (let i = 0; i < n; i++) {
+ const pad = String(i).padStart(3, '0');
+ out.push([`${prefix}_${d}_${i}`, `${b}animations/${folder}/${d}/frame_${pad}.png`]);
+ }
+ }
+ return out;
+ }
+
+ const D4 = ['south', 'north', 'east', 'west'];
+ const D1 = ['south'];
+
+ const entries = [
+ ['a_south', b + 'rotations/south.png'],
+ ['a_north', b + 'rotations/north.png'],
+ ['a_east', b + 'rotations/east.png'],
+ ['a_west', b + 'rotations/west.png'],
+ ...anim('awk', D4, 6, 'walking'), // marche 4 dirs × 6 frames
+ ...anim('aatk', D4, 7, 'attack'), // attaque 4 dirs × 7 frames
+ ...anim('afly', D1, 9, 'fly'), // envol south × 9 frames
+ ...anim('ask1', D1, 9, 'skill1'), // halo south × 9 frames
+ ...anim('ask2', D1,13, 'skill2'), // vague south × 13 frames
+ ];
+
+ for (const [key, path] of entries) {
+ const tex = await Assets.load(path);
+ tex.source.scaleMode = 'nearest';
+ _at[key] = tex;
+ }
+ _aldricReady = true;
+}
+
+
+// Constantes d'animation
+const TICK_DT = 0.05; // durée d'un tick serveur (20 Hz)
+
+const RUN_FPS = 8; const RUN_TICK = 1 / RUN_FPS;
+const ATK_FPS = 14; const ATK_TICK = 1 / ATK_FPS; const ATK_FRAMES = 5;
+const DASH_FPS = 14; const DASH_TICK = 1 / DASH_FPS; const DASH_FRAMES = 5;
+const DASH_DUR = DASH_FRAMES / DASH_FPS; // ~0.36s — durée totale du dash visuel
+
+const SK2_FPS = 12; const SK2_TICK = 1 / SK2_FPS; const SK2_FRAMES = 9;
+const SK3_FPS = 10; const SK3_TICK = 1 / SK3_FPS; const SK3_FRAMES = 17;
+
+// Cooldowns max des skills (pour détecter le déclenchement côté client)
+// Si le CD passe de ~0 à une valeur > THRESHOLD, le skill vient d'être utilisé.
+const DISP_CD_MAX = 9.0;
+const SK2_CD_MAX = 7.0; // kael slam (ability_1)
+const SK3_CD_MAX = 12.0; // kael tempête (ability_2)
+
+// Seris animation frame counts + FPS
+const S_RUN_FRAMES = 6;
+const S_ATK_FRAMES = 7;
+const S_TELE_FRAMES = 5;
+const S_SK1_FRAMES = 9;
+const S_SK2_FRAMES = 13;
+const S_RUN_FPS = 10; const S_RUN_TICK = 1 / S_RUN_FPS;
+const S_ATK_FPS = 14; const S_ATK_TICK = 1 / S_ATK_FPS;
+const S_TELE_FPS = 14; const S_TELE_TICK = 1 / S_TELE_FPS;
+const S_SK1_FPS = 12; const S_SK1_TICK = 1 / S_SK1_FPS;
+const S_SK2_FPS = 10; const S_SK2_TICK = 1 / S_SK2_FPS;
+
+// Cooldowns Seris pour détection côté client
+const S_DISP_CD_MAX = 4.0; // téléport
+const S_SK1_CD_MAX = 6.0; // éventail
+const S_SK2_CD_MAX = 10.0; // vide
+
+// Aldric animation frame counts + FPS
+const A_WK_FRAMES = 6;
+const A_ATK_FRAMES = 7;
+const A_FLY_FRAMES = 9;
+const A_SK1_FRAMES = 9;
+const A_SK2_FRAMES = 13;
+const A_WK_FPS = 8; const A_WK_TICK = 1 / A_WK_FPS;
+const A_ATK_FPS = 12; const A_ATK_TICK = 1 / A_ATK_FPS;
+const A_FLY_FPS = 12; const A_FLY_TICK = 1 / A_FLY_FPS;
+const A_SK1_FPS = 10; const A_SK1_TICK = 1 / A_SK1_FPS;
+const A_SK2_FPS = 10; const A_SK2_TICK = 1 / A_SK2_FPS;
+
+// Cooldowns Aldric pour détection côté client
+const A_DISP_CD_MAX = 14.0; // envol
+const A_SK1_CD_MAX = 8.0; // halo
+const A_SK2_CD_MAX = 15.0; // vague
+
+// État d'animation par joueur
+const _prevPos = {}; // position précédente (pour détecter direction et dash)
+const _animState = {}; // état d'animation complet
+const _prevAtkCd = {}; // cooldown attaque au tick précédent
+const _prevDispCd = {}; // cooldown displacement au tick précédent
+const _prevAb2Cd = {}; // cooldown ability_2 au tick précédent
+const _prevAb3Cd = {}; // cooldown ability_3 au tick précédent
+const _dashAnim = {}; // { fromX, fromY, toX, toY, t } — interpolation de position dash
+
+
+// Mise à jour
+
+export function updatePlayers(layer, pool, players) {
+ const ids = new Set(players.map(p => p.id));
+
+ // Supprimer les joueurs déconnectés
+ for (const [id, container] of Object.entries(pool)) {
+ if (!ids.has(id)) {
+ layer.removeChild(container);
+ delete pool[id];
+ delete _prevPos[id]; delete _animState[id];
+ delete _prevAtkCd[id]; delete _prevDispCd[id];
+ delete _prevAb2Cd[id]; delete _prevAb3Cd[id];
+ delete _dashAnim[id];
+ }
+ }
+
+ for (const p of players) {
+ // Création du sprite si nouveau joueur
+ if (!pool[p.id]) {
+ pool[p.id] = _createPlayerGraphic(p);
+ layer.addChild(pool[p.id]);
+ _prevPos[p.id] = { x: p.x, y: p.y };
+ _animState[p.id] = {
+ facing: 'south',
+ frame: 0, timer: 0,
+ attacking: false, atkFrame: 0, atkTimer: 0,
+ dashing: false, dashDir: 'south', dashFrame: 0, dashTimer: 0,
+ sk2ing: false, sk2Frame: 0, sk2Timer: 0,
+ sk3ing: false, sk3Frame: 0, sk3Timer: 0,
+ // Seris
+ steling: false, steleFrame: 0, steleTimer: 0,
+ ssk1ing: false, ssk1Frame: 0, ssk1Timer: 0,
+ ssk2ing: false, ssk2Frame: 0, ssk2Timer: 0,
+ // Aldric
+ aflying: false, aflyFrame: 0, aflyTimer: 0,
+ ask1ing: false, ask1Frame: 0, ask1Timer: 0,
+ ask2ing: false, ask2Frame: 0, ask2Timer: 0,
+ };
+ _prevAtkCd[p.id] = 0;
+ _prevDispCd[p.id] = 0;
+ _prevAb2Cd[p.id] = 0;
+ _prevAb3Cd[p.id] = 0;
+ }
+
+ const prev = _prevPos[p.id];
+ const dx = p.x - prev.x;
+ const dy = p.y - prev.y;
+ const moving = Math.abs(dx) > 0.01 || Math.abs(dy) > 0.01;
+ const anim = _animState[p.id];
+
+ // Détecter les déclenchements d'actions
+ // Attaque de base : attack CD 0 → > 0
+ const curAtkCd = p.cooldowns?.attack ?? 0;
+ if (curAtkCd > 0 && _prevAtkCd[p.id] === 0) {
+ anim.attacking = true; anim.atkFrame = 0; anim.atkTimer = 0;
+ }
+ _prevAtkCd[p.id] = curAtkCd;
+
+ // Displacement (E) — seuil par classe
+ const dispCdMax = DISPLACEMENT_COOLDOWNS[p.class] ?? DISP_CD_MAX;
+ const curDispCd = p.cooldowns?.displacement ?? 0;
+ if (curDispCd > _prevDispCd[p.id] + dispCdMax * 0.5) {
+ if (p.class === 'kael') {
+ anim.dashing = true;
+ anim.dashDir = anim.facing;
+ anim.dashFrame = 0;
+ anim.dashTimer = 0;
+ _dashAnim[p.id] = {
+ fromX: prev.x, fromY: prev.y,
+ toX: p.x, toY: p.y,
+ t: 0,
+ };
+ } else if (p.class === 'seris') {
+ anim.steling = true;
+ anim.steleFrame = 0;
+ anim.steleTimer = 0;
+ } else if (p.class === 'aldric') {
+ anim.aflying = true;
+ anim.aflyFrame = 0;
+ anim.aflyTimer = 0;
+ }
+ }
+ _prevDispCd[p.id] = curDispCd;
+
+ // Skill 1
+ const curAb2Cd = p.cooldowns?.ability_1 ?? 0;
+ if (p.class === 'kael') {
+ if (curAb2Cd > _prevAb2Cd[p.id] + SK2_CD_MAX * 0.5) {
+ anim.sk2ing = true; anim.sk2Frame = 0; anim.sk2Timer = 0;
+ }
+ } else if (p.class === 'seris') {
+ if (curAb2Cd > _prevAb2Cd[p.id] + S_SK1_CD_MAX * 0.5) {
+ anim.ssk1ing = true; anim.ssk1Frame = 0; anim.ssk1Timer = 0;
+ }
+ } else if (p.class === 'aldric') {
+ if (curAb2Cd > _prevAb2Cd[p.id] + A_SK1_CD_MAX * 0.5) {
+ anim.ask1ing = true; anim.ask1Frame = 0; anim.ask1Timer = 0;
+ }
+ }
+ _prevAb2Cd[p.id] = curAb2Cd;
+
+ // Skill 2
+ const curAb3Cd = p.cooldowns?.ability_2 ?? 0;
+ if (p.class === 'kael') {
+ if (curAb3Cd > _prevAb3Cd[p.id] + SK3_CD_MAX * 0.5) {
+ anim.sk3ing = true; anim.sk3Frame = 0; anim.sk3Timer = 0;
+ }
+ } else if (p.class === 'seris') {
+ if (curAb3Cd > _prevAb3Cd[p.id] + S_SK2_CD_MAX * 0.5) {
+ anim.ssk2ing = true; anim.ssk2Frame = 0; anim.ssk2Timer = 0;
+ }
+ } else if (p.class === 'aldric') {
+ if (curAb3Cd > _prevAb3Cd[p.id] + A_SK2_CD_MAX * 0.5) {
+ anim.ask2ing = true; anim.ask2Frame = 0; anim.ask2Timer = 0;
+ }
+ }
+ _prevAb3Cd[p.id] = curAb3Cd;
+
+ // Mise à jour direction depuis le mouvement
+ if (moving && !anim.dashing) {
+ anim.facing = _detectDir(dx, dy) || anim.facing;
+ }
+
+ // Calcul de la position à afficher
+ // Pendant un dash : interpoler linéairement de fromPos à toPos
+ let renderX = p.x, renderY = p.y;
+ const da = _dashAnim[p.id];
+ if (da) {
+ da.t += TICK_DT;
+ if (da.t >= DASH_DUR) {
+ delete _dashAnim[p.id]; // dash terminé
+ } else {
+ const alpha = da.t / DASH_DUR;
+ renderX = da.fromX + (da.toX - da.fromX) * alpha;
+ renderY = da.fromY + (da.toY - da.fromY) * alpha;
+ }
+ }
+ const pos = iso(renderX, renderY);
+
+ // Choisir la texture
+ if (p.class === 'kael' && _kaelReady && pool[p.id]._sprite) {
+ _updateKaelSprite(pool[p.id]._sprite, anim, moving);
+ } else if (p.class === 'seris' && _serisReady && pool[p.id]._sprite) {
+ _updateSerisSprite(pool[p.id]._sprite, anim, moving);
+ } else if (p.class === 'aldric' && _aldricReady && pool[p.id]._sprite) {
+ _updateAldricSprite(pool[p.id]._sprite, anim, moving);
+ }
+
+ // Debug : zones d'attaque
+ if (p.class === 'kael' && pool[p.id]._rangeGraphic) {
+ const rg = pool[p.id]._rangeGraphic;
+ rg.clear();
+ if (_debugAttackRange) {
+ const tw = getTw();
+ const th2 = getTh();
+ if (anim.attacking) {
+ // Attaque de base — demi-cercle (on affiche un cercle complet pour simplifier)
+ const rx = KAEL_MELEE_RADIUS * tw / 2;
+ const ry = KAEL_MELEE_RADIUS * th2 / 2;
+ rg.ellipse(0, 0, rx, ry).fill({ color: 0xff4444, alpha: 0.18 });
+ rg.ellipse(0, 0, rx, ry).stroke({ color: 0xff6666, width: 1.5, alpha: 0.7 });
+ }
+ if (anim.sk2ing) {
+ // Skill 2 — slam cercle complet (orange)
+ const rx = KAEL_SLAM_RADIUS * tw / 2;
+ const ry = KAEL_SLAM_RADIUS * th2 / 2;
+ rg.ellipse(0, 0, rx, ry).fill({ color: 0xff8800, alpha: 0.18 });
+ rg.ellipse(0, 0, rx, ry).stroke({ color: 0xffaa44, width: 1.5, alpha: 0.8 });
+ }
+ if (anim.sk3ing) {
+ // Skill 3 — tempête cercle large (violet)
+ const rx = KAEL_STORM_RADIUS * tw / 2;
+ const ry = KAEL_STORM_RADIUS * th2 / 2;
+ rg.ellipse(0, 0, rx, ry).fill({ color: 0xaa44ff, alpha: 0.15 });
+ rg.ellipse(0, 0, rx, ry).stroke({ color: 0xcc88ff, width: 2, alpha: 0.8 });
+ }
+ }
+ }
+
+ // Debug : hitbox de collision du joueur
+ if (pool[p.id]._hboxG) {
+ const hg = pool[p.id]._hboxG;
+ hg.clear();
+ if (_debugHitboxes) {
+ const rx = PLAYER_HITBOX_RADIUS * getTw() / 2;
+ const ry = PLAYER_HITBOX_RADIUS * getTh() / 2;
+ hg.ellipse(0, 0, rx, ry).stroke({ color: 0x00ffff, width: 1.5, alpha: 0.9 });
+ }
+ }
+
+ _prevPos[p.id] = { x: p.x, y: p.y };
+
+ // Teinte selon buff
+ let tint = 0xffffff;
+ let alpha = 1.0;
+ for (const b of (p.buffs ?? [])) {
+ if (BUFF_TINTS[b.type]) { tint = BUFF_TINTS[b.type]; break; }
+ }
+ if (p.buffs?.some(b => b.type === 'intangible')) alpha = 0.5;
+
+ pool[p.id].x = pos.x;
+ pool[p.id].y = pos.y;
+ pool[p.id].tint = tint;
+ pool[p.id].alpha = p.alive ? alpha : 0.2;
+ }
+}
+
+
+// Logique de sprite Kael
+
+function _updateKaelSprite(sprite, anim, moving) {
+ // Priorité : dash > tempête (sk3) > frappe lourde (sk2) > attaque > run > idle
+
+ if (anim.dashing) {
+ anim.dashTimer += TICK_DT;
+ if (anim.dashTimer >= DASH_TICK) {
+ anim.dashTimer -= DASH_TICK;
+ anim.dashFrame++;
+ }
+ if (anim.dashFrame >= DASH_FRAMES) {
+ anim.dashing = false;
+ } else {
+ sprite.texture = _kt[`dash_${anim.dashDir}_${anim.dashFrame}`];
+ return;
+ }
+ }
+
+ if (anim.sk3ing) {
+ anim.sk3Timer += TICK_DT;
+ if (anim.sk3Timer >= SK3_TICK) {
+ anim.sk3Timer -= SK3_TICK;
+ anim.sk3Frame++;
+ }
+ if (anim.sk3Frame >= SK3_FRAMES) {
+ anim.sk3ing = false;
+ } else {
+ sprite.texture = _kt[`sk3_south_${anim.sk3Frame}`];
+ return;
+ }
+ }
+
+ if (anim.sk2ing) {
+ anim.sk2Timer += TICK_DT;
+ if (anim.sk2Timer >= SK2_TICK) {
+ anim.sk2Timer -= SK2_TICK;
+ anim.sk2Frame++;
+ }
+ if (anim.sk2Frame >= SK2_FRAMES) {
+ anim.sk2ing = false;
+ } else {
+ sprite.texture = _kt[`sk2_south_${anim.sk2Frame}`];
+ return;
+ }
+ }
+
+ if (anim.attacking) {
+ anim.atkTimer += TICK_DT;
+ if (anim.atkTimer >= ATK_TICK) {
+ anim.atkTimer -= ATK_TICK;
+ anim.atkFrame++;
+ }
+ if (anim.atkFrame >= ATK_FRAMES) {
+ anim.attacking = false;
+ } else {
+ sprite.texture = _kt[`atk_${anim.facing}_${anim.atkFrame}`];
+ return;
+ }
+ }
+
+ if (moving) {
+ anim.timer += TICK_DT;
+ if (anim.timer >= RUN_TICK) {
+ anim.timer -= RUN_TICK;
+ anim.frame = (anim.frame + 1) % 4;
+ }
+ sprite.texture = _kt[`run_${anim.facing}_${anim.frame}`];
+ } else {
+ anim.frame = 0;
+ anim.timer = 0;
+ sprite.texture = _kt[anim.facing];
+ }
+}
+
+
+// Logique de sprite Seris
+
+function _updateSerisSprite(sprite, anim, moving) {
+ // Priorité : téléport > vide (sk2) > éventail (sk1) > attaque > run > idle
+
+ if (anim.steling) {
+ anim.steleTimer += TICK_DT;
+ if (anim.steleTimer >= S_TELE_TICK) {
+ anim.steleTimer -= S_TELE_TICK;
+ anim.steleFrame++;
+ }
+ if (anim.steleFrame >= S_TELE_FRAMES) {
+ anim.steling = false;
+ } else {
+ sprite.texture = _st[`stele_south_${anim.steleFrame}`];
+ return;
+ }
+ }
+
+ if (anim.ssk2ing) {
+ anim.ssk2Timer += TICK_DT;
+ if (anim.ssk2Timer >= S_SK2_TICK) {
+ anim.ssk2Timer -= S_SK2_TICK;
+ anim.ssk2Frame++;
+ }
+ if (anim.ssk2Frame >= S_SK2_FRAMES) {
+ anim.ssk2ing = false;
+ } else {
+ sprite.texture = _st[`ssk2_south_${anim.ssk2Frame}`];
+ return;
+ }
+ }
+
+ if (anim.ssk1ing) {
+ anim.ssk1Timer += TICK_DT;
+ if (anim.ssk1Timer >= S_SK1_TICK) {
+ anim.ssk1Timer -= S_SK1_TICK;
+ anim.ssk1Frame++;
+ }
+ if (anim.ssk1Frame >= S_SK1_FRAMES) {
+ anim.ssk1ing = false;
+ } else {
+ sprite.texture = _st[`ssk1_south_${anim.ssk1Frame}`];
+ return;
+ }
+ }
+
+ if (anim.attacking) {
+ anim.atkTimer += TICK_DT;
+ if (anim.atkTimer >= S_ATK_TICK) {
+ anim.atkTimer -= S_ATK_TICK;
+ anim.atkFrame++;
+ }
+ if (anim.atkFrame >= S_ATK_FRAMES) {
+ anim.attacking = false;
+ } else {
+ sprite.texture = _st[`satk_${anim.facing}_${anim.atkFrame}`];
+ return;
+ }
+ }
+
+ if (moving) {
+ anim.timer += TICK_DT;
+ if (anim.timer >= S_RUN_TICK) {
+ anim.timer -= S_RUN_TICK;
+ anim.frame = (anim.frame + 1) % S_RUN_FRAMES;
+ }
+ sprite.texture = _st[`srun_${anim.facing}_${anim.frame}`];
+ } else {
+ anim.frame = 0;
+ anim.timer = 0;
+ sprite.texture = _st[`s_${anim.facing}`];
+ }
+}
+
+
+// Logique de sprite Aldric
+
+function _updateAldricSprite(sprite, anim, moving) {
+ // Priorité : envol > vague (sk2) > halo (sk1) > attaque > marche > idle
+
+ if (anim.aflying) {
+ anim.aflyTimer += TICK_DT;
+ if (anim.aflyTimer >= A_FLY_TICK) {
+ anim.aflyTimer -= A_FLY_TICK;
+ anim.aflyFrame++;
+ }
+ if (anim.aflyFrame >= A_FLY_FRAMES) {
+ anim.aflying = false;
+ } else {
+ sprite.texture = _at[`afly_south_${anim.aflyFrame}`];
+ return;
+ }
+ }
+
+ if (anim.ask2ing) {
+ anim.ask2Timer += TICK_DT;
+ if (anim.ask2Timer >= A_SK2_TICK) {
+ anim.ask2Timer -= A_SK2_TICK;
+ anim.ask2Frame++;
+ }
+ if (anim.ask2Frame >= A_SK2_FRAMES) {
+ anim.ask2ing = false;
+ } else {
+ sprite.texture = _at[`ask2_south_${anim.ask2Frame}`];
+ return;
+ }
+ }
+
+ if (anim.ask1ing) {
+ anim.ask1Timer += TICK_DT;
+ if (anim.ask1Timer >= A_SK1_TICK) {
+ anim.ask1Timer -= A_SK1_TICK;
+ anim.ask1Frame++;
+ }
+ if (anim.ask1Frame >= A_SK1_FRAMES) {
+ anim.ask1ing = false;
+ } else {
+ sprite.texture = _at[`ask1_south_${anim.ask1Frame}`];
+ return;
+ }
+ }
+
+ if (anim.attacking) {
+ anim.atkTimer += TICK_DT;
+ if (anim.atkTimer >= A_ATK_TICK) {
+ anim.atkTimer -= A_ATK_TICK;
+ anim.atkFrame++;
+ }
+ if (anim.atkFrame >= A_ATK_FRAMES) {
+ anim.attacking = false;
+ } else {
+ sprite.texture = _at[`aatk_${anim.facing}_${anim.atkFrame}`];
+ return;
+ }
+ }
+
+ if (moving) {
+ anim.timer += TICK_DT;
+ if (anim.timer >= A_WK_TICK) {
+ anim.timer -= A_WK_TICK;
+ anim.frame = (anim.frame + 1) % A_WK_FRAMES;
+ }
+ sprite.texture = _at[`awk_${anim.facing}_${anim.frame}`];
+ } else {
+ anim.frame = 0;
+ anim.timer = 0;
+ sprite.texture = _at[`a_${anim.facing}`];
+ }
+}
+
+
+// Helpers
+
+// Renvoie la direction dominante ('south'|'north'|'east'|'west') ou null si stationnaire
+function _detectDir(dx, dy) {
+ if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01) return null;
+ if (Math.abs(dx) >= Math.abs(dy)) return dx > 0 ? 'east' : 'west';
+ return dy > 0 ? 'south' : 'north';
+}
+
+
+// Création du sprite
+
+function _createPlayerGraphic(player) {
+ const th = getTh();
+ const container = new Container();
+ const r = Math.max(10, Math.min(40, th * 0.4));
+
+ const hboxG = new Graphics();
+ container._hboxG = hboxG;
+
+ if (player.class === 'kael' && _kaelReady) {
+ const shadow = new Graphics();
+ shadow.ellipse(0, r * 0.3, r * 0.5, r * 0.13).fill({ color: 0x000000, alpha: 0.3 });
+
+ const scale = (th * 1.2) / 48;
+ const sprite = new Sprite(_kt.south);
+ sprite.anchor.set(0.5, 0.75);
+ sprite.scale.set(scale);
+ container._sprite = sprite;
+
+ const rangeG = new Graphics();
+ container._rangeGraphic = rangeG;
+
+ container.addChild(rangeG, shadow, sprite, hboxG);
+
+ } else if (player.class === 'seris' && _serisReady) {
+ const shadow = new Graphics();
+ shadow.ellipse(0, r * 0.3, r * 0.5, r * 0.13).fill({ color: 0x000000, alpha: 0.3 });
+
+ const scale = (th * 1.2) / 48;
+ const sprite = new Sprite(_st.s_south);
+ sprite.anchor.set(0.5, 0.75);
+ sprite.scale.set(scale);
+ container._sprite = sprite;
+
+ container.addChild(shadow, sprite, hboxG);
+
+ } else if (player.class === 'aldric' && _aldricReady) {
+ const shadow = new Graphics();
+ shadow.ellipse(0, r * 0.3, r * 0.5, r * 0.13).fill({ color: 0x000000, alpha: 0.3 });
+
+ const scale = (th * 1.2) / 48;
+ const sprite = new Sprite(_at.a_south);
+ sprite.anchor.set(0.5, 0.75);
+ sprite.scale.set(scale);
+ container._sprite = sprite;
+
+ container.addChild(shadow, sprite, hboxG);
+
+ } else {
+ const color = CLASS_COLORS[player.class] ?? 0xffffff;
+ const body = new Graphics();
+ body.ellipse(0, r * 0.5, r * 0.6, r * 0.15).fill({ color: 0x000000, alpha: 0.25 });
+ body.circle(0, 0, r).fill({ color });
+ body.circle(0, 0, r).stroke({ color: 0xffffff, width: 1.5, alpha: 0.3 });
+
+ const label = new Text({
+ text: player.username,
+ style: {
+ fontSize: Math.max(9, Math.min(14, th * 0.15)),
+ fill: 0xdddddd,
+ fontFamily: 'Courier New',
+ },
+ });
+ label.anchor.set(0.5, 1);
+ label.y = -(r * 1.05 + 8);
+
+ container.addChild(body, label, hboxG);
+ }
+
+ return container;
+}
diff --git a/client/js/renderer.js b/client/js/renderer.js
new file mode 100644
index 0000000..254b5a5
--- /dev/null
+++ b/client/js/renderer.js
@@ -0,0 +1,184 @@
+// renderer.js : projection iso (world -> screen et inverse), camera, projectiles + AOE
+// formule : sx = (wx - wy) * tw/2 / sy = (wx + wy) * th/2
+
+import { Graphics } from 'pixi.js';
+
+import {
+ ARENA_WIDTH, ARENA_HEIGHT,
+ TILE_WIDTH, TILE_HEIGHT,
+ PROJECTILE_HIT_RADIUS, SOULGATE_HITBOX_RADIUS,
+ SOULGATE_X, SOULGATE_Y,
+} from './constants.js';
+
+let _debugHitboxes = false;
+export function setDebugHitboxes(v) { _debugHitboxes = v; }
+
+// Re-exports : ces fonctions viennent d'autres fichiers mais sont exposées ici
+// → main.js peut tout importer depuis renderer.js en un seul endroit
+export { drawStaticArena } from './renderArena.js'; // dessine l'arène statique (sol, Soulgate, etc.)
+export { updateEnemies } from './renderEnemies.js'; // met à jour les sprites ennemis
+export { updatePlayers } from './renderPlayers.js'; // met à jour les sprites joueurs
+
+
+// Zoom / Layout
+
+// Nombre d'unités monde visibles en diagonale iso — détermine le niveau de zoom
+// Plus c'est grand → plus on voit loin → plus les objets sont petits
+const CAMERA_VIEWPORT_DIAG = 22;
+
+// tw / th = pixels par "unité monde" dans les axes X et Y isométriques
+// recalcules au resize
+let tw, th;
+
+export function getTh() { return th; }
+export function getTw() { return tw; }
+
+
+export function updateIsoLayout(screenW, screenH) {
+ const bW = CAMERA_VIEWPORT_DIAG * TILE_WIDTH / 2;
+ const bH = CAMERA_VIEWPORT_DIAG * TILE_HEIGHT / 2;
+ tw = TILE_WIDTH * (screenW / bW);
+ th = TILE_HEIGHT * (screenH / bH);
+}
+
+
+// projection iso : (wx - wy) * tw/2 / (wx + wy) * th/2
+export function iso(wx, wy) {
+ return {
+ x: (wx - wy) * tw / 2,
+ y: (wx + wy) * th / 2,
+ };
+}
+
+
+export function screenToWorld(screenX, screenY, containerX, containerY) {
+ // inverse de iso, sert au clic souris
+ const sx = screenX - containerX;
+ const sy = screenY - containerY;
+ return {
+ wx: sx / tw + sy / th,
+ wy: sy / th - sx / tw,
+ };
+}
+
+
+export function updateCamera(worldContainer, screenW, screenH, wx, wy) {
+ const HW = ARENA_WIDTH / 2;
+ const HH = ARENA_HEIGHT / 2;
+ // clamp pour pas voir du vide au bord
+ const camX = Math.max(-HW, Math.min(HW, wx));
+ const camY = Math.max(-HH, Math.min(HH, wy));
+
+ const p = iso(camX, camY);
+ worldContainer.x = screenW / 2 - p.x;
+ worldContainer.y = screenH / 2 - p.y;
+}
+
+
+export function updateProjectiles(layer, pool, projectiles) {
+ const ids = new Set(projectiles.map(p => p.id));
+ for (const [id, g] of Object.entries(pool)) {
+ if (!ids.has(id)) {
+ layer.removeChild(g);
+ delete pool[id];
+ }
+ }
+
+ for (const p of projectiles) {
+ if (!pool[p.id]) {
+ pool[p.id] = _createProjectileGraphic();
+ layer.addChild(pool[p.id]);
+ }
+ const pos = iso(p.x, p.y);
+ pool[p.id].x = pos.x;
+ pool[p.id].y = pos.y;
+
+ // Debug : hitbox du projectile
+ if (_debugHitboxes && pool[p.id]._hboxG) {
+ const hg = pool[p.id]._hboxG;
+ hg.clear();
+ const rx = PROJECTILE_HIT_RADIUS * tw / 2;
+ const ry = PROJECTILE_HIT_RADIUS * th / 2;
+ hg.ellipse(0, 0, rx, ry).stroke({ color: 0xffff00, width: 1, alpha: 0.8 });
+ } else if (!_debugHitboxes && pool[p.id]._hboxG) {
+ pool[p.id]._hboxG.clear();
+ }
+ }
+}
+
+
+function _createProjectileGraphic() {
+ const g = new Graphics();
+ const r = Math.max(3, th * 0.08);
+ g.circle(0, 0, r).fill({ color: 0xffee44 });
+ g.circle(0, 0, r).stroke({ color: 0xffffff, width: 1, alpha: 0.3 });
+
+ // sous-graphics pour la hitbox debug
+ const hboxG = new Graphics();
+ g.addChild(hboxG);
+ g._hboxG = hboxG;
+
+ return g;
+}
+
+
+// AOE
+
+const _AOE_STYLE = {
+ slam: { fill: 0xff6600, stroke: 0xffaa44 },
+ storm: { fill: 0x8800ff, stroke: 0xcc88ff },
+ void: { fill: 0x00ccff, stroke: 0x88eeff },
+ pulse: { fill: 0xffd700, stroke: 0xfff0a0 },
+};
+
+export function updateAoeZones(layer, pool, zones) {
+ // Supprimer les zones disparues
+ const ids = new Set(zones.map(z => z.id));
+ for (const [id, g] of Object.entries(pool)) {
+ if (!ids.has(id)) { layer.removeChild(g); delete pool[id]; }
+ }
+
+ for (const z of zones) {
+ if (!pool[z.id]) {
+ pool[z.id] = new Graphics();
+ layer.addChild(pool[z.id]);
+ }
+ const g = pool[z.id];
+ const pos = iso(z.x, z.y);
+ g.x = pos.x;
+ g.y = pos.y;
+
+ const progress = z.ticks / z.max_ticks; // 1.0 → 0.0 au fil du temps
+ const pal = _AOE_STYLE[z.type] ?? _AOE_STYLE.slam;
+ const rx = z.radius * tw / 2;
+ const ry = z.radius * th / 2;
+
+ g.clear();
+ g.ellipse(0, 0, rx, ry).fill({ color: pal.fill, alpha: 0.18 * progress });
+ g.ellipse(0, 0, rx, ry).stroke({ color: pal.stroke, width: 2, alpha: 0.85 * progress });
+ }
+}
+
+
+// hitbox debug Soulgate (plus utilise mais on garde le graphics au cas ou)
+
+let _sgHboxG = null;
+
+export function initSoulgateHitbox(layer) {
+ if (_sgHboxG) { layer.removeChild(_sgHboxG); }
+ _sgHboxG = new Graphics();
+ layer.addChild(_sgHboxG);
+}
+
+export function updateSoulgateHitbox() {
+ if (!_sgHboxG) return;
+ _sgHboxG.clear();
+ if (_debugHitboxes) {
+ const pos = iso(SOULGATE_X, SOULGATE_Y);
+ _sgHboxG.x = pos.x;
+ _sgHboxG.y = pos.y;
+ const rx = SOULGATE_HITBOX_RADIUS * tw / 2;
+ const ry = SOULGATE_HITBOX_RADIUS * th / 2;
+ _sgHboxG.ellipse(0, 0, rx, ry).stroke({ color: 0x00ff88, width: 2, alpha: 0.9 });
+ }
+}
diff --git a/client/js/tiledLoader.js b/client/js/tiledLoader.js
new file mode 100644
index 0000000..78cd4cc
--- /dev/null
+++ b/client/js/tiledLoader.js
@@ -0,0 +1,95 @@
+// tiledLoader.js : charge une map Tiled (.tmj + .tsx) et la dessine en iso
+
+import { Sprite, Assets } from 'pixi.js';
+import { iso, getTw, getTh } from './renderer.js';
+
+
+export async function loadTiledMap(mapPath) {
+ // baseUrl = dossier du .tmj (les .tsx sont a cote)
+ const baseUrl = mapPath.substring(0, mapPath.lastIndexOf('/') + 1);
+ const map = await fetch(mapPath).then(r => r.json());
+
+ // parse les .tsx (XML) referencements (firstgid + chemin image par tile id)
+ const tilesets = [];
+ for (const ts of map.tilesets) {
+ const tsxText = await fetch(baseUrl + ts.source).then(r => r.text());
+ const xml = new DOMParser().parseFromString(tsxText, 'text/xml');
+ const tiles = {};
+ for (const tile of xml.querySelectorAll('tile')) {
+ const id = parseInt(tile.getAttribute('id'));
+ const img = tile.querySelector('image');
+ if (img) {
+ tiles[id] = {
+ src: baseUrl + img.getAttribute('source'),
+ w: parseInt(img.getAttribute('width')),
+ h: parseInt(img.getAttribute('height')),
+ };
+ }
+ }
+ tilesets.push({ firstgid: ts.firstgid, tiles });
+ }
+
+ // precharger toutes les textures
+ const urls = [];
+ for (const ts of tilesets) {
+ for (const id in ts.tiles) urls.push(ts.tiles[id].src);
+ }
+ await Assets.load(urls);
+
+ return { map, tilesets };
+}
+
+
+function _gidToTile(gid, tilesets) {
+ // chercher dans le bon tileset (firstgid descendant)
+ for (let i = tilesets.length - 1; i >= 0; i--) {
+ if (gid >= tilesets[i].firstgid) {
+ return tilesets[i].tiles[gid - tilesets[i].firstgid] || null;
+ }
+ }
+ return null;
+}
+
+
+export function drawTiledMap(layer, data) {
+ const { map, tilesets } = data;
+ const W = map.width;
+ const H = map.height;
+ const tw = getTw();
+ const th = getTh();
+
+ // reference : la base (footprint iso) d'un sprite Kenney fait 256x128 px
+ // au tw courant, on veut que cette base remplisse 1 unite monde (tw x th px)
+ const REF_TILE_W = 256;
+ const scale = tw / REF_TILE_W;
+
+ for (const tlayer of map.layers) {
+ if (tlayer.type !== 'tilelayer') continue;
+ if (tlayer.visible === false) continue;
+
+ for (let i = 0; i < tlayer.data.length; i++) {
+ const gid = tlayer.data[i];
+ if (gid === 0) continue; // 0 = case vide
+
+ const tile = _gidToTile(gid, tilesets);
+ if (!tile) continue;
+
+ const col = i % W;
+ const row = Math.floor(i / W);
+ // centrer la map sur (0, 0)
+ const wx = col - (W - 1) / 2;
+ const wy = row - (H - 1) / 2;
+
+ const tex = Assets.get(tile.src);
+ if (!tex) continue;
+
+ const sprite = new Sprite(tex);
+ sprite.anchor.set(0.5, 1.0); // base centree sur la tile, le sprite "monte"
+ const p = iso(wx, wy);
+ sprite.x = p.x;
+ sprite.y = p.y + th / 2; // decalage half pour que la pointe basse de la diamond touche la base
+ sprite.scale.set(scale);
+ layer.addChild(sprite);
+ }
+ }
+}
diff --git a/server/__pycache__/boss_ai.cpython-313.pyc b/server/__pycache__/boss_ai.cpython-313.pyc
new file mode 100644
index 0000000..3fba631
Binary files /dev/null and b/server/__pycache__/boss_ai.cpython-313.pyc differ
diff --git a/server/__pycache__/buff_system.cpython-313.pyc b/server/__pycache__/buff_system.cpython-313.pyc
new file mode 100644
index 0000000..6cb05a5
Binary files /dev/null and b/server/__pycache__/buff_system.cpython-313.pyc differ
diff --git a/server/__pycache__/combat.cpython-313.pyc b/server/__pycache__/combat.cpython-313.pyc
new file mode 100644
index 0000000..973b4a7
Binary files /dev/null and b/server/__pycache__/combat.cpython-313.pyc differ
diff --git a/server/__pycache__/constants.cpython-313.pyc b/server/__pycache__/constants.cpython-313.pyc
new file mode 100644
index 0000000..05d1a84
Binary files /dev/null and b/server/__pycache__/constants.cpython-313.pyc differ
diff --git a/server/__pycache__/displacement.cpython-313.pyc b/server/__pycache__/displacement.cpython-313.pyc
new file mode 100644
index 0000000..df51310
Binary files /dev/null and b/server/__pycache__/displacement.cpython-313.pyc differ
diff --git a/server/__pycache__/game_loop.cpython-313.pyc b/server/__pycache__/game_loop.cpython-313.pyc
new file mode 100644
index 0000000..03244ca
Binary files /dev/null and b/server/__pycache__/game_loop.cpython-313.pyc differ
diff --git a/server/__pycache__/game_state.cpython-313.pyc b/server/__pycache__/game_state.cpython-313.pyc
new file mode 100644
index 0000000..e245f42
Binary files /dev/null and b/server/__pycache__/game_state.cpython-313.pyc differ
diff --git a/server/__pycache__/lobby.cpython-313.pyc b/server/__pycache__/lobby.cpython-313.pyc
new file mode 100644
index 0000000..24064b7
Binary files /dev/null and b/server/__pycache__/lobby.cpython-313.pyc differ
diff --git a/server/__pycache__/main.cpython-313.pyc b/server/__pycache__/main.cpython-313.pyc
new file mode 100644
index 0000000..0e3f582
Binary files /dev/null and b/server/__pycache__/main.cpython-313.pyc differ
diff --git a/server/__pycache__/stats.cpython-313.pyc b/server/__pycache__/stats.cpython-313.pyc
new file mode 100644
index 0000000..1ffd864
Binary files /dev/null and b/server/__pycache__/stats.cpython-313.pyc differ
diff --git a/server/__pycache__/waves.cpython-313.pyc b/server/__pycache__/waves.cpython-313.pyc
new file mode 100644
index 0000000..8356db0
Binary files /dev/null and b/server/__pycache__/waves.cpython-313.pyc differ
diff --git a/server/__pycache__/websocket.cpython-313.pyc b/server/__pycache__/websocket.cpython-313.pyc
new file mode 100644
index 0000000..cf59411
Binary files /dev/null and b/server/__pycache__/websocket.cpython-313.pyc differ
diff --git a/server/boss_ai.py b/server/boss_ai.py
new file mode 100644
index 0000000..a33405e
--- /dev/null
+++ b/server/boss_ai.py
@@ -0,0 +1,254 @@
+# boss_ai.py : IA des boss Vexaris et Morveth
+# l'etat interne du boss est stocke dans enemy.data (pas broadcast au client)
+
+import logging
+
+from buff_system import player_is_immune
+from combat import circle_overlap, normalize
+from constants import (
+ ARENA_HEIGHT, ARENA_WIDTH,
+ ENEMY_ATTACK_COOLDOWN,
+ MORVETH_BURST_COOLDOWN, MORVETH_BURST_COUNT,
+ MORVETH_CHARGE_COOLDOWN, MORVETH_CHARGE_DURATION, MORVETH_CHARGE_SPEED,
+ MORVETH_HITBOX_RADIUS, MORVETH_MELEE_DAMAGE, MORVETH_SG_DAMAGE,
+ MORVETH_SPEED_P1, MORVETH_SPEED_P2, MORVETH_SPEED_P3,
+ PLAYER_HITBOX_RADIUS,
+ SOULGATE_HITBOX_RADIUS, SOULGATE_X, SOULGATE_Y,
+ TICK_DURATION,
+ VEXARIS_BURST_COOLDOWN, VEXARIS_BURST_COUNT,
+ VEXARIS_CHARGE_COOLDOWN, VEXARIS_CHARGE_DURATION, VEXARIS_CHARGE_SPEED,
+ VEXARIS_HITBOX_RADIUS, VEXARIS_MELEE_DAMAGE, VEXARIS_SG_DAMAGE,
+ VEXARIS_SPEED_P1, VEXARIS_SPEED_P2,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def nearest_player(ex, ey, players):
+ # joueur le plus proche de (ex, ey), None si liste vide
+ best = None
+ best_dist = float("inf")
+ for p in players:
+ d = (p.x - ex) ** 2 + (p.y - ey) ** 2 # carre, evite math.sqrt
+ if d < best_dist:
+ best_dist = d
+ best = p
+ return best
+
+
+def update_boss_ai(enemy, state_players, state_soulgate, pending_spawns):
+ """dispatch vers la bonne IA selon le type de boss.
+
+ pending_spawns = liste de (type_ennemi, x, y) à créer.
+ On n'appelle pas spawn_enemy() directement depuis ici pour éviter de modifier
+ la liste d'ennemis pendant qu'on l'itère dans game_loop.
+ """
+ alive = [p for p in state_players if p.alive] # uniquement les joueurs vivants
+
+ if enemy.type == "morveth":
+ _update_morveth(enemy, alive, state_soulgate, pending_spawns)
+ else:
+ # Vexaris ou Général (les généraux ont une IA simplifiée du même type)
+ _update_vexaris(enemy, alive, state_soulgate, pending_spawns)
+
+
+# Vexaris
+
+def _update_vexaris(enemy, alive_players, soulgate, pending_spawns):
+ # 2 phases (>50% / <=50%), charge sur joueur, burst d'eclats en phase 2
+ phase2 = enemy.hp <= enemy.max_hp // 2
+ speed = VEXARIS_SPEED_P2 if phase2 else VEXARIS_SPEED_P1
+ d = enemy.data
+
+ _init_vexaris_data(d)
+ _tick_boss_cooldowns(enemy, d, phase2, VEXARIS_BURST_COOLDOWN)
+ _check_soulgate_contact(enemy, soulgate, VEXARIS_HITBOX_RADIUS, VEXARIS_SG_DAMAGE)
+ _vexaris_burst(d, enemy, phase2, pending_spawns)
+
+ if d["is_charging"]:
+ _boss_charge_move(enemy, d, alive_players, VEXARIS_CHARGE_SPEED, VEXARIS_HITBOX_RADIUS, VEXARIS_MELEE_DAMAGE * 2)
+ elif d["charge_cd"] <= 0 and alive_players:
+ _start_charge(d, enemy, alive_players, VEXARIS_CHARGE_DURATION, VEXARIS_CHARGE_COOLDOWN, "Vexaris")
+ else:
+ _boss_normal_move(enemy, alive_players, speed, VEXARIS_HITBOX_RADIUS, VEXARIS_MELEE_DAMAGE)
+
+
+def _init_vexaris_data(d):
+ if "charge_cd" in d:
+ return
+ d.update({
+ "charge_cd": VEXARIS_CHARGE_COOLDOWN,
+ "charge_timer": 0.0,
+ "charge_tx": 0.0, "charge_ty": 0.0,
+ "is_charging": False,
+ "burst_cd": VEXARIS_BURST_COOLDOWN,
+ })
+
+
+def _vexaris_burst(d, enemy, phase2, pending):
+ """Spawn un burst d'éclats autour de Vexaris (seulement en phase 2, si CD = 0)."""
+ if not phase2 or d["burst_cd"] > 0:
+ return # pas en phase 2, ou burst en cooldown → on ne fait rien
+
+ # Spawner VEXARIS_BURST_COUNT éclats à la position du boss
+ for _ in range(VEXARIS_BURST_COUNT):
+ pending.append(("eclat", enemy.x, enemy.y)) # ("type", x, y) — game_loop les crée après
+
+ d["burst_cd"] = VEXARIS_BURST_COOLDOWN # réinitialiser le cooldown du burst
+ logger.debug("Vexaris burst : %d eclats", VEXARIS_BURST_COUNT)
+
+
+# Morveth
+
+def _update_morveth(enemy, alive_players, soulgate, pending_spawns):
+ # 3 phases : <75% (p2), <25% (p3), invoque des generaux a 75/50/25
+ hp_pct = enemy.hp / enemy.max_hp
+ phase3 = hp_pct <= 0.25
+ phase2 = hp_pct <= 0.75
+
+ speed = MORVETH_SPEED_P3 if phase3 else (MORVETH_SPEED_P2 if phase2 else MORVETH_SPEED_P1)
+ d = enemy.data
+
+ _init_morveth_data(d)
+ _tick_boss_cooldowns(enemy, d, phase2, MORVETH_BURST_COOLDOWN)
+ _morveth_spawn_generals(d, hp_pct, enemy, pending_spawns) # invoquer généraux aux seuils HP
+ _check_soulgate_contact(enemy, soulgate, MORVETH_HITBOX_RADIUS, MORVETH_SG_DAMAGE)
+ _morveth_burst(d, enemy, phase2, phase3, pending_spawns)
+
+ # En phase 3, le cooldown de charge est divisé par 2 → il charge 2× plus souvent
+ charge_reset = MORVETH_CHARGE_COOLDOWN / 2 if phase3 else MORVETH_CHARGE_COOLDOWN
+
+ if d["is_charging"]:
+ _boss_charge_move(enemy, d, alive_players, MORVETH_CHARGE_SPEED, MORVETH_HITBOX_RADIUS, MORVETH_MELEE_DAMAGE * 2)
+ elif d["charge_cd"] <= 0 and alive_players:
+ _start_charge(d, enemy, alive_players, MORVETH_CHARGE_DURATION, charge_reset, "Morveth")
+ else:
+ _boss_normal_move(enemy, alive_players, speed, MORVETH_HITBOX_RADIUS, MORVETH_MELEE_DAMAGE)
+
+
+def _init_morveth_data(d):
+ # init des champs internes de Morveth, fait 1 seule fois
+ if "charge_cd" in d:
+ return
+ d.update({
+ "charge_cd": MORVETH_CHARGE_COOLDOWN,
+ "charge_timer": 0.0,
+ "charge_tx": 0.0, "charge_ty": 0.0,
+ "is_charging": False,
+ "burst_cd": MORVETH_BURST_COOLDOWN,
+ "general_1_spawned": False, # généraux déjà invoqués ? (évite de les respawner)
+ "general_2_spawned": False,
+ "general_3_spawned": False,
+ })
+
+
+def _morveth_spawn_generals(d, hp_pct, enemy, pending):
+ """Invoque un Général à chaque seuil HP (une seule fois par seuil).
+
+ hp_pct = pourcentage de HP de Morveth (0.0 à 1.0).
+ Les seuils : 75%, 50%, 25%.
+ Le flag (ex: "general_1_spawned") empêche de respawner si Morveth repasse sous le seuil.
+ """
+ thresholds = [
+ # (seuil HP, flag dans d, offset X spawn, offset Y spawn)
+ (0.75, "general_1_spawned", -3, 0), # général 1 à gauche de Morveth
+ (0.50, "general_2_spawned", 3, 0), # général 2 à droite
+ (0.25, "general_3_spawned", 0, 2), # général 3 devant
+ ]
+ for pct, flag, dx, dy in thresholds:
+ if hp_pct <= pct and not d[flag]: # seuil atteint ET pas encore spawné
+ pending.append(("general", enemy.x + dx, enemy.y + dy)) # ajouter à la file
+ d[flag] = True # marquer comme spawné pour ne pas recommencer
+ logger.info("Morveth invoque General (%s)", flag)
+
+
+def _morveth_burst(d, enemy, phase2, phase3, pending):
+ """Burst d'éclats de Morveth. En phase 3, le cooldown est divisé par 2."""
+ if not phase2 or d["burst_cd"] > 0:
+ return
+
+ for _ in range(MORVETH_BURST_COUNT):
+ pending.append(("eclat", enemy.x, enemy.y))
+
+ # Phase 3 = rage → burst 2× plus fréquent
+ d["burst_cd"] = MORVETH_BURST_COOLDOWN / 2 if phase3 else MORVETH_BURST_COOLDOWN
+ logger.debug("Morveth burst : %d eclats", MORVETH_BURST_COUNT)
+
+
+# helpers partages Vexaris/Morveth
+
+def _tick_boss_cooldowns(enemy, d, phase2, burst_cd_const):
+ if enemy.attack_cooldown > 0:
+ enemy.attack_cooldown = max(0.0, enemy.attack_cooldown - TICK_DURATION)
+ d["charge_cd"] = max(0.0, d["charge_cd"] - TICK_DURATION)
+ if phase2:
+ d["burst_cd"] = max(0.0, d["burst_cd"] - TICK_DURATION)
+
+
+def _check_soulgate_contact(enemy, soulgate, hitbox, damage):
+ sg_x, sg_y = float(SOULGATE_X), float(SOULGATE_Y)
+ if not circle_overlap(enemy.x, enemy.y, hitbox, sg_x, sg_y, float(SOULGATE_HITBOX_RADIUS)):
+ return
+ if enemy.attack_cooldown > 0:
+ return
+ soulgate.hp = max(0, soulgate.hp - damage)
+ enemy.attack_cooldown = ENEMY_ATTACK_COOLDOWN
+
+
+def _start_charge(d, enemy, alive, duration, cooldown, name):
+ # la cible est fixee au declenchement (le joueur peut bouger pendant la charge)
+ target = nearest_player(enemy.x, enemy.y, alive)
+ if target is None:
+ return
+ d["is_charging"] = True
+ d["charge_timer"] = duration
+ d["charge_tx"] = target.x
+ d["charge_ty"] = target.y
+ d["charge_cd"] = cooldown
+ logger.debug("%s charge vers %s", name, target.username)
+
+
+def _boss_charge_move(enemy, d, alive_players, charge_speed, hitbox, charge_damage):
+ d["charge_timer"] -= TICK_DURATION
+ if d["charge_timer"] <= 0.0:
+ d["is_charging"] = False
+ return
+
+ # direction vers la cible figee au declenchement (le joueur peut bouger pendant la charge)
+ dx, dy = normalize(d["charge_tx"] - enemy.x, d["charge_ty"] - enemy.y)
+
+ if dx != 0.0 or dy != 0.0:
+ half_w, half_h = ARENA_WIDTH / 2, ARENA_HEIGHT / 2
+ enemy.x = max(-half_w, min(half_w, enemy.x + dx * charge_speed * TICK_DURATION))
+ enemy.y = max(-half_h, min(half_h, enemy.y + dy * charge_speed * TICK_DURATION))
+
+ # check collision avec chaque joueur vivant non immunise
+ for player in alive_players:
+ if not circle_overlap(enemy.x, enemy.y, hitbox, player.x, player.y, float(PLAYER_HITBOX_RADIUS)):
+ continue
+ if player.alive and not player_is_immune(player):
+ player.hp = max(0, player.hp - charge_damage)
+ if player.hp <= 0:
+ player.alive = False
+
+
+def _boss_normal_move(enemy, alive_players, speed, hitbox, melee_damage):
+ if not alive_players:
+ return
+ target = nearest_player(enemy.x, enemy.y, alive_players)
+ if target is None:
+ return
+
+ dx, dy = normalize(target.x - enemy.x, target.y - enemy.y)
+ enemy.x += dx * speed * TICK_DURATION
+ enemy.y += dy * speed * TICK_DURATION
+
+ if not circle_overlap(enemy.x, enemy.y, hitbox, target.x, target.y, float(PLAYER_HITBOX_RADIUS)):
+ return
+ if enemy.attack_cooldown > 0 or player_is_immune(target):
+ return
+
+ target.hp = max(0, target.hp - melee_damage)
+ if target.hp <= 0:
+ target.alive = False
+ enemy.attack_cooldown = ENEMY_ATTACK_COOLDOWN
diff --git a/server/buff_system.py b/server/buff_system.py
new file mode 100644
index 0000000..9499608
--- /dev/null
+++ b/server/buff_system.py
@@ -0,0 +1,54 @@
+# buff_system.py : buffs joueurs (immunites, vitesse, sorts divins)
+# un buff = {"type": "...", "timer": float, ...} dans player.buffs
+
+import logging
+
+from constants import (
+ ALDRIC_FLYING_SPEED_MULT,
+ TICK_DURATION,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def player_has_buff(player, buff_type):
+ return any(b["type"] == buff_type for b in player.buffs)
+
+
+def player_is_immune(player):
+ """immunise aux degats si invuln/intangible/flying.
+
+ invuln = sort divin Kael, intangible = teleport Seris, flying = vol Aldric
+ """
+ return any(b["type"] in {"invulnerable", "intangible", "flying"} for b in player.buffs)
+
+
+def player_damage_mult(player):
+ mult = 1.0
+ for b in player.buffs:
+ if b["type"] in ("damage_mult", "combat_buff"):
+ mult *= b.get("damage_mult", 1.0)
+ return mult
+
+
+def player_speed_mult(player):
+ # on prend le max et pas le produit : 2 buffs vitesse en meme temps doivent pas se cumuler a fond
+ mult = 1.0
+ for b in player.buffs:
+ if b["type"] == "flying":
+ mult = max(mult, ALDRIC_FLYING_SPEED_MULT)
+ elif b["type"] in ("speed_mult", "combat_buff"):
+ mult = max(mult, b.get("speed_mult", 1.0))
+ return mult
+
+
+def update_buffs(state):
+ # decremente les timers de tous les buffs, vire les expires
+ for player in state.players:
+ kept = []
+ for buff in player.buffs:
+ buff["timer"] = max(0.0, buff["timer"] - TICK_DURATION)
+ if buff["timer"] > 0:
+ kept.append(buff)
+ # TODO si on rajoute des effets en fin de buff, c'est ici
+ player.buffs = kept
diff --git a/server/combat.py b/server/combat.py
new file mode 100644
index 0000000..02b9bc3
--- /dev/null
+++ b/server/combat.py
@@ -0,0 +1,34 @@
+# combat.py : maths pures (hitbox, normalisation, distance segment)
+
+import math
+
+
+def circle_overlap(ax, ay, ar, bx, by, br):
+ # comparer les carres evite math.sqrt
+ dx = ax - bx
+ dy = ay - by
+ dist_sq = dx * dx + dy * dy
+ radii = ar + br
+ return dist_sq <= radii * radii
+
+
+def normalize(dx, dy):
+ mag_sq = dx * dx + dy * dy
+ if mag_sq == 0.0:
+ return 0.0, 0.0
+ mag = math.sqrt(mag_sq)
+ return dx / mag, dy / mag
+
+
+def segment_point_dist(px, py, ax, ay, bx, by):
+ # distance min entre le point P et le segment [AB], utilise pour la charge de Kael
+ dx, dy = bx - ax, by - ay
+
+ if dx == 0.0 and dy == 0.0:
+ # A == B, le segment est un point
+ return math.sqrt((px - ax) ** 2 + (py - ay) ** 2)
+
+ # projection de P sur AB, clamp entre 0 et 1
+ t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)))
+ cx, cy = ax + t * dx, ay + t * dy
+ return math.sqrt((px - cx) ** 2 + (py - cy) ** 2)
diff --git a/server/constants.py b/server/constants.py
new file mode 100644
index 0000000..528f524
--- /dev/null
+++ b/server/constants.py
@@ -0,0 +1,220 @@
+# Constantes gameplay de SOULGATE — toutes les valeurs en un seul endroit
+
+# --- Arène ---
+ARENA_WIDTH: float = 35.0 # world units, X : -17.5 à +17.5
+ARENA_HEIGHT: float = 35.0 # world units, Y : -17.5 à +17.5
+
+# Positions clés (recalees pour la nouvelle taille 35x35)
+SOULGATE_X: float = 0.0
+SOULGATE_Y: float = -15.0
+PLAYER_SPAWN_X: float = 0.0
+PLAYER_SPAWN_Y: float = -10.0
+ENEMY_MAIN_SPAWN_X: float = 0.0
+ENEMY_MAIN_SPAWN_Y: float = 15.0
+
+# --- Game loop ---
+TICK_RATE: int = 20 # ticks par seconde
+TICK_DURATION: float = 0.05 # secondes par tick (1 / TICK_RATE)
+
+# --- Vagues ---
+PREPARATION_DURATION: float = 20.0 # secondes de préparation entre les vagues
+
+# --- Soulgate ---
+SOULGATE_MAX_HP: int = 100
+
+# --- Reconnexion ---
+RECONNECT_TIMEOUT: int = 30 # secondes avant passage en IA basique
+
+# --- Joueurs ---
+# Vitesse de déplacement (world units / seconde)
+PLAYER_SPEED: float = 10.0
+
+# HP max par classe
+PLAYER_MAX_HP: dict[str, int] = {
+ "kael": 150, # Tank
+ "seris": 80, # Mage DPS (fragile)
+ "aldric": 100, # Support
+}
+
+# --- Combat ---
+# Cooldown entre deux attaques basiques (secondes)
+ATTACK_COOLDOWN: float = 0.5
+
+# Vitesse des projectiles par classe (world units / seconde)
+PROJECTILE_SPEED: dict[str, float] = {
+ "kael": 8.0, # lent — portée courte (pseudo-mêlée)
+ "seris": 20.0, # rapide — longue portée
+ "aldric": 12.0, # moyen — portée moyenne
+}
+
+# Dégâts de l'attaque basique par classe
+PROJECTILE_DAMAGE: dict[str, int] = {
+ "kael": 30,
+ "seris": 15,
+ "aldric": 10,
+}
+
+# Durée de vie des projectiles (secondes) — détermine la portée effective
+PROJECTILE_TTL: dict[str, float] = {
+ "kael": 0.3, # ~2.4 wu (mêlée)
+ "seris": 1.5, # ~30 wu (longue portée)
+ "aldric": 0.8, # ~9.6 wu
+}
+
+# --- Attaque mêlée de Kael (remplace le projectile) ---
+KAEL_MELEE_RADIUS: float = 3.0 # portée du slash en world units
+KAEL_MELEE_DAMAGE: int = 40 # dégâts par coup (un peu plus qu'avant car pas de portée)
+
+# --- Compétences actives de Kael ---
+KAEL_SLAM_RADIUS: float = 3.5 # rayon du slam AOE (skill 2 — frappe lourde, cercle complet)
+KAEL_SLAM_TICKS: int = 8 # durée de la zone en ticks (8 × 50ms = 0.4s)
+KAEL_SLAM_DMG_PER_TICK: int = 8 # dégâts par tick (8 × 8 = 64 total max)
+KAEL_STORM_RADIUS: float = 5.5 # rayon de la tempête (skill 3 — AOE large autour de Kael)
+KAEL_STORM_TICKS: int = 20 # durée de la zone en ticks (20 × 50ms = 1.0s)
+KAEL_STORM_DMG_PER_TICK: int = 5 # dégâts par tick (5 × 20 = 100 total max)
+KAEL_SHIELD_DURATION: float = 3.0 # durée du bouclier (skill 1 — invulnérabilité en secondes)
+
+# Rayons des hitboxes (world units)
+PROJECTILE_RADIUS: float = 0.4
+PLAYER_HITBOX_RADIUS: float = 0.5
+ENEMY_HITBOX_RADIUS: float = 0.5
+
+# --- Boss Vexaris (vague 2) ---
+VEXARIS_HP: int = 600
+VEXARIS_HITBOX_RADIUS: float = 1.2
+VEXARIS_SPEED_P1: float = 3.5 # wu/s — phase 1 (>50 % PV)
+VEXARIS_SPEED_P2: float = 5.5 # wu/s — phase 2 (≤50 % PV)
+VEXARIS_MELEE_DAMAGE: int = 35
+VEXARIS_SG_DAMAGE: int = 40
+VEXARIS_CHARGE_COOLDOWN: float = 7.0 # s entre deux charges
+VEXARIS_CHARGE_SPEED: float = 20.0 # wu/s pendant la charge
+VEXARIS_CHARGE_DURATION: float = 0.5 # s de durée de charge
+VEXARIS_BURST_COOLDOWN: float = 10.0 # s entre deux bursts (phase 2)
+VEXARIS_BURST_COUNT: int = 3 # éclats par burst
+
+# --- Boss Morveth (vague 3) ---
+MORVETH_HP: int = 1000
+MORVETH_HITBOX_RADIUS: float = 1.5
+MORVETH_SPEED_P1: float = 2.5 # wu/s — phase 1 (>75 % PV)
+MORVETH_SPEED_P2: float = 4.0 # wu/s — phase 2 (25–75 % PV)
+MORVETH_SPEED_P3: float = 6.5 # wu/s — phase 3 enragé (≤25 % PV)
+MORVETH_MELEE_DAMAGE: int = 45
+MORVETH_SG_DAMAGE: int = 50
+MORVETH_CHARGE_COOLDOWN: float = 9.0
+MORVETH_CHARGE_SPEED: float = 22.0
+MORVETH_CHARGE_DURATION: float = 0.6
+MORVETH_BURST_COOLDOWN: float = 12.0 # s entre deux bursts (phase 2+)
+MORVETH_BURST_COUNT: int = 4
+
+# Généraux (sub-boss invoqués par Morveth)
+GENERAL_HP: int = 150
+GENERAL_HITBOX_RADIUS: float = 0.8
+GENERAL_SPEED: float = 4.5
+GENERAL_MELEE_DAMAGE: int = 25
+GENERAL_SG_DAMAGE: int = 15
+
+# Bosses par vague (numéro 1-indexed → type)
+WAVE_BOSSES: dict[int, str] = {2: "vexaris", 3: "morveth"}
+
+# --- Ennemis ---
+# Stats par type : hp, vitesse (wu/s), dégâts au joueur, dégâts au Soulgate, rayon hitbox
+ENEMY_STATS: dict[str, dict] = {
+ "fracture": {"hp": 40, "speed": 4.0, "damage": 10, "sg_damage": 0, "radius": 0.5},
+ "rampant": {"hp": 20, "speed": 7.0, "damage": 5, "sg_damage": 5, "radius": 0.4},
+ "colosse": {"hp": 200, "speed": 2.0, "damage": 20, "sg_damage": 20, "radius": 1.0},
+ "eclat": {"hp": 10, "speed": 8.0, "damage": 8, "sg_damage": 2, "radius": 0.3},
+ # Boss — valeurs doivent correspondre aux constantes VEXARIS_* / MORVETH_* ci-dessus
+ "vexaris": {"hp": 600, "speed": 3.5, "damage": 35, "sg_damage": 40, "radius": 1.2},
+ "morveth": {"hp": 1000, "speed": 2.5, "damage": 45, "sg_damage": 50, "radius": 1.5},
+ "general": {"hp": 150, "speed": 4.5, "damage": 25, "sg_damage": 15, "radius": 0.8},
+}
+
+# Intervalle minimum entre deux frappes du même ennemi sur la même cible (secondes)
+ENEMY_ATTACK_COOLDOWN: float = 1.0
+
+# Rayon de la hitbox du Soulgate (wu) — correspond approximativement au pad visuel
+SOULGATE_HITBOX_RADIUS: float = 1.0
+
+# --- Cooldowns des compétences ---
+# Cooldowns des compétences par classe (ability_1, ability_2, ability_3) en secondes
+ABILITY_COOLDOWNS: dict[str, list[float]] = {
+ "kael": [7.0, 12.0], # skill 1 = slam, skill 2 = tempête
+ "seris": [6.0, 10.0], # skill 1 : éventail, skill 2 : vide (ratio ×1.67)
+ "aldric": [8.0, 15.0], # à définir lors de l'implémentation d'Aldric
+}
+
+# --- Positions de spawn ---
+# Positions de spawn initiales des 3 joueurs (devant le Soulgate, légèrement décalés)
+PLAYER_SPAWN_POSITIONS: list[tuple[float, float]] = [
+ (0.0, -10.0), # joueur 1 — centre
+ (-2.0, -9.0), # joueur 2 — gauche
+ (2.0, -9.0), # joueur 3 — droite
+]
+
+# --- Déplacements spéciaux (touche E) ---
+DISPLACEMENT_COOLDOWNS: dict[str, float] = {
+ "kael": 9.0, # augmenté (dash puissant même court)
+ "seris": 4.0,
+ "aldric": 14.0,
+}
+KAEL_CHARGE_DISTANCE: float = 2.5 # wu parcourus
+KAEL_CHARGE_DAMAGE: int = 20 # dégâts aux ennemis traversés
+KAEL_CHARGE_HIT_RADIUS: float = 1.2 # rayon de détection autour du segment
+SERIS_INTANGIBLE_DURATION: float = 0.3 # s intangible post-téléport
+
+# --- Compétences actives de Seris ---
+SERIS_FAN_COUNT: int = 3 # nombre de dagues dans l'éventail (skill 1)
+SERIS_FAN_SPREAD: float = 0.35 # demi-angle du cône en radians (~20°)
+SERIS_FAN_DAMAGE: int = 12 # dégâts par dague (3 × 12 = 36 max)
+SERIS_VOID_RADIUS: float = 4.0 # rayon explosion vide (skill 2)
+SERIS_VOID_TICKS: int = 10 # durée en ticks (10 × 50ms = 0.5s)
+SERIS_VOID_DMG_PER_TICK: int = 8 # dégâts/tick (8 × 10 = 80 total max)
+ALDRIC_FLYING_DURATION: float = 4.0 # s de vol
+ALDRIC_FLYING_SPEED_MULT: float = 1.6 # vitesse ×1,6 en vol
+
+# --- Compétences actives d'Aldric ---
+ALDRIC_HEAL_RADIUS: float = 6.0 # rayon du halo de soin (skill 1)
+ALDRIC_HEAL_AMOUNT: int = 25 # PV soignés par utilisation
+ALDRIC_PULSE_RADIUS: float = 5.0 # rayon de la vague d'énergie (skill 2)
+ALDRIC_PULSE_TICKS: int = 8 # durée en ticks (8 × 50ms = 0.4s)
+ALDRIC_PULSE_DMG_PER_TICK: int = 8 # dégâts/tick (8 × 8 = 64 total max)
+
+# --- Sorts Divins (touche R) ---
+DIVINE_POST_USE_COOLDOWN: float = 60.0 # cooldown entre 2 utilisations (évite spam)
+KAEL_DIVINE_DURATION: float = 8.0
+KAEL_DIVINE_DAMAGE_MULT: float = 3.0
+KAEL_DIVINE_SG_HEAL_PER_KILL: float = 0.01 # 1 % HP max par kill
+SERIS_DIVINE_CHANNEL: float = 3.0 # s de canalisation
+SERIS_DIVINE_FREEZE: float = 6.0 # s de gel des ennemis
+SERIS_DIVINE_DPS: int = 15 # dégâts/s aux ennemis gelés
+ALDRIC_DIVINE_CHANNEL: float = 2.0 # s de canalisation
+ALDRIC_DIVINE_BUFF_DURATION: float = 10.0
+ALDRIC_DIVINE_DAMAGE_MULT: float = 1.3
+ALDRIC_DIVINE_SPEED_MULT: float = 1.3
+ALDRIC_DIVINE_SG_HEAL: float = 0.10 # 10 % HP max Soulgate
+
+# --- Upgrades ---
+# Âmes gagnées par kill selon le type d'ennemi
+SOUL_REWARDS: dict[str, int] = {
+ "fracture": 10,
+ "rampant": 8,
+ "colosse": 50,
+ "eclat": 5,
+ "vexaris": 200,
+ "general": 75,
+ "morveth": 300,
+}
+
+# Catalogue d'upgrades disponibles pendant la phase de préparation
+UPGRADE_CATALOG: dict[str, dict] = {
+ "damage_up": {"name": "Frappe +", "description": "+25% dégâts projectile", "cost": 30, "max_stacks": 2},
+ "cooldown_down": {"name": "Cadence +", "description": "-20% cooldown d'attaque", "cost": 25, "max_stacks": 2},
+ "hp_up": {"name": "Vitalité +", "description": "+30 PV max", "cost": 35, "max_stacks": 2},
+ "speed_up": {"name": "Vitesse +", "description": "+15% vitesse de déplacement", "cost": 20, "max_stacks": 3},
+}
+
+# --- Leaderboard / Score ---
+SCORE_PER_KILL = 10 # points par ennemi tué
+SCORE_PER_SOUL = 2 # points par âme collectée
+SCORE_VICTORY = 1000 # bonus de victoire
+SCORE_PER_WAVE = 200 # points par vague complétée
diff --git a/server/displacement.py b/server/displacement.py
new file mode 100644
index 0000000..ca7f1dc
--- /dev/null
+++ b/server/displacement.py
@@ -0,0 +1,71 @@
+# displacement.py : touche E (charge Kael / teleport Seris / vol Aldric)
+
+import logging
+
+from buff_system import player_has_buff
+from combat import normalize, segment_point_dist
+from constants import (
+ ALDRIC_FLYING_DURATION,
+ ARENA_HEIGHT, ARENA_WIDTH,
+ DISPLACEMENT_COOLDOWNS,
+ KAEL_CHARGE_DAMAGE, KAEL_CHARGE_DISTANCE, KAEL_CHARGE_HIT_RADIUS,
+ SERIS_INTANGIBLE_DURATION,
+ SOUL_REWARDS,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def handle_displacement(player, tx, ty, state):
+ if not player.alive or player.cooldowns.get("displacement", 0) > 0:
+ return
+ if player_has_buff(player, "casting"):
+ return # canalisation sort divin = on bloque le E
+
+ cls = player.class_name
+ if cls == "kael":
+ _kael_charge(player, tx, ty, state)
+ elif cls == "seris":
+ _seris_teleport(player, tx, ty)
+ elif cls == "aldric":
+ _aldric_fly(player)
+
+ player.cooldowns["displacement"] = DISPLACEMENT_COOLDOWNS[cls]
+
+
+def _kael_charge(player, tx, ty, state):
+ # charge en ligne droite, frappe les ennemis dans le couloir
+ dx, dy = normalize(tx - player.x, ty - player.y)
+ if dx == 0.0 and dy == 0.0:
+ return
+
+ half_w, half_h = ARENA_WIDTH / 2, ARENA_HEIGHT / 2
+ end_x = max(-half_w, min(half_w, player.x + dx * KAEL_CHARGE_DISTANCE))
+ end_y = max(-half_h, min(half_h, player.y + dy * KAEL_CHARGE_DISTANCE))
+
+ dead_ids = set()
+ for enemy in state.enemies:
+ dist = segment_point_dist(enemy.x, enemy.y, player.x, player.y, end_x, end_y)
+ if dist >= KAEL_CHARGE_HIT_RADIUS:
+ continue
+ enemy.hp = max(0, enemy.hp - KAEL_CHARGE_DAMAGE)
+ if enemy.hp <= 0:
+ dead_ids.add(enemy.id)
+ player.souls += SOUL_REWARDS.get(enemy.type, 0)
+
+ if dead_ids:
+ state.enemies = [e for e in state.enemies if e.id not in dead_ids]
+
+ player.x, player.y = end_x, end_y
+
+
+def _seris_teleport(player, tx, ty):
+ half_w, half_h = ARENA_WIDTH / 2, ARENA_HEIGHT / 2
+ player.x = max(-half_w, min(half_w, tx))
+ player.y = max(-half_h, min(half_h, ty))
+ # immunite breve pendant le teleport (sinon on peut tomber sur un boss et perdre direct)
+ player.buffs.append({"type": "intangible", "timer": SERIS_INTANGIBLE_DURATION})
+
+
+def _aldric_fly(player):
+ player.buffs.append({"type": "flying", "timer": ALDRIC_FLYING_DURATION})
diff --git a/server/game_loop.py b/server/game_loop.py
new file mode 100644
index 0000000..b6a6e61
--- /dev/null
+++ b/server/game_loop.py
@@ -0,0 +1,557 @@
+# game_loop.py : boucle 20Hz du serveur (mouvement, projectiles, ennemis, vagues, broadcast)
+
+import asyncio
+import logging
+import time
+
+from boss_ai import nearest_player, update_boss_ai
+from buff_system import player_damage_mult, player_has_buff, player_is_immune, player_speed_mult, update_buffs
+from combat import circle_overlap, normalize
+from displacement import handle_displacement
+from waves import WaveManager
+from constants import (
+ KAEL_MELEE_RADIUS, KAEL_MELEE_DAMAGE,
+ KAEL_SLAM_RADIUS, KAEL_SLAM_TICKS, KAEL_SLAM_DMG_PER_TICK,
+ KAEL_STORM_RADIUS, KAEL_STORM_TICKS, KAEL_STORM_DMG_PER_TICK,
+ SERIS_FAN_COUNT, SERIS_FAN_SPREAD, SERIS_FAN_DAMAGE,
+ SERIS_VOID_RADIUS, SERIS_VOID_TICKS, SERIS_VOID_DMG_PER_TICK,
+ ALDRIC_HEAL_RADIUS, ALDRIC_HEAL_AMOUNT,
+ ALDRIC_PULSE_RADIUS, ALDRIC_PULSE_TICKS, ALDRIC_PULSE_DMG_PER_TICK,
+ ABILITY_COOLDOWNS, ARENA_HEIGHT, ARENA_WIDTH,
+ ATTACK_COOLDOWN,
+ ENEMY_ATTACK_COOLDOWN, ENEMY_MAIN_SPAWN_X, ENEMY_MAIN_SPAWN_Y, ENEMY_STATS,
+ PLAYER_HITBOX_RADIUS, PLAYER_MAX_HP, PLAYER_SPAWN_POSITIONS, PLAYER_SPEED,
+ PROJECTILE_DAMAGE, PROJECTILE_RADIUS, PROJECTILE_SPEED, PROJECTILE_TTL,
+ SOULGATE_HITBOX_RADIUS, SOULGATE_MAX_HP, SOULGATE_X, SOULGATE_Y, TICK_DURATION,
+ SOUL_REWARDS, UPGRADE_CATALOG,
+ VEXARIS_HP, MORVETH_HP,
+)
+from game_state import AoeZone, EnemyState, GameState, PlayerState, ProjectileState, SoulgateState, WaveState
+
+logger = logging.getLogger(__name__)
+
+
+class GameLoop:
+ def __init__(self, lobby, broadcast):
+ self.lobby = lobby
+ self._broadcast = broadcast
+ self.state = self._init_state()
+ self._running = False
+ self._inputs = {} # {conn_id: (dx, dy)} dernier input de mouvement
+ self._proj_counter = 0
+ self._enemy_counter = 0
+ self._aoe_counter = 0
+ self._wave_manager = WaveManager()
+ self._pending_spawns = [] # spawns au prochain tick (eviter de modif la liste pendant l'iteration)
+ self._debug_invincible = False
+ self._debug_one_shot = False
+
+ def _init_state(self):
+ players = [
+ PlayerState(
+ id=p.conn_id,
+ class_name=p.player_class,
+ username=p.username,
+ x=float(PLAYER_SPAWN_POSITIONS[i][0]),
+ y=float(PLAYER_SPAWN_POSITIONS[i][1]),
+ hp=PLAYER_MAX_HP[p.player_class],
+ max_hp=PLAYER_MAX_HP[p.player_class],
+ alive=True,
+ souls=50, # ames de depart pour pouvoir acheter qq upgrades dans la 1ere prep
+ )
+ for i, p in enumerate(self.lobby.players)
+ ]
+ return GameState(
+ tick=0,
+ players=players,
+ enemies=[],
+ projectiles=[],
+ soulgate=SoulgateState(hp=SOULGATE_MAX_HP, max_hp=SOULGATE_MAX_HP),
+ wave=WaveState(number=1, phase=0, enemies_remaining=0, state="preparation"),
+ effects=[],
+ )
+
+ def _get_player(self, conn_id):
+ return next((p for p in self.state.players if p.id == conn_id), None)
+
+ # --- inputs (appeles depuis main.py au reception ws) ---
+
+ def handle_input(self, conn_id, dx, dy):
+ self._inputs[conn_id] = (dx, dy)
+
+ def handle_attack(self, conn_id, tx, ty):
+ # melee Kael / projectile Seris+Aldric
+ player = self._get_player(conn_id)
+ if not player or not player.alive:
+ return
+ if player_has_buff(player, "casting") or player.cooldowns.get("attack", 0) > 0:
+ return
+
+ dx, dy = normalize(tx - player.x, ty - player.y)
+ if dx == 0.0 and dy == 0.0:
+ return
+
+ cls = player.class_name
+ cd = ATTACK_COOLDOWN * (1 - 0.20 * player.upgrades.get("cooldown_down", 0))
+
+ if cls == "kael":
+ self._kael_melee(player, dx, dy)
+ else:
+ damage = int(PROJECTILE_DAMAGE[cls] * (1 + 0.25 * player.upgrades.get("damage_up", 0)))
+ damage = int(damage * player_damage_mult(player))
+ self._proj_counter += 1
+ self.state.projectiles.append(ProjectileState(
+ id=f"pr_{self._proj_counter}",
+ owner_id=conn_id,
+ x=player.x, y=player.y,
+ vx=dx * PROJECTILE_SPEED[cls],
+ vy=dy * PROJECTILE_SPEED[cls],
+ damage=damage,
+ radius=PROJECTILE_RADIUS,
+ ttl=PROJECTILE_TTL[cls],
+ ))
+
+ player.cooldowns["attack"] = cd
+
+ def _kael_melee(self, player, dx, dy):
+ # frappe melee Kael, demi-cercle devant
+ import math
+ # FIXME le bonus de damage_up je sais pas si c'est bien 25% par stack
+ damage = int(KAEL_MELEE_DAMAGE * (1 + 0.25 * player.upgrades.get("damage_up", 0)))
+ damage = int(damage * player_damage_mult(player))
+
+ dead = set()
+ for e in self.state.enemies:
+ ex = e.x - player.x
+ ey = e.y - player.y
+ d = math.sqrt(ex*ex + ey*ey)
+ if d > KAEL_MELEE_RADIUS:
+ continue
+ # check qu'on tape devant et pas derriere (produit scalaire)
+ if d > 0:
+ if dx * (ex/d) + dy * (ey/d) < 0:
+ continue
+ e.hp = max(0, e.hp - damage)
+ if e.hp <= 0:
+ dead.add(e.id)
+ player.souls += SOUL_REWARDS.get(e.type, 10)
+
+ if dead:
+ self.state.enemies = [x for x in self.state.enemies if x.id not in dead]
+
+ def handle_ability(self, conn_id, ability_id, tx, ty):
+ # touches 1 et 2 (skill par classe)
+ player = self._get_player(conn_id)
+ if not player or not player.alive:
+ return
+ key = f"ability_{ability_id}"
+ if player.cooldowns.get(key, 0) > 0:
+ return
+
+ cooldown_list = ABILITY_COOLDOWNS.get(player.class_name, [8.0, 12.0])
+ idx = ability_id - 1
+ if not (0 <= idx < len(cooldown_list)):
+ return
+
+ cls = player.class_name
+ if cls == "kael":
+ if ability_id == 1: self._kael_slam(player)
+ elif ability_id == 2: self._kael_storm(player)
+ elif cls == "seris":
+ if ability_id == 1: self._seris_fan(player, tx, ty)
+ elif ability_id == 2: self._seris_void(player)
+ elif cls == "aldric":
+ if ability_id == 1: self._aldric_heal(player)
+ elif ability_id == 2: self._aldric_pulse(player)
+
+ player.cooldowns[key] = cooldown_list[idx]
+
+ def _kael_slam(self, player):
+ self._aoe_counter += 1
+ dmg = int(KAEL_SLAM_DMG_PER_TICK * (1 + 0.25 * player.upgrades.get("damage_up", 0)))
+ dmg = int(dmg * player_damage_mult(player))
+ self.state.aoe_zones.append(AoeZone(
+ id=f"aoe_{self._aoe_counter}",
+ owner_id=player.id,
+ zone_type="slam",
+ x=player.x, y=player.y,
+ radius=KAEL_SLAM_RADIUS,
+ damage_per_tick=dmg,
+ ticks_remaining=KAEL_SLAM_TICKS,
+ max_ticks=KAEL_SLAM_TICKS,
+ ))
+
+ def _kael_storm(self, player):
+ # tempete : grosse zone aoe sur plusieurs ticks
+ self._aoe_counter += 1
+ dmg = int(KAEL_STORM_DMG_PER_TICK * (1 + 0.25 * player.upgrades.get("damage_up", 0)))
+ dmg = int(dmg * player_damage_mult(player))
+ self.state.aoe_zones.append(AoeZone(
+ id=f"aoe_{self._aoe_counter}",
+ owner_id=player.id,
+ zone_type="storm",
+ x=player.x, y=player.y,
+ radius=KAEL_STORM_RADIUS,
+ damage_per_tick=dmg,
+ ticks_remaining=KAEL_STORM_TICKS,
+ max_ticks=KAEL_STORM_TICKS,
+ ))
+
+ def _seris_fan(self, player, tx, ty):
+ # 3 dagues en eventail vers la souris
+ import math
+ dx, dy = normalize(tx - player.x, ty - player.y)
+ if dx == 0.0 and dy == 0.0:
+ return
+ base_angle = math.atan2(dy, dx)
+ dmg = int(SERIS_FAN_DAMAGE * (1 + 0.25 * player.upgrades.get("damage_up", 0)))
+ dmg = int(dmg * player_damage_mult(player))
+
+ for i in range(SERIS_FAN_COUNT):
+ # repartition symetrique : -spread, 0, +spread (avec count impair)
+ offset = SERIS_FAN_SPREAD * (i - (SERIS_FAN_COUNT - 1) / 2)
+ angle = base_angle + offset
+ vx = math.cos(angle) * PROJECTILE_SPEED["seris"]
+ vy = math.sin(angle) * PROJECTILE_SPEED["seris"]
+ self._proj_counter += 1
+ self.state.projectiles.append(ProjectileState(
+ id=f"pr_{self._proj_counter}",
+ owner_id=player.id,
+ x=player.x, y=player.y,
+ vx=vx, vy=vy,
+ damage=dmg,
+ radius=PROJECTILE_RADIUS,
+ ttl=PROJECTILE_TTL["seris"],
+ ))
+
+ def _seris_void(self, player):
+ self._aoe_counter += 1
+ dmg = int(SERIS_VOID_DMG_PER_TICK * (1 + 0.25 * player.upgrades.get("damage_up", 0)))
+ dmg = int(dmg * player_damage_mult(player))
+ self.state.aoe_zones.append(AoeZone(
+ id=f"aoe_{self._aoe_counter}",
+ owner_id=player.id,
+ zone_type="void",
+ x=player.x, y=player.y,
+ radius=SERIS_VOID_RADIUS,
+ damage_per_tick=dmg,
+ ticks_remaining=SERIS_VOID_TICKS,
+ max_ticks=SERIS_VOID_TICKS,
+ ))
+
+ def _aldric_heal(self, player):
+ # halo de soin sur tous les joueurs dans le rayon
+ for t in self.state.players:
+ if not t.alive:
+ continue
+ if circle_overlap(player.x, player.y, ALDRIC_HEAL_RADIUS, t.x, t.y, PLAYER_HITBOX_RADIUS):
+ t.hp = min(t.hp + ALDRIC_HEAL_AMOUNT, t.max_hp)
+
+ def _aldric_pulse(self, player):
+ self._aoe_counter += 1
+ dmg = int(ALDRIC_PULSE_DMG_PER_TICK * (1 + 0.25 * player.upgrades.get("damage_up", 0)))
+ dmg = int(dmg * player_damage_mult(player))
+ self.state.aoe_zones.append(AoeZone(
+ id=f"aoe_{self._aoe_counter}",
+ owner_id=player.id,
+ zone_type="pulse",
+ x=player.x, y=player.y,
+ radius=ALDRIC_PULSE_RADIUS,
+ damage_per_tick=dmg,
+ ticks_remaining=ALDRIC_PULSE_TICKS,
+ max_ticks=ALDRIC_PULSE_TICKS,
+ ))
+
+ def _process_aoe_zones(self):
+ import math
+ if not self.state.aoe_zones:
+ return
+
+ dead = set()
+ owner_map = {p.id: p for p in self.state.players}
+
+ for zone in self.state.aoe_zones:
+ owner = owner_map.get(zone.owner_id)
+ for e in self.state.enemies:
+ if e.id in dead:
+ continue
+ dx = e.x - zone.x
+ dy = e.y - zone.y
+ if math.sqrt(dx*dx + dy*dy) <= zone.radius:
+ e.hp = max(0, e.hp - zone.damage_per_tick)
+ if e.hp <= 0:
+ dead.add(e.id)
+ if owner:
+ owner.souls += SOUL_REWARDS.get(e.type, 10)
+ zone.ticks_remaining -= 1
+
+ if dead:
+ self.state.enemies = [e for e in self.state.enemies if e.id not in dead]
+
+ self.state.aoe_zones = [z for z in self.state.aoe_zones if z.ticks_remaining > 0]
+
+ def handle_displacement(self, conn_id, tx, ty):
+ # touche E, on delegue a displacement.py
+ p = self._get_player(conn_id)
+ if p:
+ handle_displacement(p, tx, ty, self.state)
+
+ def spawn_enemy(self, enemy_type):
+ self._enemy_counter += 1
+ eid = f"en_{self._enemy_counter}"
+
+ if enemy_type == "vexaris":
+ self.state.enemies.append(EnemyState(
+ id=eid, type="vexaris", x=0.0, y=12.0,
+ hp=VEXARIS_HP, max_hp=VEXARIS_HP, is_boss=True,
+ ))
+ elif enemy_type == "morveth":
+ # TODO peut etre baisser ses HP si c trop dur
+ self.state.enemies.append(EnemyState(
+ id=eid, type="morveth", x=0.0, y=12.0,
+ hp=MORVETH_HP, max_hp=MORVETH_HP, is_boss=True,
+ ))
+ else:
+ stats = ENEMY_STATS[enemy_type]
+ self.state.enemies.append(EnemyState(
+ id=eid, type=enemy_type,
+ x=float(ENEMY_MAIN_SPAWN_X), y=float(ENEMY_MAIN_SPAWN_Y),
+ hp=stats["hp"], max_hp=stats["hp"],
+ ))
+
+ def _spawn_enemy_at(self, enemy_type, x, y):
+ # spawn a une position precise (pour les generaux de Morveth)
+ stats = ENEMY_STATS[enemy_type]
+ self._enemy_counter += 1
+ self.state.enemies.append(EnemyState(
+ id=f"en_{self._enemy_counter}", type=enemy_type,
+ x=x, y=y, hp=stats["hp"], max_hp=stats["hp"],
+ ))
+
+ def _update_enemies(self):
+ sg_x, sg_y = float(SOULGATE_X), float(SOULGATE_Y)
+ alive_players = [p for p in self.state.players if p.alive]
+ surviving = []
+
+ for enemy in self.state.enemies:
+
+ if enemy.frozen_timer > 0:
+ # Ennemi gelé (sort divin Seris) → il ne bouge pas ce tick
+ surviving.append(enemy)
+ continue
+
+ if enemy.is_boss:
+ # Boss → IA spéciale dans boss_ai.py
+ update_boss_ai(enemy, self.state.players, self.state.soulgate, self._pending_spawns)
+ surviving.append(enemy) # les boss ne "meurent" pas via keep=False
+ continue
+
+ # Ennemi normal
+ keep = self._update_single_enemy(enemy, sg_x, sg_y, alive_players)
+ if keep:
+ surviving.append(enemy) # False = ennemi mort (a touché le Soulgate)
+
+ self.state.enemies = surviving # remplacer par la liste filtrée
+
+ # Spawner les ennemis en attente (ex: généraux de Morveth)
+ for etype, ex, ey in self._pending_spawns:
+ self._spawn_enemy_at(etype, ex, ey)
+ self._pending_spawns.clear() # vider la file d'attente
+
+ def _update_single_enemy(self, enemy, sg_x, sg_y, alive_players):
+ # retourne False si l'ennemi doit etre supprime (mort)
+ stats = ENEMY_STATS[enemy.type]
+ speed, e_radius = stats["speed"], stats["radius"]
+
+ if enemy.attack_cooldown > 0:
+ enemy.attack_cooldown = max(0.0, enemy.attack_cooldown - TICK_DURATION)
+
+ if stats["sg_damage"] > 0:
+ # vise le Soulgate (ex fracture)
+ return self._enemy_target_soulgate(enemy, sg_x, sg_y, e_radius, stats)
+ # vise les joueurs (rampant, eclat)
+ return self._enemy_target_player(enemy, alive_players, e_radius, speed, stats)
+
+ def _enemy_target_soulgate(self, enemy, sg_x, sg_y, e_radius, stats):
+ t_radius = float(SOULGATE_HITBOX_RADIUS)
+
+ if circle_overlap(enemy.x, enemy.y, e_radius, sg_x, sg_y, t_radius):
+ if enemy.attack_cooldown <= 0:
+ self.state.soulgate.hp = max(0, self.state.soulgate.hp - stats["sg_damage"])
+ return False
+ else:
+ dx, dy = normalize(sg_x - enemy.x, sg_y - enemy.y)
+ enemy.x += dx * stats["speed"] * TICK_DURATION
+ enemy.y += dy * stats["speed"] * TICK_DURATION
+ return True
+
+ def _enemy_target_player(self, enemy, alive_players, e_radius, speed, stats):
+ target = nearest_player(enemy.x, enemy.y, alive_players)
+ if target is None:
+ return True
+
+ t_radius = float(PLAYER_HITBOX_RADIUS)
+ in_contact = circle_overlap(enemy.x, enemy.y, e_radius, target.x, target.y, t_radius)
+
+ if in_contact and enemy.attack_cooldown <= 0:
+ if not player_is_immune(target):
+ target.hp = max(0, target.hp - stats["damage"])
+ if target.hp <= 0:
+ target.alive = False
+ enemy.attack_cooldown = ENEMY_ATTACK_COOLDOWN
+
+ if not in_contact:
+ dx, dy = normalize(target.x - enemy.x, target.y - enemy.y)
+ enemy.x += dx * speed * TICK_DURATION
+ enemy.y += dy * speed * TICK_DURATION
+
+ return True # l'ennemi reste vivant (il ne se consume pas)
+
+ def debug_kill_all(self):
+ self.state.enemies.clear()
+ self.state.projectiles.clear()
+
+ def debug_toggle_invincible(self):
+ self._debug_invincible = not self._debug_invincible
+
+ def debug_toggle_one_shot(self):
+ self._debug_one_shot = not self._debug_one_shot
+
+ def debug_revive_all(self):
+ for p in self.state.players:
+ p.hp = p.max_hp
+ p.alive = True
+
+ def handle_upgrade(self, conn_id, upgrade_id):
+ # achat d'upgrade pendant la phase de prep
+ if self._wave_manager.state != "preparation":
+ return
+ p = self._get_player(conn_id)
+ if not p or upgrade_id not in UPGRADE_CATALOG:
+ return
+
+ spec = UPGRADE_CATALOG[upgrade_id]
+ stacks = p.upgrades.get(upgrade_id, 0)
+ if stacks >= spec["max_stacks"] or p.souls < spec["cost"]:
+ return
+
+ p.souls -= spec["cost"]
+ p.upgrades[upgrade_id] = stacks + 1
+
+ if upgrade_id == "hp_up":
+ # cas special : augmente les hp max + soigne du delta
+ p.max_hp += 30
+ p.hp = min(p.hp + 30, p.max_hp)
+
+ def _apply_movement(self):
+ half_w, half_h = ARENA_WIDTH / 2, ARENA_HEIGHT / 2
+
+ for p in self.state.players:
+ if not p.alive or player_has_buff(p, "casting"):
+ continue
+ dx, dy = self._inputs.get(p.id, (0.0, 0.0))
+ if dx == 0.0 and dy == 0.0:
+ continue
+
+ speed = PLAYER_SPEED * (1 + 0.15 * p.upgrades.get("speed_up", 0))
+ speed *= player_speed_mult(p)
+
+ # clamp dans l'arene
+ p.x = max(-half_w, min(half_w, p.x + dx * speed * TICK_DURATION))
+ p.y = max(-half_h, min(half_h, p.y + dy * speed * TICK_DURATION))
+
+ def _update_projectiles(self):
+ half_w, half_h = ARENA_WIDTH / 2, ARENA_HEIGHT / 2
+ dead_enemy_ids = set()
+ surviving = []
+
+ for proj in self.state.projectiles:
+ # Déplacer le projectile : position += vitesse × durée tick
+ proj.x += proj.vx * TICK_DURATION
+ proj.y += proj.vy * TICK_DURATION
+ proj.ttl -= TICK_DURATION # réduire le temps de vie restant
+
+ # Vérifier si le projectile doit disparaître
+ if proj.ttl <= 0 or abs(proj.x) > half_w or abs(proj.y) > half_h:
+ continue # TTL écoulé ou hors arène → ne pas ajouter à surviving
+
+ # Vérifier les collisions avec les ennemis
+ hit = self._check_proj_hit(proj, dead_enemy_ids)
+ if not hit:
+ surviving.append(proj) # pas de collision → projectile continue
+
+ self.state.projectiles = surviving # remplacer par les projectiles encore en vol
+
+ # Supprimer les ennemis tués par des projectiles ce tick
+ if dead_enemy_ids:
+ self.state.enemies = [e for e in self.state.enemies if e.id not in dead_enemy_ids]
+
+ def _check_proj_hit(self, proj, dead_ids):
+ for e in self.state.enemies:
+ if e.id in dead_ids:
+ continue
+ r = ENEMY_STATS[e.type]["radius"]
+ if not circle_overlap(proj.x, proj.y, proj.radius, e.x, e.y, r):
+ continue
+ # collision
+ e.hp = 0 if self._debug_one_shot else e.hp - proj.damage
+ if e.hp <= 0:
+ dead_ids.add(e.id)
+ owner = self._get_player(proj.owner_id)
+ if owner:
+ owner.souls += SOUL_REWARDS.get(e.type, 0)
+ owner.enemies_killed += 1
+ return True
+ return False
+
+ def _update_cooldowns(self):
+ for p in self.state.players:
+ for k, v in p.cooldowns.items():
+ if v > 0:
+ p.cooldowns[k] = max(0.0, v - TICK_DURATION)
+
+ def _update_wave(self):
+ wm = self._wave_manager
+ wm.tick(self.spawn_enemy, len(self.state.enemies))
+
+ self.state.wave.number = wm.wave_number
+ self.state.wave.phase = wm.phase_number
+ self.state.wave.state = wm.state
+ self.state.wave.enemies_remaining = len(self.state.enemies) + wm.enemies_remaining()
+ self.state.wave.prep_timer = wm.prep_timer_remaining
+
+ boss = next((e for e in self.state.enemies if e.is_boss), None)
+ self.state.wave.boss_hp = boss.hp if boss else 0
+ self.state.wave.boss_max_hp = boss.max_hp if boss else 0
+ self.state.wave.boss_name = boss.type if boss else ""
+
+ def _tick(self):
+ # 1 tick = 1 frame de jeu (20Hz)
+ self.state.tick += 1
+
+ self._apply_movement()
+ self._update_projectiles()
+ self._process_aoe_zones()
+ self._update_enemies()
+ self._update_cooldowns()
+ update_buffs(self.state)
+ self._update_wave()
+
+ if self._debug_invincible:
+ for p in self.state.players:
+ p.hp = p.max_hp
+ p.alive = True
+
+ async def run(self):
+ self._running = True
+ logger.info("Game loop demarree — lobby %s", self.lobby.code)
+ while self._running:
+ start = time.monotonic()
+ self._tick()
+ await self._broadcast(self.state.to_dict())
+ elapsed = time.monotonic() - start
+ # si le tick a pris plus de 50ms on rattrape pas, on enchaine
+ await asyncio.sleep(max(0.0, TICK_DURATION - elapsed))
+
+ def stop(self):
+ self._running = False
diff --git a/server/game_state.py b/server/game_state.py
new file mode 100644
index 0000000..6143620
--- /dev/null
+++ b/server/game_state.py
@@ -0,0 +1,184 @@
+# game_state.py : structures de donnees broadcastees au client a chaque tick
+
+from dataclasses import dataclass, field
+
+
+@dataclass
+class ProjectileState:
+ id: str
+ owner_id: str
+ x: float
+ y: float
+ vx: float
+ vy: float
+ damage: int
+ radius: float
+ ttl: float
+
+ def to_dict(self):
+ # vx/vy/damage/ttl : info serveur, pas envoyes au client
+ return {
+ "id": self.id,
+ "owner": self.owner_id,
+ "x": self.x,
+ "y": self.y,
+ "radius": self.radius,
+ }
+
+
+@dataclass
+class EnemyState:
+ id: str
+ type: str
+ x: float
+ y: float
+ hp: int
+ max_hp: int
+ attack_cooldown: float = 0.0
+ is_boss: bool = False
+ frozen_timer: float = 0.0 # gel du sort divin Seris
+ data: dict = field(default_factory=dict) # interne au boss (phases, charge), pas broadcast
+
+ def to_dict(self):
+ return {
+ "id": self.id,
+ "type": self.type,
+ "x": self.x,
+ "y": self.y,
+ "hp": self.hp,
+ "max_hp": self.max_hp,
+ "is_boss": self.is_boss,
+ "frozen": self.frozen_timer > 0,
+ "is_charging": self.data.get("is_charging", False) if self.is_boss else False,
+ }
+
+
+@dataclass
+class PlayerState:
+ id: str
+ class_name: str
+ username: str
+ x: float
+ y: float
+ hp: int
+ max_hp: int
+ alive: bool
+
+ revive_progress: float = 0.0
+
+ cooldowns: dict = field(default_factory=lambda: {
+ "attack": 0,
+ "ability_1": 0,
+ "ability_2": 0,
+ "displacement": 0,
+ })
+
+ # buffs actifs : {"type": "...", "timer": float, ...}
+ # types : invulnerable, intangible, flying, casting, damage_mult, speed_mult, combat_buff, divine_freeze_dps
+ buffs: list = field(default_factory=list)
+
+ souls: int = 0
+ enemies_killed: int = 0
+
+ # upgrade_id -> nb stacks (damage_up, cooldown_down, hp_up, speed_up)
+ upgrades: dict = field(default_factory=dict)
+
+ def to_dict(self):
+ return {
+ "id": self.id,
+ "class": self.class_name,
+ "username": self.username,
+ "x": self.x,
+ "y": self.y,
+ "hp": self.hp,
+ "max_hp": self.max_hp,
+ "alive": self.alive,
+ "revive_progress": self.revive_progress,
+ "cooldowns": self.cooldowns,
+ "buffs": self.buffs,
+ "souls": self.souls,
+ "enemies_killed": self.enemies_killed,
+ "upgrades": self.upgrades,
+ }
+
+
+@dataclass
+class AoeZone:
+ id: str
+ owner_id: str
+ zone_type: str
+ x: float
+ y: float
+ radius: float
+ damage_per_tick: int
+ ticks_remaining: int
+ max_ticks: int
+
+ def to_dict(self):
+ return {
+ "id": self.id,
+ "type": self.zone_type,
+ "x": self.x,
+ "y": self.y,
+ "radius": self.radius,
+ "ticks": self.ticks_remaining,
+ "max_ticks": self.max_ticks,
+ }
+
+
+@dataclass
+class SoulgateState:
+ hp: int
+ max_hp: int
+
+ def to_dict(self):
+ return {"hp": self.hp, "max_hp": self.max_hp}
+
+
+@dataclass
+class WaveState:
+ number: int
+ phase: int
+ enemies_remaining: int
+ state: str # combat | preparation | boss | victory
+ prep_timer: float = 0.0
+ boss_hp: int = 0
+ boss_max_hp: int = 0
+ boss_name: str = ""
+
+ def to_dict(self):
+ return {
+ "number": self.number,
+ "phase": self.phase,
+ "enemies_remaining": self.enemies_remaining,
+ "state": self.state,
+ "prep_timer": self.prep_timer,
+ "boss_hp": self.boss_hp,
+ "boss_max_hp": self.boss_max_hp,
+ "boss_name": self.boss_name,
+ }
+
+
+@dataclass
+class GameState:
+ tick: int
+ players: list
+ enemies: list
+ projectiles: list
+ soulgate: SoulgateState
+ wave: WaveState
+ effects: list
+ aoe_zones: list = field(default_factory=list)
+
+ def to_dict(self):
+ return {
+ "type": "game_state",
+ "tick": self.tick,
+ "players": [p.to_dict() for p in self.players],
+ "enemies": [e.to_dict() for e in self.enemies],
+ "projectiles": [p.to_dict() for p in self.projectiles],
+ "aoe_zones": [z.to_dict() for z in self.aoe_zones],
+ "soulgate": self.soulgate.to_dict(),
+ "wave": self.wave.to_dict(),
+ "effects": self.effects,
+ }
diff --git a/server/lobby.py b/server/lobby.py
new file mode 100644
index 0000000..5d19454
--- /dev/null
+++ b/server/lobby.py
@@ -0,0 +1,206 @@
+# lobby.py : gestion des lobbies (creation, classe, ready, demarrage)
+# les methodes retournent (conn_id, message), c'est main.py qui envoie
+
+import logging
+import random
+from dataclasses import dataclass, field
+
+logger = logging.getLogger(__name__)
+
+# pas de 0/O/I/1 pour eviter les confusions
+_LOBBY_CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
+_LOBBY_CODE_LENGTH = 6
+MAX_PLAYERS = 3
+VALID_CLASSES = {"kael", "seris", "aldric"}
+
+
+@dataclass
+class LobbyPlayer:
+ conn_id: str
+ username: str
+ player_class: str | None = None
+ is_ready: bool = False
+ is_host: bool = False
+
+ def to_dict(self) -> dict:
+ return {
+ "id": self.conn_id,
+ "username": self.username,
+ "class": self.player_class,
+ "ready": self.is_ready,
+ "host": self.is_host,
+ }
+
+
+@dataclass
+class Lobby:
+ code: str
+ players: list[LobbyPlayer] = field(default_factory=list)
+ started: bool = False
+
+ def get_conn_ids(self) -> list[str]:
+ return [p.conn_id for p in self.players]
+
+ def get_player(self, conn_id):
+ return next((p for p in self.players if p.conn_id == conn_id), None)
+
+ def is_class_taken(self, class_name):
+ return any(p.player_class == class_name for p in self.players)
+
+ def all_ready(self):
+ return (
+ len(self.players) == MAX_PLAYERS
+ and all(p.is_ready and p.player_class for p in self.players)
+ )
+
+
+class LobbyManager:
+ def __init__(self):
+ self.lobbies: dict[str, Lobby] = {}
+ self.conn_to_lobby: dict[str, str] = {}
+
+ def _generate_code(self):
+ while True:
+ code = "".join(random.choices(_LOBBY_CODE_CHARS, k=_LOBBY_CODE_LENGTH))
+ if code not in self.lobbies:
+ return code
+
+ def _error(self, conn_id, code, message):
+ return [(conn_id, {"type": "error", "code": code, "message": message})]
+
+ def create_lobby(self, conn_id, username):
+ # si on est deja dans un lobby on le libere d'abord, evite les blocages au retour menu
+ leave_msgs = []
+ if conn_id in self.conn_to_lobby:
+ leave_msgs = self.disconnect(conn_id)
+
+ code = self._generate_code()
+ player = LobbyPlayer(conn_id=conn_id, username=username, is_host=True)
+ lobby = Lobby(code=code, players=[player])
+
+ self.lobbies[code] = lobby
+ self.conn_to_lobby[conn_id] = code
+
+ logger.info("Lobby %s créé par %s", code, username)
+ return leave_msgs + [(conn_id, {"type": "lobby_created", "code": code})]
+
+ def join_lobby(self, conn_id, username, code):
+ code = code.upper()
+
+ leave_msgs = []
+ if conn_id in self.conn_to_lobby:
+ leave_msgs = self.disconnect(conn_id)
+
+ lobby = self.lobbies.get(code)
+ if not lobby:
+ return self._error(conn_id, "lobby_not_found", f"Lobby {code} introuvable.")
+ if lobby.started:
+ return self._error(conn_id, "game_in_progress", "La partie a déjà commencé.")
+ if len(lobby.players) >= MAX_PLAYERS:
+ return self._error(conn_id, "lobby_full", "Le lobby est plein (3/3).")
+
+ player = LobbyPlayer(conn_id=conn_id, username=username)
+ lobby.players.append(player)
+ self.conn_to_lobby[conn_id] = code
+
+ messages = [
+ (conn_id, {"type": "lobby_joined", "code": code, "players": [p.to_dict() for p in lobby.players]}),
+ ]
+ # notifier les autres
+ for p in lobby.players:
+ if p.conn_id != conn_id:
+ messages.append((p.conn_id, {"type": "player_joined", "player": player.to_dict()}))
+
+ logger.info("%s a rejoint le lobby %s (%d/3)", username, code, len(lobby.players))
+ return leave_msgs + messages
+
+ def select_class(self, conn_id, class_name):
+ code = self.conn_to_lobby.get(conn_id)
+ if not code:
+ return self._error(conn_id, "not_in_lobby", "Tu n'es pas dans un lobby.")
+
+ lobby = self.lobbies[code]
+ player = lobby.get_player(conn_id)
+ if not player:
+ return self._error(conn_id, "not_in_lobby", "Joueur introuvable dans le lobby.")
+ if class_name not in VALID_CLASSES:
+ return self._error(conn_id, "invalid_class", f"Classe invalide : {class_name}.")
+ # un joueur peut reselectionner sa propre classe sans erreur
+ if lobby.is_class_taken(class_name) and player.player_class != class_name:
+ return self._error(conn_id, "class_taken", f"La classe {class_name} est déjà prise.")
+
+ player.player_class = class_name
+ player.is_ready = False # reset du ready si on change de classe
+
+ logger.info("%s a choisi la classe %s", player.username, class_name)
+ return [(p.conn_id, {"type": "class_selected", "player_id": conn_id, "class": class_name}) for p in lobby.players]
+
+ def set_ready(self, conn_id):
+ code = self.conn_to_lobby.get(conn_id)
+ if not code:
+ return self._error(conn_id, "not_in_lobby", "Tu n'es pas dans un lobby.")
+
+ lobby = self.lobbies[code]
+ player = lobby.get_player(conn_id)
+ if not player:
+ return self._error(conn_id, "not_in_lobby", "Joueur introuvable.")
+ if not player.player_class:
+ return self._error(conn_id, "no_class_selected", "Choisis une classe avant de te mettre prêt.")
+
+ player.is_ready = True
+ logger.info("%s est prêt", player.username)
+ return [(p.conn_id, {"type": "player_ready", "player_id": conn_id}) for p in lobby.players]
+
+ def start_game(self, conn_id):
+ code = self.conn_to_lobby.get(conn_id)
+ if not code:
+ return self._error(conn_id, "not_in_lobby", "Tu n'es pas dans un lobby.")
+
+ lobby = self.lobbies[code]
+ player = lobby.get_player(conn_id)
+ if not player or not player.is_host:
+ return self._error(conn_id, "not_host", "Seul le chef de partie peut démarrer.")
+ if not lobby.all_ready():
+ return self._error(
+ conn_id, "not_all_ready",
+ f"Tous les joueurs doivent être prêts avec une classe ({len(lobby.players)}/3).",
+ )
+
+ lobby.started = True
+ logger.info("Partie démarrée dans le lobby %s", code)
+ return [(p.conn_id, {"type": "game_starting", "countdown": 3}) for p in lobby.players]
+
+ def leave_lobby(self, conn_id):
+ # bouton Back : on quitte volontairement, meme logique que disconnect + confirmation
+ if conn_id not in self.conn_to_lobby:
+ return []
+ notifications = self.disconnect(conn_id)
+ notifications.insert(0, (conn_id, {"type": "lobby_left"}))
+ return notifications
+
+ def disconnect(self, conn_id):
+ code = self.conn_to_lobby.pop(conn_id, None)
+ if not code:
+ return []
+
+ lobby = self.lobbies.get(code)
+ if not lobby:
+ return []
+
+ player = lobby.get_player(conn_id)
+ if not player:
+ return []
+
+ lobby.players = [p for p in lobby.players if p.conn_id != conn_id]
+ logger.info("%s a quitté le lobby %s", player.username, code)
+
+ if not lobby.players:
+ del self.lobbies[code]
+ logger.info("Lobby %s supprimé (vide)", code)
+ return []
+
+ # transferer l'host au premier restant
+ if player.is_host:
+ lobby.players[0].is_host = True
+
+ return [(p.conn_id, {"type": "player_left", "player_id": conn_id}) for p in lobby.players]
diff --git a/server/main.py b/server/main.py
new file mode 100644
index 0000000..5168d59
--- /dev/null
+++ b/server/main.py
@@ -0,0 +1,243 @@
+# main.py : point d'entree FastAPI (routes + websocket + lancement game loop)
+
+import asyncio
+import logging
+import os
+from contextlib import asynccontextmanager
+
+from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+
+from game_loop import GameLoop
+from lobby import Lobby, LobbyManager
+from stats import get_leaderboard, init_db, is_discord_taken, save_result
+from websocket import ConnectionManager
+
+LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
+logging.basicConfig(
+ level=getattr(logging, LOG_LEVEL, logging.INFO),
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ await init_db()
+ logger.info("Base de données initialisée")
+ yield
+
+
+app = FastAPI(title="SOULGATE", lifespan=lifespan)
+app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
+
+manager = ConnectionManager()
+lobby_manager = LobbyManager()
+active_games: dict[str, tuple[GameLoop, asyncio.Task]] = {}
+
+logger.info("Serveur SOULGATE démarré")
+
+
+# --- routes leaderboard ---
+
+class SubmitPayload(BaseModel):
+ team_name: str
+ score: int
+ time_seconds: int
+ waves_completed: int
+ victory: bool
+ lobby_code: str = ""
+ submitter_id: str = ""
+ p1_username: str = ""
+ p1_class: str = ""
+ p1_discord: str = ""
+ p2_username: str = ""
+ p2_class: str = ""
+ p2_discord: str = ""
+ p3_username: str = ""
+ p3_class: str = ""
+ p3_discord: str = ""
+
+
+# lobbies dont le score a deja ete soumis : empeche les doublons
+_submitted_lobbies: set[str] = set()
+
+
+@app.get("/leaderboard")
+async def route_leaderboard() -> list[dict]:
+ return await get_leaderboard()
+
+
+@app.get("/leaderboard/check-discord/{tag}")
+async def route_check_discord(tag: str) -> dict:
+ taken = await is_discord_taken(tag)
+ return {"taken": taken}
+
+
+@app.post("/leaderboard/submit")
+async def route_submit(payload: SubmitPayload) -> dict:
+ if not payload.team_name.strip():
+ raise HTTPException(400, "Le nom d'équipe est obligatoire.")
+
+ # une defaite n'a rien a faire dans le leaderboard
+ if not payload.victory:
+ raise HTTPException(403, "Seules les victoires peuvent être enregistrées.")
+
+ # seul le host peut soumettre, et 1 seule fois par lobby
+ code = payload.lobby_code.upper().strip()
+ submitter = payload.submitter_id.strip()
+ if not code or not submitter:
+ raise HTTPException(400, "Identifiants de session manquants.")
+ if code in _submitted_lobbies:
+ raise HTTPException(409, "Le score de cette partie a déjà été enregistré.")
+ lobby = lobby_manager.lobbies.get(code)
+ if not lobby:
+ raise HTTPException(404, "Lobby introuvable ou déjà nettoyé.")
+ player = lobby.get_player(submitter)
+ if not player or not player.is_host:
+ raise HTTPException(403, "Seul le chef d'équipe peut enregistrer le score.")
+
+ # pas de doublon de discord
+
+ for tag in (payload.p1_discord, payload.p2_discord, payload.p3_discord):
+ if tag and await is_discord_taken(tag):
+ raise HTTPException(409, f"Le Discord '{tag}' est déjà dans le leaderboard.")
+
+ rank = await save_result(payload.model_dump(exclude={"lobby_code", "submitter_id"}))
+ _submitted_lobbies.add(code)
+ return {"rank": rank}
+
+
+@app.get("/health")
+async def health() -> dict:
+ return {"status": "ok"}
+
+
+@app.websocket("/ws")
+async def websocket_endpoint(websocket: WebSocket) -> None:
+ conn_id = await manager.connect(websocket)
+ try:
+ while True:
+ try:
+ data = await websocket.receive_json()
+ except WebSocketDisconnect:
+ raise
+ except Exception:
+ try:
+ await manager.send(conn_id, {"type": "error", "code": "invalid_json", "message": "Message JSON invalide."})
+ except Exception:
+ pass
+ continue
+ await _handle_message(conn_id, data.get("type", ""), data)
+ except WebSocketDisconnect:
+ messages = lobby_manager.disconnect(conn_id)
+ await manager.dispatch(messages)
+ manager.disconnect(conn_id)
+
+
+async def _handle_message(conn_id: str, msg_type: str, data: dict) -> None:
+ username = manager.get_username(conn_id)
+
+ if msg_type == "set_username":
+ uname = str(data.get("username", "")).strip()
+ if not uname:
+ await manager.send(conn_id, {"type": "error", "code": "invalid_username", "message": "Le pseudo ne peut pas être vide."})
+ return
+ manager.set_username(conn_id, uname)
+ await manager.send(conn_id, {"type": "username_set", "username": uname, "my_id": conn_id})
+
+ elif msg_type in ("create_lobby", "join_lobby"):
+ if not username:
+ await manager.send(conn_id, {"type": "error", "code": "no_username", "message": "Définis ton pseudo d'abord (set_username)."})
+ return
+ if msg_type == "create_lobby":
+ messages = lobby_manager.create_lobby(conn_id, username)
+ else:
+ messages = lobby_manager.join_lobby(conn_id, username, data.get("code", ""))
+ await manager.dispatch(messages)
+
+ elif msg_type == "leave_lobby":
+ await manager.dispatch(lobby_manager.leave_lobby(conn_id))
+
+ elif msg_type == "select_class":
+ await manager.dispatch(lobby_manager.select_class(conn_id, data.get("class", "")))
+
+ elif msg_type == "ready":
+ await manager.dispatch(lobby_manager.set_ready(conn_id))
+
+ elif msg_type == "start_game":
+ messages = lobby_manager.start_game(conn_id)
+ await manager.dispatch(messages)
+ if messages and messages[0][1].get("type") == "game_starting":
+ code = lobby_manager.conn_to_lobby.get(conn_id)
+ if code and code in lobby_manager.lobbies:
+ await _launch_game(lobby_manager.lobbies[code])
+
+ elif msg_type == "input":
+ code = lobby_manager.conn_to_lobby.get(conn_id)
+ if code and code in active_games:
+ game, _ = active_games[code]
+ try:
+ dx = float(data.get("dx", 0))
+ dy = float(data.get("dy", 0))
+ except (ValueError, TypeError):
+ return
+ game.handle_input(conn_id, dx, dy)
+
+ elif msg_type == "attack":
+ code = lobby_manager.conn_to_lobby.get(conn_id)
+ if code and code in active_games:
+ game, _ = active_games[code]
+ try:
+ tx = float(data.get("tx", 0))
+ ty = float(data.get("ty", 0))
+ except (ValueError, TypeError):
+ return
+ game.handle_attack(conn_id, tx, ty)
+
+ elif msg_type == "ability":
+ code = lobby_manager.conn_to_lobby.get(conn_id)
+ if code and code in active_games:
+ game, _ = active_games[code]
+ try:
+ ability_id = int(data.get("id", 0))
+ tx = float(data.get("tx", 0))
+ ty = float(data.get("ty", 0))
+ except (ValueError, TypeError):
+ return
+ if ability_id in (1, 2, 3):
+ game.handle_ability(conn_id, ability_id, tx, ty)
+
+ elif msg_type == "player_upgrade":
+ code = lobby_manager.conn_to_lobby.get(conn_id)
+ if code and code in active_games:
+ game, _ = active_games[code]
+ game.handle_upgrade(conn_id, data.get("upgrade_id", ""))
+
+ elif msg_type == "displacement":
+ code = lobby_manager.conn_to_lobby.get(conn_id)
+ if code and code in active_games:
+ game, _ = active_games[code]
+ try:
+ tx = float(data.get("tx", 0))
+ ty = float(data.get("ty", 0))
+ except (ValueError, TypeError):
+ return
+ game.handle_displacement(conn_id, tx, ty)
+
+
+ else:
+ logger.warning("Message inconnu de %s : %s", conn_id, msg_type)
+ await manager.send(conn_id, {"type": "error", "code": "unknown_message", "message": f"Type de message inconnu : {msg_type}"})
+
+
+async def _launch_game(lobby: Lobby) -> None:
+ async def broadcast(msg: dict) -> None:
+ for conn_id in lobby.get_conn_ids():
+ await manager.send(conn_id, msg)
+
+ game = GameLoop(lobby, broadcast)
+ task = asyncio.create_task(game.run())
+ active_games[lobby.code] = (game, task)
+ logger.info("Game loop lancée — lobby %s", lobby.code)
diff --git a/server/pyproject.toml b/server/pyproject.toml
new file mode 100644
index 0000000..f0e4c8a
--- /dev/null
+++ b/server/pyproject.toml
@@ -0,0 +1,3 @@
+[tool.pytest.ini_options]
+pythonpath = ["."]
+testpaths = ["tests"]
diff --git a/server/requirements.txt b/server/requirements.txt
new file mode 100644
index 0000000..73f2efe
--- /dev/null
+++ b/server/requirements.txt
@@ -0,0 +1,50 @@
+aiosqlite==0.21.0
+annotated-doc==0.0.4
+annotated-types==0.7.0
+anyio==4.12.1
+certifi==2026.1.4
+click==8.3.1
+dnspython==2.8.0
+email-validator==2.3.0
+fastapi==0.133.0
+fastapi-cli==0.0.24
+fastapi-cloud-cli==0.13.0
+fastar==0.8.0
+h11==0.16.0
+httpcore==1.0.9
+httptools==0.7.1
+httpx==0.28.1
+idna==3.11
+iniconfig==2.3.0
+Jinja2==3.1.6
+lxml==6.0.2
+markdown-it-py==4.0.0
+MarkupSafe==3.0.3
+mdurl==0.1.2
+packaging==26.0
+pillow==12.2.0
+pluggy==1.6.0
+pydantic==2.12.5
+pydantic-extra-types==2.11.0
+pydantic-settings==2.13.1
+pydantic_core==2.41.5
+Pygments==2.19.2
+pytest==9.0.2
+python-docx==1.2.0
+python-dotenv==1.2.1
+python-multipart==0.0.22
+PyYAML==6.0.3
+rich==14.3.3
+rich-toolkit==0.19.7
+rignore==0.7.6
+sentry-sdk==2.53.0
+shellingham==1.5.4
+starlette==0.52.1
+typer==0.24.1
+typing-inspection==0.4.2
+typing_extensions==4.15.0
+urllib3==2.6.3
+uvicorn==0.41.0
+uvloop==0.22.1
+watchfiles==1.1.1
+websockets==16.0
diff --git a/server/stats.py b/server/stats.py
new file mode 100644
index 0000000..f53b77f
--- /dev/null
+++ b/server/stats.py
@@ -0,0 +1,80 @@
+"""Leaderboard — stockage SQLite et requêtes."""
+
+import aiosqlite
+from pathlib import Path
+
+DB_PATH = str(Path(__file__).parent / "soulgate.db")
+
+_CREATE = """
+CREATE TABLE IF NOT EXISTS game_results (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ team_name TEXT NOT NULL,
+ score INTEGER NOT NULL,
+ time_seconds INTEGER NOT NULL,
+ waves_completed INTEGER NOT NULL,
+ victory INTEGER NOT NULL DEFAULT 0,
+ p1_username TEXT, p1_class TEXT, p1_discord TEXT,
+ p2_username TEXT, p2_class TEXT, p2_discord TEXT,
+ p3_username TEXT, p3_class TEXT, p3_discord TEXT,
+ played_at TEXT NOT NULL DEFAULT (datetime('now'))
+)
+"""
+
+
+async def init_db() -> None:
+ async with aiosqlite.connect(DB_PATH) as db:
+ await db.execute(_CREATE)
+ await db.commit()
+
+
+async def is_discord_taken(tag: str) -> bool:
+ """Retourne True si ce tag Discord est déjà enregistré dans le leaderboard."""
+ if not tag:
+ return False
+ async with aiosqlite.connect(DB_PATH) as db:
+ async with db.execute(
+ "SELECT COUNT(*) FROM game_results WHERE p1_discord=? OR p2_discord=? OR p3_discord=?",
+ (tag, tag, tag),
+ ) as cur:
+ row = await cur.fetchone()
+ return bool(row and row[0] > 0)
+
+
+async def save_result(data: dict) -> int:
+ """Sauvegarde un résultat de partie. Retourne le rang (1 = meilleur score)."""
+ async with aiosqlite.connect(DB_PATH) as db:
+ await db.execute(
+ """INSERT INTO game_results
+ (team_name, score, time_seconds, waves_completed, victory,
+ p1_username, p1_class, p1_discord,
+ p2_username, p2_class, p2_discord,
+ p3_username, p3_class, p3_discord)
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
+ (
+ data["team_name"],
+ int(data["score"]),
+ int(data["time_seconds"]),
+ int(data["waves_completed"]),
+ 1 if data.get("victory") else 0,
+ data.get("p1_username"), data.get("p1_class"), data.get("p1_discord") or None,
+ data.get("p2_username"), data.get("p2_class"), data.get("p2_discord") or None,
+ data.get("p3_username"), data.get("p3_class"), data.get("p3_discord") or None,
+ ),
+ )
+ await db.commit()
+ async with db.execute(
+ "SELECT COUNT(*) FROM game_results WHERE score > ?", (int(data["score"]),)
+ ) as cur:
+ row = await cur.fetchone()
+ return (row[0] if row else 0) + 1 # rang 1-based
+
+
+async def get_leaderboard(limit: int = 20) -> list[dict]:
+ """Retourne les `limit` meilleures parties par score décroissant."""
+ async with aiosqlite.connect(DB_PATH) as db:
+ db.row_factory = aiosqlite.Row
+ async with db.execute(
+ "SELECT * FROM game_results ORDER BY score DESC LIMIT ?", (limit,)
+ ) as cur:
+ rows = await cur.fetchall()
+ return [dict(r) for r in rows]
diff --git a/server/tests/__init__.py b/server/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/server/tests/__pycache__/__init__.cpython-313.pyc b/server/tests/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..038d189
Binary files /dev/null and b/server/tests/__pycache__/__init__.cpython-313.pyc differ
diff --git a/server/tests/__pycache__/test_boss.cpython-313-pytest-9.0.2.pyc b/server/tests/__pycache__/test_boss.cpython-313-pytest-9.0.2.pyc
new file mode 100644
index 0000000..3622c0a
Binary files /dev/null and b/server/tests/__pycache__/test_boss.cpython-313-pytest-9.0.2.pyc differ
diff --git a/server/tests/__pycache__/test_combat.cpython-313-pytest-9.0.2.pyc b/server/tests/__pycache__/test_combat.cpython-313-pytest-9.0.2.pyc
new file mode 100644
index 0000000..31fdf0a
Binary files /dev/null and b/server/tests/__pycache__/test_combat.cpython-313-pytest-9.0.2.pyc differ
diff --git a/server/tests/__pycache__/test_displacement.cpython-313-pytest-9.0.2.pyc b/server/tests/__pycache__/test_displacement.cpython-313-pytest-9.0.2.pyc
new file mode 100644
index 0000000..13ceb7c
Binary files /dev/null and b/server/tests/__pycache__/test_displacement.cpython-313-pytest-9.0.2.pyc differ
diff --git a/server/tests/__pycache__/test_game_state.cpython-313-pytest-9.0.2.pyc b/server/tests/__pycache__/test_game_state.cpython-313-pytest-9.0.2.pyc
new file mode 100644
index 0000000..f34cdb6
Binary files /dev/null and b/server/tests/__pycache__/test_game_state.cpython-313-pytest-9.0.2.pyc differ
diff --git a/server/tests/__pycache__/test_health.cpython-313-pytest-9.0.2.pyc b/server/tests/__pycache__/test_health.cpython-313-pytest-9.0.2.pyc
new file mode 100644
index 0000000..b6ee1d4
Binary files /dev/null and b/server/tests/__pycache__/test_health.cpython-313-pytest-9.0.2.pyc differ
diff --git a/server/tests/__pycache__/test_lobby.cpython-313-pytest-9.0.2.pyc b/server/tests/__pycache__/test_lobby.cpython-313-pytest-9.0.2.pyc
new file mode 100644
index 0000000..bf19a3a
Binary files /dev/null and b/server/tests/__pycache__/test_lobby.cpython-313-pytest-9.0.2.pyc differ
diff --git a/server/tests/__pycache__/test_waves.cpython-313-pytest-9.0.2.pyc b/server/tests/__pycache__/test_waves.cpython-313-pytest-9.0.2.pyc
new file mode 100644
index 0000000..200c550
Binary files /dev/null and b/server/tests/__pycache__/test_waves.cpython-313-pytest-9.0.2.pyc differ
diff --git a/server/tests/__pycache__/test_websocket.cpython-313-pytest-9.0.2.pyc b/server/tests/__pycache__/test_websocket.cpython-313-pytest-9.0.2.pyc
new file mode 100644
index 0000000..2fe2690
Binary files /dev/null and b/server/tests/__pycache__/test_websocket.cpython-313-pytest-9.0.2.pyc differ
diff --git a/server/tests/test_boss.py b/server/tests/test_boss.py
new file mode 100644
index 0000000..03cb7a3
--- /dev/null
+++ b/server/tests/test_boss.py
@@ -0,0 +1,142 @@
+# tests boss : vexaris + morveth + transitions
+
+from unittest.mock import AsyncMock
+
+from lobby import Lobby, LobbyPlayer
+from game_loop import GameLoop
+from constants import (
+ VEXARIS_HP, VEXARIS_HITBOX_RADIUS,
+ MORVETH_HP,
+ WAVE_BOSSES, SOUL_REWARDS,
+)
+from waves import WaveManager
+from constants import PREPARATION_DURATION, TICK_DURATION
+
+
+def make_lobby():
+ lobby = Lobby("TEST1", "p1")
+ lobby.players = [
+ LobbyPlayer("p1", "Alice", "kael"),
+ LobbyPlayer("p2", "Bob", "seris"),
+ LobbyPlayer("p3", "Clara", "aldric"),
+ ]
+ return lobby
+
+
+def _ticks(wm, n, enemy_count=0):
+ events = []
+ for _ in range(n):
+ events.extend(wm.tick(lambda t: None, enemy_count))
+ return events
+
+
+def _complete_wave_phases(wm):
+ for _ in range(200_000):
+ wm.tick(lambda t: None, 0)
+ if wm.state != "combat":
+ return
+ raise AssertionError("vague combat infinie")
+
+
+def _prep_ticks():
+ return int(PREPARATION_DURATION / TICK_DURATION) + 1
+
+
+def _skip_initial_prep(wm):
+ while wm.state == "preparation":
+ wm.tick(lambda t: None, 0)
+
+
+def test_wave_bosses():
+ assert WAVE_BOSSES[2] == "vexaris"
+ assert WAVE_BOSSES[3] == "morveth"
+ assert 1 not in WAVE_BOSSES
+
+
+def test_spawn_vexaris():
+ loop = GameLoop(make_lobby(), AsyncMock())
+ loop.spawn_enemy("vexaris")
+ boss = loop.state.enemies[0]
+ assert boss.type == "vexaris"
+ assert boss.hp == VEXARIS_HP
+ assert boss.is_boss is True
+
+
+def test_boss_tape_joueur_au_contact():
+ loop = GameLoop(make_lobby(), AsyncMock())
+ loop.spawn_enemy("vexaris")
+ boss = loop.state.enemies[0]
+ player = loop.state.players[0]
+ boss.x, boss.y = player.x, player.y
+ hp_initial = player.hp
+ loop._update_enemies()
+ assert player.hp < hp_initial
+
+
+def test_boss_kill_donne_des_ames():
+ loop = GameLoop(make_lobby(), AsyncMock())
+ loop.spawn_enemy("vexaris")
+ boss = loop.state.enemies[0]
+ player = loop.state.players[0]
+ initial_souls = player.souls
+ from game_state import ProjectileState
+ loop.state.projectiles.append(ProjectileState(
+ id="pr_test", owner_id=player.id,
+ x=boss.x, y=boss.y, vx=0, vy=0,
+ damage=VEXARIS_HP, radius=VEXARIS_HITBOX_RADIUS + 1, ttl=1.0,
+ ))
+ loop._update_projectiles()
+ assert player.souls == initial_souls + SOUL_REWARDS["vexaris"]
+
+
+def test_spawn_morveth():
+ loop = GameLoop(make_lobby(), AsyncMock())
+ loop.spawn_enemy("morveth")
+ boss = loop.state.enemies[0]
+ assert boss.type == "morveth"
+ assert boss.hp == MORVETH_HP
+
+
+def _morveth_at_pct(pct):
+ loop = GameLoop(make_lobby(), AsyncMock())
+ loop.spawn_enemy("morveth")
+ boss = loop.state.enemies[0]
+ boss.hp = max(1, int(boss.max_hp * pct))
+ boss.data.update({
+ "charge_cd": 99.0, "charge_timer": 0.0,
+ "charge_tx": 0.0, "charge_ty": 0.0,
+ "is_charging": False,
+ "burst_cd": 99.0,
+ "general_1_spawned": False,
+ "general_2_spawned": False,
+ "general_3_spawned": False,
+ })
+ return loop
+
+
+def test_morveth_invoque_general_a_75():
+ loop = _morveth_at_pct(0.74)
+ loop._update_enemies()
+ generals = [e for e in loop.state.enemies if e.type == "general"]
+ assert len(generals) == 1
+
+
+def test_morveth_pas_de_double_invocation():
+ loop = _morveth_at_pct(0.74)
+ loop._update_enemies()
+ loop._update_enemies()
+ generals = [e for e in loop.state.enemies if e.type == "general"]
+ assert len(generals) == 1
+
+
+def test_victory_apres_morveth():
+ wm = WaveManager()
+ _skip_initial_prep(wm)
+ _complete_wave_phases(wm)
+ _ticks(wm, _prep_ticks())
+ _complete_wave_phases(wm)
+ _ticks(wm, 3) # Vexaris spawn + kill
+ _ticks(wm, _prep_ticks())
+ _complete_wave_phases(wm)
+ _ticks(wm, 3) # Morveth spawn + kill
+ assert wm.state == "victory"
diff --git a/server/tests/test_combat.py b/server/tests/test_combat.py
new file mode 100644
index 0000000..3eaf3fe
--- /dev/null
+++ b/server/tests/test_combat.py
@@ -0,0 +1,74 @@
+# tests combat : hitbox, cooldown, projectile
+
+import math
+from unittest.mock import AsyncMock
+
+import pytest
+
+from combat import circle_overlap, normalize
+from constants import ATTACK_COOLDOWN, TICK_DURATION
+from game_loop import GameLoop
+from lobby import Lobby, LobbyPlayer
+
+
+def make_game_loop(class_name="seris"):
+ player = LobbyPlayer(conn_id="c1", username="Test", player_class=class_name, is_ready=True, is_host=True)
+ lobby = Lobby(code="TEST01", players=[player], started=True)
+ return GameLoop(lobby, AsyncMock())
+
+
+def test_circle_overlap_overlapping():
+ assert circle_overlap(0, 0, 1, 0.5, 0, 1) is True
+
+def test_circle_overlap_far_apart():
+ assert circle_overlap(0, 0, 0.5, 10, 10, 0.5) is False
+
+
+def test_normalize_diagonal():
+ dx, dy = normalize(1, 1)
+ assert dx == pytest.approx(1 / math.sqrt(2))
+ assert dy == pytest.approx(1 / math.sqrt(2))
+
+def test_normalize_zero():
+ assert normalize(0, 0) == (0.0, 0.0)
+
+
+def test_cooldown_decroit():
+ loop = make_game_loop()
+ loop.state.players[0].cooldowns["attack"] = 0.5
+ loop._update_cooldowns()
+ assert loop.state.players[0].cooldowns["attack"] == pytest.approx(0.5 - TICK_DURATION)
+
+
+def test_handle_attack_cree_projectile():
+ loop = make_game_loop()
+ p = loop.state.players[0]
+ loop.handle_attack(p.id, p.x + 5, p.y)
+ assert len(loop.state.projectiles) == 1
+ assert p.cooldowns["attack"] == pytest.approx(ATTACK_COOLDOWN)
+
+
+def test_handle_attack_bloque_par_cd():
+ loop = make_game_loop()
+ p = loop.state.players[0]
+ p.cooldowns["attack"] = 0.5
+ loop.handle_attack(p.id, p.x + 5, p.y)
+ assert len(loop.state.projectiles) == 0
+
+
+def test_projectile_se_deplace():
+ loop = make_game_loop()
+ p = loop.state.players[0]
+ loop.handle_attack(p.id, p.x + 5, p.y)
+ old_x = loop.state.projectiles[0].x
+ loop._update_projectiles()
+ assert loop.state.projectiles[0].x != old_x
+
+
+def test_projectile_disparait_quand_ttl_zero():
+ loop = make_game_loop()
+ p = loop.state.players[0]
+ loop.handle_attack(p.id, p.x + 5, p.y)
+ loop.state.projectiles[0].ttl = 0.01
+ loop._update_projectiles()
+ assert len(loop.state.projectiles) == 0
diff --git a/server/tests/test_displacement.py b/server/tests/test_displacement.py
new file mode 100644
index 0000000..be7f378
--- /dev/null
+++ b/server/tests/test_displacement.py
@@ -0,0 +1,78 @@
+# tests touche E (Kael / Seris / Aldric)
+
+from unittest.mock import AsyncMock
+
+from lobby import Lobby, LobbyPlayer
+from game_loop import GameLoop
+from game_state import EnemyState
+from constants import (
+ DISPLACEMENT_COOLDOWNS,
+ KAEL_CHARGE_DISTANCE,
+ SERIS_INTANGIBLE_DURATION,
+ ALDRIC_FLYING_DURATION,
+ ENEMY_STATS,
+)
+
+
+def make_game():
+ classes = [("p1", "Alice", "kael"), ("p2", "Bob", "seris"), ("p3", "Clara", "aldric")]
+ lobby = Lobby("TEST1", classes[0][0])
+ lobby.players = [LobbyPlayer(cid, uname, cls) for cid, uname, cls in classes]
+ return GameLoop(lobby, AsyncMock())
+
+
+def add_enemy(game, etype="fracture", x=0.0, y=5.0):
+ stats = ENEMY_STATS[etype]
+ game._enemy_counter += 1
+ e = EnemyState(id=f"en_{game._enemy_counter}", type=etype, x=x, y=y, hp=stats["hp"], max_hp=stats["hp"])
+ game.state.enemies.append(e)
+ return e
+
+
+def test_kael_charge():
+ game = make_game()
+ p = game.state.players[0]
+ p.x, p.y = 0.0, 0.0
+ game.handle_displacement("p1", 10.0, 0.0)
+ assert abs(p.x - KAEL_CHARGE_DISTANCE) < 0.01
+ assert p.cooldowns["displacement"] == DISPLACEMENT_COOLDOWNS["kael"]
+
+
+def test_kael_charge_bloque_par_cd():
+ game = make_game()
+ p = game.state.players[0]
+ p.x, p.y = 0.0, 0.0
+ p.cooldowns["displacement"] = 3.0
+ game.handle_displacement("p1", 10.0, 0.0)
+ assert p.x == 0.0
+
+
+def test_seris_teleport():
+ game = make_game()
+ p = game.state.players[1]
+ p.x, p.y = 0.0, 0.0
+ game.handle_displacement("p2", 5.0, 3.0)
+ assert abs(p.x - 5.0) < 0.01
+ assert abs(p.y - 3.0) < 0.01
+ assert any(b["type"] == "intangible" for b in p.buffs)
+
+
+def test_seris_intangible_immunise():
+ game = make_game()
+ p = game.state.players[1]
+ p.x, p.y = 0.0, 0.0
+ p.hp = p.max_hp
+ p.buffs.append({"type": "intangible", "timer": SERIS_INTANGIBLE_DURATION})
+ e = add_enemy(game, "fracture", x=0.0, y=0.0)
+ e.attack_cooldown = 0.0
+ game._update_enemies()
+ assert p.hp == p.max_hp
+
+
+def test_aldric_vol():
+ game = make_game()
+ p = game.state.players[2]
+ game.handle_displacement("p3", 0.0, 0.0)
+ flying = next((b for b in p.buffs if b["type"] == "flying"), None)
+ assert flying is not None
+ assert abs(flying["timer"] - ALDRIC_FLYING_DURATION) < 0.001
diff --git a/server/tests/test_game_state.py b/server/tests/test_game_state.py
new file mode 100644
index 0000000..86e605d
--- /dev/null
+++ b/server/tests/test_game_state.py
@@ -0,0 +1,59 @@
+# tests game state + game loop init
+
+from unittest.mock import AsyncMock
+
+from game_loop import GameLoop
+from game_state import GameState, PlayerState, SoulgateState, WaveState
+from lobby import Lobby, LobbyPlayer
+
+
+def make_lobby():
+ lobby = Lobby(code="TEST01")
+ lobby.players = [
+ LobbyPlayer(conn_id="c1", username="Alice", player_class="kael", is_ready=True, is_host=True),
+ LobbyPlayer(conn_id="c2", username="Bob", player_class="seris", is_ready=True),
+ LobbyPlayer(conn_id="c3", username="Charlie", player_class="aldric", is_ready=True),
+ ]
+ return lobby
+
+
+def test_game_state_to_dict():
+ state = GameState(
+ tick=5, players=[], enemies=[], projectiles=[],
+ soulgate=SoulgateState(hp=80, max_hp=100),
+ wave=WaveState(number=2, phase=1, enemies_remaining=3, state="combat"),
+ effects=[],
+ )
+ d = state.to_dict()
+ assert d["type"] == "game_state"
+ assert d["tick"] == 5
+ for key in ("players", "enemies", "projectiles", "soulgate", "wave"):
+ assert key in d
+
+
+def test_player_state_to_dict():
+ p = PlayerState(id="c1", class_name="kael", username="Alice", x=0.0, y=-10.0, hp=150, max_hp=150, alive=True)
+ d = p.to_dict()
+ assert d["class"] == "kael"
+ assert d["hp"] == 150
+ assert "attack" in d["cooldowns"]
+
+
+def test_game_loop_3_joueurs():
+ loop = GameLoop(make_lobby(), AsyncMock())
+ assert len(loop.state.players) == 3
+
+
+def test_hp_par_classe():
+ loop = GameLoop(make_lobby(), AsyncMock())
+ by_class = {p.class_name: p.max_hp for p in loop.state.players}
+ assert by_class["kael"] == 150
+ assert by_class["seris"] == 80
+ assert by_class["aldric"] == 100
+
+
+def test_tick_incremente():
+ loop = GameLoop(make_lobby(), AsyncMock())
+ assert loop.state.tick == 0
+ loop._tick()
+ assert loop.state.tick == 1
diff --git a/server/tests/test_health.py b/server/tests/test_health.py
new file mode 100644
index 0000000..34f7509
--- /dev/null
+++ b/server/tests/test_health.py
@@ -0,0 +1,13 @@
+# Tests de base — vérification du serveur FastAPI
+
+from fastapi.testclient import TestClient
+
+from main import app
+
+client = TestClient(app)
+
+
+def test_health() -> None:
+ response = client.get("/health")
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok"}
diff --git a/server/tests/test_lobby.py b/server/tests/test_lobby.py
new file mode 100644
index 0000000..ecc0a41
--- /dev/null
+++ b/server/tests/test_lobby.py
@@ -0,0 +1,100 @@
+# tests lobby
+
+from lobby import LobbyManager
+
+
+def _make():
+ return LobbyManager()
+
+
+def _full_lobby():
+ m = _make()
+ code = m.create_lobby("c1", "Alice")[0][1]["code"]
+ m.join_lobby("c2", "Bob", code)
+ m.join_lobby("c3", "Charlie", code)
+ m.select_class("c1", "kael")
+ m.select_class("c2", "seris")
+ m.select_class("c3", "aldric")
+ m.set_ready("c1")
+ m.set_ready("c2")
+ m.set_ready("c3")
+ return m, code
+
+
+def test_create_lobby():
+ m = _make()
+ msgs = m.create_lobby("c1", "Alice")
+ assert msgs[0][1]["type"] == "lobby_created"
+ assert len(msgs[0][1]["code"]) == 6
+
+
+def test_create_lobby_recreate():
+ # quand on recree, l'ancien est libere (corrige un bug de "Back" qui bloquait)
+ m = _make()
+ code1 = m.create_lobby("c1", "Alice")[0][1]["code"]
+ msgs = m.create_lobby("c1", "Alice")
+ last = next(msg for cid, msg in msgs if cid == "c1" and msg["type"] == "lobby_created")
+ assert last["code"] != code1
+ assert code1 not in m.lobbies
+
+
+def test_join_lobby():
+ m = _make()
+ code = m.create_lobby("c1", "Alice")[0][1]["code"]
+ msgs = m.join_lobby("c2", "Bob", code)
+ types = {msg["type"] for _, msg in msgs}
+ assert "lobby_joined" in types and "player_joined" in types
+
+
+def test_join_lobby_not_found():
+ m = _make()
+ msgs = m.join_lobby("c1", "Alice", "XXXXXX")
+ assert msgs[0][1]["code"] == "lobby_not_found"
+
+
+def test_join_lobby_full():
+ m = _make()
+ code = m.create_lobby("c1", "Alice")[0][1]["code"]
+ m.join_lobby("c2", "Bob", code)
+ m.join_lobby("c3", "Charlie", code)
+ msgs = m.join_lobby("c4", "Dave", code)
+ assert msgs[0][1]["code"] == "lobby_full"
+
+
+def test_class_taken():
+ m = _make()
+ code = m.create_lobby("c1", "Alice")[0][1]["code"]
+ m.join_lobby("c2", "Bob", code)
+ m.select_class("c1", "kael")
+ msgs = m.select_class("c2", "kael")
+ assert msgs[0][1]["code"] == "class_taken"
+
+
+def test_ready_sans_classe():
+ m = _make()
+ m.create_lobby("c1", "Alice")
+ msgs = m.set_ready("c1")
+ assert msgs[0][1]["code"] == "no_class_selected"
+
+
+def test_start_game_not_host():
+ m = _make()
+ code = m.create_lobby("c1", "Alice")[0][1]["code"]
+ m.join_lobby("c2", "Bob", code)
+ msgs = m.start_game("c2")
+ assert msgs[0][1]["code"] == "not_host"
+
+
+def test_start_game_ok():
+ m, _ = _full_lobby()
+ msgs = m.start_game("c1")
+ assert len(msgs) == 3
+ assert all(msg["type"] == "game_starting" for _, msg in msgs)
+
+
+def test_disconnect_transfere_host():
+ m = _make()
+ code = m.create_lobby("c1", "Alice")[0][1]["code"]
+ m.join_lobby("c2", "Bob", code)
+ m.disconnect("c1")
+ assert m.lobbies[code].players[0].is_host is True
diff --git a/server/tests/test_waves.py b/server/tests/test_waves.py
new file mode 100644
index 0000000..ffe4bad
--- /dev/null
+++ b/server/tests/test_waves.py
@@ -0,0 +1,91 @@
+# tests vagues : WaveManager + WAVE_CONFIGS
+
+from waves import WaveManager, WAVE_CONFIGS
+from constants import PREPARATION_DURATION, TICK_DURATION
+
+
+def _ticks(wm, n, enemy_count=0):
+ events = []
+ for _ in range(n):
+ events.extend(wm.tick(lambda t: None, enemy_count))
+ return events
+
+
+def _complete_wave(wm):
+ # avance jusqu'a la fin de la vague (preparation ou victory)
+ for _ in range(100_000):
+ wm.tick(lambda t: None, 0)
+ if wm.state in ("preparation", "victory"):
+ return
+ raise AssertionError("vague jamais finie")
+
+
+def _prep_ticks():
+ return int(PREPARATION_DURATION / TICK_DURATION) + 1
+
+
+def _skip_initial_prep(wm):
+ while wm.state == "preparation":
+ wm.tick(lambda t: None, 0)
+
+
+def test_three_waves():
+ assert len(WAVE_CONFIGS) == 3
+
+
+def test_initial_state():
+ wm = WaveManager()
+ assert wm.wave_number == 1
+ assert wm.state == "preparation"
+
+
+def test_first_spawn():
+ spawned = []
+ wm = WaveManager()
+ _skip_initial_prep(wm)
+ wm.tick(spawned.append, 999)
+ assert len(spawned) >= 1
+ assert spawned[0] == WAVE_CONFIGS[0][0].groups[0].enemy_type
+
+
+def test_phase_avance_quand_zero_ennemis():
+ wm = WaveManager()
+ for _ in range(100_000):
+ if wm.enemies_remaining() == 0:
+ break
+ wm.tick(lambda t: None, 999)
+ wm.tick(lambda t: None, 0)
+ assert wm.phase_number == 2
+
+
+def test_preparation_apres_vague():
+ wm = WaveManager()
+ _complete_wave(wm)
+ assert wm.state == "preparation"
+
+
+def test_pas_de_spawn_en_prep():
+ wm = WaveManager()
+ _complete_wave(wm)
+ spawned = []
+ wm.tick(spawned.append, 0)
+ assert spawned == []
+
+
+def test_vague_2_apres_timer_prep():
+ wm = WaveManager()
+ _skip_initial_prep(wm)
+ _complete_wave(wm)
+ _ticks(wm, _prep_ticks())
+ assert wm.wave_number == 2
+ assert wm.state == "combat"
+
+
+def test_victory_apres_3_vagues():
+ wm = WaveManager()
+ _skip_initial_prep(wm)
+ for _ in range(3):
+ _complete_wave(wm)
+ if wm.state == "preparation":
+ _ticks(wm, _prep_ticks())
+ assert wm.state == "victory"
diff --git a/server/tests/test_websocket.py b/server/tests/test_websocket.py
new file mode 100644
index 0000000..627afef
--- /dev/null
+++ b/server/tests/test_websocket.py
@@ -0,0 +1,30 @@
+# tests integration WebSocket
+
+from fastapi.testclient import TestClient
+
+from main import app
+
+client = TestClient(app)
+
+
+def test_set_username():
+ with client.websocket_connect("/ws") as ws:
+ ws.send_json({"type": "set_username", "username": "Alice"})
+ msg = ws.receive_json()
+ assert msg["type"] == "username_set"
+
+
+def test_create_lobby_sans_pseudo():
+ with client.websocket_connect("/ws") as ws:
+ ws.send_json({"type": "create_lobby"})
+ msg = ws.receive_json()
+ assert msg["code"] == "no_username"
+
+
+def test_create_lobby_ok():
+ with client.websocket_connect("/ws") as ws:
+ ws.send_json({"type": "set_username", "username": "Alice"})
+ ws.receive_json()
+ ws.send_json({"type": "create_lobby"})
+ msg = ws.receive_json()
+ assert msg["type"] == "lobby_created"
diff --git a/server/waves.py b/server/waves.py
new file mode 100644
index 0000000..c2d1dcb
--- /dev/null
+++ b/server/waves.py
@@ -0,0 +1,180 @@
+# waves.py : machine a etats des vagues (combat / boss / preparation / victory)
+
+from dataclasses import dataclass
+
+from constants import PREPARATION_DURATION, TICK_DURATION, WAVE_BOSSES
+
+
+@dataclass
+class SpawnGroup:
+ enemy_type: str # type d'ennemi à spawner : "fracture", "rampant", "colosse", "eclat"
+ count: int # nombre total d'ennemis à spawner dans ce groupe
+ interval: int # ticks entre chaque spawn (ex: 40 ticks = 2s à 20Hz)
+ # 20 Hz = 20 ticks par seconde → 40 ticks = 2 secondes
+
+
+@dataclass
+class PhaseConfig:
+ """Configuration d'une phase : plusieurs groupes d'ennemis spawnent en parallèle."""
+ groups: list[SpawnGroup] # chaque groupe spawn indépendamment selon son interval
+
+
+# Données des vagues
+# WAVE_CONFIGS[i] = liste de PhaseConfig pour la vague i+1
+# Chaque vague a plusieurs phases. Dans chaque phase, plusieurs groupes spawnent en parallèle.
+
+WAVE_CONFIGS: list[list[PhaseConfig]] = [
+
+ # Vague 1 — L'Éveil (5 phases de difficulté croissante)
+ [
+ PhaseConfig([SpawnGroup("fracture", 4, 40)]), # phase 1 : 4 fractures, 1 toutes les 2s
+ PhaseConfig([SpawnGroup("rampant", 6, 30)]), # phase 2 : 6 rampants, 1 toutes les 1.5s
+ PhaseConfig([SpawnGroup("fracture", 4, 30), SpawnGroup("rampant", 3, 60)]), # phase 3 : les deux en même temps
+ PhaseConfig([SpawnGroup("colosse", 2, 80), SpawnGroup("fracture", 4, 40)]), # phase 4 : colosses + fractures
+ PhaseConfig([SpawnGroup("fracture", 6, 20), SpawnGroup("rampant", 6, 20), SpawnGroup("eclat", 4, 15)]), # phase 5 : chaos total
+ ],
+
+ # Vague 2 — Le Héraut (3 phases, puis boss Vexaris)
+ [
+ PhaseConfig([SpawnGroup("fracture", 6, 30), SpawnGroup("rampant", 4, 40)]),
+ PhaseConfig([SpawnGroup("eclat", 8, 15), SpawnGroup("colosse", 2, 80)]),
+ PhaseConfig([SpawnGroup("fracture", 8, 20), SpawnGroup("rampant", 6, 25), SpawnGroup("eclat", 6, 15)]),
+ ],
+
+ # Vague 3 — Morveth (3 phases encore plus dures, puis boss Morveth)
+ [
+ PhaseConfig([SpawnGroup("fracture", 10, 15), SpawnGroup("rampant", 8, 20), SpawnGroup("colosse", 3, 60)]),
+ PhaseConfig([SpawnGroup("eclat", 10, 10), SpawnGroup("fracture", 8, 15), SpawnGroup("colosse", 3, 60)]),
+ PhaseConfig([SpawnGroup("fracture", 12, 10), SpawnGroup("rampant", 10, 15), SpawnGroup("colosse", 4, 50), SpawnGroup("eclat", 8, 12)]),
+ ],
+]
+
+
+# WaveManager
+
+class WaveManager:
+ """
+ machine a etats : combat -> boss -> preparation -> ... -> victory
+ """
+
+ def __init__(self):
+ self._wave_idx = 0
+ self._phase_idx = 0
+ self._state = "preparation"
+ self._prep_timer = PREPARATION_DURATION
+ self._initial_prep = True
+ self._phase_spawned = []
+ self._phase_ticks = []
+ self._total_to_spawn = 0
+ self._total_spawned = 0
+ self._boss_spawned = False
+ self._init_phase()
+
+ @property
+ def wave_number(self):
+ return self._wave_idx + 1 # affiche 1/2/3
+
+ @property
+ def phase_number(self):
+ return self._phase_idx + 1
+
+ @property
+ def state(self):
+ return self._state
+
+ @property
+ def prep_timer_remaining(self):
+ return max(0.0, self._prep_timer)
+
+ def _current_phase(self):
+ return WAVE_CONFIGS[self._wave_idx][self._phase_idx]
+
+ def _init_phase(self):
+ phase = self._current_phase()
+ n = len(phase.groups)
+ self._phase_spawned = [0] * n
+ # ticks initialises a l'intervalle pour spawn des le 1er tick
+ self._phase_ticks = [g.interval for g in phase.groups]
+ self._total_to_spawn = sum(g.count for g in phase.groups)
+ self._total_spawned = 0
+
+ def enemies_remaining(self):
+ # ennemis restants a spawner dans la phase courante (pas ceux vivants sur la map)
+ return self._total_to_spawn - self._total_spawned
+
+ def tick(self, spawn_fn, enemy_count):
+ events = []
+
+ if self._state == "victory":
+ return events
+
+ # phase boss : on spawn le boss 1 fois, puis on attend qu'il meure
+ if self._state == "boss":
+ if not self._boss_spawned:
+ spawn_fn(WAVE_BOSSES[self._wave_idx + 1])
+ self._boss_spawned = True
+ elif enemy_count == 0:
+ next_wave = self._wave_idx + 1
+ if next_wave < len(WAVE_CONFIGS):
+ self._state = "preparation"
+ self._prep_timer = PREPARATION_DURATION
+ events.append("wave_complete")
+ events.append("preparation_start")
+ else:
+ self._state = "victory"
+ events.append("game_over_victory")
+ return events
+
+ # phase preparation : timer 20s avant la vague suivante
+ if self._state == "preparation":
+ self._prep_timer -= TICK_DURATION
+ if self._prep_timer <= 0.0:
+ if self._initial_prep:
+ self._initial_prep = False # vague 1 : pas d'increment
+ else:
+ self._wave_idx += 1
+ self._phase_idx = 0
+ self._init_phase()
+ self._state = "combat"
+ events.append("wave_start")
+ return events
+
+ # combat : spawn des groupes en parallele
+ phase = self._current_phase()
+
+ for i, group in enumerate(phase.groups):
+ if self._phase_spawned[i] >= group.count:
+ continue
+ self._phase_ticks[i] += 1
+ if self._phase_ticks[i] >= group.interval:
+ spawn_fn(group.enemy_type)
+ self._phase_spawned[i] += 1
+ self._total_spawned += 1
+ self._phase_ticks[i] = 0
+
+ # fin de phase : tout spawne ET 0 ennemis vivants
+ all_spawned = self._total_spawned >= self._total_to_spawn
+ if all_spawned and enemy_count == 0:
+ next_phase = self._phase_idx + 1
+ if next_phase < len(WAVE_CONFIGS[self._wave_idx]):
+ self._phase_idx = next_phase
+ self._init_phase()
+ events.append("wave_phase")
+ else:
+ wave_number = self._wave_idx + 1
+ next_wave = self._wave_idx + 1
+ if wave_number in WAVE_BOSSES:
+ self._state = "boss"
+ self._boss_spawned = False
+ events.append("boss_incoming")
+ elif next_wave < len(WAVE_CONFIGS):
+ self._state = "preparation"
+ self._prep_timer = PREPARATION_DURATION
+ events.append("wave_complete")
+ events.append("preparation_start")
+ else:
+ # Dernière vague sans boss → victoire
+ self._state = "victory"
+ events.append("game_over_victory")
+
+ return events
diff --git a/server/websocket.py b/server/websocket.py
new file mode 100644
index 0000000..00ad0d7
--- /dev/null
+++ b/server/websocket.py
@@ -0,0 +1,46 @@
+# Gestionnaire de connexions WebSocket — accepte, envoie, broadcast
+
+import logging
+
+from fastapi import WebSocket
+
+logger = logging.getLogger(__name__)
+
+
+class ConnectionManager:
+ def __init__(self) -> None:
+ self.connections: dict[str, WebSocket] = {}
+ self.usernames: dict[str, str] = {}
+ self._counter: int = 0
+
+ def _next_id(self) -> str:
+ self._counter += 1
+ return f"conn_{self._counter}"
+
+ async def connect(self, websocket: WebSocket) -> str:
+ await websocket.accept()
+ conn_id = self._next_id()
+ self.connections[conn_id] = websocket
+ logger.info("Connexion %s établie", conn_id)
+ return conn_id
+
+ def disconnect(self, conn_id: str) -> None:
+ self.connections.pop(conn_id, None)
+ self.usernames.pop(conn_id, None)
+ logger.info("Connexion %s fermée", conn_id)
+
+ def set_username(self, conn_id: str, username: str) -> None:
+ self.usernames[conn_id] = username
+
+ def get_username(self, conn_id: str) -> str | None:
+ return self.usernames.get(conn_id)
+
+ async def send(self, conn_id: str, message: dict) -> None:
+ ws = self.connections.get(conn_id)
+ if ws:
+ await ws.send_json(message)
+
+ async def dispatch(self, messages: list[tuple[str, dict]]) -> None:
+ """Envoie une liste de (conn_id, message) produite par le LobbyManager."""
+ for conn_id, message in messages:
+ await self.send(conn_id, message)