diff --git a/apps/game/assets/maps/meadows.map b/apps/game/assets/maps/meadows.map index a0b670e..a886260 100644 --- a/apps/game/assets/maps/meadows.map +++ b/apps/game/assets/maps/meadows.map @@ -1 +1 @@ -{"format":"rpg-map","version":1,"width":28,"height":28,"blocked":[2,3,6,7,9,10],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4},"9":{"height":1,"ground":8}},"variants":{"0":[0,11,12],"1":[1,14],"4":[4,13]},"encoding":"rle","tiles":[[0,1],[3,2],[4,1],[0,1],[4,3],[0,1],[4,3],[0,1],[4,1],[0,4],[4,8],[3,1],[4,1],[0,1],[1,1],[0,3],[4,3],[0,2],[4,1],[0,2],[4,1],[0,4],[4,2],[0,1],[4,1],[0,1],[4,4],[0,1],[3,1],[1,2],[0,2],[4,2],[0,3],[4,4],[0,4],[4,1],[0,1],[4,2],[0,1],[4,4],[3,1],[0,2],[1,2],[0,1],[4,2],[0,6],[4,9],[0,1],[4,2],[3,1],[4,2],[3,1],[0,2],[1,2],[0,1],[4,2],[0,5],[4,3],[0,1],[4,2],[0,1],[4,1],[0,1],[4,1],[0,1],[4,2],[0,1],[4,1],[3,1],[0,1],[1,3],[0,8],[4,5],[5,1],[0,5],[4,1],[3,1],[4,2],[0,4],[1,2],[0,1],[4,3],[0,1],[4,1],[0,2],[4,2],[0,1],[4,1],[0,11],[3,1],[0,3],[1,3],[4,2],[0,1],[4,1],[0,1],[4,5],[0,1],[2,4],[0,7],[4,1],[0,1],[4,1],[0,1],[4,1],[1,2],[4,2],[0,2],[4,3],[0,1],[4,1],[0,1],[2,4],[0,3],[3,1],[0,3],[4,1],[3,1],[0,3],[4,1],[1,1],[4,3],[0,1],[4,5],[0,1],[2,2],[0,1],[5,1],[0,7],[3,1],[0,1],[4,1],[0,3],[1,2],[4,2],[0,1],[4,3],[0,8],[3,1],[0,3],[4,1],[3,1],[0,4],[4,3],[1,1],[0,3],[4,5],[0,6],[3,1],[0,2],[3,1],[0,1],[4,1],[3,1],[0,3],[4,3],[1,1],[0,4],[4,5],[0,1],[4,5],[0,1],[4,2],[0,2],[3,1],[0,4],[4,2],[1,1],[4,1],[0,5],[4,3],[0,1],[4,4],[0,3],[3,1],[0,1],[4,1],[0,1],[3,1],[0,4],[4,1],[1,2],[4,2],[0,3],[4,3],[0,1],[4,4],[0,1],[1,2],[4,1],[3,2],[4,1],[0,5],[4,1],[0,1],[1,1],[0,1],[4,1],[0,4],[4,1],[0,2],[4,4],[0,3],[3,2],[0,1],[4,1],[3,1],[0,2],[2,2],[0,2],[1,3],[4,1],[0,6],[4,1],[0,1],[4,4],[0,1],[4,1],[3,1],[0,1],[3,1],[4,3],[2,4],[5,1],[0,1],[1,1],[4,1],[0,13],[3,1],[0,1],[3,1],[4,5],[2,3],[4,2],[1,1],[4,3],[0,4],[4,1],[0,3],[3,1],[0,4],[3,1],[4,1],[3,1],[0,1],[4,4],[0,2],[5,1],[1,2],[4,2],[0,3],[4,3],[0,13],[4,4],[0,1],[4,1],[1,1],[0,5],[4,2],[0,1],[10,1],[0,2],[4,2],[0,1],[3,3],[0,6],[4,1],[0,2],[4,1],[1,3],[0,9],[4,1],[0,5],[3,1],[0,6],[4,4],[0,1],[1,1],[0,8],[4,3],[0,1],[3,1],[0,1],[3,1],[0,3],[4,4],[0,1],[4,1],[0,1],[4,1],[0,1],[1,1],[0,10],[3,1],[0,2],[3,3],[0,2],[4,1],[0,2],[4,5],[0,1],[1,1],[0,9],[4,2],[0,2],[3,1],[0,4],[4,2],[0,7],[1,10],[4,2],[3,1],[0,24],[4,4],[0,2],[3,1],[0,21],[3,4]]} \ No newline at end of file +{"format":"rpg-map","version":1,"width":28,"height":28,"blocked":[2,3,6,7,9,10],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4},"9":{"height":1,"ground":8}},"variants":{"0":[0,11,12],"1":[1,14],"4":[4,13]},"decor":[{"id":16,"x":7,"y":0,"dx":-3,"dy":-2},{"id":17,"x":17,"y":0,"dx":-4,"dy":2},{"id":15,"x":2,"y":1,"dx":1,"dy":0},{"id":16,"x":7,"y":1,"dx":-3,"dy":0},{"id":15,"x":8,"y":1,"dx":-3,"dy":1},{"id":17,"x":17,"y":1,"dx":-2,"dy":-1},{"id":16,"x":25,"y":1,"dx":-2,"dy":-2},{"id":16,"x":26,"y":1,"dx":0,"dy":1},{"id":15,"x":16,"y":2,"dx":3,"dy":1},{"id":16,"x":18,"y":2,"dx":-3,"dy":-1},{"id":17,"x":10,"y":3,"dx":1,"dy":1},{"id":17,"x":11,"y":3,"dx":1,"dy":1},{"id":16,"x":26,"y":3,"dx":-3,"dy":2},{"id":16,"x":7,"y":4,"dx":-3,"dy":1},{"id":16,"x":18,"y":4,"dx":-2,"dy":2},{"id":17,"x":26,"y":4,"dx":-4,"dy":0},{"id":15,"x":11,"y":5,"dx":4,"dy":0},{"id":16,"x":13,"y":8,"dx":-2,"dy":1},{"id":16,"x":15,"y":8,"dx":3,"dy":1},{"id":17,"x":24,"y":8,"dx":-1,"dy":0},{"id":17,"x":6,"y":9,"dx":-1,"dy":-2},{"id":16,"x":13,"y":9,"dx":-3,"dy":0},{"id":17,"x":23,"y":9,"dx":-3,"dy":1},{"id":16,"x":13,"y":10,"dx":0,"dy":0},{"id":15,"x":27,"y":10,"dx":-4,"dy":0},{"id":15,"x":4,"y":11,"dx":-3,"dy":0},{"id":15,"x":10,"y":11,"dx":-2,"dy":2},{"id":16,"x":14,"y":11,"dx":3,"dy":-2},{"id":15,"x":23,"y":11,"dx":-2,"dy":1},{"id":15,"x":0,"y":12,"dx":1,"dy":1},{"id":16,"x":1,"y":12,"dx":1,"dy":1},{"id":15,"x":4,"y":12,"dx":0,"dy":1},{"id":17,"x":1,"y":13,"dx":4,"dy":0},{"id":16,"x":10,"y":13,"dx":4,"dy":0},{"id":16,"x":18,"y":13,"dx":-2,"dy":-2},{"id":16,"x":23,"y":13,"dx":2,"dy":-1},{"id":15,"x":2,"y":14,"dx":0,"dy":1},{"id":17,"x":4,"y":14,"dx":-1,"dy":0},{"id":17,"x":15,"y":14,"dx":-1,"dy":0},{"id":16,"x":16,"y":14,"dx":3,"dy":-1},{"id":16,"x":18,"y":14,"dx":0,"dy":1},{"id":15,"x":24,"y":14,"dx":4,"dy":-1},{"id":15,"x":9,"y":15,"dx":1,"dy":0},{"id":16,"x":12,"y":15,"dx":-4,"dy":-2},{"id":15,"x":13,"y":15,"dx":-2,"dy":1},{"id":17,"x":19,"y":15,"dx":0,"dy":-1},{"id":16,"x":23,"y":15,"dx":3,"dy":-1},{"id":16,"x":27,"y":16,"dx":4,"dy":2},{"id":16,"x":4,"y":17,"dx":1,"dy":-2},{"id":16,"x":13,"y":17,"dx":-2,"dy":-2},{"id":15,"x":20,"y":17,"dx":-2,"dy":0},{"id":15,"x":22,"y":17,"dx":4,"dy":0},{"id":16,"x":5,"y":18,"dx":3,"dy":0},{"id":15,"x":0,"y":19,"dx":-2,"dy":1},{"id":16,"x":8,"y":19,"dx":-3,"dy":0},{"id":15,"x":26,"y":19,"dx":2,"dy":1},{"id":15,"x":18,"y":20,"dx":-3,"dy":0},{"id":15,"x":21,"y":20,"dx":0,"dy":-1},{"id":15,"x":23,"y":20,"dx":0,"dy":2},{"id":16,"x":25,"y":20,"dx":-3,"dy":1},{"id":16,"x":25,"y":21,"dx":-3,"dy":2},{"id":17,"x":0,"y":22,"dx":1,"dy":2},{"id":17,"x":21,"y":22,"dx":3,"dy":2},{"id":15,"x":22,"y":22,"dx":3,"dy":-2},{"id":16,"x":25,"y":22,"dx":1,"dy":1},{"id":16,"x":6,"y":23,"dx":-1,"dy":0},{"id":16,"x":8,"y":23,"dx":-3,"dy":0},{"id":15,"x":12,"y":23,"dx":-3,"dy":0},{"id":16,"x":13,"y":23,"dx":1,"dy":0},{"id":16,"x":6,"y":24,"dx":2,"dy":-1},{"id":17,"x":17,"y":24,"dx":0,"dy":0},{"id":16,"x":6,"y":25,"dx":-4,"dy":2},{"id":16,"x":26,"y":26,"dx":1,"dy":1},{"id":15,"x":1,"y":27,"dx":-2,"dy":-1},{"id":17,"x":10,"y":27,"dx":2,"dy":-1}],"encoding":"rle","tiles":[[0,1],[3,2],[4,1],[0,1],[4,3],[0,1],[4,3],[0,1],[4,1],[0,4],[4,8],[3,1],[4,1],[0,1],[1,1],[0,3],[4,3],[0,2],[4,1],[0,2],[4,1],[0,4],[4,2],[0,1],[4,1],[0,1],[4,4],[0,1],[3,1],[1,2],[0,2],[4,2],[0,3],[4,4],[0,4],[4,1],[0,1],[4,2],[0,1],[4,4],[3,1],[0,2],[1,2],[0,1],[4,2],[0,6],[4,9],[0,1],[4,2],[3,1],[4,2],[3,1],[0,2],[1,2],[0,1],[4,2],[0,5],[4,3],[0,1],[4,2],[0,1],[4,1],[0,1],[4,1],[0,1],[4,2],[0,1],[4,1],[3,1],[0,1],[1,3],[0,8],[4,5],[5,1],[0,5],[4,1],[3,1],[4,2],[0,4],[1,2],[0,1],[4,3],[0,1],[4,1],[0,2],[4,2],[0,1],[4,1],[0,11],[3,1],[0,3],[1,3],[4,2],[0,1],[4,1],[0,1],[4,5],[0,1],[2,4],[0,7],[4,1],[0,1],[4,1],[0,1],[4,1],[1,2],[4,2],[0,2],[4,3],[0,1],[4,1],[0,1],[2,4],[0,3],[3,1],[0,3],[4,1],[3,1],[0,3],[4,1],[1,1],[4,3],[0,1],[4,5],[0,1],[2,2],[0,1],[5,1],[0,7],[3,1],[0,1],[4,1],[0,3],[1,2],[4,2],[0,1],[4,3],[0,8],[3,1],[0,3],[4,1],[3,1],[0,4],[4,3],[1,1],[0,3],[4,5],[0,6],[3,1],[0,2],[3,1],[0,1],[4,1],[3,1],[0,3],[4,3],[1,1],[0,4],[4,5],[0,1],[4,5],[0,1],[4,2],[0,2],[3,1],[0,4],[4,2],[1,1],[4,1],[0,5],[4,3],[0,1],[4,4],[0,3],[3,1],[0,1],[4,1],[0,1],[3,1],[0,4],[4,1],[1,2],[4,2],[0,3],[4,3],[0,1],[4,4],[0,1],[1,2],[4,1],[3,2],[4,1],[0,5],[4,1],[0,1],[1,1],[0,1],[4,1],[0,4],[4,1],[0,2],[4,4],[0,3],[3,2],[0,1],[4,1],[3,1],[0,2],[2,2],[0,2],[1,3],[4,1],[0,6],[4,1],[0,1],[4,4],[0,1],[4,1],[3,1],[0,1],[3,1],[4,3],[2,4],[5,1],[0,1],[1,1],[4,1],[0,13],[3,1],[0,1],[3,1],[4,5],[2,3],[4,2],[1,1],[4,3],[0,4],[4,1],[0,3],[3,1],[0,4],[3,1],[4,1],[3,1],[0,1],[4,4],[0,2],[5,1],[1,2],[4,2],[0,3],[4,3],[0,13],[4,4],[0,1],[4,1],[1,1],[0,5],[4,2],[0,1],[10,1],[0,2],[4,2],[0,1],[3,3],[0,6],[4,1],[0,2],[4,1],[1,3],[0,9],[4,1],[0,5],[3,1],[0,6],[4,4],[0,1],[1,1],[0,8],[4,3],[0,1],[3,1],[0,1],[3,1],[0,3],[4,4],[0,1],[4,1],[0,1],[4,1],[0,1],[1,1],[0,10],[3,1],[0,2],[3,3],[0,2],[4,1],[0,2],[4,5],[0,1],[1,1],[0,9],[4,2],[0,2],[3,1],[0,4],[4,2],[0,7],[1,10],[4,2],[3,1],[0,24],[4,4],[0,2],[3,1],[0,21],[3,4]]} \ No newline at end of file diff --git a/apps/game/assets/maps/ponds.map b/apps/game/assets/maps/ponds.map index fc38949..a0eb098 100644 --- a/apps/game/assets/maps/ponds.map +++ b/apps/game/assets/maps/ponds.map @@ -1 +1 @@ -{"format":"rpg-map","version":1,"width":24,"height":24,"blocked":[2,3,6,7,9,10],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4},"9":{"height":1,"ground":8}},"variants":{"0":[0,11,12],"1":[1,14],"4":[4,13]},"encoding":"rle","tiles":[[4,4],[0,1],[4,13],[0,3],[3,1],[4,2],[3,2],[0,4],[4,1],[0,1],[4,2],[0,1],[4,9],[0,3],[4,1],[3,2],[1,1],[0,3],[4,1],[0,1],[4,2],[0,1],[4,9],[0,1],[3,1],[4,3],[3,1],[4,1],[1,1],[0,1],[4,2],[0,2],[4,1],[0,2],[4,8],[0,1],[3,1],[0,2],[3,1],[0,5],[4,1],[0,2],[4,1],[0,2],[4,5],[0,1],[4,2],[0,1],[3,1],[0,2],[4,1],[0,2],[4,1],[1,1],[0,1],[4,1],[0,1],[4,1],[0,3],[4,4],[0,2],[4,3],[3,1],[0,1],[4,2],[3,1],[4,1],[0,1],[1,1],[0,1],[4,2],[0,6],[4,2],[5,1],[0,4],[4,1],[3,1],[0,1],[4,1],[0,1],[4,2],[1,2],[0,1],[4,1],[2,1],[0,1],[2,1],[4,1],[2,1],[4,1],[2,1],[0,1],[4,2],[0,1],[4,1],[0,2],[3,1],[4,3],[0,3],[1,1],[0,1],[4,1],[2,7],[4,1],[2,1],[4,1],[0,1],[4,1],[0,1],[4,1],[3,1],[4,2],[3,1],[0,2],[4,1],[1,3],[2,11],[0,3],[4,3],[3,2],[0,2],[2,2],[1,1],[2,10],[4,1],[2,1],[4,1],[0,1],[4,1],[3,2],[0,1],[4,1],[0,1],[4,2],[2,1],[1,2],[2,10],[4,1],[0,2],[4,2],[3,3],[0,2],[2,3],[1,1],[2,10],[0,2],[3,1],[4,2],[0,4],[4,1],[2,3],[1,1],[2,12],[4,1],[3,1],[4,2],[0,1],[3,1],[0,1],[4,1],[2,3],[1,1],[2,10],[4,5],[0,2],[4,4],[2,2],[1,2],[2,9],[4,2],[3,1],[4,1],[3,1],[4,1],[3,1],[0,2],[4,2],[2,2],[4,1],[1,1],[2,7],[4,1],[5,1],[4,2],[3,2],[4,4],[0,2],[4,1],[0,1],[5,1],[2,1],[1,1],[4,1],[2,2],[4,1],[2,1],[4,1],[2,2],[4,6],[3,1],[4,3],[0,2],[4,3],[1,7],[4,5],[3,1],[4,1],[0,1],[4,2],[3,1],[0,1],[4,2],[0,2],[4,13],[0,3],[4,1],[3,1],[4,2],[0,1],[4,1],[0,1],[4,4],[0,1],[5,1],[0,1],[4,7],[0,1],[4,1],[0,1],[4,1],[3,1],[4,3],[0,2],[4,4],[0,2],[4,10],[3,1],[4,11],[0,3],[4,21],[0,1],[4,10],[3,2]]} \ No newline at end of file +{"format":"rpg-map","version":1,"width":24,"height":24,"blocked":[2,3,6,7,9,10],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4},"9":{"height":1,"ground":8}},"variants":{"0":[0,11,12],"1":[1,14],"4":[4,13]},"decor":[{"id":16,"x":3,"y":0,"dx":-2,"dy":2},{"id":16,"x":14,"y":0,"dx":0,"dy":0},{"id":17,"x":2,"y":1,"dx":1,"dy":-1},{"id":16,"x":13,"y":1,"dx":-4,"dy":-2},{"id":16,"x":19,"y":1,"dx":-4,"dy":0},{"id":16,"x":23,"y":1,"dx":-2,"dy":1},{"id":16,"x":13,"y":2,"dx":2,"dy":1},{"id":16,"x":22,"y":2,"dx":3,"dy":1},{"id":16,"x":15,"y":3,"dx":-1,"dy":0},{"id":15,"x":10,"y":5,"dx":3,"dy":-2},{"id":16,"x":23,"y":5,"dx":-4,"dy":0},{"id":16,"x":6,"y":6,"dx":0,"dy":-1},{"id":15,"x":23,"y":6,"dx":-3,"dy":0},{"id":15,"x":3,"y":9,"dx":2,"dy":-2},{"id":16,"x":23,"y":9,"dx":1,"dy":-1},{"id":16,"x":22,"y":10,"dx":3,"dy":0},{"id":15,"x":1,"y":11,"dx":3,"dy":-1},{"id":16,"x":5,"y":11,"dx":1,"dy":-1},{"id":17,"x":20,"y":12,"dx":-3,"dy":-2},{"id":16,"x":21,"y":13,"dx":-1,"dy":1},{"id":16,"x":20,"y":14,"dx":1,"dy":2},{"id":16,"x":0,"y":16,"dx":2,"dy":1},{"id":15,"x":3,"y":17,"dx":4,"dy":-1},{"id":16,"x":15,"y":17,"dx":3,"dy":2},{"id":16,"x":22,"y":17,"dx":-2,"dy":2},{"id":16,"x":16,"y":18,"dx":0,"dy":0},{"id":15,"x":3,"y":19,"dx":3,"dy":0},{"id":15,"x":7,"y":19,"dx":0,"dy":-1},{"id":16,"x":14,"y":20,"dx":-2,"dy":-2},{"id":16,"x":19,"y":20,"dx":3,"dy":-1},{"id":16,"x":10,"y":21,"dx":2,"dy":1},{"id":16,"x":5,"y":22,"dx":1,"dy":1},{"id":16,"x":6,"y":22,"dx":-1,"dy":1},{"id":16,"x":14,"y":22,"dx":0,"dy":-1},{"id":16,"x":18,"y":22,"dx":0,"dy":-1},{"id":16,"x":19,"y":22,"dx":-1,"dy":-2},{"id":16,"x":22,"y":22,"dx":-4,"dy":-2},{"id":16,"x":7,"y":23,"dx":-2,"dy":-1},{"id":16,"x":15,"y":23,"dx":3,"dy":0},{"id":16,"x":17,"y":23,"dx":3,"dy":1}],"encoding":"rle","tiles":[[4,4],[0,1],[4,13],[0,3],[3,1],[4,2],[3,2],[0,4],[4,1],[0,1],[4,2],[0,1],[4,9],[0,3],[4,1],[3,2],[1,1],[0,3],[4,1],[0,1],[4,2],[0,1],[4,9],[0,1],[3,1],[4,3],[3,1],[4,1],[1,1],[0,1],[4,2],[0,2],[4,1],[0,2],[4,8],[0,1],[3,1],[0,2],[3,1],[0,5],[4,1],[0,2],[4,1],[0,2],[4,5],[0,1],[4,2],[0,1],[3,1],[0,2],[4,1],[0,2],[4,1],[1,1],[0,1],[4,1],[0,1],[4,1],[0,3],[4,4],[0,2],[4,3],[3,1],[0,1],[4,2],[3,1],[4,1],[0,1],[1,1],[0,1],[4,2],[0,6],[4,2],[5,1],[0,4],[4,1],[3,1],[0,1],[4,1],[0,1],[4,2],[1,2],[0,1],[4,1],[2,1],[0,1],[2,1],[4,1],[2,1],[4,1],[2,1],[0,1],[4,2],[0,1],[4,1],[0,2],[3,1],[4,3],[0,3],[1,1],[0,1],[4,1],[2,7],[4,1],[2,1],[4,1],[0,1],[4,1],[0,1],[4,1],[3,1],[4,2],[3,1],[0,2],[4,1],[1,3],[2,11],[0,3],[4,3],[3,2],[0,2],[2,2],[1,1],[2,10],[4,1],[2,1],[4,1],[0,1],[4,1],[3,2],[0,1],[4,1],[0,1],[4,2],[2,1],[1,2],[2,10],[4,1],[0,2],[4,2],[3,3],[0,2],[2,3],[1,1],[2,10],[0,2],[3,1],[4,2],[0,4],[4,1],[2,3],[1,1],[2,12],[4,1],[3,1],[4,2],[0,1],[3,1],[0,1],[4,1],[2,3],[1,1],[2,10],[4,5],[0,2],[4,4],[2,2],[1,2],[2,9],[4,2],[3,1],[4,1],[3,1],[4,1],[3,1],[0,2],[4,2],[2,2],[4,1],[1,1],[2,7],[4,1],[5,1],[4,2],[3,2],[4,4],[0,2],[4,1],[0,1],[5,1],[2,1],[1,1],[4,1],[2,2],[4,1],[2,1],[4,1],[2,2],[4,6],[3,1],[4,3],[0,2],[4,3],[1,7],[4,5],[3,1],[4,1],[0,1],[4,2],[3,1],[0,1],[4,2],[0,2],[4,13],[0,3],[4,1],[3,1],[4,2],[0,1],[4,1],[0,1],[4,4],[0,1],[5,1],[0,1],[4,7],[0,1],[4,1],[0,1],[4,1],[3,1],[4,3],[0,2],[4,4],[0,2],[4,10],[3,1],[4,11],[0,3],[4,21],[0,1],[4,10],[3,2]]} \ No newline at end of file diff --git a/apps/game/assets/maps/zvenets.map b/apps/game/assets/maps/zvenets.map index 0b75db0..d8b0052 100644 --- a/apps/game/assets/maps/zvenets.map +++ b/apps/game/assets/maps/zvenets.map @@ -1 +1 @@ -{"format":"rpg-map","version":1,"width":28,"height":20,"blocked":[2,3,6,7,9,10],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4},"9":{"height":1,"ground":8}},"variants":{"0":[0,11,12],"1":[1,14],"4":[4,13]},"encoding":"rle","tiles":[[4,30],[3,1],[4,6],[0,4],[4,13],[3,1],[4,9],[0,2],[4,2],[0,1],[4,23],[0,3],[4,1],[0,1],[4,12],[3,1],[4,2],[3,1],[4,1],[3,1],[4,5],[0,5],[4,12],[3,1],[4,2],[0,2],[4,6],[0,5],[4,1],[0,1],[4,3],[7,1],[4,6],[3,1],[4,3],[3,1],[4,5],[7,1],[4,1],[10,1],[1,10],[4,8],[3,1],[0,2],[4,6],[1,5],[6,1],[4,1],[1,1],[4,1],[1,2],[4,6],[3,3],[4,1],[3,1],[4,6],[1,8],[4,1],[1,1],[4,6],[3,1],[4,3],[0,1],[4,7],[1,1],[4,1],[1,9],[4,1],[7,1],[4,8],[1,12],[0,1],[1,1],[4,1],[1,3],[4,1],[0,3],[4,4],[3,1],[0,1],[4,7],[1,2],[4,1],[1,8],[0,4],[4,5],[3,1],[0,5],[4,2],[1,11],[4,1],[0,3],[4,6],[3,1],[0,6],[1,8],[4,1],[1,2],[4,1],[0,3],[4,4],[3,1],[4,3],[0,3],[4,1],[0,5],[4,17],[0,1],[4,1],[0,2],[4,1],[0,1],[4,1],[0,2],[4,5],[0,4],[4,7],[3,1],[0,2],[4,1],[0,1],[4,1],[0,2],[4,2],[0,2],[4,4],[0,5],[4,5],[3,2],[4,2],[0,1],[3,1],[4,5],[0,12],[4,8],[0,3],[4,6],[0,5],[4,1],[0,5],[4,4],[0,1],[3,1],[0,1],[4,1],[3,1],[0,1],[4,7],[0,7],[4,1],[0,3],[4,4],[0,3],[4,1]]} \ No newline at end of file +{"format":"rpg-map","version":1,"width":28,"height":20,"blocked":[2,3,6,7,9,10],"tall":{"3":{"height":1.5,"ground":0},"6":{"height":2.5,"ground":1},"7":{"height":1.75,"ground":4},"9":{"height":1,"ground":8}},"variants":{"0":[0,11,12],"1":[1,14],"4":[4,13]},"decor":[{"id":16,"x":14,"y":0,"dx":1,"dy":1},{"id":16,"x":23,"y":0,"dx":0,"dy":1},{"id":16,"x":26,"y":0,"dx":3,"dy":1},{"id":16,"x":3,"y":1,"dx":0,"dy":0},{"id":17,"x":9,"y":1,"dx":3,"dy":0},{"id":15,"x":10,"y":1,"dx":3,"dy":2},{"id":16,"x":19,"y":1,"dx":-1,"dy":1},{"id":16,"x":1,"y":2,"dx":1,"dy":0},{"id":16,"x":3,"y":2,"dx":4,"dy":1},{"id":16,"x":11,"y":2,"dx":-1,"dy":-2},{"id":15,"x":10,"y":3,"dx":4,"dy":1},{"id":16,"x":15,"y":3,"dx":0,"dy":0},{"id":16,"x":26,"y":5,"dx":3,"dy":-1},{"id":16,"x":20,"y":6,"dx":1,"dy":-1},{"id":16,"x":26,"y":6,"dx":-2,"dy":-2},{"id":16,"x":5,"y":7,"dx":1,"dy":-2},{"id":16,"x":7,"y":7,"dx":0,"dy":0},{"id":16,"x":3,"y":8,"dx":3,"dy":2},{"id":16,"x":17,"y":8,"dx":1,"dy":0},{"id":16,"x":22,"y":9,"dx":1,"dy":-1},{"id":16,"x":2,"y":11,"dx":-3,"dy":-1},{"id":16,"x":26,"y":11,"dx":1,"dy":1},{"id":15,"x":4,"y":12,"dx":4,"dy":0},{"id":17,"x":23,"y":12,"dx":-2,"dy":2},{"id":15,"x":4,"y":14,"dx":-1,"dy":-1},{"id":15,"x":11,"y":14,"dx":3,"dy":1},{"id":16,"x":17,"y":14,"dx":-4,"dy":2},{"id":16,"x":23,"y":16,"dx":3,"dy":-2},{"id":17,"x":16,"y":17,"dx":3,"dy":-2},{"id":16,"x":5,"y":18,"dx":0,"dy":2}],"encoding":"rle","tiles":[[4,30],[3,1],[4,6],[0,4],[4,13],[3,1],[4,9],[0,2],[4,2],[0,1],[4,23],[0,3],[4,1],[0,1],[4,12],[3,1],[4,2],[3,1],[4,1],[3,1],[4,5],[0,5],[4,12],[3,1],[4,2],[0,2],[4,6],[0,5],[4,1],[0,1],[4,3],[7,1],[4,6],[3,1],[4,3],[3,1],[4,5],[7,1],[4,1],[10,1],[1,10],[4,8],[3,1],[0,2],[4,6],[1,5],[6,1],[4,1],[1,1],[4,1],[1,2],[4,6],[3,3],[4,1],[3,1],[4,6],[1,8],[4,1],[1,1],[4,6],[3,1],[4,3],[0,1],[4,7],[1,1],[4,1],[1,9],[4,1],[7,1],[4,8],[1,12],[0,1],[1,1],[4,1],[1,3],[4,1],[0,3],[4,4],[3,1],[0,1],[4,7],[1,2],[4,1],[1,8],[0,4],[4,5],[3,1],[0,5],[4,2],[1,11],[4,1],[0,3],[4,6],[3,1],[0,6],[1,8],[4,1],[1,2],[4,1],[0,3],[4,4],[3,1],[4,3],[0,3],[4,1],[0,5],[4,17],[0,1],[4,1],[0,2],[4,1],[0,1],[4,1],[0,2],[4,5],[0,4],[4,7],[3,1],[0,2],[4,1],[0,1],[4,1],[0,2],[4,2],[0,2],[4,4],[0,5],[4,5],[3,2],[4,2],[0,1],[3,1],[4,5],[0,12],[4,8],[0,3],[4,6],[0,5],[4,1],[0,5],[4,4],[0,1],[3,1],[0,1],[4,1],[3,1],[0,1],[4,7],[0,7],[4,1],[0,3],[4,4],[0,3],[4,1]]} \ No newline at end of file diff --git a/apps/game/assets/tiles/decor_bald.png b/apps/game/assets/tiles/decor_bald.png new file mode 100644 index 0000000..c2d6337 --- /dev/null +++ b/apps/game/assets/tiles/decor_bald.png Binary files differ diff --git a/apps/game/assets/tiles/decor_stone.png b/apps/game/assets/tiles/decor_stone.png new file mode 100644 index 0000000..109f7ef --- /dev/null +++ b/apps/game/assets/tiles/decor_stone.png Binary files differ diff --git a/apps/game/assets/tiles/decor_tuft.png b/apps/game/assets/tiles/decor_tuft.png new file mode 100644 index 0000000..7abb04a --- /dev/null +++ b/apps/game/assets/tiles/decor_tuft.png Binary files differ diff --git a/apps/game/src/Game.ts b/apps/game/src/Game.ts index 27a1816..fed40c9 100644 --- a/apps/game/src/Game.ts +++ b/apps/game/src/Game.ts @@ -45,6 +45,9 @@ 'tiles/water_2', 'tiles/ash', 'tiles/ash_2', + 'tiles/decor_tuft', + 'tiles/decor_stone', + 'tiles/decor_bald', 'tiles/bellflower', 'tiles/tree', 'tiles/house', diff --git a/apps/game/src/data/map.ts b/apps/game/src/data/map.ts index 58014ff..6d315b0 100644 --- a/apps/game/src/data/map.ts +++ b/apps/game/src/data/map.ts @@ -1,4 +1,4 @@ -import type { TileMapData, TallSpec } from '@rpg/engine'; +import { createRng, type TileMapData, type TallSpec, type DecorData } from '@rpg/engine'; /** id тайлов луга «Пепельные луга». */ export const TILES = { @@ -41,15 +41,40 @@ [TILES.ASH]: [TILES.ASH, TILES.ASH_V1] }; -/** Детерминированный ГПСЧ, чтобы локация выглядела одинаково между запусками. */ -function mulberry32(seed: number): () => number { - return () => { - seed |= 0; - seed = (seed + 0x6d2b79f5) | 0; - let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; +/** id текстур мелкого декора (не тайлы — в tiles не попадают). */ +export const DECOR = { + /** Микро-кустик (сухая трава). */ + TUFT: 15, + /** Камешек. */ + STONE: 16, + /** Проплешина (тёмное пятно на траве). */ + BALD: 17 +} as const; + +/** + * Детерминированно набросать декор по готовой карте: кустики и проплешины + * на траве, камешки на пепле (~10% наземных тайлов). Декор не ставится + * на тропу/воду/высокие объекты — только GRASS и ASH. + */ +function buildDecor(tiles: number[], width: number, height: number, seed: number): DecorData[] { + const r = createRng(seed + 7919); + const out: DecorData[] = []; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + if (r.next() >= 0.1) continue; + const id = tiles[y * width + x]; + // лёгкое смещение от центра ромба — декор не по сетке + // (+0: Math.round может вернуть -0, который JSON теряет — fresh-тест разъезжается) + const dx = Math.round((r.next() - 0.5) * 8) + 0; + const dy = Math.round((r.next() - 0.5) * 4) + 0; + if (id === TILES.GRASS) { + out.push({ id: r.next() < 0.6 ? DECOR.TUFT : DECOR.BALD, x, y, dx, dy }); + } else if (id === TILES.ASH) { + out.push({ id: DECOR.STONE, x, y, dx, dy }); + } + } + } + return out; } /** @@ -57,20 +82,21 @@ * пара прудов и рощи деревьев по краям. */ export function buildMeadowsMap(width = 28, height = 28): TileMapData { - const rng = mulberry32(20260905); + const seed = 20260905; + const rng = createRng(seed); const tiles: number[] = new Array(width * height).fill(TILES.GRASS); const at = (x: number, y: number) => y * width + x; // Пятна пепла (декоративные). for (let i = 0; i < 40; i++) { - const x = Math.floor(rng() * width); - const y = Math.floor(rng() * height); - const r = 1 + Math.floor(rng() * 2); + const x = Math.floor(rng.next() * width); + const y = Math.floor(rng.next() * height); + const r = 1 + Math.floor(rng.next() * 2); for (let dy = -r; dy <= r; dy++) { for (let dx = -r; dx <= r; dx++) { const nx = x + dx; const ny = y + dy; - if (nx >= 0 && ny >= 0 && nx < width && ny < height && rng() < 0.7) { + if (nx >= 0 && ny >= 0 && nx < width && ny < height && rng.next() < 0.7) { tiles[at(nx, ny)] = TILES.ASH; } } @@ -82,7 +108,7 @@ let py = 2; while (px < width - 3 || py < height - 3) { tiles[at(px, py)] = TILES.PATH; - if (rng() < 0.5 && px < width - 3) px++; + if (rng.next() < 0.5 && px < width - 3) px++; else if (py < height - 3) py++; else px++; } @@ -94,7 +120,7 @@ ]) { for (let dy = 0; dy < 3; dy++) { for (let dx = 0; dx < 4; dx++) { - if (rng() < 0.85) tiles[at(cx + dx, cy + dy)] = TILES.WATER; + if (rng.next() < 0.85) tiles[at(cx + dx, cy + dy)] = TILES.WATER; } } } @@ -113,9 +139,9 @@ // Рощи деревьев по краям. for (let i = 0; i < 60; i++) { - const edge = rng() < 0.5; - const x = edge ? Math.floor(rng() * 4) : width - 1 - Math.floor(rng() * 4); - const y = Math.floor(rng() * height); + const edge = rng.next() < 0.5; + const x = edge ? Math.floor(rng.next() * 4) : width - 1 - Math.floor(rng.next() * 4); + const y = Math.floor(rng.next() * height); if (tiles[at(x, y)] === TILES.GRASS || tiles[at(x, y)] === TILES.ASH) { tiles[at(x, y)] = TILES.TREE; } @@ -165,7 +191,8 @@ tiles, blocked: [...BLOCKED], tall: TALL_OBJECTS, - variants: GROUND_VARIANTS + variants: GROUND_VARIANTS, + decor: buildDecor(tiles, width, height, seed) }; } @@ -175,20 +202,21 @@ * (обратный переход на луга). */ export function buildPondsMap(width = 24, height = 24): TileMapData { - const rng = mulberry32(20260906); + const seed = 20260906; + const rng = createRng(seed); const tiles: number[] = new Array(width * height).fill(TILES.ASH); const at = (x: number, y: number) => y * width + x; // Пятна чахлой травы. for (let i = 0; i < 24; i++) { - const x = Math.floor(rng() * width); - const y = Math.floor(rng() * height); - const r = 1 + Math.floor(rng() * 2); + const x = Math.floor(rng.next() * width); + const y = Math.floor(rng.next() * height); + const r = 1 + Math.floor(rng.next() * 2); for (let dy = -r; dy <= r; dy++) { for (let dx = -r; dx <= r; dx++) { const nx = x + dx; const ny = y + dy; - if (nx >= 0 && ny >= 0 && nx < width && ny < height && rng() < 0.6) { + if (nx >= 0 && ny >= 0 && nx < width && ny < height && rng.next() < 0.6) { tiles[at(nx, ny)] = TILES.GRASS; } } @@ -201,7 +229,7 @@ for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const d = Math.hypot((x - cx) / 7.5, (y - cy) / 5.5); - if (d + (rng() - 0.5) * 0.35 < 1) tiles[at(x, y)] = TILES.WATER; + if (d + (rng.next() - 0.5) * 0.35 < 1) tiles[at(x, y)] = TILES.WATER; } } @@ -221,16 +249,16 @@ let py = 3; while (py < height - 6 || px < width - 8) { tiles[at(px, py)] = TILES.PATH; - if (rng() < 0.45 && px < width - 8) px++; + if (rng.next() < 0.45 && px < width - 8) px++; else if (py < height - 6) py++; else px++; } // Сухие деревья по краям. for (let i = 0; i < 46; i++) { - const edge = rng() < 0.5; - const x = edge ? Math.floor(rng() * 3) : width - 1 - Math.floor(rng() * 3); - const y = Math.floor(rng() * height); + const edge = rng.next() < 0.5; + const x = edge ? Math.floor(rng.next() * 3) : width - 1 - Math.floor(rng.next() * 3); + const y = Math.floor(rng.next() * height); if (tiles[at(x, y)] !== TILES.WATER) tiles[at(x, y)] = TILES.TREE; } @@ -249,7 +277,8 @@ tiles, blocked: [...BLOCKED], tall: TALL_OBJECTS, - variants: GROUND_VARIANTS + variants: GROUND_VARIANTS, + decor: buildDecor(tiles, width, height, seed) }; } /** @@ -294,20 +323,21 @@ * платформ, посреди площади — звонное место. Безопасный хаб (без врагов). */ export function buildZvenetsMap(width = 28, height = 20): TileMapData { - const rng = mulberry32(20260907); + const seed = 20260907; + const rng = createRng(seed); const tiles: number[] = new Array(width * height).fill(TILES.ASH); const at = (x: number, y: number) => y * width + x; // Пятна чахлой травы по краям. for (let i = 0; i < 18; i++) { - const x = Math.floor(rng() * width); - const y = Math.floor(rng() * height); - const r = 1 + Math.floor(rng() * 2); + const x = Math.floor(rng.next() * width); + const y = Math.floor(rng.next() * height); + const r = 1 + Math.floor(rng.next() * 2); for (let dy = -r; dy <= r; dy++) { for (let dx = -r; dx <= r; dx++) { const nx = x + dx; const ny = y + dy; - if (nx >= 0 && ny >= 0 && nx < width && ny < height && rng() < 0.6) { + if (nx >= 0 && ny >= 0 && nx < width && ny < height && rng.next() < 0.6) { tiles[at(nx, ny)] = TILES.GRASS; } } @@ -317,7 +347,7 @@ // Площадь: утоптанный пепел в центре карты. for (let y = 6; y <= 13; y++) { for (let x = 9; x <= 19; x++) { - if (rng() < 0.9) tiles[at(x, y)] = TILES.PATH; + if (rng.next() < 0.9) tiles[at(x, y)] = TILES.PATH; } } @@ -326,16 +356,16 @@ let py = 10; while (px < 9 || py !== 10) { tiles[at(px, py)] = TILES.PATH; - if (px < 9 && (py === 10 || rng() < 0.6)) px++; + if (px < 9 && (py === 10 || rng.next() < 0.6)) px++; else if (py !== 10) py += py < 10 ? 1 : -1; else px++; } // Сухие деревья по краям. for (let i = 0; i < 30; i++) { - const edge = rng() < 0.5; - const x = edge ? Math.floor(rng() * 3) : width - 1 - Math.floor(rng() * 3); - const y = Math.floor(rng() * height); + const edge = rng.next() < 0.5; + const x = edge ? Math.floor(rng.next() * 3) : width - 1 - Math.floor(rng.next() * 3); + const y = Math.floor(rng.next() * height); if (tiles[at(x, y)] === TILES.ASH || tiles[at(x, y)] === TILES.GRASS) { tiles[at(x, y)] = TILES.TREE; } @@ -363,6 +393,7 @@ tiles, blocked: [...BLOCKED], tall: TALL_OBJECTS, - variants: GROUND_VARIANTS + variants: GROUND_VARIANTS, + decor: buildDecor(tiles, width, height, seed) }; } diff --git a/apps/game/src/scenes/LocationScene.ts b/apps/game/src/scenes/LocationScene.ts index 820c5f2..4e11dd5 100644 --- a/apps/game/src/scenes/LocationScene.ts +++ b/apps/game/src/scenes/LocationScene.ts @@ -26,7 +26,7 @@ import { Game } from '../Game'; import { MenuScene, SAVE_VERSION, type SaveData } from './MenuScene'; import { InventoryScene } from './InventoryScene'; -import { TILES } from '../data/map'; +import { TILES, DECOR } from '../data/map'; import { areaOf, type AreaDef, @@ -659,6 +659,9 @@ [TILES.WATER, a.texture('tiles/water_1')], [TILES.ASH, a.texture('tiles/ash')], [TILES.ASH_V1, a.texture('tiles/ash_2')], + [DECOR.TUFT, a.texture('tiles/decor_tuft')], + [DECOR.STONE, a.texture('tiles/decor_stone')], + [DECOR.BALD, a.texture('tiles/decor_bald')], [TILES.BELLFLOWER, a.texture('tiles/bellflower')], [TILES.TREE, a.texture('tiles/tree')], [TILES.TOWER, a.texture('tiles/tower')], diff --git a/apps/game/tools/pixelart/gen.mjs b/apps/game/tools/pixelart/gen.mjs index 1ca2ff3..ada13ab 100644 --- a/apps/game/tools/pixelart/gen.mjs +++ b/apps/game/tools/pixelart/gen.mjs @@ -97,6 +97,42 @@ return c; } +// ---------- декор 16x16 (прозрачное поле, рисунок в ромб 16x8) ---------- + +/** Микро-кустик сухой травы: подушка G2 с сухими стеблями G2H. */ +function decorTuft() { + const c = new Canvas(SIZES.decor.w, SIZES.decor.h); + fillDiamond(c, 8, 9, 14, 7, 'G2'); + fillDiamond(c, 8, 9, 10, 5, 'G1'); + const r = rng(151); + for (let i = 0; i < 6; i++) { + const x = 4 + Math.floor(r() * 9); + const y = 5 + Math.floor(r() * 6); + if (c.get(x, y)) c.set(x, y, 'G2H'); + } + // пара торчащих стеблей выше подушки + c.set(6, 4, 'G2H'); + c.set(10, 5, 'G2H'); + return c; +} + +/** Камешек: основание P3, верхняя грань P5. */ +function decorStone() { + const c = new Canvas(SIZES.decor.w, SIZES.decor.h); + fillDiamond(c, 8, 10, 8, 4, 'P3'); + // светлая грань — верхний склон + fillDiamond(c, 8, 9, 6, 2, 'P5'); + return c; +} + +/** Проплешина: тёмное пятно на траве (только тень, без рисунка сверху). */ +function decorBald() { + const c = new Canvas(SIZES.decor.w, SIZES.decor.h); + fillDiamond(c, 8, 9, 12, 6, 'G0'); + fillDiamond(c, 8, 9, 8, 4, 'G1'); + return c; +} + function bellflowerTile() { const c = grassTile(); // Три цветка: золото 3px + хайлайт. Тёплый цвет — только жизнь. @@ -658,6 +694,9 @@ save(waterTile(1), 'tiles/water_2.png'); save(ashTile(), 'tiles/ash.png'); save(ashTile(41), 'tiles/ash_2.png'); +save(decorTuft(), 'tiles/decor_tuft.png'); +save(decorStone(), 'tiles/decor_stone.png'); +save(decorBald(), 'tiles/decor_bald.png'); save(bellflowerTile(), 'tiles/bellflower.png'); save(treeTile(), 'tiles/tree.png'); save(houseTile(), 'tiles/house.png'); diff --git a/docs/art-style.md b/docs/art-style.md index 88bc748..41e32ac 100644 --- a/docs/art-style.md +++ b/docs/art-style.md @@ -124,6 +124,7 @@ | 7 | `tiles/house.png` | дом Звенца — 32×56, tall-объект 1.75 юнита: стены из вагонных щитов, крыша из шпал | | — | `tiles/house_big.png` | большой дом — 64×80, prop-объект footprint 2×2: два модуля щитов, козырёк над дверью, окна с переплётом | | — | `tiles/grass_2/3.png`, `tiles/path_2.png`, `tiles/ash_2.png` | варианты текстур 32×16 (та же палитра, другой seed шума) — микс по хэшу координат, в `tiles` не попадают | +| — | `tiles/decor_tuft/stone/bald.png` | декор 16×16, рисунок в ромб 16×8: кустик G1/G2/G2H, камешек P3/P5, проплешина G0/G1 — прозрачное поле, рисуются плашмя | ### Персонажи (16×24) diff --git a/docs/engine/maps.md b/docs/engine/maps.md index 6cfce74..1a3e305 100644 --- a/docs/engine/maps.md +++ b/docs/engine/maps.md @@ -82,6 +82,23 @@ - id вариантов — обычные id тайлов (нужны текстуры в общей таблице); в `tiles` они никогда не попадают. +### Декор (мелкие объекты поверх земли) + +Слой между землёй и высокими объектами — плашмя, без сортировки глубины: +микро-кусты, камни, проплешины. Движок только рисует список `decor` из +`TileMapData` (`{ id, x, y, dx?, dy? }` — тайл + смещение от центра ромба в px); +генерация списка — на стороне игры (детерминированный rng → map-файл): + +```ts +const s = map.addDecor(x, y, texture, { dx: 2, dy: -1 }); // рантайм, один спрайт на тайл +map.clearDecor(x, y); // убрать +``` + +- `setTile` декор не трогает (посадка цветов не стирает кустики). +- Повторный `addDecor` на тот же тайл заменяет предыдущий спрайт. +- В файле карты — `MapFileV1.decor` (опционально), `parseMap` валидирует + границы и целые координаты. + Подробнее про анимированные тайлы — [anim.md](anim.md). ## Крупные объекты: props (footprint w×h) diff --git a/docs/engine/practices.md b/docs/engine/practices.md index 6961bf7..b5617c6 100644 --- a/docs/engine/practices.md +++ b/docs/engine/practices.md @@ -25,6 +25,16 @@ 5. Сценарий в `apps/game/tools/checks/`: переход туда и обратно через `walkTo` + `waitFor` (готовый образец — `apps/game/tools/checks/transitions.mjs`). +## Ситуация: меняю генераторы арта или карт + +1. Правки генератора + перегенерация в одном коммите: `npm run art` → `npm run art:lint` + → `npm run maps`. Новые файлы — заодно в `Game.ASSET_KEYS` и `tileTextures()`. +2. **Fresh-тест карт падает первым прогоном всегда**, если данные генератора изменились: + он сравнивает диск с генератором ДО записи (запись идёт в том же прогоне позже). + Второй прогон должен быть зелёным — если нет, читай diff: там реальное расхождение. +3. Грабля `-0`: `Math.round` может вернуть `-0`, JSON его теряет (`"-0"` → `0`) — + fresh-тест разъезжается на равных на вид числах. Нормализуй: `Math.round(x) + 0`. + ## Ситуация: добавляю переход (дверь, портал, колодец) 1. Один механизм на всё — `TransitionDef` в `data/locations.ts`: `trigger: 'step'` diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index d88fe28..93e6619 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -99,7 +99,8 @@ type TallSpec, type PropData, type PropPlaced, - type PropView + type PropView, + type DecorData } from './map/IsometricTileMap'; export { findPath, findPathToNeighbor, type Grid } from './map/pathfinding'; export { diff --git a/packages/engine/src/map/IsometricTileMap.ts b/packages/engine/src/map/IsometricTileMap.ts index fd73a0f..ea364b6 100644 --- a/packages/engine/src/map/IsometricTileMap.ts +++ b/packages/engine/src/map/IsometricTileMap.ts @@ -48,6 +48,21 @@ } /** + * Мелкий плоский декор на тайле (кустик, камень, проплешина) — рисуется + * плашмя между землёй и высокими объектами, глубина не сортируется. + */ +export interface DecorData { + /** id текстуры декора. */ + id: number; + /** Тайл (ромб), на котором лежит декор. */ + x: number; + y: number; + /** Смещение от центра ромба в px виртуального разрешения. */ + dx?: number; + dy?: number; +} + +/** * Изометрическая тайл-карта. * Данные карты — числа (id тайлов); отрисовка — по таблице id -> Texture. * Проходимость определяется набором блокирующих id; тайлы «высоких» объектов @@ -70,6 +85,8 @@ * поэтому вид карты стабилен между запусками и при setTile. */ variants?: Record; + /** Мелкий плоский декор (микро-кусты, камни) — движок только рисует список. */ + decor?: DecorData[]; } /** Спрайты одной ячейки — для перерисовки тайла без перестроения карты. */ @@ -103,7 +120,10 @@ readonly propViews: PropView[] = []; private propBlocked = new Set(); private groundLayer: Container; + private decorLayer: Container; private objectsLayer: Container; + /** Декор по тайлам (key = ty*width+tx) — один спрайт на тайл. */ + private decorSprites = new Map(); private cells = new Map(); private anims = new Map(); private spriteAnims = new Map(); @@ -135,10 +155,13 @@ this.view = new Container(); const ground = new Container(); + const decor = new Container(); const objects = new Container(); this.groundLayer = ground; + this.decorLayer = decor; this.objectsLayer = objects; - this.view.addChild(ground, objects); + // Порядок детей = порядок отрисовки: земля -> декор (плашмя) -> объекты. + this.view.addChild(ground, decor, objects); // Сортировка глубины по диагонали (tx + ty) — классика изометрии. const cells: { tx: number; ty: number }[] = []; @@ -165,6 +188,38 @@ this.drawPropGround(prop); this.propViews.push({ view: this.makePropView(prop), prop }); } + + // Декор из данных: движок только рисует список (генерация — на стороне игры). + for (const d of data.decor ?? []) { + const tex = this.textures.get(d.id); + if (tex) this.addDecor(d.x, d.y, tex, { dx: d.dx, dy: d.dy }); + } + } + + /** + * Положить декор на тайл (микро-куст, камень): плашмя, якорь в центре ромба, + * опциональное смещение dx/dy для живости. Один спрайт на тайл — повторный + * addDecor заменяет предыдущий. setTile декор не трогает. + * Возвращает спрайт (игра может его анимировать); тайл вне карты — undefined. + */ + addDecor(tx: number, ty: number, tex: Texture, opts: { dx?: number; dy?: number } = {}): Sprite | undefined { + if (tx < 0 || ty < 0 || tx >= this.data.width || ty >= this.data.height) return undefined; + this.clearDecor(tx, ty); + const p = isoToScreen(tx, ty, this.iso); + const s = new Sprite(tex); + s.anchor.set(0.5, 0.5); + s.position.set(p.x + (opts.dx ?? 0), p.y + this.iso.tileH / 2 + (opts.dy ?? 0)); + this.decorLayer.addChild(s); + this.decorSprites.set(ty * this.data.width + tx, s); + return s; + } + + /** Убрать декор с тайла (no-op, если его нет). */ + clearDecor(tx: number, ty: number): void { + const s = this.decorSprites.get(ty * this.data.width + tx); + if (!s) return; + s.destroy(); + this.decorSprites.delete(ty * this.data.width + tx); } /** Земля под footprint'ом пропа (если задан ground и текстура есть). */ diff --git a/packages/engine/src/map/__tests__/isometricMap.test.ts b/packages/engine/src/map/__tests__/isometricMap.test.ts index 35853a3..4fbadbe 100644 --- a/packages/engine/src/map/__tests__/isometricMap.test.ts +++ b/packages/engine/src/map/__tests__/isometricMap.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; import { Texture } from 'pixi.js'; import { IsometricTileMap, type TileMapData } from '../IsometricTileMap'; +import { isoToScreen } from '../../math/iso'; function makeData(): TileMapData { return { @@ -89,6 +90,52 @@ return { ...makeData(), variants: { 0: [0, 1, 2] } }; } +describe('IsometricTileMap.decor', () => { + it('decorLayer между землёй и объектами, декор из данных рисуется', () => { + const data = makeData(); + data.decor = [{ id: 7, x: 1, y: 2, dx: 2, dy: -1 }]; + const map = new IsometricTileMap(data, new Map()); + const layers = map.view.children; + expect(layers.length).toBe(3); + // порядок детей = порядок отрисовки: ground -> decor -> objects + const s = map.addDecor(0, 0, Texture.WHITE)!; + expect(s.parent).toBe(layers[1]); + expect(s.anchor.x).toBe(0.5); + // смещение dx/dy — от центра ромба + const shifted = map.addDecor(2, 1, Texture.WHITE, { dx: 3, dy: -2 })!; + const c = isoToScreen(2, 1, map.iso); + expect(shifted.position.x).toBe(c.x + 3); + expect(shifted.position.y).toBe(c.y + map.iso.tileH / 2 - 2); + }); + + it('addDecor кладёт один спрайт на тайл, повторный — заменяет; clearDecor убирает', () => { + const map = new IsometricTileMap(makeData(), new Map()); + const first = map.addDecor(1, 1, Texture.WHITE)!; + const second = map.addDecor(1, 1, Texture.WHITE)!; // замена, а не второй спрайт + map.addDecor(2, 1, Texture.WHITE); + expect(first.parent).toBeNull(); // прежний спрайт убран + expect(second.parent).not.toBeNull(); + expect(map.data.decor).toBeUndefined(); + + map.clearDecor(1, 1); + map.clearDecor(1, 1); // повторный clear — no-op + map.clearDecor(9, 9); // чужой тайл — no-op + expect(second.parent).toBeNull(); + }); + + it('addDecor за границей карты игнорируется; setTile декор не трогает', () => { + const data = makeData(); + data.decor = [{ id: 7, x: 2, y: 2 }]; + const map = new IsometricTileMap(data, new Map()); + expect(map.addDecor(-1, 0, Texture.WHITE)).toBeUndefined(); + expect(map.addDecor(4, 0, Texture.WHITE)).toBeUndefined(); + + map.setTile(2, 2, 0); // перерисовка ячейки не должна задеть декор + map.setTile(2, 2, 2); + expect(() => map.clearDecor(2, 2)).not.toThrow(); + }); +}); + describe('IsometricTileMap.props', () => { it('блокирует весь footprint и нормализует размеры', () => { const data = makeData(); diff --git a/packages/engine/src/map/__tests__/mapFormat.test.ts b/packages/engine/src/map/__tests__/mapFormat.test.ts index 408445d..c0ebe75 100644 --- a/packages/engine/src/map/__tests__/mapFormat.test.ts +++ b/packages/engine/src/map/__tests__/mapFormat.test.ts @@ -69,6 +69,25 @@ expect(parsed.variants).toEqual(withVariants.variants); }); + it('roundtrip сохраняет decor', () => { + const withDecor: TileMapData = { + ...sample, + decor: [{ id: 7, x: 1, y: 0, dx: 2 }, { id: 9, x: 2, y: 1 }] + }; + const parsed = parseMap(JSON.parse(JSON.stringify(encodeMap(withDecor)))); + expect(parsed.decor).toEqual(withDecor.decor); + // без декора поле опускается (старые карты) + expect(parseMap(encodeMap(sample)).decor).toBeUndefined(); + }); + + it('parseMap отвергает decor вне границ карты и не с целыми числами', () => { + // карта 3×2: тайл (3, 0) вне карты по X + expect(() => parseMap({ ...encodeMap(sample), decor: [{ id: 7, x: 3, y: 0 }] })).toThrow(/границ/); + expect(() => parseMap({ ...encodeMap(sample), decor: [{ id: 7.5, x: 0, y: 0 }] })).toThrow(/целыми/); + expect(() => parseMap({ ...encodeMap(sample), decor: [{ id: 7, x: 0, y: 0, dx: 'a' }] })).toThrow(/числами/); + expect(() => parseMap({ ...encodeMap(sample), decor: [] })).not.toThrow(); + }); + it('parseMap отвергает пустой/нечисловой variants-массив', () => { expect(() => parseMap({ ...encodeMap(sample), variants: { 0: [] } })).toThrow(/variants/); expect(() => parseMap({ ...encodeMap(sample), variants: { 0: [1.5] } })).toThrow(/variants/); diff --git a/packages/engine/src/map/mapFormat.ts b/packages/engine/src/map/mapFormat.ts index 0ea3728..fc71de2 100644 --- a/packages/engine/src/map/mapFormat.ts +++ b/packages/engine/src/map/mapFormat.ts @@ -1,4 +1,4 @@ -import type { TileMapData, TallSpec, PropData } from './IsometricTileMap'; +import type { TileMapData, TallSpec, PropData, DecorData } from './IsometricTileMap'; /** * Файловый формат карт движка: JSON с RLE-сжатием тайлов. @@ -19,6 +19,8 @@ props?: PropData[]; /** Варианты текстур: id -> список id-шников вариантов (ключи — строки JSON). */ variants?: Record; + /** Мелкий плоский декор на тайлах. */ + decor?: DecorData[]; /** 'rle' — tiles как пары [id, runLength]; 'raw' — плоский массив. */ encoding: 'rle' | 'raw'; tiles: number[] | [number, number][]; @@ -62,6 +64,7 @@ variants: data.variants ? Object.fromEntries(Object.entries(data.variants).map(([id, ids]) => [String(id), ids])) : undefined, + decor: data.decor, encoding, tiles: encoding === 'rle' ? rleEncode(data.tiles) : data.tiles }; @@ -104,6 +107,7 @@ } } validateProps(f.props ?? [], f.width, f.height); + const decor = validateDecor(f.decor, f.width, f.height); const variants = validateVariants(f.variants); return { width: f.width, @@ -112,10 +116,28 @@ blocked: f.blocked ?? [], tall: f.tall, props: f.props, + decor, variants }; } +/** Валидация декора: тайл в границах карты, смещения — числа (пустой список -> undefined). */ +function validateDecor(decor: DecorData[] | undefined, width: number, height: number): DecorData[] | undefined { + if (!decor || decor.length === 0) return undefined; + for (const d of decor) { + if (!Number.isInteger(d.id) || !Number.isInteger(d.x) || !Number.isInteger(d.y)) { + throw new Error('Карта: decor — id/x/y должны быть целыми числами'); + } + if (d.x < 0 || d.y < 0 || d.x >= width || d.y >= height) { + throw new Error(`Карта: decor (${d.x}, ${d.y}) вне границ карты ${width}×${height}`); + } + if ((d.dx !== undefined && typeof d.dx !== 'number') || (d.dy !== undefined && typeof d.dy !== 'number')) { + throw new Error(`Карта: decor (${d.x}, ${d.y}) — dx/dy должны быть числами`); + } + } + return decor; +} + /** Валидация вариантов: непустые массивы целых id (пустой объект -> undefined). */ function validateVariants(variants: Record | undefined): Record | undefined { if (!variants || Object.keys(variants).length === 0) return undefined;