tilemap_layer.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. # Flexlay - A Generic 2D Game Editor
  2. # Copyright (C) 2014 Ingo Ruhnke <grumbel@gmail.com>
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. from typing import Any
  17. from PyQt5.sip import voidptr # type: ignore
  18. from flexlay.blitter import blit
  19. from flexlay.color import Color
  20. from flexlay.field import Field
  21. from flexlay.layer import Layer
  22. from flexlay.math import Point, Pointf, Size, Sizef, Rectf
  23. from flexlay.pixel_buffer import PixelBuffer
  24. from flexlay.tile_brush import TileBrush
  25. from flexlay.tileset import Tileset
  26. from flexlay.graphic_context import GraphicContext
  27. class TilemapLayer(Layer):
  28. def __init__(self, tileset: Tileset, w: int, h: int) -> None:
  29. super().__init__()
  30. self.name = "<no name>"
  31. self.tileset = tileset
  32. self.field = Field.from_size(w, h)
  33. self.background_color = Color(0, 0, 0, 0)
  34. self.foreground_color = Color(255, 255, 255, 255)
  35. # FIXME: Move this to the widget or to some more generic map-properties thingy
  36. self.draw_grid = False
  37. self.draw_attribute: bool = False
  38. self.metadata: Any = None
  39. # Do not touch! Use LayerSelector to hide/show layers.
  40. self.hidden = False
  41. for y in range(0, self.field.height):
  42. for x in range(0, self.field.width):
  43. self.field.put(x, y, 0)
  44. def draw(self, gc: GraphicContext, pos: Point = Point(0, 0)) -> None:
  45. if self.hidden:
  46. return
  47. tile_size = self.tileset.get_tile_size()
  48. if False and self.background_color.get_alpha() != 0: # type: ignore[unreachable]
  49. gc.fill_rect(Rectf.from_ps(pos, # type: ignore[unreachable]
  50. Sizef(self.field.width * tile_size,
  51. self.field.height * tile_size)),
  52. self.background_color)
  53. # The visible rectangle
  54. rect = gc.get_clip_rect().to_i()
  55. # max() here stops tiles off screen (below 0) from being drawn
  56. start_x = max(0, pos.x // tile_size)
  57. start_y = max(0, pos.y // tile_size)
  58. # min() here stops tiles off screen (above size of clip rect) from being drawn
  59. end_x = min(self.field.width + pos.x // tile_size, rect.right // tile_size + 1)
  60. end_y = min(self.field.height + pos.y // tile_size, rect.bottom // tile_size + 1)
  61. if self.foreground_color != Color(255, 255, 255, 255):
  62. for y in range(start_y, end_y):
  63. for x in range(start_x, end_x):
  64. # The coordinates on the field are from 0 - field.width
  65. tile_id = self.field.at(x - start_x, y - start_y)
  66. if tile_id:
  67. tile = self.tileset.create(tile_id)
  68. if tile: # skip transparent tile for faster draw
  69. sprite = tile.get_sprite()
  70. sprite.set_color(self.foreground_color)
  71. sprite.draw(x * tile_size, y * tile_size, gc)
  72. if self.draw_attribute:
  73. gc.fill_rect(Rectf.from_ps(Pointf(x, y),
  74. Sizef(self.tileset.get_tile_size(),
  75. self.tileset.get_tile_size())),
  76. tile.get_attribute_color())
  77. else:
  78. for y in range(start_y, end_y):
  79. for x in range(start_x, end_x):
  80. tile_id = self.field.at(x - start_x, y - start_y)
  81. if tile_id: # skip transparent tile for faster draw
  82. tile = self.tileset.create(self.field.at(x - start_x, y - start_y))
  83. if tile:
  84. tile.get_sprite().draw(x * tile_size, y * tile_size, gc)
  85. if self.draw_attribute:
  86. gc.fill_rect(Rectf.from_ps(Pointf(x, y),
  87. Sizef(self.tileset.get_tile_size(),
  88. self.tileset.get_tile_size())),
  89. tile.get_attribute_color())
  90. if self.draw_grid:
  91. for y in range(start_y, end_y):
  92. gc.draw_line(start_x * tile_size,
  93. y * tile_size,
  94. end_x * tile_size,
  95. y * tile_size,
  96. Color(150, 150, 150))
  97. for x in range(start_x, end_x):
  98. gc.draw_line(x * tile_size,
  99. start_y * tile_size,
  100. x * tile_size,
  101. end_y * tile_size,
  102. Color(150, 150, 150))
  103. def get_tile(self, x: int, y: int) -> int:
  104. if 0 <= x < self.field.width and 0 <= y < self.field.height:
  105. return self.field.at(x, y)
  106. else:
  107. return 0
  108. def resize(self, size: Size, point: Point) -> None:
  109. self.field.resize(size.width, size.height, point.x, point.y)
  110. def replace_tile(self, source_id: int, replacement_id: int) -> None:
  111. for y in range(self.field.height):
  112. for x in range(self.field.width):
  113. if self.field.at(x, y) == source_id:
  114. self.field.put(x, y, replacement_id)
  115. def flood_fill_at(self, pos: Point, brush: TileBrush) -> None:
  116. replace_id = self.field.at(pos.x, pos.y)
  117. if replace_id not in brush.field:
  118. self._flood_fill_at(pos.x, pos.y, brush, replace_id)
  119. def _flood_fill_at(self, orig_x: int, orig_y: int, brush: TileBrush, replace_id: int) -> None:
  120. stack = [(orig_x, orig_y)]
  121. def add(x: int, y: int) -> None:
  122. if 0 <= x < self.field.width and 0 <= y < self.field.height:
  123. stack.append((x, y))
  124. while stack:
  125. x, y = stack.pop()
  126. tile_id = self.field.at(x, y)
  127. if tile_id == replace_id:
  128. fill_id = brush.at((x - orig_x) % brush.width,
  129. (y - orig_y) % brush.height)
  130. self.field.put(x, y, fill_id)
  131. add(x + 1, y)
  132. add(x - 1, y)
  133. add(x, y + 1)
  134. add(x, y - 1)
  135. def draw_tile(self, tile_id: int, pos: Point) -> None:
  136. assert isinstance(tile_id, int)
  137. if 0 <= pos.x < self.field.width and 0 <= pos.y < self.field.height:
  138. self.field.put(pos.x, pos.y, tile_id)
  139. # formerly draw_tile()
  140. def draw_tile_brush(self, brush: TileBrush, pos: Point) -> None:
  141. self.draw_tiles(self.field, brush, pos)
  142. @staticmethod
  143. def draw_tiles(field: Field, brush: TileBrush, pos: Point) -> None:
  144. start_x = max(0, -pos.x)
  145. start_y = max(0, -pos.y)
  146. end_x = min(brush.width, field.width - pos.x)
  147. end_y = min(brush.height, field.height - pos.y)
  148. for y in range(start_y, end_y):
  149. for x in range(start_x, end_x):
  150. if brush.is_opaque() or brush.at(x, y) != 0:
  151. field.put(pos.x + x, pos.y + y, brush.at(x, y))
  152. def set_draw_attribute(self, draw_attribute: bool) -> None:
  153. self.draw_attribute = draw_attribute
  154. def get_draw_attribute(self) -> bool:
  155. return self.draw_attribute
  156. def set_draw_grid(self, t: bool) -> None:
  157. self.draw_grid = t
  158. def get_draw_grid(self) -> bool:
  159. return self.draw_grid
  160. def create_pixelbuffer(self) -> PixelBuffer:
  161. tile_size = self.tileset.get_tile_size()
  162. pixelbuffer = PixelBuffer.from_size(Size(self.width * tile_size,
  163. self.height * tile_size))
  164. pixelbuffer.lock()
  165. buf: voidptr = pixelbuffer.get_data()
  166. width = pixelbuffer.width
  167. height = pixelbuffer.height
  168. # Draw a nice gradient
  169. for y in range(height):
  170. for x in range(width):
  171. buf[4 * (y * width + x) + 0] = 255 # type: ignore
  172. buf[4 * (y * width + x) + 1] = 255 # type: ignore
  173. buf[4 * (y * width + x) + 2] = 255 * y // height # type: ignore
  174. buf[4 * (y * width + x) + 3] = 255 * y // height # type: ignore
  175. pixelbuffer.unlock()
  176. for y in range(self.height):
  177. for x in range(self.width):
  178. tile = self.tileset.create(self.field.at(x, y))
  179. if tile:
  180. pbuf = tile.get_pixelbuffer()
  181. if pbuf:
  182. blit(pixelbuffer, pbuf, x * tile_size, y * tile_size)
  183. return pixelbuffer
  184. def get_bounding_rect(self) -> Rectf:
  185. return Rectf(0, 0,
  186. self.field.width * self.tileset.get_tile_size(),
  187. self.field.height * self.tileset.get_tile_size())
  188. def world2tile(self, pos: Pointf) -> Point:
  189. x = int(pos.x / self.tileset.get_tile_size())
  190. y = int(pos.y / self.tileset.get_tile_size())
  191. return Point(x - 1 if (pos.x < 0) else x,
  192. y - 1 if (pos.y < 0) else y)
  193. def get_tileset(self) -> Tileset:
  194. return self.tileset
  195. def get_data(self) -> list[int]:
  196. return list(self.field._data.flatten())
  197. def set_data(self, data: list[int]) -> None:
  198. self.field = Field.from_list(self.field.width, self.field.height, data)
  199. def set_background_color(self, color: Color) -> None:
  200. self.background_color = color
  201. def set_foreground_color(self, color: Color) -> None:
  202. self.foreground_color = color
  203. @property
  204. def width(self) -> int:
  205. return self.field.width
  206. @property
  207. def height(self) -> int:
  208. return self.field.height
  209. def has_bounding_rect(self) -> bool:
  210. return True
  211. # EOF #