SNES-like engine in JavaScript.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

356 lines
10 KiB

  1. #!/usr/bin/env python3
  2. import glob
  3. import json
  4. import os
  5. import pprint # pylint: disable=unused-import
  6. import re
  7. import sys
  8. import pygame
  9. # The guide in tiles/guide.png is quite helpful.
  10. # TODO: sort the stuff in free/ and noncommercial/
  11. # TODO: in future/100/characters there are some doors, conveyor belts, screens
  12. SPRITE_FILES = [
  13. 'animals/sheets/*.png',
  14. 'beasttribes/100/*.png',
  15. 'characters/sheets/*.png',
  16. 'christmas/1x/gnome*.png',
  17. 'christmas/1x/reindeer*.png',
  18. 'christmas/1x/rudolph*.png',
  19. 'christmas/1x/xmas*.png',
  20. 'dwarvesvselves/regularsize/*.png',
  21. 'future/100/characters/cars.png',
  22. 'future/100/characters/future*.png',
  23. 'future/100/characters/military*.png',
  24. 'future/100/characters/modern*.png',
  25. 'halloween/ghost1.png',
  26. 'halloween/horseman/*1.png',
  27. 'halloween/reaper/*1.png',
  28. 'halloween/witch/1x/*.png',
  29. 'lichcrusades/100/*.png',
  30. 'monsters/1x/*.png',
  31. 'mythicalbosses/100/*.png',
  32. 'mythicalbosses/dinosaurs/*.png',
  33. 'npcanimations/rpgmaker/1/*.png',
  34. 'ship/100/char/airship*.png',
  35. 'ship/100/char/boat*.png',
  36. 'ship/100/char/pirates_100.png',
  37. 'ship/100/char/ship*.png',
  38. ]
  39. SPRITE_SIDEVIEW_FILES = [
  40. 'beasttribes/100/sv_battler/*.png',
  41. 'future/100/svbattler/*.png',
  42. # TODO: these need to get scaled down 2x before they can be used.
  43. 'sv_battle/RMMV/sv_actors/*.png',
  44. ]
  45. TILESET_FILES = [
  46. 'ashlands/ashlands_tileset.png',
  47. 'atlantis/tf_atlantis_tiles.png',
  48. 'beach/beach_tileset.png',
  49. 'christmas/1x/addon_igloo_1.png',
  50. 'christmas/1x/christmas*.png',
  51. 'cloud/cloud_tileset.png',
  52. 'darkdimension/tf_darkdimension_sheet.png',
  53. 'farmandfort/ff_master_tile_sheet.png',
  54. 'future/100/tilesets/*.png',
  55. 'gianttree/tf_gianttree_tiles.png',
  56. 'halloween/tiles/*1.png',
  57. 'jungle/tf_jungle_tileset.png',
  58. 'patron/train_sheet_1.png',
  59. 'ruindungeons/ruindungeons_sheet_full.png',
  60. 'ship/ship_big_tileset.png',
  61. 'tiles/TILESETS/*.png',
  62. 'winter/tiles/*.png',
  63. ]
  64. ANIMATION_FILES = [
  65. 'patron/fireworks*_1.png',
  66. 'pixelanimations/animationsheets/*.png',
  67. 'ship/100/char/!$ship_wave*.png',
  68. 'sv_battle/RMMV/system/States.png',
  69. 'tiles/TILESETS/animated/*.png',
  70. ]
  71. ICON_FILES = [
  72. 'farmandfort/IconSet/tf_icon_16.png',
  73. 'halloween/hallowicons_1.png',
  74. ]
  75. BACKGROUND_FILES = [
  76. 'cloud/bg_*.png',
  77. 'future/100/other/spacebg.png',
  78. 'ship/100/parallax/*.png'
  79. ]
  80. def unglob(list_of_globs):
  81. result = []
  82. for file in list_of_globs:
  83. globbed_files = glob.glob(file)
  84. assert globbed_files, 'glob for %s should be non-empty' % file
  85. result.extend(globbed_files)
  86. result.sort()
  87. return result
  88. def input_wh(prompt):
  89. while True:
  90. geometry = input(prompt).strip()
  91. try:
  92. cols, rows = [int(x) for x in geometry.split(' ')]
  93. return cols, rows
  94. except ValueError:
  95. pass
  96. # Returns True or False.
  97. def input_ok(prompt):
  98. while True:
  99. ok = input(prompt).strip()
  100. if ok.startswith('y'):
  101. return True
  102. if ok.startswith('n'):
  103. return False
  104. def draw_checkerboard(size):
  105. surface = pygame.display.get_surface()
  106. surface.fill((224, 224, 224))
  107. for i in range(surface.get_width() // size + 1):
  108. for j in range(surface.get_height() // size + 1):
  109. if (i + j) % 2 == 0:
  110. continue
  111. rect = pygame.Rect(i * size, j * size, size, size)
  112. surface.fill((192, 192, 192), rect)
  113. def show_splits(image_width, image_height, cols, rows):
  114. surface = pygame.display.get_surface()
  115. split_width = image_width / cols
  116. split_height = image_height / rows
  117. for i in range(cols):
  118. for j in range(rows):
  119. rect = pygame.Rect(
  120. i * split_width, j * split_height, split_width + 1, split_height + 1)
  121. pygame.draw.rect(surface, (255, 0, 255), rect, 1)
  122. pygame.display.flip()
  123. def render_text(text, pos, color):
  124. surface = pygame.display.get_surface()
  125. font = pygame.font.SysFont('notomono', 16)
  126. image = font.render(text, True, color)
  127. surface.blit(image, pos)
  128. pygame.display.flip()
  129. def render_sprite(metadata):
  130. line_color = (255, 0, 255)
  131. surface = pygame.display.get_surface()
  132. draw_checkerboard(8)
  133. image = pygame.image.load(metadata['filename'])
  134. surface.blit(image, (0, 0))
  135. if metadata.get('chunks'):
  136. for chunk in metadata['chunks']:
  137. rect = pygame.Rect(
  138. chunk['x'], chunk['y'], chunk['width'] + 1, chunk['height'] + 1)
  139. pygame.draw.rect(surface, line_color, rect, 1)
  140. label_pos = (chunk['x'] + 4, chunk['y'])
  141. render_text(str(chunk['index']), label_pos, line_color)
  142. caption_pos = (4, 4 + metadata['image_height'] + chunk['index'] * 20)
  143. caption = '%d: %s' % (chunk['index'], chunk.get('name', ''))
  144. render_text(caption, caption_pos, (0, 0, 0))
  145. pygame.display.flip()
  146. def set_sprite_chunk_size(metadata):
  147. cols, rows = input_wh('how many columns & rows of sprites? ')
  148. metadata['chunk_columns'] = cols
  149. metadata['chunk_rows'] = rows
  150. metadata['chunk_width'] = metadata['image_width'] // cols
  151. metadata['chunk_height'] = metadata['image_height'] // rows
  152. metadata['chunks'] = []
  153. for i in range(cols * rows):
  154. x = i % cols
  155. y = i // cols
  156. chunk_md = {
  157. 'index': i,
  158. 'x': x * metadata['chunk_width'],
  159. 'y': y * metadata['chunk_height'],
  160. 'width': metadata['chunk_width'],
  161. 'height': metadata['chunk_height']
  162. }
  163. metadata['chunks'].append(chunk_md)
  164. render_sprite(metadata)
  165. def edit_sprite_chunk_metadata(chunk):
  166. while True:
  167. name = input('name for chunk #%d: ' % chunk['index']).strip()
  168. if re.fullmatch(r'\w+', name):
  169. chunk['name'] = name
  170. return
  171. def edit_sprite_metadata(filename, metadata=None):
  172. if metadata is None:
  173. image = pygame.image.load(filename)
  174. metadata = {
  175. 'filename': filename,
  176. 'image_width': image.get_width(),
  177. 'image_height': image.get_height(),
  178. }
  179. print('\nprocessing %s (%dx%d)' % (
  180. filename, metadata['image_width'], metadata['image_height']))
  181. render_sprite(metadata)
  182. if not metadata.get('chunk_width'):
  183. set_sprite_chunk_size(metadata)
  184. while True:
  185. render_sprite(metadata)
  186. prompt = 'edit (c)hunk sizes, type a chunk #, (n)ext, or (q)uit: '
  187. choice = input(prompt).strip()
  188. if choice == 'n':
  189. return metadata, False
  190. elif choice == 'q':
  191. return metadata, True
  192. elif choice == 'c':
  193. set_sprite_chunk_size(metadata)
  194. elif re.fullmatch(r'\d+', choice):
  195. chunk_num = int(choice)
  196. if 0 <= chunk_num < len(metadata['chunks']):
  197. edit_sprite_chunk_metadata(metadata['chunks'][chunk_num])
  198. else:
  199. print('invalid chunk #')
  200. else:
  201. print('invalid choice')
  202. def annotate_sprites(all_metadata, sprite_files):
  203. pygame.init()
  204. pygame.display.set_mode((1200, 900), pygame.RESIZABLE)
  205. for filename in sprite_files:
  206. sprite_metadata, should_quit = edit_sprite_metadata(
  207. filename, all_metadata.get(filename))
  208. all_metadata[filename] = sprite_metadata
  209. with open('sprites.json', 'w') as f:
  210. json.dump(all_metadata, f, sort_keys=True, indent=2)
  211. if should_quit:
  212. return
  213. def get_named_sprites(metadata):
  214. result = {}
  215. for filename in metadata:
  216. sprite = metadata[filename]
  217. for chunk in sprite.get('chunks', []):
  218. name = chunk.get('name', '')
  219. if not name:
  220. continue
  221. if name in result:
  222. print('warning: duplicated sprite name ', name)
  223. sys.exit(1)
  224. result[name] = chunk
  225. return result
  226. def check_sprites(metadata):
  227. named_sprites = get_named_sprites(metadata)
  228. print('# named sprites:', len(named_sprites))
  229. with open('sprites.json', 'w') as f:
  230. json.dump(metadata, f, sort_keys=True, indent=2)
  231. def stitch_sprites(metadata, filename_base, sprite_names=None):
  232. sprites = get_named_sprites(metadata)
  233. if sprite_names:
  234. sprites = dict([(x[0], x[1])
  235. for x in sprites.items() if x[0] in sprite_names])
  236. max_height = 0
  237. total_width = 0
  238. for sprite_name, sprite in sprites.items():
  239. total_width += sprite['width']
  240. max_height = max(max_height, sprite['height'])
  241. print('\n# named sprites:', len(sprites))
  242. print('result will be %dx%d' % (total_width, max_height))
  243. output = pygame.surface.Surface(
  244. (total_width, max_height), flags=pygame.SRCALPHA)
  245. output_json = {}
  246. xpos = 0
  247. for sprite_name, sprite in sprites.items():
  248. sprite_image = pygame.image.load(sprite['filename'])
  249. area = pygame.Rect(
  250. sprite['x'], sprite['y'], sprite['width'], sprite['height'])
  251. output_json[sprite_name] = {
  252. 'name': sprite['name'],
  253. 'x': xpos,
  254. 'y': 0,
  255. 'width': sprite['width'],
  256. 'height': sprite['height']
  257. }
  258. output.blit(sprite_image, (xpos, 0), area)
  259. xpos += sprite['width']
  260. image_filename = os.path.expanduser(filename_base) + '.png'
  261. print('saving image to', image_filename)
  262. pygame.image.save(output, image_filename)
  263. json_filename = os.path.expanduser(filename_base) + '.js'
  264. print('saving json to', json_filename)
  265. with open(json_filename, 'w') as json_file:
  266. json_file.write('const spritesheet_json = ')
  267. json.dump(output_json, json_file, sort_keys=True, indent=2)
  268. json_file.write(';')
  269. def main(args):
  270. snej_root = os.environ.get('SNEJ_ROOT', os.path.join('~', 'snej'))
  271. time_fantasy_path = os.path.join(snej_root, 'assets', 'time_fantasy')
  272. os.chdir(os.path.expanduser(time_fantasy_path))
  273. sprite_files = unglob(SPRITE_FILES)
  274. tileset_files = unglob(TILESET_FILES)
  275. animation_files = unglob(ANIMATION_FILES)
  276. icon_files = unglob(ICON_FILES)
  277. background_files = unglob(BACKGROUND_FILES)
  278. print('\nsprites: %d tilesets: %d animations: %d icons: %d backgrounds: %d' %
  279. (len(sprite_files), len(tileset_files), len(animation_files),
  280. len(icon_files), len(background_files)))
  281. if len(args) < 1:
  282. return
  283. command = args[0]
  284. with open('sprites.json') as f:
  285. metadata = json.load(f)
  286. if command == 'annotate-sprites':
  287. annotate_sprites(metadata, sprite_files)
  288. elif command == 'check-sprites':
  289. check_sprites(metadata)
  290. elif command == 'stitch-sprites':
  291. if len(args) < 2:
  292. print('need FILENAME_BASE')
  293. return
  294. filename_base = args[1]
  295. sprite_names = args[2:]
  296. stitch_sprites(metadata, filename_base, sprite_names)
  297. else:
  298. print('unrecognized command "%s"' % command)
  299. if __name__ == '__main__':
  300. main(sys.argv[1:])