diff --git a/apps/game/assets/maps/house_elder.map b/apps/game/assets/maps/house_elder.map index c8edec6..ceeca16 100644 --- a/apps/game/assets/maps/house_elder.map +++ b/apps/game/assets/maps/house_elder.map @@ -1 +1 @@ -{"format":"rpg-map","version":1,"width":11,"height":9,"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}},"encoding":"rle","tiles":[[9,12],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,6],[8,1],[9,5]]} \ No newline at end of file +{"format":"rpg-map","version":1,"width":11,"height":9,"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,"shadow":true},"9":{"height":1,"ground":8}},"encoding":"rle","tiles":[[9,12],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,6],[8,1],[9,5]]} \ No newline at end of file diff --git a/apps/game/assets/maps/meadows.map b/apps/game/assets/maps/meadows.map index a886260..72210fa 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]},"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 +{"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,"shadow":true},"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 a0eb098..f72e0d8 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]},"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 +{"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,"shadow":true},"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/shop.map b/apps/game/assets/maps/shop.map index c8edec6..ceeca16 100644 --- a/apps/game/assets/maps/shop.map +++ b/apps/game/assets/maps/shop.map @@ -1 +1 @@ -{"format":"rpg-map","version":1,"width":11,"height":9,"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}},"encoding":"rle","tiles":[[9,12],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,6],[8,1],[9,5]]} \ No newline at end of file +{"format":"rpg-map","version":1,"width":11,"height":9,"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,"shadow":true},"9":{"height":1,"ground":8}},"encoding":"rle","tiles":[[9,12],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,2],[8,9],[9,6],[8,1],[9,5]]} \ No newline at end of file diff --git a/apps/game/assets/maps/zvenets.map b/apps/game/assets/maps/zvenets.map index d8b0052..0d8fb72 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]},"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 +{"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,"shadow":true},"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/chest.png b/apps/game/assets/tiles/chest.png new file mode 100644 index 0000000..163b9f9 --- /dev/null +++ b/apps/game/assets/tiles/chest.png Binary files differ diff --git a/apps/game/src/Game.ts b/apps/game/src/Game.ts index be584d4..e77c475 100644 --- a/apps/game/src/Game.ts +++ b/apps/game/src/Game.ts @@ -59,7 +59,8 @@ 'tiles/tower', 'tiles/floor', 'tiles/wall', - 'tiles/well' + 'tiles/well', + 'tiles/chest' ]; /** Ключи аудио (WAV): sfx + амбиент-лупы областей (реестр в data/audio.ts). */ diff --git a/apps/game/src/data/__tests__/validate.test.ts b/apps/game/src/data/__tests__/validate.test.ts index 4d61396..79d27e0 100644 --- a/apps/game/src/data/__tests__/validate.test.ts +++ b/apps/game/src/data/__tests__/validate.test.ts @@ -38,7 +38,9 @@ const inv = validateReferences(); expect(inv.filter((i) => i.severity === 'error')).toEqual([]); // got_cloth ставится диалогом и нигде не читается, но ставится — упомянут. - expect(inv.filter((i) => i.id === 'flag-dead').map((i) => i.message)).toEqual([]); + // hint_bells ставится кодом сцены (подсказка прудов) — в контенте не упомянут. + const dead = inv.filter((i) => i.id === 'flag-dead').map((i) => i.message); + expect(dead).toEqual(['флаг «hint_bells» из реестра не упоминается в контенте']); }); it('неизвестный флаг/вар в диалоге — error', () => { diff --git a/apps/game/src/data/ids.ts b/apps/game/src/data/ids.ts index 90166b0..9099c97 100644 --- a/apps/game/src/data/ids.ts +++ b/apps/game/src/data/ids.ts @@ -26,6 +26,8 @@ rang_tower: 'rang_tower', /** Прочитана записка в доме Ирвина (интерактив note_elder). */ read_note: 'read_note', + /** Подсказка «как собирать колокольчики» уже показана (вход в пруды). */ + hint_bells: 'hint_bells', /** Говорили с Ирвином (flagKey NPC elder — движок поднимает сам). */ metElder: 'metElder', /** Говорили с Милой (flagKey NPC trader — движок поднимает сам). */ diff --git a/apps/game/src/data/interactables.ts b/apps/game/src/data/interactables.ts index a6a3bfe..51402f8 100644 --- a/apps/game/src/data/interactables.ts +++ b/apps/game/src/data/interactables.ts @@ -228,6 +228,7 @@ id: 'chest_elder', pos: { x: 3.5, y: 3.5 }, kind: 'container', + view: { tex: 'tiles/chest' }, label: 'Сундук', once: true, responses: [ diff --git a/apps/game/src/data/locations.ts b/apps/game/src/data/locations.ts index e362cd6..9c15ad6 100644 --- a/apps/game/src/data/locations.ts +++ b/apps/game/src/data/locations.ts @@ -155,7 +155,8 @@ // Глобальный ветер + вода у берегов (громкость — от дистанции). ambienceLayers: [ { key: 'ambience/wind', volume: 0.3 }, - { key: 'ambience/ponds_water', tiles: [TILES.WATER], radius: 5, volume: 0.35 } + // Вода — только вплотную к берегу и совсем тихо (у нас лужи, не море). + { key: 'ambience/ponds_water', tiles: [TILES.WATER], radius: 2.5, volume: 0.22 } ], // Свежий накат — низины у берегов: без вощёного полотна здесь не дышать. hazards: [ diff --git a/apps/game/src/data/map.ts b/apps/game/src/data/map.ts index 6d315b0..c5495f2 100644 --- a/apps/game/src/data/map.ts +++ b/apps/game/src/data/map.ts @@ -23,10 +23,10 @@ const BLOCKED = [TILES.WATER, TILES.TREE, TILES.TOWER, TILES.HOUSE, TILES.WALL, TILES.WELL]; /** Дерево — высокий объект 1.5 мировых юнита (48 px), под ним трава. */ -const TALL_OBJECTS: Record = { +export const TALL_OBJECTS: Record = { [TILES.TREE]: { height: 1.5, ground: TILES.GRASS }, [TILES.TOWER]: { height: 2.5, ground: TILES.PATH }, - [TILES.HOUSE]: { height: 1.75, ground: TILES.ASH }, + [TILES.HOUSE]: { height: 1.75, ground: TILES.ASH, shadow: true }, // Стены интерьеров — вертикальные объекты, под ними пол. [TILES.WALL]: { height: 1, ground: TILES.FLOOR } }; diff --git a/apps/game/src/scenes/LocationScene.ts b/apps/game/src/scenes/LocationScene.ts index e9af4ee..b8044a1 100644 --- a/apps/game/src/scenes/LocationScene.ts +++ b/apps/game/src/scenes/LocationScene.ts @@ -15,6 +15,8 @@ renderMusicStereo, worldToScreen, worldNorm, + worldToTile, + screenToWorld, tileToWorld, DebugOverlay, SpriteDebugView, @@ -31,7 +33,7 @@ import { Game } from '../Game'; import { MenuScene, SAVE_VERSION, type SaveData } from './MenuScene'; import { InventoryScene } from './InventoryScene'; -import { TILES, DECOR } from '../data/map'; +import { TILES, DECOR, TALL_OBJECTS } from '../data/map'; import { playSfx, ROOM_TONES } from '../data/sfxSpecs'; import { areaOf, @@ -49,7 +51,7 @@ import { DIALOGUES } from '../data/dialogues'; import { QUEST_FLOWERS, questDialogueFor, questEffectFor } from '../data/quests'; import { TENSION_STEM } from '../data/music'; -import { VARS } from '../data/ids'; +import { FLAGS, VARS } from '../data/ids'; import type { EnemyKindId } from '../data/enemies'; import { PlayerController, type HeroTextures } from '../systems/PlayerController'; import { FaunaSystem } from '../systems/fauna/FaunaSystem'; @@ -72,7 +74,7 @@ private map: IsometricTileMap; private player: PlayerController; private dialogue: DialogueSystem; - private npcs: { def: NpcDef; view: Container }[] = []; + private npcs: { def: NpcDef; view: Container; body: Sprite }[] = []; private actors = new IsoDepthLayer(); private hint: Container; /** Атмосферные частицы (в интерьерах их нет). */ @@ -89,6 +91,9 @@ private combatViews: CombatViews; private playerCombat: PlayerCombat; private healthBar: HealthBar; + /** Счётчик собранных мотов (HUD под сердечками); текст меняется по факту. */ + private moteLabel: PixelText; + private lastMotes = -1; /** Индикатор заряда резонанса (под героем). */ private chargeRing: Graphics; /** Маршрутизация клика/переходов/отложенных взаимодействий. */ @@ -97,7 +102,7 @@ private interactables: Interactables; /** Объекты сцены на движковом реестре (кто на тайле — для кликов/коллизий). */ private objects: SceneObjects; - private interactViews: { def: InteractableDef; label: PixelText }[] = []; + private interactViews: { def: InteractableDef; view: Container; body: Graphics | Sprite; label: PixelText }[] = []; /** Оверлей дымки (зоны наката). Виден только в низинах. */ private fog: Graphics; /** Пейзажная фауна (безгласные олени). */ @@ -151,6 +156,25 @@ 2 ); this.world.addChild(this.map.view, this.actors); + // Выход интерьера: бронзовый контур проёма + подпись (у kind 'return'). + const exit = area.transitions.find((t) => t.target.kind === 'return'); + if (exit) { + const p = this.tileCenter(exit.tile.x, exit.tile.y); + const glow = new Graphics(); + glow + .moveTo(0, -8) + .lineTo(16, 0) + .lineTo(0, 8) + .lineTo(-16, 0) + .closePath() + .stroke({ color: 0x8a6d3a, width: 1 }); + glow.position.set(p.x, p.y); + this.world.addChildAt(glow, 1); // поверх пола карты, под акторами + const label = new PixelText({ text: 'выход', size: 9, color: 0xd8c79a }); + label.anchor.set(0.5, 1); + label.position.set(p.x, p.y - 12); + this.world.addChildAt(label, 2); + } // Источники амбиент-слоёв: позиции тайлов по id — один проход по карте. for (let y = 0; y < data.height; y++) { for (let x = 0; x < data.width; x++) { @@ -196,9 +220,9 @@ // Герой и NPC — в один depth-слой: глубина = tx + ty (кто юго-восточнее, тот ближе). this.actors.add(this.player.view, startTile.x, startTile.y); for (const def of area.npcs) { - const view = this.makeNpcView(def); - this.actors.add(view, def.tile.x, def.tile.y); - this.npcs.push({ def, view }); + const made = this.makeNpcView(def); + this.actors.add(made.view, def.tile.x, def.tile.y); + this.npcs.push({ def, view: made.view, body: made.body }); } // Интерактивные объекты: те же вьюхи в actors-слое, реакции — через Interactables. @@ -206,12 +230,13 @@ hasItem: (id) => this.game.inventory.has(id), give: (id, n) => this.game.inventory.add(id, n), playSound: (k) => this.worldAudio.playAt(k, this.player.position, 0.6), - showToast: (t) => this.showToast(t) + showToast: (t) => this.showToast(t), + onConsumed: (def) => this.consumeView(def) }); for (const def of area.interactables ?? []) { const made = this.makeInteractableView(def); this.actors.add(made.view, def.tile.x, def.tile.y); - this.interactViews.push({ def, label: made.label }); + this.interactViews.push({ def, view: made.view, body: made.body, label: made.label }); } // --- боевая система: враги на ECS, звуки/события/урон героя — через колбэки --- @@ -281,6 +306,10 @@ this.healthBar = new HealthBar(); this.healthBar.setHp(this.playerCombat.hp); this.game.renderer.uiRoot.addChild(this.healthBar); + this.moteLabel = new PixelText({ text: '', size: 10, color: 0xd8c79a }); + this.moteLabel.anchor.set(1, 0); + this.moteLabel.position.set(474, 17); // под названием локации + this.game.renderer.uiRoot.addChild(this.moteLabel); this.chargeRing = new Graphics(); this.game.renderer.worldRoot.addChild(this.chargeRing); @@ -304,6 +333,7 @@ objects: this.objects, disarmTile, worldRootOffset: () => this.game.renderer.worldRoot.position, + pixelTile: (px, py) => this.pixelTile(px, py), callbacks: { showToast: (t) => this.showToast(t), playUiClick: () => playSfx(this.game.audio, 'sfx/ui_click', 0.4), @@ -436,6 +466,11 @@ this.game.playAmbience(this.area.ambience ?? ''); this.game.playTheme(this.area.theme ?? ''); this.startRoomTone(); + // Первая подсказка сбора: как брать лунные колокольчики (один раз). + if (this.area.id === 'ponds' && !this.game.state.hasFlag(FLAGS.hint_bells)) { + this.game.state.setFlag(FLAGS.hint_bells); + this.showToast('Лунные колокольчики в земле: подойди и щёлкни по цветку.'); + } } /** Room tone интерьера (см. ROOM_TONES): тихий луп на шине ambience. */ @@ -553,6 +588,12 @@ } this.router.resolvePending(); const tile = this.player.currentTile(); + // Счётчик мотов: текст пишем только при изменении (сбор — редкое событие). + const motes = this.game.state.getNumber(VARS.motes); + if (motes !== this.lastMotes) { + this.lastMotes = motes; + this.moteLabel.text = motes > 0 ? `Пепельные моты: ${motes}` : ''; + } this.updateHazard(tile); this.actors.setDepth(this.player.view, tile.x, tile.y); this.updateCameraFollow(); @@ -973,13 +1014,71 @@ this.game.renderer.uiRoot.addChild(toast); const tweens = this.game.engine.tweens; tweens.to(toast, { alpha: 1 }, { duration: 0.25 }); - tweens.delay(1.6, () => { + // Держим, пока читается: базовые 1.4 с + ~45 мс на символ, не дольше 6 с. + tweens.delay(Math.min(6, 1.4 + text.length * 0.045), () => { tweens.to(toast, { alpha: 0, y: 32 }, { duration: 0.5, onDone: () => toast.destroy() }); }); } + /** + * Одноразовый объект поднят (sink.onConsumed): вьюха растворяется, из + * тайла вылетают пеплинки — состояние мира меняется на глазах. + */ + private consumeView(def: InteractableDef): void { + const iv = this.interactViews.find((v) => v.def.id === def.id); + if (!iv) return; + this.interactViews = this.interactViews.filter((v) => v !== iv); + const w = tileToWorld(def.tile.x, def.tile.y); + this.combatViews.hitBurst(worldToScreen(w.x, w.y)); + this.game.engine.tweens.to(iv.view, { alpha: 0 }, { + duration: 0.5, + onDone: () => { + this.actors.removeChild(iv.view); + iv.view.destroy({ children: true }); + } + }); + } + + /** + * Тайл клика «по телу объекта» (RouterDeps.pixelTile): bbox вьюх NPC и + * интерактивов + экранное тело высокого тайла (дом, дерево, башня). + * Экранно объект стоит выше своего ромба-пола, и клик по спрайту + * «проваливается под объект» — отдаём его базовому тайлу. + */ + private pixelTile(px: number, py: number): { x: number; y: number } | null { + // Тела вьюх (без подписей — они тянутся на тайл выше): границы в + // глобальных виртуальных px (рендер всегда 480×270). + for (const n of this.npcs) { + const b = n.body.getBounds(); + if (px >= b.x && px <= b.x + b.width && py >= b.y && py <= b.y + b.height) return n.def.tile; + } + for (const iv of this.interactViews) { + const b = iv.body.getBounds(); + if (px >= b.x && px <= b.x + b.width && py >= b.y && py <= b.y + b.height) return iv.def.tile; + } + // Высокий тайл: тело стоит на (tx,ty), а занимает на экране (tx-1,ty-1). + // Ремап — только если на базовом тайле есть что кликать (дверь, объект): + // иначе клик по «телу дерева» уводил бы героя к непроходимому тайлу. + const off = this.game.renderer.worldRoot.position; + const p = screenToWorld(px - off.x, py - off.y); + const t = worldToTile(p.x, p.y, this.map.data.width, this.map.data.height); + if (!t || t.x + 1 >= this.map.data.width || t.y + 1 >= this.map.data.height) return null; + const base = { x: t.x + 1, y: t.y + 1 }; + const above = this.map.data.tiles[(t.y + 1) * this.map.data.width + (t.x + 1)]!; + if (!TALL_OBJECTS[above]) return null; + const clickable = + this.area.transitions.some( + (tr) => tr.tile.x === base.x && tr.tile.y === base.y && tr.trigger === 'click' + ) || this.interactables.defAt(base.x, base.y); + return clickable ? base : null; + } + /** Вьюха интерактивного объекта: спрайт из def.view или плейсхолдер + подпись. */ - private makeInteractableView(def: InteractableDef): { view: Container; label: PixelText } { + private makeInteractableView(def: InteractableDef): { + view: Container; + body: Graphics | Sprite; + label: PixelText; + } { const view = new Container(); const p = this.tileCenter(def.tile.x, def.tile.y); view.position.set(p.x, p.y); @@ -1003,10 +1102,10 @@ g.rect(0, -8, 2, 7).fill(0x8a6d3a); g.rect(-2, -10, 4, 2).fill(0x8a6d3a); break; - case 'pickup': // кучка пепла с тёплой искрой - g.rect(-4, -3, 8, 3).fill(0x3a3a3a); - g.rect(-2, -4, 4, 2).fill(0x4a4a4a); - g.rect(0, -4, 1, 1).fill(0xf0d878); + case 'pickup': // пепельная насыпь, из которой теплится мот + g.rect(-6, -2, 12, 3).fill(0x3a3a3a); + g.rect(-4, -4, 8, 2).fill(0x4a4a4a); + g.rect(-2, -5, 4, 1).fill(0x5a5a5a); break; default: // контейнер/предмет осмотра: ящик с бронзовой кромкой g.rect(-5, -12, 10, 12).fill(0x4a3b2a); @@ -1014,10 +1113,14 @@ } } view.addChild(g); - // Искра в кучке пепла пульсирует (alpha+scale, детерминированная фаза). + // У мота — тёплая искра поверх насыпи: заметный пульс видно издалека. if (def.kind === 'pickup') { + const spark = new Graphics(); + spark.rect(-1, -1, 3, 3).fill(0xf0d878); + spark.position.set(0, -5); + view.addChild(spark); this.game.engine.fx.add( - new SpriteMotion(g, { pulse: { min: 0.4, max: 1, period: 1.6 } }, { + new SpriteMotion(spark, { pulse: { min: 0.3, max: 1, period: 1.1 } }, { seed: def.tile.x * 31 + def.tile.y }) ); @@ -1027,10 +1130,10 @@ label.position.set(0, -16); label.visible = false; view.addChild(label); - return { view, label }; + return { view, body: g, label }; } - private makeNpcView(def: NpcDef): Container { + private makeNpcView(def: NpcDef): { view: Container; body: Sprite } { const view = new Container(); const p = this.tileCenter(def.tile.x, def.tile.y); view.position.set(p.x, p.y); @@ -1059,7 +1162,7 @@ marker.rect(-1, -(frameH + 3), 2, 4).fill(0xf0d878); marker.rect(-1, -(frameH - 3), 2, 2).fill(0xf0d878); view.addChild(marker); - return view; + return { view, body: sprite }; } private saveAndExit(): void { diff --git a/apps/game/src/systems/ClickRouting.ts b/apps/game/src/systems/ClickRouting.ts index 6f4b9b5..7b0633e 100644 --- a/apps/game/src/systems/ClickRouting.ts +++ b/apps/game/src/systems/ClickRouting.ts @@ -115,6 +115,13 @@ disarmTile: { x: number; y: number } | null; /** Смещение worldRoot (виртуальные px) — перевод клика в юниты. */ worldRootOffset(): Vec2; + /** + * Тайл по пикселям указателя (виртуальные px) или null. Сцена отдаёт + * тайл объекта, чьё ТЕЛО (bbox вьюхи / высокий спрайт) накрыто кликом: + * экранно объект стоит выше своего ромба-пола, и по голому тайлу под + * курсором клик «проваливается под объект». Нет — обычный worldToTile. + */ + pixelTile?(px: number, py: number): { x: number; y: number } | null; callbacks: RouterCallbacks; } @@ -147,17 +154,28 @@ // Координаты указателя (виртуальные px) -> мировые юниты (учёт камеры). const p = screenToWorld(px - off.x, py - off.y); const clicked = worldToTile(p.x, p.y, this.deps.map.data.width, this.deps.map.data.height); - const action = resolveClick({ + const probe = (tile: { x: number; y: number } | null) => ({ world: p, - clicked, - npcAt: (x, y) => this.npcAt(x, y), - transitionPick: clicked - ? resolveTransition(this.deps.area.transitions, clicked, 'click', this.transitionCtx(), null) + clicked: tile, + npcAt: (x: number, y: number) => this.npcAt(x, y), + transitionPick: tile + ? resolveTransition(this.deps.area.transitions, tile, 'click', this.transitionCtx(), null) : null, - interactableAt: (x, y) => this.deps.objects.interactableAt(x, y), - flowerAt: (x, y) => this.isFlower(x, y), - enemyAt: (world) => this.enemyAt(world) + interactableAt: (x: number, y: number) => this.deps.objects.interactableAt(x, y), + flowerAt: (x: number, y: number) => this.isFlower(x, y), + enemyAt: (world: Vec2) => this.enemyAt(world) }); + let action = resolveClick(probe(clicked)); + // «Тело объекта» (bbox вьюх / высокий тайл) — только ФОЛБЭК: если клик + // по своему тайлу ничего не выбрал (голое движение). Иначе ломается + // маршрутизация: подписи и спрайты тянутся на тайл «за» объектом, и + // клики движения рядом с объектом начинали бы говорить/подбирать. + if (action.kind === 'move') { + const body = this.deps.pixelTile?.(px, py); + if (body && (!clicked || body.x !== clicked.x || body.y !== clicked.y)) { + action = resolveClick(probe(body)); + } + } switch (action.kind) { case 'talk': this.requestTalk(action.def); diff --git a/apps/game/src/systems/Interactables.ts b/apps/game/src/systems/Interactables.ts index 5d2b6d0..7f1a8d7 100644 --- a/apps/game/src/systems/Interactables.ts +++ b/apps/game/src/systems/Interactables.ts @@ -20,6 +20,11 @@ give(id: ItemId, count: number): void; playSound(key: string): void; showToast(text: string): void; + /** + * Одноразовый объект «поднят» (once, реакция ok): сцена убирает вьюху с + * поля (fade) и даёт визуальный отклик. Вызывается после apply. + */ + onConsumed?(def: InteractableDef, response: InteractResponse): void; } export class Interactables { @@ -52,7 +57,10 @@ used: (id) => this.isUsed(id) }; const pick = resolveInteractable(def, ctx); - if (pick?.ok) this.apply(def, pick.response); + if (pick?.ok) { + this.apply(def, pick.response); + if (def.once) this.sink.onConsumed?.(def, pick.response); + } return pick; } diff --git a/apps/game/tools/pixelart/gen.mjs b/apps/game/tools/pixelart/gen.mjs index ada13ab..52b6f1d 100644 --- a/apps/game/tools/pixelart/gen.mjs +++ b/apps/game/tools/pixelart/gen.mjs @@ -407,6 +407,33 @@ return c; } +/** Сундук: вощёное дерево с бронзовой кромкой и замком — «звонное» железо. */ +function chestTile() { + const c = new Canvas(...SIZES.prop.chest); + const r = rng(1020); + + // Крышка: слегка выпуклая, доски из тёплого дерева. + for (let x = 4; x <= 19; x++) { + const lift = x >= 7 && x <= 16 ? 1 : 0; + for (let y = 6 - lift; y <= 9; y++) c.set(x, y, r() < 0.85 ? 'T1' : 'T0'); + } + // Корпус: темнее крышки. + for (let y = 11; y <= 15; y++) { + for (let x = 4; x <= 19; x++) c.set(x, y, r() < 0.85 ? 'T0' : 'T1'); + } + // Бронзовая полоса по стыку крышки и корпуса (как на ящике-плейсхолдере). + for (let x = 4; x <= 19; x++) c.set(x, 10, x % 5 === 0 ? 'B0' : 'B1'); + // Замок: вертикальная накладка с бликом. + for (let y = 10; y <= 15; y++) { + c.set(11, y, 'B1'); + c.set(12, y, 'B0'); + } + c.set(11, 12, 'B2'); + // Тень под днищем. + for (let x = 5; x <= 19; x++) c.set(x, 16, 'P1'); + return c; +} + function towerTile() { const c = new Canvas(...SIZES.tall.tower); const r = rng(1414); @@ -705,6 +732,7 @@ save(floorTile(), 'tiles/floor.png'); save(wallTile(), 'tiles/wall.png'); save(wellTile(), 'tiles/well.png'); +save(chestTile(), 'tiles/chest.png'); console.log('Генерация персонажей:'); save(heroDown1, 'chars/hero_down_1.png'); diff --git a/apps/game/tools/pixelart/sizes.mjs b/apps/game/tools/pixelart/sizes.mjs index 6bcc911..bf28edf 100644 --- a/apps/game/tools/pixelart/sizes.mjs +++ b/apps/game/tools/pixelart/sizes.mjs @@ -19,7 +19,8 @@ }, /** Крупные пропы. */ prop: { - houseBig: [64, 80] + houseBig: [64, 80], + chest: [24, 18] }, /** Кадры героя и NPC (общая сетка атласов chars/*_sheet). */ char: { w: 16, h: 24 }, diff --git a/docs/art-style.md b/docs/art-style.md index 41e32ac..de56f2a 100644 --- a/docs/art-style.md +++ b/docs/art-style.md @@ -123,6 +123,7 @@ | 6 | `tiles/tower.png` | башня Звенца — 32×80, tall-объект 2.5 юнита: бак на ногах-рельсах, колокол B0/B2 под баком | | 7 | `tiles/house.png` | дом Звенца — 32×56, tall-объект 1.75 юнита: стены из вагонных щитов, крыша из шпал | | — | `tiles/house_big.png` | большой дом — 64×80, prop-объект footprint 2×2: два модуля щитов, козырёк над дверью, окна с переплётом | +| — | `tiles/chest.png` | сундук — 24×18, prop интерьеров: вощёное дерево T0/T1, бронзовая полоса по стыку крышки и замок B0/B1/B2 (см. `SIZES.prop.chest`) | | — | `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 — прозрачное поле, рисуются плашмя | diff --git a/docs/engine/maps.md b/docs/engine/maps.md index 1a3e305..fb38d45 100644 --- a/docs/engine/maps.md +++ b/docs/engine/maps.md @@ -44,7 +44,9 @@ tiles: [...], // length = width * height blocked: [TILE_WATER, TILE_TREE], // высота в мировых юнитах (·tileW = px на экране), ground — чем рисовать землю под ним - tall: { [TILE_TREE]: { height: 1.5, ground: TILE_GRASS } } + // shadow — спрайт шириной в тайл экранно накрывает тайл (tx-1,ty-1): + // он блокируется для движения (gridOf учитывает то же — см. collision.ts) + tall: { [TILE_TREE]: { height: 1.5, ground: TILE_GRASS }, [TILE_HOUSE]: { height: 1.75, shadow: true } } }; const map = new IsometricTileMap(data, textures, DEFAULT_ISO); worldRoot.addChild(map.view); diff --git a/docs/engine/practices.md b/docs/engine/practices.md index 8c31fdd..e66c8c1 100644 --- a/docs/engine/practices.md +++ b/docs/engine/practices.md @@ -280,6 +280,11 @@ `agent check` (проба `transitions` проверяет колодцы, `interact` — объекты). 3. Слои снапшота собираются фабриками из `agent/snapshot.ts` — при новом поле снапшота обнови фабрику и тип в одном месте; сцена/фасад дубликаты не пишут. +4. **Клик-зона объекта — не его ромб-пол**: экранно тело спрайта стоит выше + ромба, и клик «проваливается под объект». `RouterDeps.pixelTile(px, py)` — + сцена отдаёт тайл по bbox вьюх (NPC/интерактивы) + ремап на высокий тайл + (тело дома занимает (tx−1,ty−1), отдаём базовому тайлу — двери кликаются по + спрайту); роутер подставляет его ДО `resolveClick` — чистый резолвер не меняется. ## Грабли среды (кратко, подробности в CLAUDE.md) diff --git a/packages/engine/src/map/IsometricTileMap.ts b/packages/engine/src/map/IsometricTileMap.ts index ea364b6..582a176 100644 --- a/packages/engine/src/map/IsometricTileMap.ts +++ b/packages/engine/src/map/IsometricTileMap.ts @@ -1,5 +1,6 @@ import { Container, Graphics, Texture, Sprite } from 'pixi.js'; import { Grid } from './pathfinding'; +import { shadowTilesOf } from './collision'; import { IsoLayout, DEFAULT_ISO, isoToScreen } from '../math/iso'; import { hashTile } from '../math/rng'; @@ -7,10 +8,13 @@ * Высокий объект на тайле: рисуется спрайтом (якорь в центре ромба) или, * если текстуры нет, колонной высотой height мировых юнитов (·tileW px). * ground — id тайла, каким рисовать землю под объектом (по умолчанию — сам объект). + * shadow — спрайт шириной в тайл экранно накрывает тайл «за» объектом + * (tx-1, ty-1): он блокируется для движения (герой не заходит «внутрь» дома). */ export interface TallSpec { height: number; ground?: number; + shadow?: boolean; } /** @@ -113,6 +117,8 @@ private textures: Map; private blockedSet: Set; + /** Тайлы «тени» высоких объектов (tall.shadow) — непроходимы (см. collision). */ + private shadowSet: Set; private tallSpecs: Map; /** Пропы после нормализации; их footprint блокирует клетки карты. */ readonly props: PropPlaced[]; @@ -143,6 +149,7 @@ this.iso = iso; this.textures = textures; this.blockedSet = new Set(data.blocked); + this.shadowSet = shadowTilesOf(data); for (const [id, ids] of Object.entries(data.variants ?? {})) { this.variants.set(Number(id), ids); } @@ -401,6 +408,7 @@ isWalkable(x: number, y: number): boolean { if (x < 0 || y < 0 || x >= this.data.width || y >= this.data.height) return false; if (this.propBlocked.has(y * this.data.width + x)) return false; + if (this.shadowSet.has(y * this.data.width + x)) return false; const id = this.data.tiles[y * this.data.width + x]; return !this.blockedSet.has(id); } diff --git a/packages/engine/src/map/__tests__/collision.test.ts b/packages/engine/src/map/__tests__/collision.test.ts index 1ed35c6..89223bd 100644 --- a/packages/engine/src/map/__tests__/collision.test.ts +++ b/packages/engine/src/map/__tests__/collision.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { circleFits, gridOf, moveCircle, pushOutOfWalls, separateCircles } from '../collision'; +import { circleFits, gridOf, moveCircle, pushOutOfWalls, separateCircles, shadowTilesOf } from '../collision'; import type { Grid } from '../pathfinding'; import type { TileMapData } from '../IsometricTileMap'; @@ -40,6 +40,41 @@ }); }); +describe('shadowTilesOf', () => { + /** Карта 6×6: дом (id 2) в (3,2) с тенью, ещё высокий объект (id 3) в (0,0)... вне поля. */ + function shadowMap(): TileMapData { + const w = 6; + const h = 6; + const tiles = new Array(w * h).fill(0); + tiles[3 * w + 3] = 2; // дом с tall.shadow + tiles[2 * w + 4] = 3; // высокий объект БЕЗ тени + return { + width: w, + height: h, + tiles, + blocked: [2], + tall: { 2: { height: 1.75, shadow: true }, 3: { height: 1 } } + }; + } + + it('тень ложится на тайл за объектом и не блокирует ничего больше', () => { + const shadow = shadowTilesOf(shadowMap()); + expect([...shadow]).toEqual([2 * 6 + 2]); // (2,2) — тайл «за» домом + const g = gridOf(shadowMap()); + expect(g.isWalkable(2, 2)).toBe(false); // тень непроходима + expect(g.isWalkable(3, 3)).toBe(false); // сам дом + expect(g.isWalkable(4, 2)).toBe(true); // высокий без тени не блокирует соседей + expect(g.isWalkable(4, 3)).toBe(true); + }); + + it('два высоких объекта подряд не создают тень друг на друге', () => { + const data = shadowMap(); + data.tiles[2 * 6 + 2] = 3; // высокий сосед прямо в «тени» дома + const shadow = shadowTilesOf(data); + expect([...shadow]).toEqual([]); // сосед высокий — блокируется и без тени + }); +}); + describe('circleFits', () => { it('тело меньше тайла проходит в свободном тайле', () => { const g = gridOf(testMap()); diff --git a/packages/engine/src/map/collision.ts b/packages/engine/src/map/collision.ts index 0934536..a535ce7 100644 --- a/packages/engine/src/map/collision.ts +++ b/packages/engine/src/map/collision.ts @@ -11,6 +11,25 @@ * точек: выборка пропускает угловой прокол Blocked-тайла по диагонали. */ +/** + * Тайлы «тени» высоких объектов: спрайт тайла (tx, ty) с tall.shadow + * экранно накрывает тайл (tx-1, ty-1) — герой не должен заходить в тело + * (дом и т.п.). Сосед в (tx-1, ty-1) сам высокий — он блокируется и без тени. + */ +export function shadowTilesOf(data: TileMapData): Set { + const out = new Set(); + for (let ty = 1; ty < data.height; ty++) { + for (let tx = 1; tx < data.width; tx++) { + const spec = data.tall?.[data.tiles[ty * data.width + tx]!]; + if (typeof spec !== 'object' || !spec.shadow) continue; + const behind = data.tiles[(ty - 1) * data.width + (tx - 1)]!; + if (data.tall?.[behind] !== undefined) continue; + out.add((ty - 1) * data.width + (tx - 1)); + } + } + return out; +} + /** Grid-адаптер над сырыми данными карты: blocked-иды + footprint пропов. */ export function gridOf(data: TileMapData): Grid { const blockedSet = new Set(data.blocked); @@ -22,12 +41,14 @@ for (let tx = p.x; tx < p.x + w; tx++) propBlocked.add(ty * data.width + tx); } } + const shadowSet = shadowTilesOf(data); return { width: data.width, height: data.height, isWalkable: (x, y) => x >= 0 && y >= 0 && x < data.width && y < data.height && !propBlocked.has(y * data.width + x) && + !shadowSet.has(y * data.width + x) && !blockedSet.has(data.tiles[y * data.width + x]!) }; }