Browse Source

Spawn constructed objects in center of builder circle formation

djeada 3 days ago
parent
commit
14cf15ccca

+ 1 - 9
game/systems/production_system.cpp

@@ -147,15 +147,7 @@ void ProductionSystem::update(Engine::Core::World *world, float delta_time) {
         if (reg) {
         if (reg) {
           Game::Units::SpawnParams sp;
           Game::Units::SpawnParams sp;
 
 
-          float const spawn_offset = 2.5F;
-          float forward_x = 0.0F;
-          float forward_z = 1.0F;
-          float yaw = t->rotation.y;
-          forward_x = std::sin(yaw);
-          forward_z = std::cos(yaw);
-          sp.position =
-              QVector3D(t->position.x + forward_x * spawn_offset, t->position.y,
-                        t->position.z + forward_z * spawn_offset);
+          sp.position = QVector3D(t->position.x, t->position.y, t->position.z);
           sp.player_id = u->owner_id;
           sp.player_id = u->owner_id;
           sp.ai_controlled =
           sp.ai_controlled =
               e->has_component<Engine::Core::AIControlledComponent>();
               e->has_component<Engine::Core::AIControlledComponent>();

+ 3 - 3
render/entity/defense_tower_renderer.cpp

@@ -26,13 +26,13 @@ void register_defense_tower_renderer(EntityRendererRegistry &registry) {
         std::string renderer_key;
         std::string renderer_key;
         switch (unit->nation_id) {
         switch (unit->nation_id) {
         case Game::Systems::NationID::Carthage:
         case Game::Systems::NationID::Carthage:
-          renderer_key = "defense_tower_carthage";
+          renderer_key = "troops/carthage/defense_tower";
           break;
           break;
         case Game::Systems::NationID::RomanRepublic:
         case Game::Systems::NationID::RomanRepublic:
-          renderer_key = "defense_tower_roman";
+          renderer_key = "troops/roman/defense_tower";
           break;
           break;
         default:
         default:
-          renderer_key = "defense_tower_roman";
+          renderer_key = "troops/roman/defense_tower";
           break;
           break;
         }
         }
 
 

+ 195 - 71
render/entity/nations/carthage/builder_renderer.cpp

@@ -74,6 +74,9 @@ using Render::GL::Humanoid::saturate_color;
 
 
 class BuilderRenderer : public HumanoidRendererBase {
 class BuilderRenderer : public HumanoidRendererBase {
 public:
 public:
+  friend void
+  register_builder_renderer(Render::GL::EntityRendererRegistry &registry);
+
   auto get_proportion_scaling() const -> QVector3D override {
   auto get_proportion_scaling() const -> QVector3D override {
     return {0.98F, 1.01F, 0.96F};
     return {0.98F, 1.01F, 0.96F};
   }
   }
@@ -123,60 +126,26 @@ public:
 
 
     if (anim.is_constructing) {
     if (anim.is_constructing) {
 
 
+      uint32_t const pose_selector = seed % 100;
+
       float const phase_offset = float(seed % 100) * 0.0628F;
       float const phase_offset = float(seed % 100) * 0.0628F;
       float const cycle_speed = 2.0F + float(seed % 50) * 0.02F;
       float const cycle_speed = 2.0F + float(seed % 50) * 0.02F;
       float const swing_cycle =
       float const swing_cycle =
           std::fmod(anim.time * cycle_speed + phase_offset, 1.0F);
           std::fmod(anim.time * cycle_speed + phase_offset, 1.0F);
 
 
-      float swing_angle;
-      float body_lean;
-      float crouch_amount;
-
-      if (swing_cycle < 0.3F) {
+      if (pose_selector < 40) {
 
 
-        float const t = swing_cycle / 0.3F;
-        swing_angle = t * 0.85F;
-        body_lean = -t * 0.08F;
-        crouch_amount = 0.0F;
-      } else if (swing_cycle < 0.5F) {
+        apply_hammering_pose(controller, swing_cycle, asym, seed);
+      } else if (pose_selector < 70) {
 
 
-        float const t = (swing_cycle - 0.3F) / 0.2F;
-        swing_angle = 0.85F - t * 1.3F;
-        body_lean = -0.08F + t * 0.22F;
-        crouch_amount = t * 0.06F;
-      } else if (swing_cycle < 0.6F) {
+        apply_kneeling_work_pose(controller, swing_cycle, asym, seed);
+      } else if (pose_selector < 90) {
 
 
-        float const t = (swing_cycle - 0.5F) / 0.1F;
-        swing_angle = -0.45F + t * 0.15F;
-        body_lean = 0.14F - t * 0.04F;
-        crouch_amount = 0.06F - t * 0.02F;
+        apply_sawing_pose(controller, swing_cycle, asym, seed);
       } else {
       } else {
 
 
-        float const t = (swing_cycle - 0.6F) / 0.4F;
-        swing_angle = -0.30F + t * 0.30F;
-        body_lean = 0.10F * (1.0F - t);
-        crouch_amount = 0.04F * (1.0F - t);
+        apply_lifting_pose(controller, swing_cycle, asym, seed);
       }
       }
-
-      float const torso_y_offset = -crouch_amount;
-
-      float const hammer_y = HP::SHOULDER_Y + 0.10F + swing_angle * 0.20F;
-      float const hammer_forward =
-          0.18F + std::abs(swing_angle) * 0.15F + body_lean * 0.5F;
-      float const hammer_down =
-          swing_cycle > 0.4F && swing_cycle < 0.65F ? 0.08F : 0.0F;
-
-      QVector3D const hammer_hand(-0.06F + asym,
-                                  hammer_y - hammer_down + torso_y_offset,
-                                  hammer_forward);
-
-      float const brace_y =
-          HP::WAIST_Y + 0.12F + torso_y_offset - crouch_amount * 0.5F;
-      float const brace_forward = 0.15F + body_lean * 0.3F;
-      QVector3D const brace_hand(0.14F - asym * 0.5F, brace_y, brace_forward);
-
-      controller.placeHandAt(true, hammer_hand);
-      controller.placeHandAt(false, brace_hand);
       return;
       return;
     }
     }
 
 
@@ -196,11 +165,164 @@ public:
                        const HumanoidAnimationContext &anim_ctx,
                        const HumanoidAnimationContext &anim_ctx,
                        ISubmitter &out) const override {
                        ISubmitter &out) const override {
 
 
-    draw_stone_hammer(ctx, v, pose, out);
+    draw_stone_hammer(ctx, v, pose, anim_ctx, out);
+  }
+
+  void draw_helmet(const DrawContext &ctx, const HumanoidVariant &v,
+                   const HumanoidPose &pose, ISubmitter &out) const override {
+
+    draw_headwrap(ctx, v, pose, out);
+  }
+
+  void draw_armor(const DrawContext &ctx, const HumanoidVariant &v,
+                  const HumanoidPose &pose,
+                  const HumanoidAnimationContext &anim,
+                  ISubmitter &out) const override {
+    uint32_t const seed = reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU;
+    draw_craftsman_robes(ctx, v, pose, seed, out);
+  }
+
+private:
+  static constexpr uint32_t KNEEL_SEED_OFFSET = 0x5678U;
+
+  void apply_hammering_pose(HumanoidPoseController &controller,
+                            float swing_cycle, float asym,
+                            uint32_t seed) const {
+    using HP = HumanProportions;
+
+    float swing_angle;
+    float body_lean;
+    float crouch_amount;
+
+    if (swing_cycle < 0.3F) {
+
+      float const t = swing_cycle / 0.3F;
+      swing_angle = t * 0.92F;
+      body_lean = -t * 0.09F;
+      crouch_amount = 0.0F;
+    } else if (swing_cycle < 0.5F) {
+
+      float const t = (swing_cycle - 0.3F) / 0.2F;
+      swing_angle = 0.92F - t * 1.45F;
+      body_lean = -0.09F + t * 0.26F;
+      crouch_amount = t * 0.07F;
+    } else if (swing_cycle < 0.6F) {
+
+      float const t = (swing_cycle - 0.5F) / 0.1F;
+      swing_angle = -0.53F + t * 0.16F;
+      body_lean = 0.17F - t * 0.05F;
+      crouch_amount = 0.07F - t * 0.02F;
+    } else {
+
+      float const t = (swing_cycle - 0.6F) / 0.4F;
+      swing_angle = -0.37F + t * 0.37F;
+      body_lean = 0.12F * (1.0F - t);
+      crouch_amount = 0.05F * (1.0F - t);
+    }
+
+    float const torso_y_offset = -crouch_amount;
+    float const hammer_y = HP::SHOULDER_Y + 0.10F + swing_angle * 0.20F;
+    float const hammer_forward =
+        0.18F + std::abs(swing_angle) * 0.15F + body_lean * 0.5F;
+    float const hammer_down =
+        swing_cycle > 0.4F && swing_cycle < 0.65F ? 0.08F : 0.0F;
+
+    QVector3D const hammer_hand(
+        -0.06F + asym, hammer_y - hammer_down + torso_y_offset, hammer_forward);
+
+    float const brace_y =
+        HP::WAIST_Y + 0.12F + torso_y_offset - crouch_amount * 0.5F;
+    float const brace_forward = 0.15F + body_lean * 0.3F;
+    QVector3D const brace_hand(0.14F - asym * 0.5F, brace_y, brace_forward);
+
+    controller.placeHandAt(true, hammer_hand);
+    controller.placeHandAt(false, brace_hand);
+  }
+
+  void apply_kneeling_work_pose(HumanoidPoseController &controller, float cycle,
+                                float asym, uint32_t seed) const {
+    using HP = HumanProportions;
+
+    float const kneel_depth =
+        0.50F + (hash_01(seed ^ KNEEL_SEED_OFFSET) * 0.12F);
+    controller.kneel(kneel_depth);
+
+    float const work_cycle = std::sin(cycle * std::numbers::pi_v<float> * 2.0F);
+
+    float const tool_y = HP::WAIST_Y * 0.32F + work_cycle * 0.07F;
+    float const tool_x_offset = 0.06F + work_cycle * 0.05F;
+    QVector3D const tool_hand(-tool_x_offset + asym, tool_y, 0.24F);
+
+    float const brace_x = 0.20F - asym * 0.5F;
+    QVector3D const brace_hand(brace_x, HP::WAIST_Y * 0.28F, 0.22F);
+
+    controller.placeHandAt(true, tool_hand);
+    controller.placeHandAt(false, brace_hand);
+  }
+
+  void apply_sawing_pose(HumanoidPoseController &controller, float cycle,
+                         float asym, uint32_t seed) const {
+    using HP = HumanProportions;
+
+    controller.lean(QVector3D(0.0F, 0.0F, 1.0F), 0.14F);
+
+    float const saw_offset =
+        std::sin(cycle * std::numbers::pi_v<float> * 4.0F) * 0.14F;
+
+    float const saw_y = HP::WAIST_Y + 0.18F;
+    float const saw_z = 0.22F + saw_offset;
+
+    QVector3D const left_hand(-0.10F + asym, saw_y, saw_z);
+    QVector3D const right_hand(0.10F - asym, saw_y + 0.03F, saw_z);
+
+    controller.placeHandAt(true, left_hand);
+    controller.placeHandAt(false, right_hand);
+  }
+
+  void apply_lifting_pose(HumanoidPoseController &controller, float cycle,
+                          float asym, uint32_t seed) const {
+    using HP = HumanProportions;
+
+    float lift_height;
+    float crouch;
+
+    if (cycle < 0.3F) {
+
+      float const t = cycle / 0.3F;
+      lift_height = HP::WAIST_Y * (1.0F - t * 0.5F);
+      crouch = t * 0.22F;
+    } else if (cycle < 0.6F) {
+
+      float const t = (cycle - 0.3F) / 0.3F;
+      lift_height =
+          HP::WAIST_Y * 0.5F + t * (HP::SHOULDER_Y - HP::WAIST_Y * 0.5F);
+      crouch = 0.22F * (1.0F - t);
+    } else if (cycle < 0.8F) {
+
+      lift_height = HP::SHOULDER_Y;
+      crouch = 0.0F;
+    } else {
+
+      float const t = (cycle - 0.8F) / 0.2F;
+      lift_height = HP::SHOULDER_Y * (1.0F - t * 0.35F);
+      crouch = 0.0F;
+    }
+
+    QVector3D const left_hand(-0.14F + asym, lift_height, 0.18F);
+    QVector3D const right_hand(0.14F - asym, lift_height, 0.18F);
+
+    controller.placeHandAt(true, left_hand);
+    controller.placeHandAt(false, right_hand);
+
+    if (crouch > 0.0F) {
+      controller.kneel(crouch);
+    }
   }
   }
 
 
   void draw_stone_hammer(const DrawContext &ctx, const HumanoidVariant &v,
   void draw_stone_hammer(const DrawContext &ctx, const HumanoidVariant &v,
-                         const HumanoidPose &pose, ISubmitter &out) const {
+                         const HumanoidPose &pose,
+                         const HumanoidAnimationContext &anim_ctx,
+                         ISubmitter &out) const {
     QVector3D const wood = v.palette.wood;
     QVector3D const wood = v.palette.wood;
 
 
     QVector3D const stone_color(0.52F, 0.50F, 0.46F);
     QVector3D const stone_color(0.52F, 0.50F, 0.46F);
@@ -208,11 +330,29 @@ public:
 
 
     QVector3D const hand = pose.hand_l;
     QVector3D const hand = pose.hand_l;
     QVector3D const up(0.0F, 1.0F, 0.0F);
     QVector3D const up(0.0F, 1.0F, 0.0F);
+    QVector3D const forward(0.0F, 0.0F, 1.0F);
     QVector3D const right(1.0F, 0.0F, 0.0F);
     QVector3D const right(1.0F, 0.0F, 0.0F);
 
 
+    const AnimationInputs &anim = anim_ctx.inputs;
+    QVector3D handle_axis;
+    QVector3D head_axis;
+
+    if (anim.is_constructing) {
+
+      handle_axis = forward;
+      head_axis = up;
+    } else {
+
+      handle_axis = up;
+      head_axis = right;
+    }
+
     float const h_len = 0.30F;
     float const h_len = 0.30F;
-    QVector3D const h_top = hand + up * 0.11F;
-    QVector3D const h_bot = h_top - up * h_len;
+    QVector3D const handle_offset = anim.is_constructing
+                                        ? (forward * 0.11F + up * 0.02F)
+                                        : (up * 0.11F + forward * 0.02F);
+    QVector3D const h_top = hand + handle_offset;
+    QVector3D const h_bot = h_top - handle_axis * h_len;
 
 
     out.mesh(get_unit_cylinder(),
     out.mesh(get_unit_cylinder(),
              cylinder_between(ctx.model, h_bot, h_top, 0.015F), wood, nullptr,
              cylinder_between(ctx.model, h_bot, h_top, 0.015F), wood, nullptr,
@@ -220,31 +360,25 @@ public:
 
 
     float const head_len = 0.09F;
     float const head_len = 0.09F;
     float const head_r = 0.028F;
     float const head_r = 0.028F;
-    QVector3D const head_center = h_top + up * 0.03F;
+    QVector3D const head_center = h_top + handle_axis * 0.03F;
 
 
-    out.mesh(get_unit_cylinder(),
-             cylinder_between(ctx.model,
-                              head_center - right * (head_len * 0.5F),
-                              head_center + right * (head_len * 0.5F), head_r),
-             stone_color, nullptr, 1.0F);
+    out.mesh(
+        get_unit_cylinder(),
+        cylinder_between(ctx.model, head_center - head_axis * (head_len * 0.5F),
+                         head_center + head_axis * (head_len * 0.5F), head_r),
+        stone_color, nullptr, 1.0F);
 
 
     out.mesh(get_unit_sphere(),
     out.mesh(get_unit_sphere(),
-             sphere_at(ctx.model, head_center + right * (head_len * 0.5F),
+             sphere_at(ctx.model, head_center + head_axis * (head_len * 0.5F),
                        head_r * 1.1F),
                        head_r * 1.1F),
              stone_dark, nullptr, 1.0F);
              stone_dark, nullptr, 1.0F);
 
 
     out.mesh(get_unit_sphere(),
     out.mesh(get_unit_sphere(),
-             sphere_at(ctx.model, head_center - right * (head_len * 0.5F),
+             sphere_at(ctx.model, head_center - head_axis * (head_len * 0.5F),
                        head_r * 0.85F),
                        head_r * 0.85F),
              stone_color * 0.92F, nullptr, 1.0F);
              stone_color * 0.92F, nullptr, 1.0F);
   }
   }
 
 
-  void draw_helmet(const DrawContext &ctx, const HumanoidVariant &v,
-                   const HumanoidPose &pose, ISubmitter &out) const override {
-
-    draw_headwrap(ctx, v, pose, out);
-  }
-
   void draw_headwrap(const DrawContext &ctx, const HumanoidVariant &v,
   void draw_headwrap(const DrawContext &ctx, const HumanoidVariant &v,
                      const HumanoidPose &pose, ISubmitter &out) const {
                      const HumanoidPose &pose, ISubmitter &out) const {
     const BodyFrames &frames = pose.body_frames;
     const BodyFrames &frames = pose.body_frames;
@@ -261,14 +395,6 @@ public:
              wrap_color * 0.95F, nullptr, 1.0F);
              wrap_color * 0.95F, nullptr, 1.0F);
   }
   }
 
 
-  void draw_armor(const DrawContext &ctx, const HumanoidVariant &v,
-                  const HumanoidPose &pose,
-                  const HumanoidAnimationContext &anim,
-                  ISubmitter &out) const override {
-    uint32_t const seed = reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU;
-    draw_craftsman_robes(ctx, v, pose, seed, out);
-  }
-
   void draw_craftsman_robes(const DrawContext &ctx, const HumanoidVariant &v,
   void draw_craftsman_robes(const DrawContext &ctx, const HumanoidVariant &v,
                             const HumanoidPose &pose, uint32_t seed,
                             const HumanoidPose &pose, uint32_t seed,
                             ISubmitter &out) const {
                             ISubmitter &out) const {
@@ -403,7 +529,6 @@ private:
     return def;
     return def;
   }
   }
 
 
-public:
   auto resolve_shader_key(const DrawContext &ctx) const -> QString {
   auto resolve_shader_key(const DrawContext &ctx) const -> QString {
     const BuilderStyleConfig &s = resolve_style(ctx);
     const BuilderStyleConfig &s = resolve_style(ctx);
     if (!s.shader_id.empty()) {
     if (!s.shader_id.empty()) {
@@ -412,7 +537,6 @@ public:
     return QStringLiteral("builder");
     return QStringLiteral("builder");
   }
   }
 
 
-private:
   void apply_palette_overrides(const BuilderStyleConfig &style,
   void apply_palette_overrides(const BuilderStyleConfig &style,
                                const QVector3D &team_tint,
                                const QVector3D &team_tint,
                                HumanoidVariant &v) const {
                                HumanoidVariant &v) const {

+ 2 - 1
render/entity/nations/carthage/defense_tower_renderer.cpp

@@ -166,7 +166,8 @@ void draw_defense_tower(const DrawContext &p, ISubmitter &out) {
 
 
 void register_defense_tower_renderer(
 void register_defense_tower_renderer(
     Render::GL::EntityRendererRegistry &registry) {
     Render::GL::EntityRendererRegistry &registry) {
-  registry.register_renderer("defense_tower_carthage", draw_defense_tower);
+  registry.register_renderer("troops/carthage/defense_tower",
+                             draw_defense_tower);
 }
 }
 
 
 } // namespace Render::GL::Carthage
 } // namespace Render::GL::Carthage

+ 201 - 77
render/entity/nations/roman/builder_renderer.cpp

@@ -74,6 +74,9 @@ using Render::GL::Humanoid::saturate_color;
 
 
 class BuilderRenderer : public HumanoidRendererBase {
 class BuilderRenderer : public HumanoidRendererBase {
 public:
 public:
+  friend void
+  register_builder_renderer(Render::GL::EntityRendererRegistry &registry);
+
   auto get_proportion_scaling() const -> QVector3D override {
   auto get_proportion_scaling() const -> QVector3D override {
     return {1.05F, 0.98F, 1.02F};
     return {1.05F, 0.98F, 1.02F};
   }
   }
@@ -99,61 +102,26 @@ public:
 
 
     if (anim.is_constructing) {
     if (anim.is_constructing) {
 
 
+      uint32_t const pose_selector = seed % 100;
+
       float const phase_offset = float(seed % 100) * 0.0628F;
       float const phase_offset = float(seed % 100) * 0.0628F;
       float const cycle_speed = 2.0F + float(seed % 50) * 0.02F;
       float const cycle_speed = 2.0F + float(seed % 50) * 0.02F;
       float const swing_cycle =
       float const swing_cycle =
           std::fmod(anim.time * cycle_speed + phase_offset, 1.0F);
           std::fmod(anim.time * cycle_speed + phase_offset, 1.0F);
 
 
-      float swing_angle;
-      float body_lean;
-      float crouch_amount;
+      if (pose_selector < 40) {
 
 
-      if (swing_cycle < 0.3F) {
+        apply_hammering_pose(controller, swing_cycle, asymmetry, seed);
+      } else if (pose_selector < 70) {
 
 
-        float const t = swing_cycle / 0.3F;
-        swing_angle = t * 0.85F;
-        body_lean = -t * 0.08F;
-        crouch_amount = 0.0F;
-      } else if (swing_cycle < 0.5F) {
+        apply_kneeling_work_pose(controller, swing_cycle, asymmetry, seed);
+      } else if (pose_selector < 90) {
 
 
-        float const t = (swing_cycle - 0.3F) / 0.2F;
-        swing_angle = 0.85F - t * 1.3F;
-        body_lean = -0.08F + t * 0.22F;
-        crouch_amount = t * 0.06F;
-      } else if (swing_cycle < 0.6F) {
-
-        float const t = (swing_cycle - 0.5F) / 0.1F;
-        swing_angle = -0.45F + t * 0.15F;
-        body_lean = 0.14F - t * 0.04F;
-        crouch_amount = 0.06F - t * 0.02F;
+        apply_sawing_pose(controller, swing_cycle, asymmetry, seed);
       } else {
       } else {
 
 
-        float const t = (swing_cycle - 0.6F) / 0.4F;
-        swing_angle = -0.30F + t * 0.30F;
-        body_lean = 0.10F * (1.0F - t);
-        crouch_amount = 0.04F * (1.0F - t);
+        apply_lifting_pose(controller, swing_cycle, asymmetry, seed);
       }
       }
-
-      float const torso_y_offset = -crouch_amount;
-
-      float const hammer_y = HP::SHOULDER_Y + 0.10F + swing_angle * 0.20F;
-      float const hammer_forward =
-          0.18F + std::abs(swing_angle) * 0.15F + body_lean * 0.5F;
-      float const hammer_down =
-          swing_cycle > 0.4F && swing_cycle < 0.65F ? 0.08F : 0.0F;
-
-      QVector3D const hammer_hand(-0.06F + asymmetry,
-                                  hammer_y - hammer_down + torso_y_offset,
-                                  hammer_forward);
-
-      float const brace_y =
-          HP::WAIST_Y + 0.12F + torso_y_offset - crouch_amount * 0.5F;
-      float const brace_forward = 0.15F + body_lean * 0.3F;
-      QVector3D const brace_hand(0.14F - asymmetry * 0.5F, brace_y,
-                                 brace_forward);
-
-      controller.placeHandAt(true, hammer_hand);
-      controller.placeHandAt(false, brace_hand);
       return;
       return;
     }
     }
 
 
@@ -175,11 +143,171 @@ public:
                        const HumanoidAnimationContext &anim_ctx,
                        const HumanoidAnimationContext &anim_ctx,
                        ISubmitter &out) const override {
                        ISubmitter &out) const override {
 
 
-    draw_stone_hammer(ctx, v, pose, out);
+    draw_stone_hammer(ctx, v, pose, anim_ctx, out);
+  }
+
+  void draw_helmet(const DrawContext &ctx, const HumanoidVariant &v,
+                   const HumanoidPose &pose, ISubmitter &out) const override {
+
+    auto &registry = EquipmentRegistry::instance();
+    auto helmet = registry.get(EquipmentCategory::Helmet, "roman_light");
+    if (helmet) {
+      HumanoidAnimationContext anim_ctx{};
+      helmet->render(ctx, pose.body_frames, v.palette, anim_ctx, out);
+    }
+  }
+
+  void draw_armor(const DrawContext &ctx, const HumanoidVariant &v,
+                  const HumanoidPose &pose,
+                  const HumanoidAnimationContext &anim,
+                  ISubmitter &out) const override {
+    uint32_t const seed = reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU;
+    draw_work_tunic(ctx, v, pose, seed, out);
+  }
+
+private:
+  static constexpr uint32_t KNEEL_SEED_OFFSET = 0x1234U;
+
+  void apply_hammering_pose(HumanoidPoseController &controller,
+                            float swing_cycle, float asymmetry,
+                            uint32_t seed) const {
+    using HP = HumanProportions;
+
+    float swing_angle;
+    float body_lean;
+    float crouch_amount;
+
+    if (swing_cycle < 0.3F) {
+
+      float const t = swing_cycle / 0.3F;
+      swing_angle = t * 0.95F;
+      body_lean = -t * 0.10F;
+      crouch_amount = 0.0F;
+    } else if (swing_cycle < 0.5F) {
+
+      float const t = (swing_cycle - 0.3F) / 0.2F;
+      swing_angle = 0.95F - t * 1.5F;
+      body_lean = -0.10F + t * 0.28F;
+      crouch_amount = t * 0.08F;
+    } else if (swing_cycle < 0.6F) {
+
+      float const t = (swing_cycle - 0.5F) / 0.1F;
+      swing_angle = -0.55F + t * 0.18F;
+      body_lean = 0.18F - t * 0.06F;
+      crouch_amount = 0.08F - t * 0.02F;
+    } else {
+
+      float const t = (swing_cycle - 0.6F) / 0.4F;
+      swing_angle = -0.37F + t * 0.37F;
+      body_lean = 0.12F * (1.0F - t);
+      crouch_amount = 0.06F * (1.0F - t);
+    }
+
+    float const torso_y_offset = -crouch_amount;
+    float const hammer_y = HP::SHOULDER_Y + 0.10F + swing_angle * 0.22F;
+    float const hammer_forward =
+        0.18F + std::abs(swing_angle) * 0.16F + body_lean * 0.5F;
+    float const hammer_down =
+        swing_cycle > 0.4F && swing_cycle < 0.65F ? 0.10F : 0.0F;
+
+    QVector3D const hammer_hand(-0.06F + asymmetry,
+                                hammer_y - hammer_down + torso_y_offset,
+                                hammer_forward);
+
+    float const brace_y =
+        HP::WAIST_Y + 0.12F + torso_y_offset - crouch_amount * 0.5F;
+    float const brace_forward = 0.15F + body_lean * 0.3F;
+    QVector3D const brace_hand(0.14F - asymmetry * 0.5F, brace_y,
+                               brace_forward);
+
+    controller.placeHandAt(true, hammer_hand);
+    controller.placeHandAt(false, brace_hand);
+  }
+
+  void apply_kneeling_work_pose(HumanoidPoseController &controller, float cycle,
+                                float asymmetry, uint32_t seed) const {
+    using HP = HumanProportions;
+
+    float const kneel_depth =
+        0.45F + (hash_01(seed ^ KNEEL_SEED_OFFSET) * 0.15F);
+    controller.kneel(kneel_depth);
+
+    float const work_cycle = std::sin(cycle * std::numbers::pi_v<float> * 2.0F);
+
+    float const tool_y = HP::WAIST_Y * 0.3F + work_cycle * 0.08F;
+    float const tool_x_offset = 0.05F + work_cycle * 0.04F;
+    QVector3D const tool_hand(-tool_x_offset + asymmetry, tool_y, 0.25F);
+
+    float const brace_x = 0.18F - asymmetry * 0.5F;
+    QVector3D const brace_hand(brace_x, HP::WAIST_Y * 0.25F, 0.20F);
+
+    controller.placeHandAt(true, tool_hand);
+    controller.placeHandAt(false, brace_hand);
+  }
+
+  void apply_sawing_pose(HumanoidPoseController &controller, float cycle,
+                         float asymmetry, uint32_t seed) const {
+    using HP = HumanProportions;
+
+    controller.lean(QVector3D(0.0F, 0.0F, 1.0F), 0.12F);
+
+    float const saw_offset =
+        std::sin(cycle * std::numbers::pi_v<float> * 4.0F) * 0.12F;
+
+    float const saw_y = HP::WAIST_Y + 0.15F;
+    float const saw_z = 0.20F + saw_offset;
+
+    QVector3D const left_hand(-0.08F + asymmetry, saw_y, saw_z);
+    QVector3D const right_hand(0.08F - asymmetry, saw_y + 0.02F, saw_z);
+
+    controller.placeHandAt(true, left_hand);
+    controller.placeHandAt(false, right_hand);
+  }
+
+  void apply_lifting_pose(HumanoidPoseController &controller, float cycle,
+                          float asymmetry, uint32_t seed) const {
+    using HP = HumanProportions;
+
+    float lift_height;
+    float crouch;
+
+    if (cycle < 0.3F) {
+
+      float const t = cycle / 0.3F;
+      lift_height = HP::WAIST_Y * (1.0F - t * 0.5F);
+      crouch = t * 0.20F;
+    } else if (cycle < 0.6F) {
+
+      float const t = (cycle - 0.3F) / 0.3F;
+      lift_height =
+          HP::WAIST_Y * 0.5F + t * (HP::SHOULDER_Y - HP::WAIST_Y * 0.5F);
+      crouch = 0.20F * (1.0F - t);
+    } else if (cycle < 0.8F) {
+
+      lift_height = HP::SHOULDER_Y;
+      crouch = 0.0F;
+    } else {
+
+      float const t = (cycle - 0.8F) / 0.2F;
+      lift_height = HP::SHOULDER_Y * (1.0F - t * 0.3F);
+      crouch = 0.0F;
+    }
+
+    QVector3D const left_hand(-0.12F + asymmetry, lift_height, 0.15F);
+    QVector3D const right_hand(0.12F - asymmetry, lift_height, 0.15F);
+
+    controller.placeHandAt(true, left_hand);
+    controller.placeHandAt(false, right_hand);
+
+    if (crouch > 0.0F) {
+      controller.kneel(crouch);
+    }
   }
   }
 
 
   void draw_stone_hammer(const DrawContext &ctx, const HumanoidVariant &v,
   void draw_stone_hammer(const DrawContext &ctx, const HumanoidVariant &v,
-                         const HumanoidPose &pose, ISubmitter &out) const {
+                         const HumanoidPose &pose,
+                         const HumanoidAnimationContext &anim_ctx,
+                         ISubmitter &out) const {
     QVector3D const wood_color = v.palette.wood;
     QVector3D const wood_color = v.palette.wood;
 
 
     QVector3D const stone_color(0.55F, 0.52F, 0.48F);
     QVector3D const stone_color(0.55F, 0.52F, 0.48F);
@@ -190,10 +318,27 @@ public:
     QVector3D const forward(0.0F, 0.0F, 1.0F);
     QVector3D const forward(0.0F, 0.0F, 1.0F);
     QVector3D const right(1.0F, 0.0F, 0.0F);
     QVector3D const right(1.0F, 0.0F, 0.0F);
 
 
+    const AnimationInputs &anim = anim_ctx.inputs;
+    QVector3D handle_axis;
+    QVector3D head_axis;
+
+    if (anim.is_constructing) {
+
+      handle_axis = forward;
+      head_axis = up;
+    } else {
+
+      handle_axis = up;
+      head_axis = right;
+    }
+
     float const handle_len = 0.32F;
     float const handle_len = 0.32F;
     float const handle_r = 0.016F;
     float const handle_r = 0.016F;
-    QVector3D const handle_top = hand + up * 0.12F + forward * 0.02F;
-    QVector3D const handle_bot = handle_top - up * handle_len;
+    QVector3D const handle_offset = anim.is_constructing
+                                        ? (forward * 0.12F + up * 0.02F)
+                                        : (up * 0.12F + forward * 0.02F);
+    QVector3D const handle_top = hand + handle_offset;
+    QVector3D const handle_bot = handle_top - handle_axis * handle_len;
 
 
     out.mesh(get_unit_cylinder(),
     out.mesh(get_unit_cylinder(),
              cylinder_between(ctx.model, handle_bot, handle_top, handle_r),
              cylinder_between(ctx.model, handle_bot, handle_top, handle_r),
@@ -201,44 +346,25 @@ public:
 
 
     float const head_len = 0.10F;
     float const head_len = 0.10F;
     float const head_r = 0.030F;
     float const head_r = 0.030F;
-    QVector3D const head_center = handle_top + up * 0.035F;
+    QVector3D const head_center = handle_top + handle_axis * 0.035F;
 
 
-    out.mesh(get_unit_cylinder(),
-             cylinder_between(ctx.model,
-                              head_center - right * (head_len * 0.5F),
-                              head_center + right * (head_len * 0.5F), head_r),
-             stone_color, nullptr, 1.0F);
+    out.mesh(
+        get_unit_cylinder(),
+        cylinder_between(ctx.model, head_center - head_axis * (head_len * 0.5F),
+                         head_center + head_axis * (head_len * 0.5F), head_r),
+        stone_color, nullptr, 1.0F);
 
 
     out.mesh(get_unit_sphere(),
     out.mesh(get_unit_sphere(),
-             sphere_at(ctx.model, head_center + right * (head_len * 0.5F),
+             sphere_at(ctx.model, head_center + head_axis * (head_len * 0.5F),
                        head_r * 1.15F),
                        head_r * 1.15F),
              stone_dark, nullptr, 1.0F);
              stone_dark, nullptr, 1.0F);
 
 
     out.mesh(get_unit_sphere(),
     out.mesh(get_unit_sphere(),
-             sphere_at(ctx.model, head_center - right * (head_len * 0.5F),
+             sphere_at(ctx.model, head_center - head_axis * (head_len * 0.5F),
                        head_r * 0.9F),
                        head_r * 0.9F),
              stone_color * 0.95F, nullptr, 1.0F);
              stone_color * 0.95F, nullptr, 1.0F);
   }
   }
 
 
-  void draw_helmet(const DrawContext &ctx, const HumanoidVariant &v,
-                   const HumanoidPose &pose, ISubmitter &out) const override {
-
-    auto &registry = EquipmentRegistry::instance();
-    auto helmet = registry.get(EquipmentCategory::Helmet, "roman_light");
-    if (helmet) {
-      HumanoidAnimationContext anim_ctx{};
-      helmet->render(ctx, pose.body_frames, v.palette, anim_ctx, out);
-    }
-  }
-
-  void draw_armor(const DrawContext &ctx, const HumanoidVariant &v,
-                  const HumanoidPose &pose,
-                  const HumanoidAnimationContext &anim,
-                  ISubmitter &out) const override {
-    uint32_t const seed = reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU;
-    draw_work_tunic(ctx, v, pose, seed, out);
-  }
-
   void draw_work_tunic(const DrawContext &ctx, const HumanoidVariant &v,
   void draw_work_tunic(const DrawContext &ctx, const HumanoidVariant &v,
                        const HumanoidPose &pose, uint32_t seed,
                        const HumanoidPose &pose, uint32_t seed,
                        ISubmitter &out) const {
                        ISubmitter &out) const {
@@ -376,7 +502,6 @@ private:
     return default_style;
     return default_style;
   }
   }
 
 
-public:
   auto resolve_shader_key(const DrawContext &ctx) const -> QString {
   auto resolve_shader_key(const DrawContext &ctx) const -> QString {
     const BuilderStyleConfig &style = resolve_style(ctx);
     const BuilderStyleConfig &style = resolve_style(ctx);
     if (!style.shader_id.empty()) {
     if (!style.shader_id.empty()) {
@@ -385,7 +510,6 @@ public:
     return QStringLiteral("builder");
     return QStringLiteral("builder");
   }
   }
 
 
-private:
   void apply_palette_overrides(const BuilderStyleConfig &style,
   void apply_palette_overrides(const BuilderStyleConfig &style,
                                const QVector3D &team_tint,
                                const QVector3D &team_tint,
                                HumanoidVariant &variant) const {
                                HumanoidVariant &variant) const {

+ 1 - 1
render/entity/nations/roman/defense_tower_renderer.cpp

@@ -166,7 +166,7 @@ void draw_defense_tower(const DrawContext &p, ISubmitter &out) {
 
 
 void register_defense_tower_renderer(
 void register_defense_tower_renderer(
     Render::GL::EntityRendererRegistry &registry) {
     Render::GL::EntityRendererRegistry &registry) {
-  registry.register_renderer("defense_tower_roman", draw_defense_tower);
+  registry.register_renderer("troops/roman/defense_tower", draw_defense_tower);
 }
 }
 
 
 } // namespace Render::GL::Roman
 } // namespace Render::GL::Roman

+ 1 - 0
render/entity/registry.h

@@ -48,6 +48,7 @@ struct DrawContext {
   std::string renderer_id;
   std::string renderer_id;
   class Backend *backend = nullptr;
   class Backend *backend = nullptr;
   const Camera *camera = nullptr;
   const Camera *camera = nullptr;
+  float alpha_multiplier = 1.0F;
 };
 };
 
 
 using RenderFunc = std::function<void(const DrawContext &, ISubmitter &out)>;
 using RenderFunc = std::function<void(const DrawContext &, ISubmitter &out)>;

+ 15 - 0
render/gl/backend.cpp

@@ -1252,6 +1252,17 @@ void Backend::execute(const DrawQueue &queue, const Camera &cam) {
         glDepthMask(GL_TRUE);
         glDepthMask(GL_TRUE);
       }
       }
 
 
+      bool const isTransparent = (!isShadowShader) && (it.alpha < 0.999F);
+      std::unique_ptr<DepthMaskScope> transparent_depth_scope;
+      std::unique_ptr<BlendScope> transparent_blend_scope;
+      GLint prev_depth_func = GL_LESS;
+      if (isTransparent) {
+        glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func);
+        transparent_depth_scope = std::make_unique<DepthMaskScope>(false);
+        transparent_blend_scope = std::make_unique<BlendScope>(true);
+        glDepthFunc(GL_LEQUAL);
+      }
+
       if (active_shader == m_waterPipeline->m_riverShader) {
       if (active_shader == m_waterPipeline->m_riverShader) {
         if (m_lastBoundShader != active_shader) {
         if (m_lastBoundShader != active_shader) {
           active_shader->use();
           active_shader->use();
@@ -1455,6 +1466,10 @@ void Backend::execute(const DrawQueue &queue, const Camera &cam) {
       active_shader->set_uniform(uniforms->alpha, it.alpha);
       active_shader->set_uniform(uniforms->alpha, it.alpha);
       active_shader->set_uniform(uniforms->materialId, it.material_id);
       active_shader->set_uniform(uniforms->materialId, it.material_id);
       it.mesh->draw();
       it.mesh->draw();
+
+      if (isTransparent) {
+        glDepthFunc(static_cast<GLenum>(prev_depth_func));
+      }
       break;
       break;
     }
     }
     case GridCmdIndex: {
     case GridCmdIndex: {

+ 6 - 0
render/humanoid/rig.cpp

@@ -1300,6 +1300,12 @@ void HumanoidRendererBase::render(const DrawContext &ctx,
     UnitCategory category =
     UnitCategory category =
         is_mounted_spawn ? UnitCategory::Cavalry : UnitCategory::Infantry;
         is_mounted_spawn ? UnitCategory::Cavalry : UnitCategory::Infantry;
 
 
+    if (unit_comp != nullptr &&
+        unit_comp->spawn_type == Game::Units::SpawnType::Builder &&
+        anim.is_constructing) {
+      category = UnitCategory::BuilderConstruction;
+    }
+
     formation_calculator =
     formation_calculator =
         FormationCalculatorFactory::getCalculator(nation, category);
         FormationCalculatorFactory::getCalculator(nation, category);
   }
   }

+ 94 - 3
render/scene_renderer.cpp

@@ -121,13 +121,15 @@ void Renderer::mesh(Mesh *mesh, const QMatrix4x4 &model, const QVector3D &color,
     return;
     return;
   }
   }
 
 
+  float const effective_alpha = alpha * m_alpha_override;
+
   if (mesh == get_unit_cylinder() && (texture == nullptr) &&
   if (mesh == get_unit_cylinder() && (texture == nullptr) &&
       (m_current_shader == nullptr)) {
       (m_current_shader == nullptr)) {
     QVector3D start;
     QVector3D start;
     QVector3D end;
     QVector3D end;
     float radius = 0.0F;
     float radius = 0.0F;
     if (detail::decompose_unit_cylinder(model, start, end, radius)) {
     if (detail::decompose_unit_cylinder(model, start, end, radius)) {
-      cylinder(start, end, radius, color, alpha);
+      cylinder(start, end, radius, color, effective_alpha);
       return;
       return;
     }
     }
   }
   }
@@ -137,7 +139,7 @@ void Renderer::mesh(Mesh *mesh, const QMatrix4x4 &model, const QVector3D &color,
   cmd.model = model;
   cmd.model = model;
   cmd.mvp = m_view_proj * model;
   cmd.mvp = m_view_proj * model;
   cmd.color = color;
   cmd.color = color;
-  cmd.alpha = alpha;
+  cmd.alpha = effective_alpha;
   cmd.material_id = material_id;
   cmd.material_id = material_id;
   cmd.shader = m_current_shader;
   cmd.shader = m_current_shader;
   if (m_active_queue != nullptr) {
   if (m_active_queue != nullptr) {
@@ -147,12 +149,14 @@ void Renderer::mesh(Mesh *mesh, const QMatrix4x4 &model, const QVector3D &color,
 
 
 void Renderer::cylinder(const QVector3D &start, const QVector3D &end,
 void Renderer::cylinder(const QVector3D &start, const QVector3D &end,
                         float radius, const QVector3D &color, float alpha) {
                         float radius, const QVector3D &color, float alpha) {
+
+  float const effective_alpha = alpha * m_alpha_override;
   CylinderCmd cmd;
   CylinderCmd cmd;
   cmd.start = start;
   cmd.start = start;
   cmd.end = end;
   cmd.end = end;
   cmd.radius = radius;
   cmd.radius = radius;
   cmd.color = color;
   cmd.color = color;
-  cmd.alpha = alpha;
+  cmd.alpha = effective_alpha;
   if (m_active_queue != nullptr) {
   if (m_active_queue != nullptr) {
     m_active_queue->submit(cmd);
     m_active_queue->submit(cmd);
   }
   }
@@ -843,6 +847,93 @@ void Renderer::render_world(Engine::Core::World *world) {
       m_active_queue->submit(cmd);
       m_active_queue->submit(cmd);
     }
     }
   }
   }
+
+  render_construction_previews(world, vis, visibility_enabled);
+}
+
+void Renderer::render_construction_previews(
+    Engine::Core::World *world, const Game::Map::VisibilityService &vis,
+    bool visibility_enabled) {
+  if (world == nullptr || m_entity_registry == nullptr) {
+    return;
+  }
+
+  auto builders =
+      world->get_entities_with<Engine::Core::BuilderProductionComponent>();
+
+  for (auto *builder : builders) {
+    if (builder->has_component<Engine::Core::PendingRemovalComponent>()) {
+      continue;
+    }
+
+    auto *builder_prod =
+        builder->get_component<Engine::Core::BuilderProductionComponent>();
+    auto *transform =
+        builder->get_component<Engine::Core::TransformComponent>();
+    auto *unit_comp = builder->get_component<Engine::Core::UnitComponent>();
+
+    if (builder_prod == nullptr || transform == nullptr ||
+        !builder_prod->in_progress) {
+      continue;
+    }
+
+    const float preview_x = transform->position.x;
+    const float preview_z = transform->position.z;
+
+    if (unit_comp != nullptr && unit_comp->health <= 0) {
+      continue;
+    }
+
+    if (unit_comp != nullptr && unit_comp->owner_id != m_local_owner_id) {
+      if (visibility_enabled && !vis.isVisibleWorld(preview_x, preview_z)) {
+        continue;
+      }
+    }
+
+    if (m_camera != nullptr) {
+      QVector3D const pos(preview_x, transform->position.y, preview_z);
+      if (!m_camera->is_in_frustum(pos, 5.0F)) {
+        continue;
+      }
+    }
+
+    std::string nation_prefix = "roman";
+    if (unit_comp != nullptr) {
+      if (unit_comp->nation_id == Game::Systems::NationID::Carthage) {
+        nation_prefix = "carthage";
+      }
+    }
+
+    std::string renderer_key =
+        "troops/" + nation_prefix + "/" + builder_prod->product_type;
+
+    auto fn = m_entity_registry->get(renderer_key);
+    if (!fn) {
+      continue;
+    }
+
+    auto &terrain_service = Game::Map::TerrainService::instance();
+    const float terrain_height =
+        terrain_service.get_terrain_height(preview_x, preview_z);
+
+    QMatrix4x4 model_matrix;
+    model_matrix.translate(preview_x, terrain_height, preview_z);
+
+    DrawContext ctx{resources(), builder, world, model_matrix};
+    ctx.selected = false;
+    ctx.hovered = false;
+    ctx.animation_time = m_accumulated_time;
+    ctx.renderer_id = renderer_key;
+    ctx.backend = m_backend.get();
+    ctx.camera = m_camera;
+
+    float const prev_alpha = m_alpha_override;
+    m_alpha_override = 0.60F;
+
+    fn(ctx, *this);
+
+    m_alpha_override = prev_alpha;
+  }
 }
 }
 
 
 } // namespace Render::GL
 } // namespace Render::GL

+ 9 - 0
render/scene_renderer.h

@@ -22,6 +22,10 @@ class TransformComponent;
 class UnitComponent;
 class UnitComponent;
 } // namespace Engine::Core
 } // namespace Engine::Core
 
 
+namespace Game::Map {
+class VisibilityService;
+}
+
 namespace Render::GL {
 namespace Render::GL {
 class EntityRendererRegistry;
 class EntityRendererRegistry;
 }
 }
@@ -167,6 +171,10 @@ public:
                   const RainBatchParams &params);
                   const RainBatchParams &params);
 
 
 private:
 private:
+  void render_construction_previews(Engine::Core::World *world,
+                                    const Game::Map::VisibilityService &vis,
+                                    bool visibility_enabled);
+
   void enqueue_selection_ring(Engine::Core::Entity *entity,
   void enqueue_selection_ring(Engine::Core::Entity *entity,
                               Engine::Core::TransformComponent *transform,
                               Engine::Core::TransformComponent *transform,
                               Engine::Core::UnitComponent *unit_comp,
                               Engine::Core::UnitComponent *unit_comp,
@@ -191,6 +199,7 @@ private:
   GridParams m_grid_params;
   GridParams m_grid_params;
   float m_accumulated_time = 0.0F;
   float m_accumulated_time = 0.0F;
   std::atomic<bool> m_paused{false};
   std::atomic<bool> m_paused{false};
+  float m_alpha_override = 1.0F;
 
 
   std::mutex m_world_mutex;
   std::mutex m_world_mutex;
   int m_local_owner_id = 1;
   int m_local_owner_id = 1;