'OHRRPGCE CUSTOM - Spriteset, tileset, and backdrop editors and browsers '(C) Copyright 1997-2025 James Paige, Ralph Versteegen, and the OHRRPGCE Developers 'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability. #include "config.bi" #include "udts.bi" #include "custom_udts.bi" #include "allmodex.bi" #include "common.bi" #include "loading.bi" #include "customsubs.bi" #include "cglobals.bi" #include "const.bi" #include "custom.bi" #include "sliceedit.bi" #include "plankmenu.bi" CONST COLORNUM_SHOW_TICKS = 30 'External subs and functions DECLARE SUB loadpasdefaults (byref defaults as integer vector, tilesetnum as integer) DECLARE SUB savepasdefaults (byref defaults as integer vector, tilesetnum as integer) 'Local SUBs and FUNCTIONS DECLARE SUB airbrush (spr as Frame ptr, byval x as integer, byval y as integer, byval d as integer, byval m as integer, byval c as integer) DECLARE FUNCTION pick_image_pixel(image as Frame ptr, pal16 as Palette16 ptr = NULL, byref pickpos as XYPair, zoom as integer = -1, maxx as integer = 9999, maxy as integer = 9999, message as string, helpkey as string) as bool DECLARE FUNCTION mouseover (byval mousex as integer, byval mousey as integer, byref zox as integer, byref zoy as integer, byref zcsr as integer, area() as MouseArea) as integer DECLARE FUNCTION importimage_process(filename as string, pmask() as RGBcolor) as Frame ptr DECLARE FUNCTION importimage_importmxs(mxslump as string, imagenum as integer, srcfile as string, pmask() as RGBcolor) as bool DECLARE FUNCTION importimage_change_background_color(img as Frame ptr, pal as Palette16 ptr = NULL) as bool DECLARE SUB select_disabled_import_colors(pmask() as RGBcolor, image as Frame ptr) ' Tileset editor DECLARE SUB crop_tilesets(to_id as integer) DECLARE SUB picktiletoedit (byref tmode as integer, byval tilesetnum as integer, mapfile as string, bgcolor as bgType) DECLARE SUB editmaptile (ts as TileEditState, mouse as MouseInfo, area() as MouseArea, bgcolor as bgType) DECLARE SUB tilecut (ts as TileEditState, mouse as MouseInfo) DECLARE SUB refreshtileedit (state as TileEditState) DECLARE SUB writeundoblock (state as TileEditState) DECLARE SUB readundoblock (state as TileEditState) DECLARE SUB fliptile (ts as TileEditState) DECLARE SUB scrolltile (ts as TileEditState, byval shiftx as integer, byval shifty as integer) DECLARE SUB clicktile (ts as TileEditState, newkeypress as integer, toolinfo() as ToolInfoType, byref clone as TileCloneBuffer) DECLARE SUB tilecopy (cutnpaste() as integer, ts as TileEditState) DECLARE SUB tilepaste (cutnpaste() as integer, ts as TileEditState) DECLARE SUB tiletranspaste (cutnpaste() as integer, ts as TileEditState) DECLARE SUB copymapblock (sx as integer, sy as integer, sp as integer, dx as integer, dy as integer, dp as integer) DECLARE SUB tileedit_set_tool (ts as TileEditState, toolinfo() as ToolInfoType, byval toolnum as integer) DECLARE SUB tileedit_show_neighbouring_tiles(byref ts as TileEditState, byval bgcolor as bgType, byval chequer_scroll as integer) DECLARE SUB tileedit_show_tile_tiled(byref ts as TileEditState, byval bgcolor as bgType, byval chequer_scroll as integer) ' Tileset animation editor DECLARE SUB testanimpattern (tanim() as TileAnimPattern, byref taset as integer) DECLARE SUB setanimpattern (tanim() as TileAnimPattern, taset as integer, tilesetnum as integer) DECLARE SUB setanimpattern_refreshmenu(state as MenuState, state2 as MenuState, menu() as string, menu2() as string, tanim() as TileAnimPattern, byval taset as integer, animop() as string, numargs() as integer, llim() as integer, ulim() as integer) DECLARE SUB setanimpattern_forcebounds(tanim() as TileAnimPattern, byval taset as integer, numargs() as integer, llim() as integer, ulim() as integer) DECLARE SUB tile_anim_set_range(tanim() as TileAnimPattern, byval taset as integer, byval tilesetnum as integer) DECLARE SUB tile_animation(byval tilesetnum as integer) DECLARE SUB tile_edit_mode_picker(byval tilesetnum as integer, mapfile as string, byref bgcolor as bgType) ' Sprite editor DECLARE SUB sprite_editor(ss as SpriteEditState) DECLARE SUB sprite_editor_initialise(byref ss as SpriteEditState) DECLARE SUB sprite_editor_update_for_sprite_size(byref ss as SpriteEditState, sprite as Frame ptr) DECLARE SUB sprite_editor_cleanup(byref ss as SpriteEditState) DECLARE SUB init_sprite_zones(area() as MouseArea, ss as SpriteEditState) DECLARE SUB textcolor_icon(selected as bool, hover as bool) DECLARE SUB spriteedit_draw_icon(ss as SpriteEditState, icon as string, byval areanum as integer, byval highlight as bool = NO) DECLARE SUB spriteedit_draw_palette(pal16 as Palette16 ptr, x as integer, y as integer, page as integer) DECLARE SUB spriteedit_draw_sprite_area(ss as SpriteEditState, sprite as Frame ptr, pal as Palette16 ptr, page as integer) DECLARE SUB spriteedit_display(ss as SpriteEditState) DECLARE SUB spriteedit_scroll (ss as SpriteEditState, byval shiftx as integer, byval shifty as integer) DECLARE SUB spriteedit_reset_tool(byref ss as SpriteEditState) DECLARE SUB spriteedit_strait_line(byref ss as SpriteEditState) DECLARE SUB spriteedit_draw_square(byref ss as SpriteEditState) DECLARE SUB spriteedit_draw_oval(byref ss as SpriteEditState) DECLARE SUB spriteedit_put_dot(byref ss as SpriteEditState) DECLARE SUB spriteedit_spray_spot(byref ss as SpriteEditState) DECLARE SUB spriteedit_replace_col(byref ss as SpriteEditState) DECLARE SUB spriteedit_flood_fill(byref ss as SpriteEditState) DECLARE SUB spriteedit_sprctrl(byref ss as SpriteEditState) DECLARE SUB spriteedit_clip (ss as SpriteEditState) DECLARE SUB changepal (ss as SpriteEditState, palchange as integer) DECLARE SUB writeundospr (ss as SpriteEditState) DECLARE SUB readundospr (ss as SpriteEditState) DECLARE SUB readredospr (ss as SpriteEditState) ' Sprite import/export DECLARE FUNCTION spriteedit_import16(byref ss as SpriteEditState) as Frame ptr DECLARE SUB spriteedit_export (default_name as string, spr as Frame ptr, pal as Palette16 ptr) DECLARE FUNCTION default_export_name (sprtype as SpriteType, setnum as integer, framenum as integer = -1, fullset as bool = NO) as string ' Spriteset editor DECLARE SUB spriteset_resize_menu_rebuild(byref root as Slice ptr, ss as Frame ptr, pal as integer) ' Locals DIM SHARED ss_save as SpriteEditStatic WITH ss_save .tool = draw_tool .airsize = 5 .mist = 4 .palindex = 1 .hidemouse = NO END WITH SUB airbrush (spr as Frame ptr, byval x as integer, byval y as integer, byval d as integer, byval m as integer, byval c as integer) 'airbrush thanks to Ironhoof (Russel Hamrick) 'AirBrush this rutine works VERY well parameters as fallows: ' AIRBRUSH sprite , x , y , diameter , mist_amount , color ' diameter sets the width & hight by square radius ' mist_amount sets how many pixels to place i put 100 and it ran fast so ' it works EXCELLENTLY with a mouse on the DTE =) FOR count as integer = 0 TO randint(m) DIM x2 as integer = randint(d + 1) DIM y2 as integer = randint(d + 1) DIM x3 as integer = x - d / 2 DIM y3 as integer = y - d / 2 IF ABS((x3 + x2) - x) ^ 2 + ABS((y3 + y2) - y) ^ 2 <= d ^ 2 / 4 THEN putpixel spr, x3 + x2, y3 + y2, c END IF NEXT END SUB ' Save current palette and load another one. When palchange=0, just saves current SUB changepal OVERLOAD (ss as SpriteEditState, palchange as integer) palette16_save ss.palette, ss.pal_num 'Note: bounding to gen(genMaxPal) would do nothing, because the 'sprite editor increases gen(genMaxPal) whenever you reach the end ss.pal_num = bound(ss.pal_num + palchange, 0, 32767) palette16_unload @ss.palette ss.palette = palette16_load(ss.pal_num, , , NO) 'expect_exists=NO END SUB FUNCTION pal_num_intgrabber (ss as SpriteEditState, lesskey as KBScancode=ccLeft, morekey as KBScancode=ccRight) as bool DIM old as integer = ss.pal_num IF intgrabber(ss.pal_num, 0, gen(genMaxPal) + 1, lesskey, morekey) THEN palette16_save ss.palette, old palette16_unload @ss.palette ss.palette = palette16_load(ss.pal_num, , , NO) 'expect_exists=NO RETURN YES END IF END FUNCTION 'Pick ss.pal_num with the palette browser, saving/loading before/after SUB spriteedit_pal16_browser (ss as SpriteEditState, sprite as Frame ptr) '--write changes so far ss.save_callback(ss.spriteset, ss.save_callback_context, ss.pal_num) '--save current palette palette16_save ss.palette, ss.pal_num ss.pal_num = pal16browse(ss.pal_num, sprite) 'Can return > genMaxPal palette16_unload @ss.palette ss.palette = palette16_load(ss.pal_num, , , NO) 'expect_exists=NO END SUB 'Copy a tile from one vpage to another SUB copymapblock (sx as integer, sy as integer, sp as integer, dx as integer, dy as integer, dp as integer) DIM srctile as Frame ptr srctile = frame_new_view(vpages(sp), sx, sy, 20, 20) frame_draw srctile, NULL, dx, dy, NO, dp frame_unload @srctile END SUB 'Used inside select_disabled_import_colors LOCAL SUB toggle_pmask (pmask() as RGBcolor, master() as RGBcolor, index as integer) pmask(index).r xor= master(index).r pmask(index).g xor= master(index).g pmask(index).b xor= master(index).b setpal pmask() END SUB ' The "Import/Export Tilesets" menu ' (Previously it was also for importing/exporting backdrops, replaced by backdrop_browser) SUB import_export_tilesets () DIM filename as string = game + ".til" STATIC defaultdir as string 'Import & export STATIC bgcolor as bgType = 0 'Default to not transparent (color 0) DIM chequer_scroll as integer = 0 DIM pmask(255) as RGBcolor DIM menu(8) as string DIM mstate as MenuState mstate.size = 24 mstate.last = UBOUND(menu) DIM menuopts as MenuOptions menuopts.edged = YES menu(0) = "Previous Menu" menu(1) = CHR(27) + "Browse 0" + CHR(26) menu(2) = "Replace current tileset" menu(3) = "Append a new tileset" menu(4) = "Disable palette colors for import" menu(5) = "Export tileset" menu(6) = "View with background: " & bgcolor_caption(bgcolor) menu(7) = "Remap transparent color" menu(8) = "Full screen view" DIM srcfile as string DIM pt as integer = 0 'backdrop number DIM byref count as integer = gen(genMaxTile) ' FIXME: We still use vpages(2) to store the tileset, and also if it is resized ' the pointer passed to importimage_change_background_color would become invalid! lock_page_size 2, 320, 200 loadpalette pmask(), activepalette loadmxs filename, pt, vpages(2) setkeys DO setwait 55, 110 setkeys IF cropafter_keycombo(mstate.pt = 1) THEN crop_tilesets pt IF keyval(ccCancel) > 1 THEN EXIT DO IF keyval(scF1) > 1 THEN show_help "importimage" usemenu mstate IF mstate.pt = 6 THEN intgrabber(bgcolor, bgFIRST, 255) menu(6) = "View with background: " & bgcolor_caption(bgcolor) ELSE IF intgrabber(pt, 0, count) THEN menu(1) = CHR(27) + "Browse " & pt & CHR(26) loadmxs filename, pt, vpages(2) END IF END IF IF enter_space_click(mstate) THEN IF mstate.pt = 0 THEN EXIT DO IF mstate.pt = 2 THEN 'Replace current srcfile = browse(browseTileset, defaultdir, , "browse_import_tileset") IF srcfile <> "" THEN importimage_importmxs(filename, pt, srcfile, pmask()) END IF loadmxs filename, pt, vpages(2) END IF IF mstate.pt = 3 AND count < 32767 THEN 'Append new srcfile = browse(browseTileset, defaultdir, , "browse_import_tileset") IF srcfile <> "" THEN IF importimage_importmxs(filename, count + 1, srcfile, pmask()) THEN count += 1 pt = count END IF END IF menu(1) = CHR(27) + "Browse " & pt & CHR(26) loadmxs filename, pt, vpages(2) END IF IF mstate.pt = 4 THEN select_disabled_import_colors pmask(), vpages(2) END IF IF mstate.pt = 5 THEN DIM extension as string = "." & pick_graphics_export_format() DIM outfile as string outfile = inputfilename("Name of file to export to?", extension, defaultdir, "input_file_export_screen", game_fname & " tileset" & pt) '--Re-load the page to vpages(2) just in case it got clobbered by inputfilename() calling the file browser loadmxs filename, pt, vpages(2) IF outfile <> "" THEN frame_export_image vpages(2), outfile & extension, master() END IF IF mstate.pt = 6 THEN bgcolor = color_browser_256(large(bgcolor, 0)) menu(6) = "View with background: " & bgcolor_caption(bgcolor) END IF IF mstate.pt = 7 THEN importimage_change_background_color vpages(2) storemxs filename, pt, vpages(2) END IF END IF '--end enter_space_click() clearpage dpage frame_draw_with_background vpages(2), , 0, 0, bgcolor, chequer_scroll, vpages(dpage) IF mstate.pt <> 8 THEN standardmenu menu(), mstate, , , dpage, menuopts END IF SWAP vpage, dpage setvispage vpage IF dowait THEN chequer_scroll += 1 LOOP unlock_page_size 2 clearpage 2 sprite_update_cache sprTypeTileset END SUB ' This is the new browser/editor for backdrops. SUB backdrop_browser () STATIC default as string DIM pmask(255) as RGBcolor DIM srcfile as string DIM backdrop_id as integer = 0 DIM bgcolor as bgType = 0 'bgChequer DIM chequer_scroll as integer DIM menu(9) as string DIM mstate as MenuState mstate.last = UBOUND(menu) mstate.need_update = YES DIM menuopts as MenuOptions menuopts.edged = YES menuopts.drawbg = YES DIM byref count as integer = gen(genNumBackdrops) IF count = 0 THEN count = 1 loadpalette pmask(), activepalette DIM backdrops_file as string = graphics_file(rgfx_lumpnames(sprTypeBackdrop)) DIM rgfx_doc as DocPtr = rgfx_open(backdrops_file) DIM backdrop as Frame ptr backdrop = rgfx_load_spriteset(rgfx_doc, sprTypeBackdrop, backdrop_id) 'Shouldn't happen unless the .rgfx is bad, but avoid crashing. Leaks the doc IF backdrop = NULL THEN showerror "Failed to load any backdrop" : EXIT SUB setkeys DO setwait 55 setkeys chequer_scroll += 1 IF keyval(scCtrl) > 0 AND keyval(scBackspace) > 1 THEN DIM crop_this as integer = count - 1 ' FIXME: Not implemented count = crop_this + 1 END IF IF keyval(ccCancel) > 1 THEN EXIT DO IF keyval(scF1) > 1 THEN show_help "backdrop_browser" usemenu mstate IF mstate.pt <> 6 ANDALSO intgrabber(backdrop_id, 0, count - 1) THEN ' Change selected backdrop frame_assign @backdrop, rgfx_load_spriteset(rgfx_doc, sprTypeBackdrop, backdrop_id) IF backdrop = NULL THEN showbug "rgfx failed to load" : EXIT DO mstate.need_update = YES END IF IF enter_space_click(mstate) THEN IF mstate.pt = 0 THEN EXIT DO IF mstate.pt = 2 THEN 'Replace current srcfile = browse(browseImage, default, , "browse_import_backdrop") IF srcfile <> "" THEN DIM imported as Frame ptr = importimage_process(srcfile, pmask()) IF imported THEN frame_assign @backdrop, imported rgfx_save_spriteset rgfx_doc, backdrop, sprTypeBackdrop, backdrop_id SerializeBin backdrops_file, rgfx_doc END IF mstate.need_update = YES END IF END IF IF mstate.pt = 3 AND count < 32767 THEN 'Append new srcfile = browse(browseImage, default, , "browse_import_backdrop") IF srcfile <> "" THEN DIM imported as Frame ptr = importimage_process(srcfile, pmask()) IF imported THEN frame_assign @backdrop, imported backdrop_id = count count = backdrop_id + 1 rgfx_save_spriteset rgfx_doc, backdrop, sprTypeBackdrop, backdrop_id SerializeBin backdrops_file, rgfx_doc mstate.need_update = YES END IF END IF END IF IF mstate.pt = 4 THEN select_disabled_import_colors pmask(), backdrop END IF IF mstate.pt = 5 THEN DIM outfile as string DIM extension as string = "." & pick_graphics_export_format() outfile = inputfilename("Name of file to export to?", extension, "", "input_file_export_screen", _ game_fname & " backdrop" & backdrop_id) IF outfile <> "" THEN frame_export_image backdrop, outfile & extension, master() END IF IF mstate.pt = 7 THEN importimage_change_background_color backdrop rgfx_save_spriteset rgfx_doc, backdrop, sprTypeBackdrop, backdrop_id SerializeBin backdrops_file, rgfx_doc END IF END IF '--end enter_space_click() IF mstate.pt = 6 THEN mstate.need_update OR= intgrabber(bgcolor, bgFIRST, 255) IF mstate.need_update THEN mstate.need_update = NO menu(0) = "Previous Menu" menu(1) = CHR(27) + "Backdrop " & backdrop_id & CHR(26) menu(2) = "Replace current backdrop" menu(3) = "Append a new backdrop" menu(4) = "Disable palette colors for import" menu(5) = "Export backdrop" menu(6) = "View with background: " & bgcolor_caption(bgcolor) menu(7) = "Remap transparent color" menu(8) = "Hide menu" menu(9) = "Size: " & backdrop->w & " x " & backdrop->h END IF clearpage vpage 'Show the size of the backdrop by outlining it drawbox pRight + 1, pBottom + 1, backdrop->w + 2, backdrop->h + 2, uilook(uiMenuItem), 1, vpage frame_draw_with_background backdrop, , pRight + showLeft, pBottom + showTop, bgcolor, chequer_scroll, vpages(vpage) IF mstate.pt <> 8 THEN standardmenu menu(), mstate, , , vpage, menuopts END IF setvispage vpage dowait LOOP frame_unload @backdrop 'Already saved the doc FreeDocument rgfx_doc sprite_update_cache sprTypeBackdrop END SUB ' Display an image and select which colours in a copy ' of the master palette to set to black. LOCAL SUB select_disabled_import_colors(pmask() as RGBcolor, image as Frame ptr) DIM tog as integer DIM prev_menu_selected as bool ' "Previous Menu" is current selection DIM cx as integer DIM cy as integer DIM image_pos as XYPair hidemousecursor setpal pmask() setkeys DO setwait 55 setkeys tog = tog XOR 1 image_pos = get_resolution() - image->size WITH readmouse() IF .release AND mouseleft THEN IF rect_collide_point(str_rect("Previous Menu", pMenuX, pMenuY), .pos) THEN EXIT DO ELSE DIM rect as RectType rect.wide = 10 '2 pixels wider than real squares, to avoid gaps rect.high = 10 DIM col as integer = -1 'Click on a palette colour FOR xidx as integer = 0 TO 15 FOR yidx as integer = 0 TO 15 rect.topleft = XY(pMenuX + xidx * 10, pMenuY + 8 + yidx * 10) IF rect_collide_point(rect, .pos) THEN col = yidx * 16 + xidx NEXT NEXT 'Click on an image pixel (safe if the position is off the edge of the image) IF col = -1 THEN col = readpixel(image, .x - image_pos.x, .y - image_pos.y) toggle_pmask pmask(), master(), col END IF END IF END WITH IF keyval(ccCancel) > 1 THEN EXIT DO IF keyval(scF1) > 1 THEN show_help "importimage_disable" IF prev_menu_selected THEN IF enter_or_space() THEN EXIT DO IF keyval(ccDown) > 1 THEN cy = 0 prev_menu_selected = NO END IF ELSE IF keyval(ccLeft) > 1 THEN cx = large(cx - 1, 0) IF keyval(ccRight) > 1 THEN cx = small(cx + 1, 15) IF keyval(ccDown) > 1 THEN cy = small(cy + 1, 15) IF keyval(ccUp) > 1 THEN cy -= 1 IF cy < 0 THEN cy = 0 prev_menu_selected = YES END IF IF enter_or_space() THEN toggle_pmask pmask(), master(), cy * 16 + cx END IF END IF clearpage dpage frame_draw image, , image_pos.x, image_pos.y, NO, dpage textcolor uilook(uiMenuItem), 0 IF prev_menu_selected THEN textcolor uilook(uiSelectedItem + tog), 0 printstr "Previous Menu", pMenuX, pMenuY, dpage IF prev_menu_selected = NO THEN rectangle pMenuX + cx * 10, pMenuY + 8 + cy * 10, 10, 10, uilook(uiSelectedItem + tog), dpage FOR i as integer = 0 TO 15 FOR o as integer = 0 TO 15 rectangle pMenuX + 1 + o * 10, pMenuY + 9 + i * 10, 8, 8, i * 16 + o, dpage NEXT o NEXT i printstr CHR(2), readmouse.x - 2, readmouse.y - 2, dpage SWAP vpage, dpage setvispage vpage dowait LOOP setpal master() showmousecursor END SUB 'Give the user the chance to remap a color to 0. 'Returns true if they picked a color (even 0), rather than skipping/cancelling FUNCTION importimage_change_background_color(img as Frame ptr, pal as Palette16 ptr = NULL) as bool DIM pickpos as XYPair DIM ret as bool DIM message as string = !"Pick the background (transparent) color\nor press ESC to leave color 0 as it is" ret = pick_image_pixel(img, pal, pickpos, , , , message, "importimage_pickbackground") IF ret = NO THEN RETURN NO DIM bgcol as integer = readpixel(img, pickpos.x, pickpos.y) IF bgcol = 0 THEN RETURN YES 'Boring! IF countcolor(img, 0) > 0 THEN DIM nearest as integer = nearcolor(master(), 0, 1) DIM choice as integer choice = twochoice("What should I do with the existing color 0 (transparent) pixels? (This is probably irreversible! ESC to cancel)", _ "Leave them alone", "Remap to nearest match (color " & fgtag(nearest, STR(nearest)) & ")") IF choice = -1 THEN RETURN NO IF choice = 1 THEN ' If we want to map 0 to nearest and nearest to 0, need to do an atomic swap IF nearest = bgcol THEN swapcolors img, bgcol, 0 RETURN YES ELSE replacecolor img, 0, nearest END IF END IF END IF replacecolor img, bgcol, 0 RETURN YES END FUNCTION TYPE DitherMenu EXTENDS ModularMenu filename as string pmask(255) as RGBcolor imported as Frame ptr accept as bool dither_amount as integer DECLARE SUB update() DECLARE SUB draw() DECLARE FUNCTION each_tick() as bool END TYPE SUB DitherMenu.update() REDIM menu(5) menu(0) = "With dithering" menu(1) = "Less dithering" menu(2) = "Minimal dithering" menu(3) = "Custom dither amount: " & dither_amount menu(4) = "No dithering" menu(5) = "View original (can't be imported)" END SUB SUB DitherMenu.draw() IF imported THEN frame_draw imported, , 35*8 + showLeft, 20 + showTop, NO, vpage BASE.draw() END SUB FUNCTION DitherMenu.each_tick() as bool IF state.pt = 3 ANDALSO intgrabber(dither_amount, 0, 300) THEN state.need_update = YES IF state.need_update ORELSE imported = NULL ORELSE usemenu_ret THEN IF state.pt = 5 THEN frame_assign @imported, image_import_as_frame_32bit(filename) ELSE DIM options as QuantizeOptions options.firstindex = 1 options.transparency = TYPE(-1) 'No transparent color options.dither = (state.pt <> 4) SELECT CASE state.pt CASE 0: options.dither_maxerror = 180 CASE 1: options.dither_maxerror = 24 CASE 2: options.dither_maxerror = 5 CASE 3: options.dither_maxerror = dither_amount END SELECT 'Since the source image is not paletted, we don't know what the background colour 'is (if any: not all backdrops and tilesets are transparent). Import it, disallowing anything to 'be remapped to colour 0 (unfortunately colour 0 is the only pure black in the default palette), 'then let the user pick. '(If it's an image with an alpha channel, transparent pixels are also automatically mapped to 0) frame_assign @imported, image_import_as_frame_quantized(filename, pmask(), options) END IF END IF IF keyval(ccUse) > 1 ANDALSO state.pt <> 5 THEN accept = YES RETURN YES END IF RETURN NO END FUNCTION ' A menu asking the user how much dithering to do while importing an image FUNCTION importimage_dither_menu(filename as string, pmask() as RGBcolor) as Frame ptr switch_to_32bit_vpages DIM menu as DitherMenu menu.helpkey = "importimage_dithering" menu.filename = filename menu.menuopts.edged = YES memcpy @menu.pmask(0), @pmask(0), 256 * sizeof(RGBcolor) menu.title = "This image is unpaletted, and needs to be converted to your master palette. " _ "How do you want to convert it?" menu.run() IF menu.accept = NO THEN frame_unload @menu.imported switch_to_8bit_vpages RETURN menu.imported END FUNCTION ' Read an image file, check the bitdepth and palette and perform any necessary ' remapping, possibly create a new master palette (and change activepalette), ' and return the resulting (unsaved) Frame, or NULL if cancelled. FUNCTION importimage_process(filename as string, pmask() as RGBcolor) as Frame ptr DIM img as Frame ptr DIM info as ImageFileInfo = image_read_info(filename) IF info.supported = NO THEN 'browse() shouldn't let this happen showerror "Couldn't import that image; corrupt or unsupported" RETURN NULL END IF IF info.paletted THEN DIM palmapping(255) as integer DIM remapping_pal as Palette16 ptr DIM remap_nearest_match as bool = NO DIM remap_background as bool = NO DIM remap_0_to_0 as bool DIM imgpal(255) as RGBcolor img = image_import_as_frame_paletted(filename, imgpal()) IF img = NULL THEN showerror "Couldn't load the image! (See c_debug.txt for error messages)" RETURN NULL END IF 'Note imgpal() might contain non-opaque alpha, but we will drop the alpha when importing as a master palette IF memcmp(@imgpal(0), @master(0), 256 * sizeof(RGBcolor)) <> 0 THEN 'the palette is inequal to the master palette clearpage vpage DIM menu(2) as string menu(0) = "Remap to current Master Palette" menu(1) = "Import with new Master Palette" menu(2) = "Do not remap colours" DIM paloption as integer paloption = multichoice("This image's palette is not identical to your master palette." _ !"\nHint: you should probably remap to the master palette (see F1 help).", _ menu(), , , "importimage_palette") IF paloption = -1 THEN frame_unload @img RETURN NULL END IF IF paloption = 1 THEN importmasterpal filename, gen(genMaxMasterPal) + 1 'Updates master(), etc setpal master() ' For consistency, even when we add a new palette, don't use color 0 unless the ' user explicitly picks it as background (palmapping will be 1-to-1 except for color 0) loadpalette pmask(), activepalette 'Throw away pmask END IF remap_nearest_match = (paloption <> 2) remap_background = (paloption <> 2) remap_0_to_0 = NO 'If the user doesn't pick a transparent color, there won't be one ELSE ' Palettes are identical remap_nearest_match = YES 'Perform remapping, otherwise disabling colors (pmask()) won't work remap_0_to_0 = YES END IF IF remap_nearest_match THEN 'Put hint values in palmapping(), which will used if an exact match FOR idx as integer = 0 TO 255 palmapping(idx) = idx NEXT ' Disallow anything from being mapped to colour 0 to prevent accidental transparency find_palette_mapping imgpal(), pmask(), palmapping(), 1 'firstindex = 1 remapping_pal = palette16_new_from_indices(palmapping()) END IF IF remap_background THEN 'Note img doesn't contain any 0 pixels, so the user will never get asked what to do with them. IF importimage_change_background_color(img, remapping_pal) THEN 'The background color has now been converted to 0, so preserve it rather than using nearest match remap_0_to_0 = YES END IF END IF IF remap_nearest_match THEN palette16_unload @remapping_pal IF remap_0_to_0 THEN palmapping(0) = 0 remap_to_palette img, palmapping() END IF ELSE img = importimage_dither_menu(filename, pmask()) IF img = NULL THEN RETURN NULL importimage_change_background_color img END IF ' Throw away pmask() (why?) loadpalette pmask(), activepalette RETURN img END FUNCTION 'Import an image as a MXS backdrop/tileset. Returns true if imported, false if cancelled FUNCTION importimage_importmxs(mxslump as string, imagenum as integer, srcfile as string, pmask() as RGBcolor) as bool DIM img as Frame ptr = importimage_process(srcfile, pmask()) IF img = NULL THEN RETURN NO storemxs mxslump, imagenum, img frame_unload @img RETURN YES END FUNCTION 'Draw a Frame (specially a tileset) onto another Frame with the transparent 'colour replaced either with another colour, or with a chequer pattern. 'bgcolor is either between 0 and 255 (a colour), bgChequerScroll (a scrolling chequered 'background), or bgChequer (a non-scrolling chequered background) 'chequer_scroll is a counter variable which the calling function should increment once per tick. SUB frame_draw_with_background (src as Frame ptr, pal as Palette16 ptr = NULL, x as integer, y as integer, bgcolor as bgType, byref chequer_scroll as integer, dest as Frame ptr, opts as DrawOptions = def_drawoptions) draw_background dest, bgcolor, chequer_scroll, x, y, src->w * opts.scale, src->h * opts.scale frame_draw src, pal, x, y, , dest, opts END SUB ' Describe a bgType value (usually append to "Background: ") FUNCTION bgcolor_caption(bgcolor as bgType) as string IF bgcolor = bgChequer THEN RETURN "chequer" ELSEIF bgcolor = bgChequerScroll THEN RETURN "scrolling chequer" ELSE RETURN "color " & bgcolor END IF END FUNCTION SUB crop_tilesets(to_id as integer) 'Remove tilesets, animation patterns, default walls IF cropafter(to_id, gen(genMaxTile), game & ".til", 64000) THEN cropafter to_id, gen(genMaxTile), game & ".tap", 80, NO 'prompt=NO cropafter to_id, gen(genMaxTile), workingdir & SLASH & "defpass.bin", 322, NO 'prompt=NO END IF END SUB 'Doesn't include import_export_tilesets menu SUB tileset_editor () STATIC bgcolor as bgType = 0 'Default to first color in master palette DIM menu() as string DIM mapfile as string = game & ".til" DIM tilesetnum as integer DIM top as integer = -1 DIM chequer_scroll as integer DIM state as MenuState state.top = -1 state.pt = -1 state.first = -1 state.last = gen(genMaxTile) state.autosize = YES state.autosize_ignore_pixels = 4 state.need_update = YES 'The tileset editor stores the tileset on page 3 and undo on page 2, so lock them to 320x200 lock_page_size 2, 320, 200 lock_page_size 3, 320, 200 clearpage 3 setkeys DO chequer_scroll += 1 setwait 55 setkeys IF keyval(ccCancel) > 1 THEN EXIT DO IF keyval(scF1) > 1 THEN show_help "maptile_pickset" IF cropafter_keycombo(YES) AND state.pt > -1 THEN crop_tilesets state.pt state.last = gen(genMaxTile) state.need_update = YES END IF DIM tempnum as integer = large(state.pt, 0) IF intgrabber(tempnum, 0, gen(genMaxTile), , , YES) THEN state.pt = tempnum state.need_update = YES END IF IF keyval(ccDown) > 1 AND state.pt = gen(genMaxTile) AND gen(genMaxTile) < 32767 THEN state.pt += 1 IF needaddset(state.pt, gen(genMaxTile), "tile set") THEN clearpage 3 storemxs mapfile, state.pt, vpages(3) 'lazy state.last = gen(genMaxTile) state.need_update = YES END IF ELSEIF usemenu(state) THEN state.need_update = YES END IF IF enter_space_click(state) AND state.pt = -1 THEN EXIT DO IF enter_space_click(state) AND state.pt > -1 THEN tilesetnum = state.pt tile_edit_mode_picker tilesetnum, mapfile, bgcolor state.need_update = YES END IF IF state.need_update THEN state.need_update = NO REDIM menu(-1 TO gen(genMaxTile)) menu(-1) = "Previous Menu" FOR i as integer = 0 TO gen(genMaxTile) menu(i) = "Tile Set " & i NEXT IF state.pt = -1 THEN clearpage 3 ELSE loadmxs mapfile, state.pt, vpages(3) END IF clearpage dpage frame_draw_with_background vpages(3), , 0, 0, bgcolor, chequer_scroll, vpages(dpage) DIM menuopts as MenuOptions menuopts.edged = YES standardmenu menu(), state, , , dpage, menuopts SWAP vpage, dpage setvispage vpage dowait LOOP clearpage 3 clearpage 2 clearpage 1 clearpage 0 'Robust againts tileset leaks sprite_update_cache sprTypeTileset unlock_page_size 2 unlock_page_size 3 END SUB SUB tile_edit_mode_picker(byval tilesetnum as integer, mapfile as string, byref bgcolor as bgType) DIM chequer_scroll as integer DIM menu(6) as string menu(0) = "Previous Menu" menu(1) = "Draw Tiles" menu(2) = "Cut Tiles from Tilesets" menu(3) = "Cut Tiles from Backdrops" menu(4) = "Set Default Passability" menu(5) = "Define Tile Animation" 'menu(6) = "View with background: " DIM state as MenuState init_menu_state state, menu() state.need_update = YES DIM menuopt as menuOptions menuopt.edged = YES setkeys DO chequer_scroll += 1 setwait 55 setkeys IF keyval(ccCancel) > 1 THEN EXIT DO IF keyval(scF1) > 1 THEN show_help "maptile_tilemode" usemenu state IF enter_space_click(state) THEN SELECT CASE state.pt CASE 0 EXIT DO CASE 1, 2, 3, 4 picktiletoedit state.pt - 1, tilesetnum, mapfile, bgcolor CASE 5 tile_animation tilesetnum CASE 6 bgcolor = color_browser_256(large(bgcolor, 0)) END SELECT END IF IF state.pt = 6 THEN intgrabber(bgcolor, bgFIRST, 255) clearpage dpage frame_draw_with_background vpages(3), , 0, 0, bgcolor, chequer_scroll, vpages(dpage) menu(6) = "View with background: " & bgcolor_caption(bgcolor) standardmenu menu(), state, , , dpage, menuopt SWAP vpage, dpage setvispage vpage dowait LOOP END SUB SUB tile_animation(byval tilesetnum as integer) DIM tanim(1) as TileAnimPattern DIM taset as integer = 0 load_tile_anims tilesetnum, tanim() DIM menu(5) as string menu(0) = "Previous Menu" menu(2) = "Set Animation Range" menu(3) = "Set Animation Pattern" menu(5) = "Test Animations" DIM state as MenuState init_menu_state state, menu() state.need_update = YES setkeys DO setwait 55 setkeys IF keyval(ccCancel) > 1 THEN EXIT DO IF keyval(scF1) > 1 THEN show_help "maptile_tileanim" IF usemenu(state) THEN state.need_update = YES IF state.pt = 4 THEN IF tag_grabber(tanim(taset).disable_tag, state) THEN state.need_update = YES ELSE IF intgrabber(taset, 0, 1) THEN state.need_update = YES END IF IF enter_space_click(state) THEN SELECT CASE state.pt CASE 0: EXIT DO CASE 2: tile_anim_set_range tanim(), taset, tilesetnum CASE 3: setanimpattern tanim(), taset, tilesetnum CASE 5: testanimpattern tanim(), taset END SELECT END IF IF state.need_update THEN menu(1) = CHR(27) & "Animation Set " & taset & CHR(26) menu(4) = tag_condition_caption(tanim(taset).disable_tag, "Disable if Tag", "No tag check") state.need_update = YES END IF clearpage dpage standardmenu menu(), state, , , dpage SWAP vpage, dpage setvispage vpage dowait LOOP save_tile_anims tilesetnum, tanim() END SUB SUB tile_anim_set_range(tanim() as TileAnimPattern, byval taset as integer, byval tilesetnum as integer) DIM tog as integer DIM over_esc as bool 'Mouse over 'ESC when done' setkeys DO setwait 55 setkeys tog = tog XOR 1 IF keyval(ccCancel) > 1 OR enter_or_space() THEN EXIT DO IF keyval(scF1) > 1 THEN show_help "maptile_setanimrange" IF keyval(ccUp) > 1 THEN tanim(taset).range_start = large(tanim(taset).range_start - 16, 0) IF keyval(ccDown) > 1 THEN tanim(taset).range_start = small(tanim(taset).range_start + 16, 112) IF keyval(ccLeft) > 1 THEN tanim(taset).range_start = large(tanim(taset).range_start - 1, 0) IF keyval(ccRight) > 1 THEN tanim(taset).range_start = small(tanim(taset).range_start + 1, 112) WITH readmouse over_esc = rect_collide_point(str_rect("ESC when done", pInfoX, pInfoY), .pos) IF (.release AND mouseleft) ANDALSO over_esc THEN EXIT DO ELSEIF (.buttons AND mouseLeft) ANDALSO .pos < XY(320, 200) THEN tanim(taset).range_start = small(.x \ 20 + (.y \ 20) * 16, 160 - 48) '48 is the size of the range END IF END WITH clearpage dpage copypage 3, dpage tile_anim_draw_range tanim(), taset, dpage edgeprint "ESC when done", pInfoX, pInfoY, uilook(IIF(over_esc, uiMouseHoverItem, uiMenuItem)), dpage SWAP vpage, dpage setvispage vpage dowait LOOP save_tile_anims tilesetnum, tanim() END SUB SUB tile_anim_draw_range(tanim() as TileAnimPattern, byval taset as integer, byval page as integer) DIM animpos as XYPair animpos.x = 0 animpos.y = 0 FOR i as integer = 0 TO 159 IF i < tanim(taset).range_start OR i > tanim(taset).range_start + 47 THEN fuzzyrect animpos.x * 20, animpos.y * 20, 20, 20, uilook(uiText), page END IF animpos.x += 1 IF animpos.x > 15 THEN animpos.x = 0 animpos.y += 1 END IF NEXT i END SUB 'Return zone+1 which a screen position is over, or 0. FUNCTION mouseover (byval mousex as integer, byval mousey as integer, byref zox as integer, byref zoy as integer, byref zcsr as integer, area() as MouseArea) as integer 'Note: checking readmouse.active here isn't totally correct since mousex/y might be a previous 'position rather than the current one. But that's only used by the scroll tool, which this doesn't break. IF readmouse.active = NO THEN RETURN 0 FOR i as integer = UBOUND(area) TO 0 STEP -1 IF area(i).w <> 0 AND area(i).h <> 0 THEN IF mousex >= area(i).x AND mousex < area(i).x + area(i).w THEN IF mousey >= area(i).y AND mousey < area(i).y + area(i).h THEN zox = mousex - area(i).x zoy = mousey - area(i).y zcsr = area(i).hidecursor mouseover = i + 1 EXIT FUNCTION END IF 'Y OKAY--- END IF 'X OKAY--- END IF 'VALID ZONE--- NEXT i END FUNCTION 'Edit a tile animation's pattern SUB setanimpattern (tanim() as TileAnimPattern, taset as integer, tilesetnum as integer) 'Anim ops start at menu(0). 'If the pattern is full, then menu(maxTileAnimCmds + 1) is a dummy 'loop' op which can't be edited. REDIM menu(-1 TO -1) as string REDIM menu2(0) as string DIM animop(taopLAST) as string animop(0) = "end of animation" animop(1) = "up" animop(2) = "down" animop(3) = "right" animop(4) = "left" animop(5) = "wait" animop(6) = "if tag do rest" animop(7) = "reset" animop(8) = "loop" animop(9) = "shift tile" DIM numargs(taopLAST) as integer numargs(taopUp) = 1 numargs(taopDown) = 1 numargs(taopRight) = 1 numargs(taopLeft) = 1 numargs(taopWait) = 1 numargs(taopCheckTag) = 1 numargs(taopShiftTile) = 2 'These are the parameter limits for each tile animation op DIM llim(taopLAST) as integer DIM ulim(taopLAST) as integer FOR i as integer = taopUp TO taopDown llim(i) = 0 ulim(i) = 9 NEXT i FOR i as integer = taopRight TO taopLeft llim(i) = 0 ulim(i) = 159 NEXT i llim(taopWait) = 0 ulim(taopWait) = 32767 llim(taopCheckTag) = -max_tag() ulim(taopCheckTag) = max_tag() llim(taopShiftTile) = -maxTileOffset ulim(taopShiftTile) = maxTileOffset DIM context as integer = 0 '0 = main menu, 1 = detail menu DIM tog as integer DIM curcmd as TileAnimCmd ptr = NULL 'Main menu DIM state as MenuState state.autosize_ignore_lines = 4 'Leave room for the detail menu init_menu_state state, menu() state.need_update = YES 'Detail menu DIM state2 as MenuState init_menu_state state2, menu2() 'floatrect is the size of the floating detail menu not including the surrounding box or title DIM floatrect as RectType setkeys DO setwait 55 setkeys IF keyval(scF1) > 1 THEN show_help "maptile_setanimpattern" SELECT CASE context CASE 0 '''''Main menu IF usemenu(state) THEN state.need_update = YES IF in_bound(state.pt, 0, maxTileAnimCmds) THEN curcmd = @tanim(taset).cmd(state.pt) ELSE curcmd = NULL END IF IF keyval(ccCancel) > 1 THEN EXIT DO IF enter_space_click(state) THEN IF state.pt = -1 THEN 'Previous menu EXIT DO ELSE IF curcmd ANDALSO numargs(curcmd->op) = 0 THEN DIM newop as integer newop = multichoice("Select action:", animop(), taopRight) IF newop > -1 THEN curcmd->op = newop context = IIF(numargs(curcmd->op) = 0, 0, 1) state.need_update = YES END IF ELSE context = 1 END IF END IF END IF 'Shortcut keys: <,> to change op, left/right/intgrabber to change arg IF curcmd THEN WITH *curcmd DIM int_op as integer = .op IF keygrabber(int_op, 0, taopLAST, scLeftCaret, scRightCaret) THEN state.need_update = YES .op = int_op IF .op <= taopLAST THEN 'Not invalid IF intgrabber(.arg, llim(.op), ulim(.op)) THEN state.need_update = YES END IF END WITH END IF CASE 1 '''''Detail menu usemenu state2 'The final "loop" can't be changed IF curcmd THEN WITH *curcmd IF state2.pt = 0 THEN 'Select op IF enter_space_click(state2) THEN DIM newop as integer newop = multichoice("Select action:", animop(), taopRight) IF newop > -1 THEN .op = newop context = IIF(numargs(curcmd->op) = 0, 0, 1) 'Don't need to stay in this submenu state.need_update = YES END IF END IF IF intgrabber(.op, 0, taopLAST) THEN state.need_update = YES ELSEIF .op <= taopLAST THEN 'Not invalid IF state2.pt = 1 THEN 'Select arg IF .op = taopCheckTag THEN 'If tag do rest IF tag_grabber(.arg, state2) THEN state.need_update = YES ELSE IF intgrabber(.arg, llim(.op), ulim(.op)) THEN state.need_update = YES END IF ELSEIF state2.pt = 2 THEN 'Select arg2 'TODO: assuming arg and arg2 have same limits IF intgrabber(.arg2, llim(.op), ulim(.op)) THEN state.need_update = YES END IF END IF END WITH END IF IF keyval(ccCancel) > 1 ORELSE enter_or_space() ORELSE _ menu_click_outside(floatrect) ORELSE menu_right_click_close(floatrect) THEN save_tile_anims tilesetnum, tanim() context = 0 END IF END SELECT IF state.need_update THEN setanimpattern_refreshmenu state, state2, menu(), menu2(), tanim(), taset, animop(), numargs(), llim(), ulim() state.need_update = NO END IF '--Draw screen clearpage dpage state.active = (context = 0) standardmenu menu(), state, , , dpage state2.active = (context = 1) ANDALSO curcmd 'Not final forced Loop IF context = 0 THEN IF curcmd ANDALSO curcmd->op = taopEnd THEN edgeprint "Enter/ to pick op", pInfoX, pInfoY, uilook(uiDisabledItem), dpage ELSE edgeprint "Enter: Edit, : change op, Left/Right: change value", pInfoX, pInfoY, uilook(uiDisabledItem), dpage END IF END IF IF context = 1 THEN 'Draw the detail-edit menu immediately below the selected menu item in the main menu floatrect = XYWH(pMenuX + 4, pMenuY + (state.pt - state.top + 1) * 9 + 13, 310, 10 * (1 + UBOUND(menu2))) edgebox floatrect.x - 3, floatrect.y - 13, floatrect.w + 6, floatrect.h + 16, uilook(uiBackground), uilook(uiMenuItem), dpage edgeprint "Edit command", floatrect.x + floatrect.w \ 2 + ancCenter, floatrect.y - 10, uilook(eduiHeading), dpage standardmenu menu2(), state2, floatrect.x, floatrect.y, dpage END IF SWAP vpage, dpage setvispage vpage dowait LOOP setanimpattern_forcebounds tanim(), taset, numargs(), llim(), ulim() END SUB SUB setanimpattern_refreshmenu(state as MenuState, state2 as MenuState, menu() as string, menu2() as string, tanim() as TileAnimPattern, byval taset as integer, animop() as string, numargs() as integer, llim() as integer, ulim() as integer) setanimpattern_forcebounds tanim(), taset, numargs(), llim(), ulim() REDIM menu(-1 TO -1) menu(-1) = "Previous Menu" DIM anim_i as integer FOR anim_i = 0 TO maxTileAnimCmds WITH tanim(taset).cmd(anim_i) DIM mitem as string = safe_caption(animop(), .op) SELECT CASE .op CASE taopCheckTag mitem &= " (" & load_tag_name(.arg) & ")" CASE IS <= taopLAST 'Not invalid IF numargs(.op) >= 1 THEN mitem &= " " & .arg IF numargs(.op) >= 2 THEN mitem &= ", " & .arg2 END SELECT a_append menu(), mitem IF .op = taopEnd ORELSE .op = taopLoop THEN EXIT FOR END WITH NEXT anim_i 'If the tile animation doesn't end with an "end of animation" op, then it 'actually loops without resetting, meaning it can loop through the whole tileset! 'This is used in some games such as Timpoline and Super Walrus Chef. IF anim_i > maxTileAnimCmds THEN a_append menu(), "loop [pattern is full]" END IF REDIM menu2(0) IF state.pt = maxTileAnimCmds + 1 THEN 'menu2(0) = "Action: loop"' [pattern is full]" 'menu2(1) = "Value N/A" REDIM menu2(1) menu2(0) = "There is no room for more commands, so" menu2(1) = "the pattern loops here at the end." ELSEIF in_bound(state.pt, 0, maxTileAnimCmds) THEN WITH tanim(taset).cmd(state.pt) menu2(0) = "Action: " + safe_caption(animop(), .op) IF .op <= taopLAST ANDALSO numargs(.op) > 0 THEN REDIM PRESERVE menu2(1) menu2(1) = "Amount: " SELECT CASE .op CASE taopUp, taopDown, taopLeft, taopRight menu2(1) &= .arg & " tiles" CASE taopWait menu2(1) &= .arg & " ticks" CASE taopCheckTag menu2(1) &= tag_condition_caption(.arg, , "Never") CASE taopShiftTile REDIM PRESERVE menu2(2) menu2(1) = "X offset: " & .arg & " pixels" menu2(2) = "Y offset: " & .arg2 & " pixels" END SELECT END IF END WITH END IF state.last = UBOUND(menu) correct_menu_state state state2.last = UBOUND(menu2) correct_menu_state state2 END SUB SUB setanimpattern_forcebounds(tanim() as TileAnimPattern, byval taset as integer, numargs() as integer, llim() as integer, ulim() as integer) DIM tmp as integer FOR i as integer = 0 TO UBOUND(tanim(taset).cmd) WITH tanim(taset).cmd(i) 'Preserve unknown ops > taopLAST, but wipe invalid negative ones IF .op < 0 THEN .op = 0 IF .op <= taopLAST ANDALSO numargs(.op) >= 1 THEN .arg = bound(.arg, llim(.op), ulim(.op)) END IF END WITH NEXT i END SUB SUB testanimpattern (tanim() as TileAnimPattern, byref taset as integer) DIM sample as TileMap DIM tog as integer DIM csr as integer 'Selected tile 0-47 DIM x as integer DIM y as integer 'Copy tanim() into a temporary TileSetData which the drawmap API needs DIM tileset as TilesetData tileset.spr = mxs_frame_to_tileset(vpages(3)) FOR i as integer = 0 TO UBOUND(tanim) tileset.tanim(i) = tanim(i) NEXT 'TileMap for showing the 48 tile range of the animation pattern DIM tilesetview as TileMap cleantilemap tilesetview, 16, 3 FOR y = 0 TO 2 FOR x = 0 TO 15 writeblock tilesetview, x, y, tanim(taset).range_start + x + y * 16 NEXT NEXT setkeys DO setwait 55 setkeys tog = tog XOR 1 IF keyval(ccCancel) > 1 THEN EXIT DO IF keyval(scF1) > 1 THEN show_help "maptile_testanimpattern" IF keyval(ccUp) > 1 THEN loopvar csr, 0, 47, -16 IF keyval(ccDown) > 1 THEN loopvar csr, 0, 47, 16 IF keyval(ccLeft) > 1 THEN loopvar csr, 0, 47, -1 IF keyval(ccRight) > 1 THEN loopvar csr, 0, 47, 1 DIM animated_tile as integer = 160 + (taset * 48) + csr setoutside -2 'Don't draw beyond tilemap edge clearpage dpage '--draw available animating tiles-- drawmap tilesetview, 0, 0, @tileset, dpage, , , , 10, 60 '--draw sample-- animatetileset tileset cleantilemap sample, 3, 3 FOR x = 0 TO 2 FOR y = 0 TO 2 writeblock sample, x, y, animated_tile NEXT NEXT drawmap sample, -130, 0, @tileset, dpage, , , , 100, 60 '--Highlight current display tile for the sample-- DIM display_tile as integer = tile_anim_current_display_tile(animated_tile, @tileset) - tanim(taset).range_start 'This tile might not be on the previewed part of the tileset. Improve later. IF in_bound(display_tile, 0, 47) THEN y = display_tile \ 16 x = display_tile - y * 16 drawants vpages(dpage), 20 * x, 10 + 20 * y, 20, 20, findrgb(150,150,150) END IF '--Draw cursor-- y = csr \ 16 x = csr - y * 16 drawbox 20 * x, 10 + 20 * y, 20, 20, uilook(uiSelectedItem + tog), 1, dpage SWAP vpage, dpage setvispage vpage dowait LOOP frame_unload @tileset.spr unloadtilemap sample unloadtilemap tilesetview END SUB 'This is four different sub-editors of the tileset editor! 'tmode = 0: pick tile to edit (draw) 'tmode = 1: cut tile from tileset 'tmode = 2: cut tile from backdrop 'tmode = 3: default tile passability SUB picktiletoedit (byref tmode as integer, byval tilesetnum as integer, mapfile as string, bgcolor as bgType) STATIC cutnpaste(19, 19) as integer STATIC oldpaste as integer DIM ts as TileEditState DIM area(25) as MouseArea DIM mouse as MouseInfo ts.tilesetnum = tilesetnum ts.drawframe = frame_new(20, 20, , YES) DIM chequer_scroll as integer DIM tog as integer ts.gotmouse = havemouse() hidemousecursor ts.canpaste = oldpaste ts.drawcursor = 1 ts.airsize = 5 ts.mist = 4 area(0).x = 80 'Tile at 8x zoom area(0).y = 0 area(0).w = 160 area(0).h = 160 area(0).hidecursor = YES area(1).x = 0 area(1).y = 160 area(1).w = 320 area(1).h = 32 'TOOLS (more at 21+) FOR i as integer = 0 TO 5 area(2 + i).x = 4 + i * 9 area(2 + i).y = 32 area(2 + i).w = 8 area(2 + i).h = 8 NEXT i FOR i as integer = 0 TO 3 area(12 + i).x = 4 + i * 9 area(12 + i).y = 42 area(12 + i).w = 8 area(12 + i).h = 8 NEXT i 'Areas 11 and 12 used only in tile cutter 'LESS AIRBRUSH AREA area(16).x = 12 area(16).y = 60 area(16).w = 8 area(16).h = 8 area(16).hidecursor = NO 'LESS AIRBRUSH MIST area(17).x = 12 area(17).y = 76 area(17).w = 8 area(17).h = 8 area(17).hidecursor = NO 'MORE AIRBRUSH AREA area(18).x = 36 area(18).y = 60 area(18).w = 8 area(18).h = 8 area(18).hidecursor = NO 'MORE AIRBRUSH MIST area(19).x = 36 area(19).y = 76 area(19).w = 8 area(19).h = 8 area(19).hidecursor = NO FOR i as integer = 0 TO 1 'mark and clone area(21 + i).x = 49 + i * 9 area(21 + i).y = 42 area(21 + i).w = 8 area(21 + i).h = 8 NEXT i area(23).x = 58 'airbrush area(23).y = 32 area(23).w = 8 area(23).h = 8 area(24).x = 40 'scroll tool area(24).y = 42 area(24).w = 8 area(24).h = 8 area(25).x = 4 'Back button area(25).y = 3 area(25).w = textwidth("Back") area(25).h = 8 DIM pastogkey(7) as KBScancode DIM bitmenu(7) as string IF tmode = 3 THEN pastogkey(0) = ccUp pastogkey(1) = ccRight pastogkey(2) = ccDown pastogkey(3) = ccLeft pastogkey(4) = scA pastogkey(5) = scB pastogkey(6) = scH pastogkey(7) = scO loadpasdefaults ts.defaultwalls, tilesetnum bitmenu(0) = "Impassable to the North" bitmenu(1) = "Impassable to the East" bitmenu(2) = "Impassable to the South" bitmenu(3) = "Impassable to the West" bitmenu(4) = "A-type vehicle Tile" bitmenu(5) = "B-type vehicle Tile" bitmenu(6) = "Harm Tile" bitmenu(7) = "Overhead Tile (OBSOLETE!)" END IF loadmxs mapfile, tilesetnum, vpages(3) 'pick block to draw/import/default DIM bnum as integer = 0 setkeys DO setwait 17, 70 setkeys mouse = readmouse IF keyval(ccCancel) > 1 THEN EXIT DO 'In passmode, right clicking has a use IF tmode <> 3 ANDALSO (mouse.release AND mouseRight) THEN EXIT DO IF keyval(scF1) > 1 THEN IF tmode = 3 THEN show_help "default_passability" ELSE show_help "picktiletoedit" END IF END IF DIM mouse_over_tile as bool = mouse.active ANDALSO mouse.x < 320 ANDALSO mouse.y < 200 DIM mouse_click as bool = mouse_over_tile ANDALSO (mouse.release AND mouseLeft) IF mouse_over_tile THEN bnum = (mouse.y \ 20) * 16 + mouse.x \ 20 END IF IF tmode <> 3 OR keyval(scCtrl) = 0 THEN DIM movedcsr as bool = NO IF slowkey(ccLeft, 100) THEN bnum = (bnum + 159) MOD 160: movedcsr = YES IF slowkey(ccRight, 100) THEN bnum = (bnum + 1) MOD 160: movedcsr = YES IF slowkey(ccUp, 100) THEN bnum = (bnum + 144) MOD 160: movedcsr = YES IF slowkey(ccDown, 100) THEN bnum = (bnum + 16) MOD 160: movedcsr = YES IF movedcsr AND mouse.active THEN mouse.x = (mouse.x MOD 20) + (bnum MOD 16) * 20 mouse.y = (mouse.y MOD 20) + (bnum \ 16) * 20 movemouse mouse.x, mouse.y END IF END IF IF tmode = 3 THEN '--pass mode shortcuts FOR i as integer = 0 TO 7 IF keyval(scCtrl) > 0 OR i > 3 THEN IF keyval(pastogkey(i)) > 1 THEN ts.defaultwalls[bnum] XOR= 1 SHL i END IF END IF NEXT i IF keyval(scSpace) > 1 OR (mouse.release AND mouseRight) THEN 'Space works exactly the same as in the wallmap editor IF ts.defaultwalls[bnum] AND passAllWalls THEN ts.defaultwalls[bnum] = 0 ELSE ts.defaultwalls[bnum] OR= passAllWalls END IF END IF END IF IF copy_keychord() THEN tilecopy cutnpaste(), ts IF paste_keychord() THEN tilepaste cutnpaste(), ts IF (keyval(scCtrl) > 0 AND keyval(scT) > 1) THEN tiletranspaste cutnpaste(), ts ts.tilex = bnum AND 15 ts.tiley = bnum \ 16 IF enter_or_space() OR mouse_click THEN IF tmode = 0 THEN editmaptile ts, mouse, area(), bgcolor END IF IF tmode = 1 THEN ts.cuttileset = YES ts.cutfrom = small(ts.cutfrom, gen(genMaxTile)) tilecut ts, mouse END IF IF tmode = 2 THEN ts.cuttileset = NO ts.cutfrom = small(ts.cutfrom, gen(genNumBackdrops) - 1) tilecut ts, mouse END IF IF channel_to_Game THEN storemxs mapfile, tilesetnum, vpages(3) END IF IF tmode = 3 THEN IF (keyval(scAnyEnter) > 1) OR mouse_click THEN DIM buf() as integer vector_to_array buf(), ts.defaultwalls editbitset buf(), bnum, bitmenu(), , , , "Default wallmap bits" array_to_vector ts.defaultwalls, buf() END IF END IF clearpage dpage frame_draw_with_background vpages(3), , 0, 0, bgcolor, chequer_scroll, vpages(dpage) DIM msg as string IF tmode = 1 OR tmode = 2 THEN 'Show tile number msg = "Select tile to overwrite: " & bnum ELSEIF tmode = 3 THEN msg = "ENTER: edit tile CTRL+arrows: set walls" ELSEIF tmode = 0 THEN msg = "Tile to edit: " & bnum END IF edgeprint msg, pInfoX, IIF(get_resolution().h >= 210 OR bnum < 112, pInfoY, pMenuY), uilook(uiText), dpage IF tmode = 3 THEN FOR o as integer = 0 TO 9 FOR i as integer = 0 TO 15 IF (ts.defaultwalls[i + o * 16] AND 1) THEN rectangle i * 20, o * 20, 20, 3, uilook(uiMenuItem + tog), dpage IF (ts.defaultwalls[i + o * 16] AND 2) THEN rectangle i * 20 + 17, o * 20, 3, 20, uilook(uiMenuItem + tog), dpage IF (ts.defaultwalls[i + o * 16] AND 4) THEN rectangle i * 20, o * 20 + 17, 20, 3, uilook(uiMenuItem + tog), dpage IF (ts.defaultwalls[i + o * 16] AND 8) THEN rectangle i * 20, o * 20, 3, 20, uilook(uiMenuItem + tog), dpage textcolor uilook(uiSelectedItem + tog), 0 IF (ts.defaultwalls[i + o * 16] AND 16) THEN printstr "A", i * 20, o * 20, dpage IF (ts.defaultwalls[i + o * 16] AND 32) THEN printstr "B", i * 20 + 10, o * 20, dpage IF (ts.defaultwalls[i + o * 16] AND 64) THEN printstr "H", i * 20, o * 20 + 10, dpage IF (ts.defaultwalls[i + o * 16] AND 128) THEN printstr "O", i * 20 + 10, o * 20 + 10, dpage NEXT i NEXT o END IF rectangle ts.tilex * 20 + 7, ts.tiley * 20 + 7, 6, 6, IIF(tog, uilook(uiBackground), uilook(uiText)), dpage IF mouse.active THEN textcolor uilook(IIF(tog, uiText, uiDescription)), 0 printstr CHR(2), mouse.x - 2, mouse.y - 2, dpage END IF SWAP dpage, vpage setvispage vpage IF dowait THEN tog = tog XOR 1 chequer_scroll += 1 END IF LOOP storemxs mapfile, tilesetnum, vpages(3) IF tmode = 3 THEN savepasdefaults ts.defaultwalls, tilesetnum END IF v_free ts.defaultwalls oldpaste = ts.canpaste frame_unload @ts.drawframe showmousecursor END SUB SUB refreshtileedit (state as TileEditState) copymapblock state.tilex * 20, state.tiley * 20, 3, 280, 10 + (state.undo * 21), 2 frame_draw vpages(3), NULL, -state.tilex * 20, -state.tiley * 20, NO, state.drawframe 'Blit the tile onto state.drawframe END SUB SUB writeundoblock (state as TileEditState) loopvar state.undo, 0, 5 copymapblock state.tilex * 20, state.tiley * 20, 3, 280, 10 + (state.undo * 21), 2 textcolor uilook(uiMenuItem), 0 printstr ">", 270, 16 + (state.undo * 21), 2 state.allowundo = 1 END SUB SUB readundoblock (state as TileEditState) FOR j as integer = 0 TO 5 'Blank out the arrow pointing at the current undo step rectangle 270, 16 + (j * 21), 8, 8, uilook(uiBackground), 2 NEXT j copymapblock 280, 10 + (state.undo * 21), 2, state.tilex * 20, state.tiley * 20, 3 textcolor uilook(uiMenuItem), 0 printstr ">", 270, 16 + (state.undo * 21), 2 refreshtileedit state END SUB SUB editmaptile (ts as TileEditState, mouse as MouseInfo, area() as MouseArea, bgcolor as bgType) STATIC clone as TileCloneBuffer DIM spot as XYPair DIM toolinfo(SPRITEEDITOR_LAST_TOOL) as ToolInfoType WITH toolinfo(0) .name = "Draw" .icon = CHR(3) .shortcut = scD .cursor = 0 .areanum = 2 END WITH WITH toolinfo(1) .name = "Box" .icon = CHR(4) .shortcut = scB .cursor = 1 .areanum = 3 END WITH WITH toolinfo(2) .name = "Line" .icon = CHR(5) .shortcut = scL .cursor = 2 .areanum = 4 END WITH WITH toolinfo(3) .name = "Fill" .icon = "F" .shortcut = scF .cursor = 3 .areanum = 5 END WITH WITH toolinfo(4) .name = "Oval" .icon = "O" .shortcut = scO .cursor = 2 .areanum = 7 END WITH WITH toolinfo(5) .name = "Air" .icon = "A" .shortcut = scA .cursor = 3 .areanum = 23 END WITH WITH toolinfo(6) .name = "Mark" .icon = "M" .shortcut = scM .cursor = 3 .areanum = 21 END WITH WITH toolinfo(7) .name = "Clone" .icon = "C" .shortcut = scC .cursor = 3 .areanum = 22 END WITH WITH toolinfo(8) .name = "Replace" .icon = "R" .shortcut = scR .cursor = 3 .areanum = 6 END WITH WITH toolinfo(9) .name = "Scroll" .icon = "S" .shortcut = scS .cursor = 2 .areanum = 24 END WITH DIM overlay as Frame ptr overlay = frame_new(20, 20, , YES) DIM overlaypal as Palette16 ptr overlaypal = palette16_new() DIM chequer_scroll as integer = 0 DIM tog as integer = 0 DIM tick as integer = 0 ts.lastcpos = XY(ts.x, ts.y) ts.justpainted = 0 ts.didscroll = NO ts.undo = 0 ts.allowundo = 0 ts.delay = 10 ts.fastmovestep = 4 DIM zox as integer = ts.x * 8 + 4 DIM zoy as integer = ts.y * 8 + 4 DIM zcsr as integer DIM overlay_use_palette as integer DIM fgcol as integer DIM bgcol as integer mouse.x = area(0).x + zox mouse.y = area(0).y + zoy IF mouse.active THEN movemouse mouse.x, mouse.y clearpage 2 '--Undo boxes FOR i as integer = 0 TO 5 edgebox 279, 9 + (i * 21), 22, 22, uilook(uiBackground), uilook(uiMenuItem), 2 NEXT i refreshtileedit ts textcolor uilook(uiMenuItem), 0 printstr ">", 270, 16 + (ts.undo * 21), 2 '--Draw master palette FOR j as integer = 0 TO 7 FOR i as integer = 0 TO 15 rectangle i * 10, j * 4 + 160, 10, 4, j * 16 + i, 2 rectangle i * 10 + 160, j * 4 + 160, 10, 4, j * 16 + i + 128, 2 NEXT i NEXT j '--frame around the drawing area rectangle 79, 0, 162, 160, uilook(uiText), 2 '---EDIT BLOCK--- setkeys DO setwait 17, 110 setkeys IF ts.gotmouse THEN mouse = readmouse zcsr = 0 ts.zone = mouseover(mouse.x, mouse.y, zox, zoy, zcsr, area()) END IF ts.delay = large(ts.delay - 1, 0) ts.justpainted = large(ts.justpainted - 1, 0) IF keyval(ccCancel) > 1 THEN IF ts.hold = YES THEN ts.hold = NO ELSE EXIT DO END IF END IF IF ts.zone - 1 = 25 THEN 'Back button IF mouse.release AND mouseLeft THEN EXIT DO END IF IF keyval(scF1) > 1 THEN show_help "editmaptile" 'Debug keys IF keyval(scCtrl) > 0 AND keyval(sc2) > 1 THEN setvispage 2: waitforanykey IF keyval(scCtrl) > 0 AND keyval(sc3) > 1 THEN setvispage 3: waitforanykey 'Normal keys IF keyval(scAlt) = 0 THEN IF keyval(scShift) > 0 THEN IF keyval(scF) > 1 THEN ts.fastmovestep = small(ts.fastmovestep + 1, 19) IF keyval(scS) > 1 THEN ts.fastmovestep = large(ts.fastmovestep - 1, 2) END IF DIM fixmouse as bool = NO DIM stepsize as integer = IIF(keyval(scShift) > 0, ts.fastmovestep, 1) IF ts.tool <> scroll_tool THEN IF slowkey(ccUp, 100) THEN ts.y -= stepsize: fixmouse = YES IF slowkey(ccDown, 100) THEN ts.y += stepsize: fixmouse = YES IF slowkey(ccLeft, 100) THEN ts.x -= stepsize: fixmouse = YES IF slowkey(ccRight, 100) THEN ts.x += stepsize: fixmouse = YES ts.x = bound(ts.x, 0, 19) ts.y = bound(ts.y, 0, 19) ELSE DIM scrolloff as XYPair IF slowkey(ccUp, 100) THEN scrolloff.y -= stepsize IF slowkey(ccDown, 100) THEN scrolloff.y += stepsize IF slowkey(ccLeft, 100) THEN scrolloff.x -= stepsize IF slowkey(ccRight, 100) THEN scrolloff.x += stepsize scrolltile ts, scrolloff.x, scrolloff.y IF scrolloff.x OR scrolloff.y THEN fixmouse = YES ts.x = POSMOD(ts.x + scrolloff.x, 20) ts.y = POSMOD(ts.y + scrolloff.y, 20) END IF IF fixmouse AND ts.zone = 1 THEN zox = ts.x * 8 + 4 zoy = ts.y * 8 + 4 mouse.x = area(0).x + zox mouse.y = area(0).y + zoy movemouse mouse.x, mouse.y END IF END IF IF keyval(scCtrl) > 0 AND keyval(scS) > 1 THEN storemxs game + ".til", ts.tilesetnum, vpages(3) END IF '---KEYBOARD SHORTCUTS FOR TOOLS------------ IF keyval(scCtrl) = 0 AND keyval(scShift) = 0 THEN FOR i as integer = 0 TO UBOUND(toolinfo) IF keyval(toolinfo(i).shortcut) > 1 THEN tileedit_set_tool ts, toolinfo(), i END IF NEXT i END IF '---------- IF keyval(scComma) > 1 OR (keyval(scAlt) > 0 AND keyval(ccLeft) > 1) THEN ts.curcolor = (ts.curcolor + 255) MOD 256 IF ts.curcolor MOD 16 = 15 THEN ts.curcolor = (ts.curcolor + 144) MOD 256 END IF IF keyval(scPeriod) > 1 OR (keyval(scAlt) > 0 AND keyval(ccRight) > 1) THEN ts.curcolor += 1 IF ts.curcolor MOD 16 = 0 THEN ts.curcolor = (ts.curcolor + 112) MOD 256 END IF IF keyval(scAlt) > 0 AND keyval(ccUp) > 1 THEN ts.curcolor = (ts.curcolor + 240) MOD 256 IF keyval(scAlt) > 0 AND keyval(ccDown) > 1 THEN ts.curcolor = (ts.curcolor + 16) MOD 256 IF keyval(scTilde) > 1 THEN ts.hidemouse = ts.hidemouse XOR YES IF keyval(scCtrl) > 0 AND keyval(scZ) > 1 AND ts.allowundo THEN loopvar ts.undo, 0, 5, -1 readundoblock ts ts.didscroll = NO 'save a new undo block upon scrolling END IF IF keyval(scSpace) > 0 THEN clicktile ts, keyval(scSpace) AND 4, toolinfo(), clone IF keyval(scAnyEnter) > 1 ORELSE keyval(scG) > 1 THEN ts.curcolor = readpixel(ts.tilex * 20 + ts.x, ts.tiley * 20 + ts.y, 3) 'If the tool is a non-drawing tool, switch back to draw IF ts.tool = scroll_tool OR ts.tool = mark_tool OR ts.tool = clone_tool THEN tileedit_set_tool ts, toolinfo(), draw_tool END IF END IF SELECT CASE ts.zone CASE 1 'Drawing area ts.x = zox \ 8 ts.y = zoy \ 8 DIM usetool as bool = (mouse.buttons AND mouseLeft) <> 0 DIM newkeypress as bool = (mouse.clicks AND mouseLeft) <> 0 IF ts.tool = mark_tool THEN 'For mark tool, right click to cut instead of copy IF mouse.buttons AND mouseRight THEN ts.alternate_mode = YES usetool = YES newkeypress OR= (mouse.clicks AND mouseRight) <> 0 END IF ELSEIF ts.tool = clone_tool THEN ' For clone brush tool, enter/right-click moves the handle point IF ts.readjust THEN IF keyval(scEnter) = 0 AND mouse.buttons = 0 THEN ' click or key release ts.readjust = NO clone.offset.x += (ts.x - ts.adjustpos.x) clone.offset.y += (ts.y - ts.adjustpos.y) ts.adjustpos.x = 0 ts.adjustpos.y = 0 END IF ELSE IF (keyval(scEnter) AND 5) ORELSE (mouse.buttons AND mouseRight) THEN ts.readjust = YES ts.adjustpos.x = ts.x ts.adjustpos.y = ts.y END IF END IF ELSEIF ts.tool = scroll_tool ANDALSO (mouse.buttons AND mouseLeft) THEN 'Handle scrolling by dragging the mouse 'Did this drag start inside the sprite box? If not, ignore IF mouse.dragging ANDALSO mouseover(mouse.clickstart.x, mouse.clickstart.y, 0, 0, 0, area()) = 1 THEN scrolltile ts, ts.x - ts.lastcpos.x, ts.y - ts.lastcpos.y END IF ELSE 'for all other tools, pick a color IF mouse.buttons AND mouseRight THEN ts.curcolor = readpixel(ts.tilex * 20 + ts.x, ts.tiley * 20 + ts.y, 3) 'If the tool is a non-drawing tool, switch back to draw IF ts.tool = scroll_tool THEN tileedit_set_tool ts, toolinfo(), draw_tool END IF END IF IF usetool THEN clicktile ts, newkeypress, toolinfo(), clone CASE 2 'Colour selector IF mouse.buttons AND mouseLeft THEN ts.curcolor = ((zoy \ 4) * 16) + ((zox MOD 160) \ 10) + (zox \ 160) * 128 END IF CASE 13 TO 16 IF mouse.clicks AND mouseLeft THEN fliptile ts END SELECT FOR i as integer = 0 TO UBOUND(toolinfo) IF toolinfo(i).areanum = ts.zone - 1 THEN IF mouse.clicks AND mouseLeft THEN tileedit_set_tool ts, toolinfo(), i END IF END IF NEXT i '--mouse over undo IF mouse.active ANDALSO mouse.x >= 280 ANDALSO mouse.x < 300 THEN FOR i as integer = 0 TO 5 IF mouse.y >= (10 + (i * 21)) AND mouse.y < (30 + (i * 21)) THEN IF (mouse.clicks AND mouseLeft) ANDALSO ts.allowundo THEN ts.undo = i readundoblock ts END IF END IF NEXT i END IF '--toggle preview IF keyval(scP) > 1 THEN ts.preview_content XOR= 1 IF mouse.active ANDALSO mouse.x >= 10 AND mouse.x <= 70 THEN IF mouse.y >= 90 AND mouse.y <= 150 THEN IF mouse.clicks AND mouseLeft THEN ts.preview_content XOR= 1 END IF END IF END IF IF ts.tool = airbrush_tool THEN '--adjust airbrush IF mouse.buttons AND mouseLeft THEN IF ts.zone = 17 THEN ts.airsize = large(ts.airsize - tick, 1) IF ts.zone = 19 THEN ts.airsize = small(ts.airsize + tick, 30) IF ts.zone = 18 THEN ts.mist = large(ts.mist - tick, 1) IF ts.zone = 20 THEN ts.mist = small(ts.mist + tick, 99) END IF IF keyval(scMinus) > 1 OR keyval(scNumpadMinus) > 1 THEN IF keyval(scCtrl) > 0 THEN ts.mist = large(ts.mist - 1, 1) ELSE ts.airsize = large(ts.airsize - 1, 1) END IF END IF IF keyval(scPlus) > 1 OR keyval(scNumpadPlus) > 1 THEN IF keyval(scCtrl) > 0 THEN ts.mist = small(ts.mist + 1, 99) ELSE ts.airsize = small(ts.airsize + 1, 80) END IF END IF END IF IF keyval(scBackspace) > 1 OR keyval(scLeftBracket) > 1 OR keyval(scRightBracket) > 1 THEN fliptile ts DIM cy as integer = (ts.curcolor \ 16) MOD 8 DIM cx as integer = (ts.curcolor AND 15) + (ts.curcolor \ 128) * 16 ts.lastcpos = XY(ts.x, ts.y) '--Draw screen (Some of the editor is predrawn to page 2) clearpage dpage copypage 2, dpage DIM scale8 as DrawOptions scale8.scale = 8 frame_draw_with_background ts.drawframe, NULL, 80, 0, bgcolor, chequer_scroll, vpages(dpage), scale8 'Draw the tile, at 8x zoom with background '--Display x/y pos and color textcolor uilook(uiMenuItem), 0 printstr strprintf("x=%2d y=%2d", ts.x, ts.y), pRight, pBottom + 2, dpage printstr "Col: " & ts.curcolor, pLeft, pBottom + 2, dpage '--Draw tile preview IF ts.preview_content = 0 THEN tileedit_show_neighbouring_tiles(ts, bgcolor, chequer_scroll) ELSE tileedit_show_tile_tiled(ts, bgcolor, chequer_scroll) END IF frame_clear overlay overlay_use_palette = YES 'OK, this is a bit of a hack overlaypal->col(1) = ts.curcolor rectangle cx * 10 + 4, cy * 4 + 162, 3, 1, IIF(tog, uilook(uiBackground), uilook(uiText)), dpage 'Selected colour marker 'rectangle 60 + ts.x * 10, ts.y * 8, 10, 8, readpixel(ts.tilex * 20 + ts.x, ts.tiley * 20 + ts.y, 3), dpage '??? Looks redundant 'rectangle ts.x * 10 + 64, ts.y * 8 + 3, 3, 2, IIF(tog, uilook(uiBackground), uilook(uiText)), dpage 'Selecter pixel marker rectangle ts.x * 8 + 83, ts.y * 8 + 3, 3, 2, IIF(tog, uilook(uiBackground), uilook(uiText)), dpage 'Selected pixel marker IF ts.tool = airbrush_tool THEN IF tog THEN ellipse overlay, ts.x, ts.y, ts.airsize / 2 - 0.5, 1 'paintat overlay, ts.x, ts.y, 1 END IF END IF IF ts.tool = clone_tool AND tog = 0 THEN IF clone.exists = YES THEN overlay_use_palette = NO 'Don't use the palette, so colour 0 is drawn transparently FOR i as integer = 0 TO clone.size.y - 1 FOR j as integer = 0 TO clone.size.x - 1 spot.x = ts.x - clone.offset.x + j spot.y = ts.y - clone.offset.y + i IF ts.readjust = YES THEN spot.x -= (ts.x - ts.adjustpos.x) spot.y -= (ts.y - ts.adjustpos.y) END IF putpixel overlay, spot.x, spot.y, clone.buf(j, i) NEXT j NEXT i END IF END IF IF ts.hold = YES THEN DIM select_rect as RectType corners_to_rect_inclusive Type(ts.x, ts.y), ts.holdpos, select_rect SELECT CASE ts.tool CASE box_tool rectangle overlay, select_rect.x, select_rect.y, select_rect.wide, select_rect.high, 1 CASE line_tool drawline overlay, ts.x, ts.y, ts.holdpos.x, ts.holdpos.y, 1 CASE oval_tool ts.radius = SQR((ts.holdpos.x - ts.x)^2 + (ts.holdpos.y - ts.y)^2) IF ts.zone = 1 THEN 'Use mouse pointer instead of draw cursor for finer grain control of radius ts.radius = SQR( (ts.holdpos.x - (mouse.x - area(0).x - 4) / 8)^2 + (ts.holdpos.y - (mouse.y - area(0).y - 4) / 8)^2 ) END IF ellipse overlay, ts.holdpos.x, ts.holdpos.y, ts.radius, 1 CASE mark_tool IF tog = 0 THEN drawbox overlay, select_rect.x, select_rect.y, select_rect.wide, select_rect.high, 15, 1 overlaypal->col(15) = randint(10) END SELECT END IF 'Draw tool overlay, at 8x zoom frame_draw overlay, iif(overlay_use_palette, overlaypal, NULL), 80, 0, YES, dpage, scale8 textcolor uilook(uiMenuItem), uilook(uiDisabledItem): IF ts.zone - 1 = 25 THEN textcolor uilook(uiMenuItem), uilook(uiSelectedDisabled) printstr "Back", area(25).x, area(25).y, dpage textcolor uilook(uiText), uilook(uiHighlight) printstr toolinfo(ts.tool).name, 8, 15, dpage printstr "Tool", 8, 23, dpage printstr "Undo", 274, 1, dpage FOR i as integer = 0 TO UBOUND(toolinfo) fgcol = uilook(uiMenuItem) bgcol = uilook(uiDisabledItem) IF ts.tool = i THEN fgcol = uilook(uiText): bgcol = uilook(uiMenuItem) IF ts.zone - 1 = toolinfo(i).areanum THEN bgcol = uilook(uiSelectedDisabled) textcolor fgcol, bgcol printstr toolinfo(i).icon, area(toolinfo(i).areanum).x, area(toolinfo(i).areanum).y, dpage NEXT i FOR i as integer = 0 TO 3 textcolor uilook(uiMenuItem), uilook(uiDisabledItem): IF ts.zone = 13 + i THEN textcolor uilook(uiText), uilook(uiSelectedDisabled) printstr CHR(7 + i), 4 + i * 9, 42, dpage NEXT i IF ts.tool = airbrush_tool THEN textcolor uilook(uiMenuItem), 0 printstr "SIZE", 12, 52, dpage printstr STR(ts.airsize), 20, 60, dpage printstr "MIST", 12, 68, dpage printstr STR(ts.mist), 20, 76, dpage textcolor uilook(uiMenuItem), uilook(uiDisabledItem): IF ts.zone = 17 THEN textcolor uilook(uiText), uilook(uiSelectedDisabled) printstr CHR(27), 12, 60, dpage textcolor uilook(uiMenuItem), uilook(uiDisabledItem): IF ts.zone = 18 THEN textcolor uilook(uiText), uilook(uiSelectedDisabled) printstr CHR(27), 12, 76, dpage textcolor uilook(uiMenuItem), uilook(uiDisabledItem): IF ts.zone = 19 THEN textcolor uilook(uiText), uilook(uiSelectedDisabled) printstr CHR(26), 36, 60, dpage textcolor uilook(uiMenuItem), uilook(uiDisabledItem): IF ts.zone = 20 THEN textcolor uilook(uiText), uilook(uiSelectedDisabled) printstr CHR(26), 36, 76, dpage END IF IF mouse.active THEN DIM c as integer = zcsr IF c = -1 THEN c = ts.drawcursor IF ts.hidemouse THEN c = -2 END IF textcolor uilook(IIF(tog, uiText, uiDescription)), 0 printstr CHR(2 + c), mouse.x - 2, mouse.y - 2, dpage END IF SWAP dpage, vpage setvispage vpage tick = 0 IF dowait THEN tick = 1 tog = tog XOR 1 chequer_scroll += 1 END IF LOOP IF mouse.active THEN movemouse ts.tilex * 20 + 10, ts.tiley * 20 + 10 END IF frame_unload @overlay palette16_unload @overlaypal END SUB SUB tileedit_show_tile_tiled(byref ts as TileEditState, byval bgcolor as bgType, byval chequer_scroll as integer) ' draw current tile tiled as preview on the left of the tile-editor DIM as integer row = -1, column = -1 DIM tilesize as integer = 20 DIM area as RectType = (10,90,60,60) ' draw white background square (plus a border of 1px) rectangle area.x-1, area.y-1, area.wide+2, area.high+2, uilook(uiText), dpage ' draw neighboring tiles in a circle around the middle tile FOR column = -1 TO 1 FOR row = -1 TO 1 ' draw the edited tile as a block of 9 frame_draw_with_background ts.drawframe, NULL, area.x+((1+column)*tilesize), area.y+((1+row)*tilesize), bgcolor, chequer_scroll, vpages(dpage) NEXT row NEXT column END SUB SUB tileedit_show_neighbouring_tiles(byref ts as TileEditState, byval bgcolor as bgType, byval chequer_scroll as integer) ' draw neighboring tiles preview area on the left of the tile-editor DIM as integer row = -1, column = -1 DIM tssize as XYPair = (16,10) ' tileset size DIM tilesize as integer = 20 DIM temp_tilepos as XYPair ' current tile position DIM temp_frame as Frame ptr DIM area as RectType = (10,90,60,60) ' temporary blitting target for neighbouring tiles temp_frame = frame_new(tilesize, tilesize, , YES) ' draw white background square (plus a border of 1px) rectangle area.x-1, area.y-1, area.wide+2, area.high+2, uilook(uiText), dpage ' draw the edited tile as preview at the centre frame_draw_with_background ts.drawframe, NULL, area.x+tilesize, area.y+tilesize, bgcolor, chequer_scroll, vpages(dpage) ' draw neighboring tiles in a circle around the middle tile FOR column = -1 TO 1 FOR row = -1 TO 1 IF (row = 0 AND column = 0) THEN ' don't draw the centre tile as it already exists CONTINUE FOR END IF temp_tilepos.x = (ts.tilex + column) * tilesize temp_tilepos.y = (ts.tiley + row) * tilesize IF temp_tilepos.x < 0 THEN temp_tilepos.x = tssize.x*tilesize - tilesize IF temp_tilepos.y < 0 THEN temp_tilepos.y = tssize.y*tilesize - tilesize IF temp_tilepos.x >= 320 THEN temp_tilepos.x = 0 IF temp_tilepos.y >= 200 THEN temp_tilepos.y = 0 ' create a temporary copy of the tile from the tile page frame_draw vpages(3), NULL, -temp_tilepos.x, -temp_tilepos.y, NO, temp_frame ' draw the tile on the display page frame_draw_with_background temp_frame, NULL, area.x+((1+column)*tilesize), area.y+((1+row)*tilesize), bgcolor, chequer_scroll, vpages(dpage) NEXT row NEXT column frame_unload @temp_frame END SUB SUB tileedit_set_tool (ts as TileEditState, toolinfo() as ToolInfoType, byval toolnum as integer) IF ts.tool <> toolnum AND toolnum = scroll_tool THEN ts.didscroll = NO ts.tool = toolnum ts.alternate_mode = NO ts.hold = NO ts.drawcursor = toolinfo(ts.tool).cursor + 1 END SUB SUB clicktile (ts as TileEditState, newkeypress as integer, toolinfo() as ToolInfoType, byref clone as TileCloneBuffer) DIM spot as XYPair IF ts.delay > 0 THEN EXIT SUB SELECT CASE ts.tool CASE draw_tool IF ts.justpainted = 0 THEN writeundoblock ts ts.justpainted = 3 putpixel 280 + ts.x, 10 + (ts.undo * 21) + ts.y, ts.curcolor, 2 rectangle ts.tilex * 20 + ts.x, ts.tiley * 20 + ts.y, 1, 1, ts.curcolor, 3 refreshtileedit ts CASE box_tool IF newkeypress THEN IF ts.hold = YES THEN writeundoblock ts DIM select_rect as RectType corners_to_rect_inclusive Type(ts.x, ts.y), ts.holdpos, select_rect rectangle ts.tilex * 20 + select_rect.x, ts.tiley * 20 + select_rect.y, select_rect.wide, select_rect.high, ts.curcolor, 3 refreshtileedit ts ts.hold = NO ELSE ts.hold = YES ts.holdpos.x = ts.x ts.holdpos.y = ts.y END IF END IF CASE line_tool IF newkeypress THEN IF ts.hold = YES THEN writeundoblock ts drawline ts.tilex * 20 + ts.x, ts.tiley * 20 + ts.y, ts.tilex * 20 + ts.holdpos.x, ts.tiley * 20 + ts.holdpos.y, ts.curcolor, 3 refreshtileedit ts ts.hold = NO ELSE ts.hold = YES ts.holdpos.x = ts.x ts.holdpos.y = ts.y END IF END IF CASE fill_tool IF newkeypress THEN writeundoblock ts rectangle 0, 0, 22, 22, ts.curcolor, dpage FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 putpixel 1 + i, 1 + j, readpixel(ts.tilex * 20 + i, ts.tiley * 20 + j, 3), dpage NEXT j NEXT i paintat vpages(dpage), 1 + ts.x, 1 + ts.y, ts.curcolor FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 putpixel ts.tilex * 20 + i, ts.tiley * 20 + j, readpixel(1 + i, 1 + j, dpage), 3 NEXT j NEXT i refreshtileedit ts rectangle 0, 0, 22, 22, uilook(uiBackground), dpage END IF CASE replace_tool IF newkeypress THEN writeundoblock ts setclip ts.tilex * 20, ts.tiley * 20, ts.tilex * 20 + 19, ts.tiley * 20 + 19, vpages(3) replacecolor vpages(3), readpixel(ts.tilex * 20 + ts.x, ts.tiley * 20 + ts.y, 3), ts.curcolor setclip refreshtileedit ts END IF CASE oval_tool IF newkeypress THEN IF ts.hold = YES THEN writeundoblock ts rectangle 0, 0, 22, 22, uilook(uiText), dpage FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 putpixel 1 + i, 1 + j, readpixel(ts.tilex * 20 + i, ts.tiley * 20 + j, 3), dpage NEXT j NEXT i ellipse vpages(dpage), 1 + ts.holdpos.x, 1 + ts.holdpos.y, ts.radius, ts.curcolor FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 putpixel ts.tilex * 20 + i, ts.tiley * 20 + j, readpixel(1 + i, 1 + j, dpage), 3 NEXT j NEXT i refreshtileedit ts rectangle 0, 0, 22, 22, uilook(uiBackground), dpage ts.hold = NO ELSE ts.hold = YES ts.holdpos.x = ts.x ts.holdpos.y = ts.y END IF END IF CASE airbrush_tool IF ts.justpainted = 0 THEN writeundoblock ts ts.justpainted = 3 rectangle 19, 119, 22, 22, uilook(uiText), dpage FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 putpixel 20 + i, 120 + j, readpixel(ts.tilex * 20 + i, ts.tiley * 20 + j, 3), dpage NEXT j NEXT i airbrush vpages(dpage), 20 + ts.x, 120 + ts.y, ts.airsize, ts.mist, ts.curcolor FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 putpixel ts.tilex * 20 + i, ts.tiley * 20 + j, readpixel(20 + i, 120 + j, dpage), 3 NEXT j NEXT i refreshtileedit ts CASE mark_tool IF newkeypress THEN IF ts.hold = YES THEN 'alternate means cut instead of copy IF (keyval(scShift) OR keyval(scCtrl)) > 0 THEN ts.alternate_mode = YES IF ts.alternate_mode THEN writeundoblock ts DIM select_rect as RectType corners_to_rect_inclusive Type(ts.x, ts.y), ts.holdpos, select_rect clone.size.x = select_rect.wide clone.size.y = select_rect.high FOR i as integer = 0 TO clone.size.y - 1 FOR j as integer = 0 TO clone.size.x - 1 DIM pos as XYPair = XY(ts.tilex, ts.tiley) * 20 + select_rect.xy + XY(j, i) clone.buf(j, i) = readpixel(pos.x, pos.y, 3) IF ts.alternate_mode THEN putpixel(pos.x, pos.y, 0, 3) NEXT j NEXT i clone.offset.x = clone.size.x \ 2 clone.offset.y = clone.size.y \ 2 ts.readjust = NO ts.adjustpos.x = 0 ts.adjustpos.y = 0 clone.exists = YES refreshtileedit ts ' auto-select the clone tool after marking tileedit_set_tool ts, toolinfo(), clone_tool ELSE ts.hold = YES ts.holdpos.x = ts.x ts.holdpos.y = ts.y END IF END IF CASE clone_tool IF newkeypress THEN IF ts.justpainted = 0 THEN writeundoblock ts ts.justpainted = 3 IF clone.exists = YES THEN FOR i as integer = 0 TO clone.size.y - 1 FOR j as integer = 0 TO clone.size.x - 1 spot.x = ts.x - clone.offset.x + j + ts.adjustpos.x spot.y = ts.y - clone.offset.y + i + ts.adjustpos.y IF spot.x >= 0 AND spot.x <= 19 AND spot.y >= 0 AND spot.y <= 19 AND clone.buf(j, i) > 0 THEN putpixel ts.tilex * 20 + spot.x, ts.tiley * 20 + spot.y, clone.buf(j, i), 3 END IF NEXT j NEXT i refreshtileedit ts ELSE 'if no clone buffer, switch to mark tool tileedit_set_tool ts, toolinfo(), mark_tool ts.hold = YES ts.holdpos.x = ts.x ts.holdpos.y = ts.y IF readmouse.buttons AND mouseRight THEN ts.alternate_mode = YES END IF END IF END SELECT END SUB SUB scrolltile (ts as TileEditState, byval shiftx as integer, byval shifty as integer) 'Save an undo before the first of a consecutive scrolls IF shiftx = 0 AND shifty = 0 THEN EXIT SUB IF ts.didscroll = NO THEN writeundoblock ts ts.didscroll = YES rectangle 0, 0, 20, 20, uilook(uiBackground), dpage DIM tempx as integer DIM tempy as integer FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 tempx = (i + shiftx + 20) MOD 20 tempy = (j + shifty + 20) MOD 20 putpixel tempx, tempy, readpixel(ts.tilex * 20 + i, ts.tiley * 20 + j, 3), dpage NEXT j NEXT i FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 putpixel ts.tilex * 20 + i, ts.tiley * 20 + j, readpixel(i, j, dpage), 3 NEXT j NEXT i refreshtileedit ts rectangle 0, 0, 20, 20, uilook(uiBackground), dpage END SUB SUB fliptile (ts as TileEditState) writeundoblock ts rectangle 0, 0, 20, 20, uilook(uiBackground), dpage DIM flipx as integer = 0 DIM flipy as integer = 0 DIM tempx as integer DIM tempy as integer IF (ts.zone = 13 OR ts.zone = 16) OR keyval(scLeftBracket) > 1 OR (keyval(scBackspace) > 1 AND keyval(scCtrl) = 0) THEN flipx = 19 IF ts.zone = 14 OR ts.zone = 15 OR keyval(scRightBracket) > 1 OR (keyval(scBackspace) > 1 AND keyval(scCtrl) > 0) THEN flipy = 19 FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 tempx = ABS(i - flipx) tempy = ABS(j - flipy) IF (ts.zone = 15 OR ts.zone = 16) OR (keyval(scLeftBrace) > 1 OR keyval(scRightBrace) > 1) THEN SWAP tempx, tempy putpixel tempx, tempy, readpixel(ts.tilex * 20 + i, ts.tiley * 20 + j, 3), dpage NEXT j NEXT i FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 putpixel ts.tilex * 20 + i, ts.tiley * 20 + j, readpixel(i, j, dpage), 3 NEXT j NEXT i refreshtileedit ts rectangle 0, 0, 20, 20, uilook(uiBackground), dpage END SUB 'Loads a tileset or backdrop to a video page '(Note: vpage 2 and 3 are locked (lock_page_size) to 320x200) LOCAL SUB tilecut_load_source(ts as TileEditState, page as integer) DIM sprtype as SpriteType sprtype = IIF(ts.cuttileset, sprTypeTileset, sprTypeBackdrop) DIM temp as Frame ptr temp = frame_load(sprtype, ts.cutfrom) frame_clear vpages(page) IF temp THEN frame_draw temp, , 0, 0, NO, vpages(page) frame_unload @temp END IF END SUB SUB tilecut (ts as TileEditState, mouse as MouseInfo) DIM area(24) as MouseArea 'Tileset area(0).x = 0 area(0).y = 0 area(0).w = 320 area(0).h = 200 '"Prev" button area(10).x = 8 area(10).y = 200 - 10 area(10).w = 8 * 6 area(10).h = 10 '"Next" button area(11).x = 320 - 10 - 8 * 6 area(11).y = 200 - 10 area(11).w = 8 * 6 area(11).h = 10 STATIC snap_to_grid as bool = NO IF mouse.active THEN movemouse ts.x, ts.y END IF ts.delay = 3 DIM previewticks as integer = 0 tilecut_load_source ts, 2 DIM tog as integer DIM zcsr as integer setkeys DO setwait 55 setkeys tog = tog XOR 1 'Alt as alias for people who remember the old interface IF keyval(scAlt) > 1 OR keyval(scG) > 1 THEN snap_to_grid XOR= YES ts.delay = large(ts.delay - 1, 0) IF ts.gotmouse THEN mouse = readmouse zcsr = 0 ts.zone = mouseover(mouse.x, mouse.y, 0, 0, zcsr, area()) IF mouse.moved ANDALSO mouse.active THEN ts.x = small(mouse.x, 320 - 20) ts.y = small(mouse.y, 200 - 20) END IF END IF IF keyval(ccCancel) > 1 THEN EXIT DO END IF IF keyval(scF1) > 1 THEN show_help "tilecut" '' Move cursor by keyboard DIM inc as integer IF keyval(scShift) OR snap_to_grid THEN inc = 20 ELSE inc = 1 DIM as integer movex = 0, movey = 0 IF keyval(ccUp) AND 5 THEN movey = -inc IF keyval(ccDown) AND 5 THEN movey = inc IF keyval(ccLeft) AND 5 THEN movex = -inc IF keyval(ccRight) AND 5 THEN movex = inc ts.x = bound(ts.x + movex, 0, 320 - 20) ts.y = bound(ts.y + movey, 0, 200 - 20) IF (movex <> 0 OR movey <> 0) AND mouse.active THEN movemouse ts.x, ts.y IF snap_to_grid THEN ts.x -= ts.x MOD 20 ts.y -= ts.y MOD 20 END IF '' Cut tile IF enter_or_space() OR (mouse.release > 0 AND ts.zone = 1) THEN IF ts.delay = 0 THEN FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 putpixel ts.tilex * 20 + i, ts.tiley * 20 + j, readpixel(ts.x + i, ts.y + j, 2), 3 NEXT j NEXT i IF keyval(scEnter) > 1 OR (mouse.release AND (mouseRight OR mouseMiddle)) THEN ts.tiley = (ts.tiley + (ts.tilex + 1) \ 16) MOD 10 ts.tilex = (ts.tilex + 1) AND 15 ts.x += 20 IF ts.x > 300 THEN ts.x = 0 ts.y += 20 IF ts.y > 180 THEN ts.y = 0 END IF IF mouse.active THEN movemouse ts.x, ts.y previewticks = 12 ELSE EXIT DO END IF END IF END IF '' Changing source tileset/backdrop DIM oldcut as integer = ts.cutfrom DIM maxset as integer IF ts.cuttileset THEN maxset = gen(genMaxTile) ELSE maxset = gen(genNumBackdrops) - 1 END IF intgrabber ts.cutfrom, 0, maxset, scLeftCaret, scRightCaret IF ts.zone = 11 AND mouse.clicks > 0 THEN loopvar ts.cutfrom, 0, maxset, -1 IF ts.zone = 12 AND mouse.clicks > 0 THEN loopvar ts.cutfrom, 0, maxset, 1 IF oldcut <> ts.cutfrom THEN tilecut_load_source ts, 2 '' Draw screen IF previewticks THEN 'Show preview of destination tileset at top or bottom of screen DIM preview as Frame ptr DIM previewy as integer = bound(ts.tiley * 20 - 20, 0, 140) preview = frame_new_view(vpages(3), 0, previewy, vpages(3)->w, 59) clearpage dpage 'The tileset (page 2) may be smaller copypage 2, dpage IF ts.y < 100 THEN 'preview 59 pixels of tileset at bottom of screen frame_draw preview, , 0, 141, NO, dpage rectangle 0, 139, 320, 2, uilook(uiSelectedItem + tog), dpage drawbox ts.tilex * 20, 141 + ts.tiley * 20 - previewy, 20, 20, uilook(uiSelectedItem + tog), 1, dpage ELSE 'tileset preview at top of screen frame_draw preview, , 0, 0, NO, dpage rectangle 0, 59, 320, 2, uilook(uiSelectedItem + tog), dpage drawbox ts.tilex * 20, ts.tiley * 20 - previewy, 20, 20, uilook(uiSelectedItem + tog), 1, dpage END IF frame_unload @preview previewticks -= 1 ELSE clearpage dpage 'The tileset (page 2) may be smaller copypage 2, dpage END IF drawbox ts.x, ts.y, 20, 20, iif(tog, uilook(uiText), uilook(uiDescription)), 1, dpage textcolor uilook(uiMenuItem + tog), 1 IF ts.zone = 11 THEN textcolor uilook(uiSelectedItem + tog), uilook(uiHighlight) printstr "< Prev", area(10).x, area(10).y, dpage textcolor uilook(uiMenuItem + tog), 1 IF ts.zone = 12 THEN textcolor uilook(uiSelectedItem + tog), uilook(uiHighlight) printstr "Next >", area(11).x, area(11).y, dpage DIM ypos as integer IF ts.y < 100 THEN ypos = 200 - 20 ELSE ypos = 0 textcolor uilook(uiText), uilook(uiHighlight) DIM temp as string IF ts.cuttileset THEN temp = "Tileset " & ts.cutfrom ELSE temp = "Backdop " & ts.cutfrom END IF printstr temp, 320\2 + ancCenter, ypos, dpage temp = "X=" & ts.x & " Y=" & ts.y printstr temp, 320\2 + ancCenter, ypos + 10, dpage temp = hilite("G") & "ridsnap:" & yesorno(snap_to_grid, "On", "Off") printstr temp, 4, ypos, dpage, YES IF mouse.active THEN textcolor uilook(IIF(tog, uiText, uiDescription)), 0 printstr CHR(2), mouse.x - 2, mouse.y - 2, dpage END IF SWAP dpage, vpage setvispage vpage dowait LOOP IF mouse.active THEN movemouse ts.tilex * 20 + 10, ts.tiley * 20 + 10 END IF END SUB SUB tilecopy (cutnpaste() as integer, ts as TileEditState) FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 cutnpaste(i, j) = readpixel(ts.tilex * 20 + i, ts.tiley * 20 + j, 3) NEXT j NEXT i ts.canpaste = 1 END SUB SUB tilepaste (cutnpaste() as integer, ts as TileEditState) IF ts.canpaste THEN FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 putpixel ts.tilex * 20 + i, ts.tiley * 20 + j, cutnpaste(i, j), 3 NEXT j NEXT i IF channel_to_Game THEN storemxs game + ".til", ts.tilesetnum, vpages(3) END IF END SUB SUB tiletranspaste (cutnpaste() as integer, ts as TileEditState) IF ts.canpaste THEN FOR i as integer = 0 TO 19 FOR j as integer = 0 TO 19 IF cutnpaste(i, j) THEN putpixel ts.tilex * 20 + i, ts.tiley * 20 + j, cutnpaste(i, j), 3 NEXT j NEXT i IF channel_to_Game THEN storemxs game + ".til", ts.tilesetnum, vpages(3) END IF END SUB '============================================================================== ' The sprite buffer remains in a rotated state after a rotation operation, ' call this to clip it to the correct size. SUB spriteedit_clip (ss as SpriteEditState) IF ss.sprite->w <> ss.wide OR ss.sprite->h <> ss.high THEN frame_assign @ss.sprite, frame_resized(ss.sprite, ss.wide, ss.high) END IF END SUB ' Save the current edit state as an undo step SUB writeundospr (ss as SpriteEditState) WITH ss.undo(ss.framenum) ' Delete any redo steps. If undo .depth points before the end ' of history then we just undid a change before the current edit, ' and the current state is probably equal to .history[.depth], ' so we could skip saving, but to be on the safe side save overwrite ' the current step. v_delete_slice .history, .depth, v_len(.history) ' Trim history if too long IF .depth >= ss.undomax THEN v_delete_slice .history, 0, 1 .depth -= 1 END IF ' Append DIM spr as Frame ptr = frame_duplicate(ss.sprite) 'refcount == 1 v_append .history, spr 'Increments refcount .depth = v_len(.history) frame_unload @spr END WITH END SUB 'Unload ss.sprite and replace it. (Doesn't increment refcount) SUB spriteedit_replace_frame(ss as SpriteEditState, new_sprite as Frame ptr) frame_assign @ss.spriteset[ss.framenum], new_sprite 'ss.sprite doesn't hold a reference ss.sprite = new_sprite END SUB ' Change the current state of the sprite editor sprite to a new ' Frame, saving an undo step. SUB spriteedit_edit(ss as SpriteEditState, new_sprite as Frame ptr) writeundospr ss spriteedit_replace_frame ss, new_sprite END SUB SUB spriteedit_change_frame(ss as SpriteEditState, new_framenum as integer) 'Assume all frames are the same size ss.framenum = new_framenum ss.sprite = ss.spriteset[ss.framenum] IF NOT ss.fullset THEN DIM frameid as integer = ss.sprite->frameid ss.framename = "Frame " & frameid & " " & frame_name(ss.fileset, frameid) END IF ss.didscroll = NO END SUB 'Find and return the framenum for the frame with id `change_id` (1 or -1) from 'the current frame, looping within the frame. 'Unfortunately can't use frameid_to_frame because ss.spriteset is a Frame ptr vector not a Frame array. FUNCTION spriteedit_loop_frameid(ss as SpriteEditState, change_id as integer) as integer DIM frameid as integer = ss.sprite->frameid DIM as integer groupfirst = 0, grouplast = 0 FOR framenum as integer = v_len(ss.spriteset) - 1 TO 0 STEP -1 DIM thisid as integer = ss.spriteset[framenum]->frameid IF thisid = frameid + change_id THEN RETURN framenum IF thisid = frameid - (frameid MOD 100) THEN groupfirst = framenum IF thisid >= frameid - (frameid MOD 100) + 100 THEN grouplast = framenum - 1 NEXT RETURN IIF(change_id < 0, grouplast, groupfirst) END FUNCTION ' Perform undo SUB readundospr (ss as SpriteEditState) WITH ss.undo(ss.framenum) IF .depth > 0 THEN ' If there are no existing redo steps, then save current state for redo IF .depth = v_len(.history) THEN DIM spr as Frame ptr = frame_duplicate(ss.sprite) 'refcount == 1 v_append .history, spr 'Increments refcount frame_unload @spr END IF .depth -= 1 spriteedit_replace_frame ss, frame_duplicate(.history[.depth]) ss.didscroll = NO 'save a new undo block upon scrolling END IF END WITH END SUB ' Perform redo SUB readredospr (ss as SpriteEditState) WITH ss.undo(ss.framenum) IF .depth < v_len(.history) - 1 THEN .depth += 1 spriteedit_replace_frame ss, frame_duplicate(.history[.depth]) ss.didscroll = NO 'save a new undo block upon scrolling END IF END WITH END SUB ' Draw a 16-colour palette onscreen, with surrounding box SUB spriteedit_draw_palette(pal16 as Palette16 ptr, x as integer, y as integer, page as integer) drawbox x, y, 67, 8, uilook(uiText), 1, page FOR i as integer = 0 TO 15 rectangle x + 2 + (i * 4), y + 2, 3, 5, pal16->col(i), page NEXT END SUB ' Draw the zoomed and unzoomed sprite areas SUB spriteedit_draw_sprite_area(ss as SpriteEditState, sprite as Frame ptr, pal as Palette16 ptr, page as integer) drawbox ss.area(0).x - 1, ss.area(0).y - 1, ss.area(0).w + 2, ss.area(0).h + 2, uilook(uiText), 1, page frame_draw sprite, pal, 4, 1, NO, page, DrawOptions(ss.zoom) IF ss.onion_enabled ANDALSO ss.onionnum >= 0 ANDALSO ss.onionnum < v_len(ss.spriteset) THEN '-- Draw the onion layer in two steps because we can't scale and draw transparently at once DIM onion_overlay as Frame Ptr DIM onion_opts as DrawOptions WITH onion_opts .scale = ss.zoom END WITH onion_overlay = frame_new(sprite->w * ss.zoom, sprite->h * ss.zoom, , YES) frame_draw ss.spriteset[ss.onionnum], pal, 0, 0, YES, onion_overlay, onion_opts WITH onion_opts .scale = 1 .with_blending = YES .blend_mode = blendModeAdd .opacity = 0.5 END WITH frame_draw onion_overlay, , 4, 1, YES, page, onion_opts frame_unload @onion_overlay END IF drawbox ss.previewpos.x - 1, ss.previewpos.y - 1, ss.wide + 2, ss.high + 2, uilook(uiText), 1, page frame_draw sprite, pal, ss.previewpos.x, ss.previewpos.y, NO, page END SUB ' Draw sprite editor SUB spriteedit_display(ss as SpriteEditState) clearpage dpage spriteedit_draw_sprite_area ss, ss.sprite, ss.palette, dpage ss.curcolor = ss.palette->col(ss.palindex) 'Is this necessary? rectangle 247 + ((ss.curcolor - ((ss.curcolor \ 16) * 16)) * 4), 0 + ((ss.curcolor \ 16) * 6), 5, 7, uilook(uiText), dpage DIM as integer i, o FOR i = 0 TO 15 FOR o = 0 TO 15 rectangle 248 + (i * 4), 1 + (o * 6), 3, 5, o * 16 + i, dpage NEXT o NEXT i ' Draw the <-Pal###-> or <-Col###-> display textcolor_icon NO, ss.zonenum = 5 printstr CHR(27), 243, 100, dpage textcolor_icon NO, ss.zonenum = 6 printstr CHR(26), 307, 100, dpage textcolor uilook(uiText), 0 DIM paldisplay as string IF ss.showcolnum > 0 THEN paldisplay = " Col" & rlpad(STR(ss.curcolor), " ", 3, 4) IF keyval(scAlt) = 0 THEN ss.showcolnum -= 1 END IF ELSE paldisplay = " Pal" & rlpad(STR(ss.pal_num), " ", 3, 4) END IF printstr paldisplay, 243, 100, dpage rectangle 247 + (ss.palindex * 4), 110, 5, 7, uilook(uiText), dpage spriteedit_draw_palette ss.palette, 246, 109, dpage DIM select_rect as RectType corners_to_rect_inclusive Type(ss.x, ss.y), ss.holdpos, select_rect IF ss.hold = YES AND ss.tool = box_tool THEN rectangle 4 + select_rect.x * ss.zoom, 1 + select_rect.y * ss.zoom, select_rect.wide * ss.zoom, select_rect.high * ss.zoom, ss.curcolor, dpage rectangle 4 + ss.holdpos.x * ss.zoom, 1 + ss.holdpos.y * ss.zoom, ss.zoom, ss.zoom, IIF(ss.tog, uilook(uiBackground), uilook(uiText)), dpage END IF DIM overlay as Frame ptr overlay = frame_new(ss.wide, ss.high, , YES) 'hack: We don't draw real palette colours to overlay, otherwise we couldn't draw colour 0 DIM pal16 as Palette16 ptr pal16 = palette16_new() pal16->col(1) = ss.curcolor 'Show previews of tools IF ss.hold = YES AND ss.tool = box_tool THEN rectangle ss.previewpos.x + select_rect.x, ss.previewpos.y + select_rect.y, select_rect.wide, select_rect.high, ss.curcolor, dpage putpixel ss.previewpos.x + ss.holdpos.x, ss.previewpos.y + ss.holdpos.y, ss.tog * 15, dpage END IF IF ss.hold = YES AND ss.tool = line_tool THEN drawline ss.previewpos.x + ss.x, ss.previewpos.y + ss.y, ss.previewpos.x + ss.holdpos.x, ss.previewpos.y + ss.holdpos.y, ss.curcolor, dpage drawline overlay, ss.x, ss.y, ss.holdpos.x, ss.holdpos.y, 1 END IF IF ss.hold = YES AND ss.tool = oval_tool THEN ellipse vpages(dpage), ss.previewpos.x + ss.holdpos.x, ss.previewpos.y + ss.holdpos.y, ss.radius, ss.curcolor, , ss.ellip_minoraxis, ss.ellip_angle ellipse overlay, ss.holdpos.x, ss.holdpos.y, ss.radius, 1, , ss.ellip_minoraxis, ss.ellip_angle END IF IF ss.tool = airbrush_tool THEN ellipse vpages(dpage), ss.previewpos.x + ss.x, ss.previewpos.y + ss.y, ss.airsize / 2 - 0.5, ss.curcolor ' The 0.5,0.5 subtracted here is because ellipse internally adds 0.5,0.5 to draw from the center of a pixel. ' The 0.05 added to the size is in order to put the top/right/etc extents of the ellipse in general position ' with respect to the pixel centers, in other words it makes it look better, avoiding artifacts at the bottom ' and right edges. ellipse vpages(dpage), 4 + (ss.x + 0.5) * ss.zoom - 0.5, 1 + (ss.y + 0.5) * ss.zoom - 0.5, (ss.airsize / 2) * ss.zoom + 0.05, ss.curcolor END IF frame_draw overlay, pal16, 4, 1, , dpage, DrawOptions(ss.zoom) frame_unload @overlay palette16_unload @pal16 IF ss.tool <> clone_tool THEN 'Pixel at cursor position rectangle 4 + (ss.x * ss.zoom), 1 + (ss.y * ss.zoom), ss.zoom, ss.zoom, IIF(ss.tog, uilook(uiBackground), uilook(uiText)), dpage putpixel ss.previewpos.x + ss.x, ss.previewpos.y + ss.y, ss.tog * 15, dpage END IF IF ss.hold = YES AND ss.tool = mark_tool AND ss.tog = 0 THEN ss.curcolor = randint(255) ' Random color when marking a clone region drawbox 4 + select_rect.x * ss.zoom, 1 + select_rect.y * ss.zoom, select_rect.wide * ss.zoom, select_rect.high * ss.zoom, ss.curcolor, ss.zoom, dpage drawbox ss.previewpos.x + select_rect.x, ss.previewpos.y + select_rect.y, select_rect.wide, select_rect.high, ss.curcolor, 1, dpage END IF DIM temppos as XYPair IF ss.tool = clone_tool AND ss_save.clone_brush <> NULL AND ss.tog = 0 THEN temppos.x = ss.x - ss_save.clonepos.x temppos.y = ss.y - ss_save.clonepos.y IF ss.readjust THEN temppos.x += (ss.adjustpos.x - ss.x) temppos.y += (ss.adjustpos.y - ss.y) END IF frame_draw ss_save.clone_brush, ss.palette, 4 + temppos.x * ss.zoom, 1 + temppos.y * ss.zoom, , dpage, DrawOptions(ss.zoom) frame_draw ss_save.clone_brush, ss.palette, ss.previewpos.x + temppos.x, ss.previewpos.y + temppos.y, , dpage END IF textcolor uilook(uiMenuItem), 0 printstr strprintf("x=%2d y=%2d", ss.x, ss.y), 0, 190, dpage printstr "Tool:" & ss.toolinfo(ss.tool).name, 0, 182, dpage DIM caption_line as string = ss.framename IF ss.onion_enabled THEN DIM onionid as integer = ss.spriteset[ss.onionnum]->frameid caption_line &= " (Onionskin:" & onionid DIM onionname as string = frame_name(ss.fileset, onionid) IF LEN(onionname) > 0 THEN caption_line &= " " & onionname caption_line &= ")" END IF printstr caption_line , 0, 174, dpage FOR i = 0 TO UBOUND(ss.toolinfo) spriteedit_draw_icon ss, ss.toolinfo(i).icon, ss.toolinfo(i).areanum, (ss.tool = i) NEXT i spriteedit_draw_icon ss, CHR(7), 3 'horizontal flip spriteedit_draw_icon ss, "I", 12 spriteedit_draw_icon ss, "E", 25 DIM byref undo as SpriteEditUndoState = ss.undo(ss.framenum) IF undo.depth = 0 THEN textcolor uilook(uiBackground), uilook(uiDisabledItem) ELSE textcolor_icon ss.zonenum = 20, NO END IF printstr "UNDO", 130, 182, dpage ' Both undo.depth = len and undo.depth = len-1 are valid and indicate ' no more redo history (the later means no unsaved changes) IF undo.depth >= v_len(undo.history) - 1 THEN textcolor uilook(uiBackground), uilook(uiDisabledItem) ELSE textcolor_icon ss.zonenum = 21, NO END IF printstr "REDO", 170, 182, dpage IF ss.tool = airbrush_tool THEN textcolor uilook(uiMenuItem), 0 printstr "SIZE" & ss.airsize, 228, 182, dpage printstr "MIST" & ss.mist, 228, 190, dpage spriteedit_draw_icon ss, CHR(27), 14 spriteedit_draw_icon ss, CHR(27), 15 spriteedit_draw_icon ss, CHR(26), 16 spriteedit_draw_icon ss, CHR(26), 17 END IF IF ss.tool <> airbrush_tool THEN textcolor uilook(uiMenuItem), 0 printstr "ROTATE", 228, 190, dpage spriteedit_draw_icon ss, CHR(27), 15 spriteedit_draw_icon ss, CHR(26), 17 END IF IF ss.mouse.active THEN IF ss.zonecursor = -1 THEN IF ss.hidemouse THEN ss.zonecursor = -2 ELSE ss.zonecursor = ss.drawcursor END IF textcolor uilook(IIF(ss.tog, uiText, uiDescription)), 0 printstr CHR(2 + ss.zonecursor), ss.mouse.x - 2, ss.mouse.y - 2, dpage END IF END SUB SUB textcolor_icon(selected as bool, hover as bool) DIM as integer fg = uiMenuItem, bg = uiDisabledItem IF selected THEN fg = uiText bg = uiMenuItem END IF IF hover THEN IF selected THEN fg = uiSelectedItem ELSE fg = uiText bg = uiSelectedDisabled END IF textcolor uilook(fg), uilook(bg) END SUB 'Draw one of the clickable areas (obviously this will all be replaced with slices eventually) SUB spriteedit_draw_icon(ss as SpriteEditState, icon as string, byval areanum as integer, byval highlight as bool = NO) textcolor_icon highlight, (ss.zonenum = areanum + 1) printstr icon, ss.area(areanum).x, ss.area(areanum).y, dpage END SUB SUB init_sprite_zones(area() as MouseArea, ss as SpriteEditState) DIM i as integer 'DRAWING ZONE area(0).w = ss.wide * ss.zoom area(0).h = ss.high * ss.zoom area(0).x = 4 area(0).y = 1 area(0).hidecursor = YES 'PALETTE ZONE area(1).x = 248 area(1).y = 111 area(1).w = 64 area(1).h = 6 area(1).hidecursor = NO 'MASTER PAL ZONE area(2).x = 248 area(2).y = 1 area(2).w = 64 area(2).h = 96 area(2).hidecursor = NO 'FLIP BUTTON area(3).x = 184 area(3).y = 190 area(3).w = 8 area(3).h = 10 area(3).hidecursor = NO 'PREV PAL BUTTON area(4).x = 243 area(4).y = 100 area(4).w = 8 area(4).h = 8 area(4).hidecursor = NO 'NEXT PAL BUTTON area(5).x = 307 area(5).y = 100 area(5).w = 8 area(5).h = 8 area(5).hidecursor = NO 'TOOL BUTTONS (more below at 21-24) FOR i = 0 TO 5 area(6 + i).x = 80 + i * 10 area(6 + i).y = 190 area(6 + i).w = 8 area(6 + i).h = 10 area(6 + i).hidecursor = NO NEXT i 'IMPORT BUTTON area(12).x = 196 area(12).y = 190 area(12).w = 8 area(12).h = 10 area(12).hidecursor = NO 'SMALL DRAWING AREA area(13).w = ss.wide area(13).h = ss.high area(13).x = ss.previewpos.x area(13).y = ss.previewpos.y area(13).hidecursor = YES 'LESS AIRBRUSH AREA area(14).x = 220 area(14).y = 182 area(14).w = 8 area(14).h = 8 area(14).hidecursor = NO 'LESS AIRBRUSH MIST area(15).x = 220 area(15).y = 190 area(15).w = 8 area(15).h = 8 area(15).hidecursor = NO 'MORE AIRBRUSH AREA area(16).x = 276 area(16).y = 182 area(16).w = 8 area(16).h = 8 area(16).hidecursor = NO 'MORE AIRBRUSH MIST area(17).x = 276 area(17).y = 190 area(17).w = 8 area(17).h = 8 area(17).hidecursor = NO 'PALETTE NUMBER area(18).x = 256 area(18).y = 100 area(18).w = 48 area(18).h = 8 area(18).hidecursor = NO 'UNDO BUTTON area(19).x = 130 area(19).y = 182 area(19).w = 32 area(19).h = 8 area(19).hidecursor = NO 'REDO BUTTON area(20).x = 170 area(20).y = 182 area(20).w = 32 area(20).h = 8 area(20).hidecursor = NO 'MORE TOOLS FOR i = 0 TO 3 area(21 + i).x = 140 + i * 10 area(21 + i).y = 190 area(21 + i).w = 8 area(21 + i).h = 10 area(21 + i).hidecursor = NO NEXT i 'EXPORT BUTTON area(25).x = 206 area(25).y = 190 area(25).w = 8 area(25).h = 10 area(25).hidecursor = NO END SUB FUNCTION default_export_name (sprtype as SpriteType, setnum as integer, framenum as integer = -1, fullset as bool = NO) as string DIM s as string s = game_fname & " " & exclude(LCASE(sprite_sizes(sprtype).name), " ") IF fullset THEN s &= " set " & setnum ELSEIF framenum = -1 THEN 'For animations s &= " " & setnum ELSE s &= " " & setnum & " frame " & framenum END IF RETURN s END FUNCTION 'Customisable export format for graphic assets. Not intended for screenshots and the like. 'Returns png/bmp. In future could depend on the particular sprite or spritetype. FUNCTION pick_graphics_export_format() as string DIM image_format as string = read_config_str("graphics_export_format", "png") IF image_file_type("x." & image_format) = imUnknown THEN debug "Bad graphics_export_format " & image_format image_format = "png" END IF 'Sadly we don't support .gif import yet, so disabling that for sanity IF image_format = "jpg" OR image_format = "jpeg" OR image_format = "gif" THEN debug "Dumb graphics_export_format " & image_format image_format = "png" END IF RETURN image_format END FUNCTION SUB spriteedit_export(default_name as string, sprite as Frame ptr, pal as Palette16 ptr) STATIC defaultdir as string DIM extension as string = "." & pick_graphics_export_format() DIM outfile as string outfile = inputfilename("Export as image", extension, defaultdir, "input_file_export_sprite", default_name) IF outfile <> "" THEN frame_export_image sprite, outfile & extension, master(), pal END IF END SUB SUB spriteedit_export_spriteset(def_filename as string, ss as Frame ptr, pal as Palette16 ptr) DIM spritesheet as Frame ptr spritesheet = spriteset_to_basic_spritesheet(ss) spriteedit_export def_filename, spritesheet, pal frame_unload @spritesheet END SUB 'Load an image of any bitdepth into a Frame which has just 16 colours: those in pal16 'defaultpal is SpriteEditState.palette SUB spriteedit_import16_loadimage(srcfile as string, byref impsprite as Frame ptr, byref pal16 as Palette16 ptr, defaultpal as Palette16 ptr = NULL) DIM img as GraphicPair IF image_import_as_frame_and_palette16(img, srcfile, defaultpal) = NO THEN notification "Could not load " & srcfile & !"\nCheck c_debug.txt for detailed error messages (Press Ctrl-F8)" EXIT SUB END IF impsprite = img.sprite pal16 = img.pal IF pal16 = NULL THEN pal16 = Palette16_new_identity(256) 'Special case (mainly intended for paletted images, but this could also happen 'if there are only a few colours in the master palette): 'don't do any remapping if the indices are already 0-15 DIM require_remap as bool = NO FOR x as integer = 0 TO impsprite->w - 1 FOR y as integer = 0 TO impsprite->h - 1 IF readpixel(impsprite, x, y) >= 16 THEN require_remap = YES NEXT NEXT IF require_remap = NO THEN EXIT SUB 'We're done 'Find the set of colours used in impsprite, and remap impsprite 'to those colour indices '(This changes the order of the colors. Would be nice to keep it the same) DIM vpal16 as integer vector v_new vpal16 'v_append vpal16, 0 FOR x as integer = 0 TO impsprite->w - 1 'small(impsprite->w, ss.wide) - 1 FOR y as integer = 0 TO impsprite->h - 1 'small(impsprite->h, ss.high) - 1 DIM col as integer = pal16->col(small(pal16->numcolors - 1, readpixel(impsprite, x, y))) DIM at as integer = v_find(vpal16, col) IF at = -1 THEN v_append vpal16, col col = v_len(vpal16) - 1 ELSE col = at END IF putpixel(impsprite, x, y, col) NEXT NEXT debuginfo srcfile & " contains " & v_len(vpal16) & " colors" IF v_len(vpal16) > 16 THEN notification "This image contains " & v_len(vpal16) & " colors (after finding nearest-matches to the master palette). At most 16 are allowed." _ " Reduce the number of colors and try importing again." palette16_unload @pal16 frame_unload @impsprite v_free vpal16 EXIT SUB ELSE FOR i as integer = 0 TO v_len(vpal16) - 1 pal16->col(i) = vpal16[i] NEXT i END IF v_free vpal16 END SUB 'Returns a new Frame (NULL if cancelled). Delete the input one if didn't cancel. 'numframes is the number of frames of the existing spriteset we're replacing (in future: will be used only as a default). FUNCTION spriteedit_import16_cut_custom_frames(byref ss as SpriteEditState, impsprite as Frame ptr, pal16 as Palette16 ptr, numframes as integer) as Frame ptr DIM image_pos as XYPair = (1, 1) 'Screen position at which to draw impsprite 'This staticness is a bit hacky 'Used to reset to the default frame size if the fileset differed from last time 'The when-to-reset logic is not good. STATIC frame_size as XYPair STATIC first_offset as XYPair 'Position of first frame STATIC direction_offset as XYPair 'Offset between direction groups STATIC frame_offset as XYPair 'Offset between frames for the same direction DIM flattened_set as Frame ptr WITH sprite_sizes(ss.fileset) DIM frames_per_dir as integer 'Note that sprTypeHero has .directions = 4 'TODO: Would like to allow changing frames_per_dir IF .fixed_framecount ANDALSO numframes <> .frames THEN showerror "The existing number of frames should be " & .frames & " but is " & numframes & ". Unless this is a future .rpg, this is a bug" END IF 'IF .fixed_framecount THEN numframes = .frames frames_per_dir = large(1, numframes \ .directions) IF frames_per_dir * .directions <> numframes THEN notification "The existing number of frames, " & numframes & ", doesn't divide evenly into " & .directions & _ " directions, and this importer doesn't support changing the frame count. Change the number " _ "of frames (with correct frameids) in the existing spriteset to a multiple of " & .directions RETURN NULL END IF DIM old_frame_size as XYPair = ss.sprite->size old_frame_size.x \= ss.true_numframes IF frame_size <> old_frame_size THEN frame_size = old_frame_size first_offset = XY(0, 0) frame_offset = XY(frame_size.wide, 0) direction_offset = XY(frame_size.wide * frames_per_dir, 0) END IF DIM tog as integer DIM menu(8) as string DIM st as MenuState DIM menuopts as MenuOptions menuopts.edged = YES menuopts.itemspacing = -1 st.active = YES IF .directions > 1 THEN st.last = 7 ELSE st.last = 5 END IF st.size = st.last + 1 setkeys DO setwait 55 setkeys tog XOR= 1 'Every frame re-layout the menu 'The Y position of the text at bottom of the screen DIM texty as integer = vpages(dpage)->h - (st.last + 2) * 8 - 4 DIM zoom as integer ' Choose maximum zoom that will fit zoom = small(large(1, vpages(dpage)->w \ impsprite->w), large(1, texty \ impsprite->h)) IF keyval(ccCancel) > 1 THEN RETURN NULL IF keyval(scF1) > 1 THEN show_help "sprite_import16_cut_frames" IF enter_or_space() THEN EXIT DO usemenu st DIM temp as integer = st.pt IF .directions = 1 AND temp >= 2 THEN temp += 2 SELECT CASE temp CASE 0: intgrabber first_offset.x, -impsprite->w, impsprite->w CASE 1: intgrabber first_offset.y, -impsprite->h, impsprite->h CASE 2: intgrabber direction_offset.x, -impsprite->w, impsprite->w CASE 3: intgrabber direction_offset.y, -impsprite->h, impsprite->h CASE 4: intgrabber frame_offset.x, -impsprite->w, impsprite->w CASE 5: intgrabber frame_offset.y, -impsprite->h, impsprite->h CASE 6: intgrabber frame_size.w, 0, .size.w CASE 7: intgrabber frame_size.h, 0, .size.h 'CASE 8: intgrabber frames_per_dir, 1, 99 END SELECT menu(0) = "First frame x: " & first_offset.x menu(1) = "First frame y: " & first_offset.y temp = 2 IF .directions > 1 THEN menu(2) = "Direction group offset x: " & direction_offset.x menu(3) = "Direction group offset y: " & direction_offset.y temp = 4 END IF menu(temp) = "Each-frame offset x: " & frame_offset.x menu(temp + 1) = "Each-frame offset y: " & frame_offset.y menu(temp + 2) = "Frame width: " & frame_size.w menu(temp + 3) = "Frame height: " & frame_size.h 'menu(temp + 4) = "Frames per direction: " & frames_per_dir '--Draw screen clearpage dpage drawbox image_pos.x - 1, image_pos.y - 1, zoom * impsprite->w + 2, zoom * impsprite->h + 2, uilook(uiMenuItem), 1, dpage frame_draw impsprite, pal16, image_pos.x, image_pos.y, NO, dpage, DrawOptions(zoom) DIM framenum as integer = 0 FOR direction as integer = 0 TO .directions - 1 FOR dirframe as integer = 0 TO frames_per_dir - 1 DIM as integer x, y 'coords in terms of impsprite pixels x = first_offset.x + direction * direction_offset.x + dirframe * frame_offset.x y = first_offset.y + direction * direction_offset.y + dirframe * frame_offset.y drawbox image_pos.x + zoom * x, image_pos.y + zoom * y, zoom * frame_size.w, zoom * frame_size.h, uilook(uiText), 1, dpage framenum += 1 NEXT NEXT 'edgeprint "Offsets between spriteset frames:", 0, texty, uilook(uiText), dpage, YES, YES edgeprint "Define spriteset layout and press ENTER", 0, texty, uilook(uiText), dpage, YES, YES standardmenu menu(), st, 0, texty + 10, dpage, menuopts SWAP vpage, dpage setvispage vpage dowait LOOP ' Cut out the frames and place in a new one ' FIXME: this allows changing the size of box borders, althhough that's meant to be disallowed (but not ' atually harmful) flattened_set = frame_new(frame_size.w * frames_per_dir * .directions, frame_size.h) frame_clear flattened_set, 0 DIM framenum as integer = 0 FOR direction as integer = 0 TO .directions - 1 FOR dirframe as integer = 0 TO frames_per_dir - 1 DIM as integer x, y x = first_offset.x + direction * direction_offset.x + dirframe * frame_offset.x y = first_offset.y + direction * direction_offset.y + dirframe * frame_offset.y DIM impview as Frame ptr = frame_new_view(impsprite, x, y, frame_size.w, frame_size.h) frame_draw impview, , framenum * frame_size.w, 0, NO, flattened_set frame_unload @impview framenum += 1 NEXT NEXT frame_unload @impsprite END WITH RETURN flattened_set END FUNCTION 'Select a single pixel from a Frame, used for selecting the background colour. 'pal16 may be NULL. 'zoom is the zoom to draw at, -1 to automatically pick. 'Can restrict the selected pixel to the top left corner of the image by passing maxx, maxy args 'Returns NO if user cancelled, otherwise YES and the pixel coordinate is returned in pickpos FUNCTION pick_image_pixel(image as Frame ptr, pal16 as Palette16 ptr = NULL, byref pickpos as XYPair, zoom as integer = -1, maxx as integer = 9999, maxy as integer = 9999, message as string, helpkey as string) as bool DIM ret as bool DIM tog as integer DIM picksize as XYPair 'pickpos will be set to mouse cursor position pickpos.x = 0 pickpos.y = 0 IF zoom = -1 THEN DIM resfrac as XYPair = get_resolution() \ image->size zoom = bound(small(resfrac.w, resfrac.h), 1, 5) END IF DIM imagepos as XYPair ' If it's smaller than the screen, offset the image so it's not sitting in the corner IF maxx * zoom + 4 < vpages(dpage)->w THEN imagepos.x = 4 imagepos.y = 1 END IF DIM prev_mouse_vis as CursorVisibility = getcursorvisibility() hidemousecursor DIM mouse as MouseInfo setkeys DO setwait 20 setkeys mouse = readmouse() DIM mouse_in_bounds as bool mouse_in_bounds = mouse.active ANDALSO (mouse.pos >= imagepos) ANDALSO (mouse.pos < (imagepos + picksize * zoom)) tog XOR= 1 IF keyval(ccCancel) > 1 THEN ret = NO : EXIT DO IF keyval(scF1) > 1 THEN show_help helpkey IF enter_or_space() OR (mouse_in_bounds AND (mouse.release AND mouseleft)) THEN ret = YES EXIT DO END IF picksize.x = small(vpages(dpage)->w, small(image->w, maxx)) picksize.y = small(vpages(dpage)->h, small(image->h, maxy)) IF mouse.moved AND mouse_in_bounds THEN pickpos = (mouse.pos - imagepos) \ zoom ELSE DIM movespeed as integer IF keyval(scShift) THEN movespeed = 9 ELSE movespeed = 1 IF keyval(ccUp) > 1 THEN pickpos.y -= movespeed IF keyval(ccDown) > 1 THEN pickpos.y += movespeed IF keyval(ccleft) > 1 THEN pickpos.x -= movespeed IF keyval(ccRight) > 1 THEN pickpos.x += movespeed pickpos.x = bound(pickpos.x, 0, picksize.x - 1) pickpos.y = bound(pickpos.y, 0, picksize.y - 1) END IF clearpage dpage frame_draw image, pal16, imagepos.x, imagepos.y, NO, dpage, DrawOptions(zoom) 'Draw box around the selectable proportion of the image drawbox imagepos.x - 1, imagepos.y - 1, picksize.x * zoom + 2, picksize.y * zoom + 2, uilook(uiText), 1, dpage '--Draw info box at top right DIM current_col as integer = readpixel(image, pickpos.x, pickpos.y) DIM msg as string = !"Color:\n" & current_col DIM master_col as integer ' Index in master() IF pal16 THEN master_col = pal16->col(current_col) msg &= !"\nMapped\nto " & master_col ELSE master_col = current_col END IF rectangle pRight, 0, 50, 40, master_col, dpage edgeprint msg, pRight, ancCenter + 20, uilook(uiMenuItem), dpage, YES, YES '--Draw the pixel cursor DIM col as integer IF tog THEN col = uilook(uiBackground) ELSE col = uilook(uiText) IF zoom = 1 THEN IF mouse.active THEN 'A single pixel is too small, so draw the crosshair mouse cursor. textcolor uilook(uiSelectedItem + tog), 0 printstr CHR(5), mouse.x - 2, mouse.y - 2, dpage END IF textcolor col, 0 printstr CHR(5), imagepos.x + pickpos.x - 2, imagepos.y + pickpos.y - 2, dpage ELSE 'Draw both pixel cursor and mouse cursor rectangle imagepos.x + pickpos.x * zoom, imagepos.y + pickpos.y * zoom, zoom, zoom, col, dpage IF mouse.active THEN textcolor uilook(uiSelectedItem + tog), 0 printstr CHR(2), mouse.x - 2, mouse.y - 2, dpage END IF END IF edgeprint message, pInfoX, pInfoY, uilook(uiText), dpage, YES, YES 'edgeprint "SHIFT: move faster", pRight, pBottom, uilook(uiMenuItem), dpage, YES, YES SWAP vpage, dpage setvispage vpage dowait LOOP setcursorvisibility(prev_mouse_vis) RETURN ret END FUNCTION 'Lets the use pick one of the colour/pixels in impsprite, returns the colour index 'Returns -1 if cancelled. FUNCTION spriteedit_import16_pick_bgcol(impsprite as Frame ptr, pal16 as Palette16 ptr) as integer DIM pickpos as XYPair DIM ret as bool ret = pick_image_pixel(impsprite, pal16, pickpos, -1, , , "Pick background (transparent) color", "sprite_import16_pickbackground") IF ret = NO THEN RETURN -1 RETURN readpixel(impsprite, pickpos.x, pickpos.y) END FUNCTION 'Set can_remap to whether a mapping from new_pal to old_pal (eg. SpriteEditState.palette) exists, 'and if so write it in palmapping(). SUB spriteedit_import16_compare_palettes(byval old_pal as Palette16 ptr, byval new_pal as Palette16 ptr, palmapping() as integer, byref can_remap as bool, byref is_identical as bool) can_remap = YES is_identical = YES FOR i as integer = 1 TO 15 'IF new_pal->col(i) <> old_pal->col(i) THEN is_identical = NO IF color_distance(master(), new_pal->col(i), old_pal->col(i)) > 0 THEN is_identical = NO DIM found as bool = NO FOR j as integer = 1 TO 15 'IF new_pal->col(i) = old_pal->col(j) THEN IF color_distance(master(), new_pal->col(i), old_pal->col(j)) = 0 THEN palmapping(i) = j found = YES EXIT FOR END IF NEXT IF found = NO THEN can_remap = NO NEXT END SUB 'Return value: see retval() 'Also returns contents of palmapping() FUNCTION spriteedit_import16_remap_menu(byref ss as SpriteEditState, byref impsprite as Frame ptr, byref pal16 as Palette16 ptr, palmapping() as integer) as integer DIM can_remap as bool DIM is_identical as bool DIM usepal as Palette16 ptr DIM ret as integer DIM pmenu(2) as string DIM retval(2) as integer DIM palstate as MenuState palstate.pt = 0 palstate.last = 2 palstate.size = 2 palstate.need_update = YES showmousecursor setkeys DO setwait 55 setkeys IF keyval(ccCancel) > 1 THEN ret = 3 : EXIT DO IF keyval(scF1) > 1 THEN show_help "frame_import16" DIM clicked_zone as integer = -1 IF readmouse.release AND mouseLeft THEN 'We share the prev pal/next pal/pal browser button locations with the main sprite editor clicked_zone = mouseover(readmouse.x, readmouse.y, 0, 0, 0, ss.area()) END IF IF keyval(ccLeft) > 1 OR keyval(scLeftBrace) > 1 OR clicked_zone = 5 THEN 'Prev pal changepal ss, -1 palstate.need_update = YES END IF IF keyval(ccRight) > 1 OR keyval(scRightBrace) > 1 OR clicked_zone = 6 THEN 'Next pal changepal ss, 1 palstate.need_update = YES END IF IF pal_num_intgrabber(ss, 0, 0) THEN palstate.need_update = YES IF keyval(scP) > 1 OR clicked_zone = 19 THEN 'Clicked on "Pal ### spriteedit_pal16_browser ss, impsprite END IF IF usemenu(palstate) THEN palstate.need_update = YES IF enter_space_click(palstate) THEN ret = retval(palstate.pt) : EXIT DO IF palstate.need_update THEN palstate.need_update = NO spriteedit_import16_compare_palettes ss.palette, pal16, palmapping(), can_remap, is_identical pmenu(0) = "Overwrite Current Palette" retval(0) = 0 IF can_remap THEN pmenu(1) = "Remap into Current Palette" retval(1) = 1 ELSE pmenu(1) = "Import Without Palette" retval(1) = 2 END IF pmenu(2) = "Cancel Import" retval(2) = 3 IF palstate.pt = 1 AND can_remap = NO THEN 'Preview import without palette usepal = ss.palette ELSE usepal = pal16 END IF END IF clearpage dpage spriteedit_draw_sprite_area ss, impsprite, usepal, dpage 'Draw palettes textcolor uilook(uiText), 0 printstr bgcol_text(CHR(27), uilook(uiDisabledItem)) _ & "Pal" & rlpad(STR(ss.pal_num), " ", 3, 4) _ & bgcol_text(CHR(26), uilook(uiDisabledItem)), 243, 100, dpage, YES spriteedit_draw_palette ss.palette, 246, 109, dpage printstr "Image Pal", 245, 80, dpage spriteedit_draw_palette pal16, 246, 89, dpage rectangle 4, 144, 224, 32, uilook(uiDisabledItem), dpage standardmenu pmenu(), palstate, 8, 148, dpage edgeprint "(Press LEFT or RIGHT to select palette)", 0, 188, uilook(uiMenuItem), dpage SWAP vpage, dpage setvispage vpage dowait LOOP hidemousecursor RETURN ret END FUNCTION 'Used to import a full spriteset. Called from the editor in fullset mode. 'TODO: Therefore it's not currently possible for sprite import to change the number of frames. 'Input is an imported image (a single Frame) and output is a spriteset as a basic spritesheet '(ie all frames concatenated into one Frame -- but after cutting up and putting back together). 'The result might be a different size. 'Unloads (decrements refcount of) impsprite. Passes back ownership of a Frame (which might be the same Frame), 'or returns NULL if cancelled. FUNCTION spriteedit_import16_split_spriteset(ss as SpriteEditState, impsprite as Frame ptr, pal as Palette16 ptr) as Frame ptr 'Note that XY(ss.wide, ss.high) == ss.sprite->size IF impsprite->size <> ss.sprite->size THEN DIM numframes as integer = ss.true_numframes DIM size0 as XYPair = XY(large(1, impsprite->w \ numframes), impsprite->h) DIM choices(...) as string = { _ strprintf("Import as %d frames of %dx%d", numframes, size0.w, size0.h), _ strprintf("Import as %d frames of %dx%d (expand or trim)", numframes, ss.sprite->w \ numframes, ss.sprite->h), _ strprintf("Import from custom layout of %d frames", numframes) _ } DIM choice as integer DO choice = multichoice(strprintf("This image is %dx%d pixels, different from the %dx%d export image " _ "size of the current spriteset. (If you want to change the number of frames, you " _ "need to do so before import.)", _ impsprite->w, impsprite->h, ss.sprite->w, ss.sprite->h), choices()) IF choice = -1 THEN frame_unload @impsprite RETURN NULL ELSEIF choice = 0 THEN DIM resized as Frame ptr 'The following looks like a noop, but what it accomplishes is rounding up/down 'the frame size if numframes doesn't divide evenly into impsprite->w 'resized = spriteset_from_basic_spritesheet(impsprite, ss.fileset, sprite_sizes(ss.fileset).frames) resized = split_spritesheet(impsprite, size0, numframes) 'copy_spriteset_frameids resized, ... 'Pointless, frameids won't be used frame_assign @impsprite, spriteset_to_basic_spritesheet(resized) frame_unload @resized ELSEIF choice = 1 THEN 'Simply expand or trim the image frame_assign @impsprite, frame_resized(impsprite, ss.sprite->w, ss.sprite->h) ELSEIF choice = 2 THEN DIM adjusted as Frame ptr adjusted = spriteedit_import16_cut_custom_frames(ss, impsprite, pal, numframes) IF adjusted = NULL THEN CONTINUE DO 'Cancelled, go back to the menu frame_assign @impsprite, adjusted END IF EXIT DO LOOP END IF RETURN impsprite END FUNCTION 'state.pt is the current palette number FUNCTION spriteedit_import16(byref ss as SpriteEditState) as Frame ptr DIM srcfile as string STATIC default as string 'Any image, any size srcfile = browse(browseImage, default, , "browse_import_sprite") IF srcfile = "" THEN RETURN NULL DIM as Frame ptr impsprite, impsprite2 DIM pal16 as Palette16 ptr spriteedit_import16_loadimage srcfile, impsprite, pal16, ss.palette IF impsprite = NULL THEN RETURN NULL 'frame_export_bmp4 "debug0.bmp", impsprite, master(), pal16 'Pick background color DIM bgcol as integer bgcol = spriteedit_import16_pick_bgcol(impsprite, pal16) IF bgcol = -1 THEN 'cancelled frame_unload @impsprite palette16_unload @pal16 RETURN NULL END IF 'Swap the transparent pixels to 0 swapcolors impsprite, 0, bgcol SWAP pal16->col(0), pal16->col(bgcol) IF ss.fullset THEN 'When importing a whole spriteset, it's possible the change the frame size. impsprite = spriteedit_import16_split_spriteset(ss, impsprite, pal16) IF impsprite = NULL THEN 'Cancelled palette16_unload @pal16 RETURN NULL END IF 'Note: if we cancel import, must undo this sprite_editor_update_for_sprite_size ss, impsprite ELSE 'Trim or expand the image to final dimensions frame_assign @impsprite, frame_resized(impsprite, ss.wide, ss.high) END IF 'Check whether pal16 can be mapped directly onto the existing palette '(ignoring background colours), and whether it's actually the same DIM can_remap as bool DIM is_identical as bool DIM palmapping(15) as integer spriteedit_import16_compare_palettes ss.palette, pal16, palmapping(), can_remap, is_identical 'Prompt about remapping palette DIM remap as integer IF is_identical THEN remap = 2 debuginfo "spriteedit_import16: palette is identical" ELSE remap = spriteedit_import16_remap_menu(ss, impsprite, pal16, palmapping()) END IF IF remap = 0 THEN 'Overwrite current palette FOR i as integer = 0 TO 15 ss.palette->col(i) = pal16->col(i) NEXT i 'If the palette has changed, update genMaxPal gen(genMaxPal) = large(gen(genMaxPal), ss.pal_num) ELSEIF remap = 1 THEN 'Remap into current palette remap_to_palette impsprite, palmapping() ELSEIF remap = 2 THEN 'Import without palette ELSEIF remap = 3 THEN 'Cancel Import frame_unload @impsprite palette16_unload @pal16 sprite_editor_update_for_sprite_size ss, ss.sprite 'Restore original sprite size RETURN NULL END IF palette16_unload @pal16 RETURN impsprite END FUNCTION 'Called for SpriteEditState initialisation which depends on sprite size. 'Call this if the sprite size changes. SUB sprite_editor_update_for_sprite_size(byref ss as SpriteEditState, sprite as Frame ptr) WITH ss .wide = sprite->w .high = sprite->h .zoom = large(1, small(240 \ .wide, 170 \ .high)) .x = small(.x, .wide - 1) .y = small(.y, .high - 1) .fastmovestep = large(4, .wide \ 10) .previewpos.x = 319 - .wide .previewpos.y = 119 .undomax = maxSpriteHistoryMem \ (sizeof(Frame) + .wide * .high) 'Could shorten undo .history too END WITH 'DRAWING ZONE WITH ss.area(0) .w = ss.wide * ss.zoom .h = ss.high * ss.zoom .x = 4 .y = 1 .hidecursor = YES END WITH END SUB ' Once the public members of ss have already been filled with arguments ' to sprite_editor, this function initialises the private members. ' Should be matched with call to sprite_editor_cleanup. SUB sprite_editor_initialise(byref ss as SpriteEditState) WITH ss .palette = palette16_load(.pal_num) 'Set .sprite and .framename spriteedit_change_frame ss, .framenum .delay = 10 .x = ss_save.cursor.x .y = ss_save.cursor.y .lastpos.x = -1 .lastpos.y = -1 .zone.x = 0 .zone.y = 0 .hold = NO .gotmouse = havemouse() .didscroll = NO .drawcursor = 1 .tool = ss_save.tool .airsize = ss_save.airsize .mist = ss_save.mist .palindex = ss_save.palindex .hidemouse = ss_save.hidemouse .onionnum = -1 REDIM .undo(v_len(.spriteset) - 1) FOR i as integer = 0 TO UBOUND(.undo) v_new .undo(i).history .undo(i).depth = 0 NEXT END WITH 'Initialises everything in ss that depends on sprite size sprite_editor_update_for_sprite_size ss, ss.sprite init_sprite_zones ss.area(), ss WITH ss.toolinfo(draw_tool) .name = "Draw" .icon = CHR(3) .shortcut = scD .cursor = 0 .areanum = 6 END WITH WITH ss.toolinfo(box_tool) .name = "Box" .icon = CHR(4) .shortcut = scB .cursor = 1 .areanum = 7 END WITH WITH ss.toolinfo(line_tool) .name = "Line" .icon = CHR(5) .shortcut = scL .cursor = 2 .areanum = 8 END WITH WITH ss.toolinfo(fill_tool) .name = "Fill" .icon = "F" .shortcut = scF .cursor = 3 .areanum = 9 END WITH WITH ss.toolinfo(oval_tool) .name = "Oval" .icon = "O" .shortcut = scO .cursor = 2 .areanum = 11 END WITH WITH ss.toolinfo(airbrush_tool) .name = "Air" .icon = "A" .shortcut = scA .cursor = 3 .areanum = 21 END WITH WITH ss.toolinfo(mark_tool) .name = "Mark" .icon = "M" .shortcut = scM .cursor = 2 .areanum = 23 END WITH WITH ss.toolinfo(clone_tool) .name = "Clone" .icon = "C" .shortcut = scC .cursor = 3 .areanum = 24 END WITH WITH ss.toolinfo(replace_tool) .name = "Replace" .icon = "R" .shortcut = scR .cursor = 3 .areanum = 10 END WITH WITH ss.toolinfo(scroll_tool) .name = "Scroll" .icon = "S" .shortcut = scS .cursor = 2 .areanum = 22 END WITH END SUB ' Part of ss should be filled in with the necessary arguments. ' The modified ss.spriteset Frame ptr vector is passed back by calling ss.save_callback() ' before exiting, or whenever want to immediately save. The original .spriteset Frames might be modified in-place, ' or deleted and replaced. ' Also, this expects the caller to save the default palette (ss.pal_num). However, it saves the palette (ss.palette) itself. SUB sprite_editor(ss as SpriteEditState) sprite_editor_initialise ss hidemousecursor setkeys DO setwait 17, 70 setkeys IF ss.gotmouse THEN ss.mouse = readmouse ss.zonecursor = 0 ss.zonenum = mouseover(ss.mouse.x, ss.mouse.y, ss.zone.x, ss.zone.y, ss.zonecursor, ss.area()) END IF IF keyval(ccCancel) > 1 THEN IF ss.hold = YES THEN spriteedit_reset_tool(ss) ELSE spriteedit_clip ss spriteedit_reset_tool(ss) EXIT DO END IF END IF IF keyval(scF1) > 1 THEN show_help "sprite_editor" IF ss.delay = 0 THEN spriteedit_sprctrl(ss) END IF ss.delay = large(ss.delay - 1, 0) spriteedit_display ss SWAP vpage, dpage setvispage vpage 'blank the sprite area rectangle ss.previewpos.x, ss.previewpos.y, ss.wide, ss.high, 0, dpage ss.tick = 0 IF dowait THEN ss.tick = 1 ss.tog = ss.tog XOR 1 END IF LOOP showmousecursor 'Save the sprite before leaving spriteedit_clip ss ss.save_callback(ss.spriteset, ss.save_callback_context, ss.pal_num) palette16_save ss.palette, ss.pal_num sprite_editor_cleanup ss END SUB 'Destructs SpriteEditState, cleaning up after both sprite_editor_initialise() and SpriteSetBrowser.setup_editstate() SUB sprite_editor_cleanup(byref ss as SpriteEditState) v_free ss.spriteset ss.sprite = NULL 'Element of ss.spriteet. Not a reference palette16_unload @ss.palette FOR i as integer = 0 TO UBOUND(ss.undo) v_free ss.undo(i).history NEXT WITH ss_save .cursor = XY(ss.x, ss.y) .tool = ss.tool .airsize = ss.airsize .mist = ss.mist .palindex = ss.palindex .hidemouse = ss.hidemouse END WITH END SUB SUB spriteedit_sprctrl(byref ss as SpriteEditState) IF ss.mouse.buttons = 0 AND keyval(scSpace) = 0 THEN ss.lastpos.x = -1 ss.lastpos.y = -1 END IF IF keyval(scTilde) > 1 THEN ss.hidemouse = ss.hidemouse XOR YES ' Changing the index in the 16 color palette IF keyval(scComma) > 1 AND ss.palindex > 0 THEN ss.palindex -= 1 ss.showcolnum = COLORNUM_SHOW_TICKS END IF IF keyval(scPeriod) > 1 AND ss.palindex < 15 THEN ss.palindex += 1 ss.showcolnum = COLORNUM_SHOW_TICKS END IF IF ss.zonenum = 2 THEN IF ss.mouse.buttons > 0 THEN ss.palindex = small(ss.zone.x \ 4, 15) ss.showcolnum = COLORNUM_SHOW_TICKS END IF END IF ' Changing to a different 16 color palette IF keyval(scLeftBrace) > 1 OR (ss.zonenum = 5 AND ss.mouse.release > 0) THEN ' Previous palette changepal ss, -1 END IF IF keyval(scRightBrace) > 1 OR (ss.zonenum = 6 AND ss.mouse.release > 0) THEN ' Next palette changepal ss, 1 END IF IF keyval(scP) > 1 OR (ss.zonenum = 19 AND ss.mouse.release > 0) THEN ' Call palette browser spriteedit_pal16_browser ss, ss.sprite END IF 'If the palette has changed, update genMaxPal gen(genMaxPal) = large(gen(genMaxPal), ss.pal_num) '--UNDO IF (keyval(scCtrl) > 0 AND keyval(scZ) > 1) OR (ss.zonenum = 20 AND ss.mouse.clicks > 0) THEN readundospr ss '--REDO IF (keyval(scCtrl) > 0 AND keyval(scY) > 1) OR (ss.zonenum = 21 AND ss.mouse.clicks > 0) THEN readredospr ss '--COPY (CTRL+INS,SHIFT+DEL,CTRL+C) IF copy_keychord() THEN frame_assign @ss_save.spriteclip, frame_duplicate(ss.sprite) END IF '--PASTE (SHIFT+INS,CTRL+V) IF paste_keychord() AND ss_save.spriteclip <> NULL THEN writeundospr ss spriteedit_clip ss frame_draw ss_save.spriteclip, NULL, 0, 0, NO, ss.sprite END IF '--TRANSPARENT PASTE (CTRL+T) IF (keyval(scCtrl) > 0 AND keyval(scT) > 1) AND ss_save.spriteclip <> NULL THEN writeundospr ss spriteedit_clip ss frame_draw ss_save.spriteclip, NULL, 0, 0, YES, ss.sprite END IF '--COPY PALETTE (ALT+C) IF keyval(scAlt) > 0 AND keyval(scC) > 1 THEN palette16_unload @ss_save.pal_clipboard ss_save.pal_clipboard = palette16_duplicate(ss.palette) END IF '--PASTE PALETTE (ALT+V) IF keyval(scAlt) > 0 AND keyval(scV) > 1 THEN IF ss_save.pal_clipboard THEN palette16_unload @ss.palette ss.palette = palette16_duplicate(ss_save.pal_clipboard) END IF END IF ' Change master palette index for the selected palette color ss.curcolor = ss.palette->col(ss.palindex) IF keyval(scAlt) > 0 THEN IF keyval(ccUp) > 1 AND ss.curcolor > 15 THEN ss.curcolor -= 16 : ss.showcolnum = COLORNUM_SHOW_TICKS IF keyval(ccDown) > 1 AND ss.curcolor < 240 THEN ss.curcolor += 16 : ss.showcolnum = COLORNUM_SHOW_TICKS IF keyval(ccLeft) > 1 AND ss.curcolor > 0 THEN ss.curcolor -= 1 : ss.showcolnum = COLORNUM_SHOW_TICKS IF keyval(ccRight) > 1 AND ss.curcolor < 255 THEN ss.curcolor += 1 : ss.showcolnum = COLORNUM_SHOW_TICKS END IF IF ss.mouse.buttons > 0 ANDALSO ss.zonenum = 3 THEN ss.curcolor = ((ss.zone.y \ 6) * 16) + (ss.zone.x \ 4) ss.showcolnum = COLORNUM_SHOW_TICKS END IF ss.palette->col(ss.palindex) = ss.curcolor ' Transparent onionskin IF keyval(scT) > 1 THEN IF keyval(scShift) > 0 ORELSE keyval(scCtrl) > 0 THEN ss.onion_enabled = YES ss.onionnum = ss.framenum ELSE IF ss.onionnum = -1 THEN ss.onionnum = ss.framenum ss.onion_enabled = NOT ss.onion_enabled END IF END IF IF keyval(scCtrl) > 0 THEN DIM as integer lastframe = v_len(ss.spriteset) - 1, framenum = ss.framenum, change_id = 0 'slowkey to animate the frame at the typical speed if you hold it down IF slowkey(ccLeft, 110) THEN loopvar framenum, 0, lastframe, -1 IF slowkey(ccRight, 110) THEN loopvar framenum, 0, lastframe, 1 IF slowkey(ccUp, 110) THEN change_id = -1 IF slowkey(ccDown, 110) THEN change_id = 1 IF change_id THEN 'Loop within frame group framenum = spriteedit_loop_frameid(ss, change_id) END IF IF framenum <> ss.framenum THEN spriteedit_change_frame ss, framenum END IF END IF ' Change Shift-move speed IF keyval(scShift) > 0 THEN IF keyval(scF) > 1 THEN ss.fastmovestep += 1 IF keyval(scS) > 1 THEN ss.fastmovestep = large(ss.fastmovestep - 1, 2) END IF ' Change brush position IF keyval(scAlt) = 0 ANDALSO keyval(scCtrl) = 0 THEN DIM fixmouse as bool = NO WITH ss fixmouse = NO DIM stepsize as integer = IIF(keyval(scShift) > 0, .fastmovestep, 1) IF slowkey(ccUp, 100) THEN .y -= stepsize: fixmouse = YES IF slowkey(ccDown, 100) THEN .y += stepsize: fixmouse = YES IF slowkey(ccLeft, 100) THEN .x -= stepsize: fixmouse = YES IF slowkey(ccRight, 100) THEN .x += stepsize: fixmouse = YES .x = bound(.x, 0, .wide - 1) .y = bound(.y, 0, .high - 1) END WITH IF fixmouse THEN IF ss.zonenum = 1 THEN ss.zone.x = ss.x * ss.zoom + (ss.zoom \ 2) ss.zone.y = ss.y * ss.zoom + (ss.zoom \ 2) ss.mouse.x = ss.area(0).x + ss.zone.x ss.mouse.y = ss.area(0).y + ss.zone.y movemouse ss.mouse.x, ss.mouse.y END IF IF ss.zonenum = 14 THEN ss.zone.x = ss.x ss.zone.y = ss.y ss.mouse.x = ss.area(13).x + ss.zone.x ss.mouse.y = ss.area(13).y + ss.zone.y movemouse ss.mouse.x, ss.mouse.y END IF END IF END IF ' Mouse over main sprite view IF ss.zonenum = 1 THEN ss.x = ss.zone.x \ ss.zoom ss.y = ss.zone.y \ ss.zoom END IF IF keyval(scAlt) = 0 AND keyval(scShift) = 0 THEN ' Select palette colour by typing in a number with 0-9 keys (numpad not supported) FOR idx as integer = 1 TO 10 IF keyval(sc1 + idx - 1) > 1 THEN DIM digit as integer = IIF(idx = 10, 0, idx) IF TIMER < ss.number_typing_deadline THEN ss.palindex = small(ss.palindex * 10 + digit, 15) ELSE ss.palindex = digit END IF ' Show the colour index for exactly how long the user has to type in a 2-digit palette index ss.showcolnum = 30 ' equal to COLORNUM_SHOW_TICKS anyway ss.number_typing_deadline = TIMER + ss.showcolnum / 60 END IF NEXT idx END IF IF ss.tool = airbrush_tool THEN '--adjust airbrush IF ss.mouse.buttons AND mouseLeft THEN IF ss.zonenum = 15 THEN ss.airsize = large(ss.airsize - ss.tick, 1) IF ss.zonenum = 17 THEN ss.airsize = small(ss.airsize + ss.tick, 80) IF ss.zonenum = 16 THEN ss.mist = large(ss.mist - ss.tick, 1) IF ss.zonenum = 18 THEN ss.mist = small(ss.mist + ss.tick, 99) END IF IF keyval(scMinus) > 1 OR keyval(scNumpadMinus) > 1 THEN IF keyval(scCtrl) > 0 THEN ss.mist = large(ss.mist - 1, 1) ELSE ss.airsize = large(ss.airsize - 1, 1) END IF END IF IF keyval(scPlus) > 1 OR keyval(scNumpadPlus) > 1 THEN IF keyval(scCtrl) > 0 THEN ss.mist = small(ss.mist + 1, 99) ELSE ss.airsize = small(ss.airsize + 1, 80) END IF END IF END IF IF ss.tool = clone_tool THEN '--When clone tool is active, rotate the clone buffer IF ss.mouse.buttons AND mouseLeft THEN IF ss_save.clone_brush THEN IF ss.zonenum = 16 THEN frame_assign @ss_save.clone_brush, frame_rotated_90(ss_save.clone_brush) 'anticlockwise ss.delay = 20 END IF IF ss.zonenum = 18 THEN frame_assign @ss_save.clone_brush, frame_rotated_270(ss_save.clone_brush) 'clockwise ss.delay = 20 END IF END IF END IF ELSEIF ss.tool <> airbrush_tool THEN '--when other tools are active, rotate the whole buffer '--except for the airbrush tool because it's buttons collide. IF ss.mouse.buttons AND mouseLeft THEN IF ss.zonenum = 16 THEN spriteedit_edit ss, frame_rotated_90(ss.sprite) 'anticlockwise ss.delay = 20 END IF IF ss.zonenum = 18 THEN spriteedit_edit ss, frame_rotated_270(ss.sprite) 'clockwise ss.delay = 20 END IF END IF END IF ' Mouse over thumbnail view IF ss.zonenum = 14 THEN ss.x = ss.zone.x ss.y = ss.zone.y END IF DIM usetool as bool DIM alternate as bool = (keyval(scShift) OR keyval(scCtrl)) > 0 IF keyval(scSpace) > 0 THEN usetool = YES ' Mouse over canvas or thumbnail IF ss.zonenum = 1 OR ss.zonenum = 14 THEN IF ss.mouse.buttons AND mouseLeft THEN usetool = YES IF ss.tool = mark_tool ANDALSO (ss.mouse.buttons AND mouseRight) THEN 'Right click to cut usetool = YES alternate = YES END IF END IF IF usetool THEN SELECT CASE ss.tool CASE draw_tool spriteedit_put_dot(ss) CASE box_tool IF ss.mouse.clicks > 0 OR keyval(scSpace) > 1 THEN IF ss.hold THEN ss.hold = NO: spriteedit_draw_square(ss) ELSE ss.hold = YES ss.holdpos.x = ss.x ss.holdpos.y = ss.y END IF END IF CASE line_tool IF ss.mouse.clicks > 0 OR keyval(scSpace) > 1 THEN IF ss.hold = YES THEN ss.hold = NO spriteedit_strait_line(ss) ELSE ss.hold = YES ss.holdpos.x = ss.x ss.holdpos.y = ss.y END IF END IF CASE fill_tool IF ss.mouse.clicks > 0 OR keyval(scSpace) > 1 THEN spriteedit_flood_fill(ss) END IF CASE replace_tool IF ss.mouse.clicks > 0 OR keyval(scSpace) > 1 THEN spriteedit_replace_col(ss) END IF CASE oval_tool IF ss.mouse.clicks > 0 OR keyval(scSpace) > 1 THEN IF ss.hold = NO THEN '--start oval ss.holdpos.x = ss.x ss.holdpos.y = ss.y ss.ellip_angle = 0.0 ss.ellip_minoraxis = 0.0 ss.radius = 0.0 ss.hold = YES ELSE spriteedit_draw_oval(ss) ss.hold = NO END IF END IF CASE airbrush_tool spriteedit_spray_spot(ss) CASE mark_tool 'alternate means cut IF ss.mouse.clicks > 0 OR keyval(scSpace) > 1 THEN IF ss.hold THEN ss.hold = NO DIM rect as RectType rect.xy = XY(small(ss.x, ss.holdpos.x), small(ss.y, ss.holdpos.y)) rect.wh = XY(ABS(ss.x - ss.holdpos.x) + 1, ABS(ss.y - ss.holdpos.y) + 1) frame_assign @ss_save.clone_brush, frame_resized(ss.sprite, rect.w, rect.h, -rect.x, -rect.y) IF alternate THEN writeundospr ss rectangle ss.sprite, rect, 0 END IF ss_save.clonepos.x = ss_save.clone_brush->w \ 2 ss_save.clonepos.y = ss_save.clone_brush->h \ 2 ss.tool = clone_tool ' auto-select the clone tool after marking ELSE ss.hold = YES ss.holdpos.x = ss.x ss.holdpos.y = ss.y END IF END IF CASE clone_tool IF ss.mouse.clicks > 0 OR keyval(scSpace) > 1 THEN IF ss_save.clone_brush THEN IF ss.lastpos.x = -1 AND ss.lastpos.y = -1 THEN writeundospr ss END IF spriteedit_clip ss frame_draw ss_save.clone_brush, , ss.x - ss_save.clonepos.x, ss.y - ss_save.clonepos.y, , ss.sprite ss.lastpos.x = ss.x ss.lastpos.y = ss.y ELSE ss.tool = mark_tool ' select selection tool if clone is not available ss.hold = YES ss.holdpos.x = ss.x ss.holdpos.y = ss.y END IF END IF END SELECT END IF IF ss.hold = YES AND ss.tool = oval_tool THEN ss.radius = SQR((ss.x - ss.holdpos.x)^2 + (ss.y - ss.holdpos.y)^2) IF ss.zonenum = 1 THEN 'Use mouse pointer instead of draw cursor for finer grain control of radius ss.radius = SQR( (ss.holdpos.x + 0.5 - ss.zone.x / ss.zoom)^2 + (ss.holdpos.y + 0.5 - ss.zone.y / ss.zoom)^2 ) END IF END IF FOR i as integer = 0 TO UBOUND(ss.toolinfo) 'Check tool selection (clicking icon or shortcut key) 'Alt is used for alt+c and alt+v IF (ss.mouse.clicks > 0 AND ss.zonenum = ss.toolinfo(i).areanum + 1) OR _ (keyval(scAlt) = 0 AND keyval(scCtrl) = 0 AND keyval(scShift) = 0 AND _ keyval(ss.toolinfo(i).shortcut) > 1) THEN IF ss.tool <> i THEN ss.didscroll = NO ss.tool = i spriteedit_reset_tool(ss) ss.drawcursor = ss.toolinfo(i).cursor + 1 END IF NEXT i IF ss.tool <> clone_tool AND ss.tool <> airbrush_tool THEN IF keyval(scPlus) > 1 OR keyval(scNumpadPlus) > 1 THEN spriteedit_edit ss, frame_rotated_270(ss.sprite) 'clockwise END IF IF keyval(scMinus) > 1 OR keyval(scNumpadMinus) > 1 THEN spriteedit_edit ss, frame_rotated_90(ss.sprite) 'anticlockwise END IF END IF DIM normal_enter_rclick as bool = YES IF ss.tool = mark_tool THEN ' For mark tool, right click to cut. Handled above normal_enter_rclick = NO ELSEIF ss.tool = clone_tool THEN ' For clone brush tool, enter/right-click moves the handle point normal_enter_rclick = NO IF ss.readjust THEN IF keyval(scEnter) = 0 AND ss.mouse.buttons = 0 THEN ' click or key release ss.readjust = NO ss_save.clonepos.x += (ss.x - ss.adjustpos.x) ss_save.clonepos.y += (ss.y - ss.adjustpos.y) ss.adjustpos.x = 0 ss.adjustpos.y = 0 END IF ELSE IF (keyval(scEnter) AND 5) OR ss.mouse.buttons = mouseRight THEN ss.readjust = YES ss.adjustpos.x = ss.x ss.adjustpos.y = ss.y END IF END IF ' clone buffer rotation IF ss_save.clone_brush THEN IF keyval(scPlus) > 1 OR keyval(scNumpadPlus) > 1 THEN frame_assign @ss_save.clone_brush, frame_rotated_270(ss_save.clone_brush) 'clockwise END IF IF keyval(scMinus) > 1 OR keyval(scNumpadMinus) > 1 THEN frame_assign @ss_save.clone_brush, frame_rotated_90(ss_save.clone_brush) 'anticlockwise END IF END IF END IF ' For all other tools, pick a color IF keyval(scG) > 1 ORELSE normal_enter_rclick ANDALSO (keyval(scEnter) > 1 ORELSE (ss.zonenum = 1 AND ss.mouse.buttons = mouseRight)) THEN ss.palindex = readpixel(ss.sprite, ss.x, ss.y) ss.showcolnum = COLORNUM_SHOW_TICKS ' If the tool is a non-drawing tool, switch back to draw IF ss.tool = scroll_tool OR ss.tool = mark_tool OR ss.tool = clone_tool THEN ss.tool = draw_tool spriteedit_reset_tool ss END IF END IF IF keyval(scBackspace) > 1 OR (ss.zonenum = 4 AND ss.mouse.clicks > 0) THEN writeundospr ss frame_flip_horiz ss.sprite END IF IF ss.tool = scroll_tool AND (ss.zonenum = 1 OR ss.zonenum = 14) THEN 'Handle scrolling by dragging the mouse 'Did this drag start inside the sprite box? If not, ignore IF ss.mouse.dragging ANDALSO mouseover(ss.mouse.clickstart.x, ss.mouse.clickstart.y, 0, 0, 0, ss.area()) = ss.zonenum THEN spriteedit_scroll ss, ss.x - ss.lastcpos.x, ss.y - ss.lastcpos.y END IF END IF IF ss.tool = scroll_tool AND keyval(scAlt) = 0 AND keyval(scCtrl) = 0 THEN DIM scrolloff as XYPair DIM stepsize as integer = IIF(keyval(scShift) > 0, ss.fastmovestep, 1) IF slowkey(ccUp, 100) THEN scrolloff.y -= stepsize IF slowkey(ccDown, 100) THEN scrolloff.y += stepsize IF slowkey(ccLeft, 100) THEN scrolloff.x -= stepsize IF slowkey(ccRight, 100) THEN scrolloff.x += stepsize spriteedit_scroll ss, scrolloff.x, scrolloff.y END IF IF keyval(scI) > 1 OR (ss.zonenum = 13 AND (ss.mouse.release AND mouseLeft)) THEN DIM imported as Frame ptr = spriteedit_import16(ss) IF imported THEN spriteedit_replace_frame ss, imported END IF IF keyval(scE) > 1 OR (ss.zonenum = 26 AND (ss.mouse.release AND mouseLeft)) THEN palette16_save ss.palette, ss.pal_num 'Save palette in case it has changed spriteedit_export ss.default_export_filename, ss.sprite, ss.palette END IF ss.lastcpos = XY(ss.x, ss.y) END SUB SUB spriteedit_flood_fill(byref ss as SpriteEditState) writeundospr ss spriteedit_clip ss paintat ss.sprite, ss.x, ss.y, ss.palindex END SUB SUB spriteedit_replace_col(byref ss as SpriteEditState) writeundospr ss spriteedit_clip ss replacecolor ss.sprite, readpixel(ss.sprite, ss.x, ss.y), ss.palindex END SUB SUB spriteedit_spray_spot(byref ss as SpriteEditState) IF ss.lastpos.x = -1 AND ss.lastpos.y = -1 THEN writeundospr ss spriteedit_clip ss airbrush ss.sprite, ss.x, ss.y, ss.airsize, ss.mist, ss.palindex ss.lastpos.x = ss.x ss.lastpos.y = ss.y END SUB SUB spriteedit_put_dot(byref ss as SpriteEditState) IF ss.lastpos.x = -1 AND ss.lastpos.y = -1 THEN writeundospr ss spriteedit_clip ss putpixel ss.sprite, ss.x, ss.y, ss.palindex ELSE drawline ss.sprite, ss.x, ss.y, ss.lastpos.x, ss.lastpos.y, ss.palindex END IF ss.lastpos.x = ss.x ss.lastpos.y = ss.y END SUB SUB spriteedit_draw_oval(byref ss as SpriteEditState) writeundospr ss spriteedit_clip ss ellipse ss.sprite, ss.holdpos.x, ss.holdpos.y, ss.radius, ss.palindex, , ss.ellip_minoraxis, ss.ellip_angle END SUB SUB spriteedit_draw_square(byref ss as SpriteEditState) writeundospr ss spriteedit_clip ss rectangle ss.sprite, small(ss.x, ss.holdpos.x), small(ss.y, ss.holdpos.y), ABS(ss.x - ss.holdpos.x) + 1, ABS(ss.y - ss.holdpos.y) + 1, ss.palindex END SUB SUB spriteedit_strait_line(byref ss as SpriteEditState) writeundospr ss spriteedit_clip ss drawline ss.sprite, ss.x, ss.y, ss.holdpos.x, ss.holdpos.y, ss.palindex END SUB SUB spriteedit_reset_tool(byref ss as SpriteEditState) ss.hold = NO ss.readjust = NO ss.adjustpos.x = 0 ss.adjustpos.y = 0 END SUB SUB spriteedit_scroll (ss as SpriteEditState, byval shiftx as integer, byval shifty as integer) 'Save an undo before the first of a consecutive scrolls IF shiftx = 0 AND shifty = 0 THEN EXIT SUB IF ss.didscroll = NO THEN writeundospr ss ss.didscroll = YES spriteedit_replace_frame ss, frame_resized(ss.sprite, ss.wide, ss.high, shiftx, shifty) END SUB '========================================================================================== ' Spriteset Browser '========================================================================================== CONST MINSIZE = 1 'Must be at least 1 CONST MAXSIZE = 512 '===================================================================== ' Add New Spriteset prompt TYPE AddNewSpritesetMenu EXTENDS ModularMenu framesize as XYPair confirmed as bool 'Confirmed instead of cancelled DECLARE SUB update () DECLARE FUNCTION each_tick () as bool END TYPE SUB AddNewSpritesetMenu.update () REDIM menu(3) state.last = UBOUND(menu) menu(0) = "[Done]" menu(1) = "Width: " & framesize.w menu(2) = "Height: " & framesize.h menu(3) = "[Cancel]" END SUB FUNCTION AddNewSpritesetMenu.each_tick () as bool IF state.pt <> 1 THEN IF framesize.w < MINSIZE THEN framesize.w = MINSIZE state.need_update = YES END IF END IF IF state.pt <> 2 THEN IF framesize.h < MINSIZE THEN framesize.h = MINSIZE state.need_update = YES END IF END IF IF enter_space_click(state) THEN IF state.pt = 0 THEN confirmed = YES RETURN YES 'Confirmed ELSEIF state.pt = 3 THEN RETURN YES 'Cancel END IF IF state.pt < 3 ANDALSO enter_or_space() THEN 'Enter or space should confirm on the numbers, even though click should not confirmed = YES RETURN YES 'Confirmed END IF END IF IF state.pt = 1 THEN state.need_update OR= intgrabber(framesize.w, 0, MAXSIZE) ELSEIF state.pt = 2 THEN state.need_update OR= intgrabber(framesize.h, 0, MAXSIZE) END IF END FUNCTION 'framesize should be passed in with its default size 'Returns true if didn't cancel FUNCTION input_new_spriteset_info (byref framesize as XYPair) as bool DIM menu as AddNewSpritesetMenu menu.floating = YES menu.menuopts.edged = YES menu.helpkey = "add_new_spriteset" menu.title = "Select desired size of each frame in the spriteset and press ENTER." menu.framesize = framesize menu.run() framesize = large(XY(MINSIZE, MINSIZE), menu.framesize) RETURN menu.confirmed END FUNCTION '===================================================================== ' Resize Spriteset screen TYPE ResizeSpritesetMenu EXTENDS ModularMenu original as Frame ptr resized as Frame ptr framesize as XYPair shift as XYPair root as Slice ptr pal as integer confirmed as bool 'Didn't cancel DECLARE DESTRUCTOR () DECLARE SUB update () DECLARE FUNCTION each_tick () as bool DECLARE SUB draw_underlays() END TYPE DESTRUCTOR ResizeSpritesetMenu DeleteSlice @root END DESTRUCTOR SUB ResizeSpritesetMenu.update () REDIM menu(5) state.last = UBOUND(menu) menu(0) = "[Cancel]" menu(1) = "Width: " & framesize.w menu(2) = "Height: " & framesize.h menu(3) = "Shift X: " & shift.x menu(4) = "Shift Y: " & shift.y menu(5) = "[Done]" frame_unload @resized resized = frame_resized(original, large(MINSIZE, framesize.w), large(MINSIZE, framesize.h), shift.x, shift.y) spriteset_resize_menu_rebuild root, resized, pal END SUB FUNCTION ResizeSpritesetMenu.each_tick () as bool IF keyval(scF6) > 1 THEN IF keyval(scCtrl) > 0 THEN slice_editor SL_COLLECT_EDITOR, finddatafile("spriteset_editor.slice"), YES state.need_update = YES ELSE slice_editor root, SL_COLLECT_EDITOR, , , YES END IF END IF IF state.pt <> 1 THEN IF framesize.w < MINSIZE THEN framesize.w = MINSIZE state.need_update = YES END IF END IF IF state.pt <> 2 THEN IF framesize.h < MINSIZE THEN framesize.h = MINSIZE state.need_update = YES END IF END IF IF enter_space_click(state) THEN IF state.pt = 5 THEN confirmed = YES RETURN YES 'Confirmed ELSEIF state.pt = 0 THEN RETURN YES 'Cancel END IF IF state.pt > 0 ANDALSO enter_or_space() THEN 'Enter or space should confirm on the numbers, even though click should not confirmed = YES RETURN YES 'Confirmed END IF END IF IF state.pt = 1 THEN state.need_update OR= intgrabber(framesize.w, 0, MAXSIZE) ELSEIF state.pt = 2 THEN state.need_update OR= intgrabber(framesize.h, 0, MAXSIZE) ELSEIF state.pt = 3 THEN state.need_update OR= intgrabber(shift.x, -framesize.w, framesize.w) ELSEIF state.pt = 4 THEN state.need_update OR= intgrabber(shift.y, -framesize.h, framesize.h) END IF END FUNCTION SUB ResizeSpritesetMenu.draw_underlays() DrawSlice root, vpage END SUB 'Returns resized sprite set, or NULL. Does not free or modify 'spriteset'. FUNCTION spriteset_resize_menu(sprtype as SpriteType, setnum as integer, pal as integer) as Frame ptr DIM menu as ResizeSpritesetMenu menu.menuopts.edged = YES menu.helpkey = "resize_spriteset" menu.title = "Resize each frame to:" menu.menuopts.wide = 90 menu.original = frame_load(sprtype, setnum) menu.framesize = menu.original->size menu.pal = pal menu.run() frame_unload @menu.original IF menu.confirmed = NO THEN frame_unload @menu.resized RETURN NULL END IF RETURN menu.resized END FUNCTION '========================================================================================== ' Spriteset Browser '========================================================================================== TYPE SpriteSetBrowser sprtype as SpriteType genmax as integer 'Index in gen() defpalettes(any) as integer palettes(any) as Palette16 ptr 'Copy/paste: STATIC copy_buffer as Frame ptr vector 'One or more copied frames STATIC copied_whole_set as bool 'Copied a spriteset rather than a single frame (ignore if nothing copied) STATIC copied_defpal as integer 'The default palette of the copied spriteset, or -1 for none STATIC copied_animations as AnimationSet ptr 'If copied_whole_set, a duplicate copy of the sprite's animations 'The following are only set inside and immediately after calling edit_frame() or import_any(); 'they're used for SpriteSetBrowser_save_callback* editing_spriteset as Frame ptr 'The whole spriteset (as a single spritesheet in fullset mode) editing_setnum as integer 'The following are for restoring the cursor after rebuild_menu, and saving it when exiting. STATIC remem_setnum(sprTypeLastPickable) as integer STATIC remem_framenum(sprTypeLastPickable) as integer root as Slice ptr hover as Slice ptr 'Slice hovering over highlight_ss_id as bool 'Highlight the spriteset number, while typing ps as PlankState need_update as bool 'update_info() will be called DECLARE SUB build_menu() DECLARE SUB rebuild_menu() DECLARE SUB delete_menu_items() DECLARE SUB update_info() DECLARE SUB set_focus(setnum as integer, framenum as integer) DECLARE SUB replace_spriteset(setnum as integer, byref ss as Frame ptr = NULL) DECLARE SUB run() DECLARE SUB setup_editstate(edstate as SpriteEditState, setnum as integer, framenum as integer, fullset as bool = NO) DECLARE SUB cleanup_editstate(edstate as SpriteEditState, fullset as bool = NO) DECLARE SUB add_spriteset() DECLARE SUB edit_any(setnum as integer, framenum as integer) DECLARE SUB edit_spriteset(setnum as integer) DECLARE SUB add_frame(setnum as integer, new_group as bool = NO, after_framenum as integer = 0) DECLARE SUB delete_frame(setnum as integer, framenum as integer) DECLARE SUB export_any() DECLARE SUB import_any() DECLARE SUB copy_any() DECLARE SUB paste_any(transparent as bool) DECLARE SUB change_def_pal(diff as integer) DECLARE FUNCTION cur_setnum() as integer DECLARE FUNCTION cur_frameid() as integer DECLARE FUNCTION cur_framenum() as integer END TYPE 'Static members DIM SpriteSetBrowser.copy_buffer as Frame ptr vector DIM SpriteSetBrowser.copied_whole_set as bool DIM SpriteSetBrowser.copied_defpal as integer = -1 DIM SpriteSetBrowser.copied_animations as AnimationSet ptr DIM SpriteSetBrowser.remem_setnum(sprTypeLastPickable) as integer DIM SpriteSetBrowser.remem_framenum(sprTypeLastPickable) as integer TYPE SpriteSetEditor sprtype as SpriteType setnum as integer ss as SpriteSet ptr previews_root as Slice ptr preview_slices(any) as Slice ptr preview_anims(any) as Animation ptr '(references) Keep these separately since sl.AnimState->anim gets nulled overridden_animations(any) as string 'Names of global animations overridden by a local one pal as Palette16 ptr tog as integer context as AnimationContext DECLARE CONSTRUCTOR(sprtype as SpriteType, setnum as integer) DECLARE SUB display() DECLARE SUB run() DECLARE SUB create_preview(anim as Animation ptr, rowidx as integer, colidx as integer) DECLARE SUB update_previews() DECLARE SUB delete_previews() DECLARE SUB export_menu() END TYPE DECLARE SUB spriteset_detail_editor(sprtype as SpriteType, setnum as integer) SUB spriteset_editor(sprtype as SpriteType) 'First time we enter the browser can be very slow without precaching cache_all_spritesets sprtype DIM editor as SpriteSetBrowser editor.sprtype = sprtype editor.genmax = sprite_sizes(sprtype).genmax editor.run() END SUB FUNCTION SpriteSetBrowser.cur_setnum() as integer IF ps.cur = NULL THEN RETURN -1 RETURN ps.cur->Extra(0) END FUNCTION FUNCTION SpriteSetBrowser.cur_framenum() as integer IF ps.cur = NULL THEN RETURN -1 RETURN ps.cur->Extra(1) END FUNCTION FUNCTION SpriteSetBrowser.cur_frameid() as integer IF ps.cur = NULL THEN RETURN -1 RETURN ps.cur->Extra(2) END FUNCTION FUNCTION frame_name(sprtype as SpriteType, frameid as integer) as string DIM name as string SELECT CASE sprtype CASE sprTypeHero SELECT CASE frameid CASE 0: name = "Standing" CASE 1: name = "Stepping" CASE 100: name = "Attack A" CASE 101: name = "Attack B" CASE 200: name = "Casting" CASE 300: name = "Hurt" CASE 400: name = "Weak" CASE 500: name = "Dead" END SELECT CASE sprTypeWalkabout SELECT CASE frameid \ 100 CASE 0: name = "Up " CASE 1: name = "Right " CASE 2: name = "Down " CASE 3: name = "Left " END SELECT IF LEN(name) THEN name &= frameid MOD 100 END IF CASE sprTypeWeapon SELECT CASE frameid CASE 0: name = "A" CASE 1: name = "B" END SELECT CASE sprTypeBoxBorder SELECT CASE frameid CASE 0: name = "Top left corner" CASE 1: name = "Top edge left end" CASE 2: name = "Top edge repeat" CASE 3: name = "Top edge right end" CASE 4: name = "Top right corner" CASE 5: name = "Left edge top end" CASE 6: name = "Right edge top end" CASE 7: name = "Left edge repeat" CASE 8: name = "Right edge repeat" CASE 9: name = "Left edge bottom end" CASE 10: name = "Right edge bottom end" CASE 11: name = "Bottom left corner" CASE 12: name = "Bottom edge left end" CASE 13: name = "Bottom edge repeat" CASE 14: name = "Bottom edge right end" CASE 15: name = "Bottom right corner" END SELECT END SELECT RETURN name END FUNCTION SUB SpriteSetBrowser_set_plank_state_callback(sl as Slice Ptr, state as PlankItemState) SELECT CASE sl->SliceType CASE slText: set_plank_state_default_callback sl, state CASE slRectangle: 'Change the bgcol SELECT CASE state CASE plankNORMAL: ChangeRectangleSlice sl, , , , borderNone, transHollow CASE plankSEL: ChangeRectangleSlice sl, , , uiSelectedItem2 * -1 - 1, borderLine, transOpaque CASE plankDISABLE: ChangeRectangleSlice sl, , , , borderNone, transHollow CASE plankSELDISABLE: ChangeRectangleSlice sl, , , uiSelectedDisabled2 * -1 - 1, borderLine, transOpaque CASE plankMOUSEHOVER: ChangeRectangleSlice sl, , , uiMouseHoverItem * -1 - 1, borderLine, transHollow END SELECT END SELECT END SUB 'Called once. SUB SpriteSetBrowser.build_menu() DIM starttime as double = TIMER ' Load palettes REDIM defpalettes(gen(genmax)) loaddefaultpals sprtype, defpalettes() ' Build slice collection DeleteSlice @root 'When using Ctrl-F6 root = NewSliceOfType(slContainer) SliceLoadFromFile root, finddatafile("spriteset_browser.slice") '? "build_menu() in " & (TIMER - starttime) rebuild_menu() END SUB SUB SpriteSetBrowser.delete_menu_items() hover = NULL ps.cur = NULL 'Delete the old spriteset slices, if any DIM as Slice ptr nextsl, sl = edsl(ssed_list, root)->FirstChild WHILE sl nextsl = sl->NextSibling IF sl->Lookup = SL_EDITOR_SSED_SET THEN DeleteSlice @sl sl = nextsl WEND END SUB 'Deletes and recreates just the spriteset slices. 'Must be called every time a spriteset changes in a way that requires slices to be recreated, 'which includes changes in size or number of frames, frame IDs, or default palette, 'but not frame contents. SUB SpriteSetBrowser.rebuild_menu() DIM starttime as double = TIMER delete_menu_items() DIM ss_templ as Slice ptr = edsl(ssed_set_templ, root) 'Template for a spriteset 'Load all spritesets (maybe in future add some kind of delay-load bit to SpriteSlice 'for off-screen slices to speed this up?) FOR setnum as integer = 0 TO gen(genmax) DIM as Slice ptr ss_sl, fr_templ, fr_sl, separator_templ, sep_sl ' Load as a Frame, to get info DIM sprset as Frame ptr sprset = frame_load(sprtype, setnum) IF sprset = NULL THEN showerror "rebuild(): frame_load failed" : EXIT SUB 'Check for inconsistent default palette values stored in .rgfx and defpal#.bin '(bug introduced in r11542/r11543, fixed r11555) IF sprset->defpal > -1 ANDALSO sprset->defpal <> defpalettes(setnum) THEN sprset->defpal = defpalettes(setnum) visible_debug "Due to a bug in the Fufluns release candidate, spriteset " & setnum & _ " had an inconsistent default palette. It's been changed to palette " & sprset->defpal & _ !".\nCheck this is what you want." rgfx_save_spriteset sprset, sprtype, setnum END IF 'ss_sl = plank_menu_append( , ss_templ) 'ss_sl is not a plank ss_sl = CloneTemplate(ss_templ) ss_sl->Lookup = SL_EDITOR_SSED_SET ss_sl->Extra(0) = setnum 'The plank_holder is the plank to edit the spriteset '(Other plank_holders created by plank_menu_append) DIM ss_ed_plank as Slice ptr = LookupSlice(SL_PLANK_HOLDER, ss_sl) IF ss_ed_plank = NULL THEN EXIT SUB ss_ed_plank->Extra(0) = setnum ss_ed_plank->Extra(1) = -1 ss_ed_plank->Extra(2) = -1 'Can't have both SSED_SET_INFO and PLANK_SELECTABLE lookup codes set, sigh 'ChangeTextSlice edsl(ssed_set_info, ss_sl)->FirstChild, "Set " & setnum '& !"\nPal " & defpalettes(setnum) ChangeTextSlice edsl(ssed_set_info, ss_sl)->LastChild, !"Set\n" & setnum 'Would just use a layout slice set either to Fill or Cover Children, they aren't implemented for Layout slices yet DIM fr_holder as Slice ptr = edsl(ssed_frame_holder, ss_sl) IF fr_holder = 0 THEN EXIT SUB DIM nextx as integer = 0 'X pos of the next child of fr_holder CONST padding = 1 'Add the frames, and add a separator between frame groups, including empty groups fr_templ = edsl(ssed_frame_templ, ss_sl) separator_templ = edsl(ssed_frame_separator_templ, ss_sl) DIM lastgroup as integer = -1 FOR framenum as integer = 0 TO sprset->arraylen - 1 DIM frameid as integer = sprset[framenum].frameid DIM group as integer = frameid \ 100 WHILE group > lastgroup IF separator_templ THEN sep_sl = CloneTemplate(separator_templ) sep_sl->X = nextx nextx += sep_sl->Width + padding END IF lastgroup += 1 WEND fr_sl = plank_menu_append( , fr_templ) fr_sl->Extra(0) = setnum fr_sl->Extra(1) = framenum fr_sl->Extra(2) = frameid DIM spr_sl as Slice ptr = edsl(ssed_frame_sprite, fr_sl) ChangeSpriteSlice spr_sl, sprtype, setnum, , framenum fr_sl->X = nextx nextx += sprset->w /'fr_sl->Width'/ + padding 'Remember previous cursor position, or nearest match 'IF setnum = editing_setnum AND frameid <= editing_frame->frameid THEN ps.cur = fr_sl NEXT fr_holder->Width = nextx 'Includes '+padding' at end fr_holder->Height = sprset->h + 2 frame_unload @sprset NEXT setnum 'Just refreshing doesn't properly position everything, 'have to call DrawSlice. Probably due to CoverChildren. '(update_plank_scrolling also refreshes the tree, without which 'the positions will be wrong!) DrawSlice root, vpage ps.m = root set_focus(remem_setnum(sprtype), remem_framenum(sprtype)) IF ps.cur = NULL THEN 'FIXME: Have to then also refresh a second time, otherwise top_left_plank 'has the wrong screen positions and doesn't work... RefreshSliceTreeScreenPos root ps.cur = top_left_plank(ps) END IF update_plank_scrolling ps need_update = YES '? "rebuild_menu() in " & (TIMER - starttime) END SUB 'Called when the cursor moves to update info displays. Does not update the 'spriteset slices, which requires a rebuild_menu call. SUB SpriteSetBrowser.update_info() DIM info_text as Slice ptr = edsl(ssed_info_text, root) DIM tooltip_text as Slice ptr = edsl(ssed_tooltip_text, root) IF info_text = NULL ORELSE tooltip_text = NULL ORELSE ps.cur = NULL THEN EXIT SUB DIM as TextSliceData ptr info_text_dat = info_text->SliceData DIM caption_text as Slice ptr = edsl(ssed_caption_text, root) DIM pal_root as Slice ptr = edsl(ssed_palette_root, root) IF pal_root = NULL THEN EXIT SUB DIM info_str as string DIM caption_str as string DIM tooltip as string IF cur_setnum = -1 THEN 'Add new info_text_dat->show_insert = NO pal_root->Visible = NO ELSE tooltip = "Shift: move by set" IF cur_setnum >= 0 THEN IF cur_framenum = -1 THEN tooltip &= " Enter: spriteset menu" END IF IF sprite_sizes(sprtype).fixed_framecount = NO ANDALSO cur_framenum > -1 THEN tooltip &= " [Shift-]Insert: add frame" END IF END IF IF v_len(copy_buffer) THEN IF cur_framenum > -1 ANDALSO copied_whole_set THEN 'whole->one tooltip = "Ctrl-V: paste frame 0 Ctrl-T: transparent paste" ELSE tooltip = "Ctrl-V: paste Ctrl-T: transparent paste" END IF END IF info_str = "Spriteset " & cur_setnum 'Highlight spriteset num when typing info_text_dat->show_insert = highlight_ss_id IF highlight_ss_id THEN info_text_dat->insert = LEN(info_str) IF cur_setnum = 0 THEN info_text_dat->insert -= 1 'Highight the 0 to show you will overwrite it END IF IF cur_frameid < 0 THEN 'Whole spriteset selected rather than a frame 'caption_str = "Entire spriteset" ELSE caption_str = frame_name(sprtype, cur_frameid) info_str &= " Frame " & cur_framenum & " ID " & cur_frameid END IF 'DIM fr as Frame ptr = frame_load(sprtype, cur_setnum) 'Check for inconsistent .rgfx and defpal#.bin defpals ChangeTextSlice edsl(ssed_palette_text, root), "Def pal " & defpalettes(cur_setnum) '& "/" & fr->defpal 'frame_unload @fr 'Show the palette pal_root->Visible = YES DIM pal_sl as Slice ptr = edsl(ssed_palette_grid, root) IF pal_sl = NULL THEN EXIT SUB DIM pal as Palette16 ptr = palette16_load(defpalettes(cur_setnum)) IF pal = NULL THEN EXIT SUB DeleteSliceChildren pal_sl ChangeGridSlice pal_sl, 1, pal->numcolors FOR cidx as integer = 0 TO pal->numcolors - 1 DIM col_sl as Slice ptr col_sl = NewSliceOfType(slRectangle, pal_sl) col_sl->Fill = YES ChangeRectangleSlice col_sl, , pal->col(cidx), , borderNone NEXT cidx palette16_unload @pal END IF ChangeTextSlice info_text, info_str ChangeTextSlice caption_text, caption_str ChangeTextSlice tooltip_text, tooltip 'TODO: This is here to update the positioning of the palette box, 'and can be removed when CoverChildren is fixed to compute the size 'of a slice before it's positioned RefreshSliceTreeScreenPos root END SUB 'Move the cursor to a frame or a spriteset (does nothing if not found) SUB SpriteSetBrowser.set_focus(setnum as integer, framenum as integer) 'Needed because focus_plank_by_extra_id returns false if didn't move IF cur_setnum = setnum AND cur_framenum = framenum THEN EXIT SUB 'First, find the spriteset DIM as Slice ptr ss, list_sl = edsl(ssed_list, root) IF list_sl = NULL THEN EXIT SUB ss = list_sl->FirstChild DO IF ss = NULL THEN EXIT SUB IF ss->Extra(0) = setnum THEN EXIT DO ss = ss->NextSibling LOOP 'Whether framenum is -1 or a framenum, this will work IF focus_plank_by_extra_id(ps, 1, framenum, ss) = NO THEN 'That frame doesn't exist? Focus the spriteset then focus_plank_by_extra_id(ps, 1, -1, ss) END IF remem_setnum(sprtype) = setnum remem_framenum(sprtype) = framenum need_update = YES END SUB LOCAL FUNCTION create_spriteset(sprtype as SpriteType, framesize as XYPair) as Frame ptr DIM info() as FrameGroupInfo default_frame_group_info sprtype, info() DIM numframes as integer FOR idx as integer = 0 TO UBOUND(info) numframes += info(idx).default_num NEXT DIM newfr as Frame ptr newfr = frame_new(framesize.w, framesize.h, numframes, YES) DIM fridx as integer = 0 FOR groupidx as integer = 0 TO UBOUND(info) FOR idx as integer = 0 TO info(groupidx).default_num - 1 newfr[fridx].frameid = info(groupidx).frameid + idx fridx += 1 NEXT NEXT RETURN newfr END FUNCTION 'Append and save a new spriteset, using the FrameGroupInfo as a template SUB SpriteSetBrowser.add_spriteset() DIM framesize as XYPair WITH sprite_sizes(sprtype) framesize = .size 'Default size IF .fixed_size = NO THEN IF input_new_spriteset_info(framesize) = NO THEN EXIT SUB END IF END WITH DIM newss as Frame ptr = create_spriteset(sprtype, framesize) 'Avoid "suspicious rgfx_save_spriteset with no SpriteSet" warning spriteset_for_frame newss gen(genmax) += 1 'New default palette == 0 REDIM PRESERVE defpalettes(gen(genmax)) savedefaultpals sprtype, defpalettes(), UBOUND(defpalettes) rgfx_save_spriteset newss, sprtype, gen(genmax), 0 frame_unload @newss rebuild_menu() set_focus(gen(genmax), -1) END SUB 'Callback for sprite_editor SUB SpriteSetBrowser_save_callback(sprset as Frame ptr vector, context as any ptr, defpal as integer) DIM byref this as SpriteSetBrowser = *cast(SpriteSetBrowser ptr, context) 'DIM tt as double = TIMER 'Copy back into editing_spriteset, overwriting existing frame (trans=NO) '(Could call frame_vector_to_array + copy_spriteset_frameids/data instead, see other callback) BUG_IF(this.editing_spriteset->arraylen <> v_len(sprset), "length mismatch") FOR i as integer = 0 TO v_len(sprset) - 1 frame_draw sprset[i], NULL, 0, 0, NO, @this.editing_spriteset[i] NEXT 'Save default palettes immediately for live previewing this.defpalettes(this.editing_setnum) = defpal savedefaultpals this.sprtype, this.defpalettes(), UBOUND(this.defpalettes) this.editing_spriteset->defpal = defpal rgfx_save_spriteset this.editing_spriteset, this.sprtype, this.editing_setnum '? "saved in " & (TIMER - tt) END SUB 'Callback for sprite_editor, while editing a spriteset in fullset mode SUB SpriteSetBrowser_save_callback_fullset(sprset as Frame ptr vector, context as any ptr, defpal as integer) DIM byref this as SpriteSetBrowser = *cast(SpriteSetBrowser ptr, context) BUG_IF(v_len(sprset) <> 1, "fullset spriteset should have a single frame") DIM spr as Frame ptr = sprset[0] DIM split_ss as Frame ptr = spriteset_from_basic_spritesheet(spr, this.sprtype, this.editing_spriteset->arraylen) copy_spriteset_frameids split_ss, this.editing_spriteset copy_spriteset_data split_ss, this.editing_spriteset 'editing_spriteset doesn't need to be replaced, but may as well keep the new rather than old spriteset frame_unload @this.editing_spriteset this.editing_spriteset = split_ss 'Save default palettes immediately for live previewing this.defpalettes(this.editing_setnum) = defpal savedefaultpals this.sprtype, this.defpalettes(), UBOUND(this.defpalettes) this.editing_spriteset->defpal = defpal spr->defpal = defpal rgfx_save_spriteset this.editing_spriteset, this.sprtype, this.editing_setnum END SUB 'Setup public members of SpriteEditState for a call to sprite_editor. 'cleanup_editstate must be called when finished. 'editing_spriteset should already be loaded. SUB SpriteSetBrowser.setup_editstate(edstate as SpriteEditState, setnum as integer, framenum as integer, fullset as bool = NO) 'Members used by SpriteSetBrowser_save_callback[_fullset] editing_setnum = setnum WITH edstate '.spriteset becomes a copy of editing_spriteset in a different form, without shared pixel data IF fullset THEN '...a spriteset with a single frame v_new .spriteset, 1 .spriteset[0] = spriteset_to_basic_spritesheet(editing_spriteset) .framenum = 0 ELSE .spriteset = frame_array_to_vector(editing_spriteset) .framenum = framenum END IF .true_numframes = editing_spriteset->arraylen .fileset = sprtype .fullset = fullset ' sprite_editor uses the callback to save the edited sprite. ' It also saves changes to palettes, but not the default palette selection (ss.pal_num) IF fullset THEN .save_callback = @SpriteSetBrowser_save_callback_fullset ELSE .save_callback = @SpriteSetBrowser_save_callback END IF .save_callback_context = @this .pal_num = defpalettes(setnum) .spriteset_num = setnum .default_export_filename = default_export_name(sprtype, setnum, framenum, fullset) END WITH END SUB SUB SpriteSetBrowser.cleanup_editstate(edstate as SpriteEditState, fullset as bool = NO) 'sprite_editor cleans up edstate.spriteset frame_unload @editing_spriteset END SUB SUB SpriteSetBrowser.edit_any(setnum as integer, framenum as integer) DIM fullset as bool = (framenum < 0) 'Whole spriteset? editing_spriteset = frame_load(sprtype, setnum) DIM edstate as SpriteEditState setup_editstate edstate, setnum, framenum, fullset sprite_editor edstate 'sprite_editor calls the save callback on quitting, which writes to rgfx, saves the default palette, 'and modifies (unless fullset=YES) editing_spriteset cleanup_editstate edstate, fullset IF fullset THEN 'Unlike editing a single frame, in which the cached Frame is modified in-place, 'when editing a whole spriteset we're replacing it replace_spriteset setnum 'Deletes menu items END IF 'All palettes might have been modified, but they would have been modified 'in-place. The Frame would also have been modified in-place (unless fullset), 'and is shared with the Sprite slices. 'However, we still need to rebuild the menu if the default palette changed, 'because that doesn't affect existing Sprite slices loaded with pal=-1 '(unless we force them to be reloaded with ChangeSpriteSlice) rebuild_menu() END SUB SUB SpriteSetBrowser.edit_spriteset(setnum as integer) DIM _choices(...) as string = { _ "Exit menu", _ "Draw full spritesheet", "Export spritesheet", _ "Import spritesheet", "Resize", "Add frame group" _ } REDIM choices() as string a_copy _choices(), choices() IF sprite_sizes(sprtype).fixed_framecount THEN a_pop choices() 'Remove Add frame group DIM choice as integer = popup_choice("", choices()) SELECT CASE choice CASE 0 CASE 1 edit_any setnum, -1 CASE 2 export_any CASE 3 import_any CASE 4 DIM resized as Frame ptr resized = spriteset_resize_menu(sprtype, setnum, defpalettes(setnum)) IF resized THEN replace_spriteset setnum, resized rebuild_menu END IF CASE 5 ' Add a frame group in the first empty slot add_frame(cur_setnum, YES) END SELECT END SUB 'Export current frame or spriteset SUB SpriteSetBrowser.export_any() DIM as integer setnum = cur_setnum, framenum = cur_framenum DIM pal as Palette16 ptr = palette16_load(defpalettes(setnum)) IF pal = NULL THEN EXIT SUB editing_spriteset = frame_load(sprtype, setnum) DIM fullset as bool = (framenum < 0) 'Whole spriteset? DIM def_filename as string = default_export_name(sprtype, setnum, framenum, fullset) IF fullset THEN spriteedit_export_spriteset def_filename, editing_spriteset, pal ELSE spriteedit_export def_filename, @editing_spriteset[framenum], pal END IF frame_unload @editing_spriteset palette16_unload @pal END SUB 'Import an image/palette over current frame or spriteset SUB SpriteSetBrowser.import_any() DIM as integer setnum = cur_setnum, framenum = cur_framenum DIM fullset as bool = (framenum < 0) 'Whole spriteset? editing_spriteset = frame_load(sprtype, setnum) DIM edstate as SpriteEditState setup_editstate edstate, setnum, framenum, fullset 'spriteedit_import16 is written to be called from within sprite_editor, so we 'need to initialise private members of edstate sprite_editor_initialise edstate 'TODO: This function needs a major update/rewrite to handle variable-framecount spritesets DIM imported as Frame ptr = spriteedit_import16(edstate) showmousecursor IF imported THEN 'If fullset, SpriteSetBrowser_save_callback_fullset will cut the spritesheet up again spriteedit_replace_frame edstate, imported edstate.save_callback(edstate.spriteset, edstate.save_callback_context, edstate.pal_num) palette16_save edstate.palette, edstate.pal_num END IF sprite_editor_cleanup edstate 'Matches sprite_editor_initialise cleanup_editstate edstate, fullset 'Delete editing_spriteset 'Even if importing a single frame, the default palette may have changed replace_spriteset setnum rebuild_menu() END SUB 'Renumber any following frames in the same framegroup so frameids are in sequence without dups, 'starting with ensuring frvec[framenum] = frameid. 'Assumes that this is called after deleting at most one frame! SUB fix_following_frameids(frvec as Frame ptr vector, framenum as integer, frameid as integer) 'DIM as integer framenum = 0, frameid = new_id + 1 WHILE framenum < v_len(frvec) 'If you put 101 frames in a framegroup, we increment all the frameids in the next group. 'So effectively groups can have unlimited frames. 'That's why we treat a gap of 2 as the end of the group. IF frvec[framenum]->frameid > frameid + 1 THEN EXIT WHILE frvec[framenum]->frameid = frameid frameid += 1 framenum += 1 WEND END SUB 'Delete a frame from a spriteset SUB SpriteSetBrowser.delete_frame(setnum as integer, framenum as integer) DIM ss as Frame ptr = frame_load(sprtype, setnum) IF ss->arraylen <= 1 THEN notification "Can't delete last frame!" frame_unload @ss EXIT SUB END IF DIM frameid as integer = ss[framenum].frameid DIM frvec as Frame ptr vector = frame_array_to_vector(ss) frame_unload @ss 'Frame ptr vectors autodelete the Frames v_delete_slice frvec, framenum, framenum + 1 fix_following_frameids(frvec, framenum, frameid) ss = frame_vector_to_array(frvec) v_free frvec replace_spriteset setnum, ss rebuild_menu END SUB 'Inserts a new blank frame. '-new_group = YES: a new frame group is added to the end '-new_group = NO: a frame is inserted at the end of framenum's frame group SUB SpriteSetBrowser.add_frame(setnum as integer, new_group as bool = NO, framenum as integer = 0) DIM ss as Frame ptr = frame_load(sprtype, setnum) DIM frvec as Frame ptr vector = frame_array_to_vector(ss) DIM new_id as integer DIM insertidx as integer IF new_group THEN 'Insert the first missing group after the current DIM group as integer = 0 IF framenum >= 0 THEN group = frvec[framenum]->frameid \ 100 WHILE frameid_to_frame(ss, 100 * group, YES) > -1 'Already exists group += 1 WEND new_id = 100 * group insertidx = 0 WHILE insertidx < v_len(frvec) ANDALSO frvec[insertidx]->frameid < new_id insertidx += 1 WEND ELSE new_id = frvec[framenum]->frameid + 1 insertidx = framenum + 1 END IF WITH *frvec[0] DIM fr as Frame ptr = frame_new(.w, .h, , YES, .mask <> NULL) fr->frameid = new_id v_insert frvec, insertidx, fr frame_unload @fr END WITH fix_following_frameids frvec, insertidx, new_id frame_unload @ss ss = frame_vector_to_array(frvec) v_free frvec replace_spriteset setnum, ss rebuild_menu set_focus setnum, insertidx END SUB 'If ss is given: Save ss, empty the cache, and free ss. 'Otherwise, just does cleanup needed when a spriteset needs to be reloaded. 'This is needed when a sprite set can't be modified in-place, eg because number of frames changed. 'Note: rebuild_menu() must be called afterwards! SUB SpriteSetBrowser.replace_spriteset(setnum as integer, byref ss as Frame ptr = NULL) IF ss THEN IF ss->sprset = NULL THEN 'The animations are missing, so copy them over. Happens whenever created a new 'Frame array: add/deleting frames, resizing frames, fullset editing, importing. DIM existing_sprite as Frame ptr = frame_load(sprtype, setnum) copy_spriteset_data ss, existing_sprite frame_unload @existing_sprite END IF rgfx_save_spriteset ss, sprtype, setnum, defpalettes(setnum) frame_unload @ss END IF delete_menu_items() 'Required in order to empty cache sprite_empty_cache sprtype, setnum END SUB 'Change the default palette of the current spriteset by 'diff' SUB SpriteSetBrowser.change_def_pal(diff as integer) IF cur_setnum < 0 THEN EXIT SUB defpalettes(cur_setnum) = bound(defpalettes(cur_setnum) + diff, 0, gen(genMaxPal)) savedefaultpals sprtype, defpalettes(), UBOUND(defpalettes) editing_spriteset = frame_load(sprtype, cur_setnum) editing_spriteset->defpal = defpalettes(cur_setnum) rgfx_save_spriteset editing_spriteset, sprtype, cur_setnum frame_unload @editing_spriteset rebuild_menu() END SUB 'Copy current frame or spriteset into copy_buffer. SUB SpriteSetBrowser.copy_any() IF cur_setnum < 0 THEN EXIT SUB v_new copy_buffer animset_unload @copied_animations editing_spriteset = frame_load(sprtype, cur_setnum) DIM fr as Frame ptr IF cur_framenum >= 0 THEN fr = frame_duplicate(@editing_spriteset[cur_framenum]) v_append copy_buffer, fr 'incremenets refc frame_unload @fr copied_defpal = -1 copied_whole_set = NO ELSE 'Whole set 'Make a copy of each frame (frame_duplicate only copies a single frame) FOR framenum as integer = 0 TO editing_spriteset->arraylen - 1 fr = frame_duplicate(@editing_spriteset[framenum]) v_append copy_buffer, fr frame_unload @fr NEXT copied_defpal = defpalettes(cur_setnum) copied_whole_set = YES 'Duplicate rather than reference animations to avoid confusing semantics IF editing_spriteset->sprset THEN copied_animations = editing_spriteset->sprset->get_animset->duplicate() 'Remove the fallback_set (global animations for this sprtype), for two 'reasons: sprite_empty_cache would throw an error as the global animations 'are in use, and you can paste the spriteset as a different sprite type 'with different fallback_set. copied_animations->unload_shared_animsets() END IF END IF frame_unload @editing_spriteset END SUB SUB paste_frame(src as Frame ptr, dest as Frame ptr, transparent as bool) IF transparent = NO THEN frame_clear dest DIM opts as DrawOptions opts.write_mask = YES frame_draw src, , 0, 0, YES, dest, opts END SUB 'Paste onto the current frame or spriteset SUB SpriteSetBrowser.paste_any(transparent as bool) IF v_len(copy_buffer) = 0 THEN EXIT SUB IF cur_setnum < 0 THEN EXIT SUB editing_spriteset = frame_load(sprtype, cur_setnum) DIM overwrote as bool = NO IF cur_framenum = -1 THEN 'Whole spriteset 'copy_buffer might be either a single frame or a whole spriteset. IF copied_whole_set ANDALSO transparent = NO THEN 'Overwrite the original spriteset completely WITH sprite_sizes(sprtype) '(Currently, fixed_size implies fixed_framecount) IF .fixed_framecount ORELSE .fixed_size THEN 'Instead of a simple copy create a new spriteset so that we have the 'correct number of frames, size, and frame IDs DIM size as XYPair = IIF(.fixed_size, .size, copy_buffer[0]->size) frame_assign @editing_spriteset, create_spriteset(sprtype, size) ELSE 'Overwrite (use frame IDs from copy_buffer) frame_assign @editing_spriteset, frame_vector_to_array(copy_buffer) overwrote = YES END IF END WITH 'Also copy over the default palette IF copied_defpal > -1 THEN editing_spriteset->defpal = copied_defpal defpalettes(cur_setnum) = copied_defpal savedefaultpals sprtype, defpalettes(), UBOUND(defpalettes) END IF IF copied_animations THEN DIM ss as SpriteSet ptr = spriteset_for_frame(editing_spriteset) animset_unload @ss->animset ss->animset = copied_animations->duplicate() 'ss->animset is now missing its fallback_set of global animations, 'because copied_animations doesn't include it. 'That's OK because replace_spriteset saves the sprite and clears the cache 'which will cause it to be reloaded with the correct fallback_set. END IF END IF IF overwrote = NO THEN 'Paste each frame individually, keeping editing_spriteset's frame size and frame IDs FOR idx as integer = 0 TO small(v_len(copy_buffer), editing_spriteset->arraylen) - 1 paste_frame(copy_buffer[idx], @editing_spriteset[idx], transparent) NEXT END IF ELSE paste_frame(copy_buffer[0], @editing_spriteset[cur_framenum], transparent) END IF replace_spriteset cur_setnum, editing_spriteset 'Saves and unloads editing_spriteset rebuild_menu() END SUB SUB spriteset_editor_delete_clipboard() v_free SpriteSetBrowser.copy_buffer animset_unload @SpriteSetBrowser.copied_animations END SUB SUB SpriteSetBrowser.run() 'copy_buffer is never deleted IF copy_buffer = NULL THEN v_new copy_buffer ps.state_callback = @SpriteSetBrowser_set_plank_state_callback build_menu DIM cursor_moved as bool = YES DIM selected_frame as integer = 0 DIM last_resolution as XYPair 'This is a workaround for https://github.com/ohrrpgce/ohrrpgce/issues/1110 ' The workaround triggers a rebuild_menu on the second pass of the loop below DIM workaround as integer = 2 setkeys DO setwait 55 setkeys 'Update scroll after resolution change (not otherwise, or scrollwheel won't work) IF get_resolution() <> last_resolution THEN RefreshSliceTreeScreenPos root 'Doesn't seem to be sufficient always ScrollToChild find_plank_scroll(ps.m), top_left_plank(ps) 'Scroll all the way to the left update_plank_scrolling ps last_resolution = get_resolution() END IF IF keyval(ccCancel) > 1 THEN EXIT DO IF keyval(scF1) > 1 THEN show_help "spriteset_browser" IF keyval(scF6) > 1 THEN IF keyval(scCtrl) > 0 THEN slice_editor SL_COLLECT_EDITOR, finddatafile("spriteset_browser.slice"), YES build_menu CONTINUE DO ELSE slice_editor root, SL_COLLECT_EDITOR, , , YES END IF END IF IF workaround > 0 THEN workaround -= 1 IF workaround = 0 THEN rebuild_menu END IF 'Debug key Ctrl-R, reload sprite cache IF keyval(scCtrl) > 0 ANDALSO keyval(scR) > 1 THEN delete_menu_items sprite_empty_cache sprtype rebuild_menu END IF 'Clear selection indicators IF ps.cur THEN set_plank_state ps, ps.cur, plankNORMAL IF hover THEN set_plank_state ps, hover, plankNORMAL DIM setnum as integer = cur_setnum IF keyval(scShift) THEN 'Move by spriteset cursor_moved = YES IF keyval(ccLeft) > 1 THEN setnum -= 1 ELSEIF keyval(ccRight) > 1 THEN setnum += 1 ELSEIF keyval(scEnd) > 1 THEN setnum = gen(genmax) ELSEIF plank_menu_arrows(ps) THEN 'Up/down/pageup/pagedown/home setnum = cur_setnum ELSE cursor_moved = NO END IF IF cursor_moved THEN set_focus(setnum, -1) ELSEIF plank_menu_arrows(ps, , YES) THEN 'linear_left_right=YES cursor_moved = YES ELSEIF keyval(ccLeft) > 1 ORELSE keyval(ccUp) > 1 THEN 'Wrap to end cursor_moved = plank_menu_end(ps) ELSEIF keyval(ccRight) > 1 ORELSE keyval(ccDown) > 1 THEN 'Wrap to start cursor_moved = plank_menu_home(ps) END IF plank_menu_mouse_wheel(ps) 'Reserve Delete for deleting frames/sets, not digits of setnum IF keyval(scDelete) = 0 ANDALSO intgrabber(setnum, 0, gen(genmax), scNone, scNone, YES, NO) THEN set_focus(setnum, cur_framenum) cursor_moved = YES highlight_ss_id = YES ELSEIF anykeypressed() THEN highlight_ss_id = NO END IF DIM byref mouse as MouseInfo = readmouse IF mouse.active THEN hover = find_plank_at_screen_pos(ps, mouse.pos) ELSE hover = NULL END IF IF hover THEN IF (mouse.clicks AND mouseLeft) ORELSE (mouse.release AND mouseRight) THEN cursor_moved = ps.cur <> hover ps.cur = hover END IF ELSE IF (mouse.release AND mouseRight) ANDALSO mouse.drag_dist < 10 THEN EXIT DO END IF END IF 'Delete frames or spritesets IF cur_setnum >= 0 THEN IF cropafter_keycombo(NO) THEN 'Whole spriteset 'crop_spriteset() ELSEIF cur_framenum >= 0 ANDALSO keyval(scDelete) > 1 THEN 'One frame IF sprite_sizes(sprtype).fixed_framecount THEN notification sprite_sizes(sprtype).name & " sprites currently don't support adding or removing frames." ELSEIF yesno("Really delete this frame? There's no undo!", NO) THEN delete_frame(cur_setnum, cur_framenum) END IF END IF END IF '+: Add new frame or frame group, or even a new spriteset IF keyval(scPlus) > 1 ORELSE keyval(scNumpadPlus) > 1 ORELSE keyval(scInsert) > 1 THEN IF cur_setnum = -1 THEN 'Add new add_spriteset() ELSEIF sprite_sizes(sprtype).fixed_framecount THEN notification sprite_sizes(sprtype).name & " sprites currently don't support adding or removing frames." ELSEIF cur_framenum = -1 THEN 'Whole spriteset 'New group in first empty slot add_frame(cur_setnum, YES) ELSE 'Shift: add a new frame group in the first empty space after the existing one 'W/o shift: add a frame after the current one add_frame(cur_setnum, keyval(scShift) > 0, cur_framenum) END IF END IF IF enter_or_space() ORELSE ((mouse.release AND mouseLeft) ANDALSO hover = ps.cur) then IF cur_setnum = -1 THEN 'Add new add_spriteset() ELSEIF cur_framenum = -1 THEN 'Whole spriteset: Spriteset menu IF keyval(scShift) > 0 THEN spriteset_detail_editor sprtype, setnum rebuild_menu ELSE edit_spriteset(cur_setnum) END IF ELSE 'Single frame edit_any(cur_setnum, cur_framenum) END IF END IF 'IF keyval(scA) > 1 THEN edit_spriteset(cur_setnum) IF cur_setnum >= 0 THEN 'Previous palette IF keyval(scLeftBrace) > 1 THEN change_def_pal(-1) 'Next palette IF keyval(scRightBrace) > 1 THEN change_def_pal(1) IF copy_keychord() THEN copy_any() 'Don't use paste_keychord, which includes Shift-Insert IF keyval(scCtrl) > 0 AND keyval(scV) > 1 THEN paste_any(NO) IF keyval(scCtrl) > 0 ANDALSO keyval(scT) > 1 THEN paste_any(YES) IF keyval(scE) > 1 THEN export_any() IF keyval(scI) > 1 THEN import_any() END IF 'Set selection indicators IF hover THEN set_plank_state ps, hover, plankMOUSEHOVER IF ps.cur THEN set_plank_state ps, ps.cur, plankSEL END IF IF cursor_moved THEN remem_setnum(sprtype) = cur_setnum remem_framenum(sprtype) = cur_framenum update_plank_scrolling ps need_update = YES cursor_moved = NO END IF IF need_update THEN update_info() need_update = NO END IF clearpage vpage 'Kludge: We want the layout slice to stretch across the screen but it can't be set to fill horizontally 'or scrolling won't work. TODO: FillParent sucks, we need width-as-%-of-parent settings. DIM as Slice ptr list_sl = edsl(ssed_list, root) IF list_sl THEN list_sl->Width = list_sl->Parent->Width DrawSlice root, vpage setvispage vpage dowait LOOP DeleteSlice @root END SUB '------------------------------------------------------------------------------- 'Temporary, will probably become part of SpriteSetEditor SUB spriteset_resize_menu_rebuild(byref root as Slice ptr, sprset as Frame ptr, pal as integer) DeleteSlice @root root = NewSliceOfType(slContainer) SliceLoadFromFile root, finddatafile("spriteset_editor.slice") DIM as Slice ptr fr_templ, fr_sl ' Add the frames fr_templ = edsl(ssed_frame_templ, root) FOR framenum as integer = 0 TO sprset->arraylen - 1 DIM frameid as integer = sprset[framenum].frameid fr_sl = plank_menu_append( , fr_templ) fr_sl->Extra(0) = 0 fr_sl->Extra(1) = framenum fr_sl->Extra(2) = frameid DIM spr_sl as Slice ptr = edsl(ssed_frame_sprite, fr_sl) SetSpriteToFrame spr_sl, frame_reference(sprset), , pal ChangeSpriteSlice spr_sl, , , , framenum spr_sl->Visible = YES NEXT ' FIXME: CoverChildren is a bit broken, just refreshing doesn't properly position ' everything, and even have to draw multiple times due to nested CoverChildren. DrawSlice root, vpage DrawSlice root, vpage END SUB '========================================================================================== ' Spriteset Detail Editor '========================================================================================== ' This barebones "editor" is currently nothing but a front to the animation ' editor (and .gif export), but the plan is for it to show all frames and ' animations, so the spriteset browser can omit or overlap frames. SUB spriteset_detail_editor(sprtype as SpriteType, setnum as integer) VAR editor = SpriteSetEditor(sprtype, setnum) editor.run() END SUB SUB SpriteSetEditor.delete_previews() FOR idx as integer = 0 TO UBOUND(preview_anims) preview_anims(idx)->dereference() NEXT ERASE preview_slices ERASE preview_anims DeleteSliceChildren previews_root END SUB CONSTRUCTOR SpriteSetEditor(sprtype as SpriteType, setnum as integer) this.sprtype = sprtype this.setnum = setnum END CONSTRUCTOR SUB SpriteSetEditor.create_preview(anim as Animation ptr, rowidx as integer, colidx as integer) DIM contsl as Slice ptr = NewSliceOfType(slContainer, previews_root) DIM sprsl as Slice ptr = NewSliceOfType(slSprite, contsl) ChangeSpriteSlice sprsl, sprtype, setnum DIM xspacing as integer = large(40, ss->frames[0].w + 6) DIM rowspacing as integer = ss->frames[0].h + 70 DIM stagger as integer = colidx MOD 4 contsl->Pos = XY(10 + xspacing * colidx, 10 * stagger + rowspacing * rowidx) 'Extra spacing between each stagger group contsl->X += (colidx \ 4) * 40 DIM namesl as Slice ptr = NewSliceOfType(slText, contsl) DIM name as string = anim->name + " " + anim->variant ChangeTextSlice namesl, name namesl->AnchorVert = alignBottom ' Play each animation normally: only once if it doesn't end in Repeat sprsl->GetAnimState->start_animation(anim) ASSERT(sprsl->AnimState->anim = anim) DIM idx as integer = UBOUND(preview_slices) + 1 REDIM PRESERVE preview_slices(idx) preview_slices(idx) = sprsl REDIM PRESERVE preview_anims(idx) preview_anims(idx) = anim->reference() END SUB SUB SpriteSetEditor.update_previews() delete_previews() ERASE overridden_animations DIM as integer idx, colidx DIM animset as AnimationSet ptr = ss->get_animset FOR idx = 0 TO v_len(animset->animations) - 1 create_preview animset->animations[idx], 1, idx NEXT DIM global as AnimationSet ptr = animset->fallback_set IF global THEN FOR idx = 0 TO v_len(global->animations) - 1 DIM name as string WITH *global->animations[idx] name = .name & " " & .variant END WITH IF animset->get_animation(name) THEN a_append(overridden_animations(), name) ELSE create_preview global->animations[idx], 2, colidx colidx += 1 END IF NEXT END IF END SUB SUB SpriteSetEditor.run() previews_root = NewSlice() ss = spriteset_load(sprtype, setnum) pal = palette16_load(-1, sprtype, setnum) context = acHeroSprite 'FIXME update_previews() setkeys DO setwait 55 setkeys tog XOR= 1 IF keyval(ccCancel) > 1 ORELSE (readmouse.release AND mouseRight) THEN EXIT DO IF keyval(scE) > 1 ORELSE enter_or_space() THEN DIM animsl as Slice ptr = NewSliceOfType(slSprite) ChangeSpriteSlice animsl, sprtype, setnum ' animsl->GetAnimations() == ss->animset animations_editor animsl, ss->get_animset, context, default_export_name(sprtype, setnum) DeleteSlice @animsl update_previews() ELSEIF keyval(scP) > 1 THEN FOR idx as integer = 0 TO UBOUND(preview_slices) preview_slices(idx)->GetAnimState->reset() preview_slices(idx)->GetAnimState->start_animation(preview_anims(idx)) NEXT END IF 'IF keyval(scX) > 1 THEN export_menu() 'Super unfinished IF keyval(scF6) > 1 THEN slice_editor previews_root, SL_COLLECT_EDITOR AdvanceSlice previews_root display() dowait LOOP 'Save animations 'FIXME: save_animations_node / load_animations_node saves the animations to a temporary 'node which we will ignore in future. rgfx_save_spriteset ss->frames, sprtype, setnum rgfx_save_global_animations sprtype spriteset_unload @ss palette16_unload @pal delete_previews() DeleteSlice @previews_root END SUB SUB SpriteSetEditor.display() clearpage vpage DIM caption as string = "E: Edit animations P: Play all" edgeprint caption, pMenuX, pInfoY, uilook(uiText), vpage edgeprint "Frame groups:", pMenuX, pMenuY, uilook(eduiHeading), vpage DIM as integer x = pMenuX, y = pMenuY + 24, frameh = ss->frames[0].h, spacercol = findrgb(100,100,100), spacing FOR idx as integer = 0 to ss->num_frames - 1 spacing = 1 IF ss->frame_starts_group(idx) THEN edgeprint STR(ss->frames[idx].frameid), x + 3, y - 12, uilook(uiMenuItem), vpage spacing += 2 END IF rectangle x, y, spacing, frameh, spacercol, vpage x += spacing frame_draw @ss->frames[idx], pal, x, y, , vpage x += ss->frames[idx].w NEXT rectangle x, y, 3, frameh, spacercol, vpage DrawSlice previews_root, vpage DIM rowspacing as integer = frameh + 70 DIM where as XYPair = XY(pMenuX, rowspacing * 1 - 12 + ancBottom) edgeprint "Animations:", where.x, where.y, uilook(eduiHeading), vpage where.y += rowspacing edgeprint "Global Animations:", where.x, where.y, uilook(eduiHeading), vpage where.y += rowspacing edgeprint "Overridden Globals:", where.x, where.y, uilook(eduiHeading), vpage FOR idx as integer = 0 TO UBOUND(overridden_animations) where.y += 9 edgeprint overridden_animations(idx), where.x, where.y, uilook(uiMenuItem), vpage NEXT setvispage vpage END SUB ' Very unfinished SUB SpriteSetEditor.export_menu() DIM choices(...) as string = { _ "Current frame", "Spritesheet (unimplemented)" _ } DIM choice as integer = multichoice("Export what?", choices()) IF choice = 0 THEN 'frame_export_gif @ss->frames[0], "hero0.gif", master(), pal 'a single frame ELSEIF choice = 1 THEN 'export_gif ... END IF END SUB