Repository: Dungeon-CampusMinden/Dungeon
Pull request #1820: [Blockly] add Item blocks
Author: login: AMatutat
Labels: name: blockly
Created: 2025-03-24T10:36:55Z | Merged: 2025-03-24T11:44:42Z

fixes #1777  

- Fügt die Blöcke `aufsammeln` und `fallen lassen %1` mit dem möglichen Parameter `Brotkrumen` hinzu.  
- `aufsammeln` löst `onInteraction` für alle Entitäten mit `BlocklyItemComponent` aus, die sich auf dem gleichen Tile wie der Held befinden.  
- `fallen lassen` erzeugt eine neue Item-Entität auf dem Feld des Helden. Da Blockly kein Inventar benötigt, gibt es keinen Check, ob genug Items vorhanden sind (und das ist aktuell auch nicht vorgesehen).  
- `MiscFactory#breadcrumb` erstellt ein Item, das später in Rätseln als Wegmarkierung genutzt wird (z. B. Hänsel und Gretel).  
- Fügt `BlocklyItemComponent` als Marker-Komponente hinzu.  

---  

**Konzeptioneller Hinweis:**  

Im `Dungeon.contrib` haben wir ein recht ausgeklügeltes Item-System mit Enums, Buildern, Crafting etc.  
Warum verwende ich das nicht?  

Dungeon-Items sind für ein Inventar-System konzipiert. Sie haben getrennte Texturen für "In-World" und "Im-Inventar", und das Aufheben prüft, ob genügend Platz vorhanden ist.  

Für Blockly wäre das Overkill. Im aktuellen Konzept gibt es keine Items, die so funktionieren.

Linked issue #1777: Blockly: Item Blöcke
In Blockly wollen wir Items einführen (kein Inventar).

Die neue Aktions-Blöcke sind: 

- `AUFHEBEN` → Sammelt ein Item auf, wenn der Spieler auf einem Item steht.  
- `WEGWERFEN $ITEM` → Erstellt eine Entität (entsprechend des Items) auf dem Boden des Spielers.  
  - Ein richtiges Inventar ist aktuell nicht geplant.  
  - Begrenzungen setzen wir über die Limitierung der Blöcke.  
  - Items, die der Spieler fallen lässt, können nicht wieder aufgesammelt werden (oder gibt es aktuell einen Grund, das zu ändern?).  
- `ITEM`-Blöcke:  
  - `BROTKRUMEN`  (bitte auch direkt das Item im Dungeon umsetzen) 

Diff:
diff --git a/blockly/frontend/src/blocks/dungeon.ts b/blockly/frontend/src/blocks/dungeon.ts
index efe0bdf6c5..d13139087e 100644
--- a/blockly/frontend/src/blocks/dungeon.ts
+++ b/blockly/frontend/src/blocks/dungeon.ts
@@ -506,6 +506,29 @@ export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
       },
     ]
   },
+  {
+    type: "pickup",
+    message0: "aufheben",
+    previousStatement: null,
+    nextStatement: null,
+    colour: 0,
+    tooltip: "Sammel den Gegenstand unter dir auf.",
+  },
+  {
+    type: "drop_item",
+    message0: "fallen lassen %1",
+    previousStatement: null,
+    nextStatement: null,
+    colour: 0,
+    tooltip: "Gegenstand auf den Boden werfen.",
+    args0: [
+      {
+        type: "input_value",
+        name: "ITEM",
+        check: "Item",
+      },
+    ]
+  },
   {
     type: "wait",
     message0: "warte",
@@ -537,7 +560,14 @@ export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
     nextStatement: null,
     colour: 0,
     tooltip: "Nur einmal feste ziehen!",
-   },
+  },
+  {
+    type: "item_breadcrumbs",
+    message0: "Brotkrumen",
+    output: "Item",
+    colour: 230
+  },
+
   //  ---------------------- Functions ----------------------
   {
     type: "func_def",
diff --git a/blockly/frontend/src/generators/java.ts b/blockly/frontend/src/generators/java.ts
index dd6b5af9ec..690136ae7b 100644
--- a/blockly/frontend/src/generators/java.ts
+++ b/blockly/frontend/src/generators/java.ts
@@ -8,6 +8,7 @@ import * as skills from "./java/skills.ts";
 import * as arrays from "./java/arrays.ts";
 import * as functions from "./java/functions.ts";
 import * as directions from "./java/directions.ts";
+import * as items from "./java/items.ts";
 
 class JavaGenerator extends Blockly.Generator {
   public variables: Map<string, number|string>;
@@ -50,7 +51,8 @@ Object.assign(
   skills,
   arrays,
   functions,
-  directions
+  directions,
+  items
 );
 
 export const Order = {
diff --git a/blockly/frontend/src/generators/java/items.ts b/blockly/frontend/src/generators/java/items.ts
new file mode 100644
index 0000000000..334a057112
--- /dev/null
+++ b/blockly/frontend/src/generators/java/items.ts
@@ -0,0 +1,10 @@
+import * as Blockly from "blockly";
+import {Order} from "../java.ts";
+
+export function item_breadcrumbs(
+  _block: Blockly.Block,
+  _generator: Blockly.Generator
+) {
+  return ["\"Brotkrumen\"", Order.ATOMIC];
+}
+
diff --git a/blockly/frontend/src/generators/java/skills.ts b/blockly/frontend/src/generators/java/skills.ts
index 344ef57c8c..26ed8a03f2 100644
--- a/blockly/frontend/src/generators/java/skills.ts
+++ b/blockly/frontend/src/generators/java/skills.ts
@@ -29,3 +29,15 @@ export function pull(_block: Blockly.Block, _generator: Blockly.Generator) {
   return "ziehen();";
 }
 
+export function pickup(_block: Blockly.Block, _generator: Blockly.Generator) {
+  return "aufsammeln();";
+}
+
+export function drop_item(
+  block: Blockly.Block,
+  generator: Blockly.Generator
+) {
+  const item = generator.valueToCode(block, "ITEM", Order.NONE);
+  return "fallen_lassen(" + item + ");";
+}
+
diff --git a/blockly/frontend/src/toolbox.ts b/blockly/frontend/src/toolbox.ts
index b0e09c13d3..ebcfbf2779 100644
--- a/blockly/frontend/src/toolbox.ts
+++ b/blockly/frontend/src/toolbox.ts
@@ -73,6 +73,10 @@ export const toolbox: Blockly.utils.toolbox.ToolboxDefinition = {
           kind: "block",
           type: "direction_right",
         },
+        {
+          kind: "block",
+          type: "item_breadcrumbs",
+        },
       ],
     },
     {
@@ -209,6 +213,10 @@ export const toolbox: Blockly.utils.toolbox.ToolboxDefinition = {
           kind: "block",
           type: "use",
         },
+        {
+          kind: "block",
+          type: "pickup",
+        },
         {
           kind: "block",
           type: "push",
@@ -217,6 +225,10 @@ export const toolbox: Blockly.utils.toolbox.ToolboxDefinition = {
           kind: "block",
           type: "pull",
         },
+        {
+          kind: "block",
+          type: "drop_item",
+        },
       ],
     },
     {
diff --git a/blockly/src/components/BlocklyItemComponent.java b/blockly/src/components/BlocklyItemComponent.java
new file mode 100644
index 0000000000..9c62b86afe
--- /dev/null
+++ b/blockly/src/components/BlocklyItemComponent.java
@@ -0,0 +1,14 @@
+package components;
+
+import core.Component;
+
+/**
+ * Marks an entity as an Item in the Blockly world.
+ *
+ * <p>Items in Blockly differ from {@link contrib.item.Item Dungeon items} in that they are not
+ * designed for an inventory system. Blockly items exist only in the game world, not in the
+ * inventory.
+ *
+ * <p>This component is used to mark entities as collectible for the "pick up" block.
+ */
+public final class BlocklyItemComponent implements Component {}
diff --git a/blockly/src/entities/MiscFactory.java b/blockly/src/entities/MiscFactory.java
index 2ffb15997d..8f401511cb 100644
--- a/blockly/src/entities/MiscFactory.java
+++ b/blockly/src/entities/MiscFactory.java
@@ -1,6 +1,7 @@
 package entities;
 
 import components.BlockComponent;
+import components.BlocklyItemComponent;
 import components.PushableComponent;
 import contrib.components.CollideComponent;
 import contrib.components.InteractionComponent;
@@ -30,6 +31,7 @@ public class MiscFactory {
       new SimpleIPath("objects/pressureplate/off/pressureplate_0.png");
 
   private static final IPath PICKUP_BOCK_PATH = new SimpleIPath("items/book/spell_book.png");
+  private static final IPath BREADCRUMB_PATH = new SimpleIPath("items/resource/leaf.png");
   private static final float STONE_SPEED = 7.5f;
 
   /**
@@ -107,4 +109,28 @@ public static Entity bookPickup(Point position, String title, String pickupText)
             }));
     return pickup;
   }
+
+  /**
+   * Creates a breadcrumb entity at the given position.
+   *
+   * <p>The breadcrumb is a temporary item that can be picked up and removed upon interaction. It
+   * can be used for marking paths.
+   *
+   * @param position The initial position of the breadcrumb.
+   * @return A new breadcrumb entity.
+   */
+  public static Entity breadcrumb(Point position) {
+    Entity breadcrumb = new Entity("breadcrumb");
+    breadcrumb.add(new PositionComponent(position.toCoordinate().toCenteredPoint()));
+    breadcrumb.add(new DrawComponent(Animation.fromSingleImage(BREADCRUMB_PATH)));
+    breadcrumb.add(new BlocklyItemComponent());
+    breadcrumb.add(
+        new InteractionComponent(
+            0,
+            false,
+            (entity, entity2) -> {
+              Game.remove(breadcrumb);
+            }));
+    return breadcrumb;
+  }
 }
diff --git a/blockly/src/server/Server.java b/blockly/src/server/Server.java
index 16495c05f2..ba7a013837 100644
--- a/blockly/src/server/Server.java
+++ b/blockly/src/server/Server.java
@@ -9,9 +9,11 @@
 import com.sun.net.httpserver.HttpExchange;
 import com.sun.net.httpserver.HttpServer;
 import components.BlockComponent;
+import components.BlocklyItemComponent;
 import components.PushableComponent;
 import contrib.components.CollideComponent;
 import contrib.components.InteractionComponent;
+import contrib.components.ItemComponent;
 import contrib.utils.EntityUtils;
 import contrib.utils.components.Debugger;
 import contrib.utils.components.skill.FireballSkill;
@@ -26,6 +28,7 @@
 import core.utils.MissingHeroException;
 import core.utils.Point;
 import core.utils.components.MissingComponentException;
+import entities.MiscFactory;
 import entities.VariableHUD;
 import java.io.IOException;
 import java.io.InputStream;
@@ -111,7 +114,16 @@ public class Server {
 
   private boolean clearHUD = false;
   private final String[] reservedFunctions = {
-    "gehe", "feuerball", "naheWand", "warte", "benutzen", "schieben", "ziehen", "geheZumAusgang"
+    "gehe",
+    "feuerball",
+    "naheWand",
+    "warte",
+    "benutzen",
+    "schieben",
+    "ziehen",
+    "geheZumAusgang",
+    "aufsammeln",
+    "fallen_lassen",
   };
   private final Stack<String> currently_repeating_scope = new Stack<>();
 
@@ -1123,6 +1135,19 @@ private void performAction(String action) {
       case "ziehen" -> {
         pull();
       }
+      case "aufsammeln" -> {
+        pickup();
+      }
+      case "fallen_lassen" -> {
+        String firstArg;
+        if (args[0] instanceof String) {
+          firstArg = (String) args[0];
+        } else {
+          setError("Unexpected type for item " + args[0]);
+          return;
+        }
+        dropItem(firstArg);
+      }
       case "geheZumAusgang" -> {
         moveToExit();
       }
@@ -1341,6 +1366,30 @@ public void interact() {
                             interactionComponent.triggerInteraction(entity, hero)));
   }
 
+  /**
+   * Triggers the interaction (normally a pickup action) for each Entity with an {@link
+   * ItemComponent} at the same tile as the hero.
+   */
+  public void pickup() {
+    Game.entityAtTile(Game.tileAT(EntityUtils.getHeroCoordinate()))
+        .filter(e -> e.isPresent(BlocklyItemComponent.class))
+        .forEach(
+            item ->
+                item.fetch(InteractionComponent.class)
+                    .ifPresent(ic -> ic.triggerInteraction(item, hero)));
+  }
+
+  /**
+   * Drop an Blockly-Item at the heros position.
+   *
+   * @param item Name of the item to drop
+   */
+  public void dropItem(String item) {
+    if (item.equals("Brotkrumen")) {
+      Game.add(MiscFactory.breadcrumb(getHeroPosition().position()));
+    }
+  }
+
   /** Moves the Hero to the Exit Block of the current Level. */
   public void moveToExit() {
     PositionComponent pc =


Reviews:
user: login: Flamtky
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-24T11:09:51Z
user: login: AMatutat
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T11:28:48Z

Review comments:
diff_hunk: @@ -1341,6 +1366,30 @@ public void interact() {
                             interactionComponent.triggerInteraction(entity, hero)));
   }
 
+  /**
+   * Triggers the interaction (normally a pickup action) for each Entity with an {@link
+   * ItemComponent} at the same tile as the hero.
+   */
+  public void pickup() {
+    Game.entityAtTile(Game.tileAT(EntityUtils.getHeroCoordinate()))
+        .filter(e -> e.isPresent(BlocklyItemComponent.class))
+        .forEach(
+            item ->
+                item.fetch(InteractionComponent.class)
+                    .ifPresent(ic -> ic.triggerInteraction(item, hero)));
+  }
+
+  /**
+   * Drop an Blockly-Item at the heros position.
+   *
+   * @param item Name of the item to drop
+   */
+  public void dropItem(String item) {
+    if (item.equals("Brotkrumen")) {
+      Game.add(MiscFactory.breadcrumb(getHeroPosition().position()));
+    }
+  }
path: blockly/src/server/Server.java
user: login: Flamtky
body: 2 Fragen:
1. Hat man unendlich "Brotkrumen"?
2. Viele Items sind geplant? Weil wenn wir hier am Ende 20 If else haben wir das blöd. Alleine er Fakt das man für jedes neues Item hier ein If schreiben muss...
created_at: 2025-03-24T11:09:25Z
author_association: MEMBER
diff_hunk: @@ -1341,6 +1366,30 @@ public void interact() {
                             interactionComponent.triggerInteraction(entity, hero)));
   }
 
+  /**
+   * Triggers the interaction (normally a pickup action) for each Entity with an {@link
+   * ItemComponent} at the same tile as the hero.
+   */
+  public void pickup() {
+    Game.entityAtTile(Game.tileAT(EntityUtils.getHeroCoordinate()))
+        .filter(e -> e.isPresent(BlocklyItemComponent.class))
+        .forEach(
+            item ->
+                item.fetch(InteractionComponent.class)
+                    .ifPresent(ic -> ic.triggerInteraction(item, hero)));
+  }
+
+  /**
+   * Drop an Blockly-Item at the heros position.
+   *
+   * @param item Name of the item to drop
+   */
+  public void dropItem(String item) {
+    if (item.equals("Brotkrumen")) {
+      Game.add(MiscFactory.breadcrumb(getHeroPosition().position()));
+    }
+  }
path: blockly/src/server/Server.java
user: login: AMatutat
body: 1. Ja, war so angedacht. Evlt. kann man für ein spezifisches Level die Brotrkumen Blöcke via Blockly reduzieren
2. Nicht viele. Aktuell nur Brotkrumen, evtl. fällt @b-franklin oder mir noch 1-2 Items ein, aber nicht viel. Daher auch diese Low-Effort Sollution.
created_at: 2025-03-24T11:28:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: AMatutat
created_at: 2025-03-24T10:39:04Z
body: @Tobi511 hier kannst du mal gucken wegen den Items
author_association: CONTRIBUTOR
user: login: AMatutat
created_at: 2025-03-24T10:56:10Z
body: Yikes bug im Frontend (Cache hat mich getrollt)
author_association: CONTRIBUTOR