• The forum has been updated. For an explanation of some of the changes, head over here.

OSP Code QoL Particle Preview Presentation

Currently viewing this thread:


Is the description really necessary? Warband doesn't provide any ways to preview particles before using, so I made my own.

You press your preferred key in the included missions to start a small window, that will appear in bottom left corner of your screen. It will allow you to select particle id, percentage burst strength and the amount of instances (to make fire brighter you may want to burst it thrice, for example). Since there is no code way to retrieve particle name, you'll have to use ID_particle_systems.py for that. Then, after pressing the "Emit Particle" button, the burst with your settings will appear on the plate. If the particle is too big, you can pull the plate away to see it fully, or to compare size with other troops/props, as the plate will move alongside with your camera and is always locked in same position on the screen. If particle is too small, you can move the plate towards you, even closer than the default position.

I strongly advise not putting it in combat missions! Here's why:
- since this presentation doesn't have prsntf_read_only flag, it disables your ability to attack and/or block in missions on the time of running;
- it will intersect with Backspace command panel, every time you want to erase old number with Backspace to enter the new one, the minimap presentation will pop up, which is quite annoying;
- pressing inventory button in missions where invenotry is available only via baggage chest will remove the plate without closing the window.
The recommended templates for safe testing are:
• town_default (taverns)
• town_center (self-explanatory)
• village_center (self-explanatory)
• visit_town_castle (castle halls)
• castle_visit (castle exteriors)
• ai_training (entering scenes via cheats)

Now for the implementation, this goes to module_presentations.py:
("particle_preview", prsntf_manual_end_only, 0,[ # no idea what this flag does but just in case

      (set_fixed_point_multiplier, 1000),
      (presentation_set_duration, 999999),

      # Background mesh
      (create_mesh_overlay, "$pp_background", "mesh_ui_quick_battle_a", tf_center_justify),
      (position_set_x, pos1, 750),
      (position_set_y, pos1, 750),
      (overlay_set_size, "$pp_background", pos1),

      # Title text
      (str_store_string, s1, "@Particle Preview"),
      (create_text_overlay, "$pp_title", s1, tf_with_outline|tf_center_justify),
      (overlay_set_color, "$pp_title", 0xC5B58D),
      (position_set_x, pos1, 1700),
      (position_set_y, pos1, 1700),
      (overlay_set_size, "$pp_title", pos1),
      (position_set_x, pos1, 140),
      (position_set_y, pos1, 500),
      (overlay_set_position, "$pp_title", pos1),

      # ID number box
      (assign, reg50, 60), # this is ID of the last particle in ID_particle_systems.py of your module (60 for Native)
      (store_add, ":limit", reg50, 1),
      (assign, "$pp_id_val", 0), # default value
      (create_number_box_overlay, "$pp_id", 0, ":limit", tf_center_justify),
      (position_set_x, pos1, 160),
      (position_set_y, pos1, 450),
      (overlay_set_position, "$pp_id", pos1),
      (str_store_string, s5, "@ID (0-{reg50}):"), # it will be automatically displayed as in-game upper bound
      (create_text_overlay, "$pp_id_text", s5, tf_right_align),
      (position_set_x, pos1, 155),
      (position_set_y, pos1, 450),
      (overlay_set_position, "$pp_id_text", pos1),

      # Strength number box
      (create_number_box_overlay, "$pp_str", 1, 1000000000, tf_center_justify),
      (position_set_x, pos1, 160),
      (position_set_y, pos1, 400),
      (overlay_set_position, "$pp_str", pos1),
      (overlay_set_val, "$pp_str", 100), # default value
      (assign, "$pp_str_val", 100),
      (str_store_string, s6, "@Strength (%):"),
      (create_text_overlay, "$pp_str_text", s6, tf_right_align),
      (position_set_x, pos1, 155),
      (position_set_y, pos1, 400),
      (overlay_set_position, "$pp_str_text", pos1),

      # Instances number box
      (create_number_box_overlay, "$pp_num", 1, 1000000000, tf_center_justify),
      (position_set_x, pos1, 160),
      (position_set_y, pos1, 350),
      (overlay_set_position, "$pp_num", pos1),
      (overlay_set_val, "$pp_num", 1), # default value
      (assign, "$pp_num_val", 1),
      (str_store_string, s7, "@Instances:"),
      (create_text_overlay, "$pp_num_text", s7, tf_right_align),
      (position_set_x, pos1, 155),
      (position_set_y, pos1, 350),
      (overlay_set_position, "$pp_num_text", pos1),

      # Plate distance slider
      (create_slider_overlay, "$pp_dist", 50, 4160, tf_center_justify), # these are game-tested min and max distance limits. Particle won't be visible beyond them
      (position_set_x, pos1, 157),
      (position_set_y, pos1, 300),
      (overlay_set_position, "$pp_dist", pos1),
      (position_set_x, pos1, 800),
      (position_set_y, pos1, 800),
      (overlay_set_size, "$pp_dist", pos1),
      (overlay_set_val, "$pp_dist", 170), # default value
      (assign, "$pp_dist_val", 170),
      (str_store_string, s8, "@Plate Distance"),
      (create_text_overlay, "$pp_dist_text", s8, tf_center_justify),
      (position_set_x, pos1, 135),
      (position_set_y, pos1, 275),
      (overlay_set_position, "$pp_dist_text", pos1),
      (position_set_x, pos1, 985),
      (position_set_y, pos1, 985),
      (overlay_set_size, "$pp_dist_text", pos1),

      # Description text
      (str_store_string, s2, "@Particle of your choice will be^emitted from the center of^wooden plate. Use ID .py file^to get 'psys' reference of^the number you see here."), # no full file name here cuz in strings game treats _ symbol as space sadly
      (create_text_overlay, "$pp_description", s2, tf_center_justify),
      (position_set_x, pos1, 130),
      (position_set_y, pos1, 170),
      (overlay_set_position, "$pp_description", pos1),
      (position_set_x, pos1, 900),
      (position_set_y, pos1, 900),
      (overlay_set_size, "$pp_description", pos1),

      # Emit button
      (str_store_string, s3, "@Emit Particle"),
      (create_in_game_button_overlay, "$pp_emit", s3, tf_center_justify),
      (position_set_x, pos1, 131),
      (position_set_y, pos1, 111),
      (overlay_set_position, "$pp_emit", pos1),

      # Exit button
      (str_store_string, s4, "@Close Window"),
      (create_game_button_overlay, "$pp_close", s4, tf_center_justify),
      (position_set_x, pos1, 130),
      (position_set_y, pos1, 35),
      (overlay_set_position, "$pp_close", pos1),

      # Plate creation
      (mission_cam_get_position, pos2),
      (set_spawn_position, pos2),
      (spawn_scene_prop, "spr_plate_a"),
      (assign, "$pp_plate", reg0),

      (mission_cam_get_position, pos2), # always screen-centrified
      (position_move_x, pos2, -50),
      (position_move_y, pos2, "$pp_dist_val"), # making it slider-dependant
      (position_rotate_x, pos2, 30),
      (position_rotate_y, pos2, 7), # slightly tilting for better view on particle
      (prop_instance_set_position, "$pp_plate", pos2),
        (this_or_next|game_key_clicked, gk_quests_window), # all keys that can close presentation should also remove the plate not to make clutter
        (this_or_next|game_key_clicked, gk_game_log_window),
        (this_or_next|game_key_clicked, gk_party_window),
        (this_or_next|game_key_clicked, gk_inventory_window),
        (this_or_next|game_key_clicked, gk_character_window),
        (key_clicked, key_escape),
        (scene_prop_set_visibility, "$pp_plate", 0),
        (prop_instance_get_position, pos3, "$pp_plate"),
        (position_move_z, pos3, -3000, 1),
        (prop_instance_set_position, "$pp_plate", pos3),
        (assign, "$pp_plate", -1),

        (store_trigger_param_1, ":object"),
        (store_trigger_param_2, ":value"),
          (eq, ":object", "$pp_id"),  
          (assign, "$pp_id_val", ":value"),
          (eq, ":object", "$pp_str"),
          (assign, "$pp_str_val", ":value"),
          (eq, ":object", "$pp_num"),
          (assign, "$pp_num_val", ":value"),
          (eq, ":object", "$pp_dist"),
          (assign, "$pp_dist_val", ":value"),
          (eq, ":object", "$pp_emit"),
          (prop_instance_get_position, pos2, "$pp_plate"),
          (try_for_range, ":iteration", 0, "$pp_num_val"),
            (lt, ":iteration", "$pp_num_val"), # no need to check for lt as this is already how loops work but this line prevents compiler warning
            (particle_system_burst, "$pp_id_val", pos2, "$pp_str_val"),
          (eq, ":object", "$pp_close"),
          (presentation_set_duration, 0),
          (scene_prop_set_visibility, "$pp_plate", 0), # removing the plate upon closing
          (prop_instance_get_position, pos3, "$pp_plate"),
          (position_move_z, pos3, -3000, 1),
          (prop_instance_set_position, "$pp_plate", pos3),
          (assign, "$pp_plate", -1),

And this goes to module_mission_templates.py:
particle_preview = (0, 0, 0,
  (key_clicked, key_enter), # replace with any button you want
  (neg|is_presentation_active, "prsnt_particle_preview"), # every presentation call spawns a new plate, so we have to check if it is spawned already
  (start_presentation, "prsnt_particle_preview"),

Enjoy and embrace the power of particle systems!
Last edited:
It is truly magnificent that my innocent question about particle systems' preview had served as a reminder of the issue, which in turn prompted Dalion to create or complete this tool. Thank you again for this useful addition that will prove to be immensely helpful for the wizards of particles!
Top Bottom